{"openapi":"3.1.0","info":{"title":"brevdue","version":"0.1.0","description":"Transactional email and passwordless auth API running on Cloudflare Workers and sending through Cloudflare Email Service.\n\n## Quick start\n\n1. Obtain an API key: request one on the landing page, or have the operator create one with `POST /v1/admin/keys`.\n2. Sign a user in: `POST /v1/auth/magic-link/start` with their email and your callback URL, then `POST /v1/auth/magic-link/verify` with the token from your callback. You get a `user` back; create your own session.\n3. Notify users later with `POST /v1/send`, addressing them by `{ \"userId\": \"usr_…\" }` or by email:\n\n```bash\ncurl -X POST https://brevdue.knutegil.workers.dev/v1/send \\\n  -H \"Authorization: Bearer em_live_...\" \\\n  -H \"Idempotency-Key: login-attempt-8f2c\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"to\":\"user@example.com\",\"template\":\"magic-link\",\"data\":{\"url\":\"https://app.example.com/auth?token=abc\",\"action\":\"sign-in\"}}'\n```\n\nTemplates: `magic-link`, `otp`, `notification`, `raw`. Branding (app name, logo, colour) normally lives on the API key so callers only send template data.\n\nRegister a webhook with `POST /v1/webhooks` to receive signed `user.*` and `message.*` events."},"servers":[{"url":"https://brevdue.knutegil.workers.dev"}],"tags":[{"name":"Auth","description":"Managed magic-link and one-time-code sign-in."},{"name":"Users","description":"Users verified through the auth flow, scoped to your key."},{"name":"Send","description":"Send emails."},{"name":"Webhooks","description":"Signed event delivery to your endpoints."},{"name":"Messages","description":"Inspect what was sent with your key."},{"name":"Templates","description":"Discover templates and your key's policy."},{"name":"Admin","description":"Operator-only key management."}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"em_live_...","description":"Service API key issued via POST /v1/admin/keys. Also accepted as `X-Api-Key`."},"AdminAuth":{"type":"http","scheme":"bearer","description":"The ADMIN_TOKEN Worker secret. Operator use only."}},"schemas":{"Message":{"type":"object","properties":{"id":{"type":"string","example":"msg_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"status":{"type":"string","enum":["sent","failed"]},"template":{"type":"string","example":"magic-link"},"from":{"type":"string","example":"login@acme.com"},"to":{"type":"array","items":{"type":"string"},"example":["user@example.com"]},"subject":{"type":"string"},"providerMessageId":{"type":["string","null"],"description":"Cloudflare Email Service message id."},"idempotencyKey":{"type":["string","null"]},"error":{"type":["object","null"],"properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]},"metadata":{"type":["object","null"],"additionalProperties":{"type":"string"}},"createdAt":{"type":"string","example":"2026-09-19T18:00:00.000Z"}},"required":["id","status","template","from","to","subject","providerMessageId","idempotencyKey","error","metadata","createdAt"]},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","forbidden","validation_error","rate_limited","not_found","provider_error","not_configured","conflict"]},"message":{"type":"string"},"details":{}},"required":["code","message"]}},"required":["error"]},"SendRequest":{"oneOf":[{"$ref":"#/components/schemas/SendMagicLink"},{"$ref":"#/components/schemas/SendOtp"},{"$ref":"#/components/schemas/SendNotification"},{"$ref":"#/components/schemas/SendRaw"}],"discriminator":{"propertyName":"template","mapping":{"magic-link":"#/components/schemas/SendMagicLink","otp":"#/components/schemas/SendOtp","notification":"#/components/schemas/SendNotification","raw":"#/components/schemas/SendRaw"}}},"SendMagicLink":{"type":"object","properties":{"to":{"anyOf":[{"type":"string","maxLength":254,"format":"email","example":"user@example.com"},{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email","example":"user@example.com"},"name":{"type":"string","maxLength":100,"example":"Jane Doe"}},"required":["email"]},{"$ref":"#/components/schemas/UserRef"},{"type":"array","items":{"$ref":"#/components/schemas/Recipient"},"minItems":1,"maxItems":10}],"description":"One recipient, or up to 10. Each is an email, an address object, or `{ userId }`."},"from":{"$ref":"#/components/schemas/Address"},"replyTo":{"allOf":[{"$ref":"#/components/schemas/Address"},{"description":"Either a bare email string or an object with email + display name."}]},"branding":{"type":"object","properties":{"appName":{"type":"string","minLength":1,"maxLength":80,"example":"Acme"},"logoUrl":{"type":"string","maxLength":2048,"format":"uri","example":"https://acme.com/logo.png"},"accentColor":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","example":"#2563eb","description":"Hex color used for buttons."},"supportEmail":{"type":"string","format":"email","example":"support@acme.com"},"footerText":{"type":"string","maxLength":300,"example":"Acme Inc, 123 Main St, Oslo"}},"description":"Overrides merged on top of the API key's default branding for this send only."},"headers":{"type":"object","additionalProperties":{"type":"string","maxLength":998},"example":{"X-Entity-Ref-ID":"signup-42"}},"metadata":{"type":"object","additionalProperties":{"type":"string","maxLength":200},"description":"Free-form key/value pairs stored with the message log (never sent to the recipient).","example":{"userId":"u_123"}},"template":{"type":"string","enum":["magic-link"]},"data":{"$ref":"#/components/schemas/MagicLinkData"}},"required":["to","template","data"]},"UserRef":{"type":"object","properties":{"userId":{"type":"string","pattern":"^usr_[0-9a-f]{32}$","example":"usr_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"}},"required":["userId"],"description":"A verified user of your app (from the managed auth flow). Resolved to their email and name."},"Recipient":{"anyOf":[{"type":"string","maxLength":254,"format":"email","example":"user@example.com"},{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email","example":"user@example.com"},"name":{"type":"string","maxLength":100,"example":"Jane Doe"}},"required":["email"]},{"$ref":"#/components/schemas/UserRef"}]},"Address":{"anyOf":[{"type":"string","maxLength":254,"format":"email","example":"user@example.com"},{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email","example":"user@example.com"},"name":{"type":"string","maxLength":100,"example":"Jane Doe"}},"required":["email"]}],"description":"Sender. Must match the API key's allowedFrom patterns. Defaults to the key's default sender, then the service default."},"MagicLinkData":{"type":"object","properties":{"url":{"type":"string","maxLength":4096,"format":"uri","example":"https://app.acme.com/auth/callback?token=eyJ...","description":"The one-time link. Generate and store the token in your app; this service never sees it again."},"action":{"type":"string","enum":["sign-in","sign-up","verify"],"default":"sign-in","description":"Controls wording and subject."},"expiresInMinutes":{"type":"integer","minimum":1,"maximum":10080,"default":15,"example":15},"recipientName":{"type":"string","maxLength":100,"example":"Knut"},"requestContext":{"type":"string","maxLength":200,"example":"Chrome on Linux · Oslo, Norway","description":"Optional device/location line shown to the user."}},"required":["url"]},"SendOtp":{"type":"object","properties":{"to":{"anyOf":[{"type":"string","maxLength":254,"format":"email","example":"user@example.com"},{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email","example":"user@example.com"},"name":{"type":"string","maxLength":100,"example":"Jane Doe"}},"required":["email"]},{"$ref":"#/components/schemas/UserRef"},{"type":"array","items":{"$ref":"#/components/schemas/Recipient"},"minItems":1,"maxItems":10}],"description":"One recipient, or up to 10. Each is an email, an address object, or `{ userId }`."},"from":{"$ref":"#/components/schemas/Address"},"replyTo":{"allOf":[{"$ref":"#/components/schemas/Address"},{"description":"Either a bare email string or an object with email + display name."}]},"branding":{"type":"object","properties":{"appName":{"type":"string","minLength":1,"maxLength":80,"example":"Acme"},"logoUrl":{"type":"string","maxLength":2048,"format":"uri","example":"https://acme.com/logo.png"},"accentColor":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","example":"#2563eb","description":"Hex color used for buttons."},"supportEmail":{"type":"string","format":"email","example":"support@acme.com"},"footerText":{"type":"string","maxLength":300,"example":"Acme Inc, 123 Main St, Oslo"}},"description":"Overrides merged on top of the API key's default branding for this send only."},"headers":{"type":"object","additionalProperties":{"type":"string","maxLength":998},"example":{"X-Entity-Ref-ID":"signup-42"}},"metadata":{"type":"object","additionalProperties":{"type":"string","maxLength":200},"description":"Free-form key/value pairs stored with the message log (never sent to the recipient).","example":{"userId":"u_123"}},"template":{"type":"string","enum":["otp"]},"data":{"$ref":"#/components/schemas/OtpData"}},"required":["to","template","data"]},"OtpData":{"type":"object","properties":{"code":{"type":"string","minLength":4,"maxLength":12,"pattern":"^[A-Za-z0-9-]+$","example":"482913"},"expiresInMinutes":{"type":"integer","minimum":1,"maximum":1440,"default":10,"example":10},"purpose":{"type":"string","maxLength":80,"default":"sign in","example":"sign in","description":"Used in copy: \"Your code to {purpose}\"."},"recipientName":{"type":"string","maxLength":100}},"required":["code"]},"SendNotification":{"type":"object","properties":{"to":{"anyOf":[{"type":"string","maxLength":254,"format":"email","example":"user@example.com"},{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email","example":"user@example.com"},"name":{"type":"string","maxLength":100,"example":"Jane Doe"}},"required":["email"]},{"$ref":"#/components/schemas/UserRef"},{"type":"array","items":{"$ref":"#/components/schemas/Recipient"},"minItems":1,"maxItems":10}],"description":"One recipient, or up to 10. Each is an email, an address object, or `{ userId }`."},"from":{"$ref":"#/components/schemas/Address"},"replyTo":{"allOf":[{"$ref":"#/components/schemas/Address"},{"description":"Either a bare email string or an object with email + display name."}]},"branding":{"type":"object","properties":{"appName":{"type":"string","minLength":1,"maxLength":80,"example":"Acme"},"logoUrl":{"type":"string","maxLength":2048,"format":"uri","example":"https://acme.com/logo.png"},"accentColor":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","example":"#2563eb","description":"Hex color used for buttons."},"supportEmail":{"type":"string","format":"email","example":"support@acme.com"},"footerText":{"type":"string","maxLength":300,"example":"Acme Inc, 123 Main St, Oslo"}},"description":"Overrides merged on top of the API key's default branding for this send only."},"headers":{"type":"object","additionalProperties":{"type":"string","maxLength":998},"example":{"X-Entity-Ref-ID":"signup-42"}},"metadata":{"type":"object","additionalProperties":{"type":"string","maxLength":200},"description":"Free-form key/value pairs stored with the message log (never sent to the recipient).","example":{"userId":"u_123"}},"template":{"type":"string","enum":["notification"]},"data":{"$ref":"#/components/schemas/NotificationData"}},"required":["to","template","data"]},"NotificationData":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":150,"example":"Your export is ready"},"body":{"type":"string","minLength":1,"maxLength":5000,"example":"The CSV export you requested has finished.\n\nIt will be available for 24 hours.","description":"Plain text. Blank lines separate paragraphs. HTML is escaped."},"ctaLabel":{"type":"string","maxLength":60,"example":"Download export"},"ctaUrl":{"type":"string","maxLength":4096,"format":"uri","example":"https://app.acme.com/exports/123"},"recipientName":{"type":"string","maxLength":100}},"required":["title","body"]},"SendRaw":{"type":"object","properties":{"to":{"anyOf":[{"type":"string","maxLength":254,"format":"email","example":"user@example.com"},{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email","example":"user@example.com"},"name":{"type":"string","maxLength":100,"example":"Jane Doe"}},"required":["email"]},{"$ref":"#/components/schemas/UserRef"},{"type":"array","items":{"$ref":"#/components/schemas/Recipient"},"minItems":1,"maxItems":10}],"description":"One recipient, or up to 10. Each is an email, an address object, or `{ userId }`."},"from":{"$ref":"#/components/schemas/Address"},"replyTo":{"allOf":[{"$ref":"#/components/schemas/Address"},{"description":"Either a bare email string or an object with email + display name."}]},"branding":{"type":"object","properties":{"appName":{"type":"string","minLength":1,"maxLength":80,"example":"Acme"},"logoUrl":{"type":"string","maxLength":2048,"format":"uri","example":"https://acme.com/logo.png"},"accentColor":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","example":"#2563eb","description":"Hex color used for buttons."},"supportEmail":{"type":"string","format":"email","example":"support@acme.com"},"footerText":{"type":"string","maxLength":300,"example":"Acme Inc, 123 Main St, Oslo"}},"description":"Overrides merged on top of the API key's default branding for this send only."},"headers":{"type":"object","additionalProperties":{"type":"string","maxLength":998},"example":{"X-Entity-Ref-ID":"signup-42"}},"metadata":{"type":"object","additionalProperties":{"type":"string","maxLength":200},"description":"Free-form key/value pairs stored with the message log (never sent to the recipient).","example":{"userId":"u_123"}},"template":{"type":"string","enum":["raw"]},"data":{"$ref":"#/components/schemas/RawData"}},"required":["to","template","data"]},"RawData":{"type":"object","properties":{"subject":{"type":"string","minLength":1,"maxLength":200,"example":"Hello from Acme"},"html":{"type":"string","maxLength":500000,"example":"<h1>Hello</h1>"},"text":{"type":"string","maxLength":500000,"example":"Hello"}},"required":["subject"]},"ChallengeStarted":{"type":"object","properties":{"challengeId":{"type":"string","example":"ch_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"email":{"type":"string","format":"email"},"expiresAt":{"type":"string"},"messageId":{"type":"string","description":"Id of the email in /v1/messages."}},"required":["challengeId","email","expiresAt","messageId"]},"MagicLinkStart":{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email","example":"user@example.com"},"callbackUrl":{"type":"string","maxLength":2048,"format":"uri","example":"https://app.acme.com/auth/callback","description":"Your endpoint. Brevdue appends `?token=…` (or `&token=…`). It must POST the token to /v1/auth/magic-link/verify server-side."},"name":{"type":"string","maxLength":100,"description":"Stored on the user record on first verification."},"action":{"type":"string","enum":["sign-in","sign-up","verify"],"default":"sign-in"},"expiresInMinutes":{"type":"integer","minimum":1,"maximum":1440,"default":15},"requestContext":{"type":"string","maxLength":200,"example":"Chrome on Linux · Oslo"},"branding":{"type":"object","properties":{"appName":{"type":"string","minLength":1,"maxLength":80,"example":"Acme"},"logoUrl":{"type":"string","maxLength":2048,"format":"uri","example":"https://acme.com/logo.png"},"accentColor":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","example":"#2563eb","description":"Hex color used for buttons."},"supportEmail":{"type":"string","format":"email","example":"support@acme.com"},"footerText":{"type":"string","maxLength":300,"example":"Acme Inc, 123 Main St, Oslo"}}},"metadata":{"type":"object","additionalProperties":{"type":"string","maxLength":500},"description":"Echoed back on verify. Use it for a return path or a client nonce.","example":{"returnTo":"/dashboard"}}},"required":["email","callbackUrl"]},"Verified":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/User"},"isNewUser":{"type":"boolean"},"challengeId":{"type":"string"},"metadata":{"type":["object","null"],"additionalProperties":{"type":"string"}}},"required":["user","isNewUser","challengeId","metadata"],"description":"The user is verified. Create your own session for them now."},"User":{"type":"object","properties":{"id":{"type":"string","example":"usr_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"email":{"type":"string","format":"email"},"name":{"type":["string","null"]},"metadata":{"type":["object","null"],"additionalProperties":{}},"verifiedCount":{"type":"integer"},"firstVerifiedAt":{"type":["string","null"]},"lastVerifiedAt":{"type":["string","null"]},"blocked":{"type":"boolean"},"createdAt":{"type":"string"}},"required":["id","email","name","metadata","verifiedCount","firstVerifiedAt","lastVerifiedAt","blocked","createdAt"]},"MagicLinkVerify":{"type":"object","properties":{"token":{"type":"string","minLength":20,"maxLength":200}},"required":["token"]},"OtpStart":{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email"},"name":{"type":"string","maxLength":100},"purpose":{"type":"string","maxLength":80,"default":"sign in"},"expiresInMinutes":{"type":"integer","minimum":1,"maximum":60,"default":10},"branding":{"type":"object","properties":{"appName":{"type":"string","minLength":1,"maxLength":80,"example":"Acme"},"logoUrl":{"type":"string","maxLength":2048,"format":"uri","example":"https://acme.com/logo.png"},"accentColor":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","example":"#2563eb","description":"Hex color used for buttons."},"supportEmail":{"type":"string","format":"email","example":"support@acme.com"},"footerText":{"type":"string","maxLength":300,"example":"Acme Inc, 123 Main St, Oslo"}}},"metadata":{"type":"object","additionalProperties":{"type":"string","maxLength":500},"description":"Echoed back on verify. Use it for a return path or a client nonce.","example":{"returnTo":"/dashboard"}}},"required":["email"]},"OtpVerify":{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email"},"code":{"type":"string","pattern":"^\\d{6}$"}},"required":["email","code"]},"UserList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/User"}},"nextCursor":{"type":["string","null"]}},"required":["items","nextCursor"]},"UpdateUser":{"type":"object","properties":{"name":{"type":["string","null"],"maxLength":100},"metadata":{"type":["object","null"],"additionalProperties":{}},"blocked":{"type":"boolean","description":"Blocked users cannot start sign-in and are skipped as recipients."}}},"CreatedWebhook":{"allOf":[{"$ref":"#/components/schemas/Webhook"},{"type":"object","properties":{"secret":{"type":"string","example":"whsec_…","description":"HMAC-SHA256 signing secret, shown once. Verify `Brevdue-Signature: t=<unix>,v1=<hex>` where v1 = HMAC(secret, `${t}.${rawBody}`)."}},"required":["secret"]}]},"Webhook":{"type":"object","properties":{"id":{"type":"string","example":"wh_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"description":{"type":["string","null"]},"active":{"type":"boolean"},"createdAt":{"type":"string"}},"required":["id","url","events","description","active","createdAt"]},"CreateWebhook":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri","example":"https://app.acme.com/webhooks/brevdue"},"events":{"type":"array","items":{"anyOf":[{"$ref":"#/components/schemas/WebhookEventType"},{"type":"string","enum":["*"]}]},"minItems":1,"maxItems":20,"default":["*"]},"description":{"type":"string","maxLength":200}},"required":["url"]},"WebhookEventType":{"type":"string","enum":["user.created","user.verified","message.sent","message.failed","webhook.test"]},"UpdateWebhook":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"events":{"type":"array","items":{"anyOf":[{"$ref":"#/components/schemas/WebhookEventType"},{"type":"string","enum":["*"]}]},"minItems":1,"maxItems":20},"description":{"type":["string","null"],"maxLength":200},"active":{"type":"boolean"}}},"WebhookDelivery":{"type":"object","properties":{"id":{"type":"string"},"eventId":{"type":"string"},"event":{"type":"string"},"status":{"type":"string","enum":["pending","delivered","failed"]},"attempts":{"type":"integer"},"lastStatusCode":{"type":["integer","null"]},"lastError":{"type":["string","null"]},"nextAttemptAt":{"type":["string","null"]},"deliveredAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","eventId","event","status","attempts","lastStatusCode","lastError","nextAttemptAt","deliveredAt","createdAt"]},"MessageList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"nextCursor":{"type":["string","null"],"description":"Pass as ?cursor= to fetch the next page."}},"required":["items","nextCursor"]},"TemplateInfo":{"type":"object","properties":{"name":{"type":"string","example":"magic-link"},"description":{"type":"string"},"dataSchema":{"type":"object","additionalProperties":{},"description":"JSON Schema for the `data` field."}},"required":["name","description","dataSchema"]},"ApiKey":{"type":"object","properties":{"id":{"type":"string","example":"key_5c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f"},"name":{"type":"string"},"keyPrefix":{"type":"string","example":"em_live_3kJ9xQ2p"},"allowedFrom":{"type":["array","null"],"items":{"type":"string"}},"allowedTemplates":{"type":["array","null"],"items":{"type":"string"}},"allowedRecipientDomains":{"type":["array","null"],"items":{"type":"string"}},"allowedCallbackOrigins":{"type":["array","null"],"items":{"type":"string"}},"defaults":{"type":"object","properties":{"from":{"type":"string","format":"email"},"fromName":{"type":"string","maxLength":100},"replyTo":{"type":"string","format":"email"},"branding":{"$ref":"#/components/schemas/Branding"}}},"createdAt":{"type":"string"},"lastUsedAt":{"type":["string","null"]},"revokedAt":{"type":["string","null"]}},"required":["id","name","keyPrefix","allowedFrom","allowedTemplates","allowedRecipientDomains","allowedCallbackOrigins","defaults","createdAt","lastUsedAt","revokedAt"]},"Branding":{"type":"object","properties":{"appName":{"type":"string","minLength":1,"maxLength":80,"example":"Acme"},"logoUrl":{"type":"string","maxLength":2048,"format":"uri","example":"https://acme.com/logo.png"},"accentColor":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","example":"#2563eb","description":"Hex color used for buttons."},"supportEmail":{"type":"string","format":"email","example":"support@acme.com"},"footerText":{"type":"string","maxLength":300,"example":"Acme Inc, 123 Main St, Oslo"}},"required":["appName"],"description":"Visual identity applied to built-in templates. Usually set once per API key (defaults.branding) so callers only send template data."},"CreatedApiKey":{"allOf":[{"$ref":"#/components/schemas/ApiKey"},{"type":"object","properties":{"key":{"type":"string","example":"em_live_3kJ9xQ2pLm...","description":"The full API key. Shown exactly once; store it as a secret in the consuming app."}},"required":["key"]}]},"CreateApiKey":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80,"example":"acme-web (production)"},"allowedFrom":{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":254},"maxItems":50,"description":"Sender patterns the key may set in `from`: \"login@acme.com\", \"*@acme.com\". null = may not override the default sender.","example":["*@acme.com"]},"allowedTemplates":{"type":["array","null"],"items":{"type":"string"},"description":"Template names the key may use. null = all.","example":["magic-link","otp"]},"allowedRecipientDomains":{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":254},"maxItems":50,"description":"Recipient domain patterns (\"acme.com\", \"*\"). null = any recipient. Useful for internal-only keys.","example":null},"allowedCallbackOrigins":{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":254},"maxItems":50,"description":"Origins that magic-link callbackUrl may point to, e.g. [\"https://app.acme.com\"]. null = any https URL (plus http://localhost for development).","example":["https://app.acme.com"]},"defaults":{"type":["object","null"],"properties":{"from":{"type":"string","format":"email"},"fromName":{"type":"string","maxLength":100},"replyTo":{"type":"string","format":"email"},"branding":{"$ref":"#/components/schemas/Branding"}},"description":"Per-key defaults so callers can omit from/branding.","example":{"from":"login@acme.com","fromName":"Acme","branding":{"appName":"Acme","accentColor":"#16a34a"}}}},"required":["name"]},"UpdateApiKey":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80},"allowedFrom":{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":254},"maxItems":50,"description":"Sender patterns the key may set in `from`: \"login@acme.com\", \"*@acme.com\". null = may not override the default sender.","example":["*@acme.com"]},"allowedTemplates":{"type":["array","null"],"items":{"type":"string"},"description":"Template names the key may use. null = all.","example":["magic-link","otp"]},"allowedRecipientDomains":{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":254},"maxItems":50,"description":"Recipient domain patterns (\"acme.com\", \"*\"). null = any recipient. Useful for internal-only keys.","example":null},"allowedCallbackOrigins":{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":254},"maxItems":50,"description":"Origins that magic-link callbackUrl may point to, e.g. [\"https://app.acme.com\"]. null = any https URL (plus http://localhost for development).","example":["https://app.acme.com"]},"defaults":{"type":["object","null"],"properties":{"from":{"type":"string","format":"email"},"fromName":{"type":"string","maxLength":100},"replyTo":{"type":"string","format":"email"},"branding":{"$ref":"#/components/schemas/Branding"}},"description":"Per-key defaults so callers can omit from/branding.","example":{"from":"login@acme.com","fromName":"Acme","branding":{"appName":"Acme","accentColor":"#16a34a"}}}}},"AccessRequest":{"type":"object","properties":{"id":{"type":"string","example":"req_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"name":{"type":"string"},"email":{"type":"string"},"appName":{"type":"string"},"website":{"type":["string","null"]},"useCase":{"type":"string"},"status":{"type":"string","enum":["pending","approved","rejected"]},"apiKeyId":{"type":["string","null"]},"claimExpiresAt":{"type":["string","null"]},"claimedAt":{"type":["string","null"]},"note":{"type":["string","null"]},"createdAt":{"type":"string"},"decidedAt":{"type":["string","null"]}},"required":["id","name","email","appName","website","useCase","status","apiKeyId","claimExpiresAt","claimedAt","note","createdAt","decidedAt"]},"ApproveAccessRequest":{"type":"object","properties":{"keyName":{"type":"string","minLength":1,"maxLength":80,"description":"Defaults to the requested app name."},"allowedFrom":{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":254},"maxItems":50,"description":"Sender patterns the key may set in `from`: \"login@acme.com\", \"*@acme.com\". null = may not override the default sender.","example":["*@acme.com"]},"allowedTemplates":{"type":["array","null"],"items":{"type":"string"},"description":"Template names the key may use. null = all.","example":["magic-link","otp"]},"allowedRecipientDomains":{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":254},"maxItems":50,"description":"Recipient domain patterns (\"acme.com\", \"*\"). null = any recipient. Useful for internal-only keys.","example":null},"allowedCallbackOrigins":{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":254},"maxItems":50,"description":"Origins that magic-link callbackUrl may point to, e.g. [\"https://app.acme.com\"]. null = any https URL (plus http://localhost for development).","example":["https://app.acme.com"]},"defaults":{"type":["object","null"],"properties":{"from":{"type":"string","format":"email"},"fromName":{"type":"string","maxLength":100},"replyTo":{"type":"string","format":"email"},"branding":{"$ref":"#/components/schemas/Branding"}},"description":"Per-key defaults so callers can omit from/branding.","example":{"from":"login@acme.com","fromName":"Acme","branding":{"appName":"Acme","accentColor":"#16a34a"}}},"note":{"type":"string","maxLength":500}},"description":"The key is created when the requester opens their one-time claim link, using this policy."}},"parameters":{}},"paths":{"/v1/send":{"post":{"tags":["Send"],"summary":"Send a transactional email","description":"Renders one of the built-in templates (or raw html/text) and sends it through Cloudflare Email Service.\n\nRecipients can be email addresses or `{ \"userId\": \"usr_…\" }` references to users verified through the managed auth flow.\n\n**Idempotency:** pass an `Idempotency-Key` header (any unique string, e.g. your login-attempt id). Retrying with the same key returns the original message instead of sending again. Only successful sends are replayed; a retry after a provider failure sends again.\n\n**Rate limits:** per API key (120/min) and per recipient address (5/min). Exceeding either returns 429.\n\n**Webhooks:** emits `message.sent` or `message.failed`.","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":128,"example":"login-attempt-8f2c"},"required":false,"name":"idempotency-key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendRequest"}}}},"responses":{"200":{"description":"Replayed: an identical Idempotency-Key was already processed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"202":{"description":"Accepted and handed to Cloudflare Email Service.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing/invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is not allowed to use this sender, template or recipient domain, or the user is blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"A referenced userId does not exist for this key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Cloudflare Email Service rejected the message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/auth/magic-link/start":{"post":{"tags":["Auth"],"summary":"Start a magic-link sign-in","description":"Creates a single-use token, emails a branded magic link to the address, and returns the challenge id.\nThe link is `callbackUrl?token=…`. When the user lands on your callback, POST the token to **/v1/auth/magic-link/verify** from your server and create your own session from the returned user.\n\nTokens are stored hashed. Unknown email addresses are accepted (that is how sign-up works); apply your own allow-list before calling if you need one.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicLinkStart"}}}},"responses":{"202":{"description":"Challenge created and email handed to the provider.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChallengeStarted"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Policy violation (callback origin, template, blocked user).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (per key and per email).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Email provider error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/auth/magic-link/verify":{"post":{"tags":["Auth"],"summary":"Verify a magic-link token","description":"Consumes the token (single use). On success the user is created or updated and `user.verified` (plus `user.created` the first time) webhooks fire. Call this from your server, never from the browser.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicLinkVerify"}}}},"responses":{"200":{"description":"Verified.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Verified"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Invalid, expired, already used, or too many attempts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/auth/otp/start":{"post":{"tags":["Auth"],"summary":"Start a one-time-code sign-in","description":"Emails a 6-digit code. Verify it with **/v1/auth/otp/verify**. Five wrong attempts invalidate the code.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpStart"}}}},"responses":{"202":{"description":"Challenge created and email handed to the provider.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChallengeStarted"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Policy violation (callback origin, template, blocked user).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (per key and per email).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Email provider error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/auth/otp/verify":{"post":{"tags":["Auth"],"summary":"Verify a one-time code","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpVerify"}}}},"responses":{"200":{"description":"Verified.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Verified"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Invalid, expired, already used, or too many attempts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/users":{"get":{"tags":["Users"],"summary":"List users verified for this app","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"string","description":"Exact match (case-insensitive)."},"required":false,"description":"Exact match (case-insensitive).","name":"email","in":"query"}],"responses":{"200":{"description":"Newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserList"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/users/{id}":{"get":{"tags":["Users"],"summary":"Get a user","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","example":"usr_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Users"],"summary":"Update a user (name, metadata, blocked)","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","example":"usr_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUser"}}}},"responses":{"200":{"description":"Updated user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Users"],"summary":"Delete a user","description":"Removes the user record and their pending challenges. Message logs are kept.","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","example":"usr_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}},"required":["deleted"]}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks":{"post":{"tags":["Webhooks"],"summary":"Register a webhook endpoint","description":"Brevdue POSTs a JSON event `{ id, type, createdAt, data }` to the URL. Deliveries are at-least-once with retries over ~9 hours; deduplicate on `id`.\n\nVerify the `Brevdue-Signature` header: split `t=<unix>,v1=<hex>`, compute HMAC-SHA256(secret, `<t>.<raw body>`), compare to v1 in constant time and reject if `t` is older than 5 minutes.\n\nEvents: `user.created`, `user.verified`, `message.sent`, `message.failed`, `webhook.test`, or `*`.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhook"}}}},"responses":{"201":{"description":"Created. The secret is shown once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedWebhook"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Webhooks"],"summary":"List webhooks","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Webhooks.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}},"required":["items"]}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks/{id}":{"get":{"tags":["Webhooks"],"summary":"Get a webhook","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","example":"wh_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Webhook.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Webhooks"],"summary":"Update a webhook","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","example":"wh_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhook"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Webhooks"],"summary":"Delete a webhook","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","example":"wh_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}},"required":["deleted"]}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks/{id}/test":{"post":{"tags":["Webhooks"],"summary":"Send a webhook.test event to this endpoint","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","example":"wh_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"required":true,"name":"id","in":"path"}],"responses":{"202":{"description":"Queued.","content":{"application/json":{"schema":{"type":"object","properties":{"eventId":{"type":["string","null"]}},"required":["eventId"]}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks/{id}/deliveries":{"get":{"tags":["Webhooks"],"summary":"Recent deliveries for a webhook","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","example":"wh_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"required":true,"name":"id","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}}},"required":["items"]}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/messages":{"get":{"tags":["Messages"],"summary":"List messages sent with this API key","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous response."},"required":false,"description":"Opaque cursor from a previous response.","name":"cursor","in":"query"},{"schema":{"type":"string","description":"Filter by exact recipient address."},"required":false,"description":"Filter by exact recipient address.","name":"to","in":"query"}],"responses":{"200":{"description":"Newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageList"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/messages/{id}":{"get":{"tags":["Messages"],"summary":"Get a message by id","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","example":"msg_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found (or belongs to another key).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/templates":{"get":{"tags":["Templates"],"summary":"List available templates and their data schemas","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Templates.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TemplateInfo"}}},"required":["items"]}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/me":{"get":{"tags":["Templates"],"summary":"Describe the authenticated API key","description":"Returns the policy (allowed senders/templates/recipient domains) and defaults for the calling key. Handy when wiring up a new app.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"The key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKey"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/admin/keys":{"post":{"tags":["Admin"],"summary":"Create an API key for one of your apps","description":"The raw key is returned once. Store it in the consuming app as a secret.","security":[{"AdminAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKey"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedApiKey"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Invalid admin token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"ADMIN_TOKEN secret not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Admin"],"summary":"List API keys","security":[{"AdminAuth":[]}],"parameters":[{"schema":{"type":"string","enum":["true","false"],"default":"false"},"required":false,"name":"includeRevoked","in":"query"}],"responses":{"200":{"description":"Keys (hashes are never returned).","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}},"required":["items"]}}}},"401":{"description":"Invalid admin token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"ADMIN_TOKEN secret not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/admin/keys/{id}":{"get":{"tags":["Admin"],"summary":"Get an API key","security":[{"AdminAuth":[]}],"parameters":[{"schema":{"type":"string","example":"key_5c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKey"}}}},"401":{"description":"Invalid admin token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"ADMIN_TOKEN secret not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Admin"],"summary":"Update an API key policy or defaults","description":"Only fields present in the body are changed. Send `null` to clear a list.","security":[{"AdminAuth":[]}],"parameters":[{"schema":{"type":"string","example":"key_5c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateApiKey"}}}},"responses":{"200":{"description":"Updated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKey"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Invalid admin token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"ADMIN_TOKEN secret not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Admin"],"summary":"Revoke an API key","description":"Revocation is immediate and permanent; the key row is kept for the audit trail.","security":[{"AdminAuth":[]}],"parameters":[{"schema":{"type":"string","example":"key_5c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Revoked key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKey"}}}},"401":{"description":"Invalid admin token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"ADMIN_TOKEN secret not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/admin/access-requests":{"get":{"tags":["Admin"],"summary":"List access requests from the landing page","security":[{"AdminAuth":[]}],"parameters":[{"schema":{"type":"string","enum":["pending","approved","rejected","all"],"default":"pending"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Requests, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AccessRequest"}}},"required":["items"]}}}},"401":{"description":"Invalid admin token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"ADMIN_TOKEN secret not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/admin/access-requests/{id}/approve":{"post":{"tags":["Admin"],"summary":"Approve an access request and email a one-time claim link","description":"Stores the key policy and emails the requester a link valid for 72 hours. The API key itself is generated when they open it, so nothing secret is in the email. Approving twice is refused (409); use `…/resend-claim` if the link was lost or expired.","security":[{"AdminAuth":[]}],"parameters":[{"schema":{"type":"string","example":"req_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApproveAccessRequest"}}}},"responses":{"200":{"description":"Approved; claim email sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessRequest"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Invalid admin token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already approved, claimed or rejected.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Claim email could not be sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"ADMIN_TOKEN secret not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/admin/access-requests/{id}/resend-claim":{"post":{"tags":["Admin"],"summary":"Send a fresh claim link for an approved, unclaimed request","description":"Invalidates the previous link. Use when the requester lost the email or the 72-hour window passed.","security":[{"AdminAuth":[]}],"parameters":[{"schema":{"type":"string","example":"req_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"New claim email sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessRequest"}}}},"401":{"description":"Invalid admin token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Not approved yet, or already claimed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Claim email could not be sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"ADMIN_TOKEN secret not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/admin/access-requests/{id}/reject":{"post":{"tags":["Admin"],"summary":"Reject an access request","description":"No email is sent to the requester.","security":[{"AdminAuth":[]}],"parameters":[{"schema":{"type":"string","example":"req_1f3c9a2e4b6d4c1e9a7b2c3d4e5f6a7b"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"note":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"Rejected.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessRequest"}}}},"401":{"description":"Invalid admin token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already claimed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"ADMIN_TOKEN secret not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/admin/keys/{id}/users":{"get":{"tags":["Admin"],"summary":"List users verified under a key","security":[{"AdminAuth":[]}],"parameters":[{"schema":{"type":"string","example":"key_5c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f"},"required":true,"name":"id","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Users, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/User"}}},"required":["items"]}}}},"401":{"description":"Invalid admin token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"ADMIN_TOKEN secret not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{}}