https://app.sideshift.app/api/embed uses one envelope:
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: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.Access Denied - Token domain mismatch
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
targetDomainwhen minting, set to the domain you are embedding on.
www: a bare example.com entry does not cover
www.example.com at widget load, which needs its own entry or *.example.com.Access Denied - Token expired, Session expired, Session not found or revoked, Invalid token
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.Access Denied - Failed to initialize payment system
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.Widget Not Available - the payout or payin widget is not enabled
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.Account Setup Required, or the account belongs to a different integration
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.Camera or microphone blocked during verification
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.