Skip to main content

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:

  1. Create the transfer without a card. It comes back requires_action with a collect_card action.
  2. 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 required

The 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.

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"

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

codeHTTPWhen it happens
transfer_destination_invalid400The card destination is missing required details — most often consent.
transfer_card_token_required400A card destination was created in a way that needs a token up front.
transfer_invalid_order_state409/execute was called on a transfer that is not awaiting card collection — including one already executed.
transfer_in_progress409Another /execute for this transfer is already running. Do not retry in a tight loop.
transfer_unroutable400No 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.* webhookstransfer.paid, transfer.failed, transfer.canceled. Do not treat the 200 as confirmation that the recipient has the money.

Next steps