Fraud checks
View as MarkdownEvaluate a shopper before you charge them, and report abuse you have confirmed
yourself. These endpoints are advisory: POST /charges runs the full fraud engine
again server-side, so a check that says allow never overrides a decline at charge
time, and a check you skip never weakens the real decision.
Use them to fail fast: stop a hopeless checkout before the shopper types a card, or step up to 3DS on a borderline one.
Integrator must-dos (IP when you proxy, card_bin+last4, token is not pre-approval):
Integration hard rules.
POST /v1/check#
Requires the check:read scope.
curl https://api.worthypayments.com/v1/check \
-H "x-api-key: sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{
"email": "shopper@example.com",
"amount": 4999,
"card_bin": "424242",
"billing_country": "US",
"fingerprint": "wp_fp_9f2c...",
"transaction_type": "one_off"
}'
| Field | Type | Req | Notes |
|---|---|---|---|
email | string | yes | Hashed before it leaves the edge; the plaintext is never stored |
amount | integer | Cents. Send it when you know it; omit for a pre-cart check | |
card_bin | string | First 6 to 8 digits. Never send the full PAN. (bin is accepted as a legacy alias; prefer card_bin) | |
last4 | string | Last 4 of the PAN. Send with card_bin so the decision token can bind the card; omit either and that binding is not enforced later | |
billing_country | string | ISO-3166 alpha-2 | |
fingerprint | string | Device fingerprint from the SDK | |
currency | string | ISO-4217, lowercase. Defaults to usd. Sent with amount, it is bound into the decision token. A token minted as usd cannot be presented for a eur charge | |
ip | string | yes¹ | Buyer’s public IP. ¹Nominally optional (we fall back to the request peer), but the peer is your server, so in practice this is required. See Client IP |
transaction_type | string | PURCHASE or SUBSCRIPTION. Advisory only. It is recorded, not scored |
Client IP (effectively required)#
IP is one of the strongest fraud signals (velocity, blocklist, geo, cascade). Wrong IP makes the verdict useless or noisy.
Always set ip yourself. The endpoint does fall back to the request peer, but the
only key that can call it is a secret key on your server, so the peer is your egress
address, not the buyer's. Omitting ip scores your data centre.
Who calls /v1/check | What happens |
|---|---|
| Your backend (secret key) | The supported path. Set ip to the shopper’s real IP. |
| Browser / publishable key | Not possible. A pk_ is narrowed server-side to payments:charge and scope-none endpoints and can never hold check:read, so the call returns 403 permission_denied regardless of what the body contains. Proxy it through your own server. |
Resolve the shopper IP the same way a well-configured proxy does: the hop your
load balancer or CDN appended (not the left-most X-Forwarded-For entry, which the
client can forge). Pass a single public IPv4/IPv6 string, e.g. "203.0.113.42".
POST /v1/chargesdoes not accept bodyip. Charge IP is never read from the body, so it cannot be spoofed by a caller. A charge built on your server records your egress address as your server's, never scored or blocklisted as a buyer's; send the buyer'sdevice_sessionto score it on the buyer's IP. The Payment Element does not need one: the browser confirms directly, so the peer is the buyer. Either way, use/checkwith an explicitipfor the advisory step. Details: Charges → Client IP.
{
"object": "fraud_check",
"status": "clean",
"action": "allow",
"requires_3ds": false,
"reasons": [],
"risk_score": 12,
"check_id": "8300054a-bc4d-4746-861f-63804710d69d",
"risk_decision_token": "cmR0MXxt….9f8e7d…"
}
| Field | Notes |
|---|---|
status | clean, flagged, or blocked |
action | allow, review, or deny. What we recommend you do |
requires_3ds | Advisory: if true, expect a possible 3-D Secure challenge on the later charge. To run 3DS on that charge whatever your rules decide, send three_ds.request (any, challenge or challenge_mandated). When the charge returns requires_action, complete the challenge and resubmit with three_ds.session_id (see 3-D Secure). |
reasons | Machine-readable reason codes behind the verdict |
risk_score | 0 to 100, higher is riskier |
check_id | Correlates this evaluation with the later charge in your dashboard. An empty string means the engine was never reached (see the fail-open note below) |
risk_decision_token | Signed receipt for this evaluation. Echo it on the charge to tie the two together. See Risk decision token. Absent when token signing is not configured |
This endpoint fails open, and you can detect it. If the fraud engine is unreachable the call still returns
201withstatus: "clean",action: "allow"andrisk_score: 0. An outage must not stop a merchant taking payments. The tell ischeck_id: ""(and norisk_decision_token): a real evaluation always carries an id. Treat that response as unscreened, not as approved. The charge itself is unaffected. It runs its own fraud pass server-side either way.
This endpoint is advisory.
POST /chargesruns the full fraud engine again on the data the charge actually carries. Skipping/checknever bypasses screening, and passing it never pre-approves a charge. What the token adds is provenance: a charge that drifts from the evaluation it claims (different amount, card or device) is rejected.
Linked-account blocks#
BLOCKED_BYPASS_LINK means an active device or IP link points to a customer who is
currently blocked in the same merchant account and environment. It returns
status: "blocked", action: "deny", and risk_score: 99. The link reason is a
fixed code, never a customer or anchor identifier. This check runs after the inline
Cascade check and before velocity; an earlier allowlist match or disabled fraud
screening still wins.
/check has no customer or card, so a buyer allowlisted only by customer or card
can still be denied by /check on a device/IP link that the charge then approves.
The Cascade sync-block switch controls both device and IP link checks. The device switch additionally controls device links. The inline-IP switch does not control IP links. Missing settings and NULL device/sync-block values keep link protection on; stored false disables the corresponding protection.
Blocked attempts still record link evidence while these switches are off. Device links last 30 days and IP links 7 days; re-enabling protection re-arms links that have not expired and still point to a blocked customer.
POST /v1/check/recheck#
Identical request and response. Call it once the final amount is known. A cart that grew after the first check is exactly the case a single check misses.
POST /v1/report#
Report abuse you have confirmed yourself, so it counts against future attempts.
Requires the report:write scope.
curl https://api.worthypayments.com/v1/report \
-H "x-api-key: sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{ "email": "abuser@example.com", "action": "block", "reason": "confirmed chargeback fraud" }'
action is block or flag. block denies future attempts outright; flag marks
them for review without stopping them.
GET /v1/blocklist#
Read back what is currently blocked for your account. Requires blocklist:read.
Paginates like every other list. See Pagination.
{
"object": "list",
"data": [
{
"object": "blocklist_entry",
"id": "d2fb8af6-d015-4a27-ac59-f43ec314ae64",
"type": "email",
"value": "abuser@example.com",
"reason": "confirmed chargeback fraud",
"expires_at": null,
"created_at": "2026-07-27T10:00:00Z",
"is_global": false
}
],
"has_more": false
}
is_global: true marks a platform-wide entry. You cannot remove those, and they apply
across every merchant.
3DS and fraud controls from your own tools#
If you keep your own admin tooling, these endpoints let it read and drive the same 3DS and fraud levers as the dashboard, so you do not maintain two rule sets by hand.
| Lever | Scope of effect | API |
|---|---|---|
| Ask for 3DS on one payment | that payment | three_ds.request on POST /v1/charges and POST /v1/payment_intents |
| The customer's Require 3DS flag | every later card charge for that customer | GET, POST, DELETE /v1/customers/{id}/three_ds |
| The risk-score 3DS step-up (score 70–95) | every card charge in this mode | GET, POST /v1/fraud/three_ds_policy |
| Fraud rules | every charge they match | GET /v1/fraud/rules (read-only) |
| Payment Workflow routes, incl. Attempt 3DS | every charge the route wins | GET /v1/payment_workflow/routes (read-only) |
A rule outranks a request. You can always start 3DS — on one payment with
three_ds.request, or for a customer with the flag — but nothing you send can switch it
off when our side asks for it. If a fraud rule, a detector, the customer's flag or the
winning route asks for 3DS, the charge is stepped up and challenged even when the request
asked for something softer. Clearing the customer's flag lifts only the flag; the rules
and routes keep applying.
The risk-score 3DS step-up#
Every card charge gets a fraud risk score from 0 to 100. By default a charge scoring 70
to 95 is stepped up to 3DS, whatever your own rules decide. If you choose 3DS yourself
with three_ds.request, turn that off so your rules are the only trigger:
curl https://api.worthypayments.com/v1/fraud/three_ds_policy \
-H "x-api-key: sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{ "risk_step_up": { "enabled": false } }'
{
"object": "three_ds_policy",
"risk_step_up": { "enabled": false, "min_score": 70, "max_score": 95 },
"livemode": false
}
- The body sets the whole band. To narrow it instead, send
{ "risk_step_up": { "enabled": true, "min_score": 85 } }. A bound you leave out goes back to its default (70 or 95). - It applies from the next charge. TEST and LIVE keep separate settings; your key
decides which one you change.
GET /v1/fraud/three_ds_policyreads it. - It lifts only the score band. A charge is still stepped up when a fraud rule asks for it, the customer is marked Require 3DS, or the route has Attempt 3DS on, and a score above your decline threshold still declines.
- Scopes: reading needs
fraud:read; changing needsfraud:write, which only secret and restricted keys can hold.
The customer's Require 3DS flag#
curl https://api.worthypayments.com/v1/customers/cus_5f0c1f7e-8d2a-4c1b-9a3e-2b7d6c5e4f10/three_ds \
-H "x-api-key: sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{ "reason": "Chargeback on file" }'
{
"object": "customer_three_ds",
"customer": "cus_5f0c1f7e-8d2a-4c1b-9a3e-2b7d6c5e4f10",
"require_3ds": true,
"livemode": false
}
POST turns it on, DELETE clears it and GET reads it. It is the flag the dashboard
shows under Restrictions on the customer's page, so a change made in either place is
visible in the other. Both writes are safe to repeat and answer the flag as read back
after the write. DELETE clears the flag whoever set it, a fraud detector included —
exactly what Turn off does in the dashboard. A change made through the API is recorded
against your key, so it can be told apart from a dashboard click. TEST and LIVE keys
see separate flags. An unknown customer id is a 404.
Reading your rules and routes#
GET /v1/fraud/rules returns your fraud rules in evaluation order with their
conditions, action and enabled state; requires_3ds marks a rule that steps the
charge up. GET /v1/payment_workflow/routes returns the Payment Workflow canvas route by
route, in the order it is evaluated, with require_3ds for the route's Attempt 3DS
toggle. Each route also keeps one fraud rule in step with it; that rule's route field
names the route.
Both are read-only. Creating, editing or disabling rules and routes stays in the dashboard: a key that could rewrite them could also switch your fraud protection off or send money to a different processor, and opening that up needs its own decision. Tell us if your integration needs it.
Scopes#
These endpoints are server-side only. A publishable (pk_) key can never reach
them: the authority is the key, not the request body, so no browser call can grant
itself check:read, report:write, fraud:read or fraud:write. fraud:read covers
the three reads above; fraud:write covers setting and clearing the customer flag.
Related: Authentication · Errors · 3-D Secure
