{
  "openapi": "3.1.0",
  "info": {
    "title": "DomainDrift - Continuous Internet Telemetry API",
    "version": "0.89.2",
    "description": "Continuous internet telemetry across ~1,006,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": "boolean"
          },
          "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 shape 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. Reads from D1 hot cache (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. Returns from D1 hot cache (last 24h) - use bundle endpoint for full history.",
        "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 D1 hot cache (last 24h). Each receipt covers one scanning plane. Use the bundle endpoint for full archival history.",
        "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": "Data source - d1 = hot cache (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. Reads from the signed archive (last 3 days - cost-bounded window, see bundle.ts). Two modes: full (every receipt) or compact (Merkle rollup).",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mode",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "full",
                "compact"
              ],
              "default": "full"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "JSON evidence bundle (Content-Disposition: attachment)"
          },
          "404": {
            "description": "Domain not tracked"
          }
        }
      }
    },
    "/v1/changes": {
      "get": {
        "summary": "Global change feed",
        "description": "Material infrastructure changes detected across the catalog. From D1 hot cache (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 prefix, e.g. tls, dns, 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"
          }
        }
      }
    }
  }
}