{"openapi":"3.0.0","paths":{"/v1/workspaces/{workspace_id}/brand":{"get":{"operationId":"BrandController_get","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Get the resolved brand tokens for the workspace. Always returns a complete object, with platform defaults filling in any unset fields.","tags":["brand"]},"put":{"operationId":"BrandController_update","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBrandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandResponseDto"}}}},"400":{"description":"Validation failure on the brand payload."}},"security":[{"bearer":[]}],"summary":"Replace the workspace brand. Any field omitted from the body is reset to the platform default for that field. To partially-update, send back what GET returned with the desired diff applied.","tags":["brand"]},"delete":{"operationId":"BrandController_reset","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Brand customisations cleared."}},"security":[{"bearer":[]}],"summary":"Remove all brand customizations and revert to platform defaults.","tags":["brand"]}},"/v1/workspaces/{workspace_id}/domains":{"post":{"operationId":"DomainController_createDomain","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDomainDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainDto"}}}},"400":{"description":"Validation failure on the request body."},"409":{"description":"Domain already registered (globally unique)."}},"security":[{"bearer":[]}],"summary":"Register a domain for a workspace","tags":["domains"]},"get":{"operationId":"DomainController_listDomains","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListDomainsResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List domains registered to this workspace","tags":["domains"]}},"/v1/workspaces/{workspace_id}/domains/{domain_id}":{"get":{"operationId":"DomainController_getDomain","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainDto"}}}},"404":{"description":"Domain not found in this workspace."}},"security":[{"bearer":[]}],"summary":"Get a single domain (with DNS instructions).","tags":["domains"]},"patch":{"operationId":"DomainController_updateDomain","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDomainDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainDto"}}}},"400":{"description":"Validation failure on the request body."},"404":{"description":"Domain not found in this workspace."}},"security":[{"bearer":[]}],"summary":"Update user-side domain metadata (today: dnsProvider; the wizard saves the user's provider choice here so hostnames render correctly across reloads).","tags":["domains"]},"delete":{"operationId":"DomainController_deleteDomain","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Domain deleted."},"404":{"description":"Domain not found in this workspace."},"409":{"description":"Domain has existing mailboxes; delete them first"}},"security":[{"bearer":[]}],"summary":"Delete a domain (must have no mailboxes left).","tags":["domains"]}},"/v1/workspaces/{workspace_id}/domains/{domain_id}/verify":{"post":{"operationId":"DomainController_verifyDomain","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainDto"}}}},"400":{"description":"Verification failed — one or more required records are still missing or wrong."},"404":{"description":"Domain not found in this workspace."}},"security":[{"bearer":[]}],"summary":"Run a synchronous DNS check against this domain; flips status to active when the required record set passes.","tags":["domains"]}},"/v1/workspaces/{workspace_id}/smtp-config":{"get":{"operationId":"SmtpConfigController_get","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSmtpConfigResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Get the workspace-default SMTP config (returns null if unset). Per-domain overrides live under /workspaces/:id/domains/:domainId/smtp-config.","tags":["smtp-config"]},"put":{"operationId":"SmtpConfigController_upsert","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertSmtpConfigDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmtpConfigDto"}}}},"400":{"description":"Validation failure or username/password mismatch."}},"security":[{"bearer":[]}],"summary":"Create or update the workspace-default SMTP config (resets verification)","tags":["smtp-config"]},"delete":{"operationId":"SmtpConfigController_delete","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Workspace-default SMTP config deleted."},"404":{"description":"No SMTP config to delete"}},"security":[{"bearer":[]}],"summary":"Revert the workspace to the shared SMTP relay","tags":["smtp-config"]}},"/v1/workspaces/{workspace_id}/smtp-config/verify":{"post":{"operationId":"SmtpConfigController_verifyTransient","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifySmtpConfigDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmtpVerifyResultDto"}}}},"400":{"description":"Validation failure on the request body."}},"security":[{"bearer":[]}],"summary":"Test arbitrary SMTP creds without persisting","tags":["smtp-config"]}},"/v1/workspaces/{workspace_id}/smtp-config/verify-saved":{"post":{"operationId":"SmtpConfigController_verifySaved","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmtpVerifyResultDto"}}}},"404":{"description":"No SMTP config saved"}},"security":[{"bearer":[]}],"summary":"Test the persisted workspace-default SMTP config; updates status","tags":["smtp-config"]}},"/v1/workspaces/{workspace_id}/smtp-configs":{"get":{"operationId":"SmtpConfigListController_list","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListSmtpConfigsResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List every SMTP config in the workspace (default + per-domain overrides)","tags":["smtp-config"]}},"/v1/workspaces/{workspace_id}/domains/{domain_id}/smtp-config":{"get":{"operationId":"DomainSmtpConfigController_get","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSmtpConfigResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Get the per-domain SMTP override (returns null if unset; the workspace default applies in that case).","tags":["smtp-config"]},"put":{"operationId":"DomainSmtpConfigController_upsert","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertSmtpConfigDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmtpConfigDto"}}}},"400":{"description":"Validation failure or username/password mismatch."},"404":{"description":"Domain not found in this workspace."}},"security":[{"bearer":[]}],"summary":"Create or update the per-domain SMTP override (resets verification)","tags":["smtp-config"]},"delete":{"operationId":"DomainSmtpConfigController_delete","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Per-domain override deleted."},"404":{"description":"No per-domain override to delete"}},"security":[{"bearer":[]}],"summary":"Drop the per-domain override; sends from this domain fall back to the workspace default (or shared relay).","tags":["smtp-config"]}},"/v1/workspaces/{workspace_id}/domains/{domain_id}/smtp-config/verify-saved":{"post":{"operationId":"DomainSmtpConfigController_verifySaved","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmtpVerifyResultDto"}}}},"404":{"description":"No per-domain override saved"}},"security":[{"bearer":[]}],"summary":"Test the per-domain SMTP override; updates status","tags":["smtp-config"]}},"/v1/webhooks/bounces/{workspace_id}":{"post":{"operationId":"BounceWebhookController_handleBounceHeader","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-envoi-bounce-secret","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BounceWebhookResponseDto"}}}}},"summary":"Inbound bounce webhook (header-auth, preferred). Authenticates via the `X-Envoi-Bounce-Secret` header + provider signature. Public — no bearer / cookie auth.","tags":["webhooks"]}},"/v1/webhooks/bounces/{workspace_id}/d/{domain_id}":{"post":{"operationId":"BounceWebhookController_handleDomainBounceHeader","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-envoi-bounce-secret","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BounceWebhookResponseDto"}}}}},"summary":"Inbound bounce webhook for a per-domain SMTP override (header-auth, preferred). Header secret + provider signature; tagged with the domain so audit attributes correctly.","tags":["webhooks"]}},"/v1/webhooks/bounces/{workspace_id}/{secret}":{"post":{"operationId":"BounceWebhookController_handleBounce","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"secret","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BounceWebhookResponseDto"}}}}},"summary":"DEPRECATED — use the header-auth variant (`POST /webhooks/bounces/:workspaceId` + `X-Envoi-Bounce-Secret`). The path-style URL leaks the secret into ingress access logs; it remains live for 90 days for existing provider configs.","tags":["webhooks"]}},"/v1/webhooks/bounces/{workspace_id}/{secret}/d/{domain_id}":{"post":{"operationId":"BounceWebhookController_handleDomainBounce","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"secret","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BounceWebhookResponseDto"}}}}},"summary":"DEPRECATED per-domain variant of the path-style route. Use the header-auth variant (`POST /webhooks/bounces/:workspaceId/d/:domainId` + `X-Envoi-Bounce-Secret`).","tags":["webhooks"]}},"/v1/workspaces/{workspace_id}/mailboxes":{"post":{"operationId":"MailboxController_createMailbox","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMailboxDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailboxDto"}}}},"400":{"description":"Validation failure (invalid local part, retention out of range)."},"403":{"description":"Caller lacks `mailbox.create` permission, or workspace has not enabled the `envoi` service."},"404":{"description":"Domain not found in this workspace."},"409":{"description":"Mailbox local-part already exists on this domain, or the domain is not yet verified."},"422":{"description":"Local part or retention rejected by service-layer validation."}},"security":[{"bearer":[]}],"summary":"Create a mailbox under a verified domain. Local-part + domain combination must be globally unique.","tags":["mailboxes"]},"get":{"operationId":"MailboxController_listMailboxes","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"cursor","required":false,"in":"query","description":"Opaque pagination cursor","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListMailboxesResponseDto"}}}},"403":{"description":"Caller lacks `mailbox.read` permission, or workspace has not enabled the `envoi` service."}},"security":[{"bearer":[]}],"summary":"List mailboxes in this workspace. 20 per page by default (max 200) — paginate with the cursor returned in `pagination.next_cursor` while `pagination.has_more` is true.","tags":["mailboxes"]}},"/v1/workspaces/{workspace_id}/mailboxes/{id}":{"get":{"operationId":"MailboxController_getMailbox","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailboxDto"}}}},"404":{"description":"Mailbox not found in this workspace."}},"security":[{"bearer":[]}],"summary":"Get one mailbox by id.","tags":["mailboxes"]},"patch":{"operationId":"MailboxController_updateMailbox","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMailboxDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailboxDto"}}}},"400":{"description":"Validation failure or invalid status transition."},"404":{"description":"Mailbox not found in this workspace."},"422":{"description":"Retention or status rejected by service-layer validation."}},"security":[{"bearer":[]}],"summary":"Update mailbox display name, retention, or status. Status transitions are restricted (active ↔ suspended; archived is terminal).","tags":["mailboxes"]},"delete":{"operationId":"MailboxController_deleteMailbox","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Mailbox soft-deleted."},"404":{"description":"Mailbox not found in this workspace."}},"security":[{"bearer":[]}],"summary":"Soft-delete a mailbox. The row is marked deleted and stops accepting new mail; existing messages are kept until retention purges them.","tags":["mailboxes"]}},"/v1/workspaces/{workspace_id}/mailboxes/{id}/messages":{"get":{"operationId":"MessageController_listMessages","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"from","required":false,"in":"query","description":"Filter by From address (substring match)","schema":{"type":"string"}},{"name":"subject","required":false,"in":"query","description":"Filter by subject (substring match)","schema":{"type":"string"}},{"name":"since","required":false,"in":"query","description":"Only include messages received on or after this ISO date","schema":{"type":"string"}},{"name":"until","required":false,"in":"query","description":"Only include messages received on or before this ISO date","schema":{"type":"string"}},{"name":"unread","required":false,"in":"query","description":"Filter to only unread (true) or only read (false) messages","schema":{"type":"boolean"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"cursor","required":false,"in":"query","description":"Opaque pagination cursor","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListMessagesResponseDto"}}}},"400":{"description":"Invalid filter or cursor."},"404":{"description":"Mailbox not found in this workspace."}},"security":[{"bearer":[]}],"summary":"List messages in a mailbox","tags":["messages"]}},"/v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}":{"get":{"operationId":"MessageController_getMessage","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"mid","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageDetailDto"}}}},"404":{"description":"Message not found"}},"security":[{"bearer":[]}],"summary":"Get a single message (metadata)","tags":["messages"]},"delete":{"operationId":"MessageController_deleteMessage","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"mid","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Message soft-deleted"},"404":{"description":"Message not found"}},"security":[{"bearer":[]}],"summary":"Soft-delete a message","tags":["messages"]}},"/v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}/raw":{"get":{"operationId":"MessageController_getRaw","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"mid","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Raw message stream"},"404":{"description":"Message not found"}},"security":[{"bearer":[]}],"summary":"Stream the raw RFC5322 .eml for a message","tags":["messages"]}},"/v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}/attachments/{position}":{"get":{"operationId":"MessageController_getAttachment","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"mid","required":true,"in":"path","schema":{"type":"string"}},{"name":"position","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"Attachment stream"},"404":{"description":"Attachment not found"}},"security":[{"bearer":[]}],"summary":"Stream a single attachment by its position","tags":["messages"]}},"/v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}/read":{"post":{"operationId":"MessageController_markRead","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"mid","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Marked read"},"404":{"description":"Message not found"}},"security":[{"bearer":[]}],"summary":"Mark a message as read","tags":["messages"]}},"/v1/workspaces/{workspace_id}/messages/timeline":{"get":{"description":"Returns dense day buckets in [since, until). `metric=sent` (default) reads received messages; `metric=bounced` reads MX-time rejections matched to this workspace by envelope-to.","operationId":"MessageTimelineController_getTimeline","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"granularity","required":true,"in":"query","description":"Bucket size. Only `day` is supported today; the param is here so future hour/week additions don’t break clients.","schema":{"example":"day","enum":["day"],"type":"string"}},{"name":"since","required":true,"in":"query","description":"Inclusive start of the window (ISO-8601). Treated in UTC for bucket alignment.","schema":{"example":"2026-04-01T00:00:00Z","type":"string"}},{"name":"until","required":true,"in":"query","description":"Exclusive end of the window (ISO-8601).","schema":{"example":"2026-05-01T00:00:00Z","type":"string"}},{"name":"metric","required":false,"in":"query","description":"Which counter to bucket. `sent` (default) reads from received messages; `bounced` reads from MX-time rejections matching this workspace's mailbox addresses.","schema":{"default":"sent","enum":["sent","bounced"],"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageTimelineResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Day-bucketed message volume for a workspace","tags":["messages"]}},"/v1/workspaces/{workspace_id}/messages":{"get":{"operationId":"OutboundMessageController_list","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Page size, 1–100","schema":{"default":20,"type":"number"}},{"name":"cursor","required":false,"in":"query","description":"Opaque cursor from previous page.","schema":{"type":"string"}},{"name":"recipient","required":false,"in":"query","description":"Filter by exact recipient address.","schema":{"type":"string"}},{"name":"template","required":false,"in":"query","description":"Filter by template name.","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","description":"Filter by status.","schema":{"enum":["queued","sent","delivered","bounced","complained","failed","suppressed"],"type":"string"}},{"name":"subject_contains","required":false,"in":"query","description":"Substring filter on subject (case-insensitive, max 200 chars).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListOutboundMessagesResponseDto"}}}},"400":{"description":"Invalid filter or cursor."}},"security":[{"bearer":[]}],"summary":"List outbound messages with cursor pagination","tags":["outbound-messages"]},"delete":{"operationId":"OutboundMessageController_deleteByRecipient","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"recipient","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteByRecipientResponseDto"}}}},"400":{"description":"Missing or invalid recipient."}},"security":[{"bearer":[]}],"summary":"RTBF: delete every message log row for `recipient` (case-insensitive). Returns the deleted-row count.","tags":["outbound-messages"]}},"/v1/workspaces/{workspace_id}/messages/export":{"get":{"operationId":"OutboundMessageController_exportCsv","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"recipient","required":false,"in":"query","description":"Filter by exact recipient address.","schema":{"type":"string"}},{"name":"template","required":false,"in":"query","description":"Filter by template name.","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","description":"Filter by status.","schema":{"enum":["queued","sent","delivered","bounced","complained","failed","suppressed"],"type":"string"}},{"name":"subject_contains","required":false,"in":"query","description":"Substring filter on subject (case-insensitive, max 200 chars).","schema":{"type":"string"}}],"responses":{"200":{"description":"text/csv stream with one header row + one row per message. Content-Disposition: attachment."},"429":{"description":"Another export is already running, or this workspace has hit the hourly export cap."}},"security":[{"bearer":[]}],"summary":"Stream the filtered message log as CSV. Same filters as GET /messages. Body columns are intentionally omitted; bodies remain gated by `envoi.message.body.read`. Rate-limited 1 concurrent + 5/hour per workspace.","tags":["outbound-messages"]}},"/v1/workspaces/{workspace_id}/messages/settings":{"get":{"operationId":"OutboundMessageController_getSettings","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundMessageSettingsDto"}}}}},"security":[{"bearer":[]}],"summary":"Read this workspace's outbound-message settings. Absent → defaults (redact_bodies: false).","tags":["outbound-messages"]},"patch":{"operationId":"OutboundMessageController_updateSettings","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOutboundMessageSettingsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundMessageSettingsDto"}}}},"400":{"description":"Invalid payload."}},"security":[{"bearer":[]}],"summary":"Update this workspace's outbound-message settings. Gated on `envoi.message.delete` — same privilege bar as RTBF, since both touch the PII boundary.","tags":["outbound-messages"]}},"/v1/workspaces/{workspace_id}/messages/{id}":{"get":{"operationId":"OutboundMessageController_get","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundMessageDto"}}}},"404":{"description":"Not found."}},"security":[{"bearer":[]}],"summary":"Get a single outbound message (metadata + delivery verdicts; body not included). Use /:id/body for cleartext content.","tags":["outbound-messages"]}},"/v1/workspaces/{workspace_id}/messages/{id}/body":{"get":{"operationId":"OutboundMessageController_getBody","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundMessageWithBodyDto"}}}},"404":{"description":"Not found."}},"security":[{"bearer":[]}],"summary":"Get the cleartext body of an outbound message. Gated by `envoi.message.body.read` (admin+ by default; not granted to plain members).","tags":["outbound-messages"]}},"/v1/workspaces/{workspace_id}/webhooks":{"get":{"operationId":"OutboundWebhookController_list","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OutboundWebhookConfigDto"}}}}}},"security":[{"bearer":[]}],"summary":"List the workspace default + every per-domain override. Plaintext signing secrets are NOT returned — only the last-4-char hint. Mint or rotate via the secret endpoints to receive the plaintext (once).","tags":["outbound-webhooks"]}},"/v1/workspaces/{workspace_id}/webhooks/default":{"get":{"operationId":"OutboundWebhookController_getDefault","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Workspace-default config, or null if not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundWebhookConfigDto"}}}}},"security":[{"bearer":[]}],"summary":"Get the workspace-default webhook config (returns null if unset).","tags":["outbound-webhooks"]},"put":{"operationId":"OutboundWebhookController_upsertDefault","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertOutboundWebhookDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundWebhookConfigWithSecretDto"}}}},"400":{"description":"Validation failure (bad URL or event-type set)."},"422":{"description":"URL failed SSRF + reachability checks at config time."}},"security":[{"bearer":[]}],"summary":"Create or replace the workspace-default outbound webhook. Returns the plaintext signing secret ONCE — store it now.","tags":["outbound-webhooks"]},"delete":{"operationId":"OutboundWebhookController_deleteDefault","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Workspace-default webhook deleted."}},"security":[{"bearer":[]}],"summary":"Delete the workspace-default webhook config.","tags":["outbound-webhooks"]}},"/v1/workspaces/{workspace_id}/webhooks/default/rotate-secret":{"post":{"operationId":"OutboundWebhookController_rotateDefaultSecret","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundWebhookConfigWithSecretDto"}}}},"404":{"description":"No webhook configured to rotate."}},"security":[{"bearer":[]}],"summary":"Rotate the workspace-default webhook signing secret. Previous secret remains valid for 24h. Plaintext returned once.","tags":["outbound-webhooks"]}},"/v1/workspaces/{workspace_id}/webhooks/domains/{domain_id}":{"get":{"operationId":"OutboundWebhookController_getDomain","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Per-domain override, or null if not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundWebhookConfigDto"}}}}},"security":[{"bearer":[]}],"summary":"Get the per-domain webhook override (returns null if unset).","tags":["outbound-webhooks"]},"put":{"operationId":"OutboundWebhookController_upsertDomain","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertOutboundWebhookDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundWebhookConfigWithSecretDto"}}}},"400":{"description":"Validation failure (bad URL or event-type set)."},"422":{"description":"URL failed SSRF + reachability checks at config time."}},"security":[{"bearer":[]}],"summary":"Create or replace the per-domain webhook override. Returns the plaintext signing secret ONCE — store it now.","tags":["outbound-webhooks"]},"delete":{"operationId":"OutboundWebhookController_deleteDomain","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Per-domain webhook override deleted."}},"security":[{"bearer":[]}],"summary":"Delete the per-domain webhook override.","tags":["outbound-webhooks"]}},"/v1/workspaces/{workspace_id}/webhooks/domains/{domain_id}/rotate-secret":{"post":{"operationId":"OutboundWebhookController_rotateDomainSecret","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"domain_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundWebhookConfigWithSecretDto"}}}},"404":{"description":"No per-domain webhook configured to rotate."}},"security":[{"bearer":[]}],"summary":"Rotate the per-domain webhook signing secret. Previous secret remains valid for 24h. Plaintext returned once.","tags":["outbound-webhooks"]}},"/v1/workspaces/{workspace_id}/webhooks/attempts":{"get":{"operationId":"OutboundWebhookController_listAttempts","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OutboundWebhookAttemptDto"}}}}}},"security":[{"bearer":[]}],"summary":"Recent delivery attempts across all of this workspace's webhook configs. Includes status code, latency, response body (truncated to 1KB), and error if any.","tags":["outbound-webhooks"]}},"/v1/workspaces/{workspace_id}/templates":{"post":{"operationId":"TemplateController_upsert","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertTemplateDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateDto"}}}},"400":{"description":"Validation failure (bad name / payload)."}},"security":[{"bearer":[]}],"summary":"Create or update a template, layout, or partial. Idempotent on (workspace_id, name); `kind` cannot change after the first upsert.","tags":["templates"]},"get":{"operationId":"TemplateController_list","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"tags","required":true,"in":"query","schema":{"type":"string"}},{"name":"kind","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListTemplatesResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List templates / layouts / partials in a workspace. Filter via ?kind=template|layout|partial (default: all). Filter by tags via ?tags=foo,bar (comma-separated).","tags":["templates"]}},"/v1/workspaces/{workspace_id}/templates/tags":{"get":{"operationId":"TemplateController_listTags","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListTemplateTagsResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Distinct tag list across all templates in this workspace (for autocomplete)","tags":["templates"]}},"/v1/workspaces/{workspace_id}/templates/{name}":{"get":{"operationId":"TemplateController_get","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"name","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateDto"}}}},"404":{"description":"Template not found in this workspace."}},"security":[{"bearer":[]}],"summary":"Get a template, layout, or partial by name.","tags":["templates"]},"delete":{"operationId":"TemplateController_delete","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"name","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Template deleted."},"404":{"description":"Template not found in this workspace."}},"security":[{"bearer":[]}],"summary":"Delete a template, layout, or partial by name.","tags":["templates"]}},"/v1/workspaces/{workspace_id}/templates/{name}/render":{"post":{"operationId":"TemplateController_render","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"name","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderTemplateDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderedMessageDto"}}}},"404":{"description":"Template not found in this workspace."},"409":{"description":"Cannot render a layout/partial — only sendable templates."},"422":{"description":"Render or schema validation failed."}},"security":[{"bearer":[]}],"summary":"Render a saved template against a data payload. Returns subject + html + text without sending.","tags":["templates"]}},"/v1/workspaces/{workspace_id}/templates/preview":{"post":{"operationId":"TemplateController_preview","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewTemplateDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderedMessageDto"}}}},"422":{"description":"Render failed (compile or runtime error)."}},"security":[{"bearer":[]}],"summary":"Preview an unsaved draft. Renders subject + bodyHtml against the workspace brand tokens + supplied data, with all workspace layouts/partials registered. Used by the editor for live preview.","tags":["templates"]}},"/v1/workspaces/{workspace_id}/templates/lint":{"post":{"operationId":"TemplateController_lint","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LintTemplateDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LintResultDto"}}}},"422":{"description":"Underlying render failed."}},"security":[{"bearer":[]}],"summary":"Lint a draft for deliverability red flags. Same render pipeline as /preview; returns a 0-100 score (higher is better) and a sorted list of findings (errors first). Self-developed heuristic linter, no external classifier — runs in-process.","tags":["templates"]}},"/v1/workspaces/{workspace_id}/templates/{name}/variables":{"get":{"operationId":"TemplateController_variables","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"name","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateVariablesResponseDto"}}}},"404":{"description":"Template not found in this workspace."}},"security":[{"bearer":[]}],"summary":"Variables auto-detected from the saved template body + subject (Handlebars expressions, dotted paths preserved, dedup'd in first-occurrence order).","tags":["templates"]}},"/v1/workspaces/{workspace_id}/templates/{name}/test-send":{"post":{"operationId":"TemplateController_testSend","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"name","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestSendTemplateDto"}}}},"responses":{"202":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendAcceptedDto"}}}},"403":{"description":"From-address domain is not registered to this workspace."},"404":{"description":"Template not found in this workspace."},"409":{"description":"Cannot test-send a layout/partial; or domain not yet verified."},"422":{"description":"Invalid from address or render failure."}},"security":[{"bearer":[]}],"summary":"Send a test rendering of the template to an arbitrary recipient. Bypasses suppression-list and rate-limit checks; subject is auto-prefixed with [TEST]; missing variables render as [varname] placeholders.","tags":["templates"]}},"/v1/workspaces/{workspace_id}/templates/{name}/send":{"post":{"operationId":"TemplateController_send","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"name","required":true,"in":"path","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Opaque client-generated string (1–256 chars, `[A-Za-z0-9_-:]`) used to deduplicate retries within a 24h window.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendTemplateDto"}}}},"responses":{"202":{"description":"Send accepted; queued for delivery.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendAcceptedDto"}}}},"400":{"description":"Validation failure (malformed body or recipient)."},"403":{"description":"From-address domain is not registered to this workspace."},"404":{"description":"Template not found in this workspace."},"409":{"description":"IDEMPOTENCY_KEY_REUSE — same key, different body — or domain not yet verified."},"422":{"description":"Recipient suppressed, render failure, or invalid from address."},"429":{"description":"Rate limited."}},"security":[{"bearer":[]}],"summary":"Send a template-rendered message. Pass an `Idempotency-Key` header (1..256 chars, [A-Za-z0-9_\\-:]) to make retries safe — replays return the original response with `Idempotent-Replay: true` for 24h. Same key + different request body returns 409 IDEMPOTENCY_KEY_REUSE.","tags":["templates"]}},"/v1/workspaces/{workspace_id}/templates/{name}/send-batch":{"post":{"operationId":"TemplateController_sendBatch","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"name","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendBatchDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendBatchOpenApiResponseDto"}}}},"400":{"description":"Validation failure (malformed body, batch too large)."},"404":{"description":"Template not found in this workspace."},"429":{"description":"Rate limited."}},"security":[{"bearer":[]}],"summary":"Send up to 100 messages with the same template in one request. Per-row partial-success: each row carries its own status (`accepted` | `rejected`). Pass `idempotency_key` per row to make retries safe — same Redis namespace as the request-header path on /send.","tags":["templates"]}},"/v1/workspaces/{workspace_id}/suppression":{"post":{"operationId":"SuppressionController_add","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddSuppressionDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuppressionEntryDto"}}}},"400":{"description":"Invalid payload (missing or malformed email)."},"422":{"description":"Email address rejected as invalid by the service layer."}},"security":[{"bearer":[]}],"summary":"Add an email to the workspace suppression list","tags":["suppression"]},"get":{"operationId":"SuppressionController_list","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListSuppressionResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List suppressed recipients in the workspace (most-recent first, capped at 500).","tags":["suppression"]}},"/v1/workspaces/{workspace_id}/suppression/{email}":{"delete":{"operationId":"SuppressionController_remove","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"email","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Suppression removed."},"404":{"description":"Email is not on this workspace suppression list."}},"security":[{"bearer":[]}],"summary":"Remove an address from the workspace suppression list.","tags":["suppression"]}}},"info":{"title":"Mailbox API","description":"Receive-and-store mail platform for ProductCraft","version":"0.1.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"}},"schemas":{"ResolvedBrandTokensDto":{"type":"object","properties":{"logo_url":{"type":"string","nullable":true},"brand_name":{"type":"string","nullable":true},"primary_color":{"type":"string","example":"#2d29d7"},"secondary_color":{"type":"string","example":"#4338ca"},"text_color":{"type":"string","example":"#0a0a0e"},"muted_text_color":{"type":"string","example":"#6b7280"},"background_color":{"type":"string","example":"#f4f4f7"},"card_background_color":{"type":"string","example":"#ffffff"},"font_stack":{"type":"string","example":"Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica Neue, Arial, sans-serif"},"button_radius":{"type":"string","example":"6px"},"container_max_width":{"type":"string","example":"560px"},"footer_html":{"type":"string","nullable":true},"footer_address":{"type":"string","nullable":true},"social_links":{"type":"object","description":"Map of social-platform → URL.","additionalProperties":{"type":"string"}},"extras":{"type":"object","description":"Extra key/value tokens addressable as {{brand.extras.<key>}} in templates.","additionalProperties":{"type":"string"}}},"required":["logo_url","brand_name","primary_color","secondary_color","text_color","muted_text_color","background_color","card_background_color","font_stack","button_radius","container_max_width","footer_html","footer_address","social_links","extras"]},"RawBrandDto":{"type":"object","properties":{"logo_url":{"type":"string","nullable":true},"brand_name":{"type":"string","nullable":true},"primary_color":{"type":"string","nullable":true},"secondary_color":{"type":"string","nullable":true},"text_color":{"type":"string","nullable":true},"muted_text_color":{"type":"string","nullable":true},"background_color":{"type":"string","nullable":true},"card_background_color":{"type":"string","nullable":true},"font_stack":{"type":"string","nullable":true},"button_radius":{"type":"string","nullable":true},"container_max_width":{"type":"string","nullable":true},"footer_html":{"type":"string","nullable":true},"footer_address":{"type":"string","nullable":true},"social_links":{"type":"object","additionalProperties":true,"nullable":true},"extras":{"type":"object","additionalProperties":true,"nullable":true},"updated_at":{"type":"string","format":"date-time"}},"required":["logo_url","brand_name","primary_color","secondary_color","text_color","muted_text_color","background_color","card_background_color","font_stack","button_radius","container_max_width","footer_html","footer_address","social_links","extras","updated_at"]},"BrandResponseDto":{"type":"object","properties":{"resolved":{"description":"Fully-resolved tokens every render uses. The frontend reads this for previews so what the editor shows == what recipients get.","allOf":[{"$ref":"#/components/schemas/ResolvedBrandTokensDto"}]},"raw":{"nullable":true,"description":"User-set values only (null when the workspace has never customised). Null fields signal \"not customised — falling through to default\".","allOf":[{"$ref":"#/components/schemas/RawBrandDto"}]}},"required":["resolved","raw"]},"UpdateBrandDto":{"type":"object","properties":{"logo_url":{"type":"string","description":"Public URL of your logo. Recommended: PNG/SVG, ≤200px tall, hosted on a domain you control or any CDN reachable at send time."},"brand_name":{"type":"string","description":"Display name shown in the email header (falls back to your workspace display name if not set)."},"primary_color":{"type":"string","description":"Primary brand color. Used for buttons, links, and accent strokes. Hex string (e.g. #2d29d7)."},"secondary_color":{"type":"string","description":"Secondary brand color, typically darker than primary. Used for hover states + secondary accents."},"text_color":{"type":"string","description":"Body text color (default: near-black)."},"muted_text_color":{"type":"string","description":"Muted text color for footers + secondary copy."},"background_color":{"type":"string","description":"Page background — the area outside the email card."},"card_background_color":{"type":"string","description":"Email card background."},"font_stack":{"type":"string","description":"CSS font-family stack. Email clients can't use webfonts reliably — list system fonts in fallback order."},"button_radius":{"type":"string","description":"CSS length value for button border-radius (e.g. \"6px\", \"0\", \"9999px\" for fully round)."},"container_max_width":{"type":"string","description":"Maximum width of the email card. Bulletproof tables max-out at this."},"footer_html":{"type":"string","description":"Custom HTML to render in the email footer. Inline styles only — no <style> tags. Used by templates that opt into `{{> bp.footer}}`."},"footer_address":{"type":"string","description":"Postal address line for compliance footers (CAN-SPAM, etc.). Plain text, displayed verbatim."},"social_links":{"type":"object","description":"Map of social-platform → URL. Recognized keys: twitter, x, linkedin, github, instagram, facebook, youtube, tiktok, mastodon, bluesky, web.","additionalProperties":{"type":"string"}},"extras":{"type":"object","description":"Extra key/value tokens addressable as {{brand.extras.<key>}} in templates. Use for marketing strings, secondary accent colors, etc.","additionalProperties":{"type":"string"}}}},"CreateDomainDto":{"type":"object","properties":{"fqdn":{"type":"string","description":"Fully qualified domain name","example":"productcraft.co"},"intent":{"type":"string","description":"How the workspace plans to use this domain. Drives which DNS records the cron treats as required (and so the auto-flip to active).","enum":["receive","send","both"],"default":"both"}},"required":["fqdn"]},"DnsInstructionDto":{"type":"object","properties":{"purpose":{"type":"string","description":"Purpose of this record (ownership, sending policy, etc.).","example":"spf"},"type":{"type":"string","description":"Record type (TXT, MX, CNAME).","example":"TXT"},"host":{"type":"string","description":"DNS hostname to publish.","example":"_amplify.example.com."},"value":{"type":"string","description":"Record value the customer must publish."},"ttl":{"type":"number","description":"TTL hint (in seconds)."},"priority":{"type":"number","description":"Priority for MX records."}},"required":["purpose","type","host","value"]},"DomainDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"seq_id":{"type":"string","description":"Sequential id per workspace.","example":"3"},"workspace_id":{"type":"string","format":"uuid"},"owner_account_id":{"type":"string","format":"uuid","nullable":true},"fqdn":{"type":"string","example":"productcraft.co"},"intent":{"type":"string","enum":["receive","send","both"],"description":"How the workspace intends to use this domain."},"status":{"type":"string","enum":["pending","active","failed"],"example":"pending"},"verification_token":{"type":"string","nullable":true},"test_token":{"type":"string"},"test_received_at":{"type":"string","format":"date-time","nullable":true},"dkim_selector":{"type":"string","nullable":true},"dkim_public_key":{"type":"string","nullable":true,"description":"Public DKIM key in PEM form (private half stays server-side)."},"dns_provider":{"type":"string","nullable":true,"description":"Identifier from the wizard provider list, or null when not chosen."},"dns_check_results":{"type":"object","nullable":true,"description":"Snapshot of the most recent DNS check (any shape)."},"last_dns_check_at":{"type":"string","format":"date-time","nullable":true},"verified_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"},"dns_instructions":{"description":"DNS records the customer needs to publish to verify ownership + sending.","type":"array","items":{"$ref":"#/components/schemas/DnsInstructionDto"}},"test_address":{"type":"string","description":"Convenience accessor for the test-email mailbox address on this domain.","example":"test-abc123@productcraft.co"}},"required":["id","seq_id","workspace_id","owner_account_id","fqdn","intent","status","verification_token","test_token","test_received_at","dkim_selector","dkim_public_key","dns_provider","dns_check_results","last_dns_check_at","verified_at","created_at","dns_instructions","test_address"]},"ListDomainsResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DomainDto"}}},"required":["data"]},"UpdateDomainDto":{"type":"object","properties":{"dns_provider":{"type":"string","description":"Identifier from the wizard's provider list. Free-form so we can ship new entries without a migration. NULL clears the choice.","nullable":true}}},"SmtpConfigDto":{"type":"object","properties":{"workspace_id":{"type":"string","format":"uuid"},"domain_id":{"type":"string","nullable":true,"description":"Null for the workspace-default config, set for a per-domain override."},"host":{"type":"string"},"port":{"type":"number"},"secure":{"type":"boolean"},"username":{"type":"string","nullable":true},"password":{"type":"string","nullable":true,"description":"Always null on the wire — the stored ciphertext is never returned."},"password_set":{"type":"boolean"},"from_name":{"type":"string"},"from_email":{"type":"string"},"status":{"type":"string","enum":["pending","active","failed"]},"last_verified_at":{"type":"string","format":"date-time","nullable":true},"last_verify_error":{"type":"string","nullable":true},"bounce_webhook_secret":{"type":"string","description":"Per-workspace bounce-webhook secret. Embedded in the URL the user pastes into provider dashboards."},"bounce_webhook_url":{"type":"string","description":"Fully-qualified webhook URL the user pastes into their provider."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["workspace_id","domain_id","host","port","secure","username","password","password_set","from_name","from_email","status","last_verified_at","last_verify_error","bounce_webhook_secret","bounce_webhook_url","created_at","updated_at"]},"GetSmtpConfigResponseDto":{"type":"object","properties":{"data":{"nullable":true,"description":"Null when this workspace has not configured SMTP.","allOf":[{"$ref":"#/components/schemas/SmtpConfigDto"}]}},"required":["data"]},"UpsertSmtpConfigDto":{"type":"object","properties":{"host":{"type":"string","description":"SMTP server hostname.","example":"smtp.sendgrid.net","minLength":1,"maxLength":255},"port":{"type":"number","description":"SMTP server port (commonly 587 for STARTTLS, 465 for implicit TLS).","example":587,"minimum":1,"maximum":65535},"secure":{"type":"boolean","description":"Whether to use implicit TLS (true → port 465 style). For STARTTLS on 587, set false.","example":false},"username":{"type":"string","description":"SMTP username. May be omitted alongside password for anonymous SMTP (e.g. postfix on a private network); otherwise both are required.","minLength":1,"maxLength":255},"password":{"type":"string","description":"SMTP password. Stored encrypted at rest; never returned by GET endpoints.","minLength":1,"maxLength":1024},"from_name":{"type":"string","description":"Default From name on outbound messages.","example":"Acme Notifications","minLength":1,"maxLength":100},"from_email":{"type":"string","description":"Default From email address. Must be authorized to send on the chosen domain.","example":"noreply@acme.com","minLength":1,"maxLength":255}},"required":["host","port","secure","from_name","from_email"]},"VerifySmtpConfigDto":{"type":"object","properties":{"host":{"type":"string","description":"SMTP server hostname.","example":"smtp.sendgrid.net","minLength":1,"maxLength":255},"port":{"type":"number","description":"SMTP server port (commonly 587 for STARTTLS, 465 for implicit TLS).","example":587,"minimum":1,"maximum":65535},"secure":{"type":"boolean","description":"Whether to use implicit TLS (true → port 465 style). For STARTTLS on 587, set false.","example":false},"username":{"type":"string","description":"SMTP username. May be omitted alongside password for anonymous SMTP (e.g. postfix on a private network); otherwise both are required.","minLength":1,"maxLength":255},"password":{"type":"string","description":"SMTP password. Stored encrypted at rest; never returned by GET endpoints.","minLength":1,"maxLength":1024},"from_name":{"type":"string","description":"Default From name on outbound messages.","example":"Acme Notifications","minLength":1,"maxLength":100},"from_email":{"type":"string","description":"Default From email address. Must be authorized to send on the chosen domain.","example":"noreply@acme.com","minLength":1,"maxLength":255}},"required":["host","port","secure","from_name","from_email"]},"SmtpVerifyErrorDto":{"type":"object","properties":{"code":{"type":"string","example":"EAUTH"},"message":{"type":"string","example":"Authentication failed — check the username and password."}},"required":["code","message"]},"SmtpVerifyResultDto":{"type":"object","properties":{"ok":{"type":"boolean"},"error":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/SmtpVerifyErrorDto"}]}},"required":["ok"]},"ListSmtpConfigsResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SmtpConfigDto"}}},"required":["data"]},"BounceWebhookResponseDto":{"type":"object","properties":{"ok":{"type":"boolean","description":"Always true on a 200; providers retry on non-2xx.","example":true},"accepted":{"type":"number","description":"Count of bounces accepted and queued for downstream handling.","example":1},"ignored":{"type":"number","description":"Count of bounces dropped (e.g. envelope-to did not match any mailbox).","example":0}},"required":["ok","accepted","ignored"]},"CreateMailboxDto":{"type":"object","properties":{"local_part":{"type":"string","description":"Local part of the address (portion before @)","example":"claude-qa-alpha"},"domain_id":{"type":"string","description":"Domain ID (uuid)"},"retention_days":{"type":"number","description":"Retention period in days (1–30)","minimum":1,"maximum":30},"display_name":{"type":"string","description":"Human-readable display name"}},"required":["local_part","domain_id"]},"MailboxDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"seq_id":{"type":"string","description":"Sequential id per workspace (stable, monotonic).","example":"7"},"workspace_id":{"type":"string","format":"uuid"},"domain_id":{"type":"string","format":"uuid"},"local_part":{"type":"string"},"display_name":{"type":"string","nullable":true},"owner_account_id":{"type":"string","format":"uuid","nullable":true},"retention_days":{"type":"number","description":"Retention period in days.","example":14},"status":{"type":"string","enum":["active","disabled"],"example":"active"},"created_at":{"type":"string","format":"date-time"}},"required":["id","seq_id","workspace_id","domain_id","local_part","display_name","owner_account_id","retention_days","status","created_at"]},"PaginationDto":{"type":"object","properties":{"next_cursor":{"type":"string","description":"Opaque cursor for the next page. `null` when no more pages are available.","nullable":true,"example":"eyJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wMVQwMDowMDowMFoifQ"},"has_more":{"type":"boolean","description":"True iff another page exists; if true, pass `next_cursor` on the next request.","example":false}},"required":["next_cursor","has_more"]},"ListMailboxesResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MailboxDto"}},"pagination":{"$ref":"#/components/schemas/PaginationDto"}},"required":["data","pagination"]},"UpdateMailboxDto":{"type":"object","properties":{"display_name":{"type":"string","description":"Display name"},"retention_days":{"type":"number","description":"Retention period in days (1–30)","minimum":1,"maximum":30},"status":{"type":"string","description":"Mailbox status","enum":["active","disabled"]}}},"MessageDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mailbox_id":{"type":"string","format":"uuid"},"seq_id":{"type":"string","description":"Sequential ID per mailbox.","example":"42"},"raw_object_key":{"type":"string","description":"Storage object key for the raw RFC5322 .eml."},"envelope_to":{"type":"string"},"from_address":{"type":"string"},"from_local":{"type":"string"},"from_domain":{"type":"string"},"to_addresses":{"type":"array","items":{"type":"string"}},"cc_addresses":{"type":"array","items":{"type":"string"}},"subject":{"type":"string","nullable":true},"body_preview":{"type":"string","nullable":true},"rfc822_message_id":{"type":"string","nullable":true},"size_bytes":{"type":"number"},"spam_score":{"type":"string","nullable":true},"spam_verdict":{"type":"string","nullable":true},"spf_result":{"type":"string","nullable":true},"dkim_result":{"type":"string","nullable":true},"dmarc_result":{"type":"string","nullable":true},"quarantined":{"type":"boolean"},"received_at":{"type":"string","format":"date-time"},"read_at":{"type":"string","format":"date-time","nullable":true},"deleted_at":{"type":"string","format":"date-time","nullable":true}},"required":["id","mailbox_id","seq_id","raw_object_key","envelope_to","from_address","from_local","from_domain","to_addresses","cc_addresses","subject","body_preview","rfc822_message_id","size_bytes","spam_score","spam_verdict","spf_result","dkim_result","dmarc_result","quarantined","received_at","read_at","deleted_at"]},"ListMessagesResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MessageDto"}},"pagination":{"$ref":"#/components/schemas/PaginationDto"}},"required":["data","pagination"]},"MessageAttachmentDto":{"type":"object","properties":{"position":{"type":"number"},"filename":{"type":"string","nullable":true},"content_id":{"type":"string","nullable":true},"content_type":{"type":"string"},"size_bytes":{"type":"number"},"sha256":{"type":"string"}},"required":["position","filename","content_id","content_type","size_bytes","sha256"]},"MessageDetailDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mailbox_id":{"type":"string","format":"uuid"},"seq_id":{"type":"string","description":"Sequential ID per mailbox.","example":"42"},"raw_object_key":{"type":"string","description":"Storage object key for the raw RFC5322 .eml."},"envelope_to":{"type":"string"},"from_address":{"type":"string"},"from_local":{"type":"string"},"from_domain":{"type":"string"},"to_addresses":{"type":"array","items":{"type":"string"}},"cc_addresses":{"type":"array","items":{"type":"string"}},"subject":{"type":"string","nullable":true},"body_preview":{"type":"string","nullable":true},"rfc822_message_id":{"type":"string","nullable":true},"size_bytes":{"type":"number"},"spam_score":{"type":"string","nullable":true},"spam_verdict":{"type":"string","nullable":true},"spf_result":{"type":"string","nullable":true},"dkim_result":{"type":"string","nullable":true},"dmarc_result":{"type":"string","nullable":true},"quarantined":{"type":"boolean"},"received_at":{"type":"string","format":"date-time"},"read_at":{"type":"string","format":"date-time","nullable":true},"deleted_at":{"type":"string","format":"date-time","nullable":true},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/MessageAttachmentDto"}}},"required":["id","mailbox_id","seq_id","raw_object_key","envelope_to","from_address","from_local","from_domain","to_addresses","cc_addresses","subject","body_preview","rfc822_message_id","size_bytes","spam_score","spam_verdict","spf_result","dkim_result","dmarc_result","quarantined","received_at","read_at","deleted_at","attachments"]},"TimelinePointDto":{"type":"object","properties":{"date":{"type":"string","description":"UTC date in YYYY-MM-DD format.","example":"2026-04-02"},"count":{"type":"number","description":"Count of messages in this bucket.","example":42}},"required":["date","count"]},"MessageTimelineResponseDto":{"type":"object","properties":{"granularity":{"type":"string","enum":["day"]},"metric":{"type":"string","enum":["sent","bounced"]},"points":{"description":"Dense day-bucketed counts for the requested window. Days with zero messages still appear with `count: 0`.","type":"array","items":{"$ref":"#/components/schemas/TimelinePointDto"}}},"required":["granularity","metric","points"]},"OutboundMessageDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"domain_id":{"type":"string","format":"uuid","nullable":true},"from_address":{"type":"string"},"to_address":{"type":"string"},"subject":{"type":"string","nullable":true},"template_name":{"type":"string","nullable":true},"status":{"type":"string","enum":["queued","sent","delivered","bounced","complained","failed","suppressed"]},"smtp_transcript":{"type":"string","nullable":true},"spf_result":{"type":"string","nullable":true},"dkim_result":{"type":"string","nullable":true},"dmarc_result":{"type":"string","nullable":true},"bounce_code":{"type":"string","nullable":true},"bounce_detail":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"delivered_at":{"type":"string","format":"date-time","nullable":true}},"required":["id","workspace_id","domain_id","from_address","to_address","subject","template_name","status","smtp_transcript","spf_result","dkim_result","dmarc_result","bounce_code","bounce_detail","created_at","delivered_at"]},"OutboundMessagePaginationDto":{"type":"object","properties":{"has_more":{"type":"boolean","description":"True iff another page exists; if true, pass `next_cursor` on the next request."},"next_cursor":{"type":"string","description":"Opaque cursor for the next page; null when no more pages.","nullable":true}},"required":["has_more","next_cursor"]},"ListOutboundMessagesResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OutboundMessageDto"}},"pagination":{"$ref":"#/components/schemas/OutboundMessagePaginationDto"}},"required":["data","pagination"]},"OutboundMessageSettingsDto":{"type":"object","properties":{"workspace_id":{"type":"string","format":"uuid"},"redact_bodies":{"type":"boolean"}},"required":["workspace_id","redact_bodies"]},"UpdateOutboundMessageSettingsDto":{"type":"object","properties":{"redact_bodies":{"type":"boolean","description":"When true, /messages/:id/body returns null body_html + body_text even to callers with envoi.message.body.read. Defaults to false."}},"required":["redact_bodies"]},"OutboundMessageWithBodyDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"domain_id":{"type":"string","format":"uuid","nullable":true},"from_address":{"type":"string"},"to_address":{"type":"string"},"subject":{"type":"string","nullable":true},"template_name":{"type":"string","nullable":true},"status":{"type":"string","enum":["queued","sent","delivered","bounced","complained","failed","suppressed"]},"smtp_transcript":{"type":"string","nullable":true},"spf_result":{"type":"string","nullable":true},"dkim_result":{"type":"string","nullable":true},"dmarc_result":{"type":"string","nullable":true},"bounce_code":{"type":"string","nullable":true},"bounce_detail":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"delivered_at":{"type":"string","format":"date-time","nullable":true},"body_html":{"type":"string","nullable":true,"description":"Cleartext HTML body. Null when the workspace has `redact_bodies = true`."},"body_text":{"type":"string","nullable":true,"description":"Cleartext text body. Null when the workspace has `redact_bodies = true`."}},"required":["id","workspace_id","domain_id","from_address","to_address","subject","template_name","status","smtp_transcript","spf_result","dkim_result","dmarc_result","bounce_code","bounce_detail","created_at","delivered_at","body_html","body_text"]},"DeleteByRecipientResponseDto":{"type":"object","properties":{"deleted":{"type":"number","description":"Number of rows deleted."}},"required":["deleted"]},"OutboundWebhookConfigDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"domain_id":{"type":"string","format":"uuid","nullable":true,"description":"Null for the workspace-default config; set for a per-domain override."},"url":{"type":"string"},"event_types":{"type":"array","items":{"type":"string"}},"signing_secret_hint":{"type":"string","description":"Last 4 chars of the active signing secret."},"prev_signing_secret_active":{"type":"boolean","description":"True while a previous signing secret is still being honoured (24h grace after rotation)."},"prev_signing_secret_expires_at":{"type":"string","format":"date-time","nullable":true},"disabled_at":{"type":"string","format":"date-time","nullable":true,"description":"Set when auto-disabled after sustained failure."},"disabled_reason":{"type":"string","nullable":true},"consecutive_failures":{"type":"number"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","workspace_id","domain_id","url","event_types","signing_secret_hint","prev_signing_secret_active","prev_signing_secret_expires_at","disabled_at","disabled_reason","consecutive_failures","created_at","updated_at"]},"UpsertOutboundWebhookDto":{"type":"object","properties":{"url":{"type":"string","description":"Destination URL. Must be absolute https:// with a public TLD.","example":"https://api.acme.com/webhooks/mail","maxLength":2048},"event_types":{"type":"array","description":"Events the destination subscribes to.","example":["message.delivered","message.bounced"],"items":{"type":"string","enum":["message.delivered","message.bounced"]}}},"required":["url","event_types"]},"OutboundWebhookConfigWithSecretDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"domain_id":{"type":"string","format":"uuid","nullable":true,"description":"Null for the workspace-default config; set for a per-domain override."},"url":{"type":"string"},"event_types":{"type":"array","items":{"type":"string"}},"signing_secret_hint":{"type":"string","description":"Last 4 chars of the active signing secret."},"prev_signing_secret_active":{"type":"boolean","description":"True while a previous signing secret is still being honoured (24h grace after rotation)."},"prev_signing_secret_expires_at":{"type":"string","format":"date-time","nullable":true},"disabled_at":{"type":"string","format":"date-time","nullable":true,"description":"Set when auto-disabled after sustained failure."},"disabled_reason":{"type":"string","nullable":true},"consecutive_failures":{"type":"number"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"signing_secret":{"type":"string","description":"Plaintext signing secret. Returned once on create / rotate; never reconstructable."}},"required":["id","workspace_id","domain_id","url","event_types","signing_secret_hint","prev_signing_secret_active","prev_signing_secret_expires_at","disabled_at","disabled_reason","consecutive_failures","created_at","updated_at","signing_secret"]},"OutboundWebhookAttemptDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"event_id":{"type":"string"},"event_type":{"type":"string"},"attempt_number":{"type":"number"},"status_code":{"type":"number","nullable":true},"error_message":{"type":"string","nullable":true},"latency_ms":{"type":"number","nullable":true},"attempted_at":{"type":"string","format":"date-time"},"url":{"type":"string"},"domain_id":{"type":"string","format":"uuid","nullable":true}},"required":["id","event_id","event_type","attempt_number","status_code","error_message","latency_ms","attempted_at","url","domain_id"]},"UpsertTemplateDto":{"type":"object","properties":{"name":{"type":"string","description":"Unique template name within the app (lowercase letters, digits, underscore, hyphen)","example":"order-confirmation"},"kind":{"type":"string","description":"Kind: \"template\" (sendable email; default), \"layout\" (chrome wrapper referenced via {{> layout.<name>}}), \"partial\" (reusable block referenced via {{> partial.<name>}}). Cannot be changed after create — delete + recreate to switch kinds.","enum":["template","layout","partial"],"default":"template"},"subject":{"type":"string","description":"Subject line (Handlebars). Layouts and partials ignore this field — they are not standalone-sendable. Pass an empty string for those.","maxLength":255},"body_html":{"type":"string","description":"HTML body (Handlebars)"},"body_text":{"type":"string","description":"Plain-text body (Handlebars). Auto-derived if omitted."},"variables_schema":{"type":"object","description":"Declared variable schema. Shape: `{version:1, vars:{<name>:{type, required?, default?, options?}}}`. When set, render + send validate the payload against the schema and 400 with descriptive errors on mismatch."},"tags":{"description":"Workspace-scoped labels for organizing templates. Free-form strings; the editor surfaces existing tags as autocomplete.","type":"array","items":{"type":"string"}}},"required":["name","subject","body_html"]},"TemplateDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"kind":{"type":"string","enum":["template","layout","partial"]},"subject":{"type":"string"},"body_html":{"type":"string"},"body_text":{"type":"string","nullable":true},"variables_schema":{"type":"object","description":"Declared variable schema; arbitrary JSON or null if not configured.","nullable":true},"tags":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","workspace_id","name","kind","subject","body_html","body_text","variables_schema","tags","created_at","updated_at"]},"ListTemplatesResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TemplateDto"}}},"required":["data"]},"ListTemplateTagsResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"type":"string"}}},"required":["data"]},"RenderTemplateDto":{"type":"object","properties":{"data":{"type":"object","description":"Data object to render into the template","example":{"name":"Alice","order_id":"ord_123"}}},"required":["data"]},"RenderedMessageDto":{"type":"object","properties":{"subject":{"type":"string"},"html":{"type":"string","description":"Fully-rendered HTML body."},"text":{"type":"string","description":"Plain-text body (derived if not authored)."}},"required":["subject","html","text"]},"PreviewTemplateDto":{"type":"object","properties":{"subject":{"type":"string","description":"Subject template (Handlebars)."},"body_html":{"type":"string","description":"HTML body to render (Handlebars)."},"data":{"type":"object","description":"Sample data to render variables with."}},"required":["body_html"]},"LintTemplateDto":{"type":"object","properties":{"subject":{"type":"string","description":"Subject template (Handlebars)."},"body_html":{"type":"string","description":"HTML body to render (Handlebars)."},"data":{"type":"object","description":"Sample data to render variables with."}},"required":["body_html"]},"LintFindingDto":{"type":"object","properties":{"id":{"type":"string","description":"Stable identifier for telemetry / suppressions."},"severity":{"type":"string","enum":["error","warn","info"]},"message":{"type":"string"},"suggestion":{"type":"string"}},"required":["id","severity","message"]},"LintResultDto":{"type":"object","properties":{"score":{"type":"number","description":"0-100, higher is better. ≥90 \"looks great\"; <70 \"likely to spam-folder\"."},"findings":{"type":"array","items":{"$ref":"#/components/schemas/LintFindingDto"}}},"required":["score","findings"]},"TemplateVariablesResponseDto":{"type":"object","properties":{"data":{"description":"Variables auto-detected from the saved template body + subject (Handlebars expressions, dotted paths preserved).","type":"array","items":{"type":"string"}}},"required":["data"]},"TestSendTemplateDto":{"type":"object","properties":{"from":{"type":"string","description":"From email address. Must be on a verified domain owned by the workspace, same as a normal send."},"to":{"type":"string","description":"Recipient address for the test email. Suppression-list and rate-limit checks are bypassed for test sends, so a recently-bounced address is still reachable for QA."},"data":{"type":"object","description":"Variable values to render into the template. Keys correspond to the dotted paths returned by the /variables endpoint.","example":{"name":"Alice","code":"123456"}}},"required":["from","to"]},"SendAcceptedDto":{"type":"object","properties":{"accepted":{"type":"boolean","description":"Always true on a 202.","example":true},"from":{"type":"string"},"to":{"type":"string"},"subject":{"type":"string"}},"required":["accepted","from","to","subject"]},"SendTemplateDto":{"type":"object","properties":{"data":{"type":"object","description":"Data object to render into the template","example":{"name":"Alice","order_id":"ord_123"}},"from":{"type":"string","description":"From email address","example":"hello@acme.com"},"to":{"type":"string","description":"To email address or list of addresses","example":"user@example.com"},"subject":{"type":"string","description":"Subject override (optional; uses template subject otherwise)"}},"required":["data","from","to"]},"SendBatchRowDto":{"type":"object","properties":{"from":{"type":"string","description":"From address. Must be authorized to send on its domain.","example":"noreply@acme.com"},"to":{"type":"string","description":"Recipient address.","example":"alice@example.com"},"subject":{"type":"string","description":"Optional override for the rendered subject. Falls back to the template subject.","maxLength":998},"data":{"type":"object","description":"Template data passed into the Handlebars render.","example":{"name":"Alice","code":"A1B2C3"}},"idempotency_key":{"type":"string","description":"Optional per-row idempotency key. Same dedupe namespace as the `Idempotency-Key` request header.","maxLength":256}},"required":["from","to","data"]},"SendBatchDto":{"type":"object","properties":{"messages":{"description":"Up to 100 messages to render and enqueue in a single call. Each is processed independently — partial successes are allowed.","minItems":1,"maxItems":100,"type":"array","items":{"$ref":"#/components/schemas/SendBatchRowDto"}}},"required":["messages"]},"SendBatchRowErrorDto":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]},"SendBatchRowStatusDto":{"type":"object","properties":{"index":{"type":"number","description":"Index into the request `messages[]` array."},"id":{"type":"string","description":"Outbound message id when accepted; absent on reject."},"status":{"type":"string","enum":["accepted","rejected"]},"error":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/SendBatchRowErrorDto"}]}},"required":["index","status"]},"SendBatchSummaryDto":{"type":"object","properties":{"accepted":{"type":"number"},"rejected":{"type":"number"}},"required":["accepted","rejected"]},"SendBatchOpenApiResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SendBatchRowStatusDto"}},"summary":{"$ref":"#/components/schemas/SendBatchSummaryDto"}},"required":["data","summary"]},"AddSuppressionDto":{"type":"object","properties":{}},"SuppressionEntryDto":{"type":"object","properties":{"id":{"type":"string","description":"Suppression row id.","format":"uuid"},"workspace_id":{"type":"string","description":"Workspace this suppression belongs to.","format":"uuid"},"email":{"type":"string","description":"Suppressed recipient (lowercased on insert).","example":"user@example.com"},"reason":{"type":"string","description":"Free-text reason; null when unspecified.","nullable":true},"source":{"type":"string","description":"Where the row originated.","enum":["manual","bounce","complaint"]},"added_at":{"type":"string","description":"Insertion (or last-touch) timestamp.","format":"date-time"}},"required":["id","workspace_id","email","reason","source","added_at"]},"ListSuppressionResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SuppressionEntryDto"}}},"required":["data"]}}}}