{
  "openapi": "3.1.0",
  "info": {
    "title": "DomainDrift - Continuous Internet Telemetry API",
    "version": "1.180.1",
    "description": "Continuous internet telemetry across ~2,051,000 curated domains. DNS, TLS, WHOIS, HTTPS, subdomains, DNSSEC, ASN - every observation Ed25519-signed with provenance receipts. Public alpha."
  },
  "servers": [
    {
      "url": "https://domaindrift.io"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-DomainDrift-Key",
        "description": "API key with dd_ prefix (legacy keys and headers are still accepted). Rate limit: 100 req/min per key (default)."
      }
    },
    "schemas": {
      "DomainSummary": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "example": "openai.com"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "example": "ai_ml"
          },
          "last_scanned_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "scan_count": {
            "type": "integer"
          },
          "ping_ms": {
            "type": "number",
            "nullable": true
          },
          "http_status": {
            "type": "integer",
            "nullable": true
          },
          "probe_verdict": {
            "type": "string",
            "nullable": true,
            "enum": [
              "ok",
              "redirect",
              "bot_blocked",
              "not_found",
              "method_blocked",
              "rate_limited",
              "client_error",
              "server_error",
              "cf_blocked",
              "no_response",
              "timeout",
              "tls_error",
              "connection_refused",
              "network_error",
              "dns_failed",
              "nxdomain",
              "dns_servfail",
              "dns_refused",
              "dns_timeout",
              "dns_empty"
            ]
          },
          "subdomain_count": {
            "type": "integer"
          },
          "service_count": {
            "type": "integer"
          },
          "has_dnssec": {
            "type": [
              "integer",
              "null"
            ],
            "enum": [
              0,
              1,
              null
            ]
          },
          "dnssec_ok_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the DNSSEC plane last succeeded. NULL means has_dnssec is unmeasured, not false."
          },
          "has_ipv6": {
            "type": "boolean"
          },
          "complexity_score": {
            "type": "number"
          }
        }
      },
      "DeltaChange": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "example": "dns.ns.changed"
          },
          "field": {
            "type": "string",
            "example": "dns.ns"
          },
          "action": {
            "type": "string",
            "enum": [
              "added",
              "removed",
              "changed",
              "warning"
            ]
          },
          "old_value": {
            "type": "string",
            "nullable": true
          },
          "new_value": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ProvenanceReceipt": {
        "type": "object",
        "properties": {
          "receipt_id": {
            "type": "string",
            "format": "uuid"
          },
          "domain": {
            "type": "string"
          },
          "scanned_at": {
            "type": "string",
            "format": "date-time"
          },
          "public_key": {
            "type": "string"
          },
          "output_hash": {
            "type": "string"
          },
          "plane": {
            "type": "string",
            "enum": [
              "fast",
              "whois",
              "asn",
              "robots",
              "dnssec",
              "composite"
            ]
          },
          "parent_receipt_id": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      }
    }
  },
  "paths": {
    "/health": {
      "get": {
        "summary": "Health check",
        "description": "Returns service status, domain counts, and scan rate. No authentication required.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "degraded"
                      ]
                    },
                    "product": {
                      "type": "string",
                      "example": "DomainDrift"
                    },
                    "version": {
                      "type": "string"
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "domains_tracked": {
                      "type": "integer",
                      "description": "Total domains in catalog"
                    },
                    "domains_observed": {
                      "type": "integer",
                      "description": "Domains scanned in last 24h"
                    },
                    "snapshots_total": {
                      "type": "integer",
                      "description": "All-time scan count (sum of per-domain scan_count)"
                    },
                    "obsPerMin": {
                      "type": "number",
                      "description": "Current scan rate (observations per minute)"
                    },
                    "scanningNow": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains": {
      "get": {
        "summary": "List tracked domains",
        "description": "Paginated list of domains in the catalog. Filter by category or search by name.",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by category slug (e.g. ai_ml, crypto_web3)"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Substring search on the domain name (case-insensitive; % and _ are matched literally)"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv"
              ]
            },
            "description": "csv returns the same filtered+paginated set as a text/csv download (flat columns)."
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated domain list (JSON), or a text/csv download when format=csv",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "domains": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DomainSummary"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "page": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        },
                        "pages": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Add domain(s) to catalog",
        "description": "Add one or more domains (max 100 per request). Duplicates are NOT an error: they come back 200 with results[].status \"exists\". Per-domain outcomes: added, reactivated, exists, rejected.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Provide either `domain` (single) or `domains` (bulk, max 100).",
                "properties": {
                  "domain": {
                    "type": "string",
                    "example": "example.com"
                  },
                  "domains": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Bulk add (max 100)"
                  },
                  "category": {
                    "type": "string",
                    "example": "dev_tools"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Nothing new added (every domain already exists or was rejected). Same body format as 201."
          },
          "201": {
            "description": "At least one domain added or reactivated - scanned on the next sweep",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "service": {
                      "type": "string",
                      "example": "connor"
                    },
                    "version": {
                      "type": "string"
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "added": {
                      "type": "integer",
                      "description": "Count of added + reactivated domains"
                    },
                    "total": {
                      "type": "integer",
                      "description": "Count of processed domains"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "domain": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "added",
                              "reactivated",
                              "exists",
                              "rejected"
                            ]
                          },
                          "error": {
                            "type": "string",
                            "description": "Present when status is rejected"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing domain(s) or more than 100 domains in one request"
          }
        }
      }
    },
    "/v1/domains/{domain}": {
      "get": {
        "summary": "Latest signal snapshot for domain",
        "description": "Returns the most recent DNS, HTTPS probe, and enrichment data, from the recent-observation window (last 24h).",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Latest snapshot with DNS records, probe status, and services"
          },
          "404": {
            "description": "Domain not in catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains/{domain}/history": {
      "get": {
        "summary": "Snapshot history",
        "description": "Time-series of scan snapshots from the recent-observation window (last 24h) - use the bundle endpoint for a signed 7-day (included) or 90-day (bought) export.",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Array of snapshots ordered by time descending"
          }
        }
      }
    },
    "/v1/domains/{domain}/provenance": {
      "get": {
        "summary": "Provenance receipts",
        "description": "Ed25519-signed receipts from the recent-observation window (last 24h). Each receipt covers one scanning plane. Use the bundle endpoint for a signed 7-day (included) or 90-day (bought) export.",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Provenance receipt chain",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "domain": {
                      "type": "string"
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "d1"
                      ],
                      "description": "Where these receipts came from: the recent-observation window (last 24h)"
                    },
                    "receipts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProvenanceReceipt"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains/{domain}/bundle": {
      "get": {
        "summary": "Evidence bundle",
        "description": "Self-contained signed evidence bundle for offline verification, read from the signed archive. TWO LANES. `window=short` (the default) covers the last 7 days and is included in your plan's monthly bundle allowance. `window=deep` covers 90 days, is included in no plan, and is bought per export with DRM3 credits. A paid lane requires `pay=credits` in so many words: without it you get a 402 stating the price and the URL that pays it, and NOTHING is debited. A purchase is idempotent per account, lane, domain and UTC day, so a retried download never debits twice. Two modes: full (every receipt) or compact (Merkle rollup).",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "window",
            "in": "query",
            "description": "short = the included 7-day export. deep = the 90-day export, bought with credits.",
            "schema": {
              "type": "string",
              "enum": [
                "short",
                "deep"
              ],
              "default": "short"
            }
          },
          {
            "name": "pay",
            "in": "query",
            "description": "Explicit consent to debit your DRM3 credit purse for this export. Only value: credits.",
            "schema": {
              "type": "string",
              "enum": [
                "credits"
              ]
            }
          },
          {
            "name": "mode",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "full",
                "compact"
              ],
              "default": "full"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "JSON evidence bundle (Content-Disposition: attachment). A purchased export also carries X-DRM3-Charged-Credits, plus X-DRM3-Charge-Replay when the debit was a replay of an earlier identical purchase and X-DRM3-Credits-Overdraft when it drew the purse negative."
          },
          "402": {
            "description": "Payment required, and nothing was charged: the monthly allowance is spent, the deep export needs consent, or the credit purse is empty. The body states the price in credits and money, and the URL that pays it."
          },
          "403": {
            "description": "This account has no bundle entitlement"
          },
          "404": {
            "description": "Domain not tracked"
          },
          "502": {
            "description": "The credit ledger could not be reached. Nothing was charged."
          },
          "503": {
            "description": "The signed archive could not be read, or credit purchases are not configured. Nothing was charged."
          }
        }
      }
    },
    "/v1/changes": {
      "get": {
        "summary": "Global change feed",
        "description": "Material infrastructure changes detected across the catalog, from the recent-observation window (last 24h). Two modes: feed (default, newest first) and sync (pass since and/or cursor - ordered (detected_at, id) ascending with keyset pagination for incremental data pulls).",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 500
            },
            "description": "Feed mode caps at 100 (default 20); sync mode caps at 500 (default 100)"
          },
          {
            "name": "domain",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter to a specific domain"
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by event type, PREFIX match: type=dns matches every dns.* event (dns.ns.changed, dns.a.added, ...); a full type like dns.ns.changed matches exactly. Prefixes: dns, tls, whois, http, subdomain, content."
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Sync mode: return changes with detected_at >= this ISO timestamp, ordered (detected_at, id) ascending. Page forward with next_cursor."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Sync mode: opaque keyset cursor (the next_cursor of the previous response). Resumes strictly after the last row of that page."
          }
        ],
        "responses": {
          "200": {
            "description": "Recent changes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "changes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "domain": {
                            "type": "string"
                          },
                          "detected_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "change_count": {
                            "type": "integer"
                          },
                          "change_summary": {
                            "type": "string"
                          },
                          "changes": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/DeltaChange"
                            }
                          }
                        }
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Sync mode only: opaque cursor for the next page, or null when this page is the end. Absent in feed mode."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unparseable since timestamp or cursor"
          }
        }
      }
    },
    "/v1/alerts": {
      "get": {
        "summary": "List your change alerts",
        "description": "Your account's webhook subscriptions. Requires an account key (a signed-in owner). Signing secrets are never re-shown.",
        "responses": {
          "200": {
            "description": "Your subscriptions"
          },
          "400": {
            "description": "Not an account key (service/demo/x402 have no owner) - sign in for a key"
          }
        }
      },
      "post": {
        "summary": "Create a change alert (webhook)",
        "description": "Subscribe a public https webhook to material changes on a domain, a cohort tag, or the whole catalog. Every delivery carries X-DomainDrift-Signature = HMAC-SHA256(secret, body). The signing secret is returned once. Email channel is not available yet and is rejected.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "scope_kind",
                  "target"
                ],
                "properties": {
                  "scope_kind": {
                    "type": "string",
                    "enum": [
                      "domain",
                      "tag",
                      "catalog"
                    ]
                  },
                  "scope_value": {
                    "type": "string",
                    "description": "domain, or tag \"key\"/\"key:value\"; omit for catalog"
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "webhook"
                    ],
                    "default": "webhook"
                  },
                  "target": {
                    "type": "string",
                    "description": "public https webhook URL",
                    "example": "https://hooks.example.com/domaindrift"
                  },
                  "min_weight": {
                    "type": "integer",
                    "default": 1,
                    "description": "fire only when change significance >= this"
                  },
                  "type_prefix": {
                    "type": "string",
                    "description": "optional change-type prefixes, comma-separated - any one matching fires. A prefix covers itself and its dotted children, so \"tls\" covers tls.issuer.changed but never tlsfoo.changed. e.g. \"tls\" or \"dns, tls, whois\""
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created - body includes signing_secret (shown once)"
          },
          "400": {
            "description": "Invalid scope/target, or channel not available (email), or limit reached"
          }
        }
      }
    },
    "/v1/alerts/{id}": {
      "delete": {
        "summary": "Delete one of your alerts",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted"
          },
          "404": {
            "description": "Not found or not yours"
          }
        }
      }
    },
    "/v1/stream": {
      "get": {
        "summary": "SSE event stream",
        "description": "Server-Sent Events stream of scans, changes, and provenance receipts in real time.",
        "responses": {
          "200": {
            "description": "text/event-stream"
          }
        }
      }
    },
    "/v1/scan/{domain}": {
      "post": {
        "summary": "Push scan result",
        "description": "Push a complete scan signal with provenance receipt. Requires both signal and signed receipt - unsigned pushes are rejected.",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Scan stored"
          },
          "400": {
            "description": "Missing signal or provenance"
          }
        }
      }
    }
  }
}