Authentication
Authenticate server-to-server calls with API keys and browser calls with an Atlas session plus the x-org-id header.
Referalli accepts two kinds of credentials. Servers use an API key in the Authorization header. Browsers use a session cookie obtained by signing in with Atlas. A small set of public endpoints (tracked links, the tracking script, inbound webhooks) need neither.
API keys
API keys start with rk_ and belong to one organization. Send them as a bearer token:
curl https://referalli.com/v1/programs \
-H "Authorization: Bearer rk_3f9c..."The key decides the org. You do not send x-org-id with an API key; if you do, it is ignored.
Creating and revoking keys
Create keys in the dashboard under Settings → API keys, or through the API with an admin key (scope admin) or an owner or admin session:
curl -X POST https://referalli.com/v1/api-keys \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "content-type: application/json" \
-d '{"name": "production server"}'
# { "id": "key_...", "key": "rk_live_...", "scopes": ["full"], "livemode": true, ... }The key value is returned once and only its SHA-256 hash is stored. If you lose it, create a new key.
To see which key you're calling with, GET /v1/me with the key returns apiKey: its id, name, prefix, scopes, apiVersion (the version it's pinned to), livemode, owner and expiresAt. Keys created before versioning are pinned to 2026-10-21.
Call | What it does | Who can call it |
|---|---|---|
| Issue a key. Body: | admin key; owner, admin |
| List the org's live and test keys: | any org member, any key |
| One key, in the same shape as the list. | any org member, any key |
| Change | admin key; owner, admin |
| Revoke a key. | admin key; owner, admin |
Developer credentials
Give each developer their own keys instead of sharing one. Whoever hands out credentials holds an admin key, and mints each developer a matching live and test pair:
curl -X POST https://referalli.com/v1/api-keys \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "content-type: application/json" \
-d '{"name": "alice laptop", "owner": "alice@acme.io", "mode": "both", "expiresInDays": 90}'
# { "pairId": "kpair_...",
# "live": { "key": "rk_live_...", "livemode": true, ... },
# "test": { "key": "rk_test_...", "livemode": false, ... } }Or from a terminal with the CLI: referally keys create --name "alice laptop" --owner alice@acme.io --mode both --expires-days 90, then referally keys list and referally keys revoke <id> --pair.
The live key (
rk_live_…) reaches production data. The test key (rk_test_…) reaches only the org's test mode data.New keys get
fullaccess unless you passscopes. Afullkey can use the whole API but can't create or revoke keys; only anadminkey can.A key never grants more than it holds: an admin key can mint any key, and nothing else can mint at all.
An expired key fails with
401like a revoked one.lastUsedAt(updated at most once a minute) shows keys nobody uses any more.A test-mode admin key (
rk_test_, scopeadmin) can only mint, list and revoke test keys. A CI pipeline can hold one without being able to touch production.When a developer leaves, revoke their pair. If they held an admin key, revoke it with
?cascade=trueto revoke every key they minted too.
Scopes
A key's scopes are chosen when you create it. In the dashboard this is the Access for the next key picker in Settings → API keys. A key's scopes can't be changed; revoke it and create another.
| What the key can do |
|---|---|
| Everything a |
| Everything except managing API keys. A |
| Only |
Resource scopes, e.g. | Only those parts of the API. |
A read-only or read-scoped key still reaches read endpoints that are limited to owner, admin or finance for people (such as GET /v1/payout-connections, GET /v1/fx-rates and GET /v1/tax/1099).
Create a key that can only attribute customers and read referrals:
curl -X POST https://referalli.com/v1/api-keys \
-H "Authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{"name": "signup service", "scopes": ["referrals:write"]}'Resources
Every /v1 endpoint belongs to exactly one resource:
Resource | Endpoints |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
| payout runs, payouts, payout connections, tax, FX rates, payout schedule, partner payout profiles and tax forms, accounting export |
|
|
|
|
| analytics, reports, GraphQL, anomalies, setup status, assistant, incrementality, full export |
|
|
|
|
| team members, API keys, |
|
|
| auth connections, imports |
| discovery, prospects, outreach, creators, marketplace, network, agents, negotiations, clean room, deals |
|
|
Platform admin endpoints and the partner portal (/v1/partner/…) take a person's session and never accept a key.
Publishable keys
A publishable key (rk_pub_…) is safe to put in browser code. It can hold only programs:read and events:write (custom events through POST /v1/track/events; clicks are refused), and it only answers requests from the origins you list:
curl -X POST https://referalli.com/v1/api-keys \
-H "Authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{"name": "storefront", "publishable": true, "allowedOrigins": ["https://shop.example.com"]}'A request must carry an
Originthe key allows; otherwise it fails with403andthis publishable key is not allowed from this origin.Allowed requests get
Access-Control-Allow-Originfor that origin, and the browser's preflight (OPTIONS) is answered for it.allowedOriginsare origins only (https://app.example.com, with an optional port), up to 20. A server key can't have them.
Quotas and retries
Every key has its own request quota, reported in RateLimit-* headers, and every write accepts an Idempotency-Key header so retries are safe. See Errors and limits.
Browser sessions
People sign in through the deployment's OpenID Connect provider (on the hosted service, Atlas; a self-hosted deployment can use any OIDC provider). There are no password endpoints.
Send the browser to
GET /v1/auth/oidc/start(/v1/auth/atlas/startworks too). Optional query parameters:as(merchant, the default, orpartner),provider(instance, the default, oratlasidfor "Sign in with Atlas" where it's offered),next(a path to return to after sign-in) andlogin_hint(an email to prefill).The provider redirects back to the callback, which sets the
referally_sessioncookie (HTTP-only,SameSite=Lax, valid for 30 days).POST /v1/auth/logoutends the session and returns{ "ok": true, "logoutUrl": "…" }. WhenlogoutUrlisn't null, send the browser there to end the provider's session too; providers without RP-initiated logout returnnull, and signing out is local.
GET /v1/auth/providers lists the configured sign-in providers, so you know which buttons to draw: { "providers": ["instance", "atlasid"], "details": [{ "id": "instance", "name": "Atlas", "atlas": true }, …] }.
Choosing an org with x-org-id
A user can belong to several orgs, so session-authenticated requests must name one in the x-org-id header:
curl https://referalli.com/v1/programs \
-H "Cookie: referally_session=..." \
-H "x-org-id: org_01J..."The user must be a member of that org. Two endpoints work without the header: GET /v1/me (returns the user and their memberships, each with orgId and role) and POST /v1/orgs (creates an org with the caller as owner).
Roles
Org members have one of these roles: owner, admin, finance, analyst, support. Most read endpoints are open to every member (GET /v1/payout-connections and GET /v1/tax/1099 need owner, admin or finance). Writes check the role, for example:
Programs, partners, enrollments, links, commission rules, webhooks, billing connections, fraud case decisions:
owneroradmin.Approving commissions, adjusting commission lines, payout profiles and thresholds, payout runs, payout connections, FX rates and the payout schedule:
owner,adminorfinance.
Managing the team
Call | What it does | Who can call it |
|---|---|---|
| List members with | any org member |
| Body | owner, admin |
| Body | owner, admin |
| Remove a member. Any member can remove themselves (leave), whatever their role. Removing an owner needs an owner, and the last owner can't be removed or leave ( | owner, admin; any member for their own |
Only an owner can add an owner, change an owner's role or remove an owner; an admin trying returns 403. An org always keeps at least one owner: demoting or removing the last one returns 409 an organization needs at least one owner. Every change is written to the audit log.
Cross-site requests
State-changing requests that carry the session cookie must come from an allowed Origin. A cross-site browser request is rejected with 403 and the message cross-origin request blocked. Requests with an Authorization: Bearer header are not affected.
Partner sessions
Partners sign in with as=partner. A partner session is not tied to one org and only works on the partner portal endpoints under /v1/partner/…. Calling a merchant endpoint with it fails with 400 and an org context is required (x-org-id header).
Partners can turn on two-factor sign-in (TOTP) with POST /v1/partner/security/totp/setup and POST /v1/partner/security/totp/enable ({ "code": "123456" }). While it is on, changing a payout profile (PUT /v1/partner/orgs/{orgId}/payout-profile) needs a current code in the x-totp-code header; without a valid one the call fails with 401 step_up_required. POST /v1/partner/security/totp/disable with { "code": "123456" } turns two-factor off; it also needs a valid current code (401 invalid code otherwise). GET /v1/partner/security returns { "totpEnabled": true }.
Enterprise SSO
A merchant org can let its team sign in through the company's identity provider (SAML or OIDC, with SCIM directory sync). An SSO provider (an identity broker) runs the connection; today that is Atlas. Referalli links each org to a directory tenant at the provider (an Atlas organization) and follows it. People are added to the org, with a role mapped from their directory role, the first time they sign in with SSO, and the provider's signed webhooks keep membership in step with the directory. The setup steps for merchants are in the guide Single sign-on for your team.
Call | What it does | Who can call it |
|---|---|---|
| Status: | owner, admin |
| Create the org's directory tenant at the provider, seating the caller as its first admin. The caller must have an identity at the provider (for Atlas: an account on Referalli's Atlas instance); otherwise | owner |
| Body | owner, admin |
| Revoke a pending setup link. | owner, admin |
|
| owner, admin |
| Body | owner |
Role keys are the provider's. Each provider has a default mapping (defaultRoleMapping); for Atlas it is org:admin → admin and org:member → analyst. The org's own roleMapping is applied on top. Nobody is made owner unless the mapping says so, and an existing owner's role is never changed by the directory. SSO sign-in errors:
Status |
| Cause |
|---|---|---|
403 |
| The org requires SSO and this session didn't come from it. |
403 |
| The directory removed or suspended this member, and they were the last owner (so the row is kept), or the provider suspended the user. |
503 |
| The deployment has no SSO provider configured (for Atlas: no |
502 |
| The provider refused or failed the call; |
GET /v1/auth/atlas/start also takes login_hint (an email), passed to Atlas so it can route an enterprise domain straight to its connection. A session counts as SSO when the ID token's amr names a federated method (saml, oidc, sso, fed) or carries sso_connection_id.
Provider webhooks
POST /webhooks/sso/{provider} receives the provider's lifecycle webhooks; for Atlas, POST /webhooks/atlas stays as its registered URL. Each delivery is verified by the provider's own scheme, processed once by its delivery id, and turned into one of these changes:
Change | Effect |
|---|---|
Member added or role changed | Add the member, or change their role, using the org's mapping. |
Member removed | Remove the member. The last active owner is suspended instead and the org is notified. |
User deleted | End every session and remove the user from every org linked to this provider (same last-owner rule). |
User suspended or banned | End every session and suspend access in those orgs. |
User restored | Restore that access. |
Connections changed | Refresh the org's SSO domains. With none left, |
For Atlas, the scheme is atlas-id, atlas-timestamp (epoch milliseconds) and atlas-signature (v1, + base64 HMAC-SHA256 of {atlas-id}.{atlas-timestamp}.{raw body}, keyed with ATLAS_WEBHOOK_SECRET, with ATLAS_WEBHOOK_SECRET_PREVIOUS also accepted during a rotation), within 5 minutes. Its events map as: organizationMembership.created / .updated → member added or changed; organizationMembership.deleted → member removed; user.deleted; user.banned / user.suspended; user.unbanned / user.unsuspended → restored; ssoConnection.created / .updated / .deleted → connections changed. The operator registers the endpoint once: pnpm --filter @referally/api exec tsx scripts/register-atlas-webhook.ts (with ATLAS_SECRET_KEY and APP_BASE_URL set) prints the signing secret to store as ATLAS_WEBHOOK_SECRET.
Choosing or adding a provider
SSO_PROVIDER picks the deployment's provider (default atlas). An unknown value, or a provider without its credentials, leaves SSO "not configured" rather than failing. Each org records which provider its link uses, so switching providers never applies one provider's directory to another's orgs.
A new provider (for example WorkOS SSO or Auth0 Organizations) plugs in by implementing the SsoProvider interface in packages/api/src/ssoProvider.ts: create a tenant, create and revoke setup links, list connections and memberships, return SAML metadata, mirror "require SSO", map a sign-in identity to its subject, and verify and parse its webhooks into the changes above. It then registers in ssoProviderOf (packages/api/src/ssoProviders/index.ts). Provisioning at sign-in, role mapping, require-SSO and the last-owner rule are provider-neutral and need no changes.
Public endpoints
These need no credentials:
GET /go/{trackingToken}/{code}, the tracked link redirect.GET /t/{trackingToken}/aff.js,POST /t/{trackingToken}/visitandPOST /t/{trackingToken}/identify, used by the tracking script. The tracking token is public by design; it identifies your org but grants no API access.POST /webhooks/billing/{connectionId},POST /webhooks/auth/{connectionId},POST /webhooks/sso/{provider}andPOST /webhooks/atlas, which verify the provider's signature instead.GET /v1/healthandGET /v1/openapi.json.
Authentication errors
All errors use the standard envelope described in Errors and limits.
Status |
|
| Cause |
|---|---|---|---|
401 |
|
| No API key and no session cookie. |
401 |
|
| The |
401 |
|
| The session cookie is no longer valid. Sign in again. |
401 |
|
| A partner with two-factor on changed payout details without a valid code. |
400 |
|
| Session request without |
403 |
|
| The user does not belong to the org in |
403 |
|
| The org in |
403 |
|
| The user's role is not allowed to make this call. |
403 |
|
| A |
403 |
|
| A resource-scoped key called an endpoint outside its scopes. |
403 |
|
| A publishable key was used without an allowed |
403 |
|
| Cookie-authenticated write from a disallowed origin. |