Skip to main content

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:

  1. POST /v1/cards on the Vault API - send the card details (and the CVV, if you have it) and receive a token.
  2. POST /v1/payments on the Merchant API - charge the token under the same merchant_reference.
Enabled per account

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​

HostWhat it doesAuthentication
Merchant API - https://api.sandbox.upprove.comPayments, refunds, and generating Vault access tokensSecret key + X-Signature (Signing)
Vault API - https://vault.sandbox.card.upprove.comReceives card data, returns tokens, holds a CVV for its next authorizationAuthorization: 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.

Card data in the response

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_required and 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.

codeHTTPWhen it happens
vault_not_enabled403The 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​

FieldRequiredNotes
card.numberone of card.number / card.token12–19 digits, no spaces. Must pass the Luhn check
card.expiry_monthwith card.numberInteger 1–12
card.expiry_yearwith card.numberFour-digit year, not in the past
card.holder_namewith card.numberName as printed on the card. Needed for network tokenization. At most 255 characters, at most 5 digits
card.tokenone of card.number / card.tokenAn existing tok_…. Use with cvv to attach a CVV to a stored card
cvv.valuewith cvv3 or 4 digits
cvv.merchant_referencewith cvvThe merchant_reference of the payment this CVV is for
  • A request with card.token and no cvv is rejected: there is nothing to do.
  • Sending the same merchant_reference twice 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 200 with cvv.status: "absent". Retry with card.token and the same cvv - you never need to resend the card number.

Errors​

codeHTTPWhen it happens
unauthenticated401Missing, expired or invalid Vault access token
insufficient_scope403The Vault access token does not grant this operation (cards:write for card fields, cvv:write for cvv)
validation_failed400A field is missing or malformed: failed Luhn check, bad expiry, name rules. errors[] names the field
card_not_found404card.token does not exist on this account
card_not_usable409card.token is not an active, reusable, chargeable card: it was revoked, or it is a single-use token from a browser session
test_card_required422Sandbox only: the number is not a registered test card
bin_not_resolvable422The card's issuer range is unknown to the schemes; the card cannot be processed
bin_lookup_unavailable503Issuer lookup is temporarily unavailable. Retry with backoff
service_unavailable503The 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 same merchant_reference still finds it until it expires. A retry under a new merchant_reference needs 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.status and merchant_reference in the Vault response before creating the payment.
  • 30-minute lifetime. Create the payment promptly after storing the CVV. The expires_at in 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.token for 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-Signature header used on the Merchant API.