Create a withdrawal
Submits an ACH credit from the user's wallet to a bank account they have linked. The wallet is debited immediately and the response status is always `pending`; the credit settles over the following business days and its outcome appears as later entries in `GET /users/{userId}/wallet/transactions` rather than on this response. `paymentMethodId` is required and must identify a bank account, so complete Plaid Link before calling this endpoint. Accepts either the partner `Api-Key`, which may act on any user, or a user-scoped client token from `POST /users/{userId}/auth/client-token`. Every request requires an `idempotencyKey`; see the notes for its lifecycle.
/v1/users/{userId}/wallet/withdrawalsPath parameters
| Field | Type | Required | Description |
|---|---|---|---|
| userId | string | required | Uptop user id returned from `POST /users:upsert`. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| amountCents | integer | required | Amount to withdraw, in cents. The minimum is `100` ($1.00). Amounts above the per-withdrawal cap are refused with `VALIDATION_ERROR`. |
| paymentMethodId | string | required | Destination bank account: an `id` from `GET /users/{userId}/wallet/payment-methods` with `type: "bank_account"`, or the `paymentMethodId` returned when the account was linked. Cards are rejected. The field has no default, so the destination is always explicit. |
| idempotencyKey | string (UUID) | required | Identifies one withdrawal intent and must stay constant across every retry of that intent, so that a replay returns the original transaction rather than submitting a second ACH credit. Generate a new key whenever the amount or destination changes; reusing a key with a different amount returns 409. |
{
"amountCents": 5000,
"paymentMethodId": "pm-3c8b1e94",
"idempotencyKey": "6f8b0d4a-2e51-4b7c-9a13-8c5d2f0e7b64"
}Response — 201
Click the type for the full field-by-field shape.
{
"transactionId": "txn-7b21c4de",
"status": "pending",
"amountCents": 5000,
"paymentMethodId": "pm-3c8b1e94",
"balance": 10336,
"currency": "usd",
"createdAt": "2026-08-24T15:04:00.000Z"
}Errors
- UNAUTHORIZED401— Missing or invalid `Api-Key` or client token.
- VALIDATION_ERROR400— `amountCents` is below `100` or over the per-withdrawal cap, `idempotencyKey` is missing or not a UUID, or `paymentMethodId` is a card rather than a bank account.
- VALIDATION_ERROR409— The `idempotencyKey` was already used for a withdrawal of a different amount, which identifies a stale key rather than a replay. Create a new intent with a new key; do not retry this request.
- INSUFFICIENT_FUNDS402— The withdrawable balance is below `amountCents`, because the wallet holds less than that amount, a recent load has not cleared, or a card hold is open. Retriable once the funds clear.
- WITHDRAWAL_LIMIT_EXCEEDED400— The request exceeds the daily outbound limit. Retriable with a smaller amount or on a later day.
- FORBIDDEN403— Declined by a risk check, or withdrawals are not yet configured for the program. Not retriable; contact your account contact.
- NOT_FOUND404— `userId` does not exist, or `paymentMethodId` is not a payment method on this user's wallet.
- INTERNAL_ERROR502— The wallet service was unavailable or did not respond in time. The withdrawal may or may not have been created; retry with the same `idempotencyKey`.
Notes
- **Reuse the `idempotencyKey` on every retry.** A request that times out and is retried under a new key submits a second ACH credit. Generate one UUID per withdrawal intent, meaning the amount and destination the user confirmed, persist it before the first request, and send that same key, amount, and destination on every retry. Discard the key after a 2xx, and generate a new one when the user changes the amount or destination.
- **A timeout or a 502 is an unknown outcome, not a failure.** The withdrawal may already exist. Retry with the same key, since a replay returns the original transaction rather than submitting a second ACH credit. Do not resolve the ambiguity by creating a new withdrawal, and note that the transaction list cannot identify one: transaction records carry no idempotency key, so two withdrawals of the same amount to the same destination are indistinguishable.
- **Check the withdrawable balance before submitting.** The wallet balance is an upper bound: recently added funds are held until they clear, an open card hold blocks withdrawals entirely, and the per-withdrawal limits bound what one request may take. `GET /users/{userId}/wallet/withdrawable-balance` reports the amount this endpoint will accept, and any maximum offered to the user should come from there. That figure is a snapshot rather than a reservation, so still handle `INSUFFICIENT_FUNDS` and read it again before retrying.
- **Withdrawing recently added funds can deduct points.** Programs may award points when funds are added to the wallet; when recently added funds leave as cash rather than being spent, the points awarded for the withdrawn amount are deducted again. Withdrawing part of a recent load deducts the bonus on that part only, and one load is never deducted twice across withdrawals. The deduction posts as a negative-`delta` entry of type `other` in `GET /users/{userId}/points/events`, and the points balance can go negative when those points were already redeemed. In-venue spending never triggers this. The lookback window, like the funds-availability holds, is a fraud control and is not disclosed.
Try it (curl)
$UPTOP_API_KEY in the curl will be substituted for it on send.Edit anything above, then send through the local proxy.