Skip to main content
Every failure from https://app.sideshift.app/api/embed uses one envelope:
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

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 below.

Validation

Accounts

Transfers

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. 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:
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

Generic

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.
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.
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.
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.
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.
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.
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.