Authentication

Every programmatic request authenticates with a Bearer API key:

GET /v1/auth/me HTTP/1.1
Host: api.replylayer.ai
Authorization: Bearer rly_live_k3m9p2qx7vn4hjd0.uZ8Qb1vK3mN0pR7sT2wX9yA4cF6gH8jL1nP3rT5vW7z

The base URL is https://api.replylayer.ai/v1. All endpoints except the auth and health routes require a key. Session cookies exist for the dashboard only — as an agent you always use a Bearer key.

Key format

A key is a single opaque string in two dot-separated parts:

rly_live_<public_id>.<secret>
  • <public_id> — the 16-character head after rly_live_ (lowercased base32). It is not secret; it identifies the key for leak-attribution and appears (masked) in the key-list read. Do not authenticate with it alone.
  • <secret> — the credential proper (the part after the .). The whole token, both parts together, is what authenticates.

Treat the entire rly_live_... string as the secret. The plaintext is returned exactly once, on the create response — there is no endpoint that re-reveals it. If you lose it, mint a replacement. A key using a retired prefix (anything other than rly_live_) is rejected at the door with 401 API_KEY_FORMAT_LEGACY — rotate to a current key.

Two roles: admin and agent

RoleScopeCan it manage keys / mailboxes / account?
adminFull account access — same reach as a dashboard session.Yes.
agentSend, receive, read, reply, and quarantine-manage on its bound mailboxes only.No — cannot create/delete mailboxes, mint or list keys, edit scanner policy, or touch account settings.

An agent key is bound to one or more mailboxes at creation (mailbox_ids, non-empty and required for role: "agent"; forbidden for role: "admin"). It can never see or act on a mailbox outside that set.

Fail-to-zero scoping

Agent scope fails closed. If every mailbox an agent key was bound to is later deleted, the key resolves to zero mailbox access — never to unrestricted access. A scoped key that has lost all its bindings can authenticate but cannot send or read anything; it does not silently widen to "all mailboxes". Deleted mailboxes also drop out of the key's reported mailbox_ids / api_key_mailbox_ids.

Bearer-over-session precedence

If a request carries both an Authorization: Bearer header and an rl_session cookie, the Bearer key wins and the request runs as that key — the cookie is ignored. This matters most during rotation: a tool that quietly injects a stale Authorization header (a copied browser cURL, an SDK holding an old key) will run the Bearer branch even when you meant to act as your dashboard session. Send exactly one credential per request.

Rotation: create → verify → revoke

The zero-downtime pattern for a running integration is three explicit steps. Both new and old keys are valid simultaneously between step 1 and step 3, so there is no window where your integration is un-authenticated.

1. Create a new key (does not touch existing keys). Admin-only:

POST /v1/accounts/api-keys
Authorization: Bearer <current-admin-key>
Content-Type: application/json

{ "role": "agent", "label": "support-bot", "mailbox_ids": ["<mailbox-uuid>"] }
201 Created
{
  "id": "<key-uuid>",
  "api_key": "rly_live_<public_id>.<secret>",
  "role": "agent",
  "label": "support-bot",
  "mailbox_ids": ["<mailbox-uuid>"]
}

Save api_key now — it is shown only here. An account may hold at most 10 active keys; at the cap the create call returns 403 FORBIDDEN, so revoke a stale key first. To replace an agent key with the same reach, create a fresh agent key carrying the same mailbox_ids.

2. Verify the new key against an authenticated read before you cut over. Use GET /v1/auth/me (cheap, single response):

GET /v1/auth/me
Authorization: Bearer <new-key>
200 OK
{ "account_id": "...", "email": "...", "tier": "...", "status": "active", "mfa_enabled": false, "email_verified": true, "phone_verification_required": true, "phone_verified": true, "phone_number_masked": "•••• 0123" }

For a Bearer key the response also carries api_key_role ("admin" | "agent") and api_key_mailbox_ids, so you can confirm the new key resolves to the role and mailbox set you expect. Do not use GET /v1/health to validate a key — health is auth-exempt and returns 200 regardless of whether the token is valid, revoked, or absent.

3. Revoke the old key once traffic has moved:

DELETE /v1/accounts/api-keys/<old-key-id>
Authorization: Bearer <new-admin-key>
200 OK
{ "status": "revoked" }

Find <old-key-id> in the id field of the key list (GET /v1/accounts/api-keys). DELETE on a non-existent or already-revoked key returns 404 NOT_FOUND. Revocation is not cached — the revoked key's very next request fails auth.

The rotate endpoint (destructive, admin-only)

POST /v1/accounts/api-keys/rotate mints a new admin key and revokes old admin key(s) in the same transaction — there is no verify window, so any in-flight request that lands between the commit and your secret-store update fails with 401. Its blast radius depends on the caller:

  • Bearer (API key) — revokes only the calling key, mints one new admin key.
  • Session (dashboard) — revokes all admin keys, mints one new admin key.

Rotate never touches agent keys. Prefer the create → verify → revoke sequence above for anything you can't afford to 401; use rotate only for a personal session or a compromise sweep where absorbing in-flight 401s is acceptable.

Self-revoke

Any key — including an agent key — may revoke itself by passing its own id to DELETE /v1/accounts/api-keys/:id. This lets a compromised or retiring agent burn its own credential without an admin in the loop; it is strictly capability-reducing. After a successful self-revoke, that key's next request returns 401.

Revoking any other key still requires an admin key or a dashboard session. An agent key that targets a different key's id gets 403 INSUFFICIENT_SCOPE.

Per-key instruction-trust capability (read-only)

Each key exposes a boolean instruction_trust_enabled (visible in GET /v1/accounts/api-keys). It is off by default and is a read-path capability only — it can, under the right conditions, relax the agent_safety_context guidance an inbound read returns for a granted, verified sender. It grants no send or write power. See Trusted instruction sources for exactly when a read relaxes and how to branch on it.

Two hard rules for an agent integration:

  • An agent cannot enable this for itself. Turning instruction_trust_enabled on is a loosening action, restricted to a dashboard session with fresh re-authentication. A Bearer key — admin or agent — that calls the enable route is rejected with 403 REAUTH_REQUIRES_SESSION. There is no client-sent opt-in; the relaxation is applied server-side by operator configuration only.
  • Turning it off is unprivileged. Disabling the capability is a tightening action and is agent-callable, so an agent can drop its own relaxed reads instantly for incident response.

The capability is also per-key and non-transferable: a freshly created or rotated key always starts instruction_trust_enabled: false, regardless of what the key it replaced had. Re-enable it deliberately (session + re-auth) if a replacement key needs it. Until it is enabled, reads by that key fall back to the standard untrusted-content guidance with no error surfaced.

Scope and auth error codes

These are the codes that signal an auth or scope problem. The full, curated error catalog lives at /agents/errors — branch on code, not on the human error string.

HTTPCodeMeaning
401UNAUTHORIZEDMissing, malformed, invalid, or revoked Bearer key.
401API_KEY_FORMAT_LEGACYKey uses a retired prefix — rotate to a current rly_live_ key.
403INSUFFICIENT_SCOPEAn agent key hit an admin-only route (or tried to revoke another key).
403MAILBOX_ACCESS_DENIEDAn agent key acted on a mailbox it is not bound to (collection reads/writes).
404NOT_FOUNDSingle-resource detail read of a resource the key can't reach (masked to 404 so existence doesn't leak), or DELETE of an already-revoked/missing key.
403REAUTH_REQUIRES_SESSIONA Bearer key hit a route that is session-only + fresh re-auth — enabling instruction trust, the loosening attachment gates, or any MFA-management / SMS step-up ceremony.
403ACCOUNT_SUSPENDEDThe account is not active.
403EMAIL_NOT_VERIFIEDThe account's email is unverified — most routes stay gated until it is.
403PHONE_NOT_VERIFIEDThe required signup phone is unverified — protected routes stay gated until the SMS check completes.

Note the masking asymmetry for agent keys reaching outside their scope: a detail read (GET /v1/messages/:id, GET /v1/mailboxes/:id) returns 404 NOT_FOUND so it can't be used to probe for the existence of mailboxes you can't see, while a collection read (GET /v1/mailboxes/:id/messages and siblings) returns 403 MAILBOX_ACCESS_DENIED for an unbound mailbox UUID (and 404 for an unbound mailbox name).

MFA is a human, session-only concern

Two-factor authentication (TOTP or SMS) protects the human dashboard session, not the API-key surface. Do not attempt any MFA route with an API key. Enrolling, switching, or disabling a factor, the SMS step-up used to re-authorize a loosening action, and the lost-factor recovery and phone-change ceremonies are all dashboard-session-only — a Bearer key (admin or agent) is rejected with 403 REAUTH_REQUIRES_SESSION before anything happens. The full family of SMS-MFA and step-up codes (MFA_SMS_*, REAUTH_*_SMS_*, MFA_RECOVERY_*, PRIVACY_ACK_REQUIRED) is catalogued for completeness at /agents/errors, but an API key only ever sees REAUTH_REQUIRES_SESSION from this surface.

Signup verification bootstrap

Every new password, API/CLI, Google, or GitHub account supplies a mobile number. Existing OAuth logins do not. Protected product routes and provisioning wait for both email and phone verification; gate precedence is email first, then phone. The initial API key/session remains valid for these recovery routes while gated:

POST /v1/auth/verify-email
Authorization: Bearer <new-key>
Content-Type: application/json

{ "code": "482917" }
POST /v1/auth/verify-phone
Authorization: Bearer <new-key>
Content-Type: application/json

{ "code": "593104" }

SMS codes contain six digits, expire after 5 minutes, and only the newest challenge is valid. Resend with an empty object to POST /v1/auth/resend-phone-verification; before verification, the same endpoint also accepts { "phone_number": "+13125550124" } to correct a typo and send a fresh code. Treat 429 PHONE_VERIFICATION_RATE_LIMITED as a wait instruction and honor Retry-After. GET /v1/auth/me returns only phone_number_masked, never the full number.

See also

  • Messages and state — the message lifecycle and verdict vocabulary.
  • Security model — the untrusted-content contract for inbound bodies, attachments, and links.
  • Send gates — why an agent-origin send may be additionally contained.
  • Errors — the full error-code catalog and denial envelope.