{"components":{"parameters":{"AuditId":{"in":"path","name":"auditId","required":true,"schema":{"format":"uuid","type":"string"}},"AuditStatusFilter":{"description":"Applied before pagination. active selects every non-terminal audit state.","in":"query","name":"status","schema":{"enum":["active","created","queued","discovering","capturing","evaluating","finalising_evidence","timestamping","completed","failed","cancelled","partial","quota_blocked"],"type":"string"}},"DeliveryCursor":{"description":"Last delivery ID from the preceding page, exclusive. Omit on page one.","in":"query","name":"cursor","schema":{"maxLength":36,"type":"string"}},"DeliveryLimit":{"description":"Global default 50; endpoint default 200.","in":"query","name":"limit","schema":{"maximum":200,"minimum":1,"type":"integer"}},"DeliveryStatus":{"description":"Pending selects queued with no attempts; retrying includes queued after an attempt; queued selects all queued records.","in":"query","name":"status","schema":{"enum":["pending","queued","retrying","delivering","delivered","failed"],"type":"string"}},"DestinationId":{"in":"path","name":"destinationId","required":true,"schema":{"format":"uuid","type":"string"}},"EndpointId":{"in":"path","name":"endpointId","required":true,"schema":{"format":"uuid","type":"string"}},"HistoryCursor":{"description":"Opaque next_cursor from the preceding page. Sort order is immutable created_at descending, then ID descending. This is a continuation boundary, not a frozen snapshot; newer inserts do not shift older pages, and removed records disappear.","in":"query","name":"cursor","schema":{"maxLength":256,"type":"string"}},"HistoryLimit":{"description":"Bounded page size. The existing default of 200 is preserved; the portal requests 50.","in":"query","name":"limit","schema":{"default":200,"maximum":200,"minimum":1,"type":"integer"}},"IdempotencyKey":{"description":"Makes queueing an audit safe to retry. Surrounding whitespace is ignored. A key is remembered per organisation, across both operations that queue an audit, for as long as the audit it queued exists. A request under a key already used returns that audit with 200 instead of queueing another. The request is not compared with the first, so there is no conflict response.","in":"header","name":"Idempotency-Key","required":false,"schema":{"maxLength":255,"type":"string"}},"MonitorId":{"in":"path","name":"monitorId","required":true,"schema":{"format":"uuid","type":"string"}},"SiteId":{"in":"path","name":"siteId","required":true,"schema":{"format":"uuid","type":"string"}}},"responses":{"Problem":{"content":{"application/json":{"schema":{"properties":{"error":{"properties":{"code":{"description":"Stable machine-readable code. Billing refusals use the codes every Ironfang product shares (BillingCode), with the same status and message whichever product answered; everything else is Ironfang Audit's own.","type":"string"},"docs":{"description":"Where the errors are documented.","example":"https://ironfang.com/audit/docs#errors","format":"uri","type":"string"},"message":{"type":"string"},"meter":{"description":"Billing refusals only: the meter that refused, audit.capture or audit.retention_extension.","type":"string"},"product":{"description":"Billing refusals only: the product the refused work belongs to (audit).","type":"string"},"reset_at":{"description":"Billing refusals only: when the free allowance renews (the next month's start, UTC).","format":"date-time","type":"string"}},"required":["code","message"],"type":"object"},"request_id":{"description":"The X-Ironfang-Request-ID of the failed request.","type":"string"}},"type":"object"}}},"description":"Problem response"}},"schemas":{"Artifact":{"properties":{"audit_id":{"type":"string"},"byte_length":{"type":"integer"},"created_at":{"format":"date-time","type":"string"},"derived_from_sha256":{"type":"string"},"expires_at":{"format":"date-time","type":"string"},"hosted_state":{"enum":["available","expiring","deleted"],"type":"string"},"id":{"type":"string"},"media_type":{"type":"string"},"name":{"type":"string"},"object_key":{"type":"string"},"organisation_id":{"type":"string"},"page_observation_id":{"type":"string"},"sha256":{"type":"string"},"transformation":{"type":"string"},"type":{"type":"string"}},"required":["id","organisation_id","audit_id","type","name","media_type","byte_length","sha256","object_key","created_at"],"type":"object"},"Audit":{"additionalProperties":false,"properties":{"capture_profile_snapshot":{"$ref":"#/components/schemas/CaptureProfile"},"captured_page_count":{"minimum":0,"type":"integer"},"change_policy_snapshot":{"$ref":"#/components/schemas/ChangePolicy"},"completed_at":{"format":"date-time","type":"string"},"compliance_state":{"enum":["PASS","WARNING","FAIL","ERROR"],"type":"string"},"created_at":{"format":"date-time","type":"string"},"credits_consumed":{"description":"Page captures this audit was charged against audit.capture: one per finalised page evidence record, settled when it finalised. Zero while running, when not metered, or when its admission had expired. The name is kept from the launch-era credits.","minimum":0,"type":"integer"},"discovered_page_count":{"minimum":0,"type":"integer"},"error":{"type":"string"},"error_page_count":{"minimum":0,"type":"integer"},"evidence_expires_at":{"format":"date-time","type":"string"},"evidence_hosted":{"type":"boolean"},"failed_page_count":{"minimum":0,"type":"integer"},"id":{"format":"uuid","type":"string"},"manifest_sha256":{"type":"string"},"monitor_id":{"format":"uuid","type":"string"},"organisation_id":{"format":"uuid","type":"string"},"page_bound":{"description":"The most pages this audit may capture: what was reserved against audit.capture when it was admitted. Absent on audits from before admission.","minimum":1,"type":"integer"},"passed_page_count":{"minimum":0,"type":"integer"},"reason":{"type":"string"},"retention_days":{"description":"The hosted retention promised when the audit was created: the rate catalogue's standard retention for audit.capture (a retired plan's, on older audits). A retention extension moves evidence_expires_at later; it never shortens it.","minimum":0,"type":"integer"},"root_hash":{"type":"string"},"scheduled_for":{"format":"date-time","type":"string"},"scope_snapshot":{"$ref":"#/components/schemas/ScopeSnapshot"},"signing_key_id":{"type":"string"},"site_id":{"format":"uuid","type":"string"},"site_name":{"description":"The audited site's name, resolved with the audit so a list need not read every site to label its rows.","type":"string"},"started_at":{"format":"date-time","type":"string"},"status":{"$ref":"#/components/schemas/AuditStatus"},"trigger_type":{"enum":["manual","api","schedule"],"type":"string"},"warning_page_count":{"minimum":0,"type":"integer"}},"required":["id","organisation_id","site_id","site_name","trigger_type","status","created_at","discovered_page_count","captured_page_count","passed_page_count","warning_page_count","failed_page_count","error_page_count","credits_consumed","evidence_hosted"],"type":"object"},"AuditList":{"additionalProperties":false,"properties":{"audits":{"items":{"$ref":"#/components/schemas/Audit"},"maxItems":200,"type":"array"},"next_cursor":{"description":"Opaque continuation boundary. Absent on the final page; no total is inferred from a page.","maxLength":256,"type":"string"}},"required":["audits"],"type":"object"},"AuditPageList":{"properties":{"next_cursor":{"description":"Opaque timestamp/ID boundary; absent on the final page and for legacy unpaged requests.","type":"string"},"pages":{"items":{"$ref":"#/components/schemas/PageObservation"},"type":"array"},"total":{"description":"Unfiltered observation count for this audit; not the matching count.","minimum":0,"type":"integer"}},"required":["pages","total"],"type":"object"},"AuditStatus":{"description":"quota_blocked is historical: audits the retired plans could not pay for ended there. Audits are now admitted before they are accepted, so none enters it.","enum":["created","queued","discovering","capturing","evaluating","finalising_evidence","timestamping","completed","failed","cancelled","partial","quota_blocked"],"type":"string"},"BillingCode":{"description":"The billing refusals, shared by every Ironfang product: nothing was reserved and nothing was charged. Status: 429 free_allowance_exhausted, account_budget_exhausted, product_budget_exhausted, exemption_limit_reached; 402 paid_usage_paused, payment_required, payment_action_required, account_restricted; 403 product_not_eligible; 410 account_closed; 413 operation_too_large; 503 billing_temporarily_unavailable (with Retry-After); 409 idempotency_conflict. quote_expired (409) is Ironfang Audit's answer for a retention quote that can no longer be bought.","enum":["free_allowance_exhausted","account_budget_exhausted","product_budget_exhausted","paid_usage_paused","payment_required","payment_action_required","account_restricted","account_closed","product_not_eligible","exemption_limit_reached","operation_too_large","billing_temporarily_unavailable","idempotency_conflict","quote_expired"],"type":"string"},"CaptureMetadata":{"properties":{"browser":{"type":"string"},"browser_version":{"type":"string"},"build_id":{"type":"string"},"capture_truncated":{"type":"boolean"},"colour_scheme":{"type":"string"},"device_scale_factor":{"type":"number"},"dns_answers":{"items":{"type":"string"},"type":["array","null"]},"duration_ms":{"type":"integer"},"locale":{"type":"string"},"region":{"type":"string"},"remote_port":{"type":"integer"},"response_headers":{"additionalProperties":{"type":"string"},"type":["object","null"]},"settle_ms":{"type":"integer"},"settle_outcome":{"type":"string"},"timezone":{"type":"string"},"tls_certificate_sha256":{"type":"string"},"tls_cipher":{"type":"string"},"tls_issuer":{"type":"string"},"tls_protocol":{"type":"string"},"tls_subject":{"type":"string"},"tls_valid_from":{"type":"string"},"tls_valid_to":{"type":"string"},"transformations":{"items":{"type":"string"},"type":["array","null"]},"user_agent":{"type":"string"},"viewport_height":{"type":"integer"},"viewport_width":{"type":"integer"},"worker_id":{"type":"string"}},"required":["browser","browser_version","build_id","user_agent","viewport_width","viewport_height","device_scale_factor","locale","timezone","colour_scheme","settle_outcome","settle_ms","duration_ms","worker_id","region","transformations","response_headers","dns_answers","capture_truncated"],"type":"object"},"CaptureProfile":{"description":"Viewport and rendering settings for the monitor's evidence captures. Clamped to Ironfang Render's ceilings.","properties":{"color_scheme":{"enum":["light","dark"],"type":"string"},"device_scale_factor":{"type":"number"},"height":{"type":"integer"},"locale":{"type":"string"},"width":{"type":"integer"}},"type":"object"},"ChangePolicy":{"description":"When a page observation counts as changed.","properties":{"ignore_numbers":{"type":"boolean"},"ignored_selectors":{"items":{"type":"string"},"type":"array"},"min_text_change":{"description":"Fraction of text that must differ, 0-1.","type":"number"},"min_visual_change":{"description":"Fraction of pixels that must differ, 0-1.","type":"number"},"notify_only_on_rule_change":{"type":"boolean"}},"type":"object"},"CrawlPolicy":{"additionalProperties":false,"properties":{"allowed_hosts":{"items":{"type":"string"},"type":["array","null"]},"exclude_patterns":{"items":{"type":"string"},"type":["array","null"]},"identity_parameters":{"items":{"type":"string"},"type":["array","null"]},"ignored_parameters":{"items":{"type":"string"},"type":["array","null"]},"include_patterns":{"items":{"type":"string"},"type":["array","null"]},"max_depth":{"minimum":0,"type":"integer"},"max_pages":{"minimum":0,"type":"integer"},"max_response_bytes":{"minimum":0,"type":"integer"},"requests_per_second":{"minimum":0,"type":"integer"},"respect_robots":{"type":"boolean"}},"required":["allowed_hosts","include_patterns","exclude_patterns","ignored_parameters","identity_parameters","respect_robots","max_pages","max_depth","requests_per_second","max_response_bytes"],"type":"object"},"ExportJob":{"additionalProperties":false,"properties":{"attempt":{"minimum":0,"type":"integer"},"audit_id":{"format":"uuid","type":"string"},"completed_at":{"format":"date-time","type":"string"},"created_at":{"format":"date-time","type":"string"},"destination_id":{"format":"uuid","type":"string"},"id":{"format":"uuid","type":"string"},"last_error":{"type":"string"},"next_attempt_at":{"format":"date-time","type":"string"},"object_prefix":{"type":"string"},"organisation_id":{"format":"uuid","type":"string"},"status":{"enum":["queued","exporting","retrying","completed","failed"],"type":"string"}},"required":["id","organisation_id","audit_id","destination_id","status","attempt","next_attempt_at","created_at"],"type":"object"},"ExportList":{"additionalProperties":false,"properties":{"exports":{"items":{"$ref":"#/components/schemas/ExportJob"},"maxItems":200,"type":"array"},"next_cursor":{"description":"Opaque continuation boundary. Absent on the final page; no total is inferred from a page.","maxLength":256,"type":"string"}},"required":["exports"],"type":"object"},"Finding":{"additionalProperties":false,"properties":{"accepted_risk_until":{"format":"date-time","type":"string"},"assignee_user_id":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"due_at":{"format":"date-time","type":"string"},"first_audit_id":{"format":"uuid","type":"string"},"first_observation_id":{"format":"uuid","type":"string"},"first_seen_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"last_seen_at":{"format":"date-time","type":"string"},"latest_audit_id":{"format":"uuid","type":"string"},"latest_observation_id":{"format":"uuid","type":"string"},"organisation_id":{"format":"uuid","type":"string"},"page_id":{"format":"uuid","type":"string"},"page_url":{"type":"string"},"resolution_code":{"type":"string"},"resolved_at":{"format":"date-time","type":"string"},"resolved_observation_id":{"format":"uuid","type":"string"},"rule_lineage_id":{"format":"uuid","type":"string"},"rule_name":{"type":"string"},"severity":{"enum":["info","warning","failure","critical"],"type":"string"},"site_id":{"format":"uuid","type":"string"},"site_name":{"type":"string"},"state":{"enum":["open","acknowledged","accepted_risk","resolved","reopened"],"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","organisation_id","site_id","page_id","rule_lineage_id","state","severity","first_observation_id","latest_observation_id","first_seen_at","last_seen_at","created_at","updated_at"],"type":"object"},"FindingEvent":{"additionalProperties":false,"properties":{"actor":{"type":"string"},"actor_type":{"type":"string"},"comment":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"event_type":{"type":"string"},"finding_id":{"format":"uuid","type":"string"},"from_state":{"type":"string"},"id":{"format":"uuid","type":"string"},"observation_id":{"type":"string"},"to_state":{"type":"string"}},"required":["id","finding_id","event_type","actor_type","created_at"],"type":"object"},"FindingEventList":{"additionalProperties":false,"properties":{"events":{"items":{"$ref":"#/components/schemas/FindingEvent"},"maxItems":200,"type":"array"},"next_cursor":{"description":"Opaque continuation boundary. Absent on the final page; no total is inferred from a page.","maxLength":256,"type":"string"}},"required":["events"],"type":"object"},"FindingList":{"additionalProperties":false,"properties":{"findings":{"items":{"$ref":"#/components/schemas/Finding"},"type":"array"},"next_cursor":{"type":"string"}},"required":["findings"],"type":"object"},"LastSiteAudit":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"status":{"$ref":"#/components/schemas/AuditStatus"}},"required":["id","status","created_at"],"type":"object"},"MerkleStep":{"properties":{"hash":{"type":"string"},"side":{"enum":["left","right"],"type":"string"}},"required":["side","hash"],"type":"object"},"MeterUsage":{"properties":{"charge":{"description":"The month's charge for the meter so far, exact, before tax, in pounds.","type":"string"},"charge_minor":{"description":"The charge rounded to pence.","type":"integer"},"currency":{"const":"GBP","type":"string"},"exempt":{"type":"boolean"},"free_allowance":{"minimum":0,"type":"integer"},"free_remaining":{"minimum":0,"type":"integer"},"meter":{"type":"string"},"reserved":{"description":"Held by admitted work that has not finished.","minimum":0,"type":"integer"},"unit":{"type":"string"},"used":{"description":"Settled this month.","minimum":0,"type":"integer"}},"required":["meter","unit","free_allowance","used","reserved","free_remaining","charge","charge_minor","currency","exempt"],"type":"object"},"Monitor":{"additionalProperties":true,"properties":{"anchor_minute":{"type":"integer"},"cadence":{"enum":["manual","daily","six_hourly","hourly"],"type":"string"},"capture_profile":{"$ref":"#/components/schemas/CaptureProfile"},"captures_per_month":{"description":"Page captures the monitor may ask audit.capture for over the current month if every run captures every page: an upper bound, not a charge. Zero unless the monitor is active and scheduled, matching the usage projection.","minimum":0,"type":"integer"},"change_policy":{"$ref":"#/components/schemas/ChangePolicy"},"created_at":{"format":"date-time","type":"string"},"created_by":{"type":"string"},"id":{"format":"uuid","type":"string"},"last_scheduled_at":{"format":"date-time","type":"string"},"mode":{"enum":["full_site","url_set"],"type":"string"},"name":{"type":"string"},"next_run_at":{"format":"date-time","type":"string"},"organisation_id":{"format":"uuid","type":"string"},"pages":{"description":"Pages one run may capture: the enabled URLs, or the site's page limit, within the per-audit cap.","minimum":1,"type":"integer"},"site_id":{"format":"uuid","type":"string"},"site_name":{"description":"The monitored site's name, resolved with the monitor rather than per row by the client.","type":"string"},"status":{"enum":["active","paused","archived"],"type":"string"},"timezone":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"url_count":{"description":"Enabled URLs in a url_set monitor; zero for a full_site monitor.","minimum":0,"type":"integer"}},"required":["id","organisation_id","site_id","name","mode","status","cadence","created_at","updated_at","site_name","url_count","pages","captures_per_month"],"type":"object"},"MonitorInput":{"description":"`mode` is `full_site` (crawler discovery under the site's crawl policy) or `url_set` (exactly the given `urls`). Every `cadence` is open to every organisation; `manual` monitors only run when asked. `anchor_minute` is minutes past midnight in `timezone` for daily cadences, or minutes into the interval otherwise.\n","properties":{"anchor_minute":{"maximum":1439,"minimum":0,"type":"integer"},"cadence":{"enum":["manual","daily","six_hourly","hourly"],"type":"string"},"capture_profile":{"$ref":"#/components/schemas/CaptureProfile"},"change_policy":{"$ref":"#/components/schemas/ChangePolicy"},"mode":{"enum":["full_site","url_set"],"type":"string"},"name":{"type":"string"},"timezone":{"description":"An IANA zone name, for example Europe/London.","type":"string"},"urls":{"description":"Required for url_set on creation; ignored on update.","items":{"format":"uri","type":"string"},"type":"array"}},"required":["mode"],"type":"object"},"MonitorList":{"additionalProperties":false,"properties":{"monitors":{"items":{"$ref":"#/components/schemas/Monitor"},"maxItems":200,"type":"array"},"next_cursor":{"description":"Opaque continuation boundary. Absent on the final page; no total is inferred from a page.","maxLength":256,"type":"string"}},"required":["monitors"],"type":"object"},"Notification":{"additionalProperties":false,"properties":{"attempt":{"minimum":0,"type":"integer"},"created_at":{"format":"date-time","type":"string"},"event_id":{"format":"uuid","type":"string"},"event_type":{"type":"string"},"id":{"format":"uuid","type":"string"},"last_error":{"type":"string"},"recipient":{"type":"string"},"sent_at":{"format":"date-time","type":"string"},"status":{"enum":["queued","held","sending","sent","failed"],"type":"string"}},"required":["id","event_id","event_type","recipient","status","attempt","created_at"],"type":"object"},"NotificationList":{"additionalProperties":false,"properties":{"next_cursor":{"description":"Opaque continuation boundary. Absent on the final page; no total is inferred from a page.","maxLength":256,"type":"string"},"notifications":{"items":{"$ref":"#/components/schemas/Notification"},"maxItems":200,"type":"array"}},"required":["notifications"],"type":"object"},"ObservationList":{"properties":{"next_cursor":{"description":"Opaque timestamp/ID boundary; empty on the final page.","type":"string"},"observations":{"items":{"$ref":"#/components/schemas/PageObservation"},"type":"array"}},"required":["observations","next_cursor"],"type":"object"},"PageObservation":{"properties":{"artifacts":{"items":{"$ref":"#/components/schemas/Artifact"},"type":"array"},"audit_id":{"type":"string"},"canonical_url":{"type":"string"},"capture_status":{"enum":["captured","error"],"type":"string"},"captured_at":{"format":"date-time","type":"string"},"change_classification":{"enum":["NEW","MOVED","CHANGED","UNCHANGED"],"type":"string"},"compliance_state":{"enum":["PASS","WARNING","FAIL","ERROR"],"type":"string"},"created_at":{"format":"date-time","type":"string"},"error":{"type":"string"},"final_url":{"type":"string"},"http_status":{"type":"integer"},"id":{"type":"string"},"merkle_proof":{"items":{"$ref":"#/components/schemas/MerkleStep"},"type":"array"},"metadata":{"$ref":"#/components/schemas/CaptureMetadata"},"observed_remote_ip":{"type":"string"},"page_hash":{"type":"string"},"page_id":{"type":"string"},"previous_observation_id":{"type":"string"},"redirect_chain":{"items":{"$ref":"#/components/schemas/Redirect"},"type":["array","null"]},"requested_url":{"type":"string"},"rule_results":{"items":{"$ref":"#/components/schemas/RuleResult"},"type":"array"},"text_changed":{"type":"boolean"},"textual_change_score":{"type":"number"},"title":{"type":"string"},"visual_change_score":{"type":"number"},"visual_changed":{"type":"boolean"}},"required":["id","audit_id","page_id","requested_url","redirect_chain","capture_status","text_changed","visual_changed","textual_change_score","visual_change_score","metadata","created_at"],"type":"object"},"Redirect":{"properties":{"status_code":{"type":"integer"},"url":{"type":"string"}},"required":["url","status_code"],"type":"object"},"RetentionQuote":{"properties":{"amount":{"description":"The fixed price, exact, before tax, in pounds.","type":"string"},"amount_minor":{"description":"The price rounded to pence.","type":"integer"},"artifacts":{"minimum":0,"type":"integer"},"audit_ids":{"items":{"format":"uuid","type":"string"},"type":"array"},"bytes":{"description":"Hosted bytes the quote covers.","minimum":1,"type":"integer"},"catalogue_version":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"currency":{"const":"GBP","type":"string"},"deadlines":{"items":{"properties":{"artifact_id":{"format":"uuid","type":"string"},"audit_id":{"format":"uuid","type":"string"},"byte_length":{"minimum":0,"type":"integer"},"expires_after":{"format":"date-time","type":"string"},"expires_before":{"format":"date-time","type":"string"}},"required":["artifact_id","audit_id","byte_length","expires_before","expires_after"],"type":"object"},"type":"array"},"expires_at":{"description":"The quote can be bought until then.","format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"meter":{"const":"audit.retention_extension","type":"string"},"months":{"maximum":120,"minimum":1,"type":"integer"},"prices_exclude_tax":{"const":true,"type":"boolean"},"purchased_at":{"format":"date-time","type":"string"},"quantity":{"description":"In the meter's unit (unit): the bytes rounded up to the unit's share of a decimal gigabyte, times months, at least ten GB-months.","minimum":1,"type":"integer"},"status":{"enum":["open","expired","purchased"],"type":"string"},"unit":{"description":"The meter's unit, for example 0.1 GB-month.","type":"string"}},"required":["id","status","meter","months","bytes","quantity","unit","amount","amount_minor","currency","prices_exclude_tax","catalogue_version","created_at","expires_at","audit_ids","artifacts"],"type":"object"},"RuleResult":{"properties":{"audit_id":{"type":"string"},"evaluated_at":{"format":"date-time","type":"string"},"evidence_excerpt":{"type":"string"},"expected":{"type":"string"},"id":{"type":"string"},"observed":{"type":"string"},"page_observation_id":{"type":"string"},"rule_id":{"description":"The exact rule revision this result was evaluated against.","type":"string"},"rule_lineage_id":{"description":"The rule across every revision of it. Match a finding on this rather than reading a rule's whole revision history to see whether a result is its own.","type":"string"},"rule_name":{"type":"string"},"rule_version":{"type":"integer"},"severity":{"enum":["info","warning","failure","critical"],"type":"string"},"status":{"enum":["pass","fail","warning","not_applicable","error"],"type":"string"}},"required":["id","audit_id","page_observation_id","rule_id","rule_lineage_id","rule_version","rule_name","severity","status","evaluated_at"],"type":"object"},"RuleRevision":{"additionalProperties":true,"properties":{"configuration":{},"created_at":{"format":"date-time","type":"string"},"created_by":{"type":"string"},"description":{"type":"string"},"enabled":{"type":"boolean"},"id":{"format":"uuid","type":"string"},"lineage_id":{"format":"uuid","type":"string"},"name":{"type":"string"},"organisation_id":{"format":"uuid","type":"string"},"origin_pack_installation_id":{"type":"string"},"origin_pack_rule_key":{"type":"string"},"retired_at":{"format":"date-time","type":"string"},"rule_type":{"type":"string"},"severity":{"enum":["info","warning","failure","critical"],"type":"string"},"site_id":{"type":"string"},"supersedes_rule_id":{"type":"string"},"version":{"minimum":1,"type":"integer"}},"required":["id","lineage_id","organisation_id","name","rule_type","severity","configuration","version","created_at"],"type":"object"},"RuleRevisionList":{"additionalProperties":false,"properties":{"next_cursor":{"description":"The version to continue below. Absent on the final page.","maxLength":16,"type":"string"},"revisions":{"items":{"$ref":"#/components/schemas/RuleRevision"},"maxItems":200,"type":"array"}},"required":["revisions"],"type":"object"},"ScopeSnapshot":{"additionalProperties":false,"properties":{"crawl_policy":{"$ref":"#/components/schemas/CrawlPolicy"},"mode":{"enum":["full_site","url_set"],"type":"string"},"urls":{"items":{"type":"string"},"type":"array"}},"required":["mode"],"type":"object"},"Site":{"additionalProperties":false,"properties":{"activity":{"$ref":"#/components/schemas/SiteActivity"},"crawl_policy":{"$ref":"#/components/schemas/CrawlPolicy"},"created_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"organisation_id":{"format":"uuid","type":"string"},"primary_url":{"type":"string"},"status":{"type":"string"},"timezone":{"type":"string"}},"required":["id","organisation_id","name","primary_url","status","timezone","crawl_policy","created_at","activity"],"type":"object"},"SiteActivity":{"additionalProperties":false,"properties":{"audits":{"minimum":0,"type":"integer"},"has_capture":{"type":"boolean"},"last_audit":{"oneOf":[{"$ref":"#/components/schemas/LastSiteAudit"},{"type":"null"}]},"open_findings":{"minimum":0,"type":"integer"}},"required":["audits","has_capture","open_findings","last_audit"],"type":"object"},"SiteList":{"additionalProperties":false,"properties":{"sites":{"items":{"$ref":"#/components/schemas/Site"},"type":"array"}},"required":["sites"],"type":"object"},"Usage":{"additionalProperties":true,"properties":{"audits":{"minimum":0,"type":"integer"},"billing_enabled":{"description":"False where unified billing is not running; nothing is metered then.","type":"boolean"},"billing_mode":{"description":"The billing account's mode: free, setup_pending, enabled, paused or closed.","type":"string"},"captures":{"oneOf":[{"$ref":"#/components/schemas/MeterUsage"},{"type":"null"}]},"catalogue_version":{"description":"The rate catalogue version pricing the month.","type":"string"},"credits_consumed":{"description":"Page captures every audit of the organisation has been charged (and launch-era credits).","minimum":0,"type":"integer"},"daily":{"items":{"properties":{"captures":{"minimum":0,"type":"integer"},"day":{"format":"date","type":"string"}},"required":["day","captures"],"type":"object"},"type":"array"},"earliest_expiry":{"format":"date-time","type":"string"},"max_pages_per_audit":{"description":"The per-audit page cap; each site's own page limit applies within it.","minimum":1,"type":"integer"},"period_end":{"format":"date-time","type":"string"},"period_start":{"format":"date-time","type":"string"},"projection":{"properties":{"captures":{"minimum":0,"type":"integer"},"monitors":{"items":{"type":"object"},"type":"array"}},"required":["captures","monitors"],"type":"object"},"quota":{"deprecated":true,"description":"The launch-era quota shape, kept for existing readers - captures against the month's free allowance only. Use captures.","type":"object"},"retained_bytes":{"minimum":0,"type":"integer"},"retention_extension":{"oneOf":[{"$ref":"#/components/schemas/MeterUsage"},{"type":"null"}]},"retention_purchases":{"description":"Retention extensions bought this month, newest first, without their deadlines.","items":{"$ref":"#/components/schemas/RetentionQuote"},"type":"array"},"site_limit":{"description":"The site capacity limit; the same for every organisation.","minimum":1,"type":"integer"},"sites":{"minimum":0,"type":"integer"},"standard_retention_days":{"description":"The hosted retention a new audit is promised.","minimum":1,"type":"integer"}},"required":["period_start","period_end","billing_enabled","captures","retention_extension","retention_purchases","sites","site_limit","max_pages_per_audit","audits","standard_retention_days","retained_bytes","projection","daily","quota"],"type":"object"},"WebhookDelivery":{"additionalProperties":false,"properties":{"attempt":{"minimum":0,"type":"integer"},"delivered_at":{"format":"date-time","type":"string"},"endpoint_id":{"format":"uuid","type":"string"},"event_id":{"format":"uuid","type":"string"},"event_type":{"type":"string"},"id":{"format":"uuid","type":"string"},"last_error":{"type":"string"},"next_attempt_at":{"format":"date-time","type":"string"},"payload":{"additionalProperties":true,"type":"object"},"response_excerpt":{"type":"string"},"response_status":{"type":"integer"},"status":{"enum":["queued","retrying","delivering","delivered","failed"],"type":"string"}},"required":["id","endpoint_id","event_id","event_type","status","attempt","next_attempt_at"],"type":"object"},"WebhookDeliveryList":{"additionalProperties":false,"properties":{"deliveries":{"items":{"$ref":"#/components/schemas/WebhookDelivery"},"maxItems":200,"type":"array"},"next_cursor":{"format":"uuid","type":"string"}},"required":["deliveries"],"type":"object"}},"securitySchemes":{"apiKey":{"bearerFormat":"if_live_ platform API key","description":"A platform API key minted in the portal (Warden). Its scopes are its\npermissions - `audit:read`, `audit:run`, `audit:manage`,\n`audit:evidence`, `audit:integrations`, or `audit:*` - and\nit acts only in the organisation it was minted for. Legacy Ironfang Render\nkeys carry no Ironfang Audit scopes and are refused with 403.\n","scheme":"bearer","type":"http"},"bearerAuth":{"bearerFormat":"Warden JWT","description":"A Warden user token. Each route also requires a tenant permission\nin the acting tenant, resolved live from Warden: read routes need\n`audit.read`; creating sites and rules `audit.manage`; queueing\nan audit `audit.run`; evidence bundles and raw artifacts\n`audit.evidence`; webhooks, export destinations and manual exports\n`audit.integrations`; billing changes `billing.manage`. A missing permission is 403; a permission\nlookup failure is 503.\n","scheme":"bearer","type":"http"}}},"externalDocs":{"description":"Ironfang Audit documentation","url":"https://ironfang.com/audit/docs"},"info":{"contact":{"email":"hello@ironfang.com","name":"Ironfang","url":"https://ironfang.com/contact"},"description":"Asynchronous website audits with signed, independently timestamped evidence.\nSites, rules and rule packs, audits, findings, evidence bundles, exports,\nwebhooks and usage - authenticated with an Ironfang platform API key\ncarrying Ironfang Audit scopes. Base URL https://api.ironfang.com/audit.\n\nFor agents and tools: the canonical base URL is\nhttps://api.ironfang.com/audit and every operation has a stable\n`operationId`. Every error an operation returns is JSON of the shape\n`{\"error\": {\"code\", \"message\", \"docs\"}, \"request_id\"}`: branch on `code`\nand the HTTP status, never on `message`. The machine-readable index of the\nIronfang APIs is https://ironfang.com/.well-known/api-catalog, and\nhttps://ironfang.com/llms.txt is a plain-text map of them for a language\nmodel. Ironfang Audit is also available as MCP tools at\nhttps://mcp.ironfang.com/mcp (see https://ironfang.com/docs/mcp). Auditwolf\nis the name the product launched under, not a separate product; its\n`/auditwolf` prefix and `Auditwolf-*` webhook headers were retired on\n28 September 2026, and the prefix now answers `410`.\n\nBilling: Ironfang Audit has no plans of its own. It is billed through the\norganisation's Ironfang billing account (https://api.ironfang.com/billing/v1)\non two meters: `audit.capture`, one finalised page evidence record, and\n`audit.retention_extension`, extended hosted retention bought as a quoted,\nfixed-price operation. Every audit is admitted before it is accepted: the\nmost pages it can capture are reserved against `audit.capture`, a refusal\nis answered at once with one of the shared billing codes (see the Problem\nresponse), and a finished audit is charged only for the page evidence it\nfinalised. Capacity limits - sites per organisation, pages per audit,\ncrawl rate and depth - are the same for everyone and no payment lifts\nthem. The retired `/v1/plans` and `/v1/billing` routes answer `410\nbilling_moved`.\n","license":{"name":"Proprietary - use governed by the Ironfang terms of service","url":"https://ironfang.com/legal/terms"},"summary":"Website audits with findings and signed, independently timestamped evidence.","termsOfService":"https://ironfang.com/legal/terms","title":"Ironfang Audit API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/v1/artifacts/{artifactId}":{"get":{"description":"Streams an artifact owned by the authenticated organisation using its stored media type. Expired hosted artifacts return evidence_expired rather than empty content.","operationId":"getArtifactsByArtifactId","parameters":[{"in":"path","name":"artifactId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"Artifact bytes"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"Stream an authenticated raw artifact","tags":["Artifacts"]}},"/v1/audits":{"get":{"description":"Returns audit runs across the sites owned by the authenticated organisation, including each run's current lifecycle state.","operationId":"listAudits","parameters":[{"$ref":"#/components/parameters/HistoryLimit"},{"$ref":"#/components/parameters/HistoryCursor"},{"$ref":"#/components/parameters/AuditStatusFilter"},{"in":"query","name":"site_id","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditList"}}},"description":"Audits"},"400":{"$ref":"#/components/responses/Problem","description":"Invalid, repeated or unknown collection parameter"}},"summary":"List organisation audits","tags":["Audits"]}},"/v1/audits/{auditId}":{"get":{"description":"Returns the organisation's audit record, including its processing status and recorded compliance and evidence-integrity results. Processing completion is distinct from passing the audit rules.","operationId":"getAuditsByAuditId","responses":{"200":{"description":"Audit"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"Get audit lifecycle, compliance and integrity state","tags":["Audits"]},"parameters":[{"$ref":"#/components/parameters/AuditId"}]},"/v1/audits/{auditId}/evidence":{"get":{"description":"Downloads the evidence bundle after the audit reaches completed or partial status. A bundle whose hosted artifacts have expired returns evidence_expired; retained hashes and signing metadata do not restore those files.","operationId":"listAuditsByAuditIdEvidence","responses":{"200":{"content":{"application/zip":{}},"description":"Evidence bundle"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.evidence, or the request names another organisation"},"404":{"$ref":"#/components/responses/Problem","description":"No such audit in this organisation"},"409":{"$ref":"#/components/responses/Problem","description":"evidence_not_final: the audit's status is not completed or partial"},"410":{"$ref":"#/components/responses/Problem","description":"evidence_expired: the hosted evidence has passed its retention period; hashes and signing metadata remain on the audit"}},"summary":"Download the independently verifiable evidence ZIP","tags":["Audits"]},"parameters":[{"$ref":"#/components/parameters/AuditId"}]},"/v1/audits/{auditId}/exports":{"parameters":[{"$ref":"#/components/parameters/AuditId"}],"post":{"description":"Queues delivery of a completed audit's evidence bundle to an S3 destination owned by the same organisation. The response acknowledges the export job; delivery progress is available from the exports endpoint.","operationId":"exportAuditsByAuditId","requestBody":{"content":{"application/json":{"schema":{"properties":{"destination_id":{"format":"uuid","type":"string"}},"required":["destination_id"],"type":"object"}}},"required":true},"responses":{"202":{"description":"Export queued"},"400":{"$ref":"#/components/responses/Problem","description":"invalid_json: the body is not one JSON value of the documented shape"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.integrations, or the request names another organisation"},"404":{"$ref":"#/components/responses/Problem","description":"No such completed audit or export destination in this organisation"}},"summary":"Queue a manual S3 export","tags":["Audits"]}},"/v1/audits/{auditId}/pages":{"get":{"description":"Supply limit or cursor to opt into bounded pages ordered by immutable created_at then ID descending. Filter-only and queryless requests retain the legacy complete response. Filters are case-insensitive comma lists. change accepts new, changed, moved and unchanged; compliance accepts pass, warning, fail, error and none (no recorded state); capture accepts captured and error (failed is an alias). q is a literal substring of requested URL, final URL or title. total is always the unfiltered audit count. Paged lists include rule results but omit artifacts; use the observation detail endpoint for retained artifact metadata.","operationId":"listAuditsByAuditIdPages","parameters":[{"in":"path","name":"auditId","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"query","name":"change","schema":{"type":"string"}},{"in":"query","name":"compliance","schema":{"type":"string"}},{"in":"query","name":"capture","schema":{"type":"string"}},{"in":"query","name":"q","schema":{"maxLength":512,"type":"string"}},{"in":"query","name":"limit","schema":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},{"$ref":"#/components/parameters/HistoryCursor"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditPageList"}}},"description":"Retained observation metadata"},"400":{"$ref":"#/components/responses/Problem"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"Page observations of an audit","tags":["Audits"]}},"/v1/events":{"get":{"description":"Types: audit.started, audit.completed (every terminal audit, with `status`), audit.partial, audit.failed, change.detected, compliance.failed, monitor.skipped (`reason` is overlap, no_urls, or the billing code that refused the run), evidence.expiring. quota.threshold_reached and audit.quota_blocked belonged to the retired plans and are no longer emitted; subscriptions to them are accepted and receive nothing. Payloads carry counts and ids only. Webhook deliveries are created from these; each delivery is signed and carries the event id as its idempotency key.\n","operationId":"listEvents","responses":{"200":{"description":"Events and the type catalogue"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.read, or the request names another organisation"}},"summary":"The organisation's recent product events, newest first","tags":["Events"]}},"/v1/export-destinations":{"get":{"description":"Returns S3 destination configurations for the authenticated organisation without exposing stored access credentials.","operationId":"listExportDestinations","responses":{"200":{"description":"Destinations; credentials are never returned"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.integrations, or the request names another organisation"}},"summary":"List S3 destinations","tags":["ExportDestinations"]},"post":{"description":"Stores an S3 or S3-compatible destination for the authenticated organisation. Access credentials are encrypted before storage and are not returned by destination listings.","operationId":"createExportDestinations","requestBody":{"content":{"application/json":{"schema":{"properties":{"access_key":{"type":"string"},"bucket":{"type":"string"},"endpoint":{"format":"uri","type":"string"},"export_mode":{"enum":["after_every_audit","only_if_compliance_changes","only_if_audit_fails","manual_only"],"type":"string"},"path_style":{"default":false,"type":"boolean"},"prefix":{"default":"audit","type":"string"},"region":{"type":"string"},"secret_key":{"type":"string","writeOnly":true},"session_token":{"type":"string","writeOnly":true}},"required":["region","bucket","access_key","secret_key"],"type":"object"}}},"required":true},"responses":{"201":{"description":"Destination created"},"400":{"$ref":"#/components/responses/Problem","description":"invalid_json: the body is not one JSON value of the documented shape"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.integrations, or the request names another organisation"},"422":{"$ref":"#/components/responses/Problem","description":"invalid_destination, invalid_endpoint, invalid_prefix, object_lock_not_configured or invalid_export_mode"}},"summary":"Create an encrypted S3/S3-compatible destination","tags":["ExportDestinations"]}},"/v1/export-destinations/{destinationId}/test":{"post":{"description":"Uses the destination's stored credentials to upload and verify a connection-test object. A failed upload or verification returns destination_test_failed.","operationId":"testExportDestinationsByDestinationId","parameters":[{"$ref":"#/components/parameters/DestinationId"}],"responses":{"200":{"description":"Upload verified"},"422":{"$ref":"#/components/responses/Problem"}},"summary":"Upload and verify a connection-test object","tags":["ExportDestinations"]}},"/v1/exports":{"get":{"description":"Returns evidence export jobs for the authenticated organisation, including their recorded delivery state.","operationId":"listExports","parameters":[{"$ref":"#/components/parameters/HistoryLimit"},{"$ref":"#/components/parameters/HistoryCursor"},{"in":"query","name":"audit_id","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportList"}}},"description":"Export jobs"},"400":{"$ref":"#/components/responses/Problem","description":"Invalid, repeated or unknown collection parameter"}},"summary":"List export jobs","tags":["Exports"]}},"/v1/findings":{"get":{"description":"Outstanding problems rather than audit results. A finding is one problem on one logical page against one requirement, for as long as that pairing exists: a page that fails the same rule every night is one finding, not one per night. Defaults to everything not resolved. Use sort=created for bounded cursor pages, newest creation time then ID descending. Creation time is immutable when a later audit sees the same problem, so updates do not move findings across the cursor. Filters are evaluated on each request; pages are not a frozen snapshot of membership. Omit sort for the legacy last-seen descending, ID ascending offset list. Summary counts are separate from the number of loaded findings.","operationId":"listFindings","parameters":[{"in":"query","name":"site_id","schema":{"format":"uuid","type":"string"}},{"in":"query","name":"page_id","schema":{"format":"uuid","type":"string"}},{"description":"The rule lineage id, so a reworded rule keeps its findings.","in":"query","name":"rule_id","schema":{"format":"uuid","type":"string"}},{"description":"Repeatable. Omit for everything still outstanding.","in":"query","name":"state","schema":{"items":{"enum":["open","acknowledged","accepted_risk","resolved","reopened"],"type":"string"},"type":"array"}},{"in":"query","name":"severity","schema":{"enum":["info","warning","failure","critical"],"type":"string"}},{"in":"query","name":"assignee","schema":{"type":"string"}},{"description":"Findings first seen more than this many days ago.","in":"query","name":"open_longer_than_days","schema":{"maximum":365000,"minimum":0,"type":"integer"}},{"in":"query","name":"limit","schema":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},{"in":"query","name":"sort","schema":{"default":"last_seen","enum":["last_seen","created"],"type":"string"}},{"description":"Returned next_cursor; valid only with sort=created. Omit on the first page.","in":"query","name":"cursor","schema":{"type":"string"}},{"description":"Legacy last-seen list only; cannot be combined with sort=created.","in":"query","name":"offset","schema":{"default":0,"minimum":0,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindingList"}}},"description":"Findings; next_cursor is present only when another creation-ordered page exists."},"400":{"$ref":"#/components/responses/Problem"},"422":{"$ref":"#/components/responses/Problem"}},"summary":"The finding inbox","tags":["Findings"]}},"/v1/findings/bulk":{"post":{"description":"The same path as a single update, once per finding, so each one gets its own event and nothing is attributed differently for having been done in a batch. Per-finding failures are reported rather than failing the request: a selection that includes one finding somebody else just resolved should still assign the other nine.","operationId":"bulkUpdateFindings","requestBody":{"content":{"application/json":{"schema":{"properties":{"assignee_user_id":{"type":"string"},"comment":{"type":"string"},"ids":{"items":{"format":"uuid","type":"string"},"maxItems":200,"type":"array"},"resolution_code":{"enum":["false_positive","accepted_risk","superseded","other"],"type":"string"},"state":{"enum":["acknowledged","accepted_risk","resolved"],"type":"string"}},"required":["ids"],"type":"object"}}},"required":true},"responses":{"200":{"description":"What was updated, and what could not be"},"422":{"$ref":"#/components/responses/Problem"}},"summary":"Apply one decision to several findings","tags":["Findings"]}},"/v1/findings/summary":{"get":{"description":"Open, acknowledged and accepted counts, how many are regressions, how many are past a due date somebody set, and the median time to resolution. The median is null until something has been resolved, because a median of nothing is not zero.","operationId":"getFindingsSummary","responses":{"200":{"description":"Summary"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.read, or the request names another organisation"}},"summary":"The numbers at the top of the finding inbox","tags":["Findings"]}},"/v1/findings/{findingId}":{"get":{"description":"Returns a finding belonging to the authenticated organisation, including its current triage state. Use the finding events endpoint to retrieve its change history.","operationId":"getFindingsByFindingId","responses":{"200":{"description":"Finding"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"Get one finding","tags":["Findings"]},"parameters":[{"in":"path","name":"findingId","required":true,"schema":{"format":"uuid","type":"string"}}],"patch":{"description":"A person cannot mark a finding fixed: that is a claim about the page which only a passing observation can make, so closing one by hand needs a resolution such as false_positive or accepted_risk. Reopening is likewise something an observation does. Every change is recorded against the person who made it.","operationId":"updateFindingsByFindingId","requestBody":{"content":{"application/json":{"schema":{"properties":{"accepted_risk_until":{"description":"RFC 3339. When it passes, the finding stops being suppressed and notifies again.","type":"string"},"assignee_user_id":{"description":"Empty string clears the assignment","type":"string"},"comment":{"type":"string"},"due_at":{"description":"RFC 3339, or an empty string to clear","type":"string"},"resolution_code":{"enum":["false_positive","accepted_risk","superseded","other"],"type":"string"},"state":{"enum":["acknowledged","accepted_risk","resolved"],"type":"string"}},"type":"object"}}},"required":true},"responses":{"200":{"description":"The updated finding"},"404":{"$ref":"#/components/responses/Problem"},"422":{"$ref":"#/components/responses/Problem"}},"summary":"Assign, schedule, acknowledge, accept the risk, or close a finding","tags":["Findings"]}},"/v1/findings/{findingId}/events":{"get":{"description":"Every state change, assignment and comment, newest first, each attributed to the person or to the system. Comments are operational records and are not part of signed evidence. A finding whose rule flaps accumulates history without anyone acting, so this is paged like the other histories.","operationId":"listFindingsByFindingIdEvents","parameters":[{"$ref":"#/components/parameters/HistoryLimit"},{"$ref":"#/components/parameters/HistoryCursor"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindingEventList"}}},"description":"Events"},"400":{"$ref":"#/components/responses/Problem","description":"Invalid, repeated or unknown collection parameter"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"The append-only history of a finding","tags":["Findings"]},"parameters":[{"in":"path","name":"findingId","required":true,"schema":{"format":"uuid","type":"string"}}]},"/v1/home":{"get":{"description":"Returns setup counts and recent activity for the authenticated organisation. Activity labels omit site URLs, captured content and credentials.","operationId":"getProductHome","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"activity":{"items":{"type":"object"},"maxItems":7,"type":"array"},"counts":{"additionalProperties":{"type":"integer"},"type":"object"},"product":{"const":"audit","type":"string"}},"required":["product","counts","activity"],"type":"object"}}},"description":"Counts, timestamps and at most seven safe audit activity labels; no site URLs or credentials."},"403":{"description":"The caller lacks audit.read"},"500":{"description":"Product summary unavailable"}},"summary":"Tenant-scoped setup facts and recent activity for the portal","tags":["Usage"]}},"/v1/monitors":{"get":{"description":"Returns the organisation's non-archived monitors, newest first, with the site name, the pages one run may capture and the page captures the monitor may ask for over the current month. Archived monitors are omitted.","operationId":"listMonitors","parameters":[{"$ref":"#/components/parameters/HistoryLimit"},{"$ref":"#/components/parameters/HistoryCursor"},{"in":"query","name":"site_id","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MonitorList"}}},"description":"Monitors"},"400":{"$ref":"#/components/responses/Problem","description":"Invalid, repeated or unknown collection parameter"}},"summary":"List monitors","tags":["Monitors"]}},"/v1/monitors/{monitorId}":{"get":{"description":"Returns the organisation's monitor configuration together with its configured URL set.","operationId":"getMonitorsByMonitorId","responses":{"200":{"description":"Monitor"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"Get a monitor with its URL set","tags":["Monitors"]},"parameters":[{"$ref":"#/components/parameters/MonitorId"}],"patch":{"description":"A cadence, anchor or timezone change clears the next slot; the scheduler recomputes it on its next tick.","operationId":"updateMonitorsByMonitorId","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MonitorInput"}}},"description":"Every field is optional; only fields present are changed. `mode` and `urls` are ignored here - use `PUT /v1/monitors/{monitorId}/urls` to change the URL set. Changing `cadence`, `anchor_minute` or `timezone` clears the next scheduled run so the scheduler recomputes it. An archived monitor is refused with 409.\n","required":true},"responses":{"200":{"description":"Monitor"},"409":{"description":"The monitor is archived"}},"summary":"Change a monitor's name, cadence, anchor, timezone, capture profile or change policy","tags":["Monitors"]}},"/v1/monitors/{monitorId}/archive":{"parameters":[{"$ref":"#/components/parameters/MonitorId"}],"post":{"description":"Permanently archives the organisation's monitor so it cannot be resumed or run again. Its previous audits and evidence remain subject to their existing retention policy.","operationId":"archiveMonitorsByMonitorId","responses":{"200":{"description":"Monitor"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.manage, or the request names another organisation"},"404":{"$ref":"#/components/responses/Problem","description":"No such monitor in this organisation"},"409":{"$ref":"#/components/responses/Problem","description":"monitor_archived: the monitor is already archived"}},"summary":"Archive a monitor (one-way; its audits and evidence remain)","tags":["Monitors"]}},"/v1/monitors/{monitorId}/pause":{"parameters":[{"$ref":"#/components/parameters/MonitorId"}],"post":{"description":"Pauses future scheduling for the organisation's monitor. Existing audit records and captured evidence remain available under their retention policy.","operationId":"pauseMonitorsByMonitorId","responses":{"200":{"description":"Monitor"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.manage, or the request names another organisation"},"404":{"$ref":"#/components/responses/Problem","description":"No such monitor in this organisation"},"409":{"$ref":"#/components/responses/Problem","description":"monitor_archived: an archived monitor cannot be changed"}},"summary":"Pause scheduling","tags":["Monitors"]}},"/v1/monitors/{monitorId}/resume":{"parameters":[{"$ref":"#/components/parameters/MonitorId"}],"post":{"description":"Resumes scheduling for the organisation's non-archived monitor using its configured cadence and scope.","operationId":"resumeMonitorsByMonitorId","responses":{"200":{"description":"Monitor"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.manage, or the request names another organisation"},"404":{"$ref":"#/components/responses/Problem","description":"No such monitor in this organisation"},"409":{"$ref":"#/components/responses/Problem","description":"monitor_archived: an archived monitor cannot be changed"}},"summary":"Resume scheduling","tags":["Monitors"]}},"/v1/monitors/{monitorId}/run":{"parameters":[{"$ref":"#/components/parameters/MonitorId"}],"post":{"description":"Admits and queues an immediate audit with the monitor's current scope, capture profile and change policy frozen into the run. It is admitted against `audit.capture` exactly as a site audit is, and a refusal is the shared billing answer. An Idempotency-Key replay returns the existing audit; archived monitors cannot run.","operationId":"runMonitorsByMonitorId","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Idempotent replay; the existing audit"},"202":{"description":"Audit queued"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"402":{"$ref":"#/components/responses/Problem","description":"Billing refusal, as for a site audit"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.run, or the request names another organisation"},"404":{"$ref":"#/components/responses/Problem","description":"No such monitor in this organisation"},"409":{"$ref":"#/components/responses/Problem","description":"monitor_archived: an archived monitor cannot run"},"413":{"$ref":"#/components/responses/Problem","description":"Billing refusal: operation_too_large"},"422":{"$ref":"#/components/responses/Problem","description":"urls_required: a url_set monitor with no enabled URLs"},"429":{"$ref":"#/components/responses/Problem","description":"Billing refusal: free_allowance_exhausted or a budget reached"},"503":{"$ref":"#/components/responses/Problem","description":"Billing refusal: billing_temporarily_unavailable"}},"summary":"Queue an audit now with the monitor's scope frozen as the scheduler would freeze it","tags":["Monitors"]}},"/v1/monitors/{monitorId}/runs":{"get":{"description":"Returns audits associated with the organisation's monitor, ordered from newest to oldest.","operationId":"listMonitorsByMonitorIdRuns","parameters":[{"$ref":"#/components/parameters/HistoryLimit"},{"$ref":"#/components/parameters/HistoryCursor"},{"$ref":"#/components/parameters/AuditStatusFilter"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditList"}}},"description":"Audits"},"400":{"$ref":"#/components/responses/Problem","description":"Invalid, repeated or unknown collection parameter"}},"summary":"The monitor's audits, newest first","tags":["Monitors"]},"parameters":[{"$ref":"#/components/parameters/MonitorId"}]},"/v1/monitors/{monitorId}/urls":{"parameters":[{"$ref":"#/components/parameters/MonitorId"}],"put":{"description":"Makes the monitor's URL set exactly the given list and returns the monitor: a URL already in the set keeps its identity and label, a URL absent from the list is removed, and duplicates after normalisation are stored once. At most 500 URLs. Only a `url_set` monitor has a URL list; any other mode is answered 409 `not_url_set`. Historical audits keep the scope they ran with.","operationId":"replaceMonitorsByMonitorIdUrls","requestBody":{"content":{"application/json":{"schema":{"properties":{"urls":{"description":"The complete new URL set. Validated against the site's hosts, patterns and the public-target policy; the whole set is refused if any entry fails.","items":{"format":"uri","type":"string"},"type":"array"}},"required":["urls"],"type":"object"}}},"required":true},"responses":{"200":{"description":"Monitor"},"400":{"$ref":"#/components/responses/Problem","description":"invalid_json: the body is not one JSON value of the documented shape"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.manage, or the request names another organisation"},"404":{"$ref":"#/components/responses/Problem","description":"No such monitor in this organisation"},"409":{"$ref":"#/components/responses/Problem","description":"not_url_set: only a url_set monitor has a URL list"},"422":{"$ref":"#/components/responses/Problem","description":"urls_required, too_many_urls, invalid_url, url_out_of_scope or unsafe_url"}},"summary":"Replace a url_set monitor's URL list","tags":["Monitors"]}},"/v1/notification-preferences":{"get":{"description":"Returns the organisation's selected notification events, recipients and digest preference, together with the available event type catalogue.","operationId":"getNotificationPreferences","responses":{"200":{"description":"Preferences and the event type catalogue"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.integrations, or the request names another organisation"}},"summary":"Which events the organisation is emailed about, to whom, and whether as a daily digest","tags":["NotificationPreferences"]},"put":{"description":"Recipients must be current members of the organisation, checked against Warden with the caller's own token; an API key may keep the list but not change it. Emails carry counts, names and a signed-in portal link - never captured content.","operationId":"replaceNotificationPreferences","requestBody":{"content":{"application/json":{"schema":{"properties":{"digest":{"description":"Send one daily digest instead of an email per event.","type":"boolean"},"events":{"description":"Event names the organisation is emailed about.","items":{"type":"string"},"type":"array"},"recipients":{"description":"Recipients can only be changed by a signed-in member; an API key may replace events and digest but must send the current recipients unchanged.","items":{"format":"email","type":"string"},"maxItems":25,"type":"array"}},"required":["events","digest","recipients"],"type":"object"}}},"required":true},"responses":{"200":{"description":"Preferences"},"422":{"$ref":"#/components/responses/Problem"}},"summary":"Replace the preferences","tags":["NotificationPreferences"]}},"/v1/notifications":{"get":{"description":"Returns recent notification-email records for the authenticated organisation and their recorded delivery states.","operationId":"listNotifications","parameters":[{"$ref":"#/components/parameters/HistoryLimit"},{"$ref":"#/components/parameters/HistoryCursor"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationList"}}},"description":"Notifications"},"400":{"$ref":"#/components/responses/Problem","description":"Invalid, repeated or unknown collection parameter"}},"summary":"Recent notification emails and their delivery state","tags":["Notifications"]}},"/v1/page-observations/{observationId}":{"get":{"description":"Returns an observation belonging to the authenticated organisation, including its captured artifact metadata and deterministic rule results.","operationId":"getPageObservationsByObservationId","parameters":[{"in":"path","name":"observationId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageObservation"}}},"description":"Retained observation metadata"},"400":{"$ref":"#/components/responses/Problem"},"404":{"description":"Not found"}},"summary":"One observation with its artifacts and rule results","tags":["PageObservations"]}},"/v1/page-observations/{observationId}/comparison":{"get":{"description":"Computed from stored evidence under the audit's change policy - normalised text hunks (whitespace collapsed, numbers masked, ignored selectors removed), scores, URL/title/status changes, rule transitions and screenshot artifact ids for each side. The raw artifacts are untouched. A first capture has no previous side and says why.","operationId":"getPageObservationsByObservationIdComparison","parameters":[{"in":"path","name":"observationId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"Comparison"},"400":{"$ref":"#/components/responses/Problem"},"404":{"description":"Not found"}},"summary":"What changed against the observation before it","tags":["PageObservations"]}},"/v1/pages/{pageId}/observations":{"get":{"description":"Returns paginated observations for a page within the authenticated organisation. Supply the returned next_cursor to continue through older observations, ordered by immutable created_at then ID descending. Lists include rule results but omit artifacts. Empty and foreign page IDs return an empty list. Repeated and unknown query parameters are rejected.","operationId":"listPagesByPageIdObservations","parameters":[{"in":"path","name":"pageId","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"query","name":"limit","schema":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},{"description":"next_cursor from the previous page","in":"query","name":"cursor","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ObservationList"}}},"description":"Retained observation metadata"},"400":{"$ref":"#/components/responses/Problem"}},"summary":"A page's observation history, newest first","tags":["Pages"]}},"/v1/retention/purchases":{"post":{"description":"Buys a quote exactly as quoted, in one step: the quoted quantity of\n`audit.retention_extension` is admitted by the billing account, every\nquoted deadline moves to its quoted date, and the usage is committed -\nall of it or none. Needs billing.manage from a signed-in person with a\nverified email address: API keys and connected apps are refused\n(`insufficient_scope`, `person_required`), as for every billing change.\nA refusal from the billing account (paid usage not enabled, a budget\nreached) is the shared billing answer. A quote past its `expires_at`,\nmade in another month or at another catalogue version, that the\naccount's position would now price differently, or whose evidence has\nchanged since (deleted, or its deadline moved) is `quote_expired`: ask\nfor a new quote. Buying a quote that is already bought returns it with\n200 and charges nothing more.\n","operationId":"purchaseRetentionQuote","requestBody":{"content":{"application/json":{"schema":{"properties":{"quote_id":{"format":"uuid","type":"string"}},"required":["quote_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionQuote"}}},"description":"Already bought; the quote as it was bought."},"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionQuote"}}},"description":"Bought; the quote with its new deadlines."},"400":{"$ref":"#/components/responses/Problem","description":"invalid_json"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"402":{"$ref":"#/components/responses/Problem","description":"Billing refusal: paid_usage_paused, payment_required, payment_action_required or account_restricted"},"403":{"$ref":"#/components/responses/Problem","description":"forbidden or insufficient_scope (billing.manage), person_required, email_unverified, or the billing refusal product_not_eligible"},"404":{"$ref":"#/components/responses/Problem","description":"not_found: no such quote in this organisation"},"409":{"$ref":"#/components/responses/Problem","description":"quote_expired, or retention_capacity_reached"},"422":{"$ref":"#/components/responses/Problem","description":"quote_required"},"429":{"$ref":"#/components/responses/Problem","description":"Billing refusal: free_allowance_exhausted (the extension has no free allowance: paid usage must be enabled), account_budget_exhausted or product_budget_exhausted"},"503":{"$ref":"#/components/responses/Problem","description":"billing_disabled, or the billing refusal billing_temporarily_unavailable"}},"summary":"Buy a retention quote","tags":["Retention"]}},"/v1/retention/quotes":{"post":{"description":"Prices keeping the hosted evidence of the chosen audits (every hosted\nartifact of each) and artifacts for `months` whole months past each one's\ncurrent deadline. New audits are promised the standard retention (30 days\nin the draft catalogue); an extension is how evidence stays hosted longer.\nThe quantity is in the `audit.retention_extension` meter's unit: the\nselection's bytes rounded up to that unit's share of a decimal gigabyte,\ntimes the months, never below ten GB-months. `amount` is the fixed price of\nthat quantity at the organisation's position this month, exact and before\ntax, charged on the month's invoice through the billing account. A quote\ncan be bought until `expires_at` (about fifteen minutes), in the month and\nat the catalogue version it was made in; it moves nothing and charges\nnothing by itself, and nothing ever renews. Expired (deleted) evidence\ncannot be extended. An extension that would take the storage held under\nextended retention past the deployment's capacity is refused, paid or not.\nNeeds audit.read: a quote is a price, not a purchase.\n","operationId":"createRetentionQuote","requestBody":{"content":{"application/json":{"schema":{"properties":{"artifact_ids":{"items":{"format":"uuid","type":"string"},"maxItems":5000,"type":"array"},"audit_ids":{"items":{"format":"uuid","type":"string"},"maxItems":200,"type":"array"},"months":{"description":"Whole months of extra retention past each current deadline.","maximum":120,"minimum":1,"type":"integer"}},"required":["months"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionQuote"}}},"description":"The quote, with each artifact's deadline before and after."},"400":{"$ref":"#/components/responses/Problem","description":"invalid_json or invalid_id"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.read"},"404":{"$ref":"#/components/responses/Problem","description":"not_found: an audit or artifact that is not this organisation's"},"409":{"$ref":"#/components/responses/Problem","description":"retention_capacity_reached: the deployment cannot hold more evidence under extended retention"},"413":{"$ref":"#/components/responses/Problem","description":"Billing refusal: operation_too_large"},"422":{"$ref":"#/components/responses/Problem","description":"selection_required, selection_too_large, invalid_months, or nothing_to_extend when none of the selection is still hosted"},"503":{"$ref":"#/components/responses/Problem","description":"billing_disabled: unified billing is not running here"}},"summary":"Price an extension of hosted evidence retention","tags":["Retention"]}},"/v1/rule-packs":{"get":{"description":"Ironfang-maintained rule templates. A pack is a starting point, not a compliance product: each one states what it checks and, in the same breath, what it cannot tell you. Installing a pack copies rules into your own site, which you then own and can edit.","operationId":"listRulePacks","responses":{"200":{"description":"Packs"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.read, or the request names another organisation"}},"summary":"The rule pack catalogue","tags":["RulePacks"]}},"/v1/rule-packs/installed/{installationId}":{"delete":{"description":"Retires the rules it created. They stop being evaluated; they are not deleted, so every result that referenced one still resolves.","operationId":"deleteRulePacksInstalledByInstallationId","responses":{"204":{"description":"Uninstalled"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"Uninstall a pack","tags":["RulePacks"]},"parameters":[{"in":"path","name":"installationId","required":true,"schema":{"format":"uuid","type":"string"}}]},"/v1/rule-packs/installed/{installationId}/upgrade":{"parameters":[{"in":"path","name":"installationId","required":true,"schema":{"format":"uuid","type":"string"}}],"post":{"description":"The explicit action. Regenerates the pack's rules and writes the differences as ordinary revisions, so every change keeps its history. Rules the pack no longer ships are reported as orphaned and left alone rather than deleted, because you may still want the requirement.","operationId":"upgradeRulePacksInstalledByInstallationId","requestBody":{"content":{"application/json":{"schema":{"properties":{"variables":{"additionalProperties":{"type":"string"},"description":"Omit to reuse the values given at install.","type":"object"}},"type":"object"}}}},"responses":{"200":{"description":"What the upgrade changed"},"404":{"$ref":"#/components/responses/Problem"},"422":{"$ref":"#/components/responses/Problem"}},"summary":"Upgrade an installed pack, or change its values","tags":["RulePacks"]}},"/v1/rules/{lineageId}":{"delete":{"description":"Stops the rule being evaluated in future audits. Revisions are kept, so every historical result still resolves to the exact rule that ran. Retirement is permanent; stating the requirement again creates a new rule with its own history.","operationId":"deleteRulesByLineageId","responses":{"204":{"description":"Retired"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"Retire a rule","tags":["Rules"]},"get":{"description":"Resolves a rule's stable lineage identifier to its current revision within the authenticated organisation.","operationId":"getRulesByLineageId","responses":{"200":{"description":"Rule"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"Get the active revision of a rule","tags":["Rules"]},"parameters":[{"description":"The stable id of the rule. A rule keeps this id for its whole life; each edit writes a new revision underneath it.","in":"path","name":"lineageId","required":true,"schema":{"format":"uuid","type":"string"}}],"patch":{"description":"Writes the next version of the rule and makes it active. Nothing about the previous revision changes, so evidence captured before the edit still names the wording that produced it. Omitted fields carry forward from the active revision.","operationId":"updateRulesByLineageId","requestBody":{"content":{"application/json":{"schema":{"properties":{"configuration":{"additionalProperties":true,"type":"object"},"description":{"type":"string"},"name":{"type":"string"},"rule_type":{"type":"string"},"severity":{"enum":["info","warning","failure","critical"],"type":"string"}},"type":"object"}}},"required":true},"responses":{"200":{"description":"The new active revision"},"404":{"$ref":"#/components/responses/Problem"},"409":{"$ref":"#/components/responses/Problem"},"422":{"$ref":"#/components/responses/Problem"}},"summary":"Revise a rule","tags":["Rules"]}},"/v1/rules/{lineageId}/disable":{"parameters":[{"in":"path","name":"lineageId","required":true,"schema":{"format":"uuid","type":"string"}}],"post":{"description":"Affects future audits only. A historical result keeps the status it had: a rule that was failing in March was still failing in March.","operationId":"disableRulesByLineageId","responses":{"200":{"description":"Rule"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"Disable a rule for future audits","tags":["Rules"]}},"/v1/rules/{lineageId}/enable":{"parameters":[{"in":"path","name":"lineageId","required":true,"schema":{"format":"uuid","type":"string"}}],"post":{"description":"Enables the organisation's rule for evaluation by future audits. Previously recorded audit results are unchanged.","operationId":"enableRulesByLineageId","responses":{"200":{"description":"Rule"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"Enable a rule for future audits","tags":["Rules"]}},"/v1/rules/{lineageId}/revisions":{"get":{"description":"Returns the revision history for the organisation's rule in descending version order, preserving access to the configurations referenced by earlier audit results.","operationId":"listRulesByLineageIdRevisions","parameters":[{"description":"Bounded page size. The existing default of 200 is preserved; the portal requests 50.","in":"query","name":"limit","schema":{"default":200,"maximum":200,"minimum":1,"type":"integer"}},{"description":"The next_cursor from the preceding page. This collection is keyed on the revision version, which is unique and monotonic within a rule, so the cursor is that version and the page continues below it.","in":"query","name":"cursor","schema":{"maxLength":16,"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleRevisionList"}}},"description":"Revisions"},"400":{"$ref":"#/components/responses/Problem","description":"Invalid, repeated or unknown collection parameter"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"List every revision of a rule, newest first","tags":["Rules"]},"parameters":[{"in":"path","name":"lineageId","required":true,"schema":{"format":"uuid","type":"string"}}]},"/v1/sites":{"get":{"description":"Returns the sites owned by the authenticated organisation, including their stored audit configuration and complete per-site activity facts. Counts and last audit come from authoritative tenant-scoped queries, independently of history page limits.","operationId":"listSites","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteList"}}},"description":"Sites"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.read, or the request names another organisation"}},"summary":"List sites","tags":["Sites"]},"post":{"description":"Registers a public website for the authenticated organisation. The URL must pass the public-target policy and the organisation must be below the site capacity limit (250 sites unless the deployment sets otherwise, for every organisation whatever it pays). A crawl policy is held inside the crawl ceilings and the per-audit page cap. robots.txt is respected unless the policy explicitly sets `respect_robots` to false.","operationId":"createSites","requestBody":{"content":{"application/json":{"schema":{"properties":{"crawl_policy":{"description":"Fields of the site's crawl policy (see CrawlPolicy); omitted fields take the ceilings or defaults. `respect_robots` is true unless it is given as false.","type":"object"},"name":{"type":"string"},"timezone":{"default":"UTC","type":"string"},"url":{"format":"uri","type":"string"}},"required":["name","url"],"type":"object"}}},"required":true},"responses":{"201":{"description":"Site created"},"400":{"$ref":"#/components/responses/Problem","description":"invalid_json: the body or its crawl_policy is not of the documented shape"},"422":{"$ref":"#/components/responses/Problem","description":"invalid_url, unsafe_url, or site_limit: the organisation holds as many sites as the capacity limit allows"}},"summary":"Create a site","tags":["Sites"]}},"/v1/sites/{siteId}":{"get":{"description":"Returns stored site configuration and complete activity facts. A site outside the authenticated organisation is reported as not found.","operationId":"getSitesBySiteId","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Site"}}},"description":"Site"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"Get a site","tags":["Sites"]},"parameters":[{"$ref":"#/components/parameters/SiteId"}]},"/v1/sites/{siteId}/audits":{"get":{"description":"Returns audit runs for the selected site within the authenticated organisation. A site outside the organisation is reported as not found.","operationId":"listSitesBySiteIdAudits","parameters":[{"$ref":"#/components/parameters/HistoryLimit"},{"$ref":"#/components/parameters/HistoryCursor"},{"$ref":"#/components/parameters/AuditStatusFilter"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditList"}}},"description":"Audits"},"400":{"$ref":"#/components/responses/Problem","description":"Invalid, repeated or unknown collection parameter"}},"summary":"List site audits","tags":["Sites"]},"parameters":[{"$ref":"#/components/parameters/SiteId"}],"post":{"description":"Admits and queues an audit for the organisation's site and returns its initial lifecycle state. Admission comes first: the most pages the audit can capture (`page_bound`: the site's page limit within the per-audit cap, or one page for single_page) are reserved against the `audit.capture` meter of the organisation's billing account. A refusal is answered now with the shared billing code and no audit exists. The audit then captures at most `page_bound` pages and is charged only for the page evidence it finalises. Reusing an Idempotency-Key returns the existing audit and reserves nothing more; single_page limits the scope to the site's primary URL.","operationId":"createSitesBySiteIdAudits","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"reason":{"type":"string"},"single_page":{"default":false,"description":"Capture only the primary URL without following links or creating a monitor.","type":"boolean"},"trigger_type":{"default":"api","type":"string"}},"type":"object"}}}},"responses":{"200":{"description":"Idempotent replay; the existing audit"},"202":{"description":"Audit admitted and queued"},"402":{"$ref":"#/components/responses/Problem","description":"Billing refusal: paid_usage_paused, payment_required, payment_action_required or account_restricted"},"403":{"$ref":"#/components/responses/Problem","description":"forbidden or insufficient_scope; or the billing refusal product_not_eligible"},"404":{"$ref":"#/components/responses/Problem"},"410":{"$ref":"#/components/responses/Problem","description":"Billing refusal: account_closed"},"413":{"$ref":"#/components/responses/Problem","description":"Billing refusal: operation_too_large"},"429":{"$ref":"#/components/responses/Problem","description":"Billing refusal: free_allowance_exhausted, account_budget_exhausted, product_budget_exhausted or exemption_limit_reached. reset_at says when the free allowance renews"},"503":{"$ref":"#/components/responses/Problem","description":"Billing refusal: billing_temporarily_unavailable, with Retry-After"}},"summary":"Trigger an asynchronous audit","tags":["Sites"]}},"/v1/sites/{siteId}/monitors":{"get":{"description":"Returns the organisation's non-archived monitors for the selected site, including their scheduling configuration.","operationId":"listSitesBySiteIdMonitors","responses":{"200":{"description":"Monitors (archived ones omitted)"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.read, or the request names another organisation"},"404":{"$ref":"#/components/responses/Problem","description":"No such site in this organisation"}},"summary":"List the site's monitors","tags":["Sites"]},"parameters":[{"$ref":"#/components/parameters/SiteId"}],"post":{"description":"`mode` is `full_site` (crawler discovery under the site's crawl policy) or `url_set` (exactly the given `urls`, validated against the site's hosts, patterns and the public-target policy; the whole set is refused if any entry fails). `cadence` is `manual`, `daily`, `six_hourly` or `hourly`, open to every organisation; each scheduled run is admitted against `audit.capture` when its slot arrives, and a refused run is skipped with a `monitor.skipped` event whose `reason` is the billing code. `anchor_minute` is minutes past midnight in `timezone` for daily, or within the interval otherwise.\n","operationId":"createSitesBySiteIdMonitors","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MonitorInput"}}},"required":true},"responses":{"201":{"description":"Monitor with its URL set"},"422":{"$ref":"#/components/responses/Problem"}},"summary":"Create a monitor","tags":["Sites"]}},"/v1/sites/{siteId}/rule-packs":{"get":{"description":"Includes latest_version and upgrade_available. A newer pack is never applied on its own; you are told one exists.","operationId":"listSitesBySiteIdRulePacks","responses":{"200":{"description":"Installed packs"},"404":{"$ref":"#/components/responses/Problem"}},"summary":"Packs installed on this site","tags":["Sites"]},"parameters":[{"$ref":"#/components/parameters/SiteId"}],"post":{"description":"Installs the selected catalogue pack on the organisation's site using the supplied variable values. The pack's templates become site rules with their own revision history.","operationId":"createSitesBySiteIdRulePacks","requestBody":{"content":{"application/json":{"schema":{"properties":{"pack_key":{"type":"string"},"variables":{"additionalProperties":{"type":"string"},"description":"The values the pack declares. Missing ones are named in a 422.","type":"object"}},"required":["pack_key","variables"],"type":"object"}}},"required":true},"responses":{"201":{"description":"Installed"},"404":{"$ref":"#/components/responses/Problem"},"409":{"$ref":"#/components/responses/Problem"},"422":{"$ref":"#/components/responses/Problem"}},"summary":"Install a rule pack","tags":["Sites"]}},"/v1/sites/{siteId}/rules":{"get":{"description":"Returns the deterministic rules configured for a site owned by the authenticated organisation, with their version and configuration metadata.","operationId":"listSitesBySiteIdRules","responses":{"200":{"description":"Rules"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.read, or the request names another organisation"},"404":{"$ref":"#/components/responses/Problem","description":"No such site in this organisation"}},"summary":"List the site's versioned deterministic rules","tags":["Sites"]},"parameters":[{"$ref":"#/components/parameters/SiteId"}],"post":{"description":"Validates the rule type and configuration, then creates a versioned rule for the selected site. Invalid configurations are rejected before an audit can run them.","operationId":"createSitesBySiteIdRules","requestBody":{"content":{"application/json":{"schema":{"properties":{"configuration":{"additionalProperties":true,"type":"object"},"name":{"type":"string"},"rule_type":{"type":"string"},"severity":{"enum":["info","warning","failure","critical"],"type":"string"}},"required":["name","rule_type","configuration"],"type":"object"}}},"required":true},"responses":{"201":{"description":"Rule created"},"422":{"$ref":"#/components/responses/Problem"}},"summary":"Create a deterministic rule","tags":["Sites"]}},"/v1/usage":{"get":{"description":"The month (UTC) as the organisation's billing account has it for Ironfang\nAudit's meters - `captures` (`audit.capture`) and `retention_extension`\n(`audit.retention_extension`): the free allowance, what has been settled,\nwhat admitted audits still hold, and the charge so far before tax. Both are\nnull where unified billing is not running (`billing_enabled` false): audits\nare then admitted on capacity alone and nothing is metered. Beside them, the\nfacts that are not billing: `sites` against `site_limit` and\n`max_pages_per_audit` (capacity limits, the same for everyone), the\nstandard hosted retention new audits get, hosted bytes and the next expiry,\nthe `projection` of captures the active scheduled monitors may ask for this\nmonth (pages x runs, an upper bound), and `daily` captures by UTC day of\nadmission. `quota` is deprecated: the launch-era shape, now captures\nagainst the month's free allowance only.\n","operationId":"getUsage","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Usage"}}},"description":"Usage"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.read, or the request names another organisation"}},"summary":"This month's metered use, capacity and hosted evidence","tags":["Usage"]}},"/v1/webhook-deliveries":{"get":{"description":"Requires audit.integrations. Includes history for retired endpoints. Ordered by immutable delivery ID descending (generated UUIDv7 IDs reflect dispatch order); retrying cannot move a cursor boundary. Creation timestamps and full attempt histories were not retained and are not inferred from next_attempt_at. Lists omit payloads; use the direct detail read for the retained body and latest response.","operationId":"listWebhookDeliveries","parameters":[{"$ref":"#/components/parameters/DeliveryCursor"},{"$ref":"#/components/parameters/DeliveryLimit"},{"$ref":"#/components/parameters/DeliveryStatus"},{"in":"query","name":"destination","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryList"}}},"description":"Owned delivery page, Cache-Control no-store."},"400":{"description":"Invalid, repeated or unknown query parameter."},"401":{"description":"Authentication required."},"403":{"description":"Integration permission required."}},"summary":"Page organisation webhook deliveries","tags":["Webhooks"]}},"/v1/webhook-deliveries/{deliveryId}":{"get":{"description":"Returns retained payload and latest response without a history scan. Requires audit.integrations.","operationId":"getWebhookDelivery","parameters":[{"in":"path","name":"deliveryId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDelivery"}}},"description":"Owned delivery, Cache-Control no-store."},"400":{"description":"Detail accepts no query parameters."},"401":{"description":"Authentication required."},"403":{"description":"Integration permission required."},"404":{"description":"Unknown, malformed or foreign delivery."}},"summary":"Read one organisation-owned delivery","tags":["Webhooks"]}},"/v1/webhooks":{"get":{"description":"Returns the authenticated organisation's webhook endpoint configurations. Signing secrets are not included in the listing.","operationId":"listWebhooks","responses":{"200":{"description":"Webhook endpoints; secrets are never returned"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.integrations, or the request names another organisation"}},"summary":"List lifecycle webhook endpoints","tags":["Webhooks"]},"post":{"description":"Registers an event delivery endpoint for the authenticated organisation and returns its new signing secret. Store the secret when creating the endpoint; subsequent listings do not disclose it.","operationId":"createWebhooks","requestBody":{"content":{"application/json":{"schema":{"properties":{"events":{"items":{"enum":["audit.completed"],"type":"string"},"type":"array"},"url":{"format":"uri","type":"string"}},"required":["url"],"type":"object"}}},"required":true},"responses":{"201":{"description":"Endpoint and one-time signing secret"},"400":{"$ref":"#/components/responses/Problem","description":"invalid_json: the body is not one JSON value of the documented shape"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.integrations, or the request names another organisation"},"422":{"$ref":"#/components/responses/Problem","description":"invalid_webhook_url or unsupported_event"}},"summary":"Create a signed webhook endpoint","tags":["Webhooks"]}},"/v1/webhooks/{endpointId}":{"delete":{"description":"Retires the organisation's webhook endpoint from future event delivery while preserving its existing delivery history.","operationId":"deleteWebhooksByEndpointId","responses":{"204":{"description":"Retired"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.integrations, or the request names another organisation"},"404":{"$ref":"#/components/responses/Problem","description":"No such webhook in this organisation"}},"summary":"Retire an endpoint (soft; its delivery history is kept)","tags":["Webhooks"]},"parameters":[{"$ref":"#/components/parameters/EndpointId"}],"patch":{"description":"Enabling clears the consecutive-failure count. An endpoint is disabled automatically after 20 consecutive failed delivery attempts; the reason is on the endpoint.","operationId":"updateWebhooksByEndpointId","requestBody":{"content":{"application/json":{"schema":{"properties":{"enabled":{"type":"boolean"},"events":{"items":{"enum":["audit.completed"],"type":"string"},"type":"array"}},"type":"object"}}},"required":true},"responses":{"200":{"description":"Endpoint"},"400":{"$ref":"#/components/responses/Problem","description":"invalid_json: the body is not one JSON value of the documented shape"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.integrations, or the request names another organisation"},"404":{"$ref":"#/components/responses/Problem","description":"No such webhook in this organisation"},"422":{"$ref":"#/components/responses/Problem","description":"unknown_event"}},"summary":"Enable or disable an endpoint, or change its subscribed events","tags":["Webhooks"]}},"/v1/webhooks/{endpointId}/deliveries":{"get":{"description":"Without query parameters preserves the legacy 200 records ordered by next_attempt_at, including payloads. Include limit or cursor for stable descending delivery-ID pagination; paginated lists omit payloads. Default endpoint page limit is 200. Filters run before paging.","operationId":"listWebhooksByEndpointIdDeliveries","parameters":[{"$ref":"#/components/parameters/EndpointId"},{"$ref":"#/components/parameters/DeliveryCursor"},{"$ref":"#/components/parameters/DeliveryLimit"},{"$ref":"#/components/parameters/DeliveryStatus"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryList"}}},"description":"Owned delivery records, Cache-Control no-store."},"400":{"description":"Invalid query parameter."}},"summary":"List endpoint deliveries","tags":["Webhooks"]}},"/v1/webhooks/{endpointId}/deliveries/{deliveryId}/retry":{"parameters":[{"$ref":"#/components/parameters/EndpointId"},{"in":"path","name":"deliveryId","required":true,"schema":{"format":"uuid","type":"string"}}],"post":{"description":"Requests another attempt for a failed delivery belonging to the organisation's webhook endpoint. The response acknowledges that the retry was queued, not that delivery succeeded.","operationId":"retryWebhooksByEndpointIdDeliveriesByDeliveryId","responses":{"202":{"description":"Queued"},"401":{"$ref":"#/components/responses/Problem","description":"No credential, or one that is not accepted"},"403":{"$ref":"#/components/responses/Problem","description":"The key's scopes or the person's role do not allow audit.integrations, or the request names another organisation"},"404":{"$ref":"#/components/responses/Problem","description":"No such delivery on this webhook, or it is not retryable"}},"summary":"Re-queue a failed delivery","tags":["Webhooks"]}},"/v1/webhooks/{endpointId}/rotate-secret":{"post":{"description":"Mints a new signing secret and returns it once; deliveries signed from now on use it, so update the receiver first if it verifies strictly.","operationId":"rotateWebhooksByEndpointIdSecret","parameters":[{"$ref":"#/components/parameters/EndpointId"}],"responses":{"200":{"description":"The new signing secret, shown only in this response."},"404":{"$ref":"#/components/responses/Problem"}},"summary":"Replace the endpoint's signing secret","tags":["Webhooks"]}},"/v1/webhooks/{endpointId}/test":{"post":{"description":"Sends a signed endpoint.test event to the organisation's webhook endpoint and reports whether the destination accepted it.","operationId":"testWebhooksByEndpointId","parameters":[{"$ref":"#/components/parameters/EndpointId"}],"responses":{"200":{"description":"Test accepted"},"422":{"$ref":"#/components/responses/Problem"}},"summary":"Send a signed endpoint.test event","tags":["Webhooks"]}},"/verify/v1/bundles":{"post":{"description":"Accepts an evidence ZIP without requiring an account and checks its integrity, signature and timestamp trust. Invalid evidence returns a verification report with status 422; successful verification does not assert that the audited website complies with any law.","operationId":"createVerifyBundles","requestBody":{"content":{"application/zip":{}},"required":true},"responses":{"200":{"description":"Every integrity and trust check is valid"},"422":{"description":"Verification report for invalid evidence"}},"security":[],"summary":"Verify an Ironfang Audit evidence ZIP","tags":["Verify"]}},"/verify/v1/signing-keys":{"get":{"description":"Publishes the public signing-key ledger and lifecycle metadata used to inspect Ironfang Audit evidence signatures. Authentication is not required and private signing keys are never returned.","operationId":"listVerifySigningKeys","responses":{"200":{"description":"Public signing keys and lifecycle metadata"}},"security":[],"summary":"List the Ironfang Audit signing-key ledger","tags":["Verify"]}}},"security":[{"bearerAuth":[]},{"apiKey":[]}],"servers":[{"description":"Production","url":"https://api.ironfang.com/audit"}],"tags":[{"description":"Sites under audit.","name":"Sites"},{"description":"Installable rule packs.","name":"RulePacks"},{"description":"Findings raised by audits.","name":"Findings"},{"description":"Rules and their revisions.","name":"Rules"},{"description":"Audit runs, their pages and evidence.","name":"Audits"},{"description":"Stored screenshots and other captured artifacts.","name":"Artifacts"},{"description":"Webhook endpoints and deliveries.","name":"Webhooks"},{"description":"Where exports are delivered.","name":"ExportDestinations"},{"description":"Evidence bundle exports.","name":"Exports"},{"description":"This month's metered use, capacity and hosted evidence.","name":"Usage"},{"description":"Quoted, fixed-price extensions of hosted evidence retention.","name":"Retention"},{"description":"Public verification of evidence bundles.","name":"Verify"},{"description":"Scheduled re-audits of a site.","name":"Monitors"},{"description":"The account event stream.","name":"Events"},{"description":"How the organisation is notified.","name":"NotificationPreferences"},{"description":"Notifications raised for the organisation.","name":"Notifications"},{"description":"Pages within an audit.","name":"Pages"},{"description":"Per-page observations and comparisons.","name":"PageObservations"}]}