---
title: "Withdraw USDC from the embedded wallet to an external address"
method: POST
path: "/v1/wallet/withdraw"
tags: ["User"]
---

# Withdraw USDC from the embedded wallet to an external address

`POST /v1/wallet/withdraw`

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.

## Headers

- `Idempotency-Key` string, uuid

## Request body

- object
  - `recipient` string, required — 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` number, required — 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.

## Response `200`

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.

- object
  - `data` object
    - `signature` string — External SPL transfer signature.
    - `explorer_url` string — Solscan URL for the external SPL signature.
    - `vault_signature` string — On-chain `withdraw_from_vault` signature. Present only when the request required a vault → ATA hop to cover a shortfall.
    - `vault_amount` number — USDC amount moved from vault to ATA before the external transfer. Present only when `vault_signature` is present.
    - `recipient_ata_created` boolean — 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` number — USDC withheld from the requested amount to reimburse the one-time recipient-ATA rent. Present only with `recipient_ata_created`.
    - `delivered_usdc` number — USDC actually delivered to the recipient (`amount_usdc − chargeback_usdc`). Present only with `recipient_ata_created`.

## Other responses

- `400` — Validation error
- `401` — Missing or invalid authentication
- `409` — The caller's embedded wallet is no longer delegated for server-side signing. Re-enabling trading from the app restores the session signer.
- `429` — Rate limit exceeded
- `500` — Internal server error
- `502` — 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.

---

[API](https://skmtc.net/totalistrading/apis/totalis-rfq-api.md) · [All operations](https://skmtc.net/totalistrading/apis/totalis-rfq-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/totalistrading/totalis-rfq-api/revisions/b0adb9a590b4/schema)
