Authentication

Every request to the ReplyLayer API carries one of two credentials, and both resolve to the same account context:

  • API keyAuthorization: Bearer <api_key>. This is what agents, the CLI, and any programmatic integration use.
  • Session cookie — the rl_session HTTP-only cookie the dashboard sets when a human signs in at app.replylayer.ai.

Every endpoint except the auth and health routes requires one of them. If a request carries both, the API authenticates it as the Bearer key — see Bearer wins when both are present, the one footgun that trips people up.

For the machine-readable contract — exact request/response shapes and the auth error taxonomy — see the agent-facing auth reference.

Key format

A ReplyLayer key looks like this:

rly_live_k3m9p2qx7vn4hjd0.uZ8Qb1vK3mN0pR7sT2wX9yA4cF6gH8jL1nP3rT5vW7z

It is two dot-separated parts — a public id (rly_live_k3m9p2qx7vn4hjd0) and a secret. The public id is what the API stores and what shows up (masked) in the key list; the secret is never stored in plaintext and is shown to you exactly once, in the response body of the call that mints the key. If you lose it before saving it, you cannot recover it — mint a replacement.

Send the whole string, unmodified, in the Authorization: Bearer header.

Roles: admin vs agent

Every API key has one of two roles, chosen at creation. There is no implicit default — you must pass role when you create a key.

adminagent
ScopeFull account access — same authority as a dashboard sessionBound to one or more specific mailboxes only
mailbox_ids at creationForbidden (must be empty/absent)Required, non-empty
Send / receive / read / reply / quarantineAll mailboxesBound mailboxes only
Manage mailboxes, keys, scanner policy, accountYesNo — 403 INSUFFICIENT_SCOPE
Typical holderYour control-plane / provisioning codeA single deployed agent

An agent key that touches a mailbox it isn't bound to gets 403 MAILBOX_ACCESS_DENIED on writes; read masking is deliberately asymmetric so a key can't probe for mailboxes it can't see — a single-resource detail read of an unbound mailbox or message returns 404 NOT_FOUND, while a mailbox-scoped collection read of an unbound mailbox UUID returns 403. GET /v1/mailboxes returns only the bound mailboxes for an agent key.

Give each deployed agent its own agent-role key, scoped to only the mailboxes it needs. That way a leaked credential is contained to those mailboxes, and you can revoke it without disturbing anything else.

Bearer wins when both are present

If a request carries both an rl_session cookie and an Authorization: Bearer header, the API uses the Bearer key and ignores the session. This matters because some flows behave differently depending on which credential authenticated the call.

The classic way to hit this by accident: you're signed into the dashboard, you copy a curl command out of browser DevTools, and that copy carries an Authorization header from an old key you had lying around. The request runs as that Bearer key, not as your session — silently.

Concretely, rotation is the sharpest edge (see Rotating keys): the session-triggered rotation revokes all admin keys, but a Bearer-triggered rotation revokes only the calling key. Same endpoint, different blast radius, decided entirely by which credential authenticated.

To force the session flow: use the dashboard UI, or run your curl from a clean shell with no Authorization header set.

Surfaces that require a dashboard session

Most of the API is fully agent-drivable with a Bearer key. A small set of sensitive, human-in-the-loop toggles are deliberately session-only — a Bearer key (admin or agent) is rejected before any state change. These are enablement gates, not everyday agent operations:

SurfaceContract
Enabling approved raw attachment downloads for a mailboxDashboard session plus fresh re-auth (TOTP or password). A Bearer key → 403 REAUTH_REQUIRES_SESSION. See the attachments reference.
Enabling outbound attachments for a mailboxDashboard session plus fresh re-auth. A Bearer key → 403 REAUTH_REQUIRES_SESSION.
Loosening instruction trust (granting a trusted sender, enabling trusted-instruction mode, turning a strict-recipient guard off)Dashboard session plus fresh re-auth. Bearer keys — including admin — are rejected. An agent can never enable, grant, or loosen instruction trust for itself. See the security-model reference.
Enabling TOTP / MFASession, admin-only.
MFA management ceremonies — enrolling SMS-based MFA, switching your MFA method, the SMS disable proof, the lost-factor recovery flows, and changing your phone numberDashboard session only. A Bearer key (admin or agent) → 403 REAUTH_REQUIRES_SESSION.
Issuing a CLI signup codeSession-only — a Bearer key → 403 SESSION_AUTH_REQUIRED.
Changing your account passwordDashboard session plus fresh re-auth. A Bearer key → 403 REAUTH_REQUIRES_SESSION.
Changing your account emailDashboard session plus fresh re-auth. A Bearer key → 403 REAUTH_REQUIRES_SESSION.

Tightening the same controls is not gated this way. Revoking a trusted sender, disabling trusted-instruction mode, or turning a strict-recipient guard on is unprivileged and agent-callable, so an incident-response agent can clamp down fast. The asymmetry is intentional: loosening needs a human, tightening does not.

The "fresh re-auth" these gates require is method-aware. A TOTP account passes a current authenticator code as reauth_token; a password-only account passes its password; an SMS-based MFA account first mints a scoped SMS re-auth challenge (POST /v1/auth/reauth/sms/send, one action_scope per action) and then passes reauth_challenge plus the SMS code. The challenge is bound to your session, the specific action, and the exact values you're changing, so it cannot be replayed against a different operation.

Text-message codes are a weaker factor than an authenticator app — phone numbers can be hijacked by SIM-swap attacks. The dashboard surfaces this tradeoff wherever you choose SMS-based MFA or switch to it from an authenticator app.

Recovering from a lost phone (two-factor)

If you enrolled two-factor with a text-message (SMS) number and lose access to that phone, sign in with one of the recovery codes you saved when you turned two-factor on — either the dedicated recovery-code login (email + password + recovery code) or the recovery-code field on the ordinary two-factor prompt. A successful recovery-code sign-in carries a one-time, time-limited grant and lands you straight on Settings → Fix your two-factor, where you can:

  • Move two-factor to a new phone (SMS accounts only) — enter the new number, verify a code texted to it, and finish. Fresh recovery codes are issued as part of this step, and your other sessions are signed out for safety.
  • Turn off two-factor entirely (either method) — deletes your recovery codes and signs out your other sessions. Set two-factor up again from Settings whenever you're ready.

If you navigate away before finishing, a banner keeps reminding you and links back to the same page until the recovery window closes. The window is deliberately one-shot and short-lived — recovering doesn't leave a standing backdoor — but refreshing mid-flow, or restarting the phone move from another tab, safely restarts the same ceremony instead of locking you out of it.

This is a dashboard-only, human-in-the-loop flow: there's no way to script it with an API key, because fixing your own two-factor is deliberately not an agent operation.

Adding a phone number without a password (SSO accounts)

Accounts created before mandatory phone verification, or that sign in only via Google or GitHub with no password and no two-factor enrolled, may have no phone number on file at all. Settings offers an "Add phone number" action for exactly these accounts: since there's no password or enrolled two-factor to prove you're the owner, we first email a one-time code to your account address — confirm it, then verify the new number the same way any phone change works (a text to the new number). This applies only while the account genuinely has no password, no two-factor, and no phone bound yet; the moment any of those exist, adding or changing a phone number goes through the ordinary current-factor flow instead.

Changing your password

While signed in, Settings offers a "Change password" action — no need to sign out and drive the forgot-password flow. It requires a fresh factor proof (the same TOTP/SMS/password step-up described above), and on success it signs out every other session; the browser you completed the change in stays signed in with a rotated session. This is a dashboard-only, human-in-the-loop flow — there's no way to script it with an API key.

Changing your email

Settings also offers a "Change email" action to update the address you sign in with. After the same fresh factor proof, we email a 6-digit code to the new address — confirming it swaps your sign-in email and, like a password change, signs out every other session. You can only change your email once every 24 hours. If the address you enter is already in use on another account, you won't find out until you've proven you control it (entered the code) — this avoids leaking which addresses have accounts. Dashboard-only; not scriptable with an API key.

Creating a key

Only an admin key (or a dashboard session) can mint keys. The secret comes back once, in this response — store it before you do anything else.

Agent key:

curl -X POST https://api.replylayer.ai/v1/accounts/api-keys \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "agent",
    "label": "support-bot",
    "mailbox_ids": ["<mailbox-uuid-1>", "<mailbox-uuid-2>"]
  }'

# 201 Created
# {
#   "id": "<key-uuid>",
#   "api_key": "rly_live_k3m9p2qx7vn4hjd0.uZ8Qb1vK3mN0pR7sT2wX9yA4cF6gH8jL1nP3rT5vW7z",
#   "role": "agent",
#   "label": "support-bot",
#   "mailbox_ids": ["<mailbox-uuid-1>", "<mailbox-uuid-2>"]
# }

Admin key: same call with "role": "admin" and no mailbox_ids.

Rules the create endpoint enforces:

  • role is required — no default.
  • An agent key needs a non-empty mailbox_ids; an admin key must omit it. Getting this wrong returns 422 VALIDATION_ERROR.
  • Every mailbox id must belong to your account and be active.
  • Maximum 10 active keys per account. At the cap, the create call returns 403 FORBIDDEN — revoke a stale key first.

A freshly signed-up account's first key and session are locked on protected routes until both signup factors complete. The API returns 403 EMAIL_NOT_VERIFIED while email is pending, then 403 PHONE_NOT_VERIFIED while the required SMS check is pending. The key/session can still call the verification and resend endpoints below.

Completing signup verification

All new password, API/CLI, Google, and GitHub accounts provide a mobile number at signup. Existing Google/GitHub login and identity-link flows are unaffected. The dashboard guides password signups through email first and SMS second; API callers may complete the two authenticated checks in either order, but protected product access and mailbox/subdomain provisioning wait for both.

Verify the email code:

curl -X POST https://api.replylayer.ai/v1/auth/verify-email \
  -H "Authorization: Bearer $NEW_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code":"482917"}'

Verify the 6-digit SMS code (valid for 5 minutes):

curl -X POST https://api.replylayer.ai/v1/auth/verify-phone \
  -H "Authorization: Bearer $NEW_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code":"593104"}'

Only the latest SMS challenge is accepted. To resend, post {} to /v1/auth/resend-phone-verification. Before verification you may correct a typo and resend atomically by posting { "phone_number": "+13125550124" }; after verification this bootstrap route cannot replace the number. A successful resend returns phone_number_masked plus expires_at, never the full number.

SMS verification is available only for numbers in supported countries; others get a terminal 400 PHONE_COUNTRY_NOT_SUPPORTED whose message lists the currently supported markets.

SMS sends have a 30-second cooldown and bounded per-phone, per-account, and per-IP windows. On 429 PHONE_VERIFICATION_RATE_LIMITED, honor Retry-After / details.retry_after. If the initial signup response reported sms_delivery_status: "pending", the account exists and this authenticated resend is the recovery path.

GET /v1/auth/me exposes email_verified, phone_verification_required, phone_verified, and phone_number_masked, so a client can render the next step without handling the full phone number.

Verifying a key

After minting or deploying a key, confirm it authenticates. Use GET /v1/auth/me (a single, cheap authenticated read) or any account-scoped read such as GET /v1/mailboxes:

curl https://api.replylayer.ai/v1/auth/me \
  -H "Authorization: Bearer $NEW_KEY"

# 200 OK — returns account info for the authenticated caller

Do not use GET /v1/health to validate a key. Health is a liveness probe in the auth-exempt route set — it returns 200 ok regardless of whether the Bearer token is valid, missing, or revoked. A 200 from /health tells you the service is up, nothing about your credential. rly doctor and rly auth status (see /agents/cli) wrap the /v1/auth/me check for you.

Rotating keys

Rotate by rolling a fresh key in before you retire the old one, so a long-running integration never hits an auth gap. The pattern is create → verify → revoke:

  1. Create a new key (POST /v1/accounts/api-keys) — this mints a key without touching any existing one. Both keys are now valid simultaneously.
  2. Deploy the new key to your secret store and restart (or let your code re-read) any process holding the old secret.
  3. Verify the new key authenticates with GET /v1/auth/me.
  4. Revoke the old key explicitly:
curl -X DELETE "https://api.replylayer.ai/v1/accounts/api-keys/$OLD_KEY_ID" \
  -H "Authorization: Bearer $NEW_KEY"

# 200 OK  { "status": "revoked" }

Find $OLD_KEY_ID — the id field — via GET /v1/accounts/api-keys. A DELETE on a non-existent or already-revoked key returns 404 NOT_FOUND (and writes no audit row).

The all-in-one rotate endpoint

POST /v1/accounts/api-keys/rotate mints a new admin key and revokes old ones in a single transaction. Because it revokes inside the same transaction that mints, any in-flight request from another process using the old key fails 401 the moment the call returns — so this is the destructive path, best for a personal CLI session, a one-off script, or compromise response. For a running integration, prefer the create-verify-revoke steps above.

Its blast radius depends on how you authenticated (see Bearer wins when both are present):

  • Bearer-triggered — revokes only the calling key, mints one new admin key.
  • Session-triggered (dashboard) — revokes all admin keys, mints one new admin key. Use this to burn every admin credential at once during a suspected compromise.

Either way, agent keys are never touched by rotate, and the endpoint only ever mints an admin key. To roll an agent key, use create-verify-revoke: mint a new agent key with the same mailbox_ids, switch the agent over, verify, and delete the old one.

Per-key capabilities do not carry to a rotated or replacement key. A new key starts with the default (least-privileged) settings; any capability the old key had must be re-enabled explicitly on the new one.

Revoking a key

DELETE /v1/accounts/api-keys/:id revokes a single key. It is admin-only, with one deliberate exception: any key may revoke itself by passing its own id. That lets a compromised or retiring agent burn its own credential without an admin in the loop — a strictly capability-reducing action. An agent key trying to revoke a different key gets 403 INSUFFICIENT_SCOPE. Revocation is not cached: the revoked key fails auth (401) on its very next request.

To find keys and their state, GET /v1/accounts/api-keys lists active keys by default; pass ?include_revoked=true to include revoked keys with their revoked_at / revoked_by metadata. The list returns each key's id, masked prefix, status, role, label, mailbox_ids, and timestamps — never the full secret.

No email or webhook fires when a key is created or revoked. If your security policy needs alerting on key changes, poll GET /v1/accounts/api-keys and diff.

Auth errors

The auth-flow codes you'll encounter:

HTTPCodeMeaning
401UNAUTHORIZEDMissing, invalid, or revoked credential — re-authenticate.
403EMAIL_NOT_VERIFIEDAccount not yet email-verified — complete verification.
403PHONE_NOT_VERIFIEDRequired signup phone is not yet verified — complete the SMS check.
403INSUFFICIENT_SCOPEAdmin-only action attempted with an agent key.
403MAILBOX_ACCESS_DENIEDAgent key touched an unbound mailbox.
403REAUTH_REQUIRES_SESSIONA session-only enablement gate was hit with a Bearer key.
403SESSION_AUTH_REQUIREDA session-only route was hit with a Bearer key.
403FORBIDDENAt the 10-key cap on create.
404NOT_FOUNDDELETE on a missing/already-revoked key, or a masked unbound resource.
422VALIDATION_ERRORRole/mailbox_ids mismatch on create.

For the complete cross-surface error catalog with recovery guidance, see the agent error reference.

See also

Lost every admin key and your dashboard session? Operator recovery is intentionally bounded to verified account ownership and is slow by design — email [email protected] with your account email and id rather than relying on it as a routine path.