{"openapi":"3.0.3","info":{"title":"MFA Feed 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. Only `GET`, `HEAD`, and `OPTIONS` are accepted; 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 up to 50,000 domains matching the filter. 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"]}}],"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"}},"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"}}}},"/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":"Domain not found in the feed.","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"}}}},"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. Only `GET`, `HEAD`, and `OPTIONS` are accepted across the public API.","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":{"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 free-tier cap was applied."},"totalAvailable":{"type":"integer","description":"Total matching the filter regardless of free-tier cap."}}},"StatsResponse":{"type":"object","required":["total","red","amber","yellow"],"properties":{"total":{"type":"integer"},"red":{"type":"integer"},"amber":{"type":"integer"},"yellow":{"type":"integer"}}},"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"}}}}}}