{
  "openapi": "3.1.0",
  "info": {
    "title": "Active Defense API",
    "version": "1.0",
    "description": "Forward suspected scam messages, get the persona's approved replies back and pull the indicators scammers leaked. Human docs: https://activedefenseapi.com/docs"
  },
  "servers": [{ "url": "https://activedefenseapi.com" }],
  "security": [{ "apiKey": [] }],
  "paths": {
    "/api/ingest/webhook": {
      "post": {
        "summary": "Forward a suspected scam message",
        "description": "One honeypot conversation per sourceIdentifier. The persona drafts a reply; it waits for approval in the console unless autopilot is on.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IngestRequest" } } }
        },
        "responses": {
          "200": { "description": "Accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IngestAck" } } } },
          "400": { "description": "sourceIdentifier or content missing" },
          "401": { "description": "No or an invalid API key" },
          "402": { "description": "The plan takes no new conversation now: {error: plan-limit, plan, reason, detail}" }
        }
      }
    },
    "/api/ingest/replies": {
      "get": {
        "summary": "Pull the approved replies after a cursor",
        "parameters": [
          { "name": "since", "in": "query", "schema": { "type": "integer", "format": "int64", "default": 0 }, "description": "The nextCursor of the previous pull" },
          { "name": "max", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 } }
        ],
        "responses": {
          "200": { "description": "A page of replies", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReplyPage" } } } },
          "401": { "description": "No or an invalid API key" }
        }
      }
    },
    "/api/ingest/whatsapp/{apiKey}": {
      "post": {
        "summary": "Twilio WhatsApp inbound webhook",
        "description": "Point your Twilio number's inbound webhook here. Answers with empty TwiML at once; with a Twilio auth token configured only requests with a valid X-Twilio-Signature count.",
        "security": [],
        "parameters": [{ "name": "apiKey", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": { "content": { "application/x-www-form-urlencoded": { "schema": { "type": "object" } } } },
        "responses": { "200": { "description": "Empty TwiML", "content": { "text/xml": {} } }, "403": { "description": "Invalid Twilio signature" } }
      }
    },
    "/taxii2/t/{tenantId}/": {
      "get": {
        "summary": "TAXII 2.1 API root of your feed",
        "security": [{ "bearer": [] }, { "basic": [] }, { "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/TenantId" }],
        "responses": { "200": { "description": "API root", "content": { "application/taxii+json;version=2.1": {} } }, "401": { "description": "Not your key" } }
      }
    },
    "/taxii2/t/{tenantId}/collections/": {
      "get": {
        "summary": "The one collection of your feed",
        "security": [{ "bearer": [] }, { "basic": [] }, { "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/TenantId" }],
        "responses": { "200": { "description": "Collections", "content": { "application/taxii+json;version=2.1": {} } }, "401": { "description": "Not your key" } }
      }
    },
    "/taxii2/t/{tenantId}/collections/{collectionId}/objects/": {
      "get": {
        "summary": "STIX 2.1 objects of your feed",
        "security": [{ "bearer": [] }, { "basic": [] }, { "apiKey": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/TenantId" },
          { "name": "collectionId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "added_after", "in": "query", "schema": { "type": "string", "format": "date-time" } },
          { "name": "match[id]", "in": "query", "schema": { "type": "string" } },
          { "name": "match[type]", "in": "query", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "maximum": 1000 } },
          { "name": "next", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "TAXII envelope", "content": { "application/taxii+json;version=2.1": {} } }, "401": { "description": "Not your key" } }
      }
    },
    "/feeds/t/{tenantId}/stix.json": {
      "get": {
        "summary": "Your feed as a STIX 2.1 bundle",
        "security": [{ "bearer": [] }, { "basic": [] }, { "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/TenantId" }],
        "responses": { "200": { "description": "STIX bundle", "content": { "application/stix+json;version=2.1": {} } }, "401": { "description": "Not your key" } }
      }
    },
    "/api/check": {
      "post": {
        "summary": "Check values against the scam indicators",
        "description": "Up to 100 values a call: IBANs, BICs, crypto wallets, phone numbers, e-mail addresses, links, domains, messenger handles, SHA-256 hashes. Each is looked up in Active Defense's own feed and in yours and answered with its matches, first and last mention, chats, rings, what is publicly known about it and a risk from 0 to 100 (0 = no evidence, not proof that it is safe). Every checked value is counted.",
        "security": [{ "apiKey": [] }, { "bearer": [] }, { "basic": [] }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheckRequest" } } }
        },
        "responses": {
          "200": { "description": "One result per value, in order", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheckResponse" } } } },
          "400": { "description": "No values, more than 100, or one longer than 512 characters" },
          "401": { "description": "No or an invalid API key" },
          "402": { "description": "The plan rests (an invoice open too long): {error: plan-limit, plan, reason, detail}" },
          "429": { "description": "Too many requests for this key: wait the seconds in Retry-After" }
        }
      }
    },
    "/feeds/t/{tenantId}/iocs.csv": {
      "get": {
        "summary": "Your feed as CSV: type, value, first_seen, last_seen, confidence, chats, days_active, fresh, enrichment",
        "security": [{ "bearer": [] }, { "basic": [] }, { "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/TenantId" }],
        "responses": { "200": { "description": "CSV", "content": { "text/csv": {} } }, "401": { "description": "Not your key" } }
      }
    },
    "/taxii2/feed/": {
      "get": {
        "summary": "TAXII 2.1 API root of the Active Defense feed (add-on)",
        "description": "What scammers handed to Active Defense decoy accounts in live chats, as it comes. Answers while the feed add-on is booked and paid; 402 otherwise.",
        "security": [{ "bearer": [] }, { "basic": [] }, { "apiKey": [] }],
        "responses": { "200": { "description": "API root", "content": { "application/taxii+json;version=2.1": {} } }, "401": { "description": "No valid key" }, "402": { "description": "Feed add-on not booked or not paid" } }
      }
    },
    "/taxii2/feed/collections/": {
      "get": {
        "summary": "The one collection of the Active Defense feed",
        "security": [{ "bearer": [] }, { "basic": [] }, { "apiKey": [] }],
        "responses": { "200": { "description": "Collections", "content": { "application/taxii+json;version=2.1": {} } }, "401": { "description": "No valid key" }, "402": { "description": "Feed add-on not booked or not paid" } }
      }
    },
    "/feeds/feed/stix.json": {
      "get": {
        "summary": "The Active Defense feed as a STIX 2.1 bundle",
        "security": [{ "bearer": [] }, { "basic": [] }, { "apiKey": [] }],
        "responses": { "200": { "description": "STIX bundle", "content": { "application/stix+json;version=2.1": {} } }, "401": { "description": "No valid key" }, "402": { "description": "Feed add-on not booked or not paid" } }
      }
    },
    "/feeds/feed/iocs.csv": {
      "get": {
        "summary": "The Active Defense feed as CSV",
        "security": [{ "bearer": [] }, { "basic": [] }, { "apiKey": [] }],
        "responses": { "200": { "description": "CSV", "content": { "text/csv": {} } }, "401": { "description": "No valid key" }, "402": { "description": "Feed add-on not booked or not paid" } }
      }
    }
  },
  "webhooks": {
    "approvedReply": {
      "post": {
        "summary": "An approved reply, POSTed to your callback URL",
        "description": "X-AD-Signature = 'sha256=' + hex(HMAC-SHA256(secret, X-AD-Timestamp + '.' + raw body)); the secret is your signing secret, or your API key until you make one. Reject timestamps more than 5 minutes off. Without a 2xx answer the call is repeated after 1, 5 and 30 minutes, 2 and 6 hours: messageId is the idempotency key.",
        "parameters": [
          { "name": "X-AD-Timestamp", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Unix seconds" },
          { "name": "X-AD-Signature", "in": "header", "required": true, "schema": { "type": "string" }, "description": "sha256=<hex>" }
        ],
        "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Callback" } } } },
        "responses": { "2XX": { "description": "Received" } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": { "type": "apiKey", "in": "header", "name": "X-API-Key" },
      "bearer": { "type": "http", "scheme": "bearer", "description": "Your API key as the bearer token" },
      "basic": { "type": "http", "scheme": "basic", "description": "Your API key as the password" }
    },
    "parameters": {
      "TenantId": { "name": "tenantId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
    },
    "schemas": {
      "IngestRequest": {
        "type": "object",
        "required": ["sourceIdentifier", "content"],
        "properties": {
          "sourceIdentifier": { "type": "string", "description": "Your stable id for the scammer's thread" },
          "content": { "type": "string" },
          "eventType": { "type": ["string", "null"] },
          "personaName": { "type": ["string", "null"], "description": "Another persona for a new conversation" }
        }
      },
      "IngestAck": {
        "type": "object",
        "properties": {
          "webhookEventId": { "type": "string", "format": "uuid" },
          "sourceIdentifier": { "type": "string" },
          "status": { "type": "string", "examples": ["Accepted"] },
          "receivedAt": { "type": "string", "format": "date-time" }
        }
      },
      "Reply": {
        "type": "object",
        "properties": {
          "seq": { "type": "integer", "format": "int64" },
          "sourceIdentifier": { "type": "string" },
          "messageId": { "type": "string", "format": "uuid" },
          "content": { "type": "string" },
          "at": { "type": "string", "format": "date-time" }
        }
      },
      "ReplyPage": {
        "type": "object",
        "properties": {
          "replies": { "type": "array", "items": { "$ref": "#/components/schemas/Reply" } },
          "nextCursor": { "type": "integer", "format": "int64" }
        }
      },
      "Callback": {
        "type": "object",
        "properties": {
          "tenantId": { "type": "string", "format": "uuid" },
          "sourceIdentifier": { "type": "string" },
          "messageId": { "type": "string", "format": "uuid" },
          "reply": { "type": "string" },
          "timestamp": { "type": "string" },
          "test": { "type": "boolean", "description": "Only on a test sent from the console" }
        }
      },
      "CheckRequest": {
        "type": "object",
        "required": ["values"],
        "properties": {
          "values": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "string", "maxLength": 512 },
            "examples": [["DE89 3704 0044 0532 0130 00", "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE", "+57 300 123 4567", "evil.example"]] }
        }
      },
      "CheckMatch": {
        "type": "object",
        "properties": {
          "feed": { "type": "string", "enum": ["activedefense", "own"], "description": "Active Defense's own decoys, or your chats" },
          "match": { "type": "string", "enum": ["exact", "domain"], "description": "The same value, or only the same site" },
          "type": { "type": "string", "examples": ["BankDrop", "CryptoWallet", "Phone", "MessagingAccount", "PaymentApp", "Url", "Email", "FileHash"] },
          "value": { "type": "string", "description": "As the feed has it, e.g. IBAN:DE89370400440532013000" },
          "chats": { "type": "integer" },
          "firstSeen": { "type": "string", "format": "date-time" },
          "lastSeen": { "type": "string", "format": "date-time" },
          "confidence": { "type": "number" }
        }
      },
      "CheckResult": {
        "type": "object",
        "properties": {
          "value": { "type": "string" },
          "kind": { "type": "string", "enum": ["iban", "bic", "wallet", "phone", "email", "url", "domain", "handle", "sha256", "phash", "text"] },
          "known": { "type": "boolean" },
          "risk": { "type": "integer", "minimum": 0, "maximum": 100, "description": "0 = no evidence, not proof that it is safe" },
          "firstSeen": { "type": ["string", "null"], "format": "date-time" },
          "lastSeen": { "type": ["string", "null"], "format": "date-time" },
          "chats": { "type": "integer" },
          "fresh": { "type": "boolean", "description": "Last mentioned within 7 days" },
          "rings": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Groups of chats one operator runs; the same id in two results = the same ring" },
          "matches": { "type": "array", "items": { "$ref": "#/components/schemas/CheckMatch" } },
          "lists": { "type": "array", "items": { "type": "string" }, "description": "Public lists that name a match as well (URLhaus, OpenPhish, ScamSniffer, MEW darklist), checked daily" },
          "enrichment": { "type": ["object", "null"], "description": "What is publicly known: summary (English), source, checkedAt, country, organisation (bank, network), kind, code (BIC), registered, registrar, wallet" }
        }
      },
      "CheckResponse": {
        "type": "object",
        "properties": {
          "checkedAt": { "type": "string", "format": "date-time" },
          "results": { "type": "array", "items": { "$ref": "#/components/schemas/CheckResult" } }
        }
      }
    }
  }
}