{"openapi":"3.1.0","info":{"title":"MyDomainRisk External Developer API","version":"2.1","description":"Restricted API-key access to the same domain checks a visitor can run on the website. The API is another way into the app: a peek with a key returns exactly what a peek in the browser returns, under exactly the same allowance of five per hour per IP. It is deliberately SHALLOWER than a registered scan - a score, a verdict and finding titles, with no detailed analysis, no methodology and no sources. Available on every plan, including Free. Developer keys cannot read account data, tracked domains, scan history, report URLs or ownership IDs. Fields with a fixed set of answers carry their permitted values here; nothing needs to be inferred from example responses."},"servers":[{"url":"https://api.mydomainrisk.com/api/developer/v1","description":"Production"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"developer"},{"name":"domain-checks"}],"paths":{"/me":{"get":{"operationId":"getDeveloperCapabilities","tags":["developer"],"summary":"Confirm the key, tier, capabilities and enforced limits","responses":{"200":{"description":"The exact capabilities available to this developer key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeveloperMeResponse"},"example":{"api":"developer","version":"v1","scope":"developer_peek","tier":"pro","capabilities":[{"operation_id":"getDeveloperCapabilities","method":"GET","path":"/me"},{"operation_id":"checkDomainSecurity","method":"GET","path":"/peek/security"},{"operation_id":"checkDomainAuthenticity","method":"GET","path":"/peek/authenticity"}],"limits":{"requests_per_minute_per_key":120,"requests_per_minute_per_ip":60,"requests_per_hour_per_subnet":150,"cache_max_age_seconds":1200},"entitlements":{"security_score_advisory":true,"authenticity_verdict":true,"account_data":false,"force_refresh":false,"consumes_account_daily_allowance":false}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/peek/security":{"get":{"operationId":"checkDomainSecurity","tags":["domain-checks"],"summary":"Run a point-in-time security score advisory for a public domain","parameters":[{"name":"domain","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":253},"example":"example.com"}],"responses":{"200":{"description":"Outcome-level security advisory; not a guarantee or certification","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeekSecurityResponse"},"example":{"domain":"example.com","checked_at":"2026-08-17T12:00:00+00:00","score":82,"grade":"B","score_summary":{"bucket":"good","label":"Good"},"findings":[{"title":"DNS: DMARC policy could be stronger","severity":"medium"}],"total_findings":1,"cache":{"hit":false,"max_age_seconds":1200},"scan_time_ms":842}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/InvalidRequest"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/TemporarilyUnavailable"},"504":{"$ref":"#/components/responses/ScanTimeout"}}}},"/peek/authenticity":{"get":{"operationId":"checkDomainAuthenticity","tags":["domain-checks"],"summary":"Run a point-in-time authenticity verdict check for a public domain","parameters":[{"name":"domain","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":253},"example":"example.com"},{"name":"expected_brand","in":"query","required":false,"description":"Optional organisation or brand the domain is expected to represent","schema":{"type":"string","maxLength":200,"pattern":"^[A-Za-z0-9 .\\-&,'()/]+$"},"example":"Example Ltd"}],"responses":{"200":{"description":"Outcome-level authenticity advisory; human review remains appropriate for consequential decisions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeekAuthenticityResponse"},"example":{"domain":"example.com","checked_at":"2026-08-17T12:00:00+00:00","verdict":"genuine","findings":[],"total_findings":0,"factual":{"age_days":10950,"registrar":"Example Registrar","has_tls":true,"resolves":true},"cache":{"hit":false,"max_age_seconds":1200},"scan_time_ms":936}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/InvalidRequest"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/TemporarilyUnavailable"},"504":{"$ref":"#/components/responses/ScanTimeout"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"MDR API key"}},"responses":{"Unauthorized":{"description":"Missing, malformed, unknown or revoked developer API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"The key is valid but its plan or scope is not permitted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"InvalidRequest":{"description":"Invalid domain or request parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"RateLimited":{"description":"A key, IP, subnet or platform limit was reached","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait when available"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"TemporarilyUnavailable":{"description":"A required cache, quota store or provider is temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"ScanTimeout":{"description":"The check could not complete within the request window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"schemas":{"DeveloperCapability":{"type":"object","additionalProperties":false,"required":["operation_id","method","path"],"properties":{"operation_id":{"type":"string"},"method":{"type":"string","enum":["GET"]},"path":{"type":"string"}}},"DeveloperLimits":{"type":"object","additionalProperties":false,"required":["requests_per_minute_per_key","requests_per_minute_per_ip","requests_per_hour_per_subnet","peeks_per_hour_per_ip","cache_max_age_seconds"],"properties":{"requests_per_minute_per_key":{"type":"integer"},"requests_per_minute_per_ip":{"type":"integer"},"requests_per_hour_per_subnet":{"type":"integer"},"cache_max_age_seconds":{"type":"integer"},"peeks_per_hour_per_ip":{"type":"integer","description":"The allowance that actually binds a peek, and the SAME one a visitor gets on the website. A peek with a key is the same work as a peek in the browser and gets the same allowance; the per-minute figures are burst guards on top of it. Exceeding it returns 429 with code rate_limited. Peeks do NOT spend the account's daily check allowance - that allowance is for full scans, which are a different and deeper thing."}}},"DeveloperEntitlements":{"type":"object","additionalProperties":false,"required":["security_score_advisory","authenticity_verdict","account_data","force_refresh","consumes_account_daily_allowance"],"properties":{"security_score_advisory":{"type":"boolean"},"authenticity_verdict":{"type":"boolean"},"account_data":{"type":"boolean"},"force_refresh":{"type":"boolean"},"consumes_account_daily_allowance":{"type":"boolean","description":"false: a peek does not spend the account's daily check allowance, on the website or here. That allowance is for full scans, which are deeper and are a different endpoint."}}},"DeveloperMeResponse":{"type":"object","additionalProperties":false,"required":["api","version","scope","tier","capabilities","limits","entitlements"],"properties":{"api":{"type":"string","const":"developer"},"version":{"type":"string","const":"v1"},"scope":{"type":"string","const":"developer_peek"},"tier":{"type":"string","enum":["free","pro","msp","admin"]},"capabilities":{"type":"array","items":{"$ref":"#/components/schemas/DeveloperCapability"}},"limits":{"$ref":"#/components/schemas/DeveloperLimits"},"entitlements":{"$ref":"#/components/schemas/DeveloperEntitlements"}}},"PeekFinding":{"type":"object","additionalProperties":false,"required":["title","severity"],"properties":{"title":{"type":"string"},"severity":{"type":"string","enum":["critical","high","medium","low","info"]}}},"CacheStatus":{"type":"object","additionalProperties":false,"required":["hit","max_age_seconds"],"properties":{"hit":{"type":"boolean"},"max_age_seconds":{"type":"integer","const":1200}}},"ScoreSummary":{"type":"object","additionalProperties":false,"required":["bucket","label"],"properties":{"bucket":{"type":"string","enum":["strong","goodWithGaps","needsImprovement","atRisk","critical"],"description":"Score band, best to worst. Branch on this rather than comparing score against your own thresholds: the bucket is part of this contract and a threshold you choose yourself is not."},"label":{"type":"string","description":"Plain-English wording for the same bucket. Display text only; do not parse it."}}},"PeekSecurityResponse":{"type":"object","additionalProperties":false,"required":["domain","checked_at","score","grade","score_summary","findings","total_findings","coverage_note","cache","scan_time_ms"],"properties":{"domain":{"type":"string"},"checked_at":{"type":["string","null"],"format":"date-time"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Security score advisory, 0-100. Higher is better: 0 is the worst result and 100 the best."},"grade":{"type":"string","enum":["A","B","C","D","F"],"description":"Display grade derived from the score, best to worst. Presentation text - branch on score_summary.bucket instead."},"score_summary":{"$ref":"#/components/schemas/ScoreSummary"},"findings":{"type":"array","items":{"$ref":"#/components/schemas/PeekFinding"}},"total_findings":{"type":"integer","minimum":0},"cache":{"$ref":"#/components/schemas/CacheStatus"},"scan_time_ms":{"type":"integer","minimum":0},"coverage_note":{"type":["string","null"],"description":"The same caveat a website visitor is shown. This check reads the free exposure index, so the score is a FLOOR - a full scan can return the same score or a higher one, never a lower one. null when there is nothing to caveat."}}},"AuthenticityFacts":{"type":"object","additionalProperties":false,"required":["age_days","registrar","has_tls","resolves"],"properties":{"age_days":{"type":["integer","null"]},"registrar":{"type":["string","null"]},"has_tls":{"type":"boolean"},"resolves":{"type":"boolean"}}},"PeekAuthenticityResponse":{"type":"object","additionalProperties":false,"required":["domain","checked_at","verdict","findings","total_findings","factual","homepage","enrichment_status","cache","scan_time_ms"],"properties":{"domain":{"type":"string"},"checked_at":{"type":["string","null"],"format":"date-time"},"verdict":{"type":"string","enum":["genuine","probablyLegit","uncertain","suspicious","malicious"],"description":"Authenticity verdict, best to worst. No other value is returned. probablyLegit is a POSITIVE verdict - treating it as a failure or an unknown is the most common integration mistake."},"findings":{"type":"array","items":{"$ref":"#/components/schemas/PeekFinding"}},"total_findings":{"type":"integer","minimum":0},"factual":{"$ref":"#/components/schemas/AuthenticityFacts"},"cache":{"$ref":"#/components/schemas/CacheStatus"},"scan_time_ms":{"type":"integer","minimum":0},"homepage":{"$ref":"#/components/schemas/HomepageReview"},"enrichment_status":{"type":"string","enum":["pending","completed","not_applicable"],"description":"How to read the verdict. \"pending\" means PROVISIONAL: two slower signals are still running and nothing will be pushed to you when they finish. The enriched verdict overwrites the shared cache, so it reaches whoever asks next - to finish a pending verdict, request the same domain again shortly and the enriched answer is served from cache. This is exactly how the website behaves; a visitor is not notified either. \"completed\" means the slow signals ran and this verdict includes them. \"not_applicable\" means no slow signal was needed: the verdict is final as it stands and asking again will not change it."}}},"ErrorDetail":{"type":"object","additionalProperties":false,"required":["location","message","type"],"properties":{"location":{"type":"string"},"message":{"type":"string"},"type":{"type":"string"}}},"ErrorResponse":{"type":"object","additionalProperties":false,"required":["error"],"properties":{"error":{"type":"object","additionalProperties":false,"required":["code","message"],"properties":{"code":{"type":"string","description":"Stable machine-readable failure reason. Branch on this, not on message. Codes worth handling individually: invalid_api_key (401), forbidden (403), invalid_domain (422), rate_limited (429, per-minute throttle - retry shortly), daily_limit_reached (429, this key's day is spent - retry after the reset), temporarily_unavailable (503), scan_timeout (504), request_failed (fallback, any status). Deliberately NOT an enum: recognise these and fall back on the HTTP status for anything else, so a new code cannot fail validation in your CI."},"message":{"type":"string"},"retry_after_seconds":{"type":"integer","description":"Present on 429. Mirrors the Retry-After header."},"limit":{"type":"integer"},"details":{"type":"array","items":{"$ref":"#/components/schemas/ErrorDetail"}}}}}},"HomepageReview":{"type":"object","additionalProperties":false,"required":["attempted","reviewed","blocked_by"],"description":"Whether the homepage was actually read. Without it, \"nothing found\" and \"the page could not be fetched\" are the same answer.","properties":{"attempted":{"type":"boolean"},"reviewed":{"type":"boolean"},"blocked_by":{"type":["string","null"],"description":"The protection service that blocked the fetch, when one did. null means nothing blocked it, NOT that nothing was tried."}}}}}}