Act on Kade signals in real time.
Outbound webhooks send location, client, cluster, email, and country-risk events to your backend or a connected Slack channel. This guide describes the payloads emitted by the current implementation and the API used to manage subscriptions.
Delivery contract
URL destinations receive an HTTP POST with a JSON body. Any 2xx response is successful. A non-2xx response or network failure is retried, for 5 total attempts, with exponential backoff beginning at 5 seconds.
At-least-once delivery
Retries can deliver the same event more than once. Make processing idempotent using X-Webhook-ID, which identifies the delivery event and remains the same across its retry attempts.
Acknowledge quickly
Validate, enqueue durable work, and return a 2xx response quickly. Run expensive processing outside the request path.
Request headers
| Header | When present | Meaning |
|---|---|---|
| Content-Type | Always | application/json |
| X-Webhook-Event | Always | One of the five event names documented below. |
| X-Webhook-ID | Always | Unique delivery event ID. Use as the idempotency key. |
| X-Webhook-Batch | Batched delivery | The literal value true. |
| X-Webhook-Count | Batched delivery | Number of ping records in the batch. |
POST /your-unguessable-kade-webhook-path HTTP/1.1
Content-Type: application/json
X-Webhook-Event: PING_RECEIVED
X-Webhook-ID: 4dce3817-df9c-48b3-a813-03af6bbb61f9
X-Webhook-Batch: true
X-Webhook-Count: 2X-Webhook-ID, and a quick 2xx acknowledgement. Kade does not document stable source IPs, so this guide does not recommend IP allowlisting.PING_RECEIVED
Filterable Optional batchingFires when a persisted geolocation ping is flushed for a workspace and matches the subscription filters. Single and batched payloads have intentionally different top-level shapes.
PingLog record itself. A batched delivery wraps ping records in { event, count, pings }. Do not look for a top-level event property on a single ping.Key operator-facing PingLog fields
The ping record is an internal persistence entity and can contain more columns or loaded relations than this table. The fields below are the key values currently consumed by operators. This is not a promise that every database field or internal relation is a stable public schema.
| Field | Type | Meaning |
|---|---|---|
| id | number | Persisted ping identifier. |
| clientUserId | string or null | Your user or player identifier associated with the ping. |
| ip | string or null | Observed client IP address. |
| latitude / longitude | number | Evaluated location coordinates. |
| accuracy | number or null | Location accuracy in meters when available. |
| inside | boolean | Whether the location was inside the evaluated area. |
| status | string or null | Persisted ping outcome status. |
| riskScore | number | Computed Kade risk score. |
| reasonCodes | string[] or null | Risk and policy reason codes. |
| isIpBasedLocation | boolean | Whether the evaluated location came from IP geolocation. |
| gadmCountry / gadmProvince / gadmCity | string or null | Resolved administrative geography. |
| geohash6 / geohash7 | string or null | Resolved location cell identifiers. |
| platform | web | ios | android | SDK platform that generated the ping. |
| source | string or null | Caller-supplied source label. |
| deviceType | mobile | tablet | desktop | null | Coarse device form factor. |
| os / osVersion | string or null | Normalized operating system details. |
| isVpn / isProxy / isTor / isDatacenter / isCloud | boolean or null | Denormalized network-risk flags. |
| timestamp | timestamp | Time the ping record was created. |
Representative single payload
{
"id": 781245,
"clientUserId": "player-8472",
"ip": "203.0.113.42",
"latitude": 51.5074,
"longitude": -0.1278,
"accuracy": 18.4,
"inside": true,
"status": "PASS",
"riskScore": 12,
"reasonCodes": [],
"isIpBasedLocation": false,
"gadmCountry": "United Kingdom",
"gadmProvince": "England",
"gadmCity": "Greater London",
"geohash6": "gcpvj0",
"geohash7": "gcpvj0d",
"platform": "web",
"source": "checkout",
"deviceType": "desktop",
"os": "Mac OS",
"osVersion": "15.5",
"isVpn": false,
"isProxy": false,
"isTor": false,
"isDatacenter": false,
"isCloud": false,
"timestamp": "2026-08-04T10:45:18.511Z"
}Representative batched payload
{
"event": "PING_RECEIVED",
"count": 2,
"pings": [
{
"id": 781245,
"clientUserId": "player-8472",
"latitude": 51.5074,
"longitude": -0.1278,
"inside": true,
"riskScore": 12,
"platform": "web",
"timestamp": "2026-08-04T10:45:18.511Z"
},
{
"id": 781246,
"clientUserId": "player-8473",
"latitude": 51.5081,
"longitude": -0.1291,
"inside": false,
"riskScore": 76,
"platform": "ios",
"timestamp": "2026-08-04T10:45:18.812Z"
}
]
}Batched subscriptions group matched pings from the current flush and split them into chunks of at most 200. A batch may therefore contain fewer than 200 records. URL subscriptions can choose batching. Slack PING_RECEIVED subscriptions are always forced to batched mode.
BLACKLIST_CHANGED
Single payloadDespite its legacy name, this event fires on any client status change, not only blacklisting or unblacklisting. It is emitted only when the old and new statuses differ.
| Field | Type | Meaning |
|---|---|---|
| clientId | string | Kade client UUID. |
| clientUserId | string or null | Last user or player identifier seen for the client. |
| oldStatus | string | Status before the change. |
| newStatus | string | Status after the change. |
| timestamp | timestamp | Time Kade applied the status change. |
| backofficeUrl | string, optional | Workspace backoffice link when an absolute URL can be built. |
{
"clientId": "a8f56280-0f35-4adb-8b0a-524f127d00b2",
"clientUserId": "player-8472",
"oldStatus": "active",
"newStatus": "review",
"timestamp": "2026-08-04T10:47:03.180Z",
"backofficeUrl": "https://your-workspace.example/verified-backoffice-path"
}This event has no event-specific filters and is delivered as a single payload. The example backoffice URL is illustrative only and is not a documented Kade API base URL.
CLUSTER_DETECTED
Single payloadFires when a geographic or IP cluster first crosses its configured distinct-client threshold, then fires again when cumulative growth crosses each 50 percent milestone relative to the initial baseline.
| Field | Type | Meaning |
|---|---|---|
| kind | geo7 | geo6 | ip | Cluster cell type. |
| key | string | Geohash or IP value identifying the cluster. |
| workspaceId | number | Workspace that owns the detection. |
| clientCount | number | Cumulative distinct clients in the open detection. |
| windowMs | number | Configured rolling detection window in milliseconds. |
| firstSeen | timestamp or null | Earliest observation when available. |
| lastSeen | timestamp | Latest observation represented by this fire. |
| clientIds | string[] | Client UUID sample, capped at 500 entries. |
| clientUserIds | string[] | Known user or player identifiers in the sample. |
| truncated | boolean | Whether the full client set exceeded the retained sample. |
| reason | initial | growth | Initial threshold crossing or later growth milestone. |
| baselineCount | number | Distinct client count at initial detection. |
| milestonePct | number | 100 initially, then 150, 200, 250, and so on. |
| backofficeUrl | string, optional | Cluster Monitor deep link when available. |
{
"kind": "geo7",
"key": "gcpvj0d",
"workspaceId": 42,
"clientCount": 18,
"windowMs": 3600000,
"firstSeen": "2026-08-04T09:54:00.000Z",
"lastSeen": "2026-08-04T10:49:22.000Z",
"clientIds": [
"a8f56280-0f35-4adb-8b0a-524f127d00b2",
"8e14c91e-cc83-469d-af09-d8e4d2789365"
],
"clientUserIds": ["player-8472", "player-9021"],
"truncated": false,
"reason": "growth",
"baselineCount": 12,
"milestonePct": 150,
"backofficeUrl": "https://your-workspace.example/verified-cluster-path"
}Cluster subscriptions have no event-specific filters. They use the generic single-payload path and are not batched.
EMAIL_FLAGGED
Filterable Single payloadFires only for recorded live checks whose verdict is high or review. Dry runs do not record or dispatch, and retro scans do not dispatch webhooks.
| Field | Type | Meaning |
|---|---|---|
| event | EMAIL_FLAGGED | Event name included in this payload. |
| string | Email address evaluated by the live check. | |
| firstName | string or null | First name supplied with the check. |
| lastName | string or null | Last name supplied with the check. |
| playerId | string or null | Player identifier supplied with the check. |
| score | number or null | Model score when available. Deterministic rules can flag with no score. |
| verdict | high | review | Flagged verdict. |
| patternFamily | string or null | Detected pattern family when one applies. |
| reasons | string[] | Rules and signals supporting the verdict. |
| checkedAt | timestamp | Time used for the live check. |
| backofficeUrl | string, optional | Email Checks deep link when available. |
{
"event": "EMAIL_FLAGGED",
"email": "maria.1987@example.test",
"firstName": "Maria",
"lastName": "Silva",
"playerId": "player-9021",
"score": 0.97,
"verdict": "high",
"patternFamily": "sequential_suffix",
"reasons": [
"numeric_suffix_sequence",
"domain_registration_velocity"
],
"checkedAt": "2026-08-04T10:51:04.303Z",
"backofficeUrl": "https://your-workspace.example/verified-email-checks-path"
}Filters support minScore, verdicts, and patternFamilies, with all configured conditions combined using AND. A deterministic-rule detection with score: null bypasses minScore because there is no model score to compare. It must still satisfy any verdict and pattern-family filters.
HIGH_RISK_COUNTRY_PING
Filterable Single payloadFires once per persisted ping whose resolved country is in the workspace risk-country list. IP-based locations are included and explicitly identified in the payload.
| Field | Type | Meaning |
|---|---|---|
| event | HIGH_RISK_COUNTRY_PING | Event name included in this payload. |
| clientId | string or null | Kade client UUID when associated. |
| clientUserId | string or null | Your user or player identifier. |
| country | string | Resolved country that matched the risk list. |
| gadmProvince | string or null | Resolved province or first-level region. |
| gadmCity | string or null | Resolved city or local administrative region. |
| platform | web | ios | android | SDK platform. |
| source | string or null | Caller-supplied source label. |
| riskScore | number | Computed Kade risk score. |
| latitude / longitude | number | Evaluated coordinates. |
| isIpBasedLocation | boolean | Whether coordinates came from IP geolocation. |
| labelAdded | boolean | Whether this processing pass newly added the high-risk-country label. |
| timestamp | timestamp | Persisted ping timestamp. |
| backofficeUrl | string, optional | Workspace backoffice link when available. |
{
"event": "HIGH_RISK_COUNTRY_PING",
"clientId": "a8f56280-0f35-4adb-8b0a-524f127d00b2",
"clientUserId": "player-8472",
"country": "Example Risk Country",
"gadmProvince": "Central Province",
"gadmCity": "Capital District",
"platform": "android",
"source": "login",
"riskScore": 84,
"latitude": 35.6895,
"longitude": 139.6917,
"isIpBasedLocation": false,
"labelAdded": true,
"timestamp": "2026-08-04T10:53:11.928Z",
"backofficeUrl": "https://your-workspace.example/verified-backoffice-path"
}Filters support platforms and sources. Conditions combine using AND; each absent or empty list matches all values. Delivery remains one payload per matched ping and is not batched.
Filter reference
Filters are stored on a subscription in the filters object. Every configured field must match. Empty or omitted filters match all events of that subscription's type.
PING_RECEIVED filters
| Field | Accepted value | Matching behavior |
|---|---|---|
| outcome | pass | outside | blocked | Matches the derived operator outcome. |
| riskScoreMin / riskScoreMax | integer | Inclusive lower and upper score bounds. |
| reasonCodes | string[] | All selected codes must match. Bare codes also match subtype variants. |
| geohashPrefixes | string[] | Any prefix may match the ping's geohash7. |
| vpnFlags | isVpn, isProxy, isTor, isDatacenter, isCloud | Every selected flag must be true. |
| gadmCountry / gadmProvince / gadmCity | string | Exact administrative-area match. |
| regionId | integer | Exact Kade region ID. |
| platform | web | ios | android | Exact SDK platform. |
| deviceType | mobile | tablet | desktop | Exact form factor. |
| os | string | Exact normalized operating system. |
| osVersion | string | Prefix match against the OS version. |
| userIp / clientUserId / source | string | Exact value match. |
| labels | string[] | Client must have every manual label. Manual labels are normalized. |
| analysisLabels | string[] | Client must have every canonical analysis label. |
{
"outcome": "blocked",
"riskScoreMin": 70,
"platform": "web",
"reasonCodes": ["LOCATION_GPS_SPOOFING"],
"analysisLabels": ["risk:high-risk-country"]
}Other event filters
EMAIL_FLAGGED
minScore from 0 to 1, verdicts as strings, and patternFamilies as strings.
HIGH_RISK_COUNTRY_PING
platforms accepts web, ios, and android. sources accepts source strings.
BLACKLIST_CHANGED and CLUSTER_DETECTED currently have no event-specific filters.
Slack destinations
A subscription can deliver either to a URL or to a Slack channel connected to the workspace. Slack delivery renders a message and calls Slack directly; it does not POST the JSON payload to your endpoint.
Destination fields
Set destinationType to SLACK and provide slackChannelId. A slackChannelName can be retained for display.
Message templates
messageTemplate controls a single message or each row in a ping batch. messageHeaderTemplate controls the header for batched PING_RECEIVED messages. Null uses the event default.
batched: true for this combination even when the create or update request asks for false. This protects the destination from one Slack message per ping. Other event types continue through their single-payload paths.Fetch GET /webhooks/template-variables to obtain the server-provided variable catalog and event defaults rather than hard coding a variable list.
Webhook management API
These endpoints require a Kade JWT in the Authorization: Bearer header and operate in the authenticated user's workspace. Supply your verified Kade API origin as KADE_API_ORIGIN. This guide intentionally does not invent a public base URL or undocumented response schemas.
webhooks:read. Create, update, and delete operations require webhooks:manage.Returns the server's template-variable catalog and per-event default templates. Requires webhooks:read.
curl "$KADE_API_ORIGIN/webhooks/template-variables" \
-H "Authorization: Bearer $KADE_JWT"Creates a subscription. Requires webhooks:manage. event is required. destinationType accepts URL or SLACK and defaults to URL behavior when omitted.
- URL destination: provide a valid
url. - Slack destination: provide
slackChannelId;slackChannelNameis optional. batchedandfiltersare optional and apply as documented above.messageTemplateandmessageHeaderTemplateare optional Slack templates.
curl -X POST "$KADE_API_ORIGIN/webhooks" \
-H "Authorization: Bearer $KADE_JWT" \
-H "Content-Type: application/json" \
--data '{
"destinationType": "URL",
"url": "https://hooks.example.com/kade/unguessable-path",
"event": "PING_RECEIVED",
"batched": true,
"filters": {
"riskScoreMin": 70,
"platform": "web"
}
}'{
"destinationType": "SLACK",
"slackChannelId": "C0123456789",
"slackChannelName": "risk-alerts",
"event": "EMAIL_FLAGGED",
"filters": {
"verdicts": ["high"],
"minScore": 0.95
},
"messageTemplate": "*{{verdict}}* email: `{{email}}`"
}Lists subscriptions for the authenticated workspace, newest first. Requires webhooks:read.
curl "$KADE_API_ORIGIN/webhooks" \
-H "Authorization: Bearer $KADE_JWT"Updates an existing subscription in the authenticated workspace. Requires webhooks:manage. The current update contract accepts url, slackChannelId, slackChannelName, isActive, batched, filters, messageTemplate, and messageHeaderTemplate.
Pass null for filters to clear filters, or for either template field to restore its event default. destinationType and event are selected at creation and are not fields in the current PATCH contract. Create a replacement subscription to change either one.
curl -X PATCH "$KADE_API_ORIGIN/webhooks/$WEBHOOK_ID" \
-H "Authorization: Bearer $KADE_JWT" \
-H "Content-Type: application/json" \
--data '{
"isActive": true,
"batched": true,
"filters": {
"platforms": ["ios", "android"],
"sources": ["login"]
}
}'Deletes the subscription from the authenticated workspace. Requires webhooks:manage.
curl -X DELETE "$KADE_API_ORIGIN/webhooks/$WEBHOOK_ID" \
-H "Authorization: Bearer $KADE_JWT"Planning a webhook rollout?
Talk to Kade about destination setup, event selection, filtering, Slack templates, and safe production handling.