Vault API: send card data from your server
If your systems are PCI DSS compliant and already hold card details, you can send them to the
Orchestr Vault directly from your server instead of collecting them with
Upprove.js. The Vault exchanges the card for a tok_… token. You then
pay with that token through the normal POST /v1/payments call.
Two calls per payment:
POST /v1/cardson the Vault API - send the card details (and the CVV, if you have it) and receive a token.POST /v1/paymentson the Merchant API - charge the token under the samemerchant_reference.
This capability is switched on by Orchestr for accounts that have provided a current PCI DSS
Attestation of Compliance covering the systems that handle card data. Until it is enabled, the
access-token call in step 1 returns 403 vault_not_enabled. Everything else on this page works
exactly the same in Sandbox and Live.
How it fits together
| Host | What it does | Authentication |
|---|---|---|
Merchant API - https://api.sandbox.upprove.com | Payments, refunds, and generating Vault access tokens | Secret key + X-Signature (Signing) |
Vault API - https://vault.sandbox.card.upprove.com | Receives card data, returns tokens, holds a CVV for its next authorization | Authorization: Bearer <vault access token> only - no X-Signature |
The Vault runs in Orchestr's PCI-audited environment, separately from the Merchant API. That is why
it has its own host and its own bearer token: your secret key never travels to it. Card data is sent
as plain JSON over TLS 1.2 or later, directly into that environment, and is tokenized on arrival.
In Live the hosts are https://api.upprove.com and https://vault.card.upprove.com.
The Vault never returns a card number. Responses carry the token and display metadata only
(card_brand, bin, last4_digits, expiry).
Before you start
- Ask Orchestr to enable the Vault API on your account.
- In Sandbox, only registered test cards can be tokenized. Any other number is
refused with
422 test_card_requiredand no token is created. - Make sure nothing on your side logs the request body of Vault calls. It is card data.
Step 1: Get a Vault access token
Generate a short-lived bearer token on the Merchant API. Sign the request like any other call.
SIGNING_SECRET="$UPPROVE_SIGNING_SECRET"
REQ_PATH="/v1/vault/access_tokens"
BODY='{}'
TS=$(date +%s)
SIG=$(printf '%s' "${TS}${REQ_PATH}${BODY}" \
| openssl dgst -sha256 -hmac "$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"
{
"access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InByb2QtMSJ9…",
"token_type": "bearer",
"expires_in": 3600,
"scope": "cards:write cvv:write"
}
Cache the token and reuse it until shortly before expires_in elapses. One token covers any number
of Vault calls. A Vault call with an expired or malformed token returns 401 unauthenticated;
fetch a new one and retry.
code | HTTP | When it happens |
|---|---|---|
vault_not_enabled | 403 | The Vault API is not enabled on this account |
Step 2: Tokenize the card
Send the card to the Vault. Include the CVV when you have it, together with the
merchant_reference you will use for the payment in step 3.
BODY='{
"card": {
"number": "4242424242424242",
"expiry_month": 12,
"expiry_year": 2030,
"holder_name": "Ada Lovelace"
},
"cvv": {
"value": "123",
"merchant_reference": "order-1001"
}
}'
curl -sS -X POST "https://vault.sandbox.card.upprove.com/v1/cards" \
-H "Authorization: Bearer $VAULT_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
--data-raw "$BODY"
{
"token": "tok_1PqRsT2eZvKYlo2C",
"card_brand": "visa",
"bin": "42424242",
"last4_digits": "4242",
"expiry_month": 12,
"expiry_year": 2030,
"cvv": {
"status": "stored",
"merchant_reference": "order-1001",
"expires_at": "2026-10-14T10:41:00Z"
}
}
The token is durable: keep it against your customer record and reuse it for later payments. The
CVV is not. It is held for 30 minutes and consumed by the one authorization whose order carries
the same merchant_reference, then deleted.
Request reference
| Field | Required | Notes |
|---|---|---|
card.number | one of card.number / card.token | 12–19 digits, no spaces. Must pass the Luhn check |
card.expiry_month | with card.number | Integer 1–12 |
card.expiry_year | with card.number | Four-digit year, not in the past |
card.holder_name | with card.number | Name as printed on the card. Needed for network tokenization. At most 255 characters, at most 5 digits |
card.token | one of card.number / card.token | An existing tok_…. Use with cvv to attach a CVV to a stored card |
cvv.value | with cvv | 3 or 4 digits |
cvv.merchant_reference | with cvv | The merchant_reference of the payment this CVV is for |
- A request with
card.tokenand nocvvis rejected: there is nothing to do. - Sending the same
merchant_referencetwice replaces the stored CVV and returns the new expiry. Use this if the first value was mistyped. - If the card was tokenized but the CVV could not be stored, the response is still
200withcvv.status: "absent". Retry withcard.tokenand the samecvv- you never need to resend the card number.
Errors
code | HTTP | When it happens |
|---|---|---|
unauthenticated | 401 | Missing, expired or invalid Vault access token |
insufficient_scope | 403 | The Vault access token does not grant this operation (cards:write for card fields, cvv:write for cvv) |
validation_failed | 400 | A field is missing or malformed: failed Luhn check, bad expiry, name rules. errors[] names the field |
card_not_found | 404 | card.token does not exist on this account |
card_not_usable | 409 | card.token is not an active, reusable, chargeable card: it was revoked, or it is a single-use token from a browser session |
test_card_required | 422 | Sandbox only: the number is not a registered test card |
bin_not_resolvable | 422 | The card's issuer range is unknown to the schemes; the card cannot be processed |
bin_lookup_unavailable | 503 | Issuer lookup is temporarily unavailable. Retry with backoff |
service_unavailable | 503 | The Vault is temporarily unavailable. Retry with backoff |
Validation errors never echo the value that failed. A rejected card number is reported by field name only.
Step 3: Create the payment
Charge the token on the Merchant API. Use the same merchant_reference you attached the CVV
to, and set collect_cvv: false so no browser step is expected. The authorization picks up the
stored CVV automatically.
SIGNING_SECRET="$UPPROVE_SIGNING_SECRET"
REQ_PATH="/v1/payments"
BODY='{
"amount": "42.00",
"currency": "GBP",
"country": "GB",
"payment_method": {
"code": "card",
"details": {
"card_token": "tok_1PqRsT2eZvKYlo2C",
"collect_cvv": false,
"device_data": {
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"ip": "203.0.113.42",
"language": "en-GB",
"color_depth": "24",
"screen_height": "1080",
"screen_width": "1920",
"time_zone_offset": "-60",
"javascript_enabled": true
}
}
},
"merchant_reference": "order-1001",
"success_url": "https://shop.example.com/ok",
"cancel_url": "https://shop.example.com/cancel",
"three_ds_return_url": "https://shop.example.com/3ds/return"
}'
TS=$(date +%s)
SIG=$(printf '%s' "${TS}${REQ_PATH}${BODY}" \
| openssl dgst -sha256 -hmac "$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"
This is the customer-present server-to-server charge described in
Server-to-server card payments: device_data feeds 3-D Secure, and a
challenge comes back as a redirect or three_ds_challenge next action depending on your
account's challenge mode. A frictionless authorization returns status: "completed" (or
"authorized" with manual capture) in the same response.
{
"order_id": "pay_5b4a3c2d",
"transaction_id": "txn_9f8e7d6c",
"merchant_reference": "order-1001",
"status": "completed",
"amount": 42.00,
"currency": "GBP",
"provider_transaction_id": "pi_3PqRsT2eZvKYlo2C1aBcD3eF"
}
Saving the card for later
To keep the card on file for merchant-initiated charges, assert consent on this first payment, exactly as a browser-collected card would:
"details": {
"card_token": "tok_1PqRsT2eZvKYlo2C",
"collect_cvv": false,
"save_card": true,
"consent": {
"captured_at": "2026-10-14T10:11:00Z",
"type": "recurring",
"reference": "tos-v3-acc-8842"
},
"device_data": { "…": "…" }
}
Consent is recorded when this authorization succeeds. From then on the token can be charged
without the customer present - see
merchant-initiated charges. A zero-amount
setup ("amount": "0" with save_card and consent) works the same way if you want to store the
card without charging it now. Both ways of confirming the customer work with save_card:
collect_cvv: false as above, or a CVV attached in step 2 and the payment created under its
merchant_reference.
Consent can only be asserted on the card's first payment: the Vault stores it together with
that authorization, which the acquirer sees as the one establishing the agreement. A token that is
already on file, has been charged before, or is not reusable is refused with 409
card_consent_not_allowed before anything is charged or recorded. To save a card you charged
earlier without consent, tokenize it again and assert consent on the new token's first payment.
Charging the card again
Customer present, with CVV. Run step 2 with card.token instead of the card fields, then
step 3 under the same merchant_reference:
{
"card": { "token": "tok_1PqRsT2eZvKYlo2C" },
"cvv": { "value": "123", "merchant_reference": "order-1002" }
}
Customer present, without CVV. Skip the Vault call entirely and go straight to step 3. Some acquirers decline or price these differently; check with Orchestr for your routing.
Merchant-initiated. No Vault call and no CVV. Create the payment with
stored_credential: { "initiator": "merchant", "reason": "…" } as described in
Server-to-server card payments. This requires
consent recorded on an earlier customer-present payment.
Rules worth knowing
- One CVV, one authorization. A stored CVV is consumed by the authorization that succeeds for
the payment carrying its
merchant_reference. A declined attempt leaves it in place, so a retry under the samemerchant_referencestill finds it until it expires. A retry under a newmerchant_referenceneeds the CVV attached again. - Mismatches fail quietly. A CVV stored under a different token or reference is simply not
found, and the payment is authorized without it. Check
cvv.statusandmerchant_referencein the Vault response before creating the payment. - 30-minute lifetime. Create the payment promptly after storing the CVV. The
expires_atin the Vault response is authoritative. - Never log the request. The body of a Vault call is card data. Keep it out of application logs, proxies and error reports on your side.
- Idempotency. Tokenizing the same card twice creates two tokens. Store the token from the first
successful response and prefer
card.tokenfor every later call.
Next steps
- Server-to-server card payments - 3-D Secure, merchant-initiated
charges, and the full outcomes and errors tables for
POST /v1/payments. - Card payments - consent types and what they allow.
- Test cards - the only numbers Sandbox will tokenize.
- Signing - the
X-Signatureheader used on the Merchant API.