Transfers V2
A V2 transfer is a money movement whose type is inferred from two slots — source and destination — instead of from which V1 field you populated. The same write body is accepted on create, update, and confirm; which keys are required and which are applied depends on the action and on what those slots resolve to.
How it works
Each slot is one thing. If payment_method_id or a nested payment_method is set, the slot is a payment method. Otherwise wallet_id makes it a wallet. Otherwise account_id makes it an account. Two identifiers in the same slot is invalid.
From that pair Frame classifies the transfer:
- A payment-method or account source, with the destination omitted or set to an account, is a payment — a card or ACH charge. A payment-method destination is rejected here.
- A wallet or account source into a payment-method destination is a payout.
- An account or wallet source into a wallet destination is an account transfer.
Payments take USD, EUR, or GBP. Payouts and account transfers take USD only. Amounts are integer minor units; currency is uppercased before it is stored.
A nested payment method on create is saved. Later payment creates can send that payment_method_id instead of the raw card or bank account. A payout can do the same with a saved destination id.
Capture, refund, and void exist only for payments, and only in the shapes V2 actually supports: capture cannot increment an authorization, and the only capture/refund body field is an optional amount.value.
POST/v2/transfers requires an Idempotency-Key header on the first request, not only on a retry. Confirm, capture, refund, void, update, and read do not. A completed create is saved for 24 hours and replayed byte for byte with Idempotent-Replay: true. A key that never completed still attaches to the original transfer. See idempotency for the retry rules.
Lifecycle
The transfer record has its own status (pending, completed, failed, canceled, reversed). That is not the field you branch on for a card charge.
Payments. Branch on payment.status. The transfer record stays pending through authorize, capture, and refund. confirm: false lands in requires_confirmation with nothing authorized. authorization_mode: automatic authorizes and captures together; manual authorizes only and waits at requires_capture until you capture or void. Setup states (incomplete, requires_account, requires_payment_method, requires_3d_secure) can still be patched. A sandbox decline still creates the transfer — the payment lands on failed with failure_code set.
Payouts. payout.status is pending or processing right after create, then succeeded, failed, canceled, or returned. There is no confirm, capture, or void step. Rail defaults to ach unless the destination is a wallet payout method (PayPal, Venmo, Cash App, Payoneer, Airtm), which forces wallet. Speed is stored for ACH only.
Account transfers. account_id → wallet_id funds that account's own wallet; the account must own the destination wallet. wallet_id → wallet_id moves to the parent account's wallet. Rail and speed are not applied.
Relationships
TransferV2 is the durable record. PaymentMethod can sit in either slot as a nested object or a saved id. An Account in the source of a payment charges that account's default payment method, or the account itself when it has none. A wallet is the source of a payout and the destination of an account transfer.
V1 transfers are a separate surface (/v1/transfers). Same word, different request shape and state machine — do not mix the two in one integration path.
How V2 transfers send webhooks
Events fire at two levels. The transfer record sends transfer.completed, transfer.failed, transfer.canceled, and transfer.reversed when its own status changes. The payment, payout, or account transfer sends transfer.payment.*, transfer.payout.*, or transfer.account_transfer.* when it is created and whenever its status changes. Every event carries the whole V2 transfer, and all of one transfer's events share a single sequence counter for ordering.
For charges, listen to transfer.payment.*, because the transfer record stays pending through the charge lifecycle. The transfer record never sends transfer.created or transfer.pending. Three codes, transfer.failed, transfer.canceled, and transfer.reversed, are shared with V1 transfers, so a handler for either version must check which shape it got. The codes, payload, and ordering rules are in webhooks: V2 transfer events.
Gotchas
Symptom: the transfer status is still pending after a successful card charge. Why: V2 keeps the charge lifecycle on payment.status. Fix: branch on payment.status (succeeded, requires_capture, refunded, and so on).
Symptom: create, update, and confirm accept fields that do nothing. Why: they share one body; payouts ignore confirm and authorization_mode, and updates ignore confirm, destination, and the payment record ids. Fix: send the keys the action applies — see Use Transfers V2.
Symptom: a payment-method destination on a charge is rejected. Why: that pair is not a payment; a payment destination is an account or omitted. Fix: omit destination, or put an account_id there.
Symptom: a timeout retry of POST /v2/transfers created a second transfer. Why: you sent a new Idempotency-Key, or recovered from a missing-key 400 with a different key. Fix: generate one key when the customer submits; send it on the first create and on every retry of that attempt.
Symptom: capture or void on an automatic charge returns 422. Why: those actions are manual-authorization only. Fix: capture later by creating with authorization_mode: manual, or refund captured funds instead of voiding.
Reference
For the full write body and lifecycle actions, see POST/v2/transfers and the rest of the V2 Transfers resource.