> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sideshift.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> Every error code the Connect API returns, the HTTP status it comes with, when it happens, and what to do.

Every failure from `https://app.sideshift.app/api/embed` uses one envelope:

```json theme={"system"}
{
  "success": false,
  "error": {
    "code": "AMOUNT_TOO_LARGE",
    "message": "Amount too large. Maximum is $100000.00",
    "details": {
      "requestedAmountCents": 15000000,
      "maximumAmountCents": 10000000,
      "configuredMaximumAmountCents": 20000000,
      "systemMaximumAmountCents": 10000000
    }
  }
}
```

Branch on `error.code`. `message` is for humans and may change; `details` is present only
when there is machine-readable context to give. In a best-effort batch the same shape
appears per item at `data.results[i].error`.

## Authentication and authorization

| Code                       | Status | When                                                                                                                                                                                                                                                       | What to do                                                                   |
| -------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `API_KEY_MISSING`          | 401    | No `x-api-key` header.                                                                                                                                                                                                                                     | Send the key.                                                                |
| `INVALID_API_KEY`          | 401    | The key does not match an active key. Also returned for a revoked key, and for a key whose prefix was mistyped, since the prefix selects which index is searched.                                                                                          | Check the value and whether it was revoked.                                  |
| `AUTH_ERROR`               | 401    | `PATCH /accounts/{id}` hit an internal authentication failure.                                                                                                                                                                                             | Retry once; then contact support with the request id and time.               |
| `EMBED_NOT_ENABLED`        | 403    | Connect is not enabled for your company, its authorization was disabled, or (on `POST /auth/token`) the integration has no widget-permission configuration at all.                                                                                         | Contact support.                                                             |
| `DOMAIN_NOT_ALLOWED`       | 403    | A browser request carried an `Origin` that is not on your allowlist, or a browser user agent sent no `Origin`.                                                                                                                                             | Call the API from your backend, or add the domain.                           |
| `DOMAIN_NOT_ALLOWED`       | 400    | `POST /auth/token` could not bind a domain: the allowlist is empty and no `Origin` was sent, or `targetDomain` is not on the list.                                                                                                                         | Add a domain, or pass a `targetDomain` that is listed.                       |
| `WIDGET_NOT_PERMITTED`     | 403    | The requested widget is disabled for your integration; transfers are disabled because the payout widget is off; a transfer **out** of an account your integration did not create; or `POST /accounts/withdrawal-balance` on an account you did not create. | Use an account you created, or ask SideShift to change the permission.       |
| `INSUFFICIENT_PERMISSIONS` | 403    | `PATCH /accounts/{id}` on an account that is not yours, or `DELETE /accounts` naming an account you only link to.                                                                                                                                          | Only accounts created by your integration can be updated or detached.        |
| `RATE_LIMITED`             | 429    | The per-minute request limit, the per-hour token limit, or the per-day transfer count. The message names the seconds until reset for the first two.                                                                                                        | Back off with jitter. See [rate limits](/connect/configuration#rate-limits). |

The widget session endpoint used by the widget itself returns `TOKEN_EXPIRED` and
`TOKEN_INVALID` (401) and `WHOP_API_ERROR` (500). You will not see these from your
backend; they surface as the [widget error messages](#widget-error-messages) below.

## Validation

| Code                         | Status | When                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | What to do                                |
| ---------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| `VALIDATION_ERROR`           | 400    | A field has the wrong shape or value. Examples: a non-object body; `widgetType` outside `payout`, `payin`, `both`; `expiresInSeconds` under 60 or over 86400; `destinationBalance` outside `wallet` or `withdrawal`; `destinationBalance: "withdrawal"` on a `user_to_company` transfer; source and destination the same account; a missing commercial-evidence field on a live transfer; a bad `status` or `direction` filter; `atomic` not a boolean; a duplicate `idempotencyKey` inside an atomic batch; more than 200 items in a batch; `passedInBalance` not a non-negative integer; `allowedDomains` combined with `addDomains`; an unknown `eventType` or withdrawal `status` on `POST /webhooks/test`. | Fix the request.                          |
| `VALIDATION_ERROR`           | 403    | `POST /webhooks/test` with a live key.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | Use a sandbox key.                        |
| `VALIDATION_ERROR`           | 404    | `POST /webhooks/{eventId}/replay` for an event with no prior delivery in your mode, or a `logId` that is not yours.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Check the id in `GET /webhook-logs`.      |
| `MISSING_REQUIRED_FIELD`     | 400    | `email` missing on create; `sideshiftAccountId` missing on token or withdrawal-balance; `amountCents` missing; `idempotencyKey` missing or shorter than 8 characters; neither `fromAccountId` nor `toAccountId` on a transfer; `transferId` missing on detail or cancel.                                                                                                                                                                                                                                                                                                                                                                                                                                        | Send the field.                           |
| `INVALID_METADATA`           | 400    | `metadata` is not a flat object of strings, has more than 50 keys, a key longer than 40 characters, or a value longer than 500.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | Trim or restructure the metadata.         |
| `INVALID_EMAIL`              | 400    | `email` on create is not a valid address.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | Fix the address.                          |
| `AMOUNT_TOO_SMALL`           | 400    | `amountCents` below 1. `details.minimumAmountCents` is included.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | Send at least 1 cent.                     |
| `AMOUNT_TOO_LARGE`           | 400    | `amountCents` above the effective maximum (your configured ceiling capped at \$100,000). `details` carries `requestedAmountCents`, `maximumAmountCents`, `configuredMaximumAmountCents` and `systemMaximumAmountCents`. Also returned, without `details`, for a non-integer amount with the message `Amount must be an integer (cents)`.                                                                                                                                                                                                                                                                                                                                                                        | Split the payment, or send integer cents. |
| `ESCROW_DESTINATION_INVALID` | 400    | `escrowDestinationCompanyId` on `POST /auth/token` does not name an existing company.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | Check the id.                             |

## Accounts

| Code                          | Status | When                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | What to do                                                                                                                   |
| ----------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `ACCOUNT_NOT_FOUND`           | 404    | The account does not exist, or exists but is neither created by nor linked to your integration. Returned by token minting, balance, update, withdrawal-balance, transfers (`Source account not found`, `Destination account not found`) and the transfers list filter. Also the response to `GET` or `DELETE /transfers/{transferId}` for a transfer that is not yours or is in the other mode (`Transfer not found`), to `POST /checkout/sessions` for a destination you cannot use, and to `POST /accounts/create` with `enableFallbacks: false` when the email belongs to another SideShift user. | Check the id. A 404 is returned instead of 403 on purpose, so the existence of other integrators' accounts is not disclosed. |
| `ACCOUNT_NOT_FOUND`           | 400    | The account exists but has no payment account configured and one could not be created: on a transfer (`Source account does not have a payment account configured`, `Destination account does not have a payment account configured`), on token minting, or on withdrawal-balance (`Account does not have a withdrawal account configured`).                                                                                                                                                                                                                                                          | The account needs a payment account; contact support if it cannot be created.                                                |
| `EMAIL_ALREADY_EXISTS`        | 409    | Every fallback email slot for the address is already in use.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | See [account fallback emails](/connect/guides#account-fallback-emails).                                                      |
| `EMAIL_IN_USE`                | 409    | `PATCH /accounts/{id}` to an email another account already uses.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Choose another address.                                                                                                      |
| `PAYMENT_ACC_CREATION_FAILED` | 500    | The payment provider could not provision an account, including after three fallback attempts.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Retry later with the same `externalId`; contact support if it persists.                                                      |
| `VERIFICATION_NOT_FOUND`      | 404    | The verification or account is not found, or the request used a sandbox key (verification records are not created in sandbox).                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | Use a live key for verification reads.                                                                                       |
| `VERIFICATION_SERVICE_ERROR`  | 502    | The identity provider is temporarily unavailable.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | Retry later.                                                                                                                 |

## Transfers

| Code                               | Status | When                                                                                                                                                                                                                                                           | What to do                                                                                                                  |
| ---------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `INSUFFICIENT_BALANCE`             | 400    | The source wallet (plus, for user sources in live, the withdrawal-ready balance) cannot cover the amount. In an atomic batch, the combined source balance cannot cover every item.                                                                             | Fund the source. Retrying unchanged does not help.                                                                          |
| `DAILY_LIMIT_EXCEEDED`             | 400    | Your integration's daily transfer **amount** limit for the UTC day, counted per mode. The message names the current total and the limit.                                                                                                                       | Wait for the next UTC day or ask for a higher limit. The daily transfer **count** limit surfaces as `RATE_LIMITED` instead. |
| `DUPLICATE_TRANSFER`               | 409    | See the [conflict variants](#409-conflict-variants) below.                                                                                                                                                                                                     | Depends on `details.conflictType`.                                                                                          |
| `TRANSFER_RECONCILIATION_REQUIRED` | 409    | An earlier attempt with this key is awaiting recovery. `details.conflictType` is `"reconciliation"`, `details.retryable` is `true`.                                                                                                                            | Retry later with the **same** key.                                                                                          |
| `TRANSFER_FAILED`                  | 202    | See [reconciliation pending](#202-reconciliation-pending) below.                                                                                                                                                                                               | Treat as in-flight.                                                                                                         |
| `TRANSFER_FAILED`                  | 409    | A replay of a key whose original attempt failed on a transfer path that no longer exists.                                                                                                                                                                      | Retry with a new key.                                                                                                       |
| `TRANSFER_FAILED`                  | 400    | `POST /accounts/withdrawal-balance` on an account that has not completed identity verification.                                                                                                                                                                | Have the user verify in the payout widget first.                                                                            |
| `TRANSFER_FAILED`                  | 500    | The transfer failed for a reason SideShift could not classify. The source reservation was rolled back.                                                                                                                                                         | Retry with the same key after a delay.                                                                                      |
| `TRANSFER_NOT_CANCELLABLE`         | 409    | `DELETE /transfers/{transferId}` on a transfer that is completed, failed, already cancelled, has reached the payment provider, or may still be settling. The message says which; `details.transferId` is included. In sandbox every transfer is in this state. | Nothing to do. Cancel only applies to an unsubmitted reservation.                                                           |
| `BATCH_ABORTED`                    | 409    | An atomic batch stopped after money started moving and every settled item was reversed. `details`: `atomic`, `phase` (`execute`), `failedIndex`, `results`, `reversed`, `reverseFailed` (empty). Also the code for a replay of an aborted batch.               | Nothing moved on net. Retry with new per-item keys and a new batch key.                                                     |
| `BATCH_ABORTED`                    | 500    | An atomic batch hit an unexpected error during preflight. `details.phase` is `preflight`. Nothing moved.                                                                                                                                                       | Retry with new keys.                                                                                                        |
| `BATCH_COMPENSATION_FAILED`        | 409    | An atomic batch failed after some items settled and not all of them could be reversed. `details.reversed` and `details.reverseFailed` list which.                                                                                                              | Stop. Reconcile from `GET /transfers` and contact support with the batch details.                                           |

When an atomic batch fails preflight because of one item, the response carries that
item's own code and status (for example `400 INSUFFICIENT_BALANCE` or
`404 ACCOUNT_NOT_FOUND`) with `details.atomic: true`, `details.phase: "preflight"`,
`details.failedIndex` and `details.results`. Items after the failing one are marked
`BATCH_ABORTED` with `Not attempted because the atomic batch aborted`. No money moved.

### 409 conflict variants

`DUPLICATE_TRANSFER` and `TRANSFER_RECONCILIATION_REQUIRED` are both 409s, and the way to
tell whether a retry can help is `details.conflictType`.

| `conflictType`         | Code                               | `retryable` | Meaning                                                                                                                                                                                                                                                                                                                |
| ---------------------- | ---------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `idempotency`          | `DUPLICATE_TRANSFER`               | `false`     | The key was already used for a request with different accounts, amount, direction, destination balance or metadata. `details.blockingTransferId` names the existing transfer. On an atomic batch, the batch key was reused with different contents.                                                                    |
| `source_serialization` | `DUPLICATE_TRANSFER`               | `true`      | Withdrawal-destination transfers from one source account are serialised. Another such transfer from the same source (`details.blockingTransferId`) is still settling. Queue sends per source, or read and cancel the blocking transfer if it never settled. On an atomic batch, the same batch is already in progress. |
| `reconciliation`       | `TRANSFER_RECONCILIATION_REQUIRED` | `true`      | A previous attempt with this key is awaiting recovery. Retry with the same key after a delay.                                                                                                                                                                                                                          |

A `DUPLICATE_TRANSFER` **without** `details` is the plain idempotency mismatch
(`This idempotencyKey was already used for a different transfer`) or a ledger entry that
already completed (`Transfer already completed`). Neither is retryable with that key.

### 202 reconciliation pending

A withdrawal-destination transfer in live mode crosses the payment provider boundary. If
the provider accepted the payout but SideShift could not finalise its own record in the
same request, you receive:

```json theme={"system"}
{
  "success": false,
  "error": {
    "code": "TRANSFER_FAILED",
    "message": "Transfer was accepted and is awaiting local reconciliation. Retry only with the same idempotencyKey."
  }
}
```

with HTTP status `202`. A variant message, `Transfer outcome is awaiting reconciliation.
Retry only with the same idempotencyKey.`, covers the case where the provider's answer was
lost. In both cases the source debit is kept, the money may already have reached the
destination, and SideShift's reconciliation will settle the record.

Treat 202 as in-flight: poll `GET /transfers/{transferId}` for `status: "completed"`, or
retry with the **same** `idempotencyKey`. Never issue a new key for the same payment; that
is the one way to pay twice.

## Webhooks

| Code                      | Status | When                                                                                             | What to do                             |
| ------------------------- | ------ | ------------------------------------------------------------------------------------------------ | -------------------------------------- |
| `WEBHOOK_NOT_CONFIGURED`  | 400    | Replay was requested but no webhook URL is configured for this mode, or the webhook is disabled. | Configure the endpoint in the console. |
| `WEBHOOK_DELIVERY_FAILED` | 502    | Your endpoint did not accept a replay or a test delivery.                                        | Fix the endpoint, then replay again.   |

## Generic

| Code             | Status | When                                                                                                                                       | What to do                                                                                   |
| ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| `INTERNAL_ERROR` | 500    | An unexpected server error, including `Failed to reserve funds for transfer` and a hosted-checkout amount outside 100 to 20,000,000 cents. | Retry with the same idempotency key after a delay. Validate the checkout amount client-side. |

## Codes defined but not returned

`EmbedErrorCodes` also declares `TOKEN_REVOKED`, `ACCOUNT_CREATION_FAILED`,
`INVALID_TRANSFER_DIRECTION`, `TRANSFER_ACCOUNTS_REQUIRED`, `ESCROW_NOT_ENABLED`,
`WEBHOOK_URL_INVALID` and `PAYMENT_API_ERROR`. No endpoint emits them today. Direction
problems surface as `VALIDATION_ERROR` or `MISSING_REQUIRED_FIELD`, and payment-provider
failures as `PAYMENT_ACC_CREATION_FAILED`, `TRANSFER_FAILED` or `INTERNAL_ERROR`. Handle
them generically if you build an exhaustive switch.

## Widget error messages

The widget renders failures as a full-panel state with the raw reason as its message, so
the string your user reads is the exact string below.

<AccordionGroup>
  <Accordion title="Access Denied - Token domain mismatch">
    Every token is bound to a single domain at mint time: the `targetDomain` you passed,
    otherwise the `Origin` of the request that minted it, otherwise the first entry on
    your allowlist. That last fallback is the trap: a token minted from a backend that
    sent no `Origin` and no `targetDomain` is bound to whichever domain sits first on your
    list.

    When the widget loads, the page it is framed in is compared against that binding. A
    mismatch is not fatal on its own: the check then falls back to your allowlist and
    passes if the embedding domain is on it. `localhost` and loopback addresses always
    pass. You only see the error when both checks fail.

    Two fixes, and the first is usually the right one:

    * **Add the domain to your allowed domains.** This fixes existing tokens without
      re-minting them, because the fallback check reads the list live.
    * **Pass `targetDomain` when minting**, set to the domain you are embedding on.

    The usual near-miss is `www`: a bare `example.com` entry does not cover
    `www.example.com` at widget load, which needs its own entry or `*.example.com`.
  </Accordion>

  <Accordion title="Access Denied - Token expired, Session expired, Session not found or revoked, Invalid token">
    Four causes, one remedy. `Token expired` is the token passing its expiry. `Session
            expired` is the server-side session record passing its expiry while the token still
    verifies. `Session not found or revoked` means the record is gone. `Invalid token` is
    a token that does not verify at all, usually a truncated or double-encoded URL.

    Handle the `session:expired` event, mint a fresh token, and reload the frame.
  </Accordion>

  <Accordion title="Access Denied - Failed to initialize payment system">
    The widget could not obtain a session with the payment provider for this account.
    This is transient in most cases; reload. If it persists for one account, the account's
    payment account may be missing; contact support with the `sideshiftAccountId`.
  </Accordion>

  <Accordion title="Widget Not Available - the payout or payin widget is not enabled">
    Your integration's permission for that widget is off. Minting a token for it fails
    earlier with `403 WIDGET_NOT_PERMITTED`, so this state usually means a token minted
    with `widgetType: "both"` was used for the widget that is disabled. This is an
    integration setting and needs SideShift to change it.
  </Accordion>

  <Accordion title="Account Setup Required, or the account belongs to a different integration">
    Payment operations through a widget require the account to have been created through
    the Connect API by the integration whose token is presenting it. The underlying codes
    are `NOT_EMBED_ACCOUNT` (the account exists but was not created this way),
    `INTEGRATOR_MISMATCH` (created by a different integration) and `TOKEN_USER_MISMATCH`
    (the token is for a different account). An account merely linked to your integration
    can be read but cannot transact through the widget; it is shown a prompt to withdraw in
    the SideShift app instead.
  </Accordion>

  <Accordion title="Camera or microphone blocked during verification">
    Identity verification needs the camera, and an iframe only gets it if the parent
    grants it. Set `allow="payment; camera; microphone"` on the iframe. Without it the
    browser blocks capture inside the frame while the same flow works in a full tab, which
    makes it look like a verification problem rather than an embedding one.
  </Accordion>
</AccordionGroup>
