{"openapi":"3.0.3","info":{"title":"Verdict API","version":"0.2.2","description":"Threat decisions with heuristic v3 signals and FastDog proof-of-work clearance."},"servers":[{"url":"/"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"State":{"type":"object","additionalProperties":true,"properties":{"ip":{"type":"string"},"asn":{"oneOf":[{"type":"string"},{"type":"number"}]},"ja3":{"type":"string"},"path":{"type":"string"},"method":{"type":"string"},"ua":{"type":"string"},"headers":{"type":"object","properties":{"_order":{"type":"array","items":{"type":"string"}},"header_order":{"type":"array","items":{"type":"string"}}},"additionalProperties":{"type":"string"}},"header_order":{"type":"array","items":{"type":"string"}},"account_age_hours":{"type":"number","minimum":0},"prior_score":{"type":"number","minimum":0,"maximum":1},"signals":{"type":"array","items":{"type":"string"}},"notes":{"type":"string"}}},"Error":{"type":"object","properties":{"error":{"type":"string"}}}}},"paths":{"/metrics":{"get":{"summary":"Public process metrics (loopback intended)","description":"Lifetime request/action totals and recent per-endpoint latency histograms in milliseconds, with p50/p99 over at most 2048 samples. Resets on restart.","security":[],"responses":{"200":{"description":"Metrics snapshot","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v1/keys/{id}/revoke":{"post":{"summary":"Soft revoke API key","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked"},"401":{"description":"Unauthorized"},"404":{"description":"Key not found"}}}},"/v1/edge-auth":{"get":{"summary":"Local forward auth (no bearer required)","description":"Uses X-Forwarded-For, User-Agent, X-Forwarded-Method and X-Forwarded-Uri, plus visible headers including Accept-Language, for heuristic v3 signals and asynchronous good-bot verification. Accept application/json for a debug decision including reasons[]. A valid fd_verdict_clearance cookie bypasses heuristic evaluation. Clearance is signed, expires after the configured TTL (default 300 seconds), and is bound to host (X-Forwarded-Host or Host), IPv4 /24 or IPv6 /64 prefix, and User-Agent hash. The cookie uses HttpOnly, Secure, SameSite=Lax and Path=/. Challenge responses include a signed, binding-specific score hint in Location for PoW difficulty.","responses":{"200":{"description":"Allow"},"401":{"description":"Challenge; Location and X-Verdict-Action headers"},"403":{"description":"Block"}}}},"/challenge":{"get":{"summary":"FastDog proof-of-work and accessible fallback","parameters":[{"name":"next","in":"query","schema":{"type":"string"},"description":"Local relative path only"},{"name":"hint","in":"query","schema":{"type":"string"},"description":"Signed edge score hint, bound to request context and valid for two minutes"}],"responses":{"200":{"description":"HTML page"},"503":{"description":"Clearance secret unset"}}}},"/challenge/issue":{"get":{"summary":"Issue single-use SHA-256 challenge (two-minute TTL)","parameters":[{"name":"next","in":"query","schema":{"type":"string"},"description":"Local relative path only"},{"name":"hint","in":"query","schema":{"type":"string"},"description":"Signed edge score hint selecting PoW difficulty; invalid, expired or mismatched hints are rejected"}],"responses":{"200":{"description":"challenge_id, difficulty, expires_at"},"400":{"description":"Invalid signed hint"},"429":{"description":"Issuance limit reached"},"503":{"description":"Clearance secret unset"}}}},"/challenge/verify-fallback":{"post":{"summary":"Verify weaker signed arithmetic fallback","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["token","answer","confirm"],"properties":{"token":{"type":"string"},"answer":{"type":"string"},"confirm":{"type":"string","enum":["yes"]},"website":{"type":"string","maxLength":0}}}}}},"responses":{"303":{"description":"Clearance cookie and safe redirect"},"400":{"description":"Invalid, expired or replayed answer"},"503":{"description":"Clearance secret unset"}}}},"/challenge/verify":{"post":{"summary":"Verify SHA-256 proof and issue clearance","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["challenge_id","nonce"],"properties":{"challenge_id":{"type":"string"},"nonce":{"type":"string","pattern":"^[0-9]{1,20}$"}}}}}},"responses":{"200":{"description":"ok: true, next; HttpOnly Secure SameSite=Lax cookie"},"400":{"description":"Invalid, expired or replayed proof"},"503":{"description":"Clearance secret unset"}}}},"/health":{"get":{"summary":"Health check","responses":{"200":{"description":"Healthy","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v1/keys":{"post":{"summary":"Create API key","description":"The first key requires socket loopback or VERDICT_ALLOW_BOOTSTRAP=1 while no keys exist; otherwise 403 bootstrap_disabled. All later requests require a valid Bearer key. Plaintext is returned once.","security":[{"bearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["key","id"],"properties":{"key":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/v1/usage":{"get":{"summary":"Persistent usage for calling key","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Usage","content":{"application/json":{"schema":{"type":"object","properties":{"key_id":{"type":"string"},"decisions":{"type":"integer"},"engines":{"type":"object","additionalProperties":{"type":"integer"}},"period":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/v1/decide":{"post":{"summary":"Decide allow, challenge or block","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/State"},{"type":"object","required":["state"],"properties":{"state":{"oneOf":[{"$ref":"#/components/schemas/State"},{"type":"string"}]}}}]}}}},"responses":{"200":{"description":"Decision","content":{"application/json":{"schema":{"type":"object","required":["action","confidence","reason","reasons","engine","request_id"],"properties":{"action":{"type":"string","enum":["allow","challenge","block"]},"confidence":{"type":"number","minimum":0,"maximum":1},"score":{"type":"number","minimum":0,"maximum":1},"reason":{"type":"string"},"reasons":{"type":"array","items":{"type":"string"}},"engine":{"type":"string"},"request_id":{"type":"string","format":"uuid"}}}}}},"400":{"description":"Invalid body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}}}}