# Sendmux auth.md - Agent Access

Sendmux lets agents register for a constrained @myagent.mx mailbox and API access without a human signup form.

If you already have Sendmux MCP, CLI, SDK, or an API key configured for the task, use that existing setup first. Register a new agent identity only when the agent has no usable Sendmux credential.

## Default: save the agent credential bundle

When registering a new agent identity, immediately save the agent credential bundle in the safest secret store your environment provides before token exchange, owner invite, reporting success, or losing context. Later MCP, CLI, SDK, mailbox, and sending work depends on this bundle.

Store `claim_token`, `registration_id`, `mailbox.email`, `claim_token_expires`, `identity_assertion`, `token_endpoint`, `app_resource`, and `sending_resource`. After token exchange, add the pre-claim `smx_agent_` access token or keep `identity_assertion` available until the owner invite returns `202`. If no secure store is available, ask the user where to store these credentials before continuing.

## Optional: install Sendmux skills

If your agent supports Skills, install the Sendmux pack before continuing:

```bash
npx skills add Sendmux/skills
```

Skills are optional. Agents that cannot install skills should continue with this `auth.md` file. For install targets and individual skill names, read `https://sendmux.ai/docs/guides/agent-skills`.

## Success checklist

- Request a registration challenge.
- Solve the proof-of-work challenge.
- Register with `proof_of_work`.
- Immediately save the returned `claim_token` and agent credential bundle in a secure store before token exchange.
- Exchange the `identity_assertion` for an `smx_agent_` access token.
- Call `/api/v1/mailbox/me` with that token.
- Do not stop after pre-claim token success. Next required action: create an owner invite or ask the user for the owner email to invite.

## Critical ordering

- `claim_token` is for the post-approval claim grant. Save it, but do not use it to invite the owner.
- Exchange `identity_assertion` for the pre-claim `smx_agent_` access token.
- Keep the `identity_assertion` or the pre-claim `smx_agent_` access token available until the owner invite returns `202`.
- Use that pre-claim access token to call `/api/v1/mailbox/me`, then `POST /agent-auth/agent/identity/invite`.
- Do not poll the claim-token grant before the owner invite returns `202`; after `202`, poll with `claim_token` and handle `authorization_pending` until the owner approves sending.
- If you lose both the `identity_assertion` and pre-claim access token before inviting the owner, register a fresh agent identity and invite immediately in the same flow.

## 1. Discover the protected resource

Mailbox and Management API resource: https://app.sendmux.ai/api/v1
Sending API resource: https://smtp.sendmux.ai/api/v1
Protected-resource metadata: https://app.sendmux.ai/.well-known/oauth-protected-resource/api/v1
Authorization-server metadata: https://app.sendmux.ai/.well-known/oauth-authorization-server/agent-auth
Issuer: https://app.sendmux.ai/agent-auth
Proof-of-work challenge endpoint: https://app.sendmux.ai/agent-auth/agent/identity/challenge
Identity endpoint: https://app.sendmux.ai/agent-auth/agent/identity
Token endpoint: https://app.sendmux.ai/agent-auth/oauth2/token
Revocation endpoint: https://app.sendmux.ai/agent-auth/oauth2/revoke

Cold API requests receive the discovery hint:

```http
GET /api/v1/mailbox/me HTTP/1.1
Host: app.sendmux.ai

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://app.sendmux.ai/.well-known/oauth-protected-resource/api/v1"
```

Fetch the protected-resource metadata, then the authorization-server metadata named in `authorization_servers`.

## 2. Register anonymously

Supported identity type: `anonymous`.
Supported credential type: `access_token`.
Supported pre-claim scopes: `mailbox.read`, `email.receive`.

Sendmux uses a staged agent-auth registration pattern: registration returns a service-signed `identity_assertion` for constrained pre-claim access, plus a `claim_token` the agent can later use after the human owner approves sending in Sendmux. Sendmux does not publish an OTP `claim_endpoint`; human ownership uses the invite extension below.
Anonymous registration requires a short-lived proof-of-work payload. Request a challenge using the same registration body you intend to submit, solve it, then send the solved payload as `proof_of_work` on the registration request.

```http
POST https://app.sendmux.ai/agent-auth/agent/identity/challenge HTTP/1.1
Content-Type: application/json

{
  "type": "anonymous",
  "mailbox_local_part": "agent-demo",
  "client_name": "Demo Agent",
  "idempotency_key": "idem_agent_registration_123"
}

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "challenge": {
    "parameters": {
      "algorithm": "PBKDF2/SHA-256",
      "cost": <number_from_response>,
      "keyLength": <number_from_response>,
      "keyPrefix": "<prefix_from_response>",
      "nonce": "<nonce_from_response>",
      "salt": "<salt_from_response>",
      "expiresAt": <unix_seconds_from_response>,
      "data": {
        "purpose": "agent_registration",
        "request_hash": "<request_hash_from_response>"
      }
    },
    "signature": "<challenge_signature>"
  },
  "expires_at": "2026-06-22T22:05:00.000Z",
  "payload_field": "proof_of_work",
  "proof_format": {
    "encoding": "base64",
    "content": "UTF-8 JSON",
    "json_shape": {
      "challenge": "exact challenge object from the challenge response",
      "solution": {
        "counter": "integer",
        "derivedKey": "solver-returned hex string"
      }
    },
    "registration_body": "must match the challenge request body exactly, except for the added proof_of_work field"
  }
}
```

Build `proof_of_work` as standard base64 of a UTF-8 JSON payload with this exact shape:

```json
{
  "challenge": { "...": "the exact challenge object from the challenge response" },
  "solution": {
    "counter": 12345,
    "derivedKey": "<solver-returned hex string>"
  }
}
```

`proof_of_work` is the standard base64 encoding of the UTF-8 bytes of that JSON payload. Use the exact `challenge` object returned by the challenge endpoint. `solution.counter` is an integer, and `solution.derivedKey` is the hex string returned by your solver.

Copy-paste proof helper:

```js
const registrationBody = {
  type: "anonymous",
  mailbox_local_part: "agent-demo",
  client_name: "Demo Agent",
  idempotency_key: "idem_agent_registration_123",
};

const hexToBytes = (hex) =>
  new Uint8Array(hex.match(/../g).map((byte) => Number.parseInt(byte, 16)));

const bytesToHex = (bytes) =>
  Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");

const base64Utf8 = (value) => {
  if (typeof Buffer !== "undefined") return Buffer.from(value, "utf8").toString("base64");
  let binary = "";
  for (const byte of new TextEncoder().encode(value)) binary += String.fromCharCode(byte);
  return btoa(binary);
};

async function deriveHex(parameters, saltBytes, passwordBytes) {
  const key = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits"]);
  const bits = await crypto.subtle.deriveBits(
    { name: "PBKDF2", hash: "SHA-256", salt: saltBytes, iterations: parameters.cost },
    key,
    (parameters.keyLength ?? 32) * 8,
  );
  return bytesToHex(new Uint8Array(bits));
}

async function solveProof(challenge) {
  const { keyPrefix, nonce, salt } = challenge.parameters;
  const nonceBytes = hexToBytes(nonce);
  const saltBytes = hexToBytes(salt);
  for (let counter = 0; ; counter += 1) {
    const counterBytes = new Uint8Array(4);
    new DataView(counterBytes.buffer).setUint32(0, counter, false);
    const passwordBytes = new Uint8Array(nonceBytes.length + 4);
    passwordBytes.set(nonceBytes);
    passwordBytes.set(counterBytes, nonceBytes.length);
    const derivedKey = await deriveHex(challenge.parameters, saltBytes, passwordBytes);
    if (derivedKey.startsWith(keyPrefix)) return { counter, derivedKey };
  }
}

const challengeResponse = await fetch("https://app.sendmux.ai/agent-auth/agent/identity/challenge", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify(registrationBody),
});
const { challenge } = await challengeResponse.json();
const solution = await solveProof(challenge);
const proof_of_work = base64Utf8(JSON.stringify({ challenge, solution }));

const registrationResponse = await fetch("https://app.sendmux.ai/agent-auth/agent/identity", {
  method: "POST",
  headers: { "content-type": "application/json", "idempotency-key": registrationBody.idempotency_key },
  body: JSON.stringify({ ...registrationBody, proof_of_work }),
});
const registration = await registrationResponse.json();
```

The registration body must match the challenge request body exactly, except for the added `proof_of_work` field. If you change `mailbox_local_part`, `client_name`, or `idempotency_key`, request and solve a new challenge.

```http
POST https://app.sendmux.ai/agent-auth/agent/identity HTTP/1.1
Content-Type: application/json
Idempotency-Key: idem_agent_registration_123

{
  "type": "anonymous",
  "mailbox_local_part": "agent-demo",
  "client_name": "Demo Agent",
  "idempotency_key": "idem_agent_registration_123",
  "proof_of_work": "<base64 UTF-8 JSON proof payload>"
}

HTTP/1.1 201 Created
Content-Type: application/json
Cache-Control: no-store

{
  "registration_id": "areg_...",
  "registration_type": "anonymous",
  "identity_assertion": "<service-signed JWT>",
  "assertion_expires": "2026-06-22T22:00:00.000Z",
  "claim_token": "smx_agent_claim_...",
  "claim_token_expires": "2026-06-23T21:00:00.000Z",
  "pre_claim_scopes": ["mailbox.read", "email.receive"],
  "mailbox": {
    "email": "agent-demo@myagent.mx",
    "status": "provisioning"
  }
}
```

`proof_of_work` is required. `mailbox_local_part`, `client_name`, and `idempotency_key` are optional. If `mailbox.status` is `provisioning`, retry token exchange shortly.

## 3. Save the agent credential bundle

Immediately after registration and before exchanging the identity assertion, inviting the owner, or reporting that setup succeeded, save the agent credential bundle in a secure store controlled by the user or agent environment, such as 1Password, an OS keychain, or the agent platform's encrypted secret store. This bundle is required later for MCP, CLI, SDK, mailbox, and sending work.

Sendmux cannot recover or reveal the raw `claim_token` later. If the raw claim token is lost, the safe recovery path is to rerun registration and invite with a fresh agent identity.

Do not store these credentials in chat, logs, repo files, screenshots, temporary prompts, or memory-only state that may disappear before owner approval. Store these fields together:

- `claim_token`
- `registration_id`
- `mailbox.email`
- `claim_token_expires`
- `identity_assertion` or the pre-claim `smx_agent_` access token until the owner invite returns `202`
- `token_endpoint=https://app.sendmux.ai/agent-auth/oauth2/token`
- `app_resource=https://app.sendmux.ai/api/v1`
- `sending_resource=https://smtp.sendmux.ai/api/v1`

If no secure store is available, stop and ask the user where to store these credentials. Do not continue to owner invite if you cannot retrieve the bundle later.

## 4. Exchange the identity assertion and verify the mailbox

```http
POST https://app.sendmux.ai/agent-auth/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=<identity_assertion>&resource=https%3A%2F%2Fapp.sendmux.ai%2Fapi%2Fv1

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "access_token": "smx_agent_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "mailbox.read email.receive"
}
```

If mailbox provisioning is not finished yet, the token endpoint returns `503 temporarily_unavailable` with `Retry-After: 10` and `retry_after: 10`.

Verify the mailbox before inviting the owner:

```http
GET /api/v1/mailbox/me HTTP/1.1
Host: app.sendmux.ai
Authorization: Bearer smx_agent_...
```

After `/api/v1/mailbox/me` succeeds, invite the owner if you know their email. If you do not know it, ask the user: `What owner email should I invite for approval?`

## 5. Invite a human owner

The authorization-server metadata includes `agent_auth.x_sendmux_identity_invite_endpoint` for this Sendmux-specific invite flow. This is not an OTP claim ceremony and accepting the invite does not automatically upgrade the agent token scopes.

```http
POST https://app.sendmux.ai/agent-auth/agent/identity/invite HTTP/1.1
Authorization: Bearer smx_agent_...
Content-Type: application/json
Idempotency-Key: idem_owner_invite_123

{
  "email": "owner@example.com",
  "requested_role": "owner",
  "idempotency_key": "idem_owner_invite_123"
}

HTTP/1.1 202 Accepted
Content-Type: application/json
Cache-Control: no-store

{
  "invite_id": "ainv_...",
  "status": "pending"
}
```

The `email` field is the human owner's email address. `requested_role` must be `owner` for pre-claim agents. Sendmux sends the invite email; the agent never receives the invite token.

Copy-paste uninterrupted setup helper:

Set `SENDMUX_OWNER_EMAIL`, paste this as one file, and do not convert it to `require()`. It keeps the `identity_assertion` and pre-claim access token available until the owner invite returns `202`. Replace `storeClaimMaterial` with your secure-store call before waiting for owner approval.

```js
const ownerEmail = process.env.SENDMUX_OWNER_EMAIL;
if (!ownerEmail) throw new Error("Set SENDMUX_OWNER_EMAIL first.");

const suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
const registrationBody = {
  type: "anonymous",
  mailbox_local_part: `agent-${suffix}`.slice(0, 48),
  client_name: "Agent setup helper",
  idempotency_key: `idem_agent_registration_${suffix}`,
};
const inviteIdempotencyKey = `idem_owner_invite_${suffix}`;

const endpoints = {
  challenge: "https://app.sendmux.ai/agent-auth/agent/identity/challenge",
  identity: "https://app.sendmux.ai/agent-auth/agent/identity",
  token: "https://app.sendmux.ai/agent-auth/oauth2/token",
  invite: "https://app.sendmux.ai/agent-auth/agent/identity/invite",
  mailboxMe: "https://app.sendmux.ai/api/v1/mailbox/me",
  appResource: "https://app.sendmux.ai/api/v1",
  sendingResource: "https://smtp.sendmux.ai/api/v1",
};

const wait = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

const hexToBytes = (hex) =>
  new Uint8Array(hex.match(/../g).map((byte) => Number.parseInt(byte, 16)));

const bytesToHex = (bytes) =>
  Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");

const base64Utf8 = (value) => Buffer.from(value, "utf8").toString("base64");

async function deriveHex(parameters, saltBytes, passwordBytes) {
  const key = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits"]);
  const bits = await crypto.subtle.deriveBits(
    { name: "PBKDF2", hash: "SHA-256", salt: saltBytes, iterations: parameters.cost },
    key,
    (parameters.keyLength ?? 32) * 8,
  );
  return bytesToHex(new Uint8Array(bits));
}

async function solveProof(challenge) {
  const { keyPrefix, nonce, salt } = challenge.parameters;
  const nonceBytes = hexToBytes(nonce);
  const saltBytes = hexToBytes(salt);
  for (let counter = 0; ; counter += 1) {
    const counterBytes = new Uint8Array(4);
    new DataView(counterBytes.buffer).setUint32(0, counter, false);
    const passwordBytes = new Uint8Array(nonceBytes.length + 4);
    passwordBytes.set(nonceBytes);
    passwordBytes.set(counterBytes, nonceBytes.length);
    const derivedKey = await deriveHex(challenge.parameters, saltBytes, passwordBytes);
    if (derivedKey.startsWith(keyPrefix)) return { counter, derivedKey };
  }
}

async function readJson(response, step) {
  const text = await response.text();
  const body = text ? JSON.parse(text) : {};
  if (!response.ok) {
    throw new Error(`${step} failed: ${response.status} ${JSON.stringify(body)}`);
  }
  return body;
}

async function postJson(url, body, headers = {}) {
  const response = await fetch(url, {
    method: "POST",
    headers: { "content-type": "application/json", ...headers },
    body: JSON.stringify(body),
  });
  return readJson(response, url);
}

async function exchangeIdentityAssertion(identityAssertion) {
  for (let attempt = 0; attempt < 18; attempt += 1) {
    const form = new URLSearchParams({
      grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
      assertion: identityAssertion,
      resource: endpoints.appResource,
    });
    const response = await fetch(endpoints.token, {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: form,
    });
    const body = await response.json();
    if (response.ok) return body;
    if (response.status === 503 && body.error === "temporarily_unavailable") {
      await wait(Number(response.headers.get("Retry-After") ?? body.retry_after ?? 10));
      continue;
    }
    throw new Error(`identity_assertion exchange failed: ${response.status} ${JSON.stringify(body)}`);
  }
  throw new Error("Mailbox provisioning did not finish before the identity assertion retry window ended.");
}

async function storeClaimMaterial(claimMaterial) {
  // Replace this with 1Password, an OS keychain, or the agent platform's encrypted secret store.
  return claimMaterial;
}

const challengeResponse = await postJson(endpoints.challenge, registrationBody);
const solution = await solveProof(challengeResponse.challenge);
const proof_of_work = base64Utf8(JSON.stringify({ challenge: challengeResponse.challenge, solution }));

const registration = await postJson(
  endpoints.identity,
  { ...registrationBody, proof_of_work },
  { "idempotency-key": registrationBody.idempotency_key },
);

await storeClaimMaterial({
  claim_token: registration.claim_token,
  registration_id: registration.registration_id,
  mailbox_email: registration.mailbox?.email,
  claim_token_expires: registration.claim_token_expires,
  token_endpoint: endpoints.token,
  app_resource: endpoints.appResource,
  sending_resource: endpoints.sendingResource,
});

const preClaimToken = await exchangeIdentityAssertion(registration.identity_assertion);
const mailboxResponse = await fetch(endpoints.mailboxMe, {
  headers: { authorization: `Bearer ${preClaimToken.access_token}` },
});
const mailbox = await readJson(mailboxResponse, endpoints.mailboxMe);

const invite = await postJson(
  endpoints.invite,
  { email: ownerEmail, requested_role: "owner", idempotency_key: inviteIdempotencyKey },
  {
    authorization: `Bearer ${preClaimToken.access_token}`,
    "idempotency-key": inviteIdempotencyKey,
  },
);

console.log(JSON.stringify({ mailbox: mailbox.email ?? registration.mailbox?.email, invite_id: invite.invite_id, status: invite.status }, null, 2));
```

## 6. Wait for owner-approved sending

After the owner accepts the invite, they can approve sending for the agent's mailbox in Sendmux. The agent polls the token endpoint with its `claim_token` until that approval exists. Request the app resource for mailbox API work, and request the Sending API resource before calling `smtp.sendmux.ai`.

```http
POST https://app.sendmux.ai/agent-auth/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aworkos%3Aagent-auth%3Agrant-type%3Aclaim&claim_token=smx_agent_claim_...&resource=https%3A%2F%2Fapp.sendmux.ai%2Fapi%2Fv1

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "access_token": "smx_agent_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "mailbox.read email.receive email.send mailbox.settings.update"
}
```

For the Sending API, request the Sending API resource:

```http
POST https://app.sendmux.ai/agent-auth/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aworkos%3Aagent-auth%3Agrant-type%3Aclaim&claim_token=smx_agent_claim_...&resource=https%3A%2F%2Fsmtp.sendmux.ai%2Fapi%2Fv1

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "access_token": "smx_agent_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "mailbox.read email.receive email.send mailbox.settings.update"
}
```

If the owner has joined but not approved sending yet, the token endpoint returns `503 authorization_pending` with `Retry-After: 10` and `retry_after: 10`. If you have not sent the owner invite yet, go back to the pre-claim token from `identity_assertion`; the `claim_token` cannot create the invite.

## 7. Use owner-approved sending

```http
POST /api/v1/emails/send HTTP/1.1
Host: smtp.sendmux.ai
Authorization: Bearer smx_agent_...
Content-Type: application/json

{
  "from": { "email": "agent-demo@myagent.mx" },
  "to": [{ "email": "owner@example.com" }],
  "subject": "Hello from Sendmux",
  "text": "Owner-approved agent sending is active."
}
```

Pre-claim agents can read and receive mail for their assigned @myagent.mx mailbox. Owner-approved app-resource claim-grant tokens can send through the Mailbox API. Owner-approved Sending-resource claim-grant tokens can send through the Sending API from that same mailbox.

## 8. Revoke the credential

```http
POST https://app.sendmux.ai/agent-auth/oauth2/revoke HTTP/1.1
Content-Type: application/x-www-form-urlencoded

token=smx_agent_...&token_type_hint=access_token

HTTP/1.1 200 OK
Cache-Control: no-store
```

Revocation is idempotent. Unknown or already-revoked access tokens still return `200 OK`.

## 9. Limits and expiry

Each anonymous registration gets exactly one constrained @myagent.mx mailbox. Proof-of-work challenges are short-lived, and registration plus invite endpoints are rate limited. `429` responses include `Retry-After`, `retry_after`, and `rate_limit_scope` so agents can back off. Unclaimed registrations expire after 24 hours; Sendmux revokes pre-claim access tokens and reclaims the agent mailbox after expiry. Owner-approved access tokens stay short-lived; the owner can revoke sending from the team page.

## 10. Error table

| Endpoint | Status | Error | Meaning | Agent action |
| --- | ---: | --- | --- | --- |
| `https://app.sendmux.ai/agent-auth/agent/identity/challenge` | 400 | `invalid_request` | Body is not JSON, unsupported `type`, or has an invalid field. | Correct the request body. |
| `https://app.sendmux.ai/agent-auth/agent/identity/challenge` | 429 | `temporarily_unavailable` | Challenge rate limit reached. | Back off until `Retry-After`. |
| `https://app.sendmux.ai/agent-auth/agent/identity` | 400 | `proof_of_work_required` | Registration is missing `proof_of_work`. | Request a challenge, solve it, encode `{ "challenge": <exact challenge>, "solution": { "counter": <integer>, "derivedKey": <hex> } }` as base64 UTF-8 JSON, and retry registration. |
| `https://app.sendmux.ai/agent-auth/agent/identity` | 400 | `invalid_request` | Body is not JSON, unsupported `type`, invalid field, invalid or expired `proof_of_work`, a proof bound to a different registration body, or replayed `idempotency_key` with a different body. | Correct the request body or request a fresh challenge for the exact body you will submit. |
| `https://app.sendmux.ai/agent-auth/agent/identity` | 409 | `invalid_request` | Requested mailbox local part is unavailable. | Retry without `mailbox_local_part` or choose another value. |
| `https://app.sendmux.ai/agent-auth/agent/identity` | 429 | `temporarily_unavailable` | Registration rate limit reached. | Back off until `Retry-After`. |
| `https://app.sendmux.ai/agent-auth/agent/identity` | 503 | `server_error` | Registration is temporarily unavailable. | Stop and report the failure; do not retry repeatedly. |
| `https://app.sendmux.ai/agent-auth/oauth2/token` | 400 | `unsupported_grant_type` | `grant_type` is not one of the documented values. | Use the assertion grant before owner approval, or the claim grant after owner approval. |
| `https://app.sendmux.ai/agent-auth/oauth2/token` | 400 | `invalid_grant` | Identity assertion or claim token is invalid, expired, revoked, or bound to an expired registration. | Register again, use a fresh assertion, or use the original claim token. |
| `https://app.sendmux.ai/agent-auth/oauth2/token` | 503 | `authorization_pending` | Owner approval for sending is not complete. | If the owner invite has not been sent, exchange `identity_assertion` for a pre-claim access token and send the invite first. Otherwise retry after `Retry-After` or `retry_after`. |
| `https://app.sendmux.ai/agent-auth/oauth2/token` | 503 | `temporarily_unavailable` | Mailbox provisioning is still in progress. | Retry after the `Retry-After` header or `retry_after` body field. |
| `https://app.sendmux.ai/agent-auth/agent/identity/invite` | 401 | `invalid_token` | Missing, invalid, expired, or revoked agent access token. | Exchange a valid identity assertion for a fresh access token. |
| `https://app.sendmux.ai/agent-auth/agent/identity/invite` | 400 | `invalid_request` | Body is not JSON, email is invalid, or idempotency key conflicts. | Correct the request body. |
| `https://app.sendmux.ai/agent-auth/agent/identity/invite` | 403 | `access_denied` | Requested role is not available, or an owner invite is already pending. | Wait for the human owner or retry the same pending invite email. |
| `https://app.sendmux.ai/agent-auth/agent/identity/invite` | 429 | `temporarily_unavailable` | Invite rate limit reached. | Back off until `Retry-After`. |
| `https://app.sendmux.ai/agent-auth/oauth2/revoke` | 400 | `invalid_request` | Form body is missing or malformed. | Send form-encoded `token`. |
| `https://app.sendmux.ai/agent-auth/oauth2/revoke` | 400 | `unsupported_token_type` | `token_type_hint` is not `access_token`. | Use `access_token` or omit the hint. |