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

# Getting started

> Get a key, create an account, mint a token, and embed your first widget.

SideShift Connect puts a wallet inside your own product.
You create a Connect account for each of your users, move money in and out of it over the API, and drop in a pre-built payout or pay-in widget so the user never leaves your page.
Everything runs through one REST API at `https://app.sideshift.app/api/embed`, authenticated with a single server-side key.

## Before you start

Connect is off by default.
Until SideShift enables it for your company, every call returns `403 EMBED_NOT_ENABLED` and asks you to contact support, so check this first rather than last.

You also need a payment account on your company before you can generate a key at all.
Until that exists, the settings page shows a "Payment account required" prompt with a setup button where the key controls would be.

## API keys

Keys live in [Settings → Embed Widgets](https://app.sideshift.app/settings?tab=embed), under **API Keys**.
There are two independent slots, **Live** and **Sandbox**, each with its own Generate button.

| Prefix     | Slot    |
| ---------- | ------- |
| `sk_live_` | Live    |
| `sk_test_` | Sandbox |

Send the key in the `x-api-key` header on every request:

```bash theme={"system"}
curl https://app.sideshift.app/api/embed/accounts \
  -H "x-api-key: sk_test_YOUR_KEY"
```

SideShift stores only a SHA-256 hash of the key plus a short preview for display.
That is why a key is shown exactly once when you generate it and cannot be recovered afterwards - nobody, including SideShift, can read it back.
Generating again replaces the key in that slot.

<Warning>
  The key is a server-side credential with full access to your accounts and transfers.
  It must never reach the browser.
  Widgets authenticate with short-lived tokens instead, precisely so the key can stay on your backend.
</Warning>

### What the prefix actually changes

The prefix is the whole switch.
The base URL, the paths, the request bodies and the response shapes are identical in both modes, and the two keys are stored against separate fields on your company, so a sandbox key can never authenticate as a live one.

What does change behind it:

* Balances are written to a separate sandbox ledger and a separate wallet, so test money and real money never mix.
* Payment accounts are simulated, and the `paymentAccountId` you get back is prefixed `sim_biz_` rather than being a real provider account.
* A newly created sandbox account is seeded once with \$10,000 of test funds, so you can transfer straight away without funding anything.
* Withdrawal does not work in sandbox at all. A sandbox account has no real payment account behind it, so the withdrawable balance reads as zero and a withdrawal attempt fails with `404 NO_WHOP_ACCOUNT`. Test the withdrawal flow against a live key with a small amount. See [Guides](/connect/guides) for what sandbox does and does not cover.
* Rate limit counters are tracked separately for the two modes, so load-testing in sandbox does not eat your live budget.

Start on `sk_test_` and switch the environment variable when you go live.

## Your first integration

<Steps>
  <Step title="Create an account for your user">
    ```bash theme={"system"}
    curl -X POST https://app.sideshift.app/api/embed/accounts/create \
      -H "x-api-key: sk_test_YOUR_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "email": "jane@example.com",
        "name": "Jane Creator",
        "externalId": "usr_123"
      }'
    ```

    Only `email` is required.
    Send `externalId` anyway: it is checked before the email lookup, so the same `externalId` keeps returning the same account even if the user later changes their email address.
    Without it, one person with two addresses becomes two separately payable Connect accounts.

    You get `201` on creation and `200` when the account already existed, with the same body either way:

    ```json theme={"system"}
    {
      "success": true,
      "data": {
        "sideshiftAccountId": "acct_a1b2c3d4e5f6",
        "paymentAccountId": "sim_biz_x7y8z9",
        "email": "jane@example.com",
        "name": "Jane Creator",
        "created": true
      }
    }
    ```

    Every Connect response is wrapped in `{ "success": ..., "data": ... }`, so read `data.sideshiftAccountId` rather than the top level.
    Store that id - it is the handle for everything else.
  </Step>

  <Step title="Allow the domain you will embed on">
    Back in Settings → Embed Widgets, add the domains the widget is allowed to load on.
    A pattern matches exactly, or as `*.example.com` for the base domain and any subdomain.

    This is not optional bookkeeping.
    Every token is bound to one domain when it is minted, and the endpoint picks that domain from `targetDomain` in the body if you send one, otherwise the request's `Origin` header, otherwise the first entry on your allowlist.
    A server-side `curl` sends no `Origin`, so with an empty allowlist there is nothing left to bind to and the next step fails with `400 DOMAIN_NOT_ALLOWED`.
  </Step>

  <Step title="Mint a widget token">
    ```bash theme={"system"}
    curl -X POST https://app.sideshift.app/api/embed/auth/token \
      -H "x-api-key: sk_test_YOUR_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "sideshiftAccountId": "acct_a1b2c3d4e5f6",
        "widgetType": "both",
        "expiresInSeconds": 3600
      }'
    ```

    `sideshiftAccountId` and `widgetType` are both required.
    `widgetType` is `payout`, `payin`, or `both`.

    ```json theme={"system"}
    {
      "success": true,
      "data": {
        "accessToken": "eyJhbGciOiJIUzI1NiIs...",
        "expiresAt": "2026-08-20T13:00:00.000Z",
        "expiresInSeconds": 3600,
        "widgetUrls": {
          "payout": "https://app.sideshift.app/widget/payout/eyJhbGciOiJIUzI1NiIs...",
          "payin": "https://app.sideshift.app/widget/payin/eyJhbGciOiJIUzI1NiIs..."
        }
      }
    }
    ```

    `expiresInSeconds` defaults to 3600 and accepts 60 to 86400.
    A value outside that range is rejected with a `400` rather than clamped to the nearest bound, so range-check it before sending instead of assuming a too-large number will be trimmed.

    <Note>
      The account has to belong to your integration.
      An id you did not create or link comes back as `404` "Account not found", not `403`.
      That is deliberate: a 403 would confirm the account exists, which would turn this endpoint into a way to probe for accounts on other platforms.
    </Note>

    Token generation is rate limited per company and per mode, defaulting to 100 per hour.
    Mint one token per widget session rather than one per page render.
  </Step>

  <Step title="Embed the widget">
    Use `widgetUrls` from the response directly as the iframe `src`.

    ```html theme={"system"}
    <iframe
      src="https://app.sideshift.app/widget/payout/{ACCESS_TOKEN}"
      width="100%"
      height="500"
      frameborder="0"
      allow="payment; camera; microphone"
      title="SideShift Payout Widget"
    ></iframe>
    ```

    The `allow` attribute matters, because identity verification uses the camera and card flows may need payment permissions.

    The token sits in the URL path, so anything that logs full URLs logs a live credential.
    That is the other reason to keep lifetimes short and mint the token on your backend at the moment you render the page.
  </Step>
</Steps>

## Where to go next

<Card title="Embedding and customization" icon="palette" href="/connect/widgets">
  Sizing, overlays, the full set of theme and label parameters, and the events the widget posts back to your page.
</Card>

<Card title="Playground" icon="play" href="/connect/playground">
  Try the widget parameters interactively and copy out the generated snippet.
</Card>
