Skip to main content
An integration is shaped by a handful of settings: the keys that authenticate it, the domains its widgets may render on, which widgets it may use, the limits it runs into under load, and the webhook endpoints it reports to. Most live in the Connect console at app.sideshift.app/connect?tab=widgets. Domains can also be read and written from your own backend. The API base URL is https://app.sideshift.app/api/embed.

API keys

A key is a prefix plus 32 characters of base64url:
The random part is base64url, so it can contain - and _. If you validate keys before sending them, do not write a regex that only accepts letters and digits. Send the key in the x-api-key header on every request:
Keys are shown once, at creation. SideShift keeps a SHA-256 hash and a masked preview, not the key, so a lost key cannot be recovered. It can only be revoked and replaced.

Several keys at once

A company can hold more than one active key per mode. Each has its own id, name and creation time, and each is revoked independently, so rotation is a two-step operation rather than a cutover:
1

Create the new key

Both keys now authenticate. Nothing breaks while you roll the new value out.
2

Deploy it everywhere

Update every service, worker and scheduled job that talks to Connect.
3

Revoke the old key

Revocation takes effect on the next request that presents it.
Revoking without naming a key revokes every key in that mode. That is the right move if you believe a key has leaked and would rather break your own traffic than leave it valid. Live and sandbox keys are separate credentials against separate balances. A sandbox key cannot move real money, so develop against it and keep the live key out of anything but production.
The key is a bearer credential with full access to your integration. Never ship it to a browser, a mobile binary, or anything a user can read. Mint short-lived widget tokens on your backend instead, as described in Embedding and customization.

Allowed domains

The allowlist names the domains your widgets may render on. It is also the pool a widget token is bound to when you mint one without targetDomain.

How patterns match

A leading *. is the only wildcard, and it is more generous than it looks: it matches the base domain itself and subdomains at any depth. *.example.com already covers example.com, and you do not need a second entry for nested subdomains. That generosity is the reason to be careful with shared hosting: *.vercel.app allows every tenant of that platform, not just yours. Matching is case-insensitive. Stored entries are lowercased and de-duplicated. The www and non-www forms of a domain are treated as twins when a targetDomain is checked at token mint.

What is rejected

Entries are validated on write, and an invalid entry fails the whole request:
  • A scheme, port or path is rejected, not stripped. https://pay.example.com, pay.example.com:3000 and pay.example.com/app all return 400. Send bare hostnames.
  • Every entry must contain a dot, so localhost and *.com are both rejected.
  • The wildcard must lead. pay.*.com and a bare * are not patterns.
  • The list may not exceed 250 entries.
localhost cannot be listed and does not need to be. A widget loaded from localhost, 127.0.0.1, 0.0.0.0 or ::1 passes the domain check on any port, and an API request carrying one of those as its Origin is accepted once the key itself is valid.

Managing the list from your backend

GET /api/embed/domains returns the current settings. PATCH /api/embed/domains changes them and takes at least one of allowedDomains, addDomains, removeDomains or allowAllDomains.
  • allowedDomains replaces the whole list and cannot be combined with addDomains or removeDomains; sending both returns 400.
  • When addDomains and removeDomains arrive together, additions are applied first, so a domain in both ends up removed.
  • removeDomains matches the exact stored string. Removing *.client.com deletes that wildcard entry and leaves pay.client.com in place if you added it separately.
  • allowedDomains: [] is a valid way to clear the list. addDomains: [] or removeDomains: [] on their own return 400, since neither would change anything.
  • The response is the full settings object as stored, not an echo of what you sent.
allowAllDomains: true turns the domain check off entirely. Tokens are then bound to * and accepted from any origin, and browser requests no longer need an Origin header. It exists for cases with no meaningful origin, such as native app WebViews. It is not a fix for a stubborn Token domain mismatch; add the specific domain instead.
The allowlist controls where your widgets may render. It is checked when a widget page loads and on API requests that arrive with a browser Origin or Referer. A server-to-server call carries neither, so the allowlist never constrains your own backend; the API key is what authenticates there. Treat short-lived, server-minted tokens as the control over who may use a session, and do not lean on the allowlist as the only thing standing between a leaked token and a hostile page.

Widget permissions and other integration settings

Some settings are on the integration itself and are changed by SideShift or in the console rather than per request:

Webhooks

Live and sandbox webhook endpoints, their secrets and subscription lists are configured in the console. The events, headers, retry rules and the log, replay and test endpoints are documented in full on Webhooks.

Rate limits

All three are counted per company and separately for live and sandbox, so sandbox testing cannot exhaust your production budget. The per-minute request limit is configurable per integration. Token generation keeps its own hourly counter on top of the per-minute one, so minting tokens in a burst can hit that limit while your other calls are fine. Mint one token per widget session, not per page render. The daily transfer count is a durable per-day reservation taken inside the transfer transaction, so it holds across instances and restarts. Exceeding it returns 429 RATE_LIMITED with the message Transfer rate limit exceeded. Please try again later.

When you are limited

You get 429 with the standard envelope, and the seconds until reset in the message:
Do not build your backoff around response headers. The Connect API does not return Retry-After, and the X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset values the limiter computes are not forwarded on Connect responses. The 429 status and the seconds named in the message are what you have.
Back off on 429, add jitter so a fleet of workers does not retry in lockstep, and always send an idempotencyKey on transfers so a retry cannot double-pay.

Per-transfer amounts

The minimum transfer is 0.01(‘amountCents:1‘).Themaximumisthelowerofyourintegration′sconfiguredceilingandthesystemmaximumof0.01 (`amountCents: 1`). The maximum is the lower of your integration's configured ceiling and the system maximum of 100,000 (amountCents: 10000000). The configured ceiling defaults to 200,000,sounlessSideShifthassetaloweroneforyoutheeffectivemaximumis200,000, so unless SideShift has set a lower one for you the effective maximum is 100,000. In a best-effort batch, read the same fields from each failed item at data.results[].error.details.

Behaviours worth knowing

These are the ones that get mistaken for bugs in your own code. *.example.com already covers example.com, and a.b.example.com too. If you assumed one subdomain level, your list is broader than you think. A domain with a scheme or port is rejected, not cleaned up. https://pay.example.com fails validation instead of being stored as pay.example.com. allowedDomains and addDomains cannot be sent together. The request fails rather than merging them. The token-per-hour limit is separate from the per-minute limit. A hundred tokens in one minute is under the request limit and over the token limit. Rate-limit headers are not there to read. Handle the 429 itself.

Multi-currency opt-in

Multi-currency is off by default. From your backend, use GET /settings/currencies to read it and PATCH /settings/currencies with {"enabled":true} or {"enabled":false} to change it. Your Connect API key selects the brand and environment. Live and sandbox preferences are separate. Read the returned enabled value: multiCurrencyEnabled is the saved brand preference, while rolloutAvailable reports platform availability. Existing foreign balances remain accessible after opt-out. See the complete multi-currency guide for examples, units, funding, conversion, payout behavior, and retries. In Widgets, turn on Allow parent or guardian verification to offer this option in new individual KYC sessions. The setting is allowKycOnBehalf and is off by default. It cannot be enabled through a widget URL. The adult selects Parent or Legal guardian, enters the creator’s name and date of birth, and confirms their relationship. The rest of the identity form uses the adult’s details. The creator must be under 18 and the adult must be at least 18. This records a stated relationship; it does not replace identity or payout checks. Your server can read the record with your API key:
data.guardian.declaration contains the relationship record, or null when none exists. data.guardian.currentIdentityAndDestination contains the current stored identity and payout-method summary, or null when no matching verification record is available. The stored verification ID must match the declaration’s latest verification ID. This summary can lag provider updates. Use withdrawal events for the destination used by a specific withdrawal. The guardian record is available only to the company that owns it. Sandbox records are not created. Changing an existing guardian identity requires support. Turning the setting off stops new guardian submissions and keeps saved records available. An accessible account with no declaration for your company returns:
An inaccessible account, a sandbox request, or an account without a provider account returns HTTP 404. A company linked to an account cannot read another company’s relationship declaration.