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

# Batch transfer

> Process multiple transfers in a single request. Transfers are executed **sequentially** — use this instead of sending many parallel single-transfer requests.

Each transfer in the array supports the same parameters as the single transfer endpoint. Results are returned per-transfer: failures in one transfer do not affect the others.

**Why batch?** Sending 50+ parallel requests to `/accounts/transfer` causes Firestore transaction contention on the source wallet, resulting in `INTERNAL_ERROR` failures. The batch endpoint eliminates this by processing transfers one at a time within a single HTTP call.

**Limits:** Maximum 200 transfers per batch.

**Idempotency and evidence:** Every transfer requires its own `idempotencyKey`. Live batch items also require the same commercial-evidence metadata as the single-transfer endpoint.




## OpenAPI

````yaml /openapi/connect.yaml post /accounts/transfer/batch
openapi: 3.0.4
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 or pass them to the SDK.


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


    **npm SDK (alternative):**

    ```jsx

    import { SideShiftPayout } from '@sideshiftapp/connect/react';


    <SideShiftPayout
      token={token}
      theme={{ theme: 'light', primaryColor: '#3D8CFA', borderRadius: 12 }}
      onWithdrawCompleted={(data) => console.log('Withdrew', data.amountCents)}
      onSessionExpired={() => refreshToken()}
    />

    ```

    The SDK is a thin wrapper around the same iframe — you get typed props,
    auto-resize, and event callbacks. Also available as
    `@sideshiftapp/connect/vanilla`.


    **iOS (SwiftUI):**

    ```swift

    SideShiftConnect.configure(apiKey: "sk_live_YOUR_KEY")

    SideShiftPayoutView(accountId: "acct_abc123", currency: "USD")

    ```

    The iOS SDK manages tokens internally — no server-side token generation
    needed.


    ### 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 are
    fully isolated from live. Webhooks still fire so you can validate your full
    pipeline. All API endpoints behave identically (same validation, same error
    codes, same response shapes).


    ### Sandbox behavior


    - `paymentAccountId` values are prefixed with `sim_biz_*` (simulated)

    - Company → User transfers are simulated — no real payout is executed, but
    the internal ledger is updated normally

    - User → Company and User → User transfers work identically to production

    - Webhook events are delivered to your configured endpoint

    - Leaderboard and notification side effects are **not** triggered


    ### Payout widget in sandbox


    When a widget token is generated with a `sk_test_*` key, the payout widget
    automatically uses SideShift's sandbox payout environment. No additional
    configuration is required.


    > **Important:** The payout widget shows "Pending Balance from your
    platform" instead of the actual balance in sandbox mode. This is expected.


    When you transfer funds with a `sk_test_*` key, the sandbox wallet is
    credited correctly on the internal ledger. However, the payout widget's
    withdrawal UI cannot display the real balance because the company ID is
    simulated (`sim_biz_*`) and the widget relies on the real payments
    infrastructure to resolve balances.


    - The `passedInBalance` field (if set) appears as a display-only "Pending
    Balance" label

    - The withdrawal flow (bank account linking, payout initiation) is not fully
    functional in sandbox


    **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

    ```


    The `balanceCents` and `transactions` in the response accurately reflect all
    sandbox transfers.


    ### 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. Confirm webhook arrives and signature verification passes

    7. Trigger error cases (insufficient balance, invalid account) and verify
    your handling

    8. Test widget token generation and embedding


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


    | Endpoint | Limit |

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

    | Account creation | 100/hour |

    | Token generation | 30/min per account |

    | Transfers | 60/min (configurable) |

    | General | 100/min |


    Exceeding limits returns `429` with a `Retry-After` header.


    ## 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: 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`, `account.risk_flagged`


      `withdrawal.completed` is a derived alias of `withdrawal.updated` filtered
      to `status === "completed"`. Subscribe to it if you only want the
      terminal-success event; subscribe to `withdrawal.updated` for the full
      lifecycle (`requested → awaiting_payment → in_transit → completed | failed
      | canceled | denied`).


      All webhook deliveries include `x-sideshift-signature` and
      `x-sideshift-timestamp` headers for signature verification. Non-2xx
      responses are retried with exponential backoff (up to 5 attempts).
      Webhooks fire in both sandbox and production modes.
paths:
  /accounts/transfer/batch:
    post:
      tags:
        - Transfers
      summary: Batch transfer
      description: >
        Process multiple transfers in a single request. Transfers are executed
        **sequentially** — use this instead of sending many parallel
        single-transfer requests.


        Each transfer in the array supports the same parameters as the single
        transfer endpoint. Results are returned per-transfer: failures in one
        transfer do not affect the others.


        **Why batch?** Sending 50+ parallel requests to `/accounts/transfer`
        causes Firestore transaction contention on the source wallet, resulting
        in `INTERNAL_ERROR` failures. The batch endpoint eliminates this by
        processing transfers one at a time within a single HTTP call.


        **Limits:** Maximum 200 transfers per batch.


        **Idempotency and evidence:** Every transfer requires its own
        `idempotencyKey`. Live batch items also require the same
        commercial-evidence metadata as the single-transfer endpoint.
      operationId: createBatchTransfer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - transfers
              properties:
                transfers:
                  type: array
                  maxItems: 200
                  items:
                    type: object
                    required:
                      - amountCents
                      - idempotencyKey
                    properties:
                      fromAccountId:
                        type: string
                        description: Source account (omit for company→user)
                      toAccountId:
                        type: string
                        description: Destination account (omit for user→company)
                      destinationBalance:
                        type: string
                        enum:
                          - withdrawal
                          - wallet
                        default: withdrawal
                        description: >-
                          Settlement target for company→user and user→user
                          transfers
                      amountCents:
                        type: integer
                        minimum: 1
                        description: Amount in cents
                      idempotencyKey:
                        type: string
                        description: Unique key for safe retries
                      metadata:
                        type: object
                        additionalProperties:
                          type: string
                        description: >-
                          Required for live transfers. Include obligationType,
                          obligationReference, description, and
                          approvalReference; campaign transfers also need
                          programId or contractId.
            examples:
              payoutBatch:
                summary: Pay multiple users
                value:
                  transfers:
                    - toAccountId: acct_user1
                      amountCents: 5000
                      idempotencyKey: payout-user1-2026-04
                      metadata:
                        obligationType: campaign
                        obligationReference: contract-user1
                        description: Approved April payout for completed content
                        approvalReference: approval-user1
                        programId: campaign-april
                    - toAccountId: acct_user2
                      amountCents: 7500
                      idempotencyKey: payout-user2-2026-04
                      metadata:
                        obligationType: campaign
                        obligationReference: contract-user2
                        description: Approved April payout for completed content
                        approvalReference: approval-user2
                        programId: campaign-april
                    - toAccountId: acct_user3
                      amountCents: 3000
                      idempotencyKey: payout-user3-2026-04
                      metadata:
                        obligationType: campaign
                        obligationReference: contract-user3
                        description: Approved April payout for completed content
                        approvalReference: approval-user3
                        programId: campaign-april
      responses:
        '200':
          description: Batch completed (check individual results for per-transfer status)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      totalCount:
                        type: integer
                        description: Number of transfers in the batch
                      successCount:
                        type: integer
                        description: Number of transfers that succeeded
                      failureCount:
                        type: integer
                        description: Number of transfers that failed
                      results:
                        type: array
                        items:
                          type: object
                          properties:
                            index:
                              type: integer
                              description: Position in the original transfers array
                            success:
                              type: boolean
                            data:
                              $ref: '#/components/schemas/Transfer'
                            error:
                              type: object
                              properties:
                                code:
                                  type: string
                                message:
                                  type: string
              example:
                success: true
                data:
                  totalCount: 3
                  successCount: 2
                  failureCount: 1
                  results:
                    - index: 0
                      success: true
                      data:
                        transferId: txfr_abc123
                        fromAccountId: company
                        toAccountId: acct_user1
                        amountCents: 5000
                        amountUsd: 50
                        status: completed
                        direction: company_to_user
                        destinationBalance: withdrawal
                        createdAt: '2026-04-03T12:00:00.000Z'
                    - index: 1
                      success: true
                      data:
                        transferId: txfr_def456
                        fromAccountId: company
                        toAccountId: acct_user2
                        amountCents: 7500
                        amountUsd: 75
                        status: completed
                        direction: company_to_user
                        destinationBalance: withdrawal
                        createdAt: '2026-04-03T12:00:01.000Z'
                    - index: 2
                      success: false
                      error:
                        code: INSUFFICIENT_BALANCE
                        message: >-
                          Insufficient wallet balance in source account to
                          complete this transfer
        '400':
          description: Invalid request body or transfers array
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmbedErrorEnvelope'
              examples:
                notArray:
                  value:
                    success: false
                    error:
                      code: VALIDATION_ERROR
                      message: Request body must contain a "transfers" array
                tooMany:
                  value:
                    success: false
                    error:
                      code: VALIDATION_ERROR
                      message: transfers array exceeds maximum batch size of 200
        '429':
          description: Rate limit exceeded
components:
  schemas:
    Transfer:
      type: object
      properties:
        transferId:
          type: string
          example: txfr_abc123def456
        fromAccountId:
          type: string
          description: Source account (your company ID for company→user transfers)
        toAccountId:
          type: string
          description: Destination account
        amountCents:
          type: integer
          description: Amount in cents
          example: 5000
        amountUsd:
          type: number
          format: double
          description: Amount in USD
          example: 50
        status:
          type: string
          enum:
            - completed
            - pending
            - failed
        direction:
          type: string
          enum:
            - company_to_user
            - user_to_company
            - user_to_user
        destinationBalance:
          type: string
          enum:
            - wallet
            - withdrawal
          description: Where the transfer settled
        paymentTransferId:
          type: string
          nullable: true
          description: Underlying payment provider transfer ID
        idempotencyKey:
          type: string
          nullable: true
        metadata:
          type: object
          nullable: true
          additionalProperties:
            type: string
        createdAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
          nullable: true
        failureReason:
          type: string
          nullable: true
          description: Reason for failure (only when status is `failed`)
    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
  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.

````