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

# Send a test webhook

> Deliver one synthetic event to your **sandbox** webhook endpoint, signed with your sandbox secret and
recorded in `GET /webhook-logs` exactly like a real delivery. Use it to exercise every handler before
going live. Live keys receive `403`.

Every event type you can subscribe to is supported, and the event is delivered whether or not your
sandbox endpoint is subscribed to it, so a handler can be tested before the subscription is switched on.
The payload has the same shape as the real event; `metadata.test` is `"true"` and `metadata.sandbox` is
`"true"` so a receiver can tell a rehearsal apart. Fields that do not apply to the chosen event are ignored.




## OpenAPI

````yaml /openapi/connect.yaml post /webhooks/test
openapi: 3.1.0
info:
  title: SideShift Connect
  version: 1.0.0
  description: >
    Embed payment infrastructure directly into your platform. Create accounts
    for your users, transfer funds in any direction, and drop in pre-built
    payout and pay-in widgets — all through a single API.


    ## Setup Guide


    ### 1. Generate an API Key


    Go to [Settings → Connect](https://app.sideshift.app/settings?tab=embed) and
    click **Generate API Key**.


    Copy it immediately — keys are only shown once.


    - `sk_live_*` — Production (real money)

    - `sk_test_*` — Sandbox (isolated balances, simulated payouts)


    > **Never expose your API key in client-side code.** All API calls must be
    made from your backend.


    ### 2. Add Allowed Domains


    In Settings → Connect, add the domains where you'll embed widgets:


    | Pattern | Matches |

    |---------|---------|

    | `app.example.com` | Exact match |

    | `*.example.com` | All subdomains |

    | `localhost` | Any port (auto-allowed for development) |


    ### 3. Create User Accounts


    Every user who needs access to payments needs a SideShift Connect account:


    ```bash

    curl -X POST https://app.sideshift.app/api/embed/accounts/create \
      -H "x-api-key: sk_live_YOUR_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "email": "jane@example.com", "name": "Jane Creator", "externalId": "usr_123" }'
    ```


    Store the returned `sideshiftAccountId` — you'll need it for everything
    else.


    ### 4. Generate a Widget Token


    Tokens authenticate embedded widget sessions. Generate them server-side:


    ```bash

    curl -X POST https://app.sideshift.app/api/embed/auth/token \
      -H "x-api-key: sk_live_YOUR_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "sideshiftAccountId": "acct_abc123", "widgetType": "both" }'
    ```


    The response includes `widgetUrls.payout` and `widgetUrls.payin` — use these
    as iframe sources.


    ### 5. Embed the Widget


    **iframe (recommended):**

    ```html

    <iframe
      src="WIDGET_URL"
      width="100%" height="500"
      frameborder="0"
      allow="payment; camera; microphone"
      style="border:0; border-radius:12px"
    ></iframe>

    ```

    The iframe is the most reliable method — it works in any framework, needs no
    build tooling, and avoids dependency conflicts. The `allow="payment; camera;
    microphone"` attribute is required for KYC/identity verification inside the
    widget.


    **White-label verification and withdrawals:**

    The payout widget uses custom, theme-aware identity-verification,
    withdrawal, fee, payout-method, history, and account-reset interfaces. It
    does not render provider UI or SideShift product artwork. The KYC title,
    progress rail, and withdrawal title can be removed independently with widget
    URL query parameters:

    ```html

    <iframe
      src="WIDGET_URL?showKycHeader=false&showKycSteps=false&showWithdrawHeader=false&kycButton=Verify%20identity&kycTitle=Identity%20check&kycVerifiedTitle=Identity%20confirmed"
      width="100%"
      height="500"
      frameborder="0"
      allow="payment; camera; microphone"
    ></iframe>

    ```

    Labels use the same camel-case names shown above and should be URL-encoded.


    ### 6. Transfer Funds


    Move money between your company and user accounts:


    ```bash

    curl -X POST https://app.sideshift.app/api/embed/accounts/transfer \
      -H "x-api-key: sk_live_YOUR_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "toAccountId": "acct_abc123",
        "amountCents": 5000,
        "idempotencyKey": "payout-001",
        "metadata": {
          "obligationType": "creator_agreement",
          "obligationReference": "agreement-001",
          "description": "Approved payment for completed creator deliverable",
          "approvalReference": "approval-001"
        }
      }'
    ```


    ### 7. Set Up Webhooks


    Configure a webhook endpoint in Settings → Connect to receive
    `transfer.completed`, `deposit.*`, and `withdrawal.*` events. Always verify
    the signature:


    ```js

    const crypto = require("crypto");

    function verify(payload, timestamp, signature, secret) {
      const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${payload}`).digest("hex");
      return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
    }

    ```


    ---


    ## Authentication


    Include your API key in the `x-api-key` header on every request:


    ```

    x-api-key: sk_live_your_key_here

    ```


    Rotate your key anytime from Settings → Connect. The old key is invalidated
    immediately.


    ## Sandbox


    Use `sk_test_*` keys to test without moving real money. Sandbox balances,

    credentials, provider resources, and webhook configuration are isolated from
    live.

    SideShift keeps the documented API contract (validation, error envelopes,
    and response

    shapes) stable where a sandbox operation is supported; this does **not**
    mean every

    production provider capability has sandbox parity.


    ### Sandbox behavior


    - New connected accounts are provisioned through the payment provider's
    official sandbox API and receive
      sandbox `biz_*` provider IDs. Existing `sim_biz_*` accounts are migrated lazily the
      next time an account or widget-token flow needs the provider account; integrations
      should continue using the stable `sideshiftAccountId`.
    - Pay-in widgets and hosted checkout support **card payments only** in
    sandbox.
      Alternative payment methods are production-only.
    - Company → User, User → Company, and User → User transfers remain isolated
    simulations
      in SideShift's sandbox ledger.
    - Payout, withdrawal, and bank-link actions are blocked in sandbox because
    the payment provider does not
      support payouts there. A stable API contract cannot make an unsupported provider rail
      executable.
    - Sandbox webhook events are delivered only to the separately configured
    sandbox webhook.
      SideShift never falls back to the live webhook URL or secret.
    - Leaderboard and notification side effects are **not** triggered

    - Invoice behavior is unchanged and invoices are explicitly outside this
    sandbox
      certification scope.

    ### Payout widget in sandbox


    A token generated with a `sk_test_*` key uses the payment provider's sandbox
    environment. The widget may

    show safe account, verification, and balance status, but withdrawal,
    bank-link, payout

    destination, and payout submission controls are unavailable because the
    provider does

    not offer sandbox payouts.


    **In production** with `sk_live_*` keys, transfers call the real payments
    API, funds land in the creator's real wallet, and the balance + withdrawal
    UI works normally.


    **To verify sandbox transfers are working**, use the balance API — this is
    the source of truth:


    ```bash

    GET /accounts/balance?sideshiftAccountId=ACCOUNT_ID

    ```


    Use `walletBalanceCents` and `transactions` to verify SideShift
    sandbox-ledger transfers.

    `totalBalanceCents` combines the SideShift wallet and provider
    withdrawal-ready layers;

    it is not the authoritative value for sandbox transfer assertions while
    provider payouts

    are unsupported.


    ### Testing checklist


    1. Generate `sk_test_*` key and store securely

    2. Create at least two sandbox accounts

    3. Test all three transfer directions (company→user, user→company,
    user→user)

    4. Verify balances via the balance API after each transfer

    5. Replay a transfer with the same `idempotencyKey` — confirm no duplicate

    6. Configure a sandbox webhook, then confirm delivery and signature
    verification; verify
       that no event falls back to the live webhook
    7. Trigger error cases (insufficient balance, invalid account) and verify
    your handling

    8. Test widget token generation, card-only pay-in/checkout, and the blocked
    payout UI


    ### Go-live checklist


    Before switching to `sk_live_*`:


    1. Store your live API key in a production secrets manager

    2. Confirm production domains in Settings → Connect (remove dev wildcards)

    3. Verify webhook endpoint uses HTTPS and validates signatures

    4. Add retry handling with idempotency keys in your backend

    5. Attach commercial evidence metadata to every transfer

    6. Run a small live test (e.g. $0.50 transfer) before full volume


    ## Idempotency


    Always include an `idempotencyKey` on transfer requests. Replaying a request
    with the same key returns the original successful result instead of creating
    a duplicate.


    ## Rate Limits


    | Limit | Default | Scope |

    |-------|---------|-------|

    | Requests | 100 per minute | per API key mode (sandbox and live are counted
    separately) |

    | Widget tokens | 100 per hour | per API key mode |

    | Transfers | 1,000 per UTC day | per API key mode, reserved atomically with
    each transfer |

    | Daily transfer amount | none unless configured | `dailyTransferLimitCents`
    on your integration |


    Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and
    `X-RateLimit-Reset` (Unix seconds).

    Exceeding a limit returns `429` with error code `RATE_LIMITED`; the message
    states when to retry.


    ## Base URL


    `https://app.sideshift.app/api/embed`
  contact:
    name: SideShift Support
    url: https://app.sideshift.app
servers:
  - url: https://app.sideshift.app/api/embed
    description: Production / Sandbox (determined by API key prefix)
security:
  - apiKeyAuth: []
tags:
  - name: Accounts
    description: >-
      Create and manage user accounts. Each user gets a `sideshiftAccountId` and
      a wallet for receiving and sending funds.
  - name: Verifications
    description: >-
      Read identity verification status and required actions for connected
      accounts.
  - name: Transfers
    description: >-
      Move funds between your company and user accounts. Supports company→user,
      user→company, and user→user directions.
  - name: Tokens
    description: >-
      Generate short-lived access tokens that authenticate embedded widget
      sessions.
  - name: Domains
    description: Read and update the domains your embedded widgets are allowed to load on.
  - name: Checkout
    description: >-
      Create public hosted checkout links that settle into your SideShift
      wallet.
  - name: Webhooks
    description: >
      Receive real-time event notifications when transfers, deposits, and
      withdrawals complete or fail. Configure endpoints in Settings → Connect.


      **Available events:** `deposit.pending`, `deposit.confirmed`,
      `deposit.failed`, `transfer.completed`, `withdrawal.created`,
      `withdrawal.updated`, `withdrawal.completed`, `withdrawal.failed`,
      `account.risk_flagged`


      `withdrawal.completed` and `withdrawal.failed` are derived aliases of
      `withdrawal.updated`, filtered to `status === "completed"` and to `status`
      in (`failed`, `denied`) respectively. Subscribe to the pair if you only
      want the terminal outcome; subscribe to `withdrawal.updated` for the full
      lifecycle (`requested → awaiting_payment → in_transit → completed | failed
      | canceled | denied`).


      An endpoint registered without an explicit `events` array is subscribed to
      `deposit.pending`, `deposit.confirmed`, `deposit.failed`,
      `withdrawal.created`, `withdrawal.completed`, `withdrawal.failed` and
      `transfer.completed`. A later config save that omits `events` keeps the
      subscription list you last set.


      Every delivery carries `X-Sideshift-Signature` (HMAC-SHA256, hex, of
      `<timestamp>.<raw body>` using your endpoint secret),
      `X-Sideshift-Timestamp` (Unix seconds) and `X-Sideshift-Event-Id` (the
      same `id` as the body, for deduplication). A delivery is attempted up to
      three times in one dispatch, waiting one second and then two seconds
      between attempts, with a ten-second timeout per attempt; a `4xx` response
      other than `429` is not retried. Anything that still failed can be re-sent
      with `POST /webhooks/{eventId}/replay`, and every attempt is visible in
      `GET /webhook-logs`. Sandbox keys deliver only to the sandbox endpoint and
      secret; live keys only to the live ones.
paths:
  /webhooks/test:
    post:
      tags:
        - Webhooks
      summary: Send a test webhook
      description: >
        Deliver one synthetic event to your **sandbox** webhook endpoint, signed
        with your sandbox secret and

        recorded in `GET /webhook-logs` exactly like a real delivery. Use it to
        exercise every handler before

        going live. Live keys receive `403`.


        Every event type you can subscribe to is supported, and the event is
        delivered whether or not your

        sandbox endpoint is subscribed to it, so a handler can be tested before
        the subscription is switched on.

        The payload has the same shape as the real event; `metadata.test` is
        `"true"` and `metadata.sandbox` is

        `"true"` so a receiver can tell a rehearsal apart. Fields that do not
        apply to the chosen event are ignored.
      operationId: sendTestWebhook
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                eventType:
                  type: string
                  enum:
                    - deposit.pending
                    - deposit.confirmed
                    - deposit.failed
                    - transfer.completed
                    - withdrawal.created
                    - withdrawal.updated
                    - withdrawal.completed
                    - withdrawal.failed
                    - account.risk_flagged
                  default: deposit.confirmed
                sideshiftAccountId:
                  type: string
                  description: >-
                    Account id to place in the payload. Defaults to your company
                    id.
                externalId:
                  type: string
                amountCents:
                  type: integer
                  minimum: 1
                  default: 10000
                feeCents:
                  type: integer
                  minimum: 0
                  default: 0
                netAmountCents:
                  type: integer
                  minimum: 1
                  description: Defaults to `amountCents - feeCents`.
                depositId:
                  type: string
                  description: '`deposit.*` events only. Minted when omitted.'
                paymentId:
                  type: string
                  description: >-
                    The payment, transfer or withdrawal id to echo. Minted when
                    omitted.
                status:
                  type: string
                  enum:
                    - requested
                    - awaiting_payment
                    - in_transit
                    - completed
                    - failed
                    - canceled
                    - denied
                  description: '`withdrawal.updated` only. Defaults to `in_transit`.'
                metadata:
                  type: object
                  additionalProperties:
                    type: string
            example:
              eventType: withdrawal.updated
              status: in_transit
              amountCents: 25000
              feeCents: 250
      responses:
        '200':
          description: The event was delivered and your endpoint answered 2xx
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      eventType:
                        type: string
                      delivered:
                        type: boolean
                        enum:
                          - true
                      sandbox:
                        type: boolean
                        enum:
                          - true
                      depositId:
                        type: string
                        description: '`deposit.*` events only'
                      paymentId:
                        type: string
                      sideshiftAccountId:
                        type: string
                      amountCents:
                        type: integer
                      feeCents:
                        type: integer
                      netAmountCents:
                        type: integer
                      status:
                        type: string
              example:
                success: true
                data:
                  eventType: withdrawal.updated
                  delivered: true
                  sandbox: true
                  paymentId: pay_test_5f4c1a2e
                  sideshiftAccountId: acct_a1b2c3d4e5f6
                  amountCents: 25000
                  feeCents: 250
                  netAmountCents: 24750
                  status: in_transit
        '400':
          description: >-
            Invalid body (unknown eventType, non-integer amounts, non-string
            metadata)
        '403':
          description: A live key was used. Test webhooks are sandbox only.
        '502':
          description: >-
            No sandbox endpoint is configured, it is disabled, or it did not
            answer 2xx after every retry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmbedErrorEnvelope'
              example:
                success: false
                error:
                  code: WEBHOOK_DELIVERY_FAILED
                  message: No webhook URL configured
components:
  schemas:
    EmbedErrorEnvelope:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          enum:
            - false
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable error code
            message:
              type: string
              description: Human-readable error message
            details:
              type: object
              additionalProperties: true
              description: >-
                Machine-readable error context. Amount-limit failures include
                cent-denominated requested and effective limits.
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Your SideShift Connect API key (`sk_live_*` or `sk_test_*`). Generate
        from Settings → Connect.

````