Risk Decision Token
View as MarkdownA signed receipt for one POST /v1/check evaluation. Echo it on
POST /v1/charges and the charge records which decision it followed.
POST /v1/check → risk_decision_token: "cmR0MXxtYV8x….9f8e7d…"
POST /v1/charges → { …, "risk_decision_token": "cmR0MXxtYV8x….9f8e7d…" }
Optional to send. Strictly enforced once sent.
For the full list of integrator must-dos (IP, 3DS, idempotency, keys), see Integration hard rules.
What it is not#
It does not pre-approve a charge.
A charge carrying a valid token runs exactly the same fraud engine as a charge carrying none: pre-screen, scoring, device intelligence, the 3DS step-up decision, the block gate. The token can only ever cause a charge to be rejected. It can never cause one to skip a check.
This is a deliberate design decision, not a limitation we plan to remove. Minutes pass between a decision and a capture, and blocklists, velocity counters and rules all change inside that window. A system that treats a decision as authorization is one where an attacker who obtains one clean evaluation can spend against it. So the token carries provenance, and the engine keeps the authority.
What you get from sending it:
- A charge cannot silently drift from the decision it claims. A different amount, card or device is rejected.
- Provenance in the ledger. The charge records the
check_id, so a dispute can be traced back to the evaluation that preceded it. - A single-use guarantee. One evaluation backs one charge.
What it binds#
The token is signed over the merchant, the verdict, and whichever of these you
supplied to /v1/check:
Field on /v1/check | Bound as | Enforced at charge time |
|---|---|---|
| nothing to send | your merchant account | Always |
| nothing to send | the verdict (allow / review / deny) | Always |
amount + currency | the exact amount | When you sent an amount |
card_bin + last4 | the card's public identity | When you sent both |
fingerprint | the device | When you sent one |
A field you omit is not bound, and the matching check is skipped. That is
stated plainly rather than hidden: if you call /v1/check without an amount, the
token cannot stop an amount escalation, because the evaluation never saw an
amount to compare. Send last4 alongside card_bin if you want the card
binding; send fingerprint if you want the device binding.
Valid for 15 minutes. Single use.
Errors#
All are 400 invalid_request_error with param: "risk_decision_token", except
where noted.
| Code | Cause | Fix |
|---|---|---|
risk_decision_token_invalid | Bad signature, unknown format, or issued to a different account | Do not retry with this token. Call /v1/check again. |
risk_decision_token_expired | Older than 15 minutes | Call /v1/check again. |
risk_decision_token_amount_mismatch | Charge amount or currency differs from the evaluated one | Charge the evaluated amount, or re-evaluate with /v1/check/recheck. |
risk_decision_token_card_mismatch | A different card than the one evaluated | Re-evaluate for the card you are charging. |
risk_decision_token_device_mismatch | A different device fingerprint | Send the same fingerprint you sent to /v1/check. |
risk_decision_token_reused | Already spent | One token, one charge. Call /v1/check again. |
card_declined (402, card_error) | The token carries a deny verdict | Do not retry. Show a generic decline. |
service_degraded (503, api_error) | Verification is temporarily unavailable | Retry without the token. The charge runs the full fraud engine anyway. |
That last row is the one to build for. If token verification is degraded we refuse rather than accept an unverified token, but the recovery is a plain retry with the field omitted, and the payment goes through fully screened. An infrastructure blip costs you provenance, never the sale.
Amount changes between check and charge#
Shipping added, a discount applied, tax recalculated: the token no longer matches. Do not strip the token to make the error go away; re-evaluate:
curl -X POST https://api.worthypayments.com/v1/check/recheck \
-H "X-Api-Key: sk_live_…" \
-H "Content-Type: application/json" \
-d '{ "email": "buyer@example.com", "amount": 5499, "currency": "usd",
"card_bin": "424242", "last4": "4242", "fingerprint": "fp_9a1c2b" }'
You get a fresh verdict at the real amount, and a fresh token.
Deny tokens#
A deny evaluation is signed too. It authorizes nothing, and presenting it on a
charge declines that charge. What it gives you is something to file when a customer
disputes a rejected order: proof of what we told you, and when.
It also means the field is present for every verdict, deny included, whenever token
signing is configured. A token that appeared only for allow would leak the verdict to
anyone who could see the response shape.
Two cases omit it, and neither is a verdict: token signing is not configured on the
platform, or the fraud engine was unreachable and the response is a fail-open default. The
second is identifiable by check_id: "" (see Fraud
checks.)
Not using it#
Omit the field. Every charge runs the full fraud engine regardless, which is what your integration does today and will keep doing. The token is for merchants who want the decision and the charge provably tied together.
