Docs menu
API reference

POST /api/v1/events/withdrawal

Screen a withdrawal. Check a bonus claim or a withdrawal. Generated from the OpenAPI file.

Last updated

Overview

POST https://api.inlineshield.io/api/v1/events/withdrawal

Screens a withdrawal before you pay it out and returns the decision, the action to take and the reasons, in the shape of /verify.

Send the player's accountId, the amount and the payout account as a payout fingerprint, never as a card number, IBAN or wallet address: a raw card number or IBAN is refused with 400 RAW_PAYMENT_DATA. InlineShield stores only a keyed hash of the fingerprint, and uses it to see whether other accounts of yours withdrew to the same payout account in the last 180 days. An account with no sign-up bound still gets an answer, with the reason "Sign-up not linked to this account" and no penalty.

  • action: allow (pay), hold (do not pay until the review is decided; a live hold has a reviewId), block.
  • recommendedAction: allow, review, deny or block.
  • The rules in Settings › Rules › Bonus and withdrawal checks only hold or advise. block comes only when the IP and bot checks of your sign-up rules would block the event.

The lookups share one budget of 150 ms; one that has not finished by then is treated as unknown, never as a signal, and the reasons say so. A live event counts once against the monthly quota, like a /verify screening; a test key never counts. Limited per key like /siteverify (the key's own per-minute limit: 1,200 for a live key as created, 600 for a key without a limit of its own); both event endpoints share that bucket. A test key uses the account's own test bucket instead, 300 per minute for both event endpoints, and the daily test cap. Test mode creates no review, webhook, label or alert, and never links accounts.

Authentication

Send your secret API key in the X-Api-Key header.

Your secret API key (bs_live_… or bs_test_…), from the console under Settings › Keys. Never put it in a browser or an app. The header name X-ApiKey is also accepted (deprecated).

Request body

JSON (Content-Type: application/json), required.

Other fields are accepted and ignored.

FieldTypeRequiredDescription
accountIdstringyesYour own id for the player account (1 to 128 printable characters). It is stored as you send it, shown masked in the console (the first 2 characters and ***) and sent back in the webhooks. If the account has a sign-up bound to it (by accountId on /verify or /siteverify, or by signupRequestId below), the check compares the event with that sign-up. An unbound account is still answered, with the reason "Sign-up not linked to this account" and no penalty. Anything else gets 400 INVALID_ACCOUNT_ID.at least 1 character; at most 128 characters
ipstringnoThe player's IP address (IPv4 or IPv6) for this event. Without it the IP checks do not run: the request's own address is your server's, so it is never used. Anything that is not an IP address gets 400 INVALID_IP.
userAgentstringnoThe player's browser User-Agent. Only this field is read; a scripted HTTP client here counts as an automation signal.at most 1024 characters
amountnumberyesThe withdrawal amount, 0 or more. Stored and shown in the console; it does not change the score.0 to 1000000000000000
currencystringnoThe ISO 4217 code of amount: 3 letters, stored in upper case.at most 3 characters; matches ^[A-Za-z]{3}$
payoutobjectyesThe account the money goes to, as a fingerprint, never as the card number, IBAN or wallet itself. A value that looks like a raw card number or IBAN is refused with 400 RAW_PAYMENT_DATA, whatever type says. InlineShield stores only a keyed hash of the fingerprint.
payout.typestringyesWhat kind of account it is. The same fingerprint with another type is another payout account.Values: card, iban, wallet, other
payout.fingerprintstringyesYour payment provider's fingerprint of the account (for example Stripe's fp_…), or a value you compute yourself with a secret key. 16 to 128 characters of A-Z, a-z, 0-9, _, -, : and .. It must be the same string every time the same account is used.at least 16 characters; at most 128 characters; matches ^[A-Za-z0-9_\-:.]+$
tokenstringnoThe player's sign-up /check token (the one the browser script got at sign-up), for the browser's time zone and automation signals. Usable only within its 10-minute life and only if it was not already redeemed with /siteverify. Never run /check on bonus or cashier pages: that records a new sign-up screening, counts against the quota and creates false links. Without a token the event is screened from server-side data only. The event spends the token: a later /siteverify with the same token answers TOKEN_ALREADY_USED. A token of the other mode (test or live) is left unused, and a token that is unknown, used or expired is ignored.at most 512 characters
signupRequestIdstringnoThe requestId (AUD-…) that the sign-up's /verify, /check or /siteverify returned. It binds the account to that sign-up, but only while the account has none yet: the first binding stays. A sign-up of another account, mode or organisation never binds.at most 128 characters

Request examples

{
  "accountId": "payout-shared@test",
  "ip": "203.0.113.24",
  "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
  "amount": 250,
  "currency": "EUR",
  "payout": {
    "type": "card",
    "fingerprint": "fp_1Nq8Zt2eZvKYlo2CxY7wB3aD"
  }
}

The first example as a cURL command:

cURL
curl -X POST https://api.inlineshield.io/api/v1/events/withdrawal \
  -H "X-Api-Key: $INLINESHIELD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "accountId": "payout-shared@test",
  "ip": "203.0.113.24",
  "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
  "amount": 250,
  "currency": "EUR",
  "payout": {
    "type": "card",
    "fingerprint": "fp_1Nq8Zt2eZvKYlo2CxY7wB3aD"
  }
}'

Responses

200 OK

The decision. A live hold also has reviewId. Live responses carry the quota headers; test responses do not.

Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Quota-Limit (optional), X-Quota-Used (optional), X-Quota-Reset (optional), X-Quota-State (optional).

FieldTypeRequiredDescription
requestIdstringyesThe event's id, starting with EVT-. Use it for support; the webhooks carry it too.matches ^EVT-[0-9A-F]{16}$
decisionstringyesALLOWED, QUARANTINED (a rule of Settings › Rules held it) or BLOCKED (your sign-up rules would block this IP or client).Values: ALLOWED, QUARANTINED, BLOCKED
actionstringyesWhat to do with the event: ALLOWED → allow (pay), QUARANTINED → hold (do not pay until the review is decided), BLOCKED → block.Values: allow, hold, block
recommendedActionstringyesAdvice next to action. Bonus claims: deny_bonus (do not pay this bonus) when another account of the same person already claimed one and the rule "Recommend deny_bonus" is on, or when the score is 50 or more; else review when the claim is held, else allow. Withdrawals: review when held, deny (do not pay) at a score of 70 or more, else allow. Both: block when the decision is BLOCKED.Values: allow, review, deny_bonus, deny, block
reviewIdstringnoLive holds only (action: "hold"): the review to poll with GET /api/v1/reviews/{reviewId} or wait for in the bonus.approved / bonus.rejected (withdrawal.*) webhook. A held test event has none.matches ^rev_
riskScorenumberyes0 (lowest risk) to 100.0 to 100
reasonsarray of stringyesPlain-language reasons for the result.
triggersarray of stringyesMachine-readable signal codes: EVENT_LINKED_TO_FRAUD, EVENT_COUNTRY_CHANGED, EVENT_DATACENTER_OR_VPN_IP, EVENT_TOR_IP, EVENT_TIMEZONE_MISMATCH, EVENT_BOT_SIGNALS, SIGNUP_WAS_FLAGGED, BONUS_ALREADY_CLAIMED_IN_GROUP, PAYOUT_SHARED, PAYOUT_SHARED_WITH_FRAUD, BONUS_THEN_CASHOUT and WITHDRAWAL_VELOCITY; TEST_MAGIC_ADDRESS on a magic test account id. New codes may be added.
linkedLinked or nullyesThe linked-accounts summary of the account's bound sign-up (see Linked), including the links that a withdrawal with a shared payout account just added (same_payout), or null: no bound sign-up, no links, test mode, or the lookup did not finish within its budget.
linked.groupIdstring or nullyesThe linked-accounts group of this screening, as the console names it. null when there are only possible links (they never form a group). Groups can merge later, and then the merged group keeps one of the ids, so a later screening can report another id for the same people. In rare cases (the links could not be stored) the console has no group with this id.matches ^lg_[A-Za-z0-9_-]{16}$
linked.sizeintegeryesThe number of screenings in the group, this one included. With only possible links: 1 plus the number of possibly linked screenings.at least 1
linked.strengthstringyesstrong when at least one link is strong, otherwise possible.Values: strong, possible
linked.linkedToFraudbooleanyesTrue when at least one strong link goes to a screening that was BLOCKED or that you reported as fraud (feedback, a rejected review or a mark in the console). It is the condition of the Linked-to-fraud rule (Settings › Rules, off by default), and is reported whether the rule is on or off.
{
  "requestId": "EVT-D2C60A94E7B13F58",
  "decision": "QUARANTINED",
  "action": "hold",
  "recommendedAction": "review",
  "riskScore": 35,
  "reasons": [
    "Test mode: payout-shared@test always gives a held withdrawal (QUARANTINED / review)."
  ],
  "triggers": [
    "PAYOUT_SHARED",
    "TEST_MAGIC_ADDRESS"
  ],
  "linked": null
}

400 Bad Request

A field is missing or invalid (field names it): accountId (INVALID_ACCOUNT_ID), ip (INVALID_IP), a text field over its maxLength, amount, currency, payout (a raw card number or IBAN: RAW_PAYMENT_DATA), or a body that is not valid JSON (no field). Nothing is screened, stored or counted.

Headers: X-RateLimit-Limit (optional), X-RateLimit-Remaining (optional), X-RateLimit-Reset (optional).

FieldTypeRequiredDescription
errorstringyesValues: BAD_REQUEST, INVALID_ACCOUNT_ID, INVALID_IP, RAW_PAYMENT_DATA
messagestringyes
fieldstringnoThe request field that was refused, when one field is to blame (for example payout, amount or accountId).
{
  "error": "RAW_PAYMENT_DATA",
  "message": "payout.fingerprint looks like a raw card number or IBAN. Send the payment provider's fingerprint or the SDK's payoutFingerprint(), never raw payment data.",
  "field": "payout"
}

401 Unauthorized

No X-Api-Key header, or an unknown, revoked or expired key.

FieldTypeRequiredDescription
errorstringyesValues: UNAUTHORIZED, INVALID_API_KEY, EXPIRED_API_KEY, ORGANIZATION_NOT_FOUND
messagestringyes
{
  "error": "UNAUTHORIZED",
  "message": "Access denied: Missing required X-Api-Key header."
}

402 Payment Required

The monthly quota is used up. Test requests of an account over its quota get this too (they are still not counted).

Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.

FieldTypeRequiredDescription
error"QUOTA_EXCEEDED"yes
messagestringyes
usedintegeryesat least 0
limitintegeryesat least 0
resetsAtstring (date-time)yesThe start of the next UTC month.

403 Forbidden

The account is suspended.

FieldTypeRequiredDescription
errorstringyesValues: TENANT_SUSPENDED
messagestringyes

429 Too Many Requests

RATE_LIMIT_EXCEEDED: the per-minute limit (the key's own limit; 300 for a test key's account bucket). TEST_LIMIT: the daily cap of test screenings of the account; Retry-After is the time to the next UTC midnight.

Headers: Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.

FieldTypeRequiredDescription
errorstringyesValues: RATE_LIMIT_EXCEEDED, TEST_LIMIT
messagestringyes
rateLimit
{
  "error": "RATE_LIMIT_EXCEEDED",
  "message": "Event rate limit of 1200 req/min exceeded. Try again in 12 seconds."
}

500 Internal Server Error

The event was screened but could not be stored (PERSISTENCE_FAILURE), or the screening failed (EVENT_ERROR). Nothing is counted. Your integration decides whether to pay or wait.

Headers: X-RateLimit-Limit (optional), X-RateLimit-Remaining (optional), X-RateLimit-Reset (optional).

FieldTypeRequiredDescription
errorstringyesValues: PERSISTENCE_FAILURE, EVENT_ERROR, AUTH_ERROR
messagestringyes

503 Service Unavailable

The key could not be checked right now. Try again.

FieldTypeRequiredDescription
errorstringyesValues: DATABASE_ERROR
messagestringyes

Errors

Errors are JSON, { "error": "CODE", "message": "…" }. Branch on error, never on message.

HTTP statuserrorWhen
400BAD_REQUEST, INVALID_ACCOUNT_ID, INVALID_IP, RAW_PAYMENT_DATAA field is missing or invalid (field names it): accountId (INVALID_ACCOUNT_ID), ip (INVALID_IP), a text field over its maxLength, amount, currency, payout (a raw card number or IBAN: RAW_PAYMENT_DATA), or a body that is not valid JSON (no field). Nothing is screened, stored or counted.
401UNAUTHORIZED, INVALID_API_KEY, EXPIRED_API_KEY, ORGANIZATION_NOT_FOUNDNo X-Api-Key header, or an unknown, revoked or expired key.
402QUOTA_EXCEEDEDThe monthly quota is used up. Test requests of an account over its quota get this too (they are still not counted).
403TENANT_SUSPENDEDThe account is suspended.
429RATE_LIMIT_EXCEEDED, TEST_LIMITRATE_LIMIT_EXCEEDED: the per-minute limit (the key's own limit; 300 for a test key's account bucket). TEST_LIMIT: the daily cap of test screenings of the account; Retry-After is the time to the next UTC midnight.
500PERSISTENCE_FAILURE, EVENT_ERROR, AUTH_ERRORThe event was screened but could not be stored (PERSISTENCE_FAILURE), or the screening failed (EVENT_ERROR). Nothing is counted. Your integration decides whether to pay or wait.
503DATABASE_ERRORThe key could not be checked right now. Try again.

See all errors for every endpoint.

Headers and limits

Response headerMeaning
X-RateLimit-LimitRequests allowed in the current one-minute window.
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetSeconds until the current window ends.
X-Quota-LimitLive requests only: the monthly quota of the plan.
X-Quota-UsedLive requests only: screenings counted this month, this one included.
X-Quota-ResetLive requests only: when the count resets (the start of the next UTC month).
X-Quota-StateLive requests only: ok, warn (80% used), limit (100%), grace (over 100%, still served) or blocked.
Retry-AfterSeconds to wait before trying again.

The limits per endpoint and plan are on Rate limits and quotas.