Skip to content

API Reference Summary

Quick reference for key WebDecoy API endpoints.


The WebDecoy API uses REST architecture with JSON payloads. There are two services with different credentials:

Service Base URL Credential
Management API (everything on this page except the last section) https://api.webdecoy.com/api Auth0 session JWT (the token the dashboard uses)
Ingest API (SDKs, sensors, detection scripts) https://in.webdecoy.com/api for server-side sensors; https://ingest.webdecoy.com/api for the browser tag Organization API key (sk_live_...) or a public site key, depending on the route

Management API:

Authorization: Bearer <auth0_jwt_token>

Ingest API (routes that take an API key):

Authorization: Bearer sk_live_your_api_key

POST /auth/callback
Authorization: Bearer <auth0_jwt>

Response:

{
"id": "user-uuid",
"auth0_id": "auth0|123",
"email": "[email protected]",
"name": "User Name"
}
GET /auth/me
Authorization: Bearer <token>

POST /organizations
Authorization: Bearer <token>
{
"name": "My Organization"
}
GET /organizations?page=1&page_size=50
Authorization: Bearer <token>
GET /organizations/{organization_id}
Authorization: Bearer <token>
PUT /organizations/{organization_id}
Authorization: Bearer <token>
{
"name": "Updated Name"
}

POST /organizations/{organization_id}/properties
Authorization: Bearer <token>
{
"name": "Production Website",
"owner_id": "user-uuid"
}

owner_id (the user UUID of the property owner) is required.

GET /organizations/{organization_id}/properties?page=1&page_size=50
Authorization: Bearer <token>
GET /properties/{property_id}
Authorization: Bearer <token>

POST /organizations/{organization_id}/domains
Authorization: Bearer <token>
{
"domain": "decoy.example.com",
"record_type": "A"
}
GET /organizations/{organization_id}/domains
Authorization: Bearer <token>
POST /domains/{domain_id}/verify
Authorization: Bearer <token>
{
"dns_record": "..."
}

record_type on create is A or CNAME (default CNAME). The verify body requires dns_record.

POST /domains/{domain_id}/acme/challenge # start a DNS-01 challenge
POST /domains/{domain_id}/acme/verify-dns # check the TXT record
POST /domains/{domain_id}/acme/verify # complete issuance
POST /domains/{domain_id}/acme/reset # start over
POST /domains/{domain_id}/ssl/http01/initiate # HTTP-01 alternative (A-record domains)
POST /domains/{domain_id}/ssl/http01/complete
Authorization: Bearer <token>
POST /domains/{domain_id}/ssl-status
Authorization: Bearer <token>

POST /decoys
Authorization: Bearer <token>
{
"custom_domain_id": "domain-uuid",
"name": "Admin Backup Trap",
"path": "/admin/backup.zip",
"trigger_action": "log",
"decoy_type": "link"
}

trigger_action is one of log, block, poison, redirect. decoy_type is link or endpoint. Omit custom_domain_id and pass organization_id for a shared-domain decoy.

GET /decoys?page=1&page_size=50
Authorization: Bearer <token>
GET /decoys/{decoy_id}
Authorization: Bearer <token>
PATCH /decoys/{decoy_id}
Authorization: Bearer <token>
{
"name": "Updated Name",
"is_active": false
}

List and Get report state (live, paused, expired or click_limit_reached) and, when resuming would remove a limit, resume_clears (expires_at, click_limit). Sending "is_active": true resumes the decoy and removes the limit that stopped it, the ones resume_clears names. "is_active": false pauses it and leaves both limits alone.

DELETE /decoys/{decoy_id}
Authorization: Bearer <token>

POST /organizations/{organization_id}/bot-scanners
Authorization: Bearer <token>
{
"property_id": "property-uuid",
"name": "Main Scanner",
"enabled": true,
"tier": "lite"
}

property_id is required. tier is lite (default) or pro.

GET /organizations/{organization_id}/bot-scanners
Authorization: Bearer <token>
GET /organizations/{organization_id}/bot-scanners/{scanner_id}/snippet
Authorization: Bearer <token>
PATCH /organizations/{organization_id}/bot-scanners/{scanner_id}/toggle
Authorization: Bearer <token>

GET /organizations/{organization_id}/detections?page=1&page_size=50
Authorization: Bearer <token>

Query Parameters:

Parameter Description
property_id Filter by property
source Detection source, repeatable (for example decoy_link, endpoint, bot_scanner, sdk, wordpress_plugin, php_sensor, edge_worker)
min_unified_score Minimum unified score (0-100)
max_unified_score Maximum unified score (0-100)
threat_category Threat category, repeatable
action Action taken, repeatable
ip_address Exact IP address
actor_id Detections correlated to one actor
ai_scraper_name AI crawler name (for example GPTBot)
is_test true for test detections only, false to hide them
country Country code, repeatable
is_proxy true/false
is_vpn true/false
is_tor true/false
start_date ISO 8601 date
end_date ISO 8601 date
sort_by timestamp (default), unified_score, bot_score
sort_order asc, desc (default)

The response is { "items": [...], "total", "page", "page_size", "has_more", "next_cursor" }.

GET /organizations/{organization_id}/detections/{detection_id}
Authorization: Bearer <token>
GET /organizations/{organization_id}/detections/stats
Authorization: Bearer <token>
GET /organizations/{organization_id}/detections/stats/hourly
Authorization: Bearer <token>
GET /organizations/{organization_id}/detections/filter-options
Authorization: Bearer <token>
GET /organizations/{organization_id}/detections/export?format=csv
Authorization: Bearer <token>

Formats: csv (default), json. Respects the same filters as List Detections. limit defaults to 5,000 rows (maximum 10,000).

GET /organizations/{organization_id}/detections/geo
GET /organizations/{organization_id}/detections/stats/rule-executions
GET /organizations/{organization_id}/detections/stats/signals
GET /organizations/{organization_id}/detections/stats/ai-scrapers
GET /organizations/{organization_id}/detections/stats/ai-scrapers/by-name
GET /organizations/{organization_id}/detections/stats/llm-referrals
GET /organizations/{organization_id}/detections/stats/llm-referrals/by-platform
GET /organizations/{organization_id}/detections/stats/llm-referrals/top-pages
GET /organizations/{organization_id}/detections/stats/page-crawlers
GET /organizations/{organization_id}/detections/stats/radar
GET /organizations/{organization_id}/detections/stats/categories
GET /organizations/{organization_id}/detections/stats/top-sources
Authorization: Bearer <token>

llm-referrals/by-platform returns { platform, count, likely } per platform. count is confirmed referrals; likely is the separate likely-referral estimate, never part of count. A platform with only an estimate is listed with count: 0.


Persistent cross-IP adversary identities. See Actors.

GET /organizations/{organization_id}/actors # list; sort=threat|ip|property|detections|probes|new (default: last seen), filter=returning|secret_probe|impersonator|rotating|enforced|ready
GET /organizations/{organization_id}/actors/stats # org summary for the dashboard hero row
GET /organizations/{organization_id}/actors/{actor_id} # actor + IP/UA footprint + recent detections
Authorization: Bearer <token>

The WAF-enforcement pipeline (see Session Clearance). All routes are org-scoped.

POST /organizations/{organization_id}/enforcement/cloudflare/preflight # harmless zone live-test
GET /organizations/{organization_id}/enforcement/mode # monitor | enforce
PUT /organizations/{organization_id}/enforcement/mode
GET /organizations/{organization_id}/enforcement/signatures # active composite signatures (+ pushed count)
GET /organizations/{organization_id}/enforcement/denylist # clearance deny-list
POST /organizations/{organization_id}/enforcement/denylist
DELETE /organizations/{organization_id}/enforcement/denylist/{device_fp}
GET /organizations/{organization_id}/enforcement/routes # token-enforced route scope
POST /organizations/{organization_id}/enforcement/routes
DELETE /organizations/{organization_id}/enforcement/routes/{route_id}
GET /organizations/{organization_id}/enforcement/allowlist # verified-crawler categories
PUT /organizations/{organization_id}/enforcement/allowlist
GET /organizations/{organization_id}/enforcement/credentials # machine service tokens
POST /organizations/{organization_id}/enforcement/credentials
DELETE /organizations/{organization_id}/enforcement/credentials/{credential_id}
GET /organizations/{organization_id}/enforcement/audit # append-only change log
Authorization: Bearer <token>

WHEN→THEN automations (see Response Actions).

GET /organizations/{organization_id}/response-actions
POST /organizations/{organization_id}/response-actions
PUT /organizations/{organization_id}/response-actions/{id}
DELETE /organizations/{organization_id}/response-actions/{id}
Authorization: Bearer <token>

GET /mitre/tactics
GET /organizations/{organization_id}/mitre/stats
Authorization: Bearer <token>

POST /organizations/{organization_id}/integrations/cloudflare
Authorization: Bearer <token>
{
"api_token": "cloudflare_token",
"account_id": "account_id",
"zone_id": "zone_id",
"zone_name": "example.com"
}

Slack, Vercel, Datadog, Splunk, AWS WAF, Fastly, and CrowdStrike each have equivalent org-scoped CRUD + test endpoints under /organizations/{organization_id}/integrations/{provider}.

POST /organizations/{organization_id}/integrations/webhooks
Authorization: Bearer <token>
{
"name": "My Webhook",
"url": "https://example.com/webhook",
"on_detection": true,
"min_score": 70
}
POST /organizations/{organization_id}/integrations/webhooks/{webhook_id}/test
Authorization: Bearer <token>

POST /organizations/{organization_id}/api-keys
Authorization: Bearer <token>
{
"name": "Production SDK",
"scopes": ["detections:read", "detections:write"],
"expires_in": 365
}

Response:

{
"id": "key-uuid",
"name": "Production SDK",
"api_key": "sk_live_xxxxxxx",
"key_prefix": "sk_live_xxx",
"scopes": ["detections:read", "detections:write"]
}

Important: The full api_key is only returned once. Store it securely.

GET /organizations/{organization_id}/api-keys
Authorization: Bearer <token>
DELETE /organizations/{organization_id}/api-keys/{key_id}
Authorization: Bearer <token>

GET /billing/plans
Authorization: Bearer <token>
POST /billing/checkout-session
Authorization: Bearer <token>
{
"organization_id": "org-uuid",
"price_id": "price_xxx",
"success_url": "https://app.example.com/billing/success",
"cancel_url": "https://app.example.com/billing"
}
GET /organizations/{organization_id}/subscription
Authorization: Bearer <token>
GET /organizations/{organization_id}/billing/usage
Authorization: Bearer <token>

These endpoints live on the ingest service (host https://in.webdecoy.com for server-to-server calls, https://ingest.webdecoy.com for the browser tag; the paths below include /api) and are what the SDKs, detection scripts, and edge validators talk to.

Building a sensor for a stack we do not ship one for? The full wire contract (fields, the client signals block, client-IP resolution, and the fail-open behaviour a sensor must implement) is documented in the Sensor Protocol.

POST /api/v1/detect # submit a detection (Authorization: Bearer sk_live_..., scope detections:write)
POST /api/v1/detect/public # browser tag only (aid + sid, unauthenticated)
POST /api/v1/clearance # mint a wd_clearance token (publishable site key: aid)
GET /api/v1/clearance/config # edge-validator config
POST /api/v1/page-serve # page-serve telemetry
POST /api/v1/verify-js # JS-execution verification
GET /api/v1/auth/validate-key # validate an API key
POST /api/v1/sdk/detect # SDK detection submissions
POST /api/v1/sdk/violations/batch # SDK rules-engine violation batches
GET /api/v1/sdk/ip/{address}/enrichment # IP enrichment for SDK filter rules

List endpoints return:

{
"items": [ ... ],
"total": 100,
"page": 1,
"page_size": 50,
"has_more": true
}

Single-resource endpoints return the object itself.

Most errors return:

{
"error": "Error message"
}

Some routes return a structured error instead:

{
"code": "ERROR_CODE",
"message": "Error message"
}
Code Meaning
200 Success
201 Created
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
429 Rate Limited
500 Server Error

Rate limits apply per client IP to selected routes, not to the whole API. For example, the auth routes allow 10 requests per minute, and the ingest detection routes (/api/v1/detect, /api/v1/detect/public, /api/v1/clearance) allow 100 per minute with a burst of 20. A limited request gets 429 with {"error": "rate limit exceeded"}. No X-RateLimit-* headers are sent.