{"openapi":"3.0.3","info":{"title":"MFA Intelligence API","version":"1.0.0","description":"Programmatic access to MFA (made-for-advertising) domain intelligence. All endpoints require an API key via `Authorization: Bearer <key>` or `?api_key=<key>` query param. If both are present the header wins; the `Bearer` scheme is matched case-insensitively. Supported methods are listed per endpoint; other verbs return 405 with an `Allow` header. Repeated query keys return 400 rather than being silently de-duplicated. All responses carry `Cache-Control: private, no-store`. See https://mfa.redvolcano.uk/docs for human-readable docs.","contact":{"name":"Red Volcano","url":"https://mfa.redvolcano.uk"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://mfa.redvolcano.uk/api/v1","description":"Production"}],"security":[{"BearerAuth":[]},{"ApiKeyQuery":[]}],"paths":{"/feed":{"get":{"summary":"List MFA-flagged domains","description":"Returns a paginated list of MFA-flagged domains with scores, tiers, and detection signals. Invalid query parameters return 400 rather than being silently clamped or ignored.","operationId":"getFeed","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"1-indexed page number."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Results per page. Values above 100 return 400."},{"name":"tier","in":"query","schema":{"type":"string","enum":["red","amber","yellow"]},"description":"Filter by tier."},{"name":"search","in":"query","schema":{"type":"string","maxLength":100},"description":"Case-insensitive substring match on domain name."},{"name":"sort","in":"query","schema":{"type":"string","enum":["score","domain","created_at"],"default":"score"}},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}}],"responses":{"200":{"description":"Paginated feed","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/feed/export":{"get":{"summary":"Export full feed as CSV or JSON","description":"Exports domains matching the filter, up to 50,000 per request. When more remain, `X-Export-Truncated` is `true` and `X-Export-Next-Cursor` carries an opaque cursor: pass it as `after` to fetch the next page, and repeat until `X-Export-Truncated` is `false`. Results are ordered by score descending. Returns `application/json` or `text/csv` based on the `format` param. Subject to a separate daily export rate limit.","operationId":"exportFeed","parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"],"default":"json"}},{"name":"tier","in":"query","schema":{"type":"string","enum":["red","amber","yellow"]}},{"name":"after","in":"query","description":"Opaque cursor from a previous response's X-Export-Next-Cursor header.","schema":{"type":"string"}},{"name":"limit","in":"query","description":"Rows per page (1-50000). Defaults to the maximum.","schema":{"type":"integer","minimum":1,"maximum":50000,"default":50000}}],"responses":{"200":{"description":"Export payload","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"X-Export-Truncated":{"description":"true when further pages remain for this filter.","schema":{"type":"string","enum":["true","false"]}},"X-Export-Next-Cursor":{"description":"Cursor to pass as `after` for the next page. Absent on the final page.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FeedDomainItem"}}},"text/csv":{"schema":{"type":"string","example":"domain,score,tier,detected_at,signals\nexample-mfa.com,87,red,2026-03-15T10:30:00Z,\"Excessive Ads;Thin Content\""}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/stats":{"get":{"summary":"Aggregate tier counts","operationId":"getStats","responses":{"200":{"description":"Tier counts","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domain/{domain}":{"get":{"summary":"Look up a single domain","description":"Returns the full detection record for the domain, or 404 if the domain is not in the feed. Lookup is case-insensitive.","operationId":"getDomain","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string","example":"example-mfa.com"},"description":"Hostname (letters, digits, hyphens, dots). Case is normalized."}],"responses":{"200":{"description":"Domain record","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedDomainItem"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/domain-checks":{"get":{"summary":"List domain-check requests","operationId":"listDomainChecks","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}}],"responses":{"200":{"description":"Recent requests for the API key's organisation.","content":{"application/json":{"schema":{"type":"object","properties":{"requests":{"type":"array","items":{"$ref":"#/components/schemas/DomainCheckSummary"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"summary":"Submit one or more domains for MFA assessment","description":"Accepts one domain or up to 1,000 domains. A result observed within 24 hours is returned from cache; otherwise the response is normally 202 and should be polled. New crawls are limited to 1,000 per organisation per UTC day. Requested scans run MFA scoring only; clone analysis is deliberately not run.","operationId":"createDomainCheck","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["domain"],"properties":{"domain":{"type":"string","example":"example.com"}}},{"type":"object","required":["domains"],"properties":{"domains":{"type":"array","minItems":1,"maxItems":1000,"items":{"type":"string"}}}}]}}}},"responses":{"200":{"description":"Every item was served from the 24-hour cache.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainCheckRequest"}}}},"202":{"description":"The request was queued or joined an active scan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainCheckRequest"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"description":"The domain is paused by an administrator."},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/DomainChecksDisabled"}}}},"/domain-checks/{id}":{"get":{"summary":"Poll a domain-check request","operationId":"getDomainCheck","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":100}}],"responses":{"200":{"description":"Current request and item states.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainCheckRequest"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/monitored-domains/{domain}":{"get":{"summary":"Get monitoring state for a domain","operationId":"getMonitoredDomain","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Monitoring schedule and current valid assessment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MonitoredDomain"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"summary":"Stop monitoring a domain for this organisation","operationId":"stopMonitoringDomain","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Monitoring stopped.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/rings":{"get":{"summary":"MFA rings (clusters by identical ads.txt)","description":"Groups flagged domains by MD5 hash of their ads.txt body. Only hashes with 5 or more domains are returned.","operationId":"getRings","responses":{"200":{"description":"Rings payload","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RingsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"Opaque API key","description":"Pass via `Authorization: Bearer <key>` header."},"ApiKeyQuery":{"type":"apiKey","in":"query","name":"api_key","description":"Alternative to Bearer. Pass via `?api_key=<key>`."}},"headers":{"RateLimitLimit":{"schema":{"type":"integer"},"description":"Max requests per hour for this API key."},"RateLimitRemaining":{"schema":{"type":"integer"},"description":"Requests remaining in the current hour."},"RateLimitReset":{"schema":{"type":"integer"},"description":"Unix timestamp when the current window resets."}},"responses":{"Unauthorized":{"description":"Missing, invalid, or revoked API key. Rate-limit headers are NOT returned because no valid key identifies the bucket.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"The requested resource was not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Rate limit exceeded.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds until the client may retry."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"DomainChecksDisabled":{"description":"New domain-check submissions are disabled by the rollout flag.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Suggested seconds before checking availability again."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ValidationError":{"description":"One or more query parameters were rejected. The `details` array lists each field that failed validation. Also returned when a query key is repeated (e.g. `?tier=red&tier=amber`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"MethodNotAllowed":{"description":"The HTTP method is not supported on this endpoint. Inspect the `Allow` header for the endpoint-specific method list.","headers":{"Allow":{"schema":{"type":"string","example":"GET, HEAD, OPTIONS"},"description":"Comma-separated list of accepted methods."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}},"ValidationError":{"type":"object","required":["error","details"],"properties":{"error":{"type":"string","example":"Invalid query parameters"},"details":{"type":"array","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string","example":"tier"},"message":{"type":"string","example":"tier must be one of: red, amber, yellow"}}}}}},"Breakdown":{"type":"object","required":["key","label","score","max"],"properties":{"key":{"type":"string","example":"ads_txt"},"label":{"type":"string","example":"Monetization"},"score":{"type":"number"},"max":{"type":"number"}}},"Signal":{"type":"object","required":["key","label"],"properties":{"key":{"type":"string","example":"excessive_ads"},"label":{"type":"string","example":"Excessive Ads"},"tooltip":{"type":"string"}}},"MonetizationSummary":{"type":"object","required":["label","severity"],"properties":{"label":{"type":"string"},"severity":{"type":"string","enum":["low","medium","high"]}}},"FeedDomainItem":{"type":"object","required":["id","domain","score","tier","created_at","signals"],"properties":{"availability":{"type":"object","description":"DNS name availability, independent of the MFA rating. Resolves does not prove the website works. Ambiguous checks retain the last confirmed status.","required":["status","lastCheckedAt","lastConfirmedAt","offlineSince","lastResult"],"properties":{"status":{"type":"string","enum":["unknown","resolves","offline"]},"lastCheckedAt":{"type":"string","format":"date-time","nullable":true},"lastConfirmedAt":{"type":"string","format":"date-time","nullable":true},"offlineSince":{"type":"string","format":"date-time","nullable":true},"lastResult":{"type":"string","nullable":true,"enum":["resolves","offline","inconclusive"]}}},"id":{"type":"integer"},"domain":{"type":"string","example":"example-mfa.com"},"score":{"type":"integer","minimum":0,"maximum":100},"tier":{"type":"string","enum":["red","amber","yellow"]},"created_at":{"type":"string","format":"date-time","example":"2026-03-15T10:30:00Z"},"breakdown":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/Breakdown"}},"signals":{"type":"array","items":{"$ref":"#/components/schemas/Signal"}},"monetizationSummary":{"type":"array","items":{"$ref":"#/components/schemas/MonetizationSummary"}}}},"FeedResponse":{"type":"object","required":["domains","total","page","totalPages","isLimited","totalAvailable"],"properties":{"domains":{"type":"array","items":{"$ref":"#/components/schemas/FeedDomainItem"}},"total":{"type":"integer"},"page":{"type":"integer"},"totalPages":{"type":"integer"},"isLimited":{"type":"boolean","description":"True when the standard-access result cap was applied."},"totalAvailable":{"type":"integer","description":"Total matching the filter regardless of the standard-access cap."}}},"StatsResponse":{"type":"object","required":["total","red","amber","yellow"],"properties":{"total":{"type":"integer"},"red":{"type":"integer"},"amber":{"type":"integer"},"yellow":{"type":"integer"}}},"DomainAssessment":{"type":"object","required":["domain","status","outcome","score","tier","signals"],"properties":{"domain":{"type":"string"},"status":{"type":"string","enum":["completed","inconclusive","failed"]},"outcome":{"type":"string","enum":["completed","cached","inconclusive","failed"]},"verdict":{"type":"string","enum":["mfa_risk_detected","not_currently_flagged"]},"score":{"type":"number","nullable":true},"tier":{"type":"string","nullable":true,"enum":["green","yellow","amber","red"]},"observedAt":{"type":"string","format":"date-time","nullable":true},"breakdown":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/Breakdown"}},"signals":{"type":"array","items":{"$ref":"#/components/schemas/Signal"}},"monetizationSummary":{"type":"array","items":{"$ref":"#/components/schemas/MonetizationSummary"}},"reason":{"type":"string"},"cached":{"type":"boolean"}}},"DomainCheckItem":{"type":"object","required":["domain","status"],"properties":{"domain":{"type":"string"},"status":{"type":"string","enum":["queued","running","completed","inconclusive","failed"]},"outcome":{"type":"string","nullable":true},"result":{"allOf":[{"$ref":"#/components/schemas/DomainAssessment"}],"nullable":true},"error":{"type":"string"},"updatedAt":{"type":"string","format":"date-time","nullable":true},"nextScanAt":{"type":"string","format":"date-time","nullable":true}}},"DomainCheckSummary":{"type":"object","required":["id","status","total","completed","inconclusive","failed"],"properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string"},"total":{"type":"integer"},"completed":{"type":"integer"},"inconclusive":{"type":"integer"},"failed":{"type":"integer"},"createdAt":{"type":"string","format":"date-time","nullable":true},"updatedAt":{"type":"string","format":"date-time","nullable":true}}},"DomainCheckRequest":{"allOf":[{"$ref":"#/components/schemas/DomainCheckSummary"},{"type":"object","required":["items","page","limit","totalPages"],"properties":{"completedAt":{"type":"string","format":"date-time","nullable":true},"page":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"},"resultUrl":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/DomainCheckItem"}}}}]},"MonitoredDomain":{"type":"object","required":["domain","active","status","latestAttempt"],"properties":{"domain":{"type":"string"},"active":{"type":"boolean"},"status":{"type":"string","enum":["active","inactive","paused"]},"nextScanAt":{"type":"string","format":"date-time","nullable":true},"lastAttemptAt":{"type":"string","format":"date-time","nullable":true},"latestAttempt":{"type":"object","properties":{"status":{"type":"string","nullable":true},"reason":{"type":"string"}}},"currentAssessment":{"allOf":[{"$ref":"#/components/schemas/DomainAssessment"}],"nullable":true}}},"RingDomain":{"type":"object","required":["domain","tier","score","created_at"],"properties":{"domain":{"type":"string"},"tier":{"type":"string","enum":["red","amber","yellow"]},"score":{"type":"integer"},"created_at":{"type":"string","format":"date-time"}}},"Ring":{"type":"object","required":["hash","size","domains","tiers"],"properties":{"hash":{"type":"string","description":"First 12 chars of the ads.txt body MD5.","example":"a1b2c3d4e5f6"},"size":{"type":"integer"},"domains":{"type":"array","items":{"$ref":"#/components/schemas/RingDomain"}},"tiers":{"type":"object","required":["red","amber","yellow"],"properties":{"red":{"type":"integer"},"amber":{"type":"integer"},"yellow":{"type":"integer"}}}}},"RingsResponse":{"type":"object","required":["rings","totalRings","totalDomainsInRings"],"properties":{"rings":{"type":"array","items":{"$ref":"#/components/schemas/Ring"}},"totalRings":{"type":"integer"},"totalDomainsInRings":{"type":"integer"}}}}}}