Risk engine
Before any provider sees a card-family payment, Finscale scores it. Most payments pass in milliseconds; the risky few are held for review or refused outright — before an authorization ever happens. This page explains the risk object, the three decisions, and the signals behind the score.
Every card payment is screened
Risk screening runs inside POST /v1/payments, after validation and before routing. It always runs for the card family — card, apple_pay, google_pay. Methods where the customer authenticates at their own bank (bank redirects, real-time payments) are not risk-scored, and their risk field is null.
Three decisions, three behaviors:
risk.decision | What happens | What you see |
|---|---|---|
approved |
The payment proceeds to routing and providers, uninterrupted. The overwhelming majority of traffic. | Normal lifecycle — processing → succeeded or failed. |
review |
The payment holds in processing. No provider is attempted until an operator approves or blocks it in the dashboard. |
payment.risk_review fires; the payment stays processing until resolved. |
blocked |
The payment is refused. No provider is attempted, no authorization happens, the customer is never charged. | status: failed with failure_code: payment_blocked. |
The risk object
Once screening runs, the assessment is attached to the payment as risk — on the API response, on every retrieve, and in every webhook payload:
"risk": {
"score": 17,
"decision": "approved",
"checks": {
"avs_result": "pass",
"cvv_result": "pass"
}
}
| Field | Type | Meaning |
|---|---|---|
score | integer, 0–99 | Composite risk score — 0 is lowest risk, 99 highest. Deterministic in test mode. |
decision | enum | approved · review · blocked — what screening decided (table above). |
checks.avs_result | enum | pass · fail · unavailable — Address Verification System result. |
checks.cvv_result | enum | pass · fail · unavailable — card verification code result. |
risk is null for methods that are not risk-scored — an ideal or pix payment carries "risk": null, because the customer authenticated at their bank and the fraud model belongs to that rail.
Score bands
The score maps to a decision through thresholds. These are the platform defaults — both cut-offs are tunable per account from the dashboard, so a marketplace selling gift cards can run tighter than a subscription business:
| Score | Band | Default outcome |
|---|---|---|
0–39 | low | approved |
40–79 | elevated | approved — but a hard signal (velocity breach, country restriction) can still escalate to review |
80–89 | high | review — held for a human decision |
90–99 | critical | blocked |
Tuning is a trade: lower the review threshold and you catch more fraud but slow more good customers. Watch two dashboard numbers when you move it — the review queue's false-positive rate, and your dispute rate. Disputes are the lagging truth about where the threshold should sit.
The review flow
A review decision holds the payment in processing and fires payment.risk_review. Nothing has been authorized yet — the hold happens before any provider attempt, so approving later runs a fresh, normal authorization. In test mode, the magic amount 4991 forces this flow:
curl https://api.finscale.dev/v1/payments \
-H "Authorization: Bearer sk_test_51FinscaleDemo…" \
-H "Idempotency-Key: idem_risk_review_demo_01" \
-H "Content-Type: application/json" \
-d '{
"amount": 4991,
"currency": "EUR",
"payment_method": "card",
"payment_method_id": "pm_7Wq2xN9dT4Ls",
"reference": "ord_9f21_0716"
}'
{
"id": "pay_7Kd2nQ5wR8Xm",
"object": "payment",
"amount": 4991,
"currency": "EUR",
"status": "processing",
"payment_method": "card",
"payment_method_id": "pm_7Wq2xN9dT4Ls",
"reference": "ord_9f21_0716",
"provider": null,
"provider_attempts": [],
"risk": {
"score": 82,
"decision": "review",
"checks": { "avs_result": "pass", "cvv_result": "pass" }
},
"created_at": "2026-07-16T11:08:44Z",
"livemode": false
}
Resolve the hold in the dashboard's review queue:
-
Approve
The payment leaves the hold and enters routing as if nothing happened — provider attempts run, and
payment.succeededorpayment.failedfollows as usual. -
Block
The payment moves to
failedwithfailure_code: payment_blocked, andpayment.failedfires. The customer was never charged.
processing payment to your code. Don't spin on the status — show your normal "payment in progress" state, listen for the webhook that resolves it, and confirm the order asynchronously. Reviews are resolved by humans; minutes, not milliseconds.
The blocked flow
A blocked decision refuses the payment before any provider sees it. The create call still returns 201 — the refusal is a payment outcome, not a request error:
{
"id": "pay_9Tn4xW2mK6Rd",
"object": "payment",
"amount": 4992,
"currency": "EUR",
"status": "failed",
"provider": null,
"provider_attempts": [],
"risk": {
"score": 96,
"decision": "blocked",
"checks": { "avs_result": "fail", "cvv_result": "fail" }
},
"failure_code": "payment_blocked",
"failure_message": "The payment was blocked by risk screening.",
"livemode": false
}
Show the customer a generic decline message. Don't surface "blocked by risk screening" at checkout — it teaches card testers exactly which of their inputs tripped the wire.
Signals behind the score
The score is a composite. These are the signal families the platform weighs — the first is visible on the payment; the rest are capabilities you configure and observe in the dashboard.
AVS & CVV verification
The two instrument checks ride on risk.checks for every screened payment:
| Check | pass | fail | unavailable |
|---|---|---|---|
avs_result |
Billing address matched the issuer's records — lowers the score. | Address mismatch — raises the score, sharply when combined with other signals. | The issuer or method doesn't support AVS — common outside a handful of markets. Neutral. |
cvv_result |
Verification code matched — lowers the score. | Code mismatch — a strong fraud signal; stolen numbers usually travel without the code. | Not checked (wallet flows, network tokens with cryptograms). Neutral. |
A fail never blocks on its own — plenty of legitimate customers typo their postcode. It moves the score; the composite decides.
Velocity rules
Velocity rules count events over sliding windows and escalate when a count breaks its limit — the signature of card testing is many attempts, small amounts, one origin. Configurable dimensions include:
- Attempts per card — the same instrument retried across a short window.
- Cards per device or IP — one origin cycling through many card numbers; the strongest card-testing signal.
- Attempts per customer email — one identity, many instruments.
Each rule carries its own escalation — push the score up, force review, or blocked outright. Velocity breaches are the usual reason an otherwise mid-score payment lands in the review queue.
BIN intelligence
The leading digits of a card identify the issuer, and with it the issuing country, funding type (debit, credit, prepaid), and card class. Screening cross-references them: a prepaid card, issued in one country, used from an IP in a second, shipping to a third, scores very differently from a domestic debit card behaving domestically. The derived attributes surface on the payment in payment_method_details.card — funding, country, brand — so your own systems can use them too.
Country restrictions
Per-account allow and block lists over issuing country and customer IP country. A payment matching a blocked country goes straight to decision: blocked — no score debate, no provider attempt. Configure them in the dashboard; use them for sanctioned markets, corridors you can't ship to, or fraud hotspots you've measured. Restrictions here complement routing rules: a routing block refuses traffic you never want to process, a risk country restriction refuses traffic you consider fraudulent.
Device fingerprinting
Hosted checkout and Finscale fields collect device and browser signals automatically during entry — no work on your side. The signals feed velocity counting (cards per device) and anomaly checks (headless browsers, automation frameworks, impossible travel between attempts). A server-only API integration carries none of these signals, so card-testing detection leans harder on IP and card velocity — one more reason to take card input through Finscale-served surfaces, alongside the PCI scope argument.
Webhook events
| Event | When |
|---|---|
payment.risk_review | Screening held the payment for review. The payload is the full payment, risk.decision: "review", still processing. |
payment.succeeded | After approval (or straight through for approved), a provider approved the payment. |
payment.failed | Blocked at screening, blocked in review, or declined by providers after approval — failure_code tells you which. |
Handle payment.risk_review if you run time-sensitive fulfillment — it's your signal to pause the order rather than time it out. Signature verification and retry semantics are on the Webhooks page.
Error scenarios
| Scenario | What the API returns |
|---|---|
| Payment blocked at screening | 201 with status: failed, failure_code: payment_blocked, empty provider_attempts. Not an error envelope — the request was valid; the payment was refused. |
| Review resolved as block | No new API response — payment.failed fires with failure_code: payment_blocked. |
| Blocked by a routing rule, not risk | failure_code: blocked_by_routing_rule — your own rule matched. risk may still say approved. |
| Method not risk-scored | "risk": null on the payment — expected for bank redirects and real-time methods, not a bug. |
Related
- Cards & 3DS — 3-D Secure authentication and liability shift, the other half of card fraud defense.
- Disputes — what happens when fraud gets through, and how to fight it.
- Smart routing — where an approved payment goes next.
- Testing — magic amounts
4991(review) and4992(blocked) in test mode.