Skip to main content
These are the parts of Connect you reach for after the basics work. Setup is on Getting started, the sandbox and test plan are on Testing, and the event contract is on Webhooks.

Escrow mode

By default the pay-in widget funds the user’s wallet. Escrow mode turns it into a way for your users to pay you: deposits made through the widget are credited to your company wallet, and you learn who paid from the webhook. Mint the token with escrowMode: true:
  • Escrow is a pay-in feature. widgetType must be payin or both; anything else returns 400 VALIDATION_ERROR with Escrow mode is only available for payin widgets.
  • Funds go to your company wallet unless you pass escrowDestinationCompanyId, which must name an existing SideShift company or the mint fails with 400 ESCROW_DESTINATION_INVALID.
  • The depositing user’s sideshiftAccountId (and externalId) appear on the deposit.pending, deposit.confirmed and deposit.failed events, so you can attribute the payment. netAmountCents is what landed in your wallet; amountCents includes the processing fee the payer was charged on top.
  • New sessions use Whop for all pay-ins. Set threeDsLevel on the token request to mandate_challenge, mandate_if_required, frictionless_if_required, or null. The default is frictionless_if_required; null also uses frictionless 3DS. This controls card setup. Payment-mode checkouts use their plan policy. Whop can still require authentication for risk or recovery. For payments of $1,000 or more, Whop uses mandate_if_required unless mandate_challenge is selected.
  • allowFraudBypass is deprecated. It maps to the default Whop preference on new sessions; an explicit threeDsLevel takes precedence. Existing Stripe sessions can finish until their original expiry (at most 24 hours). Cards saved only in Stripe must be added again in Whop. Existing Whop cards remain available.
In sandbox, escrow pay-in accepts cards only; use the test cards.

Hosted checkout

A hosted checkout is a public payment page (or an embeddable checkout) that settles into a SideShift wallet, created from your backend with one call. Use it when you want to take a payment without embedding the pay-in widget, or to charge someone who does not have a Connect account at all.

Create a session

The processing fee is added on top of amountCents: the payer is charged totalChargedCents and the wallet is credited amountCents.

Hosted page or embedded

Redirect the payer to url for the full-page checkout. Once the payment is verified the page shows a receipt and, if you set successUrl, redirects there. To embed instead, render embedUrl in an iframe on your own page. When SideShift has verified the payment, the embedded checkout posts a message to the parent window:
The message is posted to any origin, so check event.origin is https://app.sideshift.app before acting on it. Treat it as a UI signal; the source of truth is the webhook.

Confirming payment

Two server-side signals confirm a session was paid:
  • Webhook. deposit.confirmed arrives with metadata.source: "connect_hosted_checkout", metadata.checkoutSessionId and your session metadata. depositId is the session id and sideshiftAccountId is the destination wallet.
  • Public read. GET https://app.sideshift.app/api/connect/checkout/{sessionId} needs no key and returns the session’s public view, including status (open, paid, expired or cancelled), the amounts, description, companyName and the redirect URLs. An unknown id returns 404 with { "success": false, "error": "Checkout session not found", "code": "NOT_FOUND" }.
In sandbox the checkout runs on the payment provider’s sandbox and accepts cards only.

Withdrawal fee markups

When a user withdraws, the fee they pay on each payout rail has two layers: SideShift’s platform fee, and a markup you can add on top. Your layer is charged to the user as part of one combined fee and paid to your SideShift wallet. Markups are set per rail in the Connect console under withdrawal fees. Each rail takes a percentage_fee (at most 10) and a fixed_fee_usd (at most 100); either may be left empty. How it works:
  • The payment provider holds one markup per rail on each connected account, so SideShift pushes the sum of its layer and yours. Every account you create gets the combined markup applied at creation; changing your markup re-syncs existing accounts.
  • When a withdrawal reports its markup fee, SideShift splits it back into the two layers proportionally to the configured rates. Your share is floored and SideShift absorbs the rounding cent. If the charged fee deviates from what the two layers imply by more than 2 cents or 2 percent, or the rail cannot be identified, the split is refused and nothing is paid onward.
  • Your share is reported as markupRevenueCents on the withdrawal.* events, only to the integration that owns the account. It is provisional until the withdrawal reaches completed, when it is credited to your wallet. A withdrawal that fails, is canceled or is denied before completing credits nothing; one that fails after completing reverses the credit.
The cap is on your layer alone. SideShift’s layer sits underneath it, so the fee the user sees can exceed the cap by SideShift’s rate.

White-label domains

A platform that hosts a payout widget on each client’s own domain has two things to get right: every client hostname must be on your allowlist, and each token must be bound to the hostname it will be embedded on.
1

Register the client's hostname when they attach it

From your onboarding backend, as soon as the CNAME is set:
The list holds up to 250 entries. Prefer exact hostnames over wildcards on shared infrastructure; *.yourplatform.app is fine, *.vercel.app is not.
2

Bind each token to the embedding hostname

Pass targetDomain when minting, set to the domain the widget will load on:
A targetDomain that is not on the allowlist is rejected at mint time with 400 DOMAIN_NOT_ALLOWED, so this is a stricter version of the allowlist rather than a way around it. The www. and bare forms of a hostname are treated as twins.
3

Remove hostnames when clients leave

removeDomains matches the exact stored string:
Why targetDomain matters: without it, a token minted from a backend that sent no Origin is bound to whichever domain happens to be first on your list. When the widget loads on a different client’s domain, the token’s binding does not match, and the widget falls back to checking your allowlist. That fallback passes as long as the domain is listed, so you would rarely notice, but it means the binding is not doing any work. Binding every token to its own client’s domain makes a leaked token useless anywhere else. GET /api/embed/domains returns the current list, which lets your onboarding backend verify what a client’s CNAME is registered as.

Moving funds to the withdrawal balance (deprecated)

Transfers settle into the destination’s withdrawable balance, so this call is only needed for money that sits in an account’s internal SideShift wallet: a pay-in through the widget or an invoice, or a transfer sent with the deprecated destinationBalance: "wallet". POST /accounts/withdrawal-balance moves that money into the withdrawable balance. The endpoint keeps working for existing integrations; new ones should not need it.
  • idempotencyKey is required and must be at least 8 characters, the same as a transfer.
  • The account must have been created by your integration; a linked account returns 403 WIDGET_NOT_PERMITTED.
  • The amount follows the per-transfer limits.
  • The account must have completed identity verification. If it has not, the call returns 400 TRANSFER_FAILED with the message Account must complete identity verification before funds can be moved into the withdrawal balance.
  • Insufficient wallet funds return 400 INSUFFICIENT_BALANCE; an account with no withdrawal account yet returns 400 ACCOUNT_NOT_FOUND.
  • There is no fee, and no sandbox implementation: this call reaches the payment provider directly, so test it with a live key and a small amount.

Account fallback emails

Every SideShift account has a unique email. When you create a Connect account for an address that already belongs to a SideShift user outside your integration, SideShift cannot hand you that user, so by default it provisions a separate account under a fallback address instead. enableFallbacks defaults to true. With it on, POST /accounts/create tries in order:
  1. externalId, if you sent one and an account with it exists under your integration. That account is returned regardless of email.
  2. The email itself, if an account with it exists under your integration. Returned with 200.
  3. Fallback slots. For each numbered slot SideShift first tries a provider alias of the original address (jane+1@example.com), then a deterministic SideShift-managed address on sideshift.app. An existing account under your integration at either address is reused; otherwise a new one is created there. A run examines at most 100 slots and stops after 3 payment-account provisioning failures.
A fallback response carries emailModified: true and originalEmail (your normalised request email) alongside the resolved email:
The same machinery runs when the payment provider rejects the original address with a classified email collision, validation or suspended-account error. A suspended-account fallback creates a separate managed account; it does not restore or merge the suspended one. Store sideshiftAccountId and the resolved email. Send the original email and a stable externalId on later calls; the externalId lookup returns the same account without re-running the fallback search.

Sub-account currency conversion (pilot)

Multi-currency is off by default. Brands opt in through the API, separately for live and sandbox. Fund managed sub-accounts in USD, then convert within those accounts. The multi-currency guide covers activation, funding estimates, precision, conversion, payouts, opt-out, and safe retries with complete request and response examples.

Duplicate-account checks

Call POST /api/embed/accounts/check from your backend with exactly one of email, externalId, or sideshiftAccountId:
Only your own managed accounts in the key’s environment are checked. No KYC documents, identity profile identifiers, or bank details are returned. exists includes unverified accounts; isKycVerified is a separate stored verification fact, not permission to withdraw. An approved identity remains verified if the payment account has an open information request. If a selector matches multiple accounts, a positive flag wins. Email matches the stored Connect email; use the external ID or SideShift account ID when creation used a fallback email. For an Earn-style signup flow:
  1. Check your stable externalId or email before provisioning to reuse an existing account.
  2. Otherwise create a Connect account and have the user complete verification in the payout widget.
  3. Check again using its sideshiftAccountId. A hasDuplicateVerifiedIdentity: true result means another account in your integration is verified against the same identity profile. Route that signup to your duplicate-account review instead of granting access.
  4. Store the SideShift account ID against your user with a unique constraint. Subscribe to account.risk_flagged for duplicate identities detected after onboarding, and recheck before enabling spending or rewards.
null means unknown, not false. Wait or review when identity/country data is unavailable; retry failed requests instead of treating them as no match. Verification data is synchronized by provider events and can lag. A new email cannot identify the person behind it before KYC, and separate provider identity profiles are not guaranteed to identify the same person. This check is read-only, does not reserve an identity, and does not provide atomic uniqueness across concurrent signups. Your integration must enforce its onboarding and access policy. geoRestricted applies the existing payment-region country list and your company’s restricted-region override to the stored KYC country. It does not use the IP of your API server, check VPN usage, or change Connect’s withdrawal rules. Missing country yields null unless your company has an override. Sandbox matches only sandbox accounts and returns isKycVerified: false with both duplicate and geo flags null.