Transfers to a card
Send funds to a card the recipient enters in your page — informally, a card payout. It is a transfer, not a payout: a payout returns money to the instrument of a payment you already took, whereas this sends money to a card you have no prior relationship with.
Because a card number can never reach your servers, a card transfer runs in two legs:
- Create the transfer without a card. It comes back
requires_actionwith acollect_cardaction. - Collect the card in the browser with Orchestr.js, then execute the transfer server-side with the resulting token.
The money only moves on the second server call. The browser's client secret authorizes tokenization and nothing else.
1. Create the collection order
POST /v1/transfers with payment_method.code: "card" and no card_token.
consent is requiredThe token the browser mints is a stored credential, so a card transfer must carry
payment_method.details.consent — the same consent object used
when saving a card on a payment. Omitting it is rejected with transfer_destination_invalid.
- cURL
- Node.js
- Python
REQ_PATH="/v1/transfers"
BODY='{"amount":"250.00","currency":"GBP","merchant_reference":"payout-1001","reference":"ACME payout","payment_method":{"code":"card","details":{"consent":{"captured_at":"2026-08-11T09:41:00Z","type":"unscheduled","reference":"tos-v3-acc-8842"}}}}'
TS=$(date +%s)
SIG=$(printf '%s' "${TS}${REQ_PATH}${BODY}" \
| openssl dgst -sha256 -hmac "$ORCHESTR_SIGNING_SECRET" | sed 's/^.*= //')
curl -sS -X POST "https://api.sandbox.upprove.com${REQ_PATH}" \
-H "Authorization: Bearer $ORCHESTR_SECRET_KEY" \
-H "X-Signature: t=${TS}, v1=${SIG}" \
-H "Content-Type: application/json" \
--data-raw "$BODY"
const path = '/v1/transfers';
const body = JSON.stringify({
amount: '250.00',
currency: 'GBP',
merchant_reference: 'payout-1001',
reference: 'ACME payout',
payment_method: {
code: 'card',
details: {
consent: {
captured_at: '2026-08-11T09:41:00Z',
type: 'unscheduled',
reference: 'tos-v3-acc-8842',
},
},
},
});
const res = await fetch(`https://api.sandbox.upprove.com${path}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ORCHESTR_SECRET_KEY}`,
'X-Signature': buildSignatureHeader(process.env.ORCHESTR_SIGNING_SECRET, path, body),
'Content-Type': 'application/json',
},
body,
});
const transfer = await res.json();
// Hand transfer.next_action.client_secret to your page.
import json
import requests
path = "/v1/transfers"
body = json.dumps(
{
"amount": "250.00",
"currency": "GBP",
"merchant_reference": "payout-1001",
"reference": "ACME payout",
"payment_method": {
"code": "card",
"details": {
"consent": {
"captured_at": "2026-08-11T09:41:00Z",
"type": "unscheduled",
"reference": "tos-v3-acc-8842",
}
},
},
},
separators=(",", ":"),
)
resp = requests.post(
f"https://api.sandbox.upprove.com{path}",
data=body,
headers={
"Authorization": f"Bearer {ORCHESTR_SECRET_KEY}",
"X-Signature": build_signature_header(ORCHESTR_SIGNING_SECRET, path, body),
"Content-Type": "application/json",
},
)
transfer = resp.json()
# Hand transfer["next_action"]["client_secret"] to your page.
The response is 201 Created with a TransferView in the collection state:
{
"order_id": "trf_5b4a3c2d",
"merchant_reference": "payout-1001",
"amount": 250.00,
"currency": "GBP",
"status": "requires_action",
"next_action": {
"type": "collect_card",
"client_secret": "eyJhbGciOiJFUzI1NiJ9...",
"expires_at": "2026-08-11T10:00:00Z"
},
"reference": "ACME payout",
"created_at": "2026-08-11T09:45:00Z"
}
Keep order_id — it is the transfer order (trf_…) you execute against in step 3.
2. Collect the card in the browser
Hand next_action.client_secret to your page and collect the card exactly as you would for a
payment — see the SDK reference:
const orchestr = Orchestr('pk_test_…');
const card = orchestr.elements().create('card');
card.mount('#card-element');
await card.ready;
const {card: collected, error} = await orchestr.collectCard(clientSecret);
if (error) { /* show error, let the recipient retry */ }
// Send collected.token to your server — it is opaque and safe to transmit.
Do not call confirmCardPayment() here. That method confirms a payment; a transfer is settled
by your server in the next step.
3. Execute the transfer
POST /v1/transfers/{orderId}/execute with the token, using the transfer order id from step 1.
REQ_PATH="/v1/transfers/trf_5b4a3c2d/execute"
BODY='{"card_token":"tok_1PqRsT2eZvKYlo2C3dEf4gHi"}'
TS=$(date +%s)
SIG=$(printf '%s' "${TS}${REQ_PATH}${BODY}" \
| openssl dgst -sha256 -hmac "$ORCHESTR_SIGNING_SECRET" | sed 's/^.*= //')
curl -sS -X POST "https://api.sandbox.upprove.com${REQ_PATH}" \
-H "Authorization: Bearer $ORCHESTR_SECRET_KEY" \
-H "X-Signature: t=${TS}, v1=${SIG}" \
-H "Content-Type: application/json" \
--data-raw "$BODY"
card_token is required and must match tok_ followed by 20–64 alphanumeric characters. The
response is 200 OK with the transfer moved to pending:
{
"order_id": "trf_5b4a3c2d",
"merchant_reference": "payout-1001",
"amount": 250.00,
"currency": "GBP",
"status": "pending",
"reference": "ACME payout",
"created_at": "2026-08-11T09:45:00Z"
}
Skipping the collection leg
If you already hold a token for the destination card, pass it as
payment_method.details.card_token on the create call. The transfer executes immediately and
comes back pending — there is no next_action and no second call.
Errors
code | HTTP | When it happens |
|---|---|---|
transfer_destination_invalid | 400 | The card destination is missing required details — most often consent. |
transfer_card_token_required | 400 | A card destination was created in a way that needs a token up front. |
transfer_invalid_order_state | 409 | /execute was called on a transfer that is not awaiting card collection — including one already executed. |
transfer_in_progress | 409 | Another /execute for this transfer is already running. Do not retry in a tight loop. |
transfer_unroutable | 400 | No provider channel could be routed. Supply channel_id. |
See Errors for the full list.
Handling the outcome
/execute returning pending means accepted, not paid. The final result arrives on the
transfer.* webhooks — transfer.paid, transfer.failed,
transfer.canceled. Do not treat the 200 as confirmation that the recipient has the money.
Next steps
- Transfers — bank, wallet, and RTP destinations.
- Card SDK reference — the browser API used in step 2.
- Webhooks — the
transfer.*events that report the final outcome.