API reference
Synthetic accounts: password by email
Create an account in email mode
POST /v1/team/synthetic-accounts takes two new fields:
| Field | Value |
|---|---|
credential_mode | generated (default) or email. email needs email, one of your verified emails; without it the answer is 400 email_required. |
email_tag | Optional. The tag of the address, agent- followed by 10 characters of 0-9 a-h j k m n p-t v-z (for example agent-7k2m9qx4bd), so your client can show the exact address before saving. Omit it and the server picks one. A tag already in use is 409 email_taken: pick a new one and retry. |
Two-factor still comes from mfa_enabled and mfa_method; in email mode the factor is set up on the link's page, never shown to you.
POST /v1/team/synthetic-accounts
{
"label": "Reconciliation bot",
"role": "agent_full",
"credential_mode": "email",
"email": "ops@acme.com",
"email_tag": "agent-7k2m9qx4bd",
"mfa_enabled": true,
"mfa_method": "totp"
}In email mode the 201 answer has credentials: null and setup, the link's state, with status: "pending". If the mail could not be sent, the account still exists with status: "send_failed": send it again with email-setup. Generated mode is unchanged.
Fields on every synthetic account
The account object (GET /v1/team/synthetic-accounts, and account in every answer below) is snake_case, like the rest of the API: created_by_end_user_sub, password_rotated_at, last_seen_at, archived_at, created_at, flow_profile and so on. The old camelCase names (createdByEndUserSub, passwordRotatedAt, …) are still sent next to them with the same values, for now, and such a response has Deprecation: true; read the snake_case names. See Conventions. Two fields belong to setup by email:
| Field | Meaning |
|---|---|
credential_mode | generated or email. |
setup | The setup link's state; null for generated accounts. |
status | pending (sent, unused), expired (ran out or was cancelled unused; send a new one), send_failed (try again), bounced (the address bounced or is suppressed; change the email) or completed (the password is set). |
sent_to | The address the link goes to: the account’s sign-in address. |
lands_in | The inbox that receives it: sent_to without its agent-… tag. |
sent_at, expires_at, completed_at, bounced_at | ISO 8601 timestamps, or null. |
send_count_24h | Setup emails sent for this account in the last 24 hours (at most 5). |
next_send_allowed_at | When another link may be sent; null means now. |
Send, resend or move a setup link
POST /v1/team/synthetic-accounts/:display_id/email-setup mails a new link and answers 200 with {"account": …}. Any earlier unused link stops working. The body is optional: a plain resend sends none, or {}.
| Field | Value |
|---|---|
email | A verified email of yours. Omit it, or send the current base, to keep the address. Another base changes the account’s sign-in address. |
email_tag | As on create: a new tag gives a new address. |
mfa_method | totp, email or none: the factor the link holder sets up. Omit it to keep the account's choice. |
| The account is | What the call does | Audit reason |
|---|---|---|
| Generated | Switches it to email mode. Its password is replaced by one nobody knows, every session ends and every two-factor factor is turned off at once. It cannot sign in until the link is used. | mode_change |
| Email, link unused | A resend. Nothing else changes. | resend |
| Email, link used | A restart: signs it out and turns off its password and factors first, like the switch. | restart |
| Any, with a new base or tag | Changes the sign-in address (re-verified), then resends or restarts as above. | email_change |
A send our mail provider refuses still answers 200, with setup.status: "send_failed". Retrying within the cooldown answers 429, so a retry never sends twice. An archived account answers 409 synthetic_account_archived.
Back to a generated password
POST /v1/team/synthetic-accounts/:display_id/rotate-password on an email-mode account switches it back to credential_mode: "generated", returns the new password once and makes any unused link stop working. POST /v1/team/synthetic-accounts/:display_id/rotate-mfa is refused on an email-mode account with 409 credential_mode_email, because it would show you a TOTP secret: send a new setup link, or switch to a generated password first. Disabling MFA and archiving work in both modes; archiving revokes an unused link.
The setup link and the public routes
The link is https://portal.dispute.markets/setup-account#token=dmsu_…. The token is in the URL fragment, so it never reaches a server log or a Refererheader. It is never returned by any API: it exists only in the mail. The portal's setup page calls these two routes; your own tooling can too.
Inspect: what the link is for
POST /v1/auth/account-setup/inspect with {"token": "dmsu_…"} consumes nothing, so it is safe to call whenever the page opens. It answers 200 with label, workspace_name, sign_in_email (the identifier to sign in with), username (null when unknown), role, expires_at, mfa_method and password_policy (min_length).
Complete: set the password
POST /v1/auth/account-setup/complete with {"token": "dmsu_…", "new_password": "…"} uses the link once. It sets the password (our sign-in service applies its password policy and breach check, and ends every session), then sets up the factor the owner chose and returns its material exactly once:
{
"sign_in_email": "ops+agent-7k2m9qx4bd@acme.com",
"username": "reconciliation-bot",
"mfa": {
"method": "totp",
"totp_uri": "otpauth://totp/…",
"totp_secret": "JBSWY3DPEHPK3PXP…",
"totp_qr_data_url": "data:image/png;base64,…",
"recovery_codes": ["…", "…"]
}
}With email codes, mfa is {"method": "email", "recovery_codes": […]}; without two-factor it is null. A password that is refused is 400 password_rejected (details.reason says why) and leaves the link usable. If the password is set but the factor cannot be, the answer is 502 mfa_setup_failed: the owner sends a new link.
When our sign-in service itself fails, complete answers 503 auth_unavailable (retry after Retry-After seconds) or 502 auth_misconfigured. The link was not used either way. These routes take no bearer token, so they never answer 401 or 403.
Error codes
| Status | code | Where | Meaning and what to do |
|---|---|---|---|
| 400 | email_invalid | create, email-setup | Quoted or non-ASCII local part, a dispute.markets domain, or a malformed email_tag. Pick another email. |
| 400 | email_too_long | create, email-setup | No room for the 17-character tag within 64 characters before the @. Use a shorter address. |
| 400 | email_not_verified | create, email-setup | Not one of your verified emails. Add and verify it (POST /v1/me/emails) first. A resend or restart checks the account’s current inbox too: if you removed that email, send email with a verified one. |
| 400 | email_required | create, email-setup | credential_mode: "email" without email, or an account with no mailbox and no email. |
| 409 | email_taken | create, email-setup | The address is in use. Send a new email_tag and retry. |
| 409 | setup_email_suppressed | create, email-setup | Mail to this address bounced before. Pick another email. Nothing was sent. |
| 409 | setup_pending_limit | create, email-setup | The workspace has 25 unused setup links. Archive or finish some first. |
| 429 | setup_email_rate_limited | create, email-setup | Too many setup emails (see the limits below). details.retry_after_s and Retry-After say when to retry. |
| 503 | setup_email_unavailable | create, email-setup | Setup emails are unavailable right now. Use a generated password or retry later. |
| 409 | credential_change_in_progress | email-setup, rotate-*, mfa, complete | Another change to this account’s credentials is still running (a link being used, a resend, a rotation). Nothing changed and a link was not used; retry after Retry-After seconds. |
| 409 | credential_mode_email | rotate-mfa | The account’s credentials are set by email. Send a new setup link, or switch to a generated password. |
| 404 | setup_link_invalid | inspect, complete | No such link. Check the whole link was copied. |
| 410 | setup_link_expired | inspect, complete | 72 hours passed. Ask the workspace owner for a new link. |
| 410 | setup_link_used | inspect, complete | Already used. Sign in, or use Forgot password. |
| 410 | setup_link_revoked | inspect, complete | A newer link was sent, or the account was archived or switched to a generated password. Use the most recent email. |
| 400 | password_rejected | complete | The password was refused (details.reason). The link stays usable. |
| 502 | mfa_setup_failed | complete | The password is set but the two-factor factor is not. Ask the owner to send a new link. |
| 429 | too_many_attempts | inspect, complete | More than 20 attempts a minute from one IP, or 10 for one link. On complete also when the IP used up its 5 password-reset codes a minute at the authentication service; the link was not used. See Retry-After. |
| 400 | bad_request | email-setup | Our sign-in service refused the address change or the password reset (message says why). No link was sent. |
| 503 | auth_unavailable | email-setup, complete | Our sign-in service is unavailable. No link was sent, or the link was not used; retry after Retry-After seconds. |
| 502 | auth_misconfigured | email-setup, complete | Our sign-in service refused a request dispute.markets made for you. No link was sent, or the link was not used. Retry later, and quote request_id if it persists. |
| 401 | api_key_not_allowed | create, email-setup | Called with an API key. Only the workspace owner, signed in, can create accounts or send links. |
| 429 | rate_limited | create, email-setup, rotate-password, rotate-credentials | Your IP used up its 5 verification and password-reset codes a minute at the authentication service (a create or address change uses 2, a sign-out resend, mode switch or rotation 1). Nothing changed; retry after Retry-After seconds. |
Limits
| Limit | Value | Answer |
|---|---|---|
| Who can send | The workspace owner, in a session. No API key, whatever its scopes. | 401 api_key_not_allowed |
| Cooldown per account | One send per 60 seconds | 429 setup_email_rate_limited |
| Sends per account | 5 per 24 hours | 429, as above |
| Sends per workspace | 20 per hour, 60 per 24 hours | 429, as above |
| Unused links per workspace | 25 | 409 setup_pending_limit |
| All workspaces together | A service-wide daily cap | 503 setup_email_unavailable |
| Link lifetime | 72 hours, single use, one live link per account | 410 setup_link_expired |
| Where mail goes | Only plus-addresses of your own verified emails | 400 email_not_verified |
The account's setup.next_send_allowed_at says when the next send is allowed, so a client can show a countdown instead of hitting the 429.
Audit
Sends are recorded as synthetic_account.send_setup_email (also with outcome: failed when a limit or the suppression check stopped the send), a used link as synthetic_account.complete_setup (the actor is the synthetic account) and a bounce found after the send as synthetic_account.setup_email_bounced. Neither the link nor the address is ever recorded. See Audit events for every field, and Testing a setup-link flow for how to test it.