Developer docs
The Active Defense API
Forward a suspected scam message, get the persona's approved reply back, pull the indicators the scammer leaked into your own tools, and check any IBAN, wallet, number or domain against them. One key.
Machine-readable: OpenAPI 3.1 · Base URL: https://activedefenseapi.com
Your key
Every call carries your API key in the X-API-Key header. Sign in to the console and
open Integration: it shows your tenant id
and key, renews the key (the old one stops working at once) and makes a separate signing secret for the
callbacks.
1 · Forward a suspected scam message
POST /api/ingest/webhook. One honeypot conversation per sourceIdentifier:
your stable id for the scammer's thread. The persona drafts a reply; it waits for approval in the console
unless autopilot is on.
curl -X POST https://activedefenseapi.com/api/ingest/webhook \
-H "X-API-Key: $AD_API_KEY" -H "Content-Type: application/json" \
-d '{"sourceIdentifier": "user-1234", "content": "Hello dear, I need 500 EUR for the customs fee."}'
Answers 200 with {"webhookEventId", "sourceIdentifier", "status": "Accepted",
"receivedAt"}, 400 without sourceIdentifier or content,
401 without a valid key. Optional: personaName picks another persona for a new
conversation.
2 · Get the approved reply back
Pull
GET /api/ingest/replies?since=0&max=100 returns the replies after your cursor and
the next cursor. Store nextCursor and pass it as since next time; replies stay
available across restarts.
curl "https://activedefenseapi.com/api/ingest/replies?since=0&max=100" -H "X-API-Key: $AD_API_KEY"
{"replies": [{"seq": 1, "sourceIdentifier": "user-1234", "messageId": "…", "content": "Oh no, customs again?", "at": "…"}],
"nextCursor": 1}
Push (callback)
Set a callback URL on Integration and every approved reply is POSTed there, signed. A callback
that does not get a 2xx is tried again after 1, 5 and 30 minutes, 2 and 6 hours, so treat
messageId as the idempotency key. The console logs every attempt and sends a signed test
("test": true) on request.
POST https://your-system.example/ad-callback
X-AD-Timestamp: 1760000000
X-AD-Signature: sha256=5f0c…
Content-Type: application/json
{"tenantId": "…", "sourceIdentifier": "user-1234", "messageId": "…", "reply": "Oh no, customs again?", "timestamp": "1760000000"}
X-AD-Signature is sha256= + hex(HMAC-SHA256(secret, timestamp + "." +
raw body)). The secret is your signing secret, or your API key until you make one. Check the timestamp
against your clock (here: at most 5 minutes apart) so a recorded request cannot be replayed, and compare
in constant time.
// TypeScript (Node)
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret: string, timestamp: string, rawBody: string, signature: string): boolean {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = Buffer.from("sha256=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex"));
const given = Buffer.from(signature);
return expected.length === given.length && timingSafeEqual(expected, given);
}
# Python
import hashlib, hmac, time
def verify(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
digest = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest("sha256=" + digest, signature)
// C#
static bool Verify(string secret, string timestamp, string rawBody, string signature)
{
if (!long.TryParse(timestamp, out var at) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - at) > 300) return false;
var hash = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}"));
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes("sha256=" + Convert.ToHexStringLower(hash)), Encoding.UTF8.GetBytes(signature));
}3 · WhatsApp through Twilio
Point your Twilio number's inbound webhook at POST /api/ingest/whatsapp/{apiKey}.
Text, photos and voice messages become turns of the conversation with that number; the endpoint answers with
empty TwiML at once. With a Twilio auth token configured, only requests with a valid
X-Twilio-Signature count.
4 · Your indicator feed
Everything your conversations harvested (links, crypto wallets, bank drops, app hashes), with your
API key as Authorization: Bearer, as the password of Basic auth or in X-API-Key:
- TAXII 2.1 API root
/taxii2/t/{tenantId}/, one collection (OpenCTI and any TAXII client), withadded_after,match[id],match[type],limitandnext - STIX 2.1 bundle
/feeds/t/{tenantId}/stix.json - CSV
/feeds/t/{tenantId}/iocs.csv: type, value, first_seen, last_seen, confidence, chats, days_active (first to last sighting), fresh (seen within 7 days), enrichment (what is publicly known, in one line)
curl https://activedefenseapi.com/feeds/t/$AD_TENANT_ID/iocs.csv -H "Authorization: Bearer $AD_API_KEY" curl https://activedefenseapi.com/taxii2/t/$AD_TENANT_ID/collections/ -H "Authorization: Bearer $AD_API_KEY" \ -H "Accept: application/taxii+json;version=2.1"
The Active Defense feed (add-on) carries what scammers handed to our own decoy
accounts, as it comes: TAXII 2.1 root /taxii2/feed/, STIX /feeds/feed/stix.json,
CSV /feeds/feed/iocs.csv, with the same key. It answers while a feed tier is booked and paid;
otherwise 402 says why. The basic tier reads the CSV only, 24 hours delayed (TAXII and STIX
answer 402 with the reason feed-tier); live and enterprise read every format as it
comes, and a subscription for Active Defense includes the live feed.
5 · Check a value
POST /api/check looks up to 100 values a call in what scammers handed to Active Defense's
decoys and in your own feed: IBANs, BICs, crypto wallets, phone numbers, e-mail addresses, links, domains,
messenger handles (@name, t.me/name, wa.me/49…) and SHA-256 hashes, written
as people write them. A domain also finds the links and addresses on it.
curl -X POST https://activedefenseapi.com/api/check \
-H "X-API-Key: $AD_API_KEY" -H "Content-Type: application/json" \
-d '{"values": ["DE89 3704 0044 0532 0130 00", "+57 300 123 4567", "evil.example"]}'
{"checkedAt": "…", "results": [
{"value": "DE89 3704 0044 0532 0130 00", "kind": "iban", "known": true, "risk": 91,
"firstSeen": "2026-09-02T14:12:00Z", "lastSeen": "2026-10-09T18:40:00Z", "chats": 3, "fresh": true,
"rings": ["5f0c…"],
"matches": [{"feed": "activedefense", "match": "exact", "type": "BankDrop",
"value": "IBAN:DE89370400440532013000", "chats": 3, "firstSeen": "…", "lastSeen": "…", "confidence": 0.9}],
"enrichment": {"summary": "bank Commerzbank; DE; BIC COBADEFFXXX", "source": "Bundesbank", "country": "DE",
"organisation": "Commerzbank", "kind": "bank", "code": "COBADEFFXXX", "…": "…"}},
{"value": "+57 300 123 4567", "kind": "phone", "known": false, "risk": 0, "chats": 0, "matches": [],
"enrichment": {"summary": "mobile; CO", "country": "CO", "kind": "mobile", "…": "…"}},
{"value": "evil.example", "kind": "domain", "known": true, "risk": 52, "matches": [{"match": "domain", "type": "Url", "…": "…"}], "…": "…"}]}
Each result says how the value was read (kind), whether it is known, a
risk from 0 to 100, first and last mention, in how many chats, whether it is
fresh (mentioned within 7 days), the rings it belongs to (the same id in two results
means the same operator) and every match with its feed (activedefense or own) and
closeness (exact, or domain for the same site). enrichment carries what
is publicly known: the bank of an IBAN, the country and kind of a number, what a wallet received, when a domain
was registered and where it is hosted. lists names the public lists that know a match as well
(URLhaus, OpenPhish, ScamSniffer, MEW darklist, checked daily). A risk of 0 means we have no evidence; it is
no proof that a value is safe.
Every checked value is counted for your workspace (Integration shows the month's count). More than
100 values or one longer than 512 characters answer 400; a resting plan 402.
Plans and limits
Without a plan your workspace is a sandbox of a few conversations. A pilot or a subscription lifts
that. When a plan does not take a new conversation, POST /api/ingest/webhook answers
402 with {"error": "plan-limit", "plan", "reason", "detail"} instead of dropping
the message; running conversations go on. An invoice left open too long pauses new conversations, the
autopilot and the feeds until it is paid; nothing is deleted.
Start with a workspace
Sign in to the console: your workspace, key and a sandbox persona are ready at once.