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 the Merchant API never accepts a card number, 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 Upprove.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 "$UPPROVE_SIGNING_SECRET" | sed 's/^.*= //')

curl -sS -X POST "https://api.sandbox.upprove.com${REQ_PATH}" \
-H "Authorization: Bearer $UPPROVE_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 upprove = Upprove('pk_test_…');
const card = upprove.elements().create('card');
card.mount('#card-element');
await card.ready;

const {card: collected, error} = await upprove.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 pay() here. That method pays 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 "$UPPROVE_SIGNING_SECRET" | sed 's/^.*= //')

curl -sS -X POST "https://api.sandbox.upprove.com${REQ_PATH}" \
-H "Authorization: Bearer $UPPROVE_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.* webhooks - transfer.paid, transfer.failed, transfer.canceled. Do not treat the 200 as confirmation that the recipient has the money.

Next steps​