Skip to main content

Card payments

Accept cards directly on your page with Orchestr.js, our browser SDK. The SDK renders the card fields inside secure, Orchestr-hosted inputs — the card number, expiry, and CVC are entered into a document served from our PCI-audited origin and never touch your page or your servers. You stay in the lightest PCI DSS scope (SAQ A) while keeping full control of your checkout's look and flow.

Orchestr.js does the heavy lifting: tokenizing the card, confirming the payment, and rendering the 3-D Secure challenge when the issuer demands one. Your integration is four steps:

  1. Create a payment on your server — POST /v1/payments with payment_method: {"code": "card"}.
  2. Collect the card in the browser — mount the card element and call collectCard().
  3. Confirm — call confirmCardPayment(); the SDK handles 3-D Secure inline.
  4. Fulfil on the webhookpayment.succeeded is the authoritative result.

1. Create a payment

The server-side call is the same POST /v1/payments you use for direct payments, with payment_method: {"code": "card"}. It reuses the buildSignatureHeader helper from the Signing guide and assumes ORCHESTR_SECRET_KEY / ORCHESTR_SIGNING_SECRET are exported as shown in Getting started.

REQ_PATH="/v1/payments"
BODY='{"amount":"42.00","currency":"GBP","country":"GB","payment_method":{"code":"card"},"merchant_reference":"order-1001","success_url":"https://shop.example.com/ok","cancel_url":"https://shop.example.com/cancel","customer":{"email":"jane@example.com"}}'
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"

Because this request carries no card_token, it comes back requires_action with a collect_card next action — the browser flow this page describes:

{
"order_id": "pay_5b4a3c2d",
"status": "requires_action",
"amount": 42.00,
"currency": "GBP",
"next_action": {
"type": "collect_card",
"client_secret": "eyJhbGciOiJFUzI1NiJ9…"
}
}
card_token decides which flow you get

Sending payment_method.details.card_token on the create switches to a server-to-server authorization instead — a real charge attempt that can return completed, failed, authorized, or requires_action with a 3-D Secure challenge. Only the token-less request produces collect_card.

The client_secret is a short-lived, single-payment grant that authorizes your page to collect this card for this payment. Hand that one string to the browser — nothing else. It expires after 15 minutes; create a new payment to mint a fresh one.

note

Never send your secret key (sk_…) or signing secret to the browser. The browser only ever sees your publishable key (pk_…) and the client_secret.

2. Collect the card

Load Orchestr.js from our CDN and mount the card element. The script is served at a pinned major version, so security fixes reach your payment page without you redeploying — and PCI DSS v4's script-integrity duties (6.4.3, 11.6.1) stay on our side of the line.

<script src="https://js.upprove.com/v1/orchestr.js"></script>
<div id="card-element"></div>
<button id="pay" disabled>Pay £42.00</button>

Using a bundler? npm install @orchestr/card-js gives you a typed loader that injects the same script — see the Orchestr.js reference.

const orchestr = Orchestr('pk_test_…'); // your publishable key
const card = orchestr.elements().create('card');

card.mount('#card-element');
card.on('change', ({complete, error}) => {
payButton.disabled = !complete;
errorEl.textContent = error ?? '';
});

payButton.onclick = async () => {
const {card: collected, error} = await orchestr.collectCard(clientSecret);
if (error) return showError(error.code);

// Non-sensitive: safe to display or send to your server.
console.log(collected.brand, collected.last4);
};

collectCard(clientSecret) submits the card from inside the secure fields and resolves with a card token (tok_…) plus display-safe details (brand, last4, expiry). The token is an opaque reference to the vaulted card — it is not card data and is safe to log, store, and send to your server.

3. Confirm the payment

Confirm with the token. The SDK calls Orchestr directly and — when the issuer requires it — renders the 3-D Secure challenge in your page, so you write no 3DS code:

const {payment, error} = await orchestr.confirmCardPayment({
orderId: 'pay_5b4a3c2d', // order_id from step 1
token: collected.token,
});

if (error) return showError(error.code);
if (payment.status === 'completed') showSuccessUi();
The browser result is a hint, not the truth

The value confirmCardPayment() resolves with is a UX convenience — it is spoofable by the cardholder's browser, and it is lost if the tab closes mid-challenge. Never ship goods, grant access, or mark an order paid on the browser result. Fulfil on the webhook (next step).

4. Fulfil on the webhook

The authoritative outcome arrives on your server as a signed payment.succeeded (or payment.failed) webhook. Verify the signature, then fulfil.

Saving cards

To charge a card again later — "remember my card", subscriptions, or metered top-ups — ask us to save the card at payment time. Card networks require an explicit, recorded cardholder agreement for stored credentials, so save_card must carry a consent object describing the agreement you captured:

"payment_method": {
"code": "card",
"details": {
"save_card": true,
"consent": {
"captured_at": "2026-07-16T09:41:00Z",
"type": "card_on_file",
"reference": "tos-v3-acc-8842"
}
}
}
FieldDescription
captured_atWhen the cardholder's affirmative act happened (RFC 3339). Must not be in the future.
typeThe kind of stored-credential agreement — see below.
referenceYour identifier for the retained agreement artifact (the checkbox event, signed terms version, subscription id). You keep the artifact; we record that it exists.

All three fields are required whenever save_card is true, and consent is rejected without save_card — the two travel together.

typeUse when the cardholder agreed to…
card_on_fileStoring the card for future purchases they initiate ("remember my card").
recurringA fixed, scheduled series of charges (subscriptions).
unscheduledMerchant-initiated charges at irregular times (auto top-up, usage billing).

The card is stored only after the payment's authorization succeeds — a declined first payment never leaves a stored card behind. Asserting a different consent for the same payment returns 409 consent_conflict; re-sending the identical consent is idempotent and safe to retry.

Charging a saved card

Charge a stored card server-side by passing its token:

{
"amount": "9.99",
"currency": "GBP",
"country": "GB",
"payment_method": {
"code": "card",
"details": {"card_token": "tok_1PqRsT2eZvKYlo2C"}
},
"merchant_reference": "sub-2026-07-renewal",
"success_url": "https://shop.example.com/ok",
"cancel_url": "https://shop.example.com/cancel"
}

The consent type recorded at save time governs how the stored card may be used: recurring and unscheduled agreements permit merchant-initiated charges like the one above; card_on_file covers customer-initiated purchases where you present the saved card at checkout.

This is a real charge attempt, and it can still need the browser

A create carrying card_token runs a server-to-server authorization — there is no collect_card step, but the issuer can still demand 3-D Secure, in which case you get back requires_action and a challenge to route the cardholder through. Handle all four outcomes; see Server-to-server card payments.

Zero-amount card setup

To store a card without charging it, create a payment with amount: "0" plus save_card and consent. Orchestr runs a zero-amount account-verification authorization against the issuer and stores the card if it succeeds:

{
"amount": "0",
"currency": "GBP",
"country": "GB",
"payment_method": {
"code": "card",
"details": {
"save_card": true,
"consent": {
"captured_at": "2026-08-11T09:41:00Z",
"type": "unscheduled",
"reference": "tos-v3-acc-8842"
}
}
},
"merchant_reference": "setup-acc-8842",
"success_url": "https://shop.example.com/ok",
"cancel_url": "https://shop.example.com/cancel"
}

A zero amount is accepted only in this combination — save_card and consent must both be present, otherwise the payment is rejected. Manual capture is also rejected at amount 0, since there is nothing to capture.

Cards on hosted checkout

The result arrives where all your outcomes arrive — the payment.succeeded webhook.

Test cards

The sandbox recognizes test cards only — a real card number is rejected before it is ever stored. Use any future expiry and any CVC.

NumberBehavior
4242 4242 4242 4242Visa — succeeds.
4000 0000 0000 9995Visa — declined, insufficient_funds.

See Test cards for the full set, including declines and 3-D Secure scenarios — and for which cards apply to your account, since Sandbox routes to an acquirer the same way Live does.

Next steps