{"openapi":"3.1.0","info":{"title":"Postvow Email API","version":"0.1.0","description":"Privacy-first transactional email service"},"components":{"schemas":{"SendResponse":{"type":"object","properties":{"messageId":{"type":"string","format":"uuid"},"stream":{"type":"string"},"warnings":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}}},"required":["messageId","stream"]},"ProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"suggestedFix":{"type":"string"},"docsUrl":{"type":"string"},"details":{"type":"object","additionalProperties":{"nullable":true}}},"required":["type","title","status","detail","instance","suggestedFix","docsUrl"]},"SendRequest":{"type":"object","properties":{"to":{"type":"string","format":"email","example":"recipient@example.com"},"from":{"type":"string","format":"email","example":"noreply@yourdomain.com"},"subject":{"type":"string","minLength":1,"example":"Welcome!"},"html":{"type":"string","example":"<h1>Hello</h1>"},"text":{"type":"string","example":"Hello"},"stream":{"type":"string","example":"default"},"headers":{"type":"object","additionalProperties":{"type":"string"}},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"priority":{"type":"string","enum":["normal","critical"],"example":"normal"}},"required":["to","from","subject"]},"DomainResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"01936f2e-7c00-7000-8000-000000000001"},"domain":{"type":"string","example":"example.com"},"status":{"type":"string","example":"pending"},"dnsRecords":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","example":"TXT"},"host":{"type":"string","example":"example.com"},"value":{"type":"string","example":"v=spf1 include:_spf.postvow.eu ~all"},"priority":{"type":"string","enum":["required","recommended"],"example":"required"}},"required":["type","host","value","priority"]}},"preconditions":{"type":"array","items":{"$ref":"#/components/schemas/DomainPrecondition"},"description":"Open preconditions — decisions or actions that belong to the TENANT and that Postvow deliberately does not take on their behalf. Additive field; existing consumers are unaffected. On this endpoint the array is currently always exactly one entry (dmarc-policy-decision-required), because the create request carries no DMARC decision and the decision is not persisted — a non-empty-by-choice array is only reachable from the operator CLI."},"createdAt":{"type":"string","format":"date-time","example":"2026-03-26T12:00:00.000Z"}},"required":["id","domain","status","dnsRecords","preconditions","createdAt"]},"DomainPrecondition":{"type":"object","properties":{"code":{"type":"string","enum":["dmarc-policy-decision-required"],"example":"dmarc-policy-decision-required","description":"Closed vocabulary. Consumers MUST render an UNKNOWN code using `detail` rather than skipping it — the enum is an extension point, and new members are added additively."},"severity":{"type":"string","enum":["blocking-onboarding"],"example":"blocking-onboarding"},"detail":{"type":"string","description":"Human-readable explanation. Always present, so an unknown `code` is still renderable."}},"required":["code","severity","detail"]},"DomainCreateRequest":{"type":"object","properties":{"domain":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\\.[a-zA-Z]{2,}$","example":"example.com"}},"required":["domain"]},"DomainListItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"domain":{"type":"string"},"status":{"type":"string","description":"Stored verification verdict from the last verify run: 'pending' | 'verifying' | 'verified' | 'failed'."},"verificationError":{"type":"string","nullable":true},"dkimSelector":{"type":"string","nullable":true,"description":"Selector of the ACTIVE DKIM key, null when none exists."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","domain","status","verificationError","dkimSelector","createdAt","updatedAt"]},"DomainVerifyResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"domain":{"type":"string"},"status":{"type":"string"},"verificationDetails":{"type":"object","properties":{"spf":{"type":"object","properties":{"found":{"type":"boolean"},"record":{"type":"string","nullable":true},"valid":{"type":"boolean"}},"required":["found","record","valid"]},"dkim":{"type":"object","properties":{"found":{"type":"boolean"},"record":{"type":"string","nullable":true},"valid":{"type":"boolean"}},"required":["found","record","valid"]},"dmarc":{"type":"object","properties":{"state":{"type":"string","enum":["absent","published","multiple-records","lookup-failed"],"description":"absent = no DMARC record AT THIS NAME (a policy inherited from the organizational domain via the RFC 9989 §4.10 Tree Walk is NOT checked, so for a subdomain 'absent' is often the correct end state); published = exactly one record with a valid leading v=DMARC1; multiple-records = more than one, which RFC 9989 §4.10 requires receivers to DISCARD ENTIRELY (so the tenant has NO policy at this name — this is the one state where an operator runs dig, sees DMARC, and is wrong); lookup-failed = we could not observe, which is NOT the same fact as observing nothing."},"record":{"type":"string","nullable":true},"policy":{"type":"string","nullable":true,"enum":["none","quarantine","reject",null],"description":"The policy AS DECLARED (RFC 9989 §4.7), null when absent or uninterpretable — NOT the receiver-effective policy of §4.10.1."},"adkim":{"type":"string","nullable":true,"enum":["s","r",null]},"aspf":{"type":"string","nullable":true,"enum":["s","r",null]},"rua":{"type":"array","items":{"type":"string"}},"ruf":{"type":"array","items":{"type":"string"},"description":"Failure-report destinations as published — a read-only observation. The Postvow emitter can never produce a ruf= tag (ruf carries message headers, a categorically higher risk class than rua)."}},"required":["state","record","policy","adkim","aspf","rua","ruf"]}},"required":["spf","dkim","dmarc"]},"verificationError":{"type":"string","nullable":true},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","domain","status","verificationDetails","verificationError","updatedAt"]},"DkimRotationStatusResponse":{"type":"object","properties":{"domainId":{"type":"string","format":"uuid","example":"01936f2e-7c00-7000-8000-000000000001"},"domain":{"type":"string","example":"example.com"},"activeKey":{"type":"object","nullable":true,"properties":{"keyId":{"type":"string","format":"uuid"},"selector":{"type":"string","example":"k1"},"rotationStatus":{"type":"string","enum":["pending","active","overlap","retired"]},"nextRotationAt":{"type":"string","nullable":true,"format":"date-time"},"overlapStartedAt":{"type":"string","nullable":true,"format":"date-time"}},"required":["keyId","selector","rotationStatus","nextRotationAt","overlapStartedAt"],"description":"Active DKIM key details, or null if no active key exists"}},"required":["domainId","domain","activeKey"]},"WarmupListResponse":{"type":"object","properties":{"warmups":{"type":"array","items":{"$ref":"#/components/schemas/WarmupProgressResponse"}}},"required":["warmups"]},"WarmupProgressResponse":{"type":"object","properties":{"domain":{"type":"string"},"dayNumber":{"type":"integer","description":"Current warmup day (1-60)"},"startedAt":{"type":"string","format":"date-time"},"currentDailyLimit":{"type":"integer","description":"Ceiling for the current tier (sends/day)"},"sentToday":{"type":"integer"},"perIsp":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/WarmupIspVelocity"},"description":"Per-ISP velocity (dynamic ISP-group keys)"},"completionPercentage":{"type":"number","description":"Ramp completion 0-100"},"estimatedCompletionDate":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["warmup","complete","paused","failed"]},"pauseReasons":{"type":"array","items":{"$ref":"#/components/schemas/WarmupPauseEntry"}},"pausedAt":{"type":"string","format":"date-time"},"isSendingGated":{"type":"boolean","description":"Are sends currently throttled by the ramp (isWarmupComplete === false)"},"nextTierIfHealthy":{"type":"object","properties":{"dayNumber":{"type":"integer"},"dailyLimit":{"type":"integer"},"delta":{"type":"integer"}},"required":["dayNumber","dailyLimit","delta"],"description":"Tomorrow's ceiling from DEFAULT_WARMUP_SCHEDULE + delta — labeled *if-healthy* because the actual advance is Go/No-Go-gated (not guaranteed)."}},"required":["domain","dayNumber","startedAt","currentDailyLimit","sentToday","perIsp","completionPercentage","estimatedCompletionDate","status","pauseReasons","isSendingGated","nextTierIfHealthy"]},"WarmupIspVelocity":{"type":"object","properties":{"limit":{"type":"integer"},"sent":{"type":"integer"},"paused":{"type":"boolean"}},"required":["limit","sent","paused"]},"WarmupPauseEntry":{"type":"object","properties":{"reason":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}},"required":["reason","createdAt"]},"ApiKeyCreateResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"key":{"type":"string","description":"Plaintext API key — shown ONCE only"},"scopes":{"type":"array","items":{"type":"string"}},"lastFourChars":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}},"required":["id","name","key","scopes","lastFourChars","createdAt"]},"ApiKeyCreateRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"example":"My Integration"},"scopes":{"type":"array","items":{"type":"string","enum":["send","webhooks:read","webhooks:write","webhooks:manage","compliance:read","compliance:export","domains:manage","domains:read","keys:manage","keys:read","team:manage","tenant:manage","metrics:read","delivery:read","suppression:manage","billing:read","admin"]},"minItems":1,"example":["send","webhooks:read"]},"environment":{"type":"string","enum":["live","test"],"example":"live"}},"required":["name","scopes","environment"]},"ApiKeyListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeyListItem"}},"pagination":{"type":"object","properties":{"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}},"required":["nextCursor","hasMore"]}},"required":["data","pagination"]},"ApiKeyListItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"lastFourChars":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"environment":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":"string","nullable":true,"format":"date-time"}},"required":["id","name","lastFourChars","scopes","environment","createdAt","lastUsedAt"]},"StreamDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"messageCount":{"type":"integer","nullable":true},"lastActivityAt":{"type":"string","nullable":true},"createdAt":{"type":"string"}},"required":["id","name","description","messageCount","lastActivityAt","createdAt"]},"StreamProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"suggestedFix":{"type":"string"},"docsUrl":{"type":"string"},"details":{"type":"object","additionalProperties":{"nullable":true}}},"required":["type","title","status","detail","instance","suggestedFix","docsUrl"]},"StreamCreateRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[a-z][a-z0-9_-]*$","example":"transactional"},"description":{"type":"string","minLength":1,"maxLength":1024,"pattern":"^(?:(?!\\p{Bidi_Control})[\\x20-\\x7E\\xA0-\\uFFFF])*$/u"}},"required":["name"]},"StreamListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/StreamSummary"}},"pagination":{"type":"object","properties":{"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}},"required":["nextCursor","hasMore"]}},"required":["data","pagination"]},"StreamSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"createdAt":{"type":"string"}},"required":["id","name","description","createdAt"]},"StreamUpdateRequest":{"type":"object","properties":{"description":{"type":"string","minLength":1,"maxLength":1024,"pattern":"^(?:(?!\\p{Bidi_Control})[\\x20-\\x7E\\xA0-\\uFFFF])*$/u"}}},"WebhookConfigWithSecret":{"allOf":[{"$ref":"#/components/schemas/WebhookConfig"},{"type":"object","properties":{"signingSecret":{"type":"string"},"previousSecretExpiresAt":{"type":"string","nullable":true}},"required":["signingSecret","previousSecretExpiresAt"]}]},"WebhookConfig":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"status":{"type":"string"},"description":{"type":"string","nullable":true},"verifiedAt":{"type":"string","nullable":true},"lastVerificationAttemptAt":{"type":"string","nullable":true},"lastVerificationError":{"type":"string","nullable":true},"createdAt":{"type":"string"}},"required":["id","url","events","status","description","verifiedAt","lastVerificationAttemptAt","lastVerificationError","createdAt"]},"WebhookProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"suggestedFix":{"type":"string"},"docsUrl":{"type":"string"},"details":{"type":"object","additionalProperties":{"nullable":true}}},"required":["type","title","status","detail","instance","suggestedFix","docsUrl"]},"WebhookCreateRequest":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"events":{"type":"array","items":{"anyOf":[{"type":"string","enum":["postvow.email.delivered","postvow.email.bounced","postvow.email.deferred","postvow.email.complained","postvow.email.dropped","postvow.dkim.rotated"]},{"type":"string","enum":["*"]}]},"minItems":1},"description":{"type":"string","maxLength":1024,"pattern":"^(?:(?!\\p{Bidi_Control})[\\x20-\\x7E\\xA0-\\uFFFF])*$/u"}},"required":["url","events"]},"WebhookListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookConfig"}},"pagination":{"type":"object","properties":{"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}},"required":["nextCursor","hasMore"]}},"required":["data","pagination"]},"WebhookUpdateRequest":{"type":"object","properties":{"events":{"type":"array","items":{"anyOf":[{"type":"string","enum":["postvow.email.delivered","postvow.email.bounced","postvow.email.deferred","postvow.email.complained","postvow.email.dropped","postvow.dkim.rotated"]},{"type":"string","enum":["*"]}]},"minItems":1},"description":{"type":"string","maxLength":1024,"pattern":"^(?:(?!\\p{Bidi_Control})[\\x20-\\x7E\\xA0-\\uFFFF])*$/u"},"status":{"type":"string","enum":["active"]}}},"WebhookMessage":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},"RotateSecretRequest":{"type":"object","properties":{"force":{"type":"boolean","default":false}}},"WebhookDeliveryListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"pagination":{"type":"object","properties":{"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}},"required":["nextCursor","hasMore"]}},"required":["data","pagination"]},"WebhookDelivery":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"eventId":{"type":"string","format":"uuid"},"eventType":{"type":"string"},"attemptNumber":{"type":"integer"},"statusCode":{"type":"integer","nullable":true},"success":{"type":"boolean"},"errorMessage":{"type":"string","nullable":true},"createdAt":{"type":"string"}},"required":["id","eventId","eventType","attemptNumber","statusCode","success","errorMessage","createdAt"]},"MessageListItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"stream":{"type":"string"},"status":{"type":"string"},"recipientDomain":{"type":"string"},"recipientHash":{"type":"string","nullable":true},"senderAddress":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}},"required":["id","stream","status","recipientDomain","recipientHash","senderAddress","createdAt"]},"MessageProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"suggestedFix":{"type":"string"},"docsUrl":{"type":"string"},"details":{"type":"object","additionalProperties":{"nullable":true}}},"required":["type","title","status","detail","instance","suggestedFix","docsUrl"]},"ExplainDecisionTreeResponse":{"type":"object","properties":{"schemaVersion":{"type":"integer","minimum":0,"exclusiveMinimum":true},"messageId":{"type":"string","format":"uuid"},"stream":{"type":"string"},"finalStatus":{"type":"string"},"decisions":{"type":"array","items":{"$ref":"#/components/schemas/ExplainDecisionStep"}},"rootCause":{"type":"object","nullable":true,"properties":{"step":{"type":"integer","minimum":0,"exclusiveMinimum":true},"action":{"type":"string"},"description":{"type":"string"},"suggestedAction":{"type":"string"}},"required":["step","action","description","suggestedAction"]},"summary":{"type":"string"},"smtpResponse":{"type":"object","nullable":true,"properties":{"code":{"type":"integer"},"enhancedCode":{"type":"string","nullable":true}},"required":["code","enhancedCode"]}},"required":["schemaVersion","messageId","stream","finalStatus","decisions","rootCause","summary","smtpResponse"]},"ExplainDecisionStep":{"type":"object","properties":{"step":{"type":"integer","minimum":0,"exclusiveMinimum":true},"action":{"type":"string"},"component":{"type":"string"},"description":{"type":"string"},"result":{"type":"string"},"severity":{"type":"string"},"durationMs":{"type":"integer","minimum":0},"timestamp":{"type":"string","format":"date-time"},"attemptNumber":{"type":"integer","minimum":0,"exclusiveMinimum":true},"suggestedAction":{"type":"string"},"metadata":{"type":"object","additionalProperties":{"nullable":true}}},"required":["step","action","component","description","result","severity","durationMs","timestamp"]},"MessageResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"stream":{"type":"string"},"status":{"type":"string"},"recipientDomain":{"type":"string","nullable":true},"createdAt":{"type":"string"},"explain":{"$ref":"#/components/schemas/ExplainDecisionTreeResponse"}},"required":["id","stream","status","recipientDomain","createdAt"]},"SuppressionEntryResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"emailHash":{"type":"string"},"reason":{"type":"string"},"expiresAt":{"type":"string","nullable":true},"createdAt":{"type":"string"}},"required":["id","emailHash","reason","expiresAt","createdAt"]},"ComplianceProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status","detail","instance"]},"SuppressionAddRequest":{"type":"object","properties":{"email":{"type":"string","format":"email"},"reason":{"type":"string","enum":["manual"]},"expiresAt":{"type":"string","format":"date-time"}},"required":["email","reason"]},"SuppressionListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SuppressionEntryResponse"}},"pagination":{"type":"object","properties":{"hasMore":{"type":"boolean"},"nextCursor":{"type":"string"}},"required":["hasMore"]}},"required":["data","pagination"]},"ErasureProofResponse":{"type":"object","properties":{"requestedAt":{"type":"string"},"completedAt":{"type":"string"},"dataCategories":{"type":"array","items":{"type":"string"}},"requestSource":{"type":"string"},"verificationHash":{"type":"string"},"deletedCount":{"type":"object","properties":{"messages":{"type":"number"}},"required":["messages"]},"warning":{"type":"string"}},"required":["requestedAt","completedAt","dataCategories","requestSource","verificationHash","deletedCount"]},"ErasureRequest":{"type":"object","properties":{"email":{"type":"string","format":"email"},"requestSource":{"type":"string","minLength":1,"maxLength":100}},"required":["email","requestSource"]},"RetentionPolicyResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"stream":{"type":"string"},"metadataRetentionDays":{"type":"number"},"complianceLogRetentionDays":{"type":"number"},"effectiveMetadataRetentionDays":{"type":"number"},"isDefault":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["stream","metadataRetentionDays","complianceLogRetentionDays","effectiveMetadataRetentionDays","isDefault"]},"RetentionPolicyRequest":{"type":"object","properties":{"stream":{"type":"string","minLength":1,"maxLength":64,"pattern":"^(\\*|[a-zA-Z0-9_-]+)$","default":"*"},"metadataRetentionDays":{"type":"integer","default":90,"minimum":1,"maximum":3650},"complianceLogRetentionDays":{"type":"integer","minimum":1095,"maximum":3650}}},"RetentionPoliciesListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RetentionPolicyResponse"}}},"required":["data"]},"Art30ExportResponse":{"type":"object","properties":{"exportId":{"type":"string"},"exportVersion":{"type":"string","enum":["1.0"]},"processorRole":{"type":"string","enum":["processor"]},"controller":{"type":"object","properties":{"name":{"type":"string"},"contact":{"type":"string","nullable":true}},"required":["name","contact"]},"processingActivities":{"type":"array","items":{"$ref":"#/components/schemas/Art30ProcessingActivity"}},"subprocessors":{"type":"array","items":{"$ref":"#/components/schemas/Art30Subprocessor"}},"securityMeasures":{"type":"array","items":{"type":"string"}},"exportedAt":{"type":"string"},"exportedBy":{"type":"string"}},"required":["exportId","exportVersion","processorRole","controller","processingActivities","subprocessors","securityMeasures","exportedAt","exportedBy"]},"Art30ProcessingActivity":{"type":"object","properties":{"purpose":{"type":"string"},"legalBasis":{"type":"string"},"stream":{"type":"string"},"recipientDomainCategories":{"type":"array","items":{"type":"string"}},"dataCategories":{"type":"array","items":{"type":"string"}},"dataSubjectCategories":{"type":"array","items":{"type":"string"}},"specialCategoryPresent":{"type":"boolean"},"retentionPeriod":{"type":"string"},"encryptionStatus":{"type":"boolean"},"mtaUsed":{"type":"array","items":{"type":"string"}},"recordCount":{"type":"number"},"dateRange":{"type":"object","nullable":true,"properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]}},"required":["purpose","legalBasis","stream","recipientDomainCategories","dataCategories","dataSubjectCategories","specialCategoryPresent","retentionPeriod","encryptionStatus","mtaUsed","recordCount","dateRange"]},"Art30Subprocessor":{"type":"object","properties":{"name":{"type":"string"},"purpose":{"type":"string"},"location":{"type":"string"},"transferMechanism":{"type":"string"}},"required":["name","purpose","location","transferMechanism"]},"Art33TemplateResponse":{"type":"object","properties":{"template":{"type":"string"}},"required":["template"]},"Art33TemplateRequest":{"type":"object","properties":{"tenantId":{"type":"string","format":"uuid"},"recipientDomain":{"type":"string","maxLength":255,"pattern":"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$"},"incidentDescription":{"type":"string","minLength":1,"maxLength":500}},"required":["tenantId","recipientDomain","incidentDescription"]},"PostExportResponse":{"type":"object","properties":{"exportId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["processing"]}},"required":["exportId","status"]},"ExportProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"exportId":{"type":"string"}},"required":["type","title","status","detail"]},"PostExportRequest":{"type":"object","properties":{"includeKeys":{"type":"boolean","default":false}}},"GetExportStatusResponse":{"type":"object","properties":{"exportId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["processing","ready","failed","expired"]},"downloadUrl":{"type":"string"},"error":{"type":"string"},"createdAt":{"type":"string"}},"required":["exportId","status","createdAt"]},"AgentAttributionResponse":{"type":"object","properties":{"messageId":{"type":"string","format":"uuid"},"tenantId":{"type":"string","format":"uuid"},"aiModel":{"type":"string"},"timestamp":{"type":"string"},"disclosureStatus":{"type":"string","enum":["labeled","pending","exempt","sender_declared"]},"confidenceScore":{"type":"number"},"classifierVersion":{"type":"string"}},"required":["messageId","tenantId","aiModel","timestamp","disclosureStatus","confidenceScore","classifierVersion"]},"DetectIncidentResponse":{"type":"object","properties":{"assessment":{"$ref":"#/components/schemas/IncidentAssessmentResponse"},"report":{"$ref":"#/components/schemas/IncidentReport"}},"required":["assessment","report"]},"IncidentAssessmentResponse":{"type":"object","properties":{"eventId":{"type":"string","format":"uuid"},"meetsReportingThreshold":{"type":"boolean"},"thresholdReason":{"type":"string"},"nis2Category":{"type":"string"},"estimatedImpact":{"type":"object","properties":{"affectedRecipients":{"type":"number"},"affectedDomains":{"type":"array","items":{"type":"string"}},"dataAtRisk":{"type":"string"}},"required":["affectedRecipients","affectedDomains","dataAtRisk"]},"assessedAt":{"type":"string"}},"required":["eventId","meetsReportingThreshold","thresholdReason","nis2Category","estimatedImpact","assessedAt"]},"IncidentReport":{"type":"object","nullable":true,"properties":{"reportId":{"type":"string","format":"uuid"},"tenantId":{"type":"string","format":"uuid"},"generatedAt":{"type":"string"},"nis2Category":{"type":"string"},"incidentTimeline":{"type":"array","items":{"type":"object","properties":{"timestamp":{"type":"string"},"description":{"type":"string"},"category":{"type":"string","enum":["detection","containment","notification","remediation"]}},"required":["timestamp","description","category"]}},"impactAssessment":{"type":"string"},"affectedServices":{"type":"array","items":{"type":"string"}},"containmentMeasures":{"type":"array","items":{"type":"string"}},"remediationSteps":{"type":"array","items":{"type":"string"}},"notificationDeadline":{"type":"string","nullable":true},"notificationDeadlineFull":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","submitted","acknowledged"]},"reportFormat":{"type":"string","enum":["json","pdf_template"]},"externalRef":{"type":"string","nullable":true}},"required":["reportId","tenantId","generatedAt","nis2Category","incidentTimeline","impactAssessment","affectedServices","containmentMeasures","remediationSteps","notificationDeadline","notificationDeadlineFull","status","reportFormat","externalRef"]},"SecurityEventRequest":{"type":"object","properties":{"eventType":{"type":"string","enum":["delivery_anomaly","authentication_failure","data_breach_suspected","service_disruption","unauthorized_access"]},"tenantId":{"type":"string","format":"uuid"},"severity":{"type":"string","enum":["low","medium","high","critical"]},"timestamp":{"type":"string","format":"date-time"},"details":{"type":"object","additionalProperties":{"nullable":true},"default":{}},"source":{"type":"string","minLength":1,"maxLength":255}},"required":["eventType","tenantId","severity","timestamp","source"]},"AttributionExportResponse":{"type":"object","properties":{"exportId":{"type":"string","format":"uuid","description":"Unique export reference for regulatory correspondence"},"tenantId":{"type":"string","format":"uuid"},"exportedAt":{"type":"string"},"dateRange":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"}},"required":["start","end"]},"totalRecords":{"type":"integer"},"format":{"type":"string","enum":["json","csv"]},"schemaVersion":{"type":"string","description":"Export format version, e.g. \"1.0\""},"data":{"type":"string","description":"Base64-encoded export content"},"checksum":{"type":"string","description":"SHA-256 hex of data buffer (integrity verification)"}},"required":["exportId","tenantId","exportedAt","dateRange","totalRecords","format","schemaVersion","data","checksum"]},"AttributionExportRequest":{"type":"object","properties":{"startDate":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"endDate":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date"}]},"format":{"type":"string","enum":["json","csv"],"default":"json"},"includeClassificationDetails":{"type":"boolean","default":true}},"required":["startDate","endDate"]},"GRCConfigureResponse":{"type":"object","properties":{"configured":{"type":"boolean","enum":[true]},"platform":{"type":"string","enum":["vanta","drata","secureframe"]},"nextSyncScheduled":{"type":"string"}},"required":["configured","platform","nextSyncScheduled"]},"GRCConfigureRequest":{"type":"object","properties":{"platform":{"type":"string","enum":["vanta","drata","secureframe"]},"apiKey":{"type":"string","minLength":20,"maxLength":512,"pattern":"^[\\x20-\\x7E]+$"},"syncFrequency":{"type":"string","enum":["hourly","daily","weekly"]},"evidenceTypes":{"type":"array","items":{"type":"string","enum":["encryption","access_control","audit_logs","compliance_records","retention_policies","suppression_list","incidents","ai_attribution"]},"maxItems":16,"default":[]},"region":{"type":"string","enum":["eu","us"]}},"required":["platform","apiKey","syncFrequency"],"additionalProperties":false},"GRCStatusResponse":{"type":"object","properties":{"integrations":{"type":"array","items":{"$ref":"#/components/schemas/GRCIntegrationStatus"}}},"required":["integrations"]},"GRCIntegrationStatus":{"type":"object","properties":{"platform":{"type":"string","enum":["vanta","drata","secureframe"]},"configured":{"type":"boolean","enum":[true]},"lastSyncAt":{"type":"string","nullable":true},"lastSyncResult":{"type":"string","nullable":true,"enum":["success","partial","failed",null]},"nextSyncScheduled":{"type":"string","nullable":true},"evidenceTypes":{"type":"array","items":{"type":"string","enum":["encryption","access_control","audit_logs","compliance_records","retention_policies","suppression_list","incidents","ai_attribution"]}},"healthy":{"type":"boolean"}},"required":["platform","configured","lastSyncAt","lastSyncResult","nextSyncScheduled","evidenceTypes","healthy"]},"CreateTenantResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"plan":{"type":"string"},"status":{"type":"string"},"defaultStreamId":{"type":"string","format":"uuid"},"bootstrapApiKey":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"key":{"type":"string","description":"Plaintext admin API key — shown ONCE only, not recoverable after this response"},"scopes":{"type":"array","items":{"type":"string"}},"lastFourChars":{"type":"string"},"environment":{"type":"string","enum":["live","test"]}},"required":["id","key","scopes","lastFourChars","environment"]}},"required":["id","name","plan","status","defaultStreamId","bootstrapApiKey"]},"CreateTenantRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","example":"dieta.io"},"plan":{"type":"string","pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","default":"internal","example":"internal"},"locale":{"type":"string","maxLength":5,"default":"en","example":"en"},"bootstrapKeyEnvironment":{"type":"string","enum":["live","test"],"default":"live","example":"live"}},"required":["name"]},"ListTenantsResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminTenantResponse"}}},"required":["items"]},"AdminTenantResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"status":{"type":"string","enum":["active","suspended"]},"plan":{"type":"string"},"createdAt":{"type":"string","format":"date-time","example":"2026-06-21T09:14:02.604Z"},"suspendedAt":{"type":"string","nullable":true,"format":"date-time","example":"2026-07-01T12:00:00.000Z"},"suspensionReason":{"type":"string","nullable":true},"suspensionExpiresAt":{"type":"string","nullable":true,"format":"date-time","example":"2026-08-01T12:00:00.000Z"}},"required":["id","name","status","plan","createdAt","suspendedAt","suspensionReason","suspensionExpiresAt"]},"ProvisionM2mClientResponse":{"type":"object","properties":{"configId":{"type":"string","format":"uuid"},"idp":{"type":"string"},"clientId":{"type":"string"},"tenantId":{"type":"string","format":"uuid"},"allowedDomains":{"type":"array","items":{"type":"string"}},"sendQuota":{"type":"integer"},"status":{"type":"string","enum":["active"]},"allowedDomainsCount":{"type":"integer"},"unverifiedDomainsCount":{"type":"integer"},"warnings":{"type":"object","properties":{"unverifiedDomains":{"type":"array","items":{"type":"string"}},"denyAll":{"type":"boolean"}},"required":["unverifiedDomains","denyAll"]}},"required":["configId","idp","clientId","tenantId","allowedDomains","sendQuota","status","allowedDomainsCount","unverifiedDomainsCount","warnings"]},"ProvisionM2mClientRequest":{"type":"object","properties":{"clientId":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","example":"postvow-prod-m2m"},"tenantId":{"type":"string","format":"uuid","example":"00000000-0000-0000-0000-000000000000"},"allowedDomains":{"type":"array","items":{"type":"string"},"example":["mail.acme.com"]},"sendQuota":{"type":"integer","minimum":1,"maximum":10000000,"example":1000},"idp":{"type":"string","minLength":1,"maxLength":50,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","default":"rakomi","example":"rakomi"},"contractSha":{"type":"string","pattern":"^[0-9a-f]{7,64}$","example":"d548a4e"},"actor":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","description":"The append-only audit actor. MUST carry one of three prefixes — operator: / svc: / cli: — which is an anti-PII boundary, not a naming convention: this value lands in a table with no erasure path. Omitted → the handler derives operator:${apiKeyId}. On a createdBy redaction, supply the DSR case reference (an identifier, not personal data).","example":"operator:dsr-2026-07-31-014"}},"required":["clientId","tenantId","sendQuota"]},"ListM2mClientsResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/M2mClientSummary"}}},"required":["items"]},"M2mClientSummary":{"type":"object","properties":{"configId":{"type":"string","format":"uuid"},"idp":{"type":"string"},"clientId":{"type":"string"},"tenantId":{"type":"string","format":"uuid"},"status":{"type":"string"},"sendQuota":{"type":"integer"},"quotaWindow":{"type":"string"},"allowedDomains":{"type":"array","items":{"type":"string"}},"allowedDomainsCount":{"type":"integer"},"allowedOperations":{"type":"array","nullable":true,"items":{"type":"string"}},"cnfRequired":{"type":"boolean"},"contractSha":{"type":"string","nullable":true},"createdBy":{"type":"string","nullable":true},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"quota":{"type":"object","properties":{"consumed":{"type":"integer"},"remaining":{"type":"integer","nullable":true},"exhausted":{"type":"boolean","nullable":true}},"required":["consumed","remaining","exhausted"]},"effectivelyDisabled":{"type":"boolean"},"warnings":{"type":"object","properties":{"missingQuotaRow":{"type":"boolean","enum":[true]},"quotaWindowUnsupported":{"type":"boolean","enum":[true]},"unverifiedDomains":{"type":"array","items":{"type":"string"}},"quotaRowRestored":{"type":"boolean","enum":[true]},"createdByRedacted":{"type":"boolean","enum":[true]},"denyAllApplied":{"type":"boolean","enum":[true]},"tenantSuspended":{"type":"boolean","enum":[true]},"tenantLastActiveClient":{"type":"boolean","enum":[true]}}}},"required":["configId","idp","clientId","tenantId","status","sendQuota","quotaWindow","allowedDomains","allowedDomainsCount","allowedOperations","cnfRequired","contractSha","createdBy","createdAt","updatedAt","quota","effectivelyDisabled"]},"PatchM2mClientRequest":{"type":"object","properties":{"allowedDomains":{"type":"array","items":{"type":"string"},"description":"REPLACES the array wholesale (not a merge/append). `[]` is legal and means DENY-ALL — the 9b-1 invariant, never deny-none; the 200 then carries warnings.denyAllApplied."},"sendQuota":{"type":"integer","minimum":1,"maximum":10000000,"example":1000},"status":{"type":"string","enum":["active","disabled"],"description":"Mirrors chk_m2m_client_configs_status. 'disabled' is the incident lever — an incident- containment measure within the meaning of NIS2 Art. 21(2)(b) and ISO 27001 A.5.26. A disabled client fails the SAME 404 unknown_client path as an unknown id (byte-identical, no status oracle) and its quota counter is left UNTOUCHED so re-enabling is lossless."},"contractSha":{"type":"string","nullable":true,"pattern":"^[0-9a-f]{7,64}$","description":"Explicit `null` CLEARS it. Free text would poison drift reconciliation."},"createdBy":{"type":"object","nullable":true,"enum":[null],"description":"Only `null` is accepted. SCOPE, stated exactly because a DPO/auditor reads this text: it erases ONE column — `m2m_client_configs.created_by` — with a real UPDATE … SET created_by = NULL. It does NOT erase the value anywhere else. In particular the pre-existing append-only m2m_client_config_audit row with action='created' STILL CARRIES the identifiable value and is not erasable (an immutability trigger, by design); backups and the WAL/PITR substrate retain it for their retention period; and this redaction itself writes a further append-only row recording only the MARKER {createdBy: \"redacted\"}, never the old value. So this is a PARTIAL Art. 17 measure covering the live policy row, not a complete erasure of the data subject from the system. Supply an `actor` carrying the DSR case reference (e.g. \"operator:dsr-2026-07-31-014\"), never the default. Full limits: infra/runbooks/rakomi-prod-onboarding.md § \"Repair a bad row\"."},"restoreMissingQuotaRow":{"type":"boolean","enum":[true],"description":"Opt-in repair for a row whose m2m_client_quota_usage counter is MISSING (a hard block — the client cannot send a single message). Refused with 422 QUOTA_ROW_EXISTS when a counter already exists: the flag must never become a way to zero a live counter."},"actor":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","description":"The append-only audit actor. MUST carry one of three prefixes — operator: / svc: / cli: — which is an anti-PII boundary, not a naming convention: this value lands in a table with no erasure path. Omitted → the handler derives operator:${apiKeyId}. On a createdBy redaction, supply the DSR case reference (an identifier, not personal data).","example":"operator:dsr-2026-07-31-014"},"reason":{"type":"string","minLength":10,"maxLength":500,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","description":"Change-authorisation justification. Written verbatim to an append-only, non-erasable operator_audit_log.reason column retained 1095 days — describe the SITUATION, never a person. No names, e-mail addresses, recipient addresses or message content. Prefer an externally verifiable reference (a coordination issue number, a ticket id) over prose. No regex can enforce this; it is an instruction to the human.","example":"coordination#12 — allowedDomains was provisioned one label too broad"}},"additionalProperties":false},"DeleteM2mClientRequest":{"type":"object","properties":{"actor":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","description":"The append-only audit actor. MUST carry one of three prefixes — operator: / svc: / cli: — which is an anti-PII boundary, not a naming convention: this value lands in a table with no erasure path. Omitted → the handler derives operator:${apiKeyId}. On a createdBy redaction, supply the DSR case reference (an identifier, not personal data).","example":"operator:dsr-2026-07-31-014"},"reason":{"type":"string","minLength":10,"maxLength":500,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","description":"Change-authorisation justification. Written verbatim to an append-only, non-erasable operator_audit_log.reason column retained 1095 days — describe the SITUATION, never a person. No names, e-mail addresses, recipient addresses or message content. Prefer an externally verifiable reference (a coordination issue number, a ticket id) over prose. No regex can enforce this; it is an instruction to the human.","example":"coordination#12 — allowedDomains was provisioned one label too broad"}},"required":["reason"],"additionalProperties":false},"PutBearerTypesResponse":{"type":"object","properties":{"key":{"type":"string"},"requested":{"type":"array","items":{"type":"string","enum":["native","rakomi"]}},"effective":{"type":"array","items":{"type":"string","enum":["native","rakomi"]}},"envCeiling":{"type":"array","items":{"type":"string","enum":["native","rakomi"]}},"previous":{"type":"array","nullable":true,"items":{"type":"string","enum":["native","rakomi"]}},"warnings":{"type":"object","properties":{"nativeRetained":{"type":"boolean"},"notRestartDurable":{"type":"boolean"},"previousUnparsable":{"type":"boolean"},"notEnforced":{"type":"boolean"}},"required":["nativeRetained","notRestartDurable","previousUnparsable","notEnforced"]},"updatedAt":{"type":"string"},"convergesByUtc":{"type":"string"}},"required":["key","requested","effective","envCeiling","previous","warnings","updatedAt","convergesByUtc"]},"PutBearerTypesRequest":{"type":"object","properties":{"acceptedBearerTypes":{"type":"array","items":{"type":"string","enum":["native","rakomi"]},"example":["native","rakomi"]},"reason":{"type":"string","minLength":8,"maxLength":500,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","example":"incident 2026-07-20: Rakomi signing key suspected compromised"}},"required":["acceptedBearerTypes","reason"]},"GetRuntimeConfigResponse":{"type":"object","properties":{"effective":{"type":"array","items":{"type":"string","enum":["native","rakomi"]}},"envCeiling":{"type":"array","items":{"type":"string","enum":["native","rakomi"]}},"dbValue":{"type":"array","nullable":true,"items":{"type":"string","enum":["native","rakomi"]}},"source":{"type":"string","enum":["db","db-noop","env-default","stale-cache","cold-floor"],"description":"Provenance of `effective`. db = a row narrows the ceiling; db-noop = a row exists but changes nothing (a dead flag is debt, not state); env-default = a read SUCCEEDED and found no row — ALSO reported when the env ceiling cannot contain `rakomi`, because the env then fully determines the effective set and the DB content is definitionally irrelevant (no read is issued at all); stale-cache = an ALARM, and it means EXACTLY ONE thing: the last read attempt FAILED, so the value served is frozen last-known-good. It does NOT mean \"past the TTL\" — under stale-while-revalidate a healthy process legitimately serves a value older than its TTL while a refresh is in flight, and reports `db`; cold-floor = this process has NEVER read successfully and is serving the safe floor [\"native\"] — M2M is OFF regardless of the ceiling. READ `warnings.notEnforced` FIRST: when it is true no resolver exists in this process, so `source` describes no read at all and a dbValue/effective divergence will NEVER converge."},"updatedAt":{"type":"string","nullable":true},"updatedBy":{"type":"string","nullable":true},"reason":{"type":"string","nullable":true},"ttlSeconds":{"type":"number","description":"The NOMINAL TTL, not a refresh period. The real per-process TTL is re-rolled with ±15 % jitter on every refresh, so it can run up to 15 % LONGER than this value. The fleet convergence predicate at WORKER_COUNT>1 is therefore `now - updatedAt > ttlSeconds * 1.15 + readBudget` — NOT `> ttlSeconds`, and NOT \"a GET showed the right value\". Use the PUT response's `convergesByUtc`, which already carries that head-room, whenever you have it."},"cacheAgeSeconds":{"type":"number","nullable":true},"fetchedAt":{"type":"string","nullable":true},"bootedAt":{"type":"string"},"warnings":{"type":"object","properties":{"dbValueExceedsCeiling":{"type":"boolean"},"dbValueInvalid":{"type":"boolean"},"effectiveIsEmpty":{"type":"boolean"},"dbValueUnavailable":{"type":"boolean"},"notEnforced":{"type":"boolean"}},"required":["dbValueExceedsCeiling","dbValueInvalid","effectiveIsEmpty","dbValueUnavailable","notEnforced"]}},"required":["effective","envCeiling","dbValue","source","updatedAt","updatedBy","reason","ttlSeconds","cacheAgeSeconds","fetchedAt","bootedAt","warnings"]},"ReissueBootstrapKeyPreview":{"type":"object","properties":{"tenantId":{"type":"string","format":"uuid"},"tenantStatus":{"type":"string"},"wouldRevokeKeys":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"lastFourChars":{"type":"string"},"environment":{"type":"string"}},"required":["id","name","scopes","lastFourChars","environment"]},"description":"The full would-be-revoked set, one rich object per key (the same shape the 201 returns in revokedKeys). Read THIS, not the by-source id arrays, before choosing an irreversible mode."},"wouldRevoke":{"type":"object","properties":{"byProvenance":{"type":"array","items":{"type":"string"}},"byNameHeuristic":{"type":"array","items":{"type":"string"}}},"required":["byProvenance","byNameHeuristic"]},"wouldRevokeCount":{"type":"integer"},"activeAdminKeyCountBefore":{"type":"integer"},"revokedAcrossEnvironments":{"type":"boolean"}},"required":["tenantId","tenantStatus","wouldRevokeKeys","wouldRevoke","wouldRevokeCount","activeAdminKeyCountBefore","revokedAcrossEnvironments"]},"ReissueBootstrapKeyResponse":{"type":"object","properties":{"apiKey":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"key":{"type":"string","description":"The plaintext API key, returned EXACTLY ONCE. Only hash columns are persisted — this value is not recoverable after this response. Hand it over under the runbook §4.2 custody discipline; never paste it on a shell command line."},"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"lastFourChars":{"type":"string"},"environment":{"type":"string"}},"required":["id","key","name","scopes","lastFourChars","environment"]},"tenantId":{"type":"string","format":"uuid"},"requestId":{"type":"string"},"warnings":{"type":"object","properties":{"tenantStatus":{"type":"string"},"keyUnusableUntilReactivated":{"type":"boolean"},"revokedAcrossEnvironments":{"type":"boolean","description":"true when at least one revoked (or would-be-revoked) key carries a DIFFERENT environment than the request. Revocation is environment-agnostic by design."}},"required":["tenantStatus","keyUnusableUntilReactivated","revokedAcrossEnvironments"]},"revokedKeys":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"lastFourChars":{"type":"string"},"environment":{"type":"string"}},"required":["id","name","scopes","lastFourChars","environment"]}},"revokedByProvenance":{"type":"array","items":{"type":"string"}},"revokedByNameHeuristic":{"type":"array","items":{"type":"string"}},"revokedKeyCount":{"type":"integer"},"activeAdminKeyCountBefore":{"type":"integer"}},"required":["apiKey","tenantId","requestId","warnings","revokedKeys","revokedByProvenance","revokedByNameHeuristic","revokedKeyCount","activeAdminKeyCountBefore"]},"ReissueBootstrapKeyRequest":{"type":"object","properties":{"reason":{"type":"string","minLength":10,"maxLength":500,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","description":"Break-glass authorization justification. Written verbatim to an append-only, non-erasable audit row retained 1095 days — describe the SITUATION, never a person. No names, e-mail addresses, recipient addresses or message content. Prefer an externally verifiable reference (a coordination issue number, a ticket id) over prose: this field records the OPERATOR'S BELIEF that a request was legitimate, never that the tenant actually asked."},"revokePredecessors":{"type":"string","enum":["bootstrap","all-admin","none"],"description":"'bootstrap' — revoke the provenance-known bootstrap/break-glass predecessors (NOT guaranteed-narrow: api_keys.name is free-form tenant input, so read revokedByNameHeuristic). 'all-admin' — revoke EVERY active key carrying the admin scope (IRREVERSIBLE, and NOT incident containment — non-admin persistence keys survive). 'none' — revoke nothing (a genuine FIRST key for a pre-seeded tenant; reason MUST then state why)."},"environment":{"type":"string","enum":["live","test"],"default":"live"},"dryRun":{"type":"boolean","default":false,"description":"Preview only: returns 200 with the revocation set that WOULD be swept, mints nothing, revokes nothing, and writes one audit row with outcome=preview. Run this FIRST."},"drill":{"type":"boolean","default":false,"description":"AUDIT LABEL ONLY — this flag changes NOTHING about what the call does, and it does NOT imply dryRun. With dryRun:false (the default) it performs the full, IRREVERSIBLE mint and predecessor revocation. That is deliberate: a rehearsal that never exercised the mint path would leave the real path first-run during an incident. Rehearse on a NON-PRODUCTION tenant. Recorded as metadata.drill so a drill is distinguishable from a genuine break-glass, which the incident count an auditor samples and the detective signal both depend on. A drill on a PRODUCTION tenant is forbidden by the runbook."},"incidentRef":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","description":"The join key between this recovery action and the report that triggered it. Makes time-to-recovery computable and correlates the mint with a NIS2 / GDPR Art. 33 incident record."}},"required":["reason","revokePredecessors"],"additionalProperties":false},"AdminInvoiceResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenantId":{"type":"string","format":"uuid"},"period":{"type":"string"},"invoiceType":{"type":"string","enum":["period_close","topup"]},"status":{"type":"string","enum":["pending","emitted","emit_failed","paid","overdue","cancelled"]},"amountNetCents":{"type":"integer"},"amountGrossCents":{"type":"integer"},"currency":{"type":"string"},"sepaReference":{"type":"string"},"externalEventId":{"type":"string"},"dueDate":{"type":"string","nullable":true},"emittedAt":{"type":"string","nullable":true,"format":"date-time"},"paidAt":{"type":"string","nullable":true,"format":"date-time"},"dunningState":{"type":"string","enum":["none","reminder_1","reminder_2","final"]},"createdAt":{"type":"string","format":"date-time"}},"required":["id","tenantId","period","invoiceType","status","amountNetCents","amountGrossCents","currency","sepaReference","externalEventId","dueDate","emittedAt","paidAt","dunningState","createdAt"]},"InvoiceProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"suggestedFix":{"type":"string"},"docsUrl":{"type":"string"}},"required":["type","title","status","detail","instance","suggestedFix","docsUrl"]},"MarkInvoicePaidRequest":{"type":"object","properties":{"paidAt":{"type":"string","format":"date-time"},"bankReference":{"type":"string","minLength":1,"maxLength":140,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u"}},"required":["paidAt","bankReference"]},"TenantInvoiceResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"period":{"type":"string"},"invoiceType":{"type":"string","enum":["period_close","topup"]},"status":{"type":"string","enum":["pending","emitted","emit_failed","paid","overdue","cancelled"]},"amountNetCents":{"type":"integer"},"amountGrossCents":{"type":"integer"},"currency":{"type":"string"},"sepaReference":{"type":"string"},"dueDate":{"type":"string","nullable":true},"emittedAt":{"type":"string","nullable":true,"format":"date-time"},"paidAt":{"type":"string","nullable":true,"format":"date-time"},"dunningState":{"type":"string","enum":["none","reminder_1","reminder_2","final"]},"createdAt":{"type":"string","format":"date-time"}},"required":["id","period","invoiceType","status","amountNetCents","amountGrossCents","currency","sepaReference","dueDate","emittedAt","paidAt","dunningState","createdAt"]},"BillingProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"suggestedFix":{"type":"string"},"docsUrl":{"type":"string"}},"required":["type","title","status","detail","instance","suggestedFix","docsUrl"]},"UsageResponse":{"type":"object","properties":{"period":{"type":"string"},"usedVolume":{"type":"integer"},"account":{"$ref":"#/components/schemas/UsageAccount"}},"required":["period","usedVolume","account"]},"UsageAccount":{"type":"object","nullable":true,"properties":{"planTier":{"type":"string"},"includedVolume":{"type":"integer"},"usageCreditsEnabled":{"type":"boolean"},"creditBalance":{"type":"integer"}},"required":["planTier","includedVolume","usageCreditsEnabled","creditBalance"]},"UsageProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"suggestedFix":{"type":"string"},"docsUrl":{"type":"string"}},"required":["type","title","status","detail","instance","suggestedFix","docsUrl"]},"ScoreResponse":{"type":"object","properties":{"score":{"type":"integer","nullable":true},"calculatedAt":{"type":"string","nullable":true},"trend":{"type":"string","enum":["improving","stable","declining","insufficient_data"]},"provisional":{"type":"boolean"},"provisionalReason":{"type":"string","nullable":true,"enum":["no_sends_in_window","insufficient_data","domain_too_new",null]},"scoreDelta":{"type":"integer","nullable":true},"synthetic":{"type":"boolean"},"components":{"$ref":"#/components/schemas/ScoreApiComponents"},"ispBreakdown":{"type":"array","items":{"$ref":"#/components/schemas/IspBreakdown"}}},"required":["score","calculatedAt","trend","provisional","provisionalReason","scoreDelta","components","ispBreakdown"]},"ScoreApiComponents":{"type":"object","nullable":true,"properties":{"deliveryRate":{"type":"number","nullable":true},"complaintRate":{"type":"number","nullable":true},"bounceRate":{"type":"number","nullable":true},"authenticationHealth":{"type":"number","nullable":true},"blacklistStatus":{"type":"number","nullable":true},"synthetic":{"type":"boolean"}},"required":["deliveryRate","complaintRate","bounceRate","authenticationHealth","blacklistStatus"]},"IspBreakdown":{"type":"object","properties":{"isp":{"type":"string"},"score":{"type":"integer"},"volume":{"type":"integer"},"trend":{"type":"string","enum":["improving","stable","declining"]},"synthetic":{"type":"boolean"}},"required":["isp","score","volume","trend"]},"ScoreProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"code":{"type":"string"}},"required":["type","title","status","detail","instance"]},"ScoreHistoryEntry":{"type":"object","properties":{"date":{"type":"string"},"score":{"type":"integer","nullable":true},"deliveryRate":{"type":"number","nullable":true},"complaintRate":{"type":"number","nullable":true},"bounceRate":{"type":"number","nullable":true},"synthetic":{"type":"boolean"}},"required":["date","score","deliveryRate","complaintRate","bounceRate"]},"DeliverabilityScoreResponse":{"type":"object","properties":{"overallScore":{"type":"integer"},"confidence":{"type":"number","description":"Data sufficiency signal: 0.5=content-only, 0.75=limited history, 0.95=rich history"},"action":{"type":"string","enum":["send","review","hold"],"description":"Binary recommendation: send, review, or hold"},"ispBreakdown":{"type":"array","items":{"$ref":"#/components/schemas/IspScore"}},"riskFactors":{"type":"array","items":{"$ref":"#/components/schemas/RiskFactor"}},"recommendations":{"type":"array","items":{"type":"string"}},"scoredAt":{"type":"string"},"synthetic":{"type":"boolean"}},"required":["overallScore","confidence","action","ispBreakdown","riskFactors","recommendations","scoredAt"]},"IspScore":{"type":"object","properties":{"isp":{"type":"string"},"score":{"type":"integer"},"factors":{"type":"array","items":{"type":"string"}}},"required":["isp","score","factors"]},"RiskFactor":{"type":"object","properties":{"code":{"type":"string"},"severity":{"type":"string","enum":["critical","warning","info"]},"description":{"type":"string"},"penaltyPoints":{"type":"number"}},"required":["code","severity","description","penaltyPoints"]},"DeliverabilityScoreRequest":{"type":"object","properties":{"from":{"type":"string","format":"email"},"to":{"type":"array","items":{"type":"string","format":"email"},"minItems":1,"maxItems":10},"subject":{"type":"string"},"html":{"type":"string","maxLength":131072},"text":{"type":"string","maxLength":65536},"stream":{"type":"string"}},"required":["from","to","subject"]},"DeliveryMetricsResponse":{"type":"object","properties":{"period":{"type":"string"},"periodStart":{"type":"string"},"periodEnd":{"type":"string"},"totalDelivered":{"type":"integer"},"inboxDelivered":{"type":"integer"},"gatewayDelivered":{"type":"integer"},"unknownDelivered":{"type":"integer"},"inboxPlacementRate":{"type":"number"},"gatewayRate":{"type":"number"},"ispBreakdown":{"type":"array","items":{"$ref":"#/components/schemas/IspDeliveryBreakdown"}},"segBreakdown":{"type":"array","items":{"$ref":"#/components/schemas/SegBreakdown"}}},"required":["period","periodStart","periodEnd","totalDelivered","inboxDelivered","gatewayDelivered","unknownDelivered","inboxPlacementRate","gatewayRate","ispBreakdown","segBreakdown"]},"IspDeliveryBreakdown":{"type":"object","properties":{"isp":{"type":"string"},"totalDelivered":{"type":"integer"},"inboxDelivered":{"type":"integer"},"gatewayDelivered":{"type":"integer"},"unknownDelivered":{"type":"integer"},"inboxPlacementRate":{"type":"number"}},"required":["isp","totalDelivered","inboxDelivered","gatewayDelivered","unknownDelivered","inboxPlacementRate"]},"SegBreakdown":{"type":"object","properties":{"provider":{"type":"string"},"gatewayDelivered":{"type":"integer"}},"required":["provider","gatewayDelivered"]},"DeliveryMetricsProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"code":{"type":"string"}},"required":["type","title","status","detail","instance"]},"RunbookEntry":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"triggeredBy":{"type":"array","items":{"type":"string"}},"severity":{"type":"string","enum":["info","warning","critical"]},"estimatedDuration":{"type":"string"},"stepCount":{"type":"integer"}},"required":["id","title","description","triggeredBy","severity","estimatedDuration","stepCount"]},"RunbookProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"code":{"type":"string"}},"required":["type","title","status","detail","instance"]},"RunbookExecuteResponse":{"type":"object","properties":{"runbookId":{"type":"string"},"tenantId":{"type":"string"},"executedAt":{"type":"string"},"steps":{"type":"array","items":{"$ref":"#/components/schemas/RunbookStep"}},"findings":{"type":"array","items":{"$ref":"#/components/schemas/RunbookFinding"}},"recommendations":{"type":"array","items":{"$ref":"#/components/schemas/RunbookRecommendation"}}},"required":["runbookId","tenantId","executedAt","steps","findings","recommendations"]},"RunbookStep":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","enum":["pass","warn","fail"]},"detail":{"type":"string"},"data":{"type":"object","additionalProperties":{"nullable":true}}},"required":["id","name","status","detail"]},"RunbookFinding":{"type":"object","properties":{"severity":{"type":"string","enum":["info","warning","critical"]},"message":{"type":"string"}},"required":["severity","message"]},"RunbookRecommendation":{"type":"object","properties":{"priority":{"type":"integer"},"action":{"type":"string"},"docsUrl":{"type":"string"}},"required":["priority","action"]},"RunbookExecuteBody":{"type":"object","properties":{"tenantId":{"type":"string","format":"uuid"}},"required":["tenantId"]},"TeamMember":{"type":"object","properties":{"memberId":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["owner","developer","dpo","viewer"]},"status":{"type":"string","enum":["invited","active","suspended"]},"invitedAt":{"type":"string"},"joinedAt":{"type":"string","nullable":true},"lastActiveAt":{"type":"string","nullable":true}},"required":["memberId","email","role","status","invitedAt","joinedAt","lastActiveAt"]},"TeamProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"code":{"type":"string"}},"required":["type","title","status","detail","instance"]},"InviteMemberBody":{"type":"object","properties":{"email":{"type":"string","maxLength":255,"format":"email"},"role":{"type":"string","enum":["owner","developer","dpo","viewer"]}},"required":["email","role"]},"TeamMemberList":{"type":"array","items":{"$ref":"#/components/schemas/TeamMember"}},"UpdateMemberBody":{"type":"object","properties":{"role":{"type":"string","enum":["owner","developer","dpo","viewer"]}},"required":["role"]},"SandboxMessagesResponse":{"type":"object","properties":{"messages":{"type":"array","items":{"$ref":"#/components/schemas/SandboxMessage"}},"meta":{"type":"object","properties":{"total":{"type":"integer"},"quotaUsed":{"type":"integer"},"quotaMax":{"type":"integer"}},"required":["total","quotaUsed","quotaMax"]},"nextCursor":{"type":"string","nullable":true}},"required":["messages","meta","nextCursor"]},"SandboxMessage":{"type":"object","properties":{"messageId":{"type":"string","format":"uuid"},"recipientDomain":{"type":"string"},"from":{"type":"string"},"subject":{"type":"string","nullable":true},"status":{"type":"string"},"sentAt":{"type":"string","format":"date-time"},"deliveredAt":{"type":"string","nullable":true,"format":"date-time"}},"required":["messageId","recipientDomain","from","subject","status","sentAt","deliveredAt"]},"SandboxProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"code":{"type":"string"}},"required":["type","title","status","detail","instance"]},"SandboxDeleteResponse":{"type":"object","properties":{"deleted":{"type":"integer"}},"required":["deleted"]},"SendGridSendRequest":{"type":"object","properties":{"personalizations":{"type":"array","items":{"type":"object","properties":{"to":{"type":"array","items":{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email"},"name":{"type":"string"}},"required":["email"]},"minItems":1,"maxItems":1},"dynamic_template_data":{"type":"object","additionalProperties":{"type":"string"}}},"required":["to"]},"minItems":1,"maxItems":1000},"from":{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email"},"name":{"type":"string"}},"required":["email"]},"subject":{"type":"string","minLength":1},"content":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"value":{"type":"string"}},"required":["type","value"]}},"custom_args":{"type":"object","additionalProperties":{"type":"string"}},"categories":{"type":"array","items":{"type":"string"}},"template_id":{"type":"string"},"asm":{"type":"object","properties":{"group_id":{"type":"number"},"groups_to_display":{"type":"array","items":{"type":"number"}}}},"mail_settings":{"nullable":true},"tracking_settings":{"nullable":true},"ip_pool_name":{"type":"string"},"reply_to":{"nullable":true},"send_at":{"type":"number"},"batch_id":{"type":"string"},"sections":{"nullable":true},"headers":{"type":"object","additionalProperties":{"type":"string"}},"substitutions":{"nullable":true}},"required":["personalizations","from","subject"]},"PostmarkSendRequest":{"type":"object","properties":{"To":{"type":"string","maxLength":254},"From":{"type":"string","maxLength":254},"Subject":{"type":"string"},"HtmlBody":{"type":"string"},"TextBody":{"type":"string"},"Tag":{"type":"string"},"Metadata":{"type":"object","additionalProperties":{"nullable":true}},"Headers":{"type":"array","items":{"type":"object","properties":{"Name":{"type":"string"},"Value":{"type":"string"}},"required":["Name","Value"]}},"ReplyTo":{"type":"string"},"TemplateId":{"anyOf":[{"type":"string"},{"type":"number"}]},"TemplateAlias":{"type":"string"},"TemplateModel":{"nullable":true},"TrackOpens":{"type":"boolean"},"TrackLinks":{"type":"string"},"Attachments":{"nullable":true},"Bcc":{"type":"string"},"Cc":{"type":"string"},"MessageStream":{"type":"string"}},"required":["To","From","Subject"]},"MailgunSendRequest":{"type":"object","properties":{},"additionalProperties":{"nullable":true}},"MigrationDiffResponse":{"type":"object","properties":{"diff":{"type":"string"},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/MigrationWarning"}},"complexity":{"type":"integer","minimum":0,"maximum":5},"detectedEsp":{"type":"string","enum":["sendgrid","postmark","mailgun","resend","ses","nodemailer"]},"message":{"type":"string"},"suggestions":{"type":"array","items":{"type":"string"}}},"required":["diff","warnings","complexity"]},"MigrationWarning":{"type":"object","properties":{"feature":{"type":"string"},"docsUrl":{"type":"string"},"severity":{"type":"string","enum":["blocker","warning","info"]}},"required":["feature","docsUrl","severity"]},"ToolsProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"suggestedFix":{"type":"string"},"docsUrl":{"type":"string"}},"required":["type","title","status","detail","instance"]},"MigrationDiffRequest":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":100000},"sourceEsp":{"type":"string","enum":["sendgrid","postmark","mailgun","resend","ses","nodemailer"]}},"required":["code"]},"WeatherReport":{"type":"object","properties":{"domain":{"type":"string"},"generatedAt":{"type":"string"},"ispConditions":{"type":"array","items":{"$ref":"#/components/schemas/ISPCondition"}},"overallOutlook":{"type":"string","enum":["favorable","neutral","degraded","critical"]},"dataConfidence":{"type":"string","enum":["none","low","medium","high"]}},"required":["domain","generatedAt","ispConditions","overallOutlook","dataConfidence"]},"ISPCondition":{"type":"object","properties":{"isp":{"type":"string"},"inboxRate":{"type":"number","nullable":true},"deferRate":{"type":"number","nullable":true},"blockRate":{"type":"number","nullable":true},"softBounceRate":{"type":"number","nullable":true},"spamRate":{"type":"number","nullable":true},"trendDirection":{"type":"string","enum":["improving","stable","declining","unknown"]},"sampleSize":{"type":"number"},"notes":{"type":"array","items":{"type":"string"}}},"required":["isp","inboxRate","deferRate","blockRate","softBounceRate","spamRate","trendDirection","sampleSize"]},"WeatherProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"code":{"type":"string"}},"required":["type","title","status","detail","instance"]},"DisposableCheckResult":{"type":"object","properties":{"email":{"type":"string"},"isDisposable":{"type":"boolean"},"confidence":{"type":"string","enum":["high","medium","low"]},"disposableScore":{"type":"number"},"provider":{"type":"string"},"checkedAt":{"type":"string"}},"required":["email","isDisposable","confidence","checkedAt"]},"DisposableProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"suggestedFix":{"type":"string"},"docsUrl":{"type":"string"}},"required":["type","title","status","detail","instance"]},"CheckDisposableRequest":{"type":"object","properties":{"email":{"type":"string","format":"email"}},"required":["email"]},"CheckDisposableBatchRequest":{"type":"object","properties":{"emails":{"type":"array","items":{"type":"string","format":"email"},"maxItems":100}},"required":["emails"]},"JurisdictionResult":{"type":"object","properties":{"email":{"type":"string"},"jurisdiction":{"type":"string"},"region":{"type":"string"},"detectionMethod":{"type":"string","enum":["domain_tld","mx_geolocation","recipient_declared","unknown"]},"confidence":{"type":"string","enum":["high","medium","low"]},"applicableFrameworks":{"type":"array","items":{"type":"string"}},"detectedAt":{"type":"string"}},"required":["email","jurisdiction","region","detectionMethod","confidence","applicableFrameworks","detectedAt"]},"JurisdictionProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"suggestedFix":{"type":"string"},"docsUrl":{"type":"string"}},"required":["type","title","status","detail","instance"]},"JurisdictionBatchResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/JurisdictionResult"}}},"required":["data"]},"JurisdictionBatchRequest":{"type":"object","properties":{"emails":{"type":"array","items":{"type":"string","maxLength":254,"format":"email"},"minItems":1}},"required":["emails"]},"ComplianceFramework":{"type":"object","properties":{"name":{"type":"string"},"jurisdiction":{"type":"string"},"requirements":{"type":"array","items":{"type":"object","properties":{"requirement":{"type":"string"},"description":{"type":"string"},"mandatory":{"type":"boolean"}},"required":["requirement","description","mandatory"]}},"consentRequired":{"type":"boolean"},"unsubscribeMandatory":{"type":"boolean"},"dataRetentionMaxDays":{"type":"number"},"disclosureRequirements":{"type":"array","items":{"type":"string"}},"regulatoryNotes":{"type":"array","items":{"type":"string"}},"version":{"type":"string"}},"required":["name","jurisdiction","requirements","consentRequired","unsubscribeMandatory","disclosureRequirements"]},"ReputationForecast":{"type":"object","properties":{"domain":{"type":"string"},"generatedAt":{"type":"string","format":"date-time"},"currentScore":{"type":"number","minimum":0,"maximum":100},"trajectory":{"type":"array","items":{"$ref":"#/components/schemas/DailyForecast"}},"overallTrend":{"type":"string","enum":["improving","stable","declining","critical"]},"riskLevel":{"type":"string","enum":["healthy","at-risk","critical"]},"recommendations":{"type":"array","items":{"$ref":"#/components/schemas/ForecastRecommendation"}},"confidenceLevel":{"type":"string","enum":["high","medium","low"]}},"required":["domain","generatedAt","currentScore","trajectory","overallTrend","riskLevel","recommendations","confidenceLevel"]},"DailyForecast":{"type":"object","properties":{"date":{"type":"string"},"predictedScore":{"type":"number","minimum":0,"maximum":100},"lowerBound":{"type":"number","minimum":0,"maximum":100},"upperBound":{"type":"number","minimum":0,"maximum":100}},"required":["date","predictedScore","lowerBound","upperBound"]},"ForecastRecommendation":{"type":"object","properties":{"priority":{"type":"string","enum":["critical","high","medium","low"]},"category":{"type":"string","enum":["volume","content","authentication","engagement"]},"title":{"type":"string"},"description":{"type":"string"},"expectedImpact":{"type":"string"}},"required":["priority","category","title","description","expectedImpact"]},"ForecastProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"code":{"type":"string"}},"required":["type","title","status","detail","instance"]},"BimiRecord":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenantId":{"type":"string","format":"uuid"},"domain":{"type":"string"},"logoUrl":{"type":"string"},"vmcType":{"type":"string","enum":["none","vmc","cmc"]},"vmcExpiry":{"type":"string","nullable":true,"format":"date-time"},"vmcStatus":{"type":"string","enum":["none","active","expired","revoked"]},"hostedVmcUrl":{"type":"string","nullable":true},"dnsPublished":{"type":"boolean"},"dnsPublishedAt":{"type":"string","nullable":true,"format":"date-time"},"dnsTtl":{"type":"integer"},"ocspCheckedAt":{"type":"string","nullable":true,"format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","tenantId","domain","logoUrl","vmcType","vmcExpiry","vmcStatus","hostedVmcUrl","dnsPublished","dnsPublishedAt","dnsTtl","ocspCheckedAt","createdAt","updatedAt"]},"BimiProblemDetails":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"number"},"detail":{"type":"string"},"instance":{"type":"string"},"suggestedFix":{"type":"string"},"docsUrl":{"type":"string"}},"required":["type","title","status","detail","instance"]},"PublishBimiRequest":{"type":"object","properties":{"logoUrl":{"type":"string","format":"uri","example":"https://example.com/logo.svg"}},"required":["logoUrl"]},"UpdateVmcRequest":{"type":"object","properties":{"vmcPem":{"type":"string","minLength":1,"description":"PEM-encoded VMC/CMC certificate"},"vmcType":{"type":"string","enum":["vmc","cmc"],"example":"vmc"}},"required":["vmcPem","vmcType"]},"BimiStatusResponse":{"type":"object","properties":{"record":{"$ref":"#/components/schemas/BimiRecord"},"readiness":{"$ref":"#/components/schemas/BimiReadinessScore"}},"required":["record","readiness"]},"BimiReadinessScore":{"type":"object","properties":{"score":{"type":"integer","minimum":0,"maximum":100},"dkimConfigured":{"type":"boolean"},"dmarcEnforced":{"type":"boolean"},"dnsPublishedDays":{"type":"number","nullable":true},"maturationDaysRemaining":{"type":"number","nullable":true},"vmcStatus":{"type":"string","enum":["none","active","expired","revoked"]},"caveats":{"type":"array","items":{"type":"string"}}},"required":["score","dkimConfigured","dmarcEnforced","dnsPublishedDays","maturationDaysRemaining","vmcStatus","caveats"]}},"parameters":{}},"paths":{"/v1/send":{"post":{"tags":["Email"],"summary":"Send an email","description":"Submit an email for asynchronous delivery. Returns a messageId for tracking.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendRequest"}}}},"responses":{"202":{"description":"Email accepted for delivery","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"402":{"description":"Billing allowance exhausted for the current period (Story 14-3). priority:critical mail is never blocked here — it delivers and is billed as overage.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"422":{"description":"Unprocessable entity (suppressed recipient, unverified domain, or sandbox address with production key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/domains":{"post":{"tags":["Domains"],"summary":"Add a sending domain","description":"Register a new sending domain for the tenant. Returns DNS records to configure.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainCreateRequest"}}}},"responses":{"201":{"description":"Domain created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"409":{"description":"Domain already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}},"get":{"tags":["Domains"],"summary":"List this tenant's sending domains (stored verification state — no DNS lookups)","parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","maxLength":255},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"A bounded page of sending domains, newest first. End-of-collection is signalled ONLY by nextCursor: null — a short page does not mean the end.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DomainListItem"}},"pagination":{"type":"object","properties":{"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}},"required":["nextCursor","hasMore"]}},"required":["data","pagination"]}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"Insufficient scope (domains:read)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"422":{"description":"Invalid cursor","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/domains/{domainId}/verify":{"get":{"tags":["Domains"],"summary":"Trigger DNS verification for a domain","description":"Triggers DNS verification checks for SPF, DKIM CNAME, and DMARC records.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"domainId","in":"path"}],"responses":{"200":{"description":"Verification result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainVerifyResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"Domain not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"422":{"description":"Invalid status transition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"503":{"description":"DNS_LOOKUP_INCONCLUSIVE — the SPF or DKIM lookup COULD NOT BE OBSERVED (resolver error, SERVFAIL, or the 3 s per-lookup budget), as distinct from observing that the record is absent. No verification verdict is written and the domain keeps its previous status, so a transient resolver failure cannot demote a `verified` domain to `failed` (which would stop sending — send-core gates on status === verified). RETRYABLE; carries Retry-After. [prep-m2-tenant-dmarc-absent-precondition code review, D-03]","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/domains/{domainId}/dkim/rotation-status":{"get":{"tags":["Domains"],"summary":"Get DKIM rotation status for a domain","description":"Returns the current DKIM rotation status for the active key on this domain, including selector, FSM state, and scheduled rotation date. Use to verify rotation progress without triggering side-effects. AC 19.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"domainId","in":"path"}],"responses":{"200":{"description":"DKIM rotation status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DkimRotationStatusResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"Domain not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/warmup":{"get":{"tags":["Warmup"],"summary":"List warmup ramp state for every domain of the authenticated tenant","description":"Returns each domain's warmup ramp state (day / current ceiling / per-ISP velocity / active pauses). A tenant with no verified domains returns { warmups: [] } (200, not an error).","responses":{"200":{"description":"Warmup ramp state per domain (possibly empty)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WarmupListResponse"}}}},"401":{"description":"Authentication required","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"Insufficient scope (domains:read required)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"500":{"description":"Unexpected error listing warmup state","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/warmup/{domain}":{"get":{"tags":["Warmup"],"summary":"Get warmup ramp state for a single domain (panel drill-down)","description":"Returns the warmup ramp state for one domain. A domain with no warmup row, OR a domain belonging to another tenant, both return the SAME 404 (cross-tenant = 404, never 403 — no enumeration).","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":253,"pattern":"^[A-Za-z0-9.-]+$","example":"example.com"},"required":true,"name":"domain","in":"path"}],"responses":{"200":{"description":"Warmup ramp state for the domain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WarmupProgressResponse"}}}},"401":{"description":"Authentication required","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"Insufficient scope (domains:read required)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"No warmup for this domain (or the domain belongs to another tenant) — WARMUP_NOT_FOUND","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/api-keys":{"post":{"tags":["API Keys"],"summary":"Create an API key","description":"Creates a new API key with specified scopes. The plaintext key is returned ONCE.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreateRequest"}}}},"responses":{"201":{"description":"API key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreateResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"409":{"description":"API key name already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}},"get":{"tags":["API Keys"],"summary":"List API keys","description":"Lists all API keys for the authenticated tenant. Never returns the full key.","parameters":[{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"number","minimum":1,"maximum":100,"default":20},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"List of API keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyListResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/api-keys/{keyId}":{"delete":{"tags":["API Keys"],"summary":"Revoke an API key","description":"Permanently revokes an API key. Takes effect immediately.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"keyId","in":"path"}],"responses":{"204":{"description":"API key revoked"},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"API key not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/streams":{"post":{"tags":["Streams"],"summary":"Create a message stream","description":"Creates a named message stream for routing emails into separate reputation profiles.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamCreateRequest"}}}},"responses":{"201":{"description":"Stream created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamDetail"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}},"409":{"description":"Stream name already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}},"422":{"description":"Stream limit reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}}}},"get":{"tags":["Streams"],"summary":"List message streams","description":"Lists all message streams for the authenticated tenant.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"number","minimum":1,"maximum":50,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"List of streams","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamListResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}}}}},"/v1/streams/{streamId}":{"get":{"tags":["Streams"],"summary":"Get stream details","description":"Returns stream details including message count and last activity.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"streamId","in":"path"}],"responses":{"200":{"description":"Stream details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamDetail"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}},"404":{"description":"Stream not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}}}},"patch":{"tags":["Streams"],"summary":"Update a message stream","description":"Updates a stream description. Name is immutable after creation.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"streamId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamUpdateRequest"}}}},"responses":{"200":{"description":"Stream updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamDetail"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}},"404":{"description":"Stream not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}}}},"delete":{"tags":["Streams"],"summary":"Delete a message stream","description":"Soft-deletes a stream. Cannot delete streams with messages or the default stream.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"streamId","in":"path"}],"responses":{"204":{"description":"Stream deleted"},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}},"404":{"description":"Stream not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}},"422":{"description":"Cannot delete stream","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamProblemDetails"}}}}}}},"/v1/webhooks":{"post":{"tags":["Webhooks"],"summary":"Create a webhook endpoint","description":"Registers a webhook endpoint with challenge-response URL ownership verification.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreateRequest"}}}},"responses":{"201":{"description":"Webhook created — signing secret shown once","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookConfigWithSecret"}}}},"400":{"description":"Validation error / SSRF / HTTPS required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"409":{"description":"Duplicate URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"422":{"description":"Webhook limit reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"500":{"description":"Internal error (encryption failure)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}}}},"get":{"tags":["Webhooks"],"summary":"List webhook endpoints","description":"Lists all webhook endpoints for the authenticated tenant.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"number","minimum":1,"maximum":20,"default":20},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"List of webhook configs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookListResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}}}}},"/v1/webhooks/{webhookId}":{"get":{"tags":["Webhooks"],"summary":"Get webhook endpoint details","description":"Returns webhook config details. Signing secret is never returned.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"webhookId","in":"path"}],"responses":{"200":{"description":"Webhook config details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookConfig"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"404":{"description":"Webhook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}}}},"patch":{"tags":["Webhooks"],"summary":"Update a webhook endpoint","description":"Updates events or description. URL is immutable (delete and re-create to change).","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"webhookId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookUpdateRequest"}}}},"responses":{"200":{"description":"Webhook updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookConfig"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"404":{"description":"Webhook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}}}},"delete":{"tags":["Webhooks"],"summary":"Delete a webhook endpoint","description":"Soft-deletes a webhook endpoint. No further events will be dispatched.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"webhookId","in":"path"}],"responses":{"204":{"description":"Webhook deleted"},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"404":{"description":"Webhook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}}}}},"/v1/webhooks/{webhookId}/verify":{"post":{"tags":["Webhooks"],"summary":"Retry webhook verification","description":"Retries challenge-response verification for a pending webhook endpoint.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"webhookId","in":"path"}],"responses":{"202":{"description":"Verification initiated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookMessage"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"404":{"description":"Webhook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"422":{"description":"Invalid state for verification","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}}}}},"/v1/webhooks/{webhookId}/rotate-secret":{"post":{"tags":["Webhooks"],"summary":"Rotate webhook signing secret","description":"Generates a new signing secret with 24-hour overlap for the old secret.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"webhookId","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RotateSecretRequest"}}}},"responses":{"200":{"description":"Secret rotated — new signing secret shown once","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookConfigWithSecret"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"404":{"description":"Webhook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"422":{"description":"Only active webhooks can rotate secrets","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"500":{"description":"Internal error (encryption failure)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}}}}},"/v1/webhooks/{webhookId}/deliveries":{"get":{"tags":["Webhooks"],"summary":"List webhook delivery attempts","description":"Returns delivery attempt history for a webhook endpoint with cursor pagination.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"webhookId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"number","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"List of webhook delivery attempts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryListResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}},"404":{"description":"Webhook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookProblemDetails"}}}}}}},"/v1/messages":{"get":{"tags":["Messages"],"summary":"List recent messages (metadata only — no content, recipient as domain + hash)","parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","maxLength":255},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"A bounded page of messages, newest first. End-of-collection is signalled ONLY by nextCursor: null — a short page does not mean the end.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MessageListItem"}},"pagination":{"type":"object","properties":{"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}},"required":["nextCursor","hasMore"]}},"required":["data","pagination"]}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageProblemDetails"}}}},"403":{"description":"Insufficient scope (messages:read)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageProblemDetails"}}}},"422":{"description":"Invalid cursor","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/MessageProblemDetails"}}}}}}},"/v1/messages/{messageId}/explain":{"get":{"tags":["Messages"],"summary":"Get delivery decision tree for a message","description":"Returns a human-readable explanation of every routing, authentication, and disposition decision for a sent message.\n\n**Examples:**\n\n**Delivered happy path:**\n```json\n{\"schemaVersion\":1,\"messageId\":\"<uuid>\",\"stream\":\"transactional\",\"finalStatus\":\"delivered\",\"decisions\":[{\"step\":1,\"action\":\"authentication\",\"component\":\"ingest\",\"description\":\"API key verified\",\"result\":\"pass\",\"severity\":\"info\",\"durationMs\":2,\"timestamp\":\"2024-01-01T00:00:00.000Z\"}],\"rootCause\":null,\"summary\":\"Message delivered successfully to gmail.com via primary MTA\",\"smtpResponse\":{\"code\":250,\"enhancedCode\":\"2.0.0\"}}\n```\n\n**Bounced with rootCause:**\n```json\n{\"schemaVersion\":1,\"messageId\":\"<uuid>\",\"stream\":\"transactional\",\"finalStatus\":\"bounced\",\"decisions\":[{\"step\":1,\"action\":\"authentication\",\"component\":\"ingest\",\"description\":\"API key verified\",\"result\":\"pass\",\"severity\":\"info\",\"durationMs\":1,\"timestamp\":\"2024-01-01T00:00:00.000Z\"},{\"step\":2,\"action\":\"delivery\",\"component\":\"delivery\",\"description\":\"550 5.1.1 user does not exist\",\"result\":\"fail\",\"severity\":\"critical\",\"durationMs\":120,\"timestamp\":\"2024-01-01T00:00:00.100Z\",\"suggestedAction\":\"Verify recipient email address\"}],\"rootCause\":{\"step\":2,\"action\":\"delivery\",\"description\":\"550 5.1.1 user does not exist\",\"suggestedAction\":\"Verify recipient email address\"},\"summary\":\"Message bounced: 550 5.1.1 user does not exist\",\"smtpResponse\":{\"code\":550,\"enhancedCode\":\"5.1.1\"}}\n```\n\n**Pre-2.5a migration (no steps):**\n```json\n{\"schemaVersion\":1,\"messageId\":\"<uuid>\",\"stream\":\"transactional\",\"finalStatus\":\"delivered\",\"decisions\":[],\"rootCause\":null,\"summary\":\"Decision tree not available for messages sent before this feature was enabled\",\"smtpResponse\":null}\n```","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"messageId","in":"path"}],"responses":{"200":{"description":"Decision tree for the message","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExplainDecisionTreeResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageProblemDetails"}}}},"404":{"description":"Message not found or access denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageProblemDetails"}}}}}}},"/v1/messages/{messageId}":{"get":{"tags":["Messages"],"summary":"Get message details","description":"Returns message metadata. Use `?expand[]=explain` to include the full decision tree inline.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"messageId","in":"path"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"expand[]","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"expand","in":"query"}],"responses":{"200":{"description":"Message details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageProblemDetails"}}}},"404":{"description":"Message not found or access denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageProblemDetails"}}}}}}},"/v1/compliance/suppressions":{"post":{"tags":["Compliance"],"summary":"Add manual suppression entry","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuppressionAddRequest"}}}},"responses":{"201":{"description":"Suppression entry created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuppressionEntryResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"503":{"description":"Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}},"get":{"tags":["Compliance"],"summary":"List suppression entries","parameters":[{"schema":{"type":"integer","minimum":1,"default":1},"required":false,"name":"page","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Paginated suppression list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuppressionListResponse"}}}},"503":{"description":"Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/suppressions/{id}":{"delete":{"tags":["Compliance"],"summary":"Delete manual suppression entry","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Suppression entry deleted"},"404":{"description":"Suppression entry not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"409":{"description":"Cannot delete immutable suppression reason","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/erasure":{"post":{"tags":["Compliance"],"summary":"GDPR Art.17 erasure request","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErasureRequest"}}}},"responses":{"200":{"description":"Erasure proof","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErasureProofResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"503":{"description":"Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/retention":{"put":{"tags":["Compliance"],"summary":"Create or update retention policy for a stream","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionPolicyRequest"}}}},"responses":{"200":{"description":"Policy upserted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionPolicyResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"500":{"description":"Internal error (upsert returned no row)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}},"get":{"tags":["Compliance"],"summary":"List all retention policies for the tenant","responses":{"200":{"description":"Retention policies","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionPoliciesListResponse"}}}}}}},"/v1/compliance/art30":{"get":{"tags":["Compliance"],"summary":"Export Art. 30 GDPR processing register (FR23)","parameters":[{"schema":{"type":"string","format":"date-time"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string","format":"date-time"},"required":false,"name":"to","in":"query"},{"schema":{"type":"string","enum":["json","csv"],"default":"json"},"required":false,"name":"format","in":"query"},{"schema":{"type":"string","enum":["en","pl"]},"required":false,"name":"locale","in":"query"}],"responses":{"200":{"description":"Art. 30 register export (JSON or CSV)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Art30ExportResponse"}},"text/csv":{"schema":{"type":"string"}}}},"400":{"description":"Invalid date range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"404":{"description":"Tenant not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/dpa":{"get":{"tags":["Compliance"],"summary":"Generate GDPR Art.28 Data Processing Agreement on demand (FR24)","parameters":[{"schema":{"type":"string"},"required":false,"name":"locale","in":"query"},{"schema":{"type":"string","enum":["md","pdf"],"default":"md"},"required":false,"name":"format","in":"query"}],"responses":{"200":{"description":"DPA document (Markdown or PDF)","content":{"text/markdown":{"schema":{"type":"string"}},"application/pdf":{"schema":{"type":"string","format":"binary","description":"PDF binary content"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"404":{"description":"Tenant not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/passport":{"get":{"tags":["Compliance"],"summary":"Compliance Passport — point-in-time compliance state summary (FR25)","responses":{"200":{"description":"Compliance Passport JSON","content":{"application/json":{"schema":{"type":"object","properties":{"passportVersion":{"type":"string"},"tenantId":{"type":"string"},"generatedAt":{"type":"string"},"subprocessorChain":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"purpose":{"type":"string"},"location":{"type":"string"},"dpaStatus":{"type":"string","enum":["active","pending","not_required"]}},"required":["name","purpose","location","dpaStatus"]}},"dpaStatus":{"type":"object","properties":{"generated":{"type":"boolean"},"lastGeneratedAt":{"type":"string","nullable":true},"version":{"type":"string","nullable":true}},"required":["generated","lastGeneratedAt","version"]},"dkimHealth":{"type":"object","properties":{"domains":{"type":"array","items":{"type":"object","properties":{"domain":{"type":"string"},"status":{"type":"string","enum":["active","rotating","expired"]},"selector":{"type":"string"},"algorithm":{"type":"string"},"lastRotatedAt":{"type":"string"},"nextRotationAt":{"type":"string","nullable":true}},"required":["domain","status","selector","algorithm","lastRotatedAt","nextRotationAt"]}}},"required":["domains"]},"securityPosture":{"type":"object","properties":{"encryptionAtRest":{"type":"boolean"},"encryptionInTransit":{"type":"boolean"},"mtaStsEnforced":{"type":"boolean"},"arcSigningEnabled":{"type":"boolean"},"bimiConfigured":{"type":"boolean"}},"required":["encryptionAtRest","encryptionInTransit","mtaStsEnforced","arcSigningEnabled","bimiConfigured"]},"retentionCompliance":{"type":"object","properties":{"policiesConfigured":{"type":"boolean"},"defaultRetentionDays":{"type":"number"},"complianceLogRetentionDays":{"type":"number"}},"required":["policiesConfigured","defaultRetentionDays","complianceLogRetentionDays"]},"overallStatus":{"type":"string","enum":["healthy","warning","critical"]},"requiredActions":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string","enum":["high","medium","low"]},"action":{"type":"string"}},"required":["severity","action"]}}},"required":["passportVersion","tenantId","generatedAt","subprocessorChain","dpaStatus","dkimHealth","securityPosture","retentionCompliance","overallStatus","requiredActions"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"404":{"description":"Tenant not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/audit-package":{"get":{"tags":["Compliance"],"summary":"One-Click Audit Package — ZIP or JSON bundle of all compliance documents (FR26)","parameters":[{"schema":{"type":"string","enum":["en","pl"],"default":"en"},"required":false,"name":"locale","in":"query"},{"schema":{"type":"string","enum":["zip","json"],"default":"json"},"required":false,"name":"format","in":"query"},{"schema":{"type":"string","format":"date-time"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string","format":"date-time"},"required":false,"name":"to","in":"query"}],"responses":{"200":{"description":"Audit Package — JSON object (format=json) or binary ZIP archive (format=zip). ZIP always contains all 6 files; failed components carry {\"status\":\"error\",\"error\":\"...\"} content.","content":{"application/json":{"schema":{"type":"object","properties":{"packageVersion":{"type":"string","enum":["1.0"]},"tenantId":{"type":"string"},"generatedAt":{"type":"string"},"fromDate":{"type":"string","nullable":true},"toDate":{"type":"string"},"locale":{"type":"string","enum":["en","pl"]},"status":{"type":"string","enum":["complete","partial"]},"components":{"type":"object","properties":{"art30Register":{"oneOf":[{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"data":{"type":"object","properties":{},"additionalProperties":{"nullable":true}}},"required":["status","data"]},{"type":"object","properties":{"status":{"type":"string","enum":["error"]},"error":{"type":"string"}},"required":["status","error"]}]},"dpa":{"oneOf":[{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"data":{"type":"object","properties":{},"additionalProperties":{"nullable":true}}},"required":["status","data"]},{"type":"object","properties":{"status":{"type":"string","enum":["error"]},"error":{"type":"string"}},"required":["status","error"]}]},"compliancePassport":{"oneOf":[{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"data":{"type":"object","properties":{"passportVersion":{"type":"string"},"tenantId":{"type":"string"},"generatedAt":{"type":"string"},"subprocessorChain":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"purpose":{"type":"string"},"location":{"type":"string"},"dpaStatus":{"type":"string","enum":["active","pending","not_required"]}},"required":["name","purpose","location","dpaStatus"]}},"dpaStatus":{"type":"object","properties":{"generated":{"type":"boolean"},"lastGeneratedAt":{"type":"string","nullable":true},"version":{"type":"string","nullable":true}},"required":["generated","lastGeneratedAt","version"]},"dkimHealth":{"type":"object","properties":{"domains":{"type":"array","items":{"type":"object","properties":{"domain":{"type":"string"},"status":{"type":"string","enum":["active","rotating","expired"]},"selector":{"type":"string"},"algorithm":{"type":"string"},"lastRotatedAt":{"type":"string"},"nextRotationAt":{"type":"string","nullable":true}},"required":["domain","status","selector","algorithm","lastRotatedAt","nextRotationAt"]}}},"required":["domains"]},"securityPosture":{"type":"object","properties":{"encryptionAtRest":{"type":"boolean"},"encryptionInTransit":{"type":"boolean"},"mtaStsEnforced":{"type":"boolean"},"arcSigningEnabled":{"type":"boolean"},"bimiConfigured":{"type":"boolean"}},"required":["encryptionAtRest","encryptionInTransit","mtaStsEnforced","arcSigningEnabled","bimiConfigured"]},"retentionCompliance":{"type":"object","properties":{"policiesConfigured":{"type":"boolean"},"defaultRetentionDays":{"type":"number"},"complianceLogRetentionDays":{"type":"number"}},"required":["policiesConfigured","defaultRetentionDays","complianceLogRetentionDays"]},"overallStatus":{"type":"string","enum":["healthy","warning","critical"]},"requiredActions":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string","enum":["high","medium","low"]},"action":{"type":"string"}},"required":["severity","action"]}}},"required":["passportVersion","tenantId","generatedAt","subprocessorChain","dpaStatus","dkimHealth","securityPosture","retentionCompliance","overallStatus","requiredActions"]}},"required":["status","data"]},{"type":"object","properties":{"status":{"type":"string","enum":["error"]},"error":{"type":"string"}},"required":["status","error"]}]},"deliveryStatistics":{"oneOf":[{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"data":{"type":"object","properties":{"generatedAt":{"type":"string"},"fromDate":{"type":"string","nullable":true},"toDate":{"type":"string"},"entries":{"type":"array","items":{"type":"object","properties":{"year":{"type":"number"},"month":{"type":"number"},"stream":{"type":"string"},"totalSent":{"type":"number"},"delivered":{"type":"number"},"bounced":{"type":"number"},"complained":{"type":"number"},"deferred":{"type":"number"}},"required":["year","month","stream","totalSent","delivered","bounced","complained","deferred"]}}},"required":["generatedAt","fromDate","toDate","entries"]}},"required":["status","data"]},{"type":"object","properties":{"status":{"type":"string","enum":["error"]},"error":{"type":"string"}},"required":["status","error"]}]},"securityPosture":{"oneOf":[{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"data":{"type":"object","properties":{"generatedAt":{"type":"string"},"encryptionAtRest":{"type":"boolean"},"encryptionInTransit":{"type":"boolean"},"mtaStsEnforced":{"type":"boolean"},"arcSigningEnabled":{"type":"boolean"},"bimiConfigured":{"type":"boolean"},"dkimInventory":{"type":"array","items":{"type":"object","properties":{"domain":{"type":"string"},"status":{"type":"string","enum":["active","rotating","expired"]},"selector":{"type":"string"},"algorithm":{"type":"string"},"lastRotatedAt":{"type":"string"},"nextRotationAt":{"type":"string","nullable":true}},"required":["domain","status","selector","algorithm","lastRotatedAt","nextRotationAt"]}}},"required":["generatedAt","encryptionAtRest","encryptionInTransit","mtaStsEnforced","arcSigningEnabled","bimiConfigured","dkimInventory"]}},"required":["status","data"]},{"type":"object","properties":{"status":{"type":"string","enum":["error"]},"error":{"type":"string"}},"required":["status","error"]}]}},"required":["art30Register","dpa","compliancePassport","deliveryStatistics","securityPosture"]}},"required":["packageVersion","tenantId","generatedAt","fromDate","toDate","locale","status","components"]}},"application/zip":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation error (e.g. invalid date format)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/art33-template":{"post":{"tags":["Compliance"],"summary":"Generate GDPR Art. 33 misdirected email notification template (FR28)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Art33TemplateRequest"}}}},"responses":{"200":{"description":"Art. 33 notification template in Markdown format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Art33TemplateResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"403":{"description":"Forbidden — requires admin scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"404":{"description":"Tenant not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/export":{"post":{"tags":["Compliance"],"summary":"Initiate full tenant data export (GDPR Art. 20 data portability, FR38)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostExportRequest"}}}},"responses":{"202":{"description":"Export initiated — process in background","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostExportResponse"}}}},"400":{"description":"Export already in progress for this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportProblemDetails"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"403":{"description":"Forbidden — requires admin scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"503":{"description":"Export service unavailable — pg-boss not started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/export/{exportId}":{"get":{"tags":["Compliance"],"summary":"Get export job status (Story 5-8, FR38)","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"exportId","in":"path"}],"responses":{"200":{"description":"Export status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetExportStatusResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"403":{"description":"Forbidden — requires admin scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"404":{"description":"Export not found or cross-tenant access denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/export/{exportId}/download":{"get":{"tags":["Compliance"],"summary":"Download completed tenant data export ZIP (Story 5-8, FR38)","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"exportId","in":"path"}],"responses":{"200":{"description":"Decrypted ZIP binary","content":{"application/zip":{"schema":{"type":"string","format":"binary","description":"Encrypted ZIP binary content"}}}},"400":{"description":"Export has expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"403":{"description":"Forbidden — requires admin scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"404":{"description":"Export not found, not ready, or cross-tenant access denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"503":{"description":"Encryption service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/attribution/{messageId}":{"get":{"tags":["compliance"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"messageId","in":"path"}],"responses":{"200":{"description":"Attribution record","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentAttributionResponse"}},"required":["data"]}}}},"404":{"description":"No attribution record found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/incidents/detect":{"post":{"tags":["compliance"],"summary":"Detect NIS2 incident from a security event (FR32)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecurityEventRequest"}}}},"responses":{"200":{"description":"Incident assessment (and optional report if threshold met)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DetectIncidentResponse"}},"required":["data"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"403":{"description":"Forbidden — requires admin scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"404":{"description":"Tenant not found (cross-tenant access denied)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/incidents":{"get":{"tags":["compliance"],"summary":"List NIS2 incident reports for the tenant (FR32)","parameters":[{"schema":{"type":"string","enum":["draft","submitted","acknowledged"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Incident reports ordered by generatedAt DESC","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/IncidentReport"}}},"required":["data"]}}}},"403":{"description":"Forbidden — requires compliance:read scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/attribution/export":{"post":{"tags":["compliance"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttributionExportRequest"}}}},"responses":{"200":{"description":"Attribution export","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AttributionExportResponse"}},"required":["data"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/grc/configure":{"post":{"tags":["Compliance"],"summary":"Configure GRC platform integration (Vanta/Drata/Secureframe)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GRCConfigureRequest"}}}},"responses":{"200":{"description":"Integration configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GRCConfigureResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"424":{"description":"GRC connectivity check failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}},"503":{"description":"Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/compliance/grc/status":{"get":{"tags":["Compliance"],"summary":"List configured GRC integrations with sync health","responses":{"200":{"description":"Integration status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GRCStatusResponse"}}}},"503":{"description":"Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceProblemDetails"}}}}}}},"/v1/admin/tenants":{"post":{"tags":["Admin"],"summary":"Create a tenant with its default stream and a one-time bootstrap admin key","description":"Operator-only. Atomically creates a tenant, its `default` message stream, and a bootstrap admin API key returned exactly ONCE (hash-only storage — not recoverable after this response). Sanctioned replacement for the seed:production edit + redeploy onboarding path.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTenantRequest"}}}},"responses":{"201":{"description":"Tenant created — bootstrap admin key returned once","headers":{"Location":{"schema":{"type":"string","description":"URI of the created tenant (GET /v1/admin/tenants/{id})"},"required":true,"description":"URI of the created tenant (GET /v1/admin/tenants/{id})"},"Cache-Control":{"schema":{"type":"string","description":"no-store — the body carries a one-time secret and MUST NOT be cached"},"required":true,"description":"no-store — the body carries a one-time secret and MUST NOT be cached"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTenantResponse"}}}},"404":{"description":"Caller lacks the operator scope (404-not-403 — no existence disclosure)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"409":{"description":"Tenant name already exists (TENANT_NAME_CONFLICT). Carries Link: rel=\"related\" pointing at the name lookup that resolves the existing tenant's id — no Location accompanies a 409, so without it the caller who most needs the id is the one who cannot get it. [prep-m2-tenant-lookup-by-name]","headers":{"Link":{"schema":{"type":"string","description":"A registered relation type per RFC 8288 §2.1.1 (IANA registry: `related` [RFC 4287]) — </v1/admin/tenants?name=…>. One hop from \"such a tenant exists\" to \"here is its id\", without retyping a name under time pressure."},"required":true,"description":"A registered relation type per RFC 8288 §2.1.1 (IANA registry: `related` [RFC 4287]) — </v1/admin/tenants?name=…>. One hop from \"such a tenant exists\" to \"here is its id\", without retyping a name under time pressure."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"422":{"description":"Validation error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}},"get":{"tags":["Admin"],"summary":"List tenants (cross-tenant, keyset-paginated) with an exact-modulo-case ?name= lookup","description":"Operator-only, CROSS-TENANT read of the tenant catalogue. Removes the \"SSH into the prod host and run a SELECT\" step that was the blocking FIRST step of the break-glass key-reissue lane; the on-host `SELECT` remains an INDEPENDENT failure-domain lane and the rollback path, not a duplicate.\n\nNAME MATCHING: `?name=` is CASE-INSENSITIVE (`lower(name) = lower($1)`), so it returns a SUPERSET of an exact match — `items: []` therefore means a POST with that name will not 409. It is NOT a homoglyph check and NOT a Unicode normalization check: `Rakomi` with a Cyrillic `о`, an NFD-composed `é`, or a name carrying a zero-width character all yield the same empty result. SCOPE OF THE FOLD: `lower()` is collation-dependent case MAPPING. Measured 2026-07-28 on BOTH production and CI/dev: `en_US.utf8` (libc provider), i.e. full Unicode case mapping — but the cluster locale is NOT pinned anywhere in this repo, so this is host state. Outside the folding range defined by the cluster's collation the filter degrades to exact match.\n\nTHE EXISTENCE ORACLE IS CONCLUSIVE ONLY WITH `?name=` ALONE. Any additional filter narrows the result and RESTORES the unsafe direction: `?name=Rakomi&status=suspended` against an existing ACTIVE Rakomi returns `items: []`, the operator reads \"Branch A — the POST will create\", and the POST bounces 409 mid-mutation. Equally, in oracle mode do NOT set `limit`, and confirm the ABSENCE of `rel=\"next\"` — the missing link is the only proof you are looking at the whole set. A case-DIFFERENT match is a FINDING (a confusable tenant exists and your POST would SUCCEED, creating a near-duplicate), not a green light. A 422 does NOT mean \"the tenant does not exist\" — check the status before interpreting anything.\n\nTOCTOU: the oracle is ADVISORY; the `UNIQUE` index is authoritative. A 409 after a green Branch A is that index doing its job, not a defect in this endpoint.\n\nPAGINATION: keyset (never OFFSET), ORDER BY created_at DESC, id DESC. Pagination state lives ONLY in the RFC 8288 `Link` header — there is no nextCursor/hasMore in the body. **The ONLY end-of-collection signal is the absence of a link-value with `rel=\"next\"`.** A short — or empty — page does NOT mean the end. Use `curl -i`/`-D -`; a `curl -s` example cannot see pagination at all. Out-of-range `limit` is a 422, never silently clamped; an unknown query parameter is ignored, never a 422.\n\nNO RLS FORWARD DEPENDENCY: unlike /v1/admin/m2m-clients, `tenants` has NO RLS at all (it is operator-managed by design), so the sibling's \"fixing RLS silently turns this into a permanently empty list\" hazard does NOT transfer. A GRANT-shaped forward dependency DOES exist and fails harder: moving the app to a non-superuser role (prep-m2-rls-inert-in-production) must grant SELECT on `tenants` explicitly — a grant list derived from \"tables that have RLS\" drops this table BY DEFINITION, and the failure is a 500 on the break-glass route, not an empty list.","parameters":[{"schema":{"type":"string","maxLength":255,"pattern":"^[^\\p{Cc}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","example":"Rakomi"},"required":false,"name":"name","in":"query"},{"schema":{"type":"string","enum":["active","suspended"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","maxLength":50,"pattern":"^[^\\p{Cc}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","example":"internal"},"required":false,"name":"plan","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","maxLength":255},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"A bounded page of tenants, newest first. Emits Link: rel=\"next\" ONLY when a next page exists; its absence is the only end-of-collection signal.","headers":{"Link":{"schema":{"type":"string","description":"A registered relation type per RFC 8288 §2.1.1 (IANA registry: `next` [HTML]). `</v1/admin/tenants?…&cursor=…>; rel=\"next\"` — present ONLY when a next page exists. Relative target (never an absolute URL rebuilt from Host/X-Forwarded-*). Never a URI template: RFC 8288 §3 admits a URI-Reference, and RFC 6570 does not apply."},"required":false,"description":"A registered relation type per RFC 8288 §2.1.1 (IANA registry: `next` [HTML]). `</v1/admin/tenants?…&cursor=…>; rel=\"next\"` — present ONLY when a next page exists. Relative target (never an absolute URL rebuilt from Host/X-Forwarded-*). Never a URI template: RFC 8288 §3 admits a URI-Reference, and RFC 6570 does not apply."},"Cache-Control":{"schema":{"type":"string","description":"no-cache (deliberately NOT no-store — the body carries no secret, and no-store would falsely claim one): permits storage but forbids use without revalidation (RFC 9111 §5.2.2.4), so a PRIVATE cache cannot compute heuristic freshness (§4.2.2) and serve a stale body to an operator asking \"does this tenant exist RIGHT NOW?\"."},"required":true,"description":"no-cache (deliberately NOT no-store — the body carries no secret, and no-store would falsely claim one): permits storage but forbids use without revalidation (RFC 9111 §5.2.2.4), so a PRIVATE cache cannot compute heuristic freshness (§4.2.2) and serve a stale body to an operator asking \"does this tenant exist RIGHT NOW?\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListTenantsResponse"}}}},"404":{"description":"Caller lacks the operator scope (404-not-403, RFC 9110 §15.5.5). There is NO other 404 on this route — an empty collection is a 200 (§15.3.1). NOTE: that INHERITED 404 ships Content-Type application/json (requireOperatorScope uses a bare c.json) and carries NO cache directive; §15.5.5 makes a 404 cacheable by default, so a private cache may heuristically retain it. Both are shared-middleware facts registered as deferred work, not faked here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"422":{"description":"Invalid limit / cursor / name / plan / status (INVALID_CURSOR or VALIDATION_ERROR). 422-not-400 on a GET is a DECLARED deviation from the literal reading of RFC 9110 §15.5.21 (which is anchored in the REQUEST ENTITY, and a GET carries none), taken for consistency across the whole /v1/admin/* family.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/admin/tenants/{tenantId}":{"get":{"tags":["Admin"],"summary":"Get tenant admin info","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"tenantId","in":"path"}],"responses":{"200":{"description":"Tenant info","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-cache"},"required":true}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminTenantResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}},"patch":{"tags":["Admin"],"summary":"Suspend or restore tenant","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"tenantId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"status":{"type":"string","enum":["suspended"]},"reason":{"type":"string","minLength":1,"maxLength":500,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u"},"suspensionExpiresAt":{"type":"string","format":"date-time"}},"required":["status","reason"]},{"type":"object","properties":{"status":{"type":"string","enum":["active"]},"reason":{"type":"string","minLength":1,"maxLength":500,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u"}},"required":["status"]}]}}}},"responses":{"200":{"description":"Updated tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminTenantResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string"},"currentSuspendedAt":{"type":"string"}},"required":["code"]}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/admin/tenants/{tenantId}/export":{"post":{"tags":["Admin"],"summary":"Trigger admin data export for tenant","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"tenantId","in":"path"}],"responses":{"202":{"description":"Export triggered","content":{"application/json":{"schema":{"type":"object","properties":{"exportId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["processing"]}},"required":["exportId","status"]}}}},"400":{"description":"Export already in progress","content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string"},"status":{"type":"number"},"exportId":{"type":"string","format":"uuid"}},"required":["type","status","exportId"]}}}},"404":{"description":"Tenant not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"503":{"description":"Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/admin/tenants/{tenantId}/export/{exportId}":{"get":{"tags":["Admin"],"summary":"Poll admin export status","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"tenantId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"exportId","in":"path"}],"responses":{"200":{"description":"Export status","content":{"application/json":{"schema":{"type":"object","properties":{"exportId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["processing","ready","failed","expired"]},"downloadUrl":{"type":"string"},"error":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}},"required":["exportId","status","createdAt"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/admin/tenants/{tenantId}/export/{exportId}/download":{"get":{"tags":["Admin"],"summary":"Download admin export ZIP","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"tenantId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"exportId","in":"path"}],"responses":{"200":{"description":"Export ZIP","content":{"application/zip":{"schema":{"nullable":true}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/admin/m2m-clients":{"post":{"tags":["Admin"],"summary":"Provision an M2M client-allowlist row (operator policy config + zeroed quota + audit)","description":"Operator-only. Writes the Postvow-owned authorization POLICY row (\"the visa\") that turns a verified M2M token from a 404 unknown_client into an authorized sender. Creates NO credential and returns NO secret (the OAuth creds stay in the IdP/1Password). REJECTs mutation of an existing (idp, client_id) — a duplicate is a 409, never an upsert. Sanctioned replacement for the \"exec ops:provision-m2m inside the api container\" onboarding step; the CLI remains the off-path repair lane.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProvisionM2mClientRequest"}}}},"responses":{"201":{"description":"M2M client provisioned. warnings.unverifiedDomains lists allow-listed-but-not-yet-verified domains (they start sending once verified — not a block); warnings.denyAll=true when allowedDomains is empty (deny-all by design).","headers":{"Location":{"schema":{"type":"string","description":"Relative URI of the created resource (GET /v1/admin/m2m-clients/{configId}), RFC 9110 §15.3.2 + §10.2.2. Relative on purpose — never rebuilt from Host/X-Forwarded-* behind the Cloudflare Tunnel. [prep-m2-m2m-read-api]"},"required":true,"description":"Relative URI of the created resource (GET /v1/admin/m2m-clients/{configId}), RFC 9110 §15.3.2 + §10.2.2. Relative on purpose — never rebuilt from Host/X-Forwarded-* behind the Cloudflare Tunnel. [prep-m2-m2m-read-api]"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProvisionM2mClientResponse"}}}},"404":{"description":"Caller lacks the operator scope (404-not-403), OR the target tenant does not exist (TENANT_NOT_FOUND). The tenantId is never echoed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"409":{"description":"An (idp, client_id) row already exists (M2M_CLIENT_ALREADY_PROVISIONED). The endpoint never mutates an existing row — but since prep-m2-m2m-policy-mutation-api the existing row IS repairable: follow Link rel=\"alternate\" (or the conflictingConfigId extension member) to the resource, then PATCH it with If-Match. Link rel=\"related\" still points at the filtered list. All three pointers are FAIL-OPEN: if the conflicting row cannot be re-read (it may have been deleted in between) the 409 is returned WITHOUT them, never as a 500.","headers":{"Link":{"schema":{"type":"string","description":"RFC 8288, one or two link-values. rel=\"related\" → </v1/admin/m2m-clients?idp=…&clientId=…>; rel=\"alternate\" → </v1/admin/m2m-clients/{configId}>, one hop to the exact resource. `.optional()` because both are fail-open."},"required":false,"description":"RFC 8288, one or two link-values. rel=\"related\" → </v1/admin/m2m-clients?idp=…&clientId=…>; rel=\"alternate\" → </v1/admin/m2m-clients/{configId}>, one hop to the exact resource. `.optional()` because both are fail-open."},"Location":{"schema":{"type":"string","description":"DELIBERATE DEVIATION — RFC 9110 §10.2.2 defines Location only for 201 and 3xx, so its meaning on a 409 is undefined. It is purely ergonomic (curl -i) and NO CLIENT MAY RELY ON IT; the normative carriers are Link rel=\"alternate\" and the conflictingConfigId extension member. Relative URI, never rebuilt from Host/X-Forwarded-*."},"required":false,"description":"DELIBERATE DEVIATION — RFC 9110 §10.2.2 defines Location only for 201 and 3xx, so its meaning on a 409 is undefined. It is purely ergonomic (curl -i) and NO CLIENT MAY RELY ON IT; the normative carriers are Link rel=\"alternate\" and the conflictingConfigId extension member. Relative URI, never rebuilt from Host/X-Forwarded-*."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"422":{"description":"Validation error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}},"get":{"tags":["Admin"],"summary":"List provisioned M2M client-allowlist rows (cross-tenant, keyset-paginated)","description":"Operator-only, CROSS-TENANT read of the Postvow-owned authorization POLICY rows. Removes the \"SSH into the prod host and run ops:provision-m2m --list inside the api container\" step; the CLI remains an INDEPENDENT failure-domain lane, not a duplicate.\n\nPAGINATION: keyset (never OFFSET), ORDER BY created_at DESC, id DESC. Pagination state lives ONLY in the RFC 8288 `Link` header — there is no nextCursor/hasMore in the body. **The ONLY end-of-collection signal is the ABSENCE of `Link: rel=\"next\"`.** A short page — or even an empty one — does NOT mean the end (it becomes literally true the moment a row is deleted at a page boundary). Use `curl -i`/`-D -`; a `curl -s` example cannot see pagination at all.\n\nChanging filters while keeping a cursor is ALLOWED and returns the filtered tail (Stripe/GitHub positional-cursor school) — it is not an error. Out-of-range `limit` is a 422, never silently clamped. An unknown query parameter is ignored, never a 422. `?tenantId=<nonexistent>` returns 200 {items: []} — a non-existent tenant and a tenant with no M2M clients are indistinguishable here BY DESIGN.\n\nFORWARD DEPENDENCY: this handler deliberately sets no tenant GUC. When RLS enforcement returns (prep-m2-rls-inert-in-production), tenant_isolation_policy will admit ZERO rows and this endpoint will return 200 {\"items\": []} — byte-identical to \"nothing matches\". It then needs an explicit decision (an operator role with BYPASSRLS, or an operator_read_policy on the table).","parameters":[{"schema":{"type":"string","maxLength":50,"pattern":"^[^\\p{Cc}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","example":"rakomi"},"required":false,"name":"idp","in":"query"},{"schema":{"type":"string","maxLength":255,"pattern":"^[^\\p{Cc}\\p{Zl}\\p{Zp}\\p{Cs}]+$/u","example":"postvow-prod-m2m"},"required":false,"name":"clientId","in":"query"},{"schema":{"type":"string","format":"uuid"},"required":false,"name":"tenantId","in":"query"},{"schema":{"type":"string","enum":["active","disabled"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","maxLength":255},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"A bounded page of policy rows, newest first. Emits Link: rel=\"next\" ONLY when a next page exists; its absence is the only end-of-collection signal.","headers":{"Link":{"schema":{"type":"string","description":"RFC 8288. `</v1/admin/m2m-clients?…&cursor=…>; rel=\"next\"` — present ONLY when a next page exists; its ABSENCE is the only end-of-collection signal. Relative target (never an absolute URL rebuilt from Host/X-Forwarded-*)."},"required":false,"description":"RFC 8288. `</v1/admin/m2m-clients?…&cursor=…>; rel=\"next\"` — present ONLY when a next page exists; its ABSENCE is the only end-of-collection signal. Relative target (never an absolute URL rebuilt from Host/X-Forwarded-*)."},"Cache-Control":{"schema":{"type":"string","description":"no-cache (deliberately NOT no-store): permits storage but forbids use without revalidation, so a private cache cannot serve a stale body to an operator whose whole purpose is verifying what was just written."},"required":true,"description":"no-cache (deliberately NOT no-store): permits storage but forbids use without revalidation, so a private cache cannot serve a stale body to an operator whose whole purpose is verifying what was just written."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListM2mClientsResponse"}}}},"404":{"description":"Caller lacks the operator scope (404-not-403, RFC 9110 §15.5.5). NOTE: that INHERITED 404 actually ships Content-Type application/json (requireOperatorScope uses a bare c.json) — which is precisely what keeps it DISCRIMINABLE from this resource-404 family.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"422":{"description":"Invalid limit / cursor / filter (INVALID_CURSOR or VALIDATION_ERROR). 422-not-400 on a GET is a DECLARED deviation from the literal reading of RFC 9110 §15.5.21, taken for consistency with this file and the rest of /v1/admin/*.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/admin/m2m-clients/{configId}":{"get":{"tags":["Admin"],"summary":"Read one provisioned M2M client-allowlist row by configId","description":"Operator-only, cross-tenant read of a single policy row. The resource the POST 201 Location header points at. Its identity handle (configId) is also the mutation handle prep-m2-m2m-policy-mutation-api will use.\n\nA 404 here means the ROW is absent — and it is discriminable from the inherited scope-404: this one is application/problem+json with code M2M_CLIENT_NOT_FOUND, the inherited one is application/json with the middleware's own code. On a read used as an incident oracle that distinction matters: an operator whose key was rotated mid-session must not read \"the row does not exist\" and re-POST.\n\nFORWARD DEPENDENCY (same as the list route, and here the disguise is worse): this handler deliberately sets no tenant GUC. When prep-m2-rls-inert-in-production lands and tenant_isolation_policy starts enforcing, getM2mClientById resolves ZERO rows and this route answers 404 M2M_CLIENT_NOT_FOUND — byte-identical to \"the row is genuinely absent\", which is exactly the wrong conclusion that sends an operator back to re-POSTing. It needs an explicit decision then (a dedicated operator role with BYPASSRLS, or an operator_read_policy on the table); it cannot be deferred, because the endpoint stops returning data and does so quietly.","parameters":[{"schema":{"type":"string","format":"uuid","example":"00000000-0000-4000-8000-000000000000"},"required":true,"name":"configId","in":"path"}],"responses":{"200":{"description":"The policy row, with the ETag that PATCH/DELETE require in If-Match. [prep-m2-m2m-policy-mutation-api] — this is the optimistic-lock seam the read story deliberately did NOT mint, decided together with the mutation it protects.","headers":{"ETag":{"schema":{"type":"string","pattern":"^\"m2m1:[0-9a-f]{64}\"$","description":"RFC 9110 §8.8.3 strong entity-tag, OPAQUE to the client — copy it VERBATIM into If-Match. Do not normalise it, do not strip a W/ if one appears (that means an intermediary weakened it; report it, do not work around it). It is a validator of POLICY STATE, NOT a hash of this response body: two responses differing only in quota.consumed carry an IDENTICAL ETag. That is safe ONLY because every such response carries Cache-Control: no-store — removing no-store here is a breaking change and requires re-opening the ETag scope.","example":"\"m2m1:3b0c4f…\""},"required":true,"description":"RFC 9110 §8.8.3 strong entity-tag, OPAQUE to the client — copy it VERBATIM into If-Match. Do not normalise it, do not strip a W/ if one appears (that means an intermediary weakened it; report it, do not work around it). It is a validator of POLICY STATE, NOT a hash of this response body: two responses differing only in quota.consumed carry an IDENTICAL ETag. That is safe ONLY because every such response carries Cache-Control: no-store — removing no-store here is a breaking change and requires re-opening the ETag scope."},"Cache-Control":{"schema":{"type":"string","description":"no-store, no-transform. no-store (RFC 9111 §5.2.2.5) rather than no-cache: no-cache permits STORAGE and mandates revalidation, and revalidation would yield a 304 at which point a cache serves a stored body carrying a STALE quota.consumed as fresh. no-transform (RFC 9110 §5.6.2) because Cloudflare rewrites strong ETags to weak (W/) when it compresses — which would make strong comparison refuse a tag this server issued, i.e. 412 always."},"required":true,"description":"no-store, no-transform. no-store (RFC 9111 §5.2.2.5) rather than no-cache: no-cache permits STORAGE and mandates revalidation, and revalidation would yield a 304 at which point a cache serves a stored body carrying a STALE quota.consumed as fresh. no-transform (RFC 9110 §5.6.2) because Cloudflare rewrites strong ETags to weak (W/) when it compresses — which would make strong comparison refuse a tag this server issued, i.e. 412 always."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/M2mClientSummary"}}}},"404":{"description":"M2M_CLIENT_NOT_FOUND (the row is absent), OR the inherited non-operator 404 (application/json). The requested configId is NEVER echoed back — in ANY member, including `instance`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"422":{"description":"configId is not a UUID. No DB round-trip happens.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}},"patch":{"tags":["Admin"],"summary":"Repair a provisioned M2M policy row (optimistic-locked, audited)","description":"Operator-only, cross-tenant SPARSE patch of one policy row. The sanctioned repair path — a typo'd clientId, an over-broad allowedDomains or a mis-targeted onboarding stops being a permanent production fact, without psql and without SSH.\n\n**If-Match is MANDATORY.** Sequence: GET /v1/admin/m2m-clients/{configId} → copy the ETag → repeat with `If-Match: <that value>`. Missing → 428. Present but not matching → 412 and the row is NOT modified; the 412 body carries a `currentEtag` extension member you can replay immediately. `If-Match: *` is ACCEPTED here (it disables the lock) but is logged and flagged in the audit row; it is REFUSED with 428 on DELETE. `If-Unmodified-Since` is NOT honoured (second granularity is coarser than the millisecond precision this validator already rejects) — a request carrying only that header gets 428.\n\nBODY: a hybrid of RFC 7396 that deliberately does NOT declare application/merge-patch+json (unknown keys are REJECTED, not added; `{}` is a 422). Any other Content-Type → 415 with Accept-Patch.\n\nIMMUTABLE by construction: configId, idp, clientId, tenantId. To fix an identity typo, DELETE the row and re-POST it. tenantId in particular is FORBIDDEN, not \"not yet supported\" — re-pointing a live authorization row at a different tenant has no safe semantics for the already-consumed quota or the already-written audit history.\n\nPRECEDENCE when conditions collide: 415 → 422 (body) → 428 → 400 (malformed If-Match) → 503 (lock timeout) → 404 → 412 → 422 (post-lock REASON_REQUIRED, then QUOTA_ROW_EXISTS) → 200. The ordering is SEMANTIC: 412 before 404 would make the endpoint an existence oracle for an offline-guessed ETag.\n\nFORWARD DEPENDENCY (same silent shape as the reads, and it is worse on a write): this handler sets no tenant GUC for the locking SELECT. When prep-m2-rls-inert-in-production lands, tenant_isolation_policy admits ZERO rows for a GUC-less read, so this route answers 404 — byte-identical to \"the row is genuinely absent\". Whoever restores RLS owes this route an explicit decision AND a policy covering UPDATE/DELETE with a WITH CHECK clause.","parameters":[{"schema":{"type":"string","format":"uuid","example":"00000000-0000-4000-8000-000000000000"},"required":true,"name":"configId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchM2mClientRequest"}}}},"responses":{"200":{"description":"The policy row AFTER the patch, with a NEW ETag.","headers":{"ETag":{"schema":{"type":"string","pattern":"^\"m2m1:[0-9a-f]{64}\"$","description":"RFC 9110 §8.8.3 strong entity-tag, OPAQUE to the client — copy it VERBATIM into If-Match. Do not normalise it, do not strip a W/ if one appears (that means an intermediary weakened it; report it, do not work around it). It is a validator of POLICY STATE, NOT a hash of this response body: two responses differing only in quota.consumed carry an IDENTICAL ETag. That is safe ONLY because every such response carries Cache-Control: no-store — removing no-store here is a breaking change and requires re-opening the ETag scope.","example":"\"m2m1:3b0c4f…\""},"required":true,"description":"RFC 9110 §8.8.3 strong entity-tag, OPAQUE to the client — copy it VERBATIM into If-Match. Do not normalise it, do not strip a W/ if one appears (that means an intermediary weakened it; report it, do not work around it). It is a validator of POLICY STATE, NOT a hash of this response body: two responses differing only in quota.consumed carry an IDENTICAL ETag. That is safe ONLY because every such response carries Cache-Control: no-store — removing no-store here is a breaking change and requires re-opening the ETag scope."},"Cache-Control":{"schema":{"type":"string","description":"no-store, no-transform. no-store (RFC 9111 §5.2.2.5) rather than no-cache: no-cache permits STORAGE and mandates revalidation, and revalidation would yield a 304 at which point a cache serves a stored body carrying a STALE quota.consumed as fresh. no-transform (RFC 9110 §5.6.2) because Cloudflare rewrites strong ETags to weak (W/) when it compresses — which would make strong comparison refuse a tag this server issued, i.e. 412 always."},"required":true,"description":"no-store, no-transform. no-store (RFC 9111 §5.2.2.5) rather than no-cache: no-cache permits STORAGE and mandates revalidation, and revalidation would yield a 304 at which point a cache serves a stored body carrying a STALE quota.consumed as fresh. no-transform (RFC 9110 §5.6.2) because Cloudflare rewrites strong ETags to weak (W/) when it compresses — which would make strong comparison refuse a tag this server issued, i.e. 412 always."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/M2mClientSummary"}}}},"400":{"description":"MALFORMED_IF_MATCH — the If-Match header is not a syntactically valid entity-tag list. 400, NOT 422: RFC 9110 §15.5.21 scopes 422 to the request CONTENT, while §15.5.1 covers malformed request syntax, and If-Match is a header field. Also returned when the list exceeds 64 members (a bound enforced before any DB access).","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"M2M_CLIENT_NOT_FOUND, OR the inherited non-operator 404 (application/json — which is what keeps the two DISCRIMINABLE). A non-operator WITHOUT If-Match gets 404, never 428: a 428 would tell an unauthorised caller that this route exists and requires a condition. The requested configId is NEVER echoed, in any member including `instance`.","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"409":{"description":"Reserved for state conflicts on this resource; 412 is reserved EXCLUSIVELY for a failed If-Match. (Emitted by DELETE as M2M_CLIENT_ACTIVE.)","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"412":{"description":"PRECONDITION_FAILED — the If-Match did not match. The row is NOT modified. Carries the `currentEtag` extension member (RFC 9457 §3.2) rather than an ETag HEADER: §8.8.3 binds ETag to the ENCLOSED representation, which here is a problem document, not the policy row. Re-GET and READ THE DIFFERENCE — do not retry blindly with a fresh validator.","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"415":{"description":"UNSUPPORTED_PATCH_MEDIA_TYPE. RFC 5789 §2.2 makes 415 the response for an unsupported patch format and §3.1 makes Accept-Patch the discoverability mechanism. NOTE the media type must be spelled EXACTLY `application/json` in lower case — see the middleware comment for why this narrowing of RFC 9110 §8.3.1 case-insensitivity is deliberate.","headers":{"Accept-Patch":{"schema":{"type":"string","description":"RFC 5789 §3.1 — application/json (our own dialect, see the body)."},"required":true,"description":"RFC 5789 §3.1 — application/json (our own dialect, see the body)."},"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"422":{"description":"Body validation (unknown key, EMPTY_PATCH, an immutable field, a bad actor/reason), OR one of the two POST-LOCK rungs: REASON_REQUIRED (a PATCH actually crossing active → disabled) and QUOTA_ROW_EXISTS. A post-lock 422 rolls the WHOLE patch back (RFC 5789 §2 atomicity) — no field in the same body is applied.","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"428":{"description":"PRECONDITION_REQUIRED (RFC 6585 §3) — If-Match is absent. MUST NOT be stored by a cache; the body states the exact retry sequence.","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"503":{"description":"MUTATION_LOCK_TIMEOUT — another mutation of this row held the lock past lock_timeout (3s), OR PostgreSQL chose this transaction as a deadlock victim (40P01). Both are EXPECTED, BOUNDED, RETRYABLE outcomes, hence 503 and not 500; and not 409, which already means M2M_CLIENT_ACTIVE on this route. Carries Retry-After.","headers":{"Retry-After":{"schema":{"type":"string","description":"Delta-seconds."},"required":true,"description":"Delta-seconds."},"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}},"delete":{"tags":["Admin"],"summary":"Destroy a DISABLED M2M policy row (two-step, irreversible)","description":"Operator-only. What makes a typo'd clientId repairable at all — clientId is IMMUTABLE, so without DELETE the headline scenario stays unrepairable.\n\nTWO-STEP BY DESIGN: the row MUST already be status=disabled; a DELETE against an active row is 409 M2M_CLIENT_ACTIVE carrying currentStatus + currentEtag. This is AWS IAM's UpdateAccessKey Status=Inactive → DeleteAccessKey pattern 1:1 — destroying a live sender's authorization in one call, from a route that also serves routine edits, is the wrong ergonomics for an irreversible operation.\n\nIf-Match is MANDATORY and `*` is REFUSED with 428: DELETE is irreversible and cascading, and `*` would permit it with NO PRIOR READ OF THE ROW AT ALL — removing the single moment at which the operator (or a stolen operator key) would have had to SEE what they are destroying. RFC 9110 §13.1.1 permits a server to require a strong validator.\n\n`reason` is MANDATORY. CASCADE: m2m_client_quota_usage goes with the row (its consumed history is IRREVERSIBLY lost — the deleted audit row records it as deletedSnapshot.consumed). m2m_client_config_audit has no FK and SURVIVES by design.\n\nA second DELETE of the same id is 404, NOT 204. RFC 9110 §9.3.5 idempotency is about the EFFECT, not the status code, and on a repair path \"did my delete apply, or was it already gone?\" is a question the operator needs answered.\n\nNOTE: disable → DELETE → re-POST with the same (idp, clientId) is a SANCTIONED quota reset. It is not blocked — releasing the key is the whole point — but it is made reconstructable via deletedSnapshot.consumed and metadata.consumedAtDelete. If the quota window is billing-relevant, record consumedAtDelete before re-provisioning.","parameters":[{"schema":{"type":"string","format":"uuid","example":"00000000-0000-4000-8000-000000000000"},"required":true,"name":"configId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteM2mClientRequest"}}}},"responses":{"204":{"description":"Deleted. No body, no ETag."},"400":{"description":"MALFORMED_IF_MATCH (see the PATCH route).","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"M2M_CLIENT_NOT_FOUND (incl. a second DELETE), or the inherited non-operator 404.","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"409":{"description":"M2M_CLIENT_ACTIVE — the row is not disabled. Carries currentStatus and currentEtag extension members: the suggestedFix says \"disable first\", and that PATCH needs an ETag.","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"412":{"description":"PRECONDITION_FAILED (see the PATCH route). Adjudicated BEFORE the 409.","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"415":{"description":"The request body must be `Content-Type: application/json`, spelled in lower case (see the PATCH route 415 and the middleware comment).","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"422":{"description":"Body validation — notably a MISSING `reason`, which is refused at the validation hook and therefore costs no row lock. (Consequence owned rather than hidden: a hook 422 writes NO audit row, so an attempt to destroy an authorization row that was refused for lack of a stated reason leaves no trace. Registered in deferred-work as the most destructive instance of the validation-hook audit gap; owned by prep-m2-operator-audit-rejected-actions.)","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"428":{"description":"PRECONDITION_REQUIRED — If-Match absent, OR `If-Match: *` (refused on DELETE).","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"503":{"description":"MUTATION_LOCK_TIMEOUT / deadlock victim (see the PATCH route).","headers":{"Retry-After":{"schema":{"type":"string","description":"Delta-seconds."},"required":true,"description":"Delta-seconds."},"Cache-Control":{"schema":{"type":"string","description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."},"required":true,"description":"no-store. RFC 6585 §3 makes it MANDATORY on the 428 (a cached \"precondition required\" would defeat the retry sequence); it is emitted on EVERY problem response of this route family so the guarantee does not depend on which rung refused the request."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/admin/runtime-config/bearer-types":{"put":{"tags":["Admin"],"summary":"Narrow (or restore, within the env ceiling) the accepted bearer types at runtime","description":"Operator-only. Writes the runtime bearer-type set. The env variable POSTVOW_AUTH_ACCEPTED_BEARER_TYPES is a hard CEILING: this endpoint can NARROW within it or restore up to it, but can NEVER grant a type the deployment config forbids — a request naming a type outside the ceiling is 422, never a silent intersection. `native` is structurally non-removable (self-lockout guard) and is auto-unioned back in with a warning. Convergence is TTL-bounded and PER-PROCESS (see convergesByUtc), never instant. This is a fast in-band NARROWING lane, NOT the kill-switch — the kill-switch remains env + restart, whose dependency set (1Password, deploy plane) is disjoint from this one (API + Postgres + operator key).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutBearerTypesRequest"}}}},"responses":{"200":{"description":"Applied. 200 (not 201) — the resource is a singleton that always conceptually exists. Re-sending the same value is a no-op for STATE but still writes an audit row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutBearerTypesResponse"}}}},"400":{"description":"Malformed JSON body — rejected by the app-wide parser, which emits application/json (NOT problem+json; that is the shared-middleware gap registered as deferred work). A MISSING or wrong Content-Type does NOT land here: the zod-openapi json validator then produces no parsed body and the per-route hook fires instead → 422. That diverges from RFC 9110 §15.5.16 (which would say 415) and is recorded as measured behaviour rather than faked conformance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"Caller lacks the operator scope. 404, never 403 — RFC 9110 §15.5.4 permits an origin server that wishes to hide the existence of a forbidden target to respond 404, and disclosing the existence of the /v1/admin/* surface to a non-operator is reconnaissance leakage. This is a DOCUMENTED DIVERGENCE from RFC 6750 §3.1, which recommends 403 for insufficient_scope; consequently no WWW-Authenticate header is emitted on this path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"422":{"description":"Validation error, OR BEARER_TYPE_NOT_PERMITTED — a requested type is outside the env ceiling. 422 and not 409: the ceiling is not resource STATE the caller can reconcile and retry (RFC 9110 §15.5.10); it is a fixed capability of the process for its whole lifetime, so no retry or resource edit will ever make the request succeed (§15.5.21). No row is written.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"500":{"description":"The write transaction rolled back. NOTHING was applied — the configuration row and its operator_audit_log record commit together, so this is never a partial write and there is never an unrecorded change to reconstruct. Declared because the route can actually return it: an undeclared-but-receivable status is the OpenAPI-drift class the contract tests exist to prevent. A persistent 500 here means the database is unhealthy, which is the documented condition for using the env + restart kill-switch (Lane B) instead.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/admin/runtime-config":{"get":{"tags":["Admin"],"summary":"Read the effective runtime config with full provenance (the no-SSH verification path)","description":"Operator-only. `effective` and `source` come from the SAME resolver instance that serves auth — a \"fresh SELECT so we show the truth\" would make this endpoint report a value the middleware does not yet see, turning the verification path into false assurance. `dbValue` IS a cache-bypassing read (it is the global truth), so `dbValue != effective` is a LEGAL, documented \"not yet converged\" state, not a bug. THIS ENDPOINT DESCRIBES ONE PROCESS: at WORKER_COUNT>1 it stops being a fleet oracle; the only fleet oracle is elapsed time (`now - updatedAt > ttlSeconds`). Returns 200 even in a degraded state (stale-cache / cold-floor) — it is a diagnostic read — but it never reports the ceiling as `effective` when degraded.","responses":{"200":{"description":"The effective runtime config for THIS process, with provenance and warnings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetRuntimeConfigResponse"}}}},"404":{"description":"Caller lacks the operator scope (404-not-403 — see the PUT route).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/admin/tenants/{tenantId}/bootstrap-key":{"post":{"tags":["Admin"],"summary":"Issue a fresh admin API key for a tenant (operator-only)","description":"Operator-only. Issues a new `admin` API key for the named tenant and returns the plaintext exactly ONCE (hash-only storage — not recoverable after this response). The predecessor-revocation set is an explicit required decision, never a default. Serves both entrances: a tenant that never received a first key, and a tenant whose one-time key was lost. POST (not PUT) — the operation is neither idempotent nor a state replacement; supply an Idempotency-Key header to make a retry safe.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"tenantId","in":"path"},{"schema":{"type":"string","minLength":8,"maxLength":255},"required":false,"name":"idempotency-key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReissueBootstrapKeyRequest"}}}},"responses":{"200":{"description":"dryRun preview — the revocation set that WOULD be swept. Nothing was minted and nothing was revoked; one audit row with outcome=preview was written.","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store — a 200 and a 201 from one route must not have divergent cache semantics."},"required":true,"description":"no-store — a 200 and a 201 from one route must not have divergent cache semantics."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReissueBootstrapKeyPreview"}}}},"201":{"description":"Admin key issued — the plaintext is in the body and is returned exactly ONCE. warnings.keyUnusableUntilReactivated=true means the tenant is suspended, so the new key is inert for everything outside /v1/admin/* until the tenant is reactivated (not a block: staging a key before reactivation is legitimate).","headers":{"Cache-Control":{"schema":{"type":"string","description":"no-store — the body carries a one-time secret (RFC 9111 §5.2.2.5, OWASP ASVS 4.0.3 V8.2.1). NECESSARY BUT NOT SUFFICIENT: §5.2.2.5's own Note says no-store \"is not an appropriate or reliable mechanism for preventing storage of sensitive information\" — it binds caches, not disks, transcripts or proxies. The runbook §4.2 custody discipline is the sufficient half."},"required":true,"description":"no-store — the body carries a one-time secret (RFC 9111 §5.2.2.5, OWASP ASVS 4.0.3 V8.2.1). NECESSARY BUT NOT SUFFICIENT: §5.2.2.5's own Note says no-store \"is not an appropriate or reliable mechanism for preventing storage of sensitive information\" — it binds caches, not disks, transcripts or proxies. The runbook §4.2 custody discipline is the sufficient half."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReissueBootstrapKeyResponse"}}}},"404":{"description":"Caller lacks the operator scope, OR the target tenant does not exist (TENANT_NOT_FOUND). The 404-not-403 for a scope failure is a DELIBERATE, DOCUMENTED DEVIATION from RFC 6750 §3.1 (which prescribes 403 insufficient_scope), adopted as an anti-enumeration measure per .claude/rules/security.md and OWASP ASVS 4.0.3 V4.1.5 (access-control failures fail securely). The resulting internal inconsistency is intentional: tenant-facing scope failures answer 403 because the tenant already knows it exists, while the operator surface must not be discoverable. NOTE: the INHERITED non-operator 404 ships Content-Type application/json (the middleware uses a bare c.json); the handler's OWN TENANT_NOT_FOUND ships application/problem+json. That media-type mismatch is a KNOWN, REGISTERED deferral shared with the operator siblings — deferred-work.md \"Middleware media-type fix (F1) [P3]\" — not an oversight of this route.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"409":{"description":"BREAK_GLASS_IN_FLIGHT — either another break-glass call for this tenant is in flight, or this is a replay of your own request under the same Idempotency-Key. Nothing was minted and NOTHING WAS REVOKED. Do NOT blindly retry — verify state first. No Retry-After: RFC 9110 §10.2.3 scopes it to 503/3xx, and a machine-readable retry hint would be actively wrong here.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"422":{"description":"Validation error (RFC 9110 §15.5.21)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/admin/invoices":{"get":{"tags":["Admin"],"summary":"List invoices (cross-tenant, keyset-paginated) for the operator SEPA lane","description":"Operator-only, CROSS-TENANT invoice read model (FR68). Keyset pagination (created_at DESC, id DESC); pagination state lives ONLY in the RFC 8288 Link header — the ABSENCE of rel=\"next\" is the only end-of-collection signal (a short or empty page is not). Reads are deliberately UNAUDITED (the standing operator-plane read exclusion).","parameters":[{"schema":{"type":"string","format":"uuid"},"required":false,"name":"tenantId","in":"query"},{"schema":{"type":"string","enum":["pending","emitted","emit_failed","paid","overdue","cancelled"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","maxLength":255},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"A bounded page of invoices, newest first","headers":{"Link":{"schema":{"type":"string","description":"RFC 8288 rel=\"next\" — present ONLY when a next page exists."},"required":false,"description":"RFC 8288 rel=\"next\" — present ONLY when a next page exists."},"Cache-Control":{"schema":{"type":"string","example":"no-cache"},"required":true}},"content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminInvoiceResponse"}}},"required":["items"]}}}},"404":{"description":"Caller lacks the operator scope (404-not-403 — no existence disclosure)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceProblemDetails"}}}},"422":{"description":"Invalid limit / cursor / filter","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/InvoiceProblemDetails"}}}}}}},"/v1/admin/invoices/{invoiceId}/mark-paid":{"post":{"tags":["Admin"],"summary":"Mark an invoice paid against a SEPA bank-statement line (manual Phase 0)","description":"Operator-only. THE manual Phase-0 SEPA reconciliation step: records the bank reference and payment timestamp against an emitted/overdue invoice. Atomic conditional transition (emitted|overdue → paid); every refusal is audited per the operator-plane convention. This is honest manual reconciliation — no bank integration exists yet.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"invoiceId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkInvoicePaidRequest"}}}},"responses":{"200":{"description":"Invoice marked paid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminInvoiceResponse"}}}},"404":{"description":"Invoice not found (or caller lacks the operator scope — same 404, no oracle)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/InvoiceProblemDetails"}}}},"409":{"description":"Invoice is already paid, or in a state that cannot become paid","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string"},"currentStatus":{"type":"string"}},"required":["code","currentStatus"]}}}},"422":{"description":"Validation error (audited: one operator_audit_log row, rejectReason=validation)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/InvoiceProblemDetails"}}}}}}},"/v1/billing/invoices":{"get":{"tags":["Billing"],"summary":"List this tenant's invoices (the 15-4 dashboard read model, FR68)","parameters":[{"schema":{"type":"string","enum":["pending","emitted","emit_failed","paid","overdue","cancelled"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","maxLength":255},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"A bounded page of invoices, newest first","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TenantInvoiceResponse"}},"pagination":{"type":"object","properties":{"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}},"required":["nextCursor","hasMore"]}},"required":["data","pagination"]}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingProblemDetails"}}}},"403":{"description":"Key lacks the billing:read scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingProblemDetails"}}}},"422":{"description":"Invalid limit / cursor","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/BillingProblemDetails"}}}}}}},"/v1/usage":{"get":{"tags":["Billing"],"summary":"Current billing period usage vs plan for this tenant (the 15-4 dashboard read model)","responses":{"200":{"description":"Current-period usage, included volume and credit balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}}},"401":{"description":"Missing/invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageProblemDetails"}}}},"403":{"description":"Credentials lack the billing:read scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageProblemDetails"}}}}}}},"/v1/score":{"get":{"summary":"Get current Postvow Score","description":"Returns the current Postvow Score for the tenant with component breakdown and per-ISP analysis. Uses a deterministic weighted formula — EU AI Act Art. 3(1) non-applicable. GDPR Art. 30 non-applicable: stores aggregate reputation metrics, no personal data.","parameters":[{"schema":{"type":"string","maxLength":255,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$"},"required":false,"name":"domain","in":"query"}],"responses":{"200":{"description":"Current score","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScoreResponse"}}}},"400":{"description":"Invalid domain format","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}},"401":{"description":"Unauthorized","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}},"404":{"description":"No verified domain","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}}}},"post":{"tags":["Deliverability"],"summary":"Pre-send deliverability score","description":"Computes a pre-send deliverability score (0-100) for a message before committing to delivery. Rule-based deterministic heuristics — EU AI Act Art. 3(66) non-applicable.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliverabilityScoreRequest"}}}},"responses":{"200":{"description":"Deliverability score","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliverabilityScoreResponse"}}}},"400":{"description":"Validation error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}},"401":{"description":"Unauthorized","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}},"413":{"description":"Payload too large","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}},"429":{"description":"Rate limit exceeded","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}}}}},"/v1/score/history":{"get":{"summary":"Get Postvow Score history","description":"Returns historical Postvow Score trend data aggregated by period and granularity.","parameters":[{"schema":{"type":"string","enum":["7d","30d","90d"],"default":"30d"},"required":false,"name":"period","in":"query"},{"schema":{"type":"string","enum":["hourly","daily","weekly"],"default":"daily"},"required":false,"name":"granularity","in":"query"},{"schema":{"type":"string","maxLength":255,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$"},"required":false,"name":"domain","in":"query"}],"responses":{"200":{"description":"Score history","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ScoreHistoryEntry"}}}}},"401":{"description":"Unauthorized","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}},"404":{"description":"No verified domain","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}},"422":{"description":"Invalid granularity","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ScoreProblemDetails"}}}}}}},"/v1/metrics/delivery":{"get":{"summary":"Get SEG-aware delivery metrics","description":"Returns delivery metrics distinguishing inbox-delivered vs gateway-delivered email (FR49). Classification is based on MX record analysis at delivery time — Phase 1 uses MX patterns only. bannerPatterns/ehloPatterns in seg-signatures.json are reserved for Phase 2 (SMTP banner capture). GDPR Art.25: delivery_classification and seg_provider are infrastructure metadata, not personal data.","parameters":[{"schema":{"type":"string","enum":["1h","6h","24h","7d","30d"],"default":"24h"},"required":false,"name":"period","in":"query"},{"schema":{"type":"string","maxLength":255,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$"},"required":false,"name":"domain","in":"query"}],"responses":{"200":{"description":"Delivery metrics","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryMetricsResponse"}}}},"400":{"description":"Invalid period value","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/DeliveryMetricsProblemDetails"}}}},"401":{"description":"Unauthorized","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/DeliveryMetricsProblemDetails"}}}},"404":{"description":"Domain not found or not verified","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/DeliveryMetricsProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/DeliveryMetricsProblemDetails"}}}}}}},"/v1/runbooks":{"get":{"tags":["Runbooks"],"responses":{"200":{"description":"Runbook registry","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RunbookEntry"}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunbookProblemDetails"}}}}}}},"/v1/runbooks/{runbookId}/execute":{"post":{"tags":["Runbooks"],"parameters":[{"schema":{"type":"string"},"required":true,"name":"runbookId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunbookExecuteBody"}}}},"responses":{"200":{"description":"Runbook execution result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunbookExecuteResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunbookProblemDetails"}}}},"404":{"description":"Runbook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunbookProblemDetails"}}}}}}},"/v1/team/members":{"post":{"tags":["Team"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteMemberBody"}}}},"responses":{"201":{"description":"Member invited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamMember"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamProblemDetails"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamProblemDetails"}}}},"409":{"description":"Member already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamProblemDetails"}}}}}},"get":{"tags":["Team"],"responses":{"200":{"description":"Team members","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamMemberList"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamProblemDetails"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamProblemDetails"}}}}}}},"/v1/team/members/{memberId}":{"patch":{"tags":["Team"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"memberId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMemberBody"}}}},"responses":{"200":{"description":"Member updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamMember"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamProblemDetails"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamProblemDetails"}}}},"404":{"description":"Member not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamProblemDetails"}}}}}},"delete":{"tags":["Team"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"memberId","in":"path"}],"responses":{"204":{"description":"Member removed"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamProblemDetails"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamProblemDetails"}}}},"404":{"description":"Member not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamProblemDetails"}}}},"409":{"description":"Last owner cannot be removed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamProblemDetails"}}}}}}},"/v1/sandbox/messages":{"get":{"tags":["Sandbox"],"summary":"List sandbox messages","description":"Returns recent sandbox test sends for the tenant. Only available with pv_test_* API keys.","parameters":[{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"subject","in":"query"},{"schema":{"type":"string"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string"},"required":false,"name":"recipientDomain","in":"query"}],"responses":{"200":{"description":"Sandbox messages","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxMessagesResponse"}}}},"404":{"description":"Not found (live key used)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxProblemDetails"}}}}}},"delete":{"tags":["Sandbox"],"summary":"Clear sandbox inbox","description":"Deletes all sandbox messages for the tenant. Idempotent.","responses":{"200":{"description":"Deleted count","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxDeleteResponse"}}}},"404":{"description":"Not found (live key used)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxProblemDetails"}}}}}}},"/v3/mail/send":{"post":{"tags":["SendGrid Compat"],"summary":"SendGrid-compatible send endpoint (credential swap only)","description":"Accepts SendGrid v3 /v3/mail/send request format. Swap your API key to migrate from SendGrid with zero code changes (FR40).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendGridSendRequest"}}}},"responses":{"202":{"description":"Email accepted — matches SendGrid 202 No Content format"},"400":{"description":"Validation error — SendGrid error format"},"401":{"description":"Unauthorized — SendGrid error format"},"413":{"description":"Request body too large (max 2MB)"},"429":{"description":"Rate limit exceeded"},"503":{"description":"Service temporarily unavailable"}}}},"/email":{"post":{"tags":["Postmark Compat"],"summary":"Postmark-compatible send endpoint (credential swap only)","description":"Accepts Postmark /email request format. Swap X-Postmark-Server-Token to a Postvow API key to migrate with zero code changes (FR40).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostmarkSendRequest"}}}},"responses":{"200":{"description":"Email accepted — matches Postmark 200 JSON format"},"401":{"description":"Unauthorized — Postmark error format"},"413":{"description":"Request body too large (max 2MB)"},"422":{"description":"Validation error — Postmark error format"},"429":{"description":"Rate limit exceeded"},"503":{"description":"Service temporarily unavailable"}}}},"/v3/{domain}/messages":{"post":{"tags":["Mailgun Compat"],"summary":"Mailgun-compatible send endpoint (credential swap only)","description":"Accepts Mailgun v3 /v3/{domain}/messages request format. Use Basic Auth with username \"api\" and your Postvow API key as password to migrate with zero code changes (FR40). Supports application/x-www-form-urlencoded, multipart/form-data, and application/json.","parameters":[{"schema":{"type":"string"},"required":true,"name":"domain","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailgunSendRequest"}}}},"responses":{"200":{"description":"Email accepted — matches Mailgun 200 JSON format"},"400":{"description":"Validation error or rejected field — Mailgun error format"},"401":{"description":"Unauthorized — Mailgun error format"},"404":{"description":"Domain not found or not verified"},"413":{"description":"Request body too large (max 2MB)"},"429":{"description":"Rate limit exceeded"},"503":{"description":"Service temporarily unavailable"}}}},"/v1/tools/migration-diff":{"post":{"tags":["Developer Tools"],"summary":"Generate migration diff from existing ESP code to Postvow","description":"Submit existing email sending code and receive a unified diff showing all required changes to adopt Postvow (@postvow/sdk planned API — Phase 2A).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MigrationDiffRequest"}}}},"responses":{"200":{"description":"Migration diff generated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MigrationDiffResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolsProblemDetails"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolsProblemDetails"}}}},"413":{"description":"Payload too large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolsProblemDetails"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolsProblemDetails"}}}}}}},"/v1/weather/{domain}":{"get":{"summary":"Get current Email Weather Report","description":"Returns ISP-specific delivery conditions for a sending domain. Requires send scope. Data is refreshed hourly by cron.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":253},"required":true,"name":"domain","in":"path"},{"schema":{"type":"string"},"required":false,"name":"hours","in":"query"}],"responses":{"200":{"description":"Weather report","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/WeatherReport"},{"type":"array","items":{"$ref":"#/components/schemas/WeatherReport"}}]}}}},"400":{"description":"Invalid domain or hours parameter","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/WeatherProblemDetails"}}}},"401":{"description":"Unauthorized","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/WeatherProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/WeatherProblemDetails"}}}},"404":{"description":"Domain not found or not verified","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/WeatherProblemDetails"}}}},"429":{"description":"Rate limit exceeded","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/WeatherProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/WeatherProblemDetails"}}}}}}},"/v1/check/disposable":{"post":{"tags":["Deliverability"],"summary":"Check if a single email address is disposable","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDisposableRequest"}}}},"responses":{"200":{"description":"Disposable check result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisposableCheckResult"}}}},"400":{"description":"Invalid email format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisposableProblemDetails"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisposableProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisposableProblemDetails"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisposableProblemDetails"}}}}}}},"/v1/check/disposable/batch":{"post":{"tags":["Deliverability"],"summary":"Check up to 100 email addresses for disposable status","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDisposableBatchRequest"}}}},"responses":{"200":{"description":"Batch disposable check results","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DisposableCheckResult"}}}}},"400":{"description":"Batch size exceeded or validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisposableProblemDetails"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisposableProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisposableProblemDetails"}}}},"413":{"description":"Request body too large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisposableProblemDetails"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisposableProblemDetails"}}}}}}},"/v1/jurisdiction/{email}":{"get":{"tags":["Compliance"],"summary":"Detect legal jurisdiction for a recipient email address","description":"Returns advisory `RateLimit-*` headers. Note: this endpoint has no dedicated rate limiter in Phase 1, so `RateLimit-Remaining` is a static value equal to `RateLimit-Limit` (the configured ceiling), not a live remaining count — do not rely on it for client-side pacing yet. An accurate remaining count lands with the Phase-2A rate-limit counter.","parameters":[{"schema":{"type":"string","maxLength":254,"format":"email"},"required":true,"name":"email","in":"path"}],"responses":{"200":{"description":"Jurisdiction detection result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionResult"}}}},"400":{"description":"Invalid email format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionProblemDetails"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionProblemDetails"}}}}}}},"/v1/jurisdiction/batch":{"post":{"tags":["Compliance"],"summary":"Detect jurisdiction for up to 100 email addresses","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionBatchRequest"}}}},"responses":{"200":{"description":"Batch jurisdiction detection results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionBatchResponse"}}}},"400":{"description":"Batch size exceeded or validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionProblemDetails"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionProblemDetails"}}}},"413":{"description":"Request body too large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionProblemDetails"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionProblemDetails"}}}}}}},"/v1/jurisdiction/framework/{jurisdiction}":{"get":{"tags":["Compliance"],"summary":"Get compliance framework details for a jurisdiction. DISCLAIMER: This data is for informational purposes only. Legal requirements change — verify with qualified legal counsel before relying on this data for compliance decisions.","description":"Returns advisory `RateLimit-*` headers. Note: this endpoint has no dedicated rate limiter in Phase 1, so `RateLimit-Remaining` is a static value equal to `RateLimit-Limit` (the configured ceiling), not a live remaining count — do not rely on it for client-side pacing yet. An accurate remaining count lands with the Phase-2A rate-limit counter.","parameters":[{"schema":{"type":"string","minLength":2,"maxLength":10},"required":true,"name":"jurisdiction","in":"path"}],"responses":{"200":{"description":"Compliance framework details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceFramework"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionProblemDetails"}}}},"404":{"description":"Framework not found for this jurisdiction","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionProblemDetails"}}}}}}},"/v1/forecast/{domain}":{"get":{"summary":"Get Reputation Forecast","description":"Returns a 30-day reputation trajectory with confidence intervals and actionable recommendations. Requires send scope. Forecast uses linear regression over the last 30 days of Postvow Score history.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":253},"required":true,"name":"domain","in":"path"}],"responses":{"200":{"description":"Reputation forecast with 30-day trajectory","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReputationForecast"}}}},"401":{"description":"Unauthorized","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ForecastProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ForecastProblemDetails"}}}},"404":{"description":"Domain not found or not verified","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ForecastProblemDetails"}}}},"422":{"description":"Domain verified but no score history","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ForecastProblemDetails"}}}},"429":{"description":"Rate limit exceeded","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ForecastProblemDetails"}}}},"500":{"description":"Internal error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ForecastProblemDetails"}}}}}}},"/v1/domains/{domain}/bimi":{"post":{"tags":["BIMI"],"summary":"Publish BIMI DNS record for a domain","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":255},"required":true,"name":"domain","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishBimiRequest"}}}},"responses":{"202":{"description":"BIMI record created — DNS publishing queued (bg job)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiRecord"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}},"422":{"description":"DKIM/module prerequisite not met","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}}}},"get":{"tags":["BIMI"],"summary":"Get BIMI status and readiness score for a domain","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":255},"required":true,"name":"domain","in":"path"}],"responses":{"200":{"description":"BIMI status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiStatusResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}},"404":{"description":"BIMI record not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}}}},"delete":{"tags":["BIMI"],"summary":"Remove BIMI record and DNS entry","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":255},"required":true,"name":"domain","in":"path"}],"responses":{"204":{"description":"BIMI record deleted"},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}},"404":{"description":"BIMI record not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}}}}},"/v1/domains/{domain}/bimi/vmc":{"put":{"tags":["BIMI"],"summary":"Upload VMC/CMC certificate for a domain (admin)","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":255},"required":true,"name":"domain","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateVmcRequest"}}}},"responses":{"200":{"description":"VMC certificate stored","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiRecord"}}}},"400":{"description":"Invalid PEM or certificate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}},"413":{"description":"Certificate too large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}}}}},"/v1/domains/{domain}/bimi/readiness":{"get":{"tags":["BIMI"],"summary":"Get BIMI readiness score without requiring an active record","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":255},"required":true,"name":"domain","in":"path"}],"responses":{"200":{"description":"BIMI readiness score","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiReadinessScore"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BimiProblemDetails"}}}}}}}}}