> ## Documentation Index
> Fetch the complete documentation index at: https://docs.totalis.trade/llms.txt
> Use this file to discover all available pages before exploring further.

# Withdraw to External Address

> Withdraw USDC from the embedded wallet to an external Solana address.



## OpenAPI

````yaml POST /v1/wallet/withdraw
openapi: 3.0.3
info:
  title: Totalis RFQ API
  version: 2.1.0
  description: >-
    Public REST surface for the Totalis parlay RFQ platform — a decentralized
    request-for-quote marketplace for parlay bets across Kalshi and Polymarket,
    with on-chain Solana vault settlement.


    **Wire format**: snake_case JSON. All successful responses are wrapped in `{
    "data": ... }`; list endpoints add `meta` with cursor-based pagination.
    Errors use a `{ "error": { code, message, details? } }` envelope.


    **Authentication**: programmatic clients send `X-API-Key`; the web dashboard
    uses Privy JWTs (`Authorization: Bearer ...`). Any authenticated user can
    both place parlays and quote as a market maker — there is no separate MM
    role. Admin endpoints are out of scope for this public reference.


    **Rate limiting**: 100 req/min anonymous, 300 req/min authenticated.
    Responses include `X-RateLimit-*` headers. Request body limit: 256KB.


    **Pagination**: list endpoints use opaque cursor pagination. Pass
    `meta.cursor` from the previous response as `?cursor=...` to fetch the next
    page; do not parse or construct cursors client-side.
servers:
  - url: https://api.totalis.trade
    description: Production
security:
  - ApiKey: []
tags:
  - name: Markets
    description: Cached Kalshi and Polymarket market data.
  - name: User
    description: User profile, wallet, and devnet helpers.
  - name: API Keys
    description: Manage programmatic access keys for the authenticated user.
paths:
  /v1/wallet/withdraw:
    post:
      tags:
        - User
      summary: Withdraw USDC from the embedded wallet to an external address
      description: |
        Sends USDC out of the caller's embedded wallet ATA to an arbitrary
        on-curve Solana address. Uses Privy's session-signer delegation
        (set up via /v1/wallet/enable-trading) and Privy's native gas
        sponsorship — the user is not prompted to sign and never needs SOL.

        Chained vault-tap: when the requested `amount_usdc` exceeds the
        caller's ATA-available balance, the handler first issues
        `withdraw_from_vault` (vault PDA → owner ATA) for the shortfall,
        waits for that TX to confirm, then issues the external SPL transfer.
        The `vault_signature` / `vault_amount` fields surface the intermediate
        TX when this happens. If the vault tap succeeds but the external
        transfer fails to construct or broadcast, the response is **502**
        with `error.code: PARTIAL_VAULT_ONLY` and the vault TX surfaced under
        `error.details.vault_signature` / `error.details.vault_amount`. The
        caller can retry with the same amount; the pre-flight will see the
        ATA is now sufficient and skip the vault tap. 502 (rather than 200)
        is deliberate: the idempotency middleware caches only 2xx, so
        same-key retries against a 502 re-enter the handler instead of
        replaying a cached partial response.

        Idempotency-Key is honored: a duplicate request with the same key
        replays the cached 2xx response without re-issuing on-chain TXs.
      operationId: withdrawFromWallet
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - recipient
                - amount_usdc
              properties:
                recipient:
                  type: string
                  description: |
                    Destination Solana wallet address (base58). Must be
                    on-curve — PDAs, program IDs, and token-account
                    addresses are rejected because funds sent to a
                    derived ATA-of-an-ATA are unrecoverable.
                amount_usdc:
                  type: number
                  minimum: 0.000001
                  description: |
                    USDC amount to send. Minimum is one USDC base unit
                    (1e-6). Must be ≤ the caller's total spendable pool
                    (ATA + vault free_balance − in-flight RFQ holds);
                    requests exceeding this are rejected with 400.
      responses:
        '200':
          description: |
            Withdrawal submitted. `signature` is the external SPL transfer
            signature. `vault_signature` and `vault_amount` are present
            only when the request required a vault → ATA hop.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      signature:
                        type: string
                        description: External SPL transfer signature.
                      explorer_url:
                        type: string
                        description: Solscan URL for the external SPL signature.
                      vault_signature:
                        type: string
                        description: |
                          On-chain `withdraw_from_vault` signature.
                          Present only when the request required a vault
                          → ATA hop to cover a shortfall.
                      vault_amount:
                        type: number
                        description: |
                          USDC amount moved from vault to ATA before the
                          external transfer. Present only when
                          `vault_signature` is present.
                      recipient_ata_created:
                        type: boolean
                        description: >
                          Present (true) when the recipient had no USDC token

                          account and this transaction created one (the

                          create-and-chargeback path, gated by

                          WITHDRAW_ATA_AUTOCREATE). The fee-payer fronts the

                          ~0.002 SOL account rent and bills it back in USDC, so

                          the recipient receives `delivered_usdc` and the
                          treasury

                          keeps `chargeback_usdc`. The create is non-idempotent,
                          so

                          a confirmed response means this tx created the account

                          (a race where it already existed reverts and retries

                          via the standard path, with no chargeback).
                      chargeback_usdc:
                        type: number
                        description: |
                          USDC withheld from the requested amount to reimburse
                          the one-time recipient-ATA rent. Present only with
                          `recipient_ata_created`.
                      delivered_usdc:
                        type: number
                        description: |
                          USDC actually delivered to the recipient
                          (`amount_usdc − chargeback_usdc`). Present only with
                          `recipient_ata_created`.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: |
            The caller's embedded wallet is no longer delegated for
            server-side signing. Re-enabling trading from the app restores
            the session signer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          description: >
            Ambiguous partial state — returned as 5xx (not 200) so

            Idempotency-Key callers retry against the handler instead of

            replaying a cached partial response. Two codes:


            `PARTIAL_VAULT_ONLY` — the vault tap succeeded but the external SPL

            transfer failed to construct/broadcast. Funds moved from vault to

            ATA; retry the same request to complete the external leg (the

            pre-flight sees the updated ATA and skips the vault tap). Returned

            only when the vault tap fired this request; a same-key retry against

            an already-topped-up ATA skips the tap (`shortfall = 0`) and a

            subsequent SPL failure surfaces as a normal 500.


            `WITHDRAW_SUBMITTED_UNCONFIRMED` — only on the recipient-ATA

            create-and-chargeback path (WITHDRAW_ATA_AUTOCREATE), where
            broadcast

            and confirmation are separate steps: the transaction was broadcast

            (`details.signature` present) but confirmation timed out, so it may

            have landed. Do NOT blind-retry — verify the recipient balance
            first,

            since a retry would re-send via the now-existing ATA and double-pay.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        enum:
                          - PARTIAL_VAULT_ONLY
                          - WITHDRAW_SUBMITTED_UNCONFIRMED
                      message:
                        type: string
                      details:
                        type: object
                        properties:
                          signature:
                            type: string
                            description: |
                              The broadcast transaction signature. Present on
                              WITHDRAW_SUBMITTED_UNCONFIRMED — the tx may have
                              landed; check it before retrying.
                          vault_signature:
                            type: string
                          vault_amount:
                            type: number
                          chargeback_usdc:
                            type: number
                            description: >
                              On WITHDRAW_SUBMITTED_UNCONFIRMED, the USDC that

                              would have been withheld for the recipient-ATA
                              rent.
                          delivered_usdc:
                            type: number
                            description: |
                              On WITHDRAW_SUBMITTED_UNCONFIRMED, the USDC that
                              would have been delivered to the recipient.
                          partial:
                            type: string
                            enum:
                              - vault_only
                              - submitted_unconfirmed
      security:
        - PrivyJWT: []
        - ApiKey: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: >-
        Client-generated UUID. Successful (2xx) responses are cached for 24h and
        replayed on retry with the same key. The request body is not part of the
        cache key — use a fresh UUID whenever the payload differs (e.g. updated
        odds), otherwise the original response is replayed. Non-2xx responses
        are not cached.
      schema:
        type: string
        format: uuid
  responses:
    BadRequest:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Unauthorized:
      description: Missing or invalid authentication
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: UNAUTHORIZED
              message: Missing or invalid authentication
    RateLimited:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  schemas:
    ErrorEnvelope:
      type: object
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorBody'
    ErrorBody:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          enum:
            - VALIDATION_ERROR
            - UNAUTHORIZED
            - FORBIDDEN
            - NOT_FOUND
            - CONFLICT
            - RATE_LIMITED
            - PAYLOAD_TOO_LARGE
            - INTERNAL_ERROR
            - SERVICE_UNAVAILABLE
          description: Machine-readable error code.
        message:
          type: string
          description: Human-readable error message.
        details:
          type: object
          description: Additional error context.
        retry_after:
          type: integer
          nullable: true
          description: Seconds until retry; set on 429 responses.
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: >-
        Programmatic API key. Sent as `X-API-Key: <key>`. Generate one from the
        Totalis dashboard. The same header is accepted on the WebSocket auth
        message.
    PrivyJWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        Privy JWT issued to the web dashboard. Sent as `Authorization: Bearer
        <jwt>`. The Privy session signer underpins all wallet-signed actions.

````