Fraud checks

View as Markdown

Evaluate 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.

bash
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"
  }'
FieldTypeReqNotes
emailstringyesHashed before it leaves the edge; the plaintext is never stored
amountintegerCents. Send it when you know it; omit for a pre-cart check
card_binstringFirst 6 to 8 digits. Never send the full PAN. (bin is accepted as a legacy alias; prefer card_bin)
last4stringLast 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_countrystringISO-3166 alpha-2
fingerprintstringDevice fingerprint from the SDK
currencystringISO-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
ipstringyes¹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_typestringPURCHASE 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/checkWhat happens
Your backend (secret key)The supported path. Set ip to the shopper’s real IP.
Browser / publishable keyNot 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/charges does not accept body ip. 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's device_session to 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 /check with an explicit ip for the advisory step. Details: Charges → Client IP.

json
{
  "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…"
}
FieldNotes
statusclean, flagged, or blocked
actionallow, review, or deny. What we recommend you do
requires_3dsAdvisory: 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).
reasonsMachine-readable reason codes behind the verdict
risk_score0 to 100, higher is riskier
check_idCorrelates 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_tokenSigned 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 201 with status: "clean", action: "allow" and risk_score: 0. An outage must not stop a merchant taking payments. The tell is check_id: "" (and no risk_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 /charges runs the full fraud engine again on the data the charge actually carries. Skipping /check never 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.

bash
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.

json
{
  "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.

LeverScope of effectAPI
Ask for 3DS on one paymentthat paymentthree_ds.request on POST /v1/charges and POST /v1/payment_intents
The customer's Require 3DS flagevery later card charge for that customerGET, POST, DELETE /v1/customers/{id}/three_ds
The risk-score 3DS step-up (score 70–95)every card charge in this modeGET, POST /v1/fraud/three_ds_policy
Fraud rulesevery charge they matchGET /v1/fraud/rules (read-only)
Payment Workflow routes, incl. Attempt 3DSevery charge the route winsGET /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:

bash
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 } }'
json
{
  "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_policy reads 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 needs fraud:write, which only secret and restricted keys can hold.

The customer's Require 3DS flag#

bash
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" }'
json
{
  "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