Fraud prevention

Score sign-ups, logins and payments with /fraud/v1/decide, report events with /fraud/v1/events, and get a webhook when a case opens.

Stop fake accounts, card testing and abuse with one API. openshield runs openfraud, which is open source, for you under /fraud/v1. Call decide before a sign-up, a login or a payment, and get allow, step_up, review, hold, block or quarantine with the reasons. Call events to report what happened.

It scores IP data (datacenter, VPN, Tor, ASN), disposable email domains, phone numbers, device and browser signals, velocity counters and linked accounts. Every rule shows its reasons, and new rules start in shadow before they act.

Your organization gets an instance of its own: its events, accounts and cases never mix with anyone else's. It runs on our server in Germany, and your events, decisions and cases are kept 30 days until your first top-up and 90 days after. The fraud API is in beta.

Turn it on

An owner or an admin turns it on in the dashboard, under Fraud prevention. The instance starts in a few seconds. Then create an API key with fraud access (API keys, then Create):

Fraud accessScopeWhat the key may call
Decide and report events onlyfraud_eventsdecide, events, events/batch, appeals and statements
All of itfraudevery route below, cases, accounts, lists and erasure included

A backend that only reports sign-ups and payments needs no more than fraud_events. Keep fraud keys for your back office.

Decide before you act

Call decide before a sensitive action (a sign-up, a login, a payment, a message sent to many people) and act on its answer.

curl -X POST "https://api.openshield.dev/fraud/v1/decide" \
  -H "Authorization: Bearer $OPENSHIELD_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "account.created",
    "actor_id": "u_123",
    "email": "someone@example.com",
    "ip": "203.0.113.50",
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
    "language": "fr-BE,fr;q=0.9",
    "country": "BE",
    "device": "dev_8f2c41"
  }'

openfraud's SDKs work as they are: point them at https://api.openshield.dev/fraud.

import { Client } from "openfraud"

const fraud = new Client({ baseUrl: "https://api.openshield.dev/fraud", apiKey: process.env.OPENSHIELD_KEY, timeoutMs: 800 })

const d = await fraud.decide({ type: "account.created", actor_id: user.id, email, ip, user_agent, language, country, device })
if (d.decision === "block" || d.decision === "quarantine") return refuse()
if (d.decision === "step_up") return askForProof()

The SDKs fail open: on a timeout, a 429 or a 5xx they retry, then answer allow. Give them at least 800 ms: a call goes through openshield's edge before it reaches your instance.

The answer has the decision (allow, step_up, review, hold, block or quarantine), the rules that fired and why, the actions to apply, and the case it opened, if any. openfraud's API reference describes every field. Besides those, the answer carries:

FieldWhat it holds
signalswhat the facts show (new_device, tor_ip, disposable_email_domain...), and the signal of every rule that fired, acting or in shadow
scoresaccount_takeover, account_abuse and bot, each from 0 to 1: a summary of the rules that fired, by family
liststhe lists this event changed, as [{"list": "trusted_devices", "action": "add"}]
stopped_bythe allow rule that allowed the event over the other rules, or null
matched_actorfor an event sent before sign-in (no actor), the account its email address belongs to, when there is one. Only a fraud key sees it: a fraud_events key gets null. It is information: never act on that account from it (see before sign-in)
sandboxtrue when the answer comes from your sandbox

Send the end user's context, not your server's

The gateway passes your request body to your instance untouched. It never fills these fields from its own request: that request comes from your backend, and its address, User-Agent and language describe your server.

FieldWhat to send
ipThe end user's address. Behind a proxy, the first X-Forwarded-For entry you trust.
user_agentThe browser's User-Agent header. Your backend's own (node, python-requests) makes every event look scripted.
languageThe browser's Accept-Language header.
countryFrom your CDN: CF-IPCountry, CloudFront-Viewer-Country.
deviceThe id from openfraud's device script, and attrs.webdriver from its automated().

email, phone, ip and device are replaced by keyed pseudonyms before any rule sees them, and the raw values are not stored. The User-Agent is read for the browser, the system and the signs of automation, then dropped.

Report what happened

POST /fraud/v1/events reports an event after the fact (a payment that went through, a refund, a dispute) without waiting for a decision. POST /fraud/v1/events/batch takes up to 100 at once. Rules learn from these: a card used by many accounts, a trial taken again and again.

Before sign-in, and events a page reports

Before sign-in there is no account to name: send a failed login or a password reset with the email the person typed, and no actor_id. Anyone can type someone else's address, so such an event is evidence about the sender, never about the account it matches. Your instance counts it under the sender's address, network, device and email domain, and never under the account: a burst of attempts with someone's address cannot change that account's decisions, counters, lists or trusted devices.

Each event says who saw it happen, in reported_by:

ValueWhen
server (the default)your backend saw it and knows who is signed in: a login it checked, a payment it took
clientyour page or app sent it, and your backend only passes it on without checking who is signed in

A client event is treated like one before sign-in: its actor_id, tenant_id and owner_id are dropped, since a page can claim any of them. If your front end reports events (a page view, a form opened before sign-in) through your backend, mark them "reported_by": "client". Any other value is refused with a 400.

Send challenge.succeeded and challenge.failed from your server, after it checked the proof itself, never relayed from the page: only a server-reported pass trusts the device (see step-up challenges).

Rules read event.reported_by too, so a rule of yours can weigh or leave out the events a page reported.

Lists

Lists hold the values your rules match: accounts, devices, emails, addresses. Read and fill them with a fraud key:

RouteWhat it does
GET /fraud/v1/listsyour lists
GET /fraud/v1/lists/{name}/itemsthe items added at run time, with their author, end and note (?archived=true includes the archived ones)
POST /fraud/v1/lists/{name}/itemsadds items, each a string or {"value", "second", "for", "note"}; for (for example 7d) overrides the list's expiry
DELETE /fraud/v1/lists/{name}/items?item=...removes an item
POST /fraud/v1/lists/{name}/items/archivearchives items, given as the entries of the list show them: they stop matching and stay on record
POST /fraud/v1/lists/{name}/items/unarchivegives archived items back

A list is read item by item: it cannot be exported as a file.

Look up an IP address

GET /fraud/v1/ips/{ip} answers what your instance knows of an address: its type, its network (asn, org, country) and the lists that hold it. POST /fraud/v1/ips/query does the same for up to 1,000 addresses, sent as {"ips": ["203.0.113.50", "198.51.100.7"]}, and answers {results, meta: {total, valid}}, with the invalid ones left out. A longer batch is refused with 400 too_many_ips. Both need a fraud key.

A lookup is billed for each valid address it answers for, not for the ones that were left out. A batch reserves the price of 1,000 lookups from your balance before it runs, and gives back what it did not use.

Prices

CallPrice
Decision (/fraud/v1/decide)EUR 0.001
Event (/fraud/v1/events, each accepted event of a batch)EUR 0.0002
IP lookup (/fraud/v1/ips/{ip}, each valid address of ips/query)EUR 0.0002
With the network data on, each decision+ EUR 0.0002
With the activity bursts on, each decision+ EUR 0.0005

The network data and the activity bursts hold more in your instance's memory, so a decision costs more while they are on. You turn them on and off in the fraud settings of the dashboard.

Every price comes from the same prepaid balance as the checks, the monthly free use included. A call that does not end in a 2xx costs nothing. The other routes (cases, accounts and lists) are free.

What your instance checks

The core always runs: blocklists you fill, trust earned by paying, and account takeover signals. In the dashboard you add rule packs for the kind of abuse you see: free tiers and trials, cards and payments, messaging, marketplaces and payouts, APIs and compute, hosting and content. Every pack rule starts in shadow: it says what it would do, in shadow, without doing it, until it is promoted to act. When rules start to act says how.

The network data (the operator and country of each address, and the lists of disposable email domains, Tor exits, data centres and VPNs) comes with your first top-up. openshield refreshes it once a day, and your instance downloads it from inside the node: it reaches nothing on the internet.

Settings

Four more settings sit next to the packs in the dashboard.

  • Activity bursts (from your first top-up, off by default). Flags an account, a device or an address that sends far more events than its usual hour: the signals high_activity_account, high_activity_device and high_activity_ip. It keeps 7 days of counters, so the memory it uses grows with your traffic.
  • Email domain check (from your first top-up, off by default). Asks DNS whether the domain of an email address takes mail, once a day per domain, and raises unreachable_email when it does not. It sends the domain only, never the address, through a resolver inside the node. It uses DNS only, no extra memory.
  • Require the device token (off by default). An event without a device_token counts as missing. Turn it on once every page sends the device token.
  • Webhook types (all by default). Narrow the types posted to your webhook address. The types are decision.step_up, decision.review, decision.hold, decision.block, decision.quarantine, case.opened, case.updated, case.resolved, case.appealed, actor.status_changed, tenant.status_changed, brake.engaged, brake.released, and the three list types, list_item.created, list_item.archived and list_item.unarchived, which are posted when an item is added to a list, archived or restored. While you keep all of them, a type added in a later release reaches you; once you narrow the list, you receive only the types you ticked.

VPN and residential proxies

The network data tells a data centre or a Tor exit. A VPN exit and a residential proxy (a home connection rented out to hide who sits behind it) need fresher data, so openshield looks the address up when the event comes in. Turn on VPN and residential proxy facts in the dashboard, from your first top-up. It is off by default, and it costs nothing more: the lookup is included in the price of the call.

When it is on, openshield adds four attributes to the attrs of an event that has an ip, on /fraud/v1/decide, /fraud/v1/events and /fraud/v1/events/batch, before the event reaches your instance:

AttributeValues
attrs.os_ip_tunnelvpn, proxy, tor, relay or none
attrs.os_proxy_typeresidential, datacenter, isp or unknown; a line is classed only when a tunnel was detected
attrs.os_tunnel_operatorthe name of the service that runs the address, when known
attrs.os_tunnel_last_seen_dayswhole days since the address was last seen acting as a tunnel, when known

The os_ prefix is openshield's: an attribute you send under another name is never touched, and one you send under an os_ name is replaced by the lookup's own. Without the setting, or when the lookup does not answer within 300 ms, the event goes through exactly as you sent it: a slow or failed lookup never refuses an event, it only leaves the attributes out. An address is looked up once a day.

Two rules read the attributes, in the openshield-tunnels rules that join your instance with the setting. Both start in shadow, like every pack rule:

RuleSignalFires when
residential_proxyresidential_proxy (abuse)os_proxy_type is residential and the address was last seen 7 days ago or less, or the days are unknown
vpn_tunnelvpn_tunnelos_ip_tunnel is vpn

A VPN is no proof of fraud (many honest users run one), so vpn_tunnel weighs little; read it with the other signals. The facts come from a third-party data source (proxycheck.io), which openshield sends the address and nothing else of the event. Its data is never exposed: your instance gets the four attributes above for the address of each event, and neither the source's lists nor its answers are served to you or to anyone else.

When rules start to act

  • Day 1. The core blocklists act from the first call. Every other rule starts in shadow: the answer lists what it would have done under shadow, and nothing is blocked because of it.
  • Automatic promotion, on by default. Every day after the first 30, openshield looks at each shadow rule that hit at least 20 accounts, and promotes it to act when it has proved reliable. Either you labelled the accounts it hit, with a precision of at least 95 % over at least 10 labelled accounts, or our decision model reviewed pseudonymized timelines of up to 10 accounts the rule hit and found at least 90 % of them fraudulent.
  • What a promoted rule may do. Whatever the rule is built for, a block included.
  • Manual mode. You can switch to manual promotion, so that a rule acts only when a person promotes it. Or you can keep automatic promotion but limit it to rules whose action is hold or review, so that a rule that blocks is promoted by a person only.
  • You are told. Every automatic promotion is emailed to you and recorded in the audit log.
  • A brake. A brake stops the acting rules when a spike of decisions shows up.
  • Back to shadow. A promoted rule whose labelled precision falls under 80 % goes back to shadow on its own.

Labelling the accounts you know to be fraud or legitimate (POST /fraud/v1/actors/{id}/label) is what makes promotion reliable, and what demotes a rule that has gone wrong.

Webhooks

Set a webhook address in the dashboard. Every decision other than allow, every change to a case, an account or a workspace, and every item added to a list, archived or given back (list_item.created, list_item.archived, list_item.unarchived), is posted to it, signed with the Standard Webhooks scheme under the secret the dashboard shows. Any Standard Webhooks library checks them, as does the SDK:

import { verifyWebhook } from "openfraud"

const event = await verifyWebhook(process.env.OPENSHIELD_FRAUD_WEBHOOK_SECRET, req.headers, rawBody)

Answer with a 2xx within 10 seconds. A webhook that is not delivered is sent again for about two days, under the same webhook-id; a 410 stops it.

Sandbox

Test your integration without touching your live data. Create an API key and pick Test as its environment: you get an om_test_ key, which reaches a sandbox of your own and nothing else. The sandbox is a separate instance, with no rule pack and no network data, so it never sees your live accounts, lists or cases, and a live key never reaches it.

  • Keys. An om_test_ key has the fraud or fraud_events scope, for all of the sandbox, and is refused by the moderation API. It counts apart from your live keys against the key limit your top-ups give. Send it the way you send a live key: Authorization: Bearer om_test_....
  • Free, with limits. Each organization may send the sandbox 5 requests a second and 10,000 calls a month (decisions, events and IP lookups, a batch counting each event or address). Past the rate you get 429 rate_limited; past the month you get 429 sandbox_quota_exceeded, until the 1st of the next month (UTC). Reading cases, lists and accounts is not counted. Nothing is taken from your balance.
  • On first use. The sandbox starts with its first call, which gets 503 fraud_waking with Retry-After: 5, and is paused after 7 days without a call. The next call wakes it. It keeps its events 30 days.
  • Mock tokens. A device token that starts with test| sets the answer, so you can drive every branch of your code without writing a rule:
{ "type": "session.login", "actor_id": "u_1", "device_token": "test|device:d1|verdict:deny|score:80|ip:BE" }
PartMeaning
devicethe device id, up to 64 characters
verdictallow, challenge (as step_up), deny (as block), or any decision name
score0 to 1,000 (or risk, 0 to 1)
ipa country for the event, such as BE

The answer carries the rule sandbox_mock. On your live instance, a test| token is invalid.

  • Webhooks. The sandbox's webhooks go to a test address you set in the dashboard, under Fraud prevention and Sandbox, signed with a test secret of their own. Nothing from the sandbox reaches your live address, and nothing from your live instance reaches the test one.

The dashboard shows the sandbox's state (off, running, paused), the calls of the month out of 10,000, the test address and its secret.

A paused instance

A free organization's instance is paused after 30 days without a call: its data is kept. The next call wakes it and gets 503 fraud_waking with Retry-After: 5, and the SDKs answer allow meanwhile. An owner or an admin can also resume it from the dashboard.

Errors

These answers come from openshield; every other one is your instance's own, in openfraud's envelope.

StatusCodeWhen
400too_many_ipsan ips/query batch holds more than 1,000 addresses
401unauthorizedno key, or a wrong one
403forbiddenthe key has no fraud access, or only fraud_events for a route that needs fraud
404fraud_not_enabledyour organization has not turned fraud prevention on
429quota_exceededthe balance is used up
429sandbox_quota_exceededan om_test_ key used the sandbox's 10,000 calls of the month
502fraud_unavailableyour instance did not answer: retry in a few seconds
503fraud_wakingyour instance was paused and is starting: retry after Retry-After
503sandbox_startingan om_test_ key was made a moment ago and the sandbox is not set up yet: retry after Retry-After