Skip to content

Authentication

Every authenticated request carries a Supabase JWT as a bearer token:

Authorization: Bearer <supabase-jwt>

The token is validated as HS256 with audience authenticated, signed by the shared Supabase auth secret. Failures return 401 Unauthorized with a WWW-Authenticate: Bearer header.

After the user is resolved, the request resolves an acting org. To act inside a specific subaccount, send its UUID:

X-Org-Id: <org-uuid>
  • Member → the request acts inside that org.
  • Not a member403 Forbidden (existence is never leaked).
  • Malformed UUID400.
  • Omitted → falls back to your earliest-joined org.

The acting org is also bound to the database session for Row-Level Security, so the database itself enforces tenant isolation. See Multi-Tenant & RLS.

An agency admin can mint a time-boxed token to act inside one of their own subaccounts.

POST /api/v1/impersonate
Content-Type: application/json
{ "org_id": "<target-subaccount-uuid>" }

Rules:

  • The caller must be an admin/owner of their current org.
  • The target must be the caller’s own org or one of its direct subaccounts (the parent_org_id chain) — never a sibling, the parent, or another agency’s client. A violation returns 403.
  • An unknown target returns 404.
  • The audit row records the real admin’s identity, not the impersonated user — this closes GoHighLevel’s “login as” attribution gap.

The minted token is then passed on subsequent requests via the X-Impersonate-Token header.

Trusted internal callers (the telephony engine, inbound webhooks, the Instantly bridge) do not carry a user JWT. They authenticate with a shared secret header:

X-Iron-CRM-Secret: <shared-secret>

and pass the org explicitly in the request body. These seams are documented under Webhooks. Do not use the shared secret for user-facing calls — use a JWT.