{"openapi":"3.0.3","info":{"title":"Consent & Unsub Tokens API","version":"1.0.0","description":"Stateless HMAC-SHA256 signed consent and RFC 8058 One-Click unsubscribe token issuer and verifier with key rotation (kid)."},"servers":[{"url":"https://api.tinplate.dev","description":"Production Edge Gateway"}],"components":{"schemas":{"ConsentIssueResponse":{"type":"object","properties":{"token":{"type":"string"},"kid":{"type":"string","example":"v1"},"issued_at":{"type":"string"},"expires_at":{"type":"string"},"payload":{"type":"object","properties":{"sub":{"type":"string","example":"user_12345"},"scope":{"type":"string","example":"marketing_email"},"purpose":{"type":"string","nullable":true,"example":"newsletter"},"iat":{"type":"integer","example":1794628800},"exp":{"type":"integer","example":1797220800},"jti":{"type":"string","example":"b3b8c9d0e1f2a3b4"}},"required":["sub","scope","purpose","iat","exp","jti"]},"rfc8058_headers":{"type":"object","nullable":true,"properties":{"List-Unsubscribe":{"type":"string"},"List-Unsubscribe-Post":{"type":"string","enum":["List-Unsubscribe=One-Click"]}},"required":["List-Unsubscribe","List-Unsubscribe-Post"]}},"required":["token","kid","issued_at","expires_at","payload","rfc8058_headers"]},"ConsentIssueRequest":{"type":"object","properties":{"subject":{"type":"string","minLength":1,"maxLength":256,"example":"user_12345","description":"Opaque subscriber or user identifier (avoid raw email if not needed)."},"scope":{"type":"string","minLength":1,"maxLength":128,"example":"marketing_email","description":"Consent or unsubscribe scope."},"purpose":{"type":"string","minLength":1,"maxLength":128,"example":"newsletter"},"ttl_seconds":{"type":"integer","minimum":60,"maximum":31536000,"example":2592000,"description":"Token lifetime in seconds (default 30 days)."},"kid":{"type":"string","minLength":1,"maxLength":64,"example":"v1","description":"Key identifier for key rotation."},"secret":{"type":"string","minLength":16,"maxLength":512,"example":"replace-with-your-32-byte-hmac-secret","description":"Tenant HMAC-SHA256 signing secret (min 16 chars, never logged or stored)."},"unsubscribe_base_url":{"type":"string","maxLength":512,"format":"uri","example":"https://example.com/unsubscribe","description":"Optional HTTPS URL to generate RFC 8058 List-Unsubscribe headers."}},"required":["subject","scope","secret"]},"ConsentVerifyResponse":{"type":"object","properties":{"valid":{"type":"boolean","example":true},"expired":{"type":"boolean","example":false},"reason":{"type":"string","nullable":true,"example":null},"kid":{"type":"string","nullable":true,"example":"v1"},"payload":{"type":"object","nullable":true,"properties":{"sub":{"type":"string","example":"user_12345"},"scope":{"type":"string","example":"marketing_email"},"purpose":{"type":"string","nullable":true,"example":"newsletter"},"iat":{"type":"integer","example":1794628800},"exp":{"type":"integer","example":1797220800},"jti":{"type":"string","example":"b3b8c9d0e1f2a3b4"}},"required":["sub","scope","purpose","iat","exp","jti"]}},"required":["valid","expired","reason","kid","payload"]},"ConsentVerifyRequest":{"type":"object","properties":{"token":{"type":"string","minLength":10,"maxLength":4096},"secret":{"type":"string","minLength":16,"maxLength":512,"example":"replace-with-your-32-byte-hmac-secret","description":"Single signing secret (if not using keys map)."},"keys":{"type":"object","additionalProperties":{"type":"string","minLength":16,"maxLength":512},"example":{"v1":"replace-with-your-32-byte-hmac-secret"},"description":"Map of kid -> secret for key rotation."},"expected_scope":{"type":"string","minLength":1,"maxLength":128,"example":"marketing_email"},"now_epoch":{"type":"integer","minimum":0}},"required":["token"]},"ResponseMeta":{"type":"object","required":["request_id"],"properties":{"request_id":{"type":"string","format":"uuid"},"ms":{"type":"integer","minimum":0},"version":{"type":"string","example":"1.0.0"}}},"ErrorEnvelope":{"type":"object","required":["ok","error","meta"],"properties":{"ok":{"type":"boolean","enum":[false]},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","example":"invalid_input"},"message":{"type":"string","example":"Request body failed validation"}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}},"parameters":{}},"paths":{"/v1/consent-tokens/issue":{"post":{"tags":["15_CONSENT_TOKENS"],"summary":"Issue a signed consent or RFC 8058 one-click unsubscribe token","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentIssueRequest"}}}},"responses":{"200":{"description":"Signed token and optional RFC 8058 headers","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/ConsentIssueResponse"},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["ok","data","meta"]}}}},"400":{"description":"Invalid input or malformed JSON request body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"413":{"description":"Request payload exceeds subscribed plan byte cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/consent-tokens/verify":{"post":{"tags":["15_CONSENT_TOKENS"],"summary":"Verify a signed consent or unsubscribe token","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentVerifyRequest"}}}},"responses":{"200":{"description":"Verification result and decoded payload","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/ConsentVerifyResponse"},"meta":{"$ref":"#/components/schemas/ResponseMeta"}},"required":["ok","data","meta"]}}}},"400":{"description":"Invalid input or malformed JSON request body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"413":{"description":"Request payload exceeds subscribed plan byte cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}