Envoi docs
API reference

Envoi API.

Send transactional email on your own domain. Every endpoint, request, and response — generated from the live OpenAPI spec.

Base URL

https://api.mail.productcraft.co

Auth

Authorization: Bearer …

Spec version

OpenAPI 3.0.0 · v0.1.0

Brand

get/v1/workspaces/{workspace_id}/brandAuth

Get the resolved brand tokens for the workspace. Always returns a complete object, with platform defaults filling in any unset fields.

Path parameters

workspace_id*string

Response · 200

resolved*object

Fully-resolved tokens every render uses. The frontend reads this for previews so what the editor shows == what recipients get.

raw*object

User-set values only (null when the workspace has never customised). Null fields signal "not customised — falling through to default".

Example

Request

GET /v1/workspaces/{workspace_id}/brand
Authorization: Bearer YOUR_TOKEN

Response

{
  "resolved": {},
  "raw": {}
}
put/v1/workspaces/{workspace_id}/brandAuth

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.

Path parameters

workspace_id*string

Request body

logo_urlstring

Public URL of your logo. Recommended: PNG/SVG, ≤200px tall, hosted on a domain you control or any CDN reachable at send time.

brand_namestring

Display name shown in the email header (falls back to your workspace display name if not set).

primary_colorstring

Primary brand color. Used for buttons, links, and accent strokes. Hex string (e.g. #2d29d7).

secondary_colorstring

Secondary brand color, typically darker than primary. Used for hover states + secondary accents.

text_colorstring

Body text color (default: near-black).

muted_text_colorstring

Muted text color for footers + secondary copy.

background_colorstring

Page background — the area outside the email card.

card_background_colorstring

Email card background.

font_stackstring

CSS font-family stack. Email clients can't use webfonts reliably — list system fonts in fallback order.

button_radiusstring

CSS length value for button border-radius (e.g. "6px", "0", "9999px" for fully round).

container_max_widthstring

Maximum width of the email card. Bulletproof tables max-out at this.

footer_htmlstring

Custom HTML to render in the email footer. Inline styles only — no <style> tags. Used by templates that opt into `{{> bp.footer}}`.

footer_addressstring

Postal address line for compliance footers (CAN-SPAM, etc.). Plain text, displayed verbatim.

social_linksobject

Map of social-platform → URL. Recognized keys: twitter, x, linkedin, github, instagram, facebook, youtube, tiktok, mastodon, bluesky, web.

extrasobject

Extra key/value tokens addressable as {{brand.extras.<key>}} in templates. Use for marketing strings, secondary accent colors, etc.

Response · 200

resolved*object

Fully-resolved tokens every render uses. The frontend reads this for previews so what the editor shows == what recipients get.

raw*object

User-set values only (null when the workspace has never customised). Null fields signal "not customised — falling through to default".

Example

Request

PUT /v1/workspaces/{workspace_id}/brand
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "logo_url": "string",
  "brand_name": "string",
  "primary_color": "string",
  "secondary_color": "string",
  "text_color": "string",
  "muted_text_color": "string",
  "background_color": "string",
  "card_background_color": "string",
  "font_stack": "string",
  "button_radius": "string",
  "container_max_width": "string",
  "footer_html": "string",
  "footer_address": "string",
  "social_links": {},
  "extras": {}
}

Response

{
  "resolved": {},
  "raw": {}
}
delete/v1/workspaces/{workspace_id}/brandAuth

Remove all brand customizations and revert to platform defaults.

Path parameters

workspace_id*string

Response · 204

Brand customisations cleared.


Domains

get/v1/workspaces/{workspace_id}/domainsAuth

List domains registered to this workspace

Path parameters

workspace_id*string

Response · 200

data*array

Example

Request

GET /v1/workspaces/{workspace_id}/domains
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "seq_id": "3",
      "workspace_id": "00000000-0000-0000-0000-000000000000",
      "owner_account_id": "00000000-0000-0000-0000-000000000000",
      "fqdn": "productcraft.co",
      "intent": "receive",
      "status": "pending",
      "verification_token": "string",
      "test_token": "string",
      "test_received_at": "2026-01-01T00:00:00.000Z",
      "dkim_selector": "string",
      "dkim_public_key": "string",
      "dns_provider": "string",
      "dns_check_results": {},
      "last_dns_check_at": "2026-01-01T00:00:00.000Z",
      "verified_at": "2026-01-01T00:00:00.000Z",
      "created_at": "2026-01-01T00:00:00.000Z",
      "dns_instructions": [
        {
          "purpose": "spf",
          "type": "TXT",
          "host": "_amplify.example.com.",
          "value": "string",
          "ttl": 0,
          "priority": 0
        }
      ],
      "test_address": "test-abc123@productcraft.co"
    }
  ]
}
post/v1/workspaces/{workspace_id}/domainsAuth

Register a domain for a workspace

Path parameters

workspace_id*string

Request body

fqdn*string

Fully qualified domain name

Example: "productcraft.co"

intentenum (3)

How the workspace plans to use this domain. Drives which DNS records the cron treats as required (and so the auto-flip to active).

Response · 201

id*string · uuid
seq_id*string

Sequential id per workspace.

Example: "3"

workspace_id*string · uuid
owner_account_id*string · uuid
fqdn*string

Example: "productcraft.co"

intent*enum (3)

How the workspace intends to use this domain.

status*enum (3)

Example: "pending"

verification_token*string
test_token*string
test_received_at*string · date-time
dkim_selector*string
dkim_public_key*string

Public DKIM key in PEM form (private half stays server-side).

dns_provider*string

Identifier from the wizard provider list, or null when not chosen.

dns_check_results*object

Snapshot of the most recent DNS check (any shape).

last_dns_check_at*string · date-time
verified_at*string · date-time
created_at*string · date-time
dns_instructions*array

DNS records the customer needs to publish to verify ownership + sending.

test_address*string

Convenience accessor for the test-email mailbox address on this domain.

Example: "test-abc123@productcraft.co"

Example

Request

POST /v1/workspaces/{workspace_id}/domains
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "fqdn": "productcraft.co",
  "intent": "receive"
}

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "seq_id": "3",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "owner_account_id": "00000000-0000-0000-0000-000000000000",
  "fqdn": "productcraft.co",
  "intent": "receive",
  "status": "pending",
  "verification_token": "string",
  "test_token": "string",
  "test_received_at": "2026-01-01T00:00:00.000Z",
  "dkim_selector": "string",
  "dkim_public_key": "string",
  "dns_provider": "string",
  "dns_check_results": {},
  "last_dns_check_at": "2026-01-01T00:00:00.000Z",
  "verified_at": "2026-01-01T00:00:00.000Z",
  "created_at": "2026-01-01T00:00:00.000Z",
  "dns_instructions": [
    {
      "purpose": "spf",
      "type": "TXT",
      "host": "_amplify.example.com.",
      "value": "string",
      "ttl": 0,
      "priority": 0
    }
  ],
  "test_address": "test-abc123@productcraft.co"
}
get/v1/workspaces/{workspace_id}/domains/{domain_id}Auth

Get a single domain (with DNS instructions).

Path parameters

workspace_id*string
domain_id*string

Response · 200

id*string · uuid
seq_id*string

Sequential id per workspace.

Example: "3"

workspace_id*string · uuid
owner_account_id*string · uuid
fqdn*string

Example: "productcraft.co"

intent*enum (3)

How the workspace intends to use this domain.

status*enum (3)

Example: "pending"

verification_token*string
test_token*string
test_received_at*string · date-time
dkim_selector*string
dkim_public_key*string

Public DKIM key in PEM form (private half stays server-side).

dns_provider*string

Identifier from the wizard provider list, or null when not chosen.

dns_check_results*object

Snapshot of the most recent DNS check (any shape).

last_dns_check_at*string · date-time
verified_at*string · date-time
created_at*string · date-time
dns_instructions*array

DNS records the customer needs to publish to verify ownership + sending.

test_address*string

Convenience accessor for the test-email mailbox address on this domain.

Example: "test-abc123@productcraft.co"

Example

Request

GET /v1/workspaces/{workspace_id}/domains/{domain_id}
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "seq_id": "3",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "owner_account_id": "00000000-0000-0000-0000-000000000000",
  "fqdn": "productcraft.co",
  "intent": "receive",
  "status": "pending",
  "verification_token": "string",
  "test_token": "string",
  "test_received_at": "2026-01-01T00:00:00.000Z",
  "dkim_selector": "string",
  "dkim_public_key": "string",
  "dns_provider": "string",
  "dns_check_results": {},
  "last_dns_check_at": "2026-01-01T00:00:00.000Z",
  "verified_at": "2026-01-01T00:00:00.000Z",
  "created_at": "2026-01-01T00:00:00.000Z",
  "dns_instructions": [
    {
      "purpose": "spf",
      "type": "TXT",
      "host": "_amplify.example.com.",
      "value": "string",
      "ttl": 0,
      "priority": 0
    }
  ],
  "test_address": "test-abc123@productcraft.co"
}
patch/v1/workspaces/{workspace_id}/domains/{domain_id}Auth

Update user-side domain metadata (today: dnsProvider; the wizard saves the user's provider choice here so hostnames render correctly across reloads).

Path parameters

workspace_id*string
domain_id*string

Request body

dns_providerstring

Identifier from the wizard's provider list. Free-form so we can ship new entries without a migration. NULL clears the choice.

Response · 200

id*string · uuid
seq_id*string

Sequential id per workspace.

Example: "3"

workspace_id*string · uuid
owner_account_id*string · uuid
fqdn*string

Example: "productcraft.co"

intent*enum (3)

How the workspace intends to use this domain.

status*enum (3)

Example: "pending"

verification_token*string
test_token*string
test_received_at*string · date-time
dkim_selector*string
dkim_public_key*string

Public DKIM key in PEM form (private half stays server-side).

dns_provider*string

Identifier from the wizard provider list, or null when not chosen.

dns_check_results*object

Snapshot of the most recent DNS check (any shape).

last_dns_check_at*string · date-time
verified_at*string · date-time
created_at*string · date-time
dns_instructions*array

DNS records the customer needs to publish to verify ownership + sending.

test_address*string

Convenience accessor for the test-email mailbox address on this domain.

Example: "test-abc123@productcraft.co"

Example

Request

PATCH /v1/workspaces/{workspace_id}/domains/{domain_id}
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "dns_provider": "string"
}

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "seq_id": "3",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "owner_account_id": "00000000-0000-0000-0000-000000000000",
  "fqdn": "productcraft.co",
  "intent": "receive",
  "status": "pending",
  "verification_token": "string",
  "test_token": "string",
  "test_received_at": "2026-01-01T00:00:00.000Z",
  "dkim_selector": "string",
  "dkim_public_key": "string",
  "dns_provider": "string",
  "dns_check_results": {},
  "last_dns_check_at": "2026-01-01T00:00:00.000Z",
  "verified_at": "2026-01-01T00:00:00.000Z",
  "created_at": "2026-01-01T00:00:00.000Z",
  "dns_instructions": [
    {
      "purpose": "spf",
      "type": "TXT",
      "host": "_amplify.example.com.",
      "value": "string",
      "ttl": 0,
      "priority": 0
    }
  ],
  "test_address": "test-abc123@productcraft.co"
}
delete/v1/workspaces/{workspace_id}/domains/{domain_id}Auth

Delete a domain (must have no mailboxes left).

Path parameters

workspace_id*string
domain_id*string

Response · 204

Domain deleted.

post/v1/workspaces/{workspace_id}/domains/{domain_id}/verifyAuth

Run a synchronous DNS check against this domain; flips status to active when the required record set passes.

Path parameters

workspace_id*string
domain_id*string

Response · 200

id*string · uuid
seq_id*string

Sequential id per workspace.

Example: "3"

workspace_id*string · uuid
owner_account_id*string · uuid
fqdn*string

Example: "productcraft.co"

intent*enum (3)

How the workspace intends to use this domain.

status*enum (3)

Example: "pending"

verification_token*string
test_token*string
test_received_at*string · date-time
dkim_selector*string
dkim_public_key*string

Public DKIM key in PEM form (private half stays server-side).

dns_provider*string

Identifier from the wizard provider list, or null when not chosen.

dns_check_results*object

Snapshot of the most recent DNS check (any shape).

last_dns_check_at*string · date-time
verified_at*string · date-time
created_at*string · date-time
dns_instructions*array

DNS records the customer needs to publish to verify ownership + sending.

test_address*string

Convenience accessor for the test-email mailbox address on this domain.

Example: "test-abc123@productcraft.co"

Example

Request

POST /v1/workspaces/{workspace_id}/domains/{domain_id}/verify
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "seq_id": "3",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "owner_account_id": "00000000-0000-0000-0000-000000000000",
  "fqdn": "productcraft.co",
  "intent": "receive",
  "status": "pending",
  "verification_token": "string",
  "test_token": "string",
  "test_received_at": "2026-01-01T00:00:00.000Z",
  "dkim_selector": "string",
  "dkim_public_key": "string",
  "dns_provider": "string",
  "dns_check_results": {},
  "last_dns_check_at": "2026-01-01T00:00:00.000Z",
  "verified_at": "2026-01-01T00:00:00.000Z",
  "created_at": "2026-01-01T00:00:00.000Z",
  "dns_instructions": [
    {
      "purpose": "spf",
      "type": "TXT",
      "host": "_amplify.example.com.",
      "value": "string",
      "ttl": 0,
      "priority": 0
    }
  ],
  "test_address": "test-abc123@productcraft.co"
}

Smtp-Config

get/v1/workspaces/{workspace_id}/smtp-configAuth

Get the workspace-default SMTP config (returns null if unset). Per-domain overrides live under /workspaces/:id/domains/:domainId/smtp-config.

Path parameters

workspace_id*string

Response · 200

data*object

Null when this workspace has not configured SMTP.

Example

Request

GET /v1/workspaces/{workspace_id}/smtp-config
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": {}
}
put/v1/workspaces/{workspace_id}/smtp-configAuth

Create or update the workspace-default SMTP config (resets verification)

Path parameters

workspace_id*string

Request body

host*string

SMTP server hostname.

Example: "smtp.sendgrid.net"

port*number

SMTP server port (commonly 587 for STARTTLS, 465 for implicit TLS).

Example: 587

secure*boolean

Whether to use implicit TLS (true → port 465 style). For STARTTLS on 587, set false.

Example: false

usernamestring

SMTP username. May be omitted alongside password for anonymous SMTP (e.g. postfix on a private network); otherwise both are required.

passwordstring

SMTP password. Stored encrypted at rest; never returned by GET endpoints.

from_name*string

Default From name on outbound messages.

Example: "Acme Notifications"

from_email*string

Default From email address. Must be authorized to send on the chosen domain.

Example: "noreply@acme.com"

Response · 200

workspace_id*string · uuid
domain_id*string

Null for the workspace-default config, set for a per-domain override.

host*string
port*number
secure*boolean
username*string
password*string

Always null on the wire — the stored ciphertext is never returned.

password_set*boolean
from_name*string
from_email*string
status*enum (3)
last_verified_at*string · date-time
last_verify_error*string
bounce_webhook_secret*string

Per-workspace bounce-webhook secret. Embedded in the URL the user pastes into provider dashboards.

bounce_webhook_url*string

Fully-qualified webhook URL the user pastes into their provider.

created_at*string · date-time
updated_at*string · date-time

Example

Request

PUT /v1/workspaces/{workspace_id}/smtp-config
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "host": "smtp.sendgrid.net",
  "port": 587,
  "secure": false,
  "username": "string",
  "password": "string",
  "from_name": "Acme Notifications",
  "from_email": "noreply@acme.com"
}

Response

{
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "domain_id": "string",
  "host": "string",
  "port": 0,
  "secure": false,
  "username": "string",
  "password": "string",
  "password_set": false,
  "from_name": "string",
  "from_email": "string",
  "status": "pending",
  "last_verified_at": "2026-01-01T00:00:00.000Z",
  "last_verify_error": "string",
  "bounce_webhook_secret": "string",
  "bounce_webhook_url": "string",
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z"
}
delete/v1/workspaces/{workspace_id}/smtp-configAuth

Revert the workspace to the shared SMTP relay

Path parameters

workspace_id*string

Response · 204

Workspace-default SMTP config deleted.

post/v1/workspaces/{workspace_id}/smtp-config/verifyAuth

Test arbitrary SMTP creds without persisting

Path parameters

workspace_id*string

Request body

host*string

SMTP server hostname.

Example: "smtp.sendgrid.net"

port*number

SMTP server port (commonly 587 for STARTTLS, 465 for implicit TLS).

Example: 587

secure*boolean

Whether to use implicit TLS (true → port 465 style). For STARTTLS on 587, set false.

Example: false

usernamestring

SMTP username. May be omitted alongside password for anonymous SMTP (e.g. postfix on a private network); otherwise both are required.

passwordstring

SMTP password. Stored encrypted at rest; never returned by GET endpoints.

from_name*string

Default From name on outbound messages.

Example: "Acme Notifications"

from_email*string

Default From email address. Must be authorized to send on the chosen domain.

Example: "noreply@acme.com"

Response · 200

ok*boolean
errorobject

Example

Request

POST /v1/workspaces/{workspace_id}/smtp-config/verify
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "host": "smtp.sendgrid.net",
  "port": 587,
  "secure": false,
  "username": "string",
  "password": "string",
  "from_name": "Acme Notifications",
  "from_email": "noreply@acme.com"
}

Response

{
  "ok": false,
  "error": {}
}
post/v1/workspaces/{workspace_id}/smtp-config/verify-savedAuth

Test the persisted workspace-default SMTP config; updates status

Path parameters

workspace_id*string

Response · 200

ok*boolean
errorobject

Example

Request

POST /v1/workspaces/{workspace_id}/smtp-config/verify-saved
Authorization: Bearer YOUR_TOKEN

Response

{
  "ok": false,
  "error": {}
}
get/v1/workspaces/{workspace_id}/smtp-configsAuth

List every SMTP config in the workspace (default + per-domain overrides)

Path parameters

workspace_id*string

Response · 200

data*array

Example

Request

GET /v1/workspaces/{workspace_id}/smtp-configs
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "workspace_id": "00000000-0000-0000-0000-000000000000",
      "domain_id": "string",
      "host": "string",
      "port": 0,
      "secure": false,
      "username": "string",
      "password": "string",
      "password_set": false,
      "from_name": "string",
      "from_email": "string",
      "status": "pending",
      "last_verified_at": "2026-01-01T00:00:00.000Z",
      "last_verify_error": "string",
      "bounce_webhook_secret": "string",
      "bounce_webhook_url": "string",
      "created_at": "2026-01-01T00:00:00.000Z",
      "updated_at": "2026-01-01T00:00:00.000Z"
    }
  ]
}
get/v1/workspaces/{workspace_id}/domains/{domain_id}/smtp-configAuth

Get the per-domain SMTP override (returns null if unset; the workspace default applies in that case).

Path parameters

workspace_id*string
domain_id*string

Response · 200

data*object

Null when this workspace has not configured SMTP.

Example

Request

GET /v1/workspaces/{workspace_id}/domains/{domain_id}/smtp-config
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": {}
}
put/v1/workspaces/{workspace_id}/domains/{domain_id}/smtp-configAuth

Create or update the per-domain SMTP override (resets verification)

Path parameters

workspace_id*string
domain_id*string

Request body

host*string

SMTP server hostname.

Example: "smtp.sendgrid.net"

port*number

SMTP server port (commonly 587 for STARTTLS, 465 for implicit TLS).

Example: 587

secure*boolean

Whether to use implicit TLS (true → port 465 style). For STARTTLS on 587, set false.

Example: false

usernamestring

SMTP username. May be omitted alongside password for anonymous SMTP (e.g. postfix on a private network); otherwise both are required.

passwordstring

SMTP password. Stored encrypted at rest; never returned by GET endpoints.

from_name*string

Default From name on outbound messages.

Example: "Acme Notifications"

from_email*string

Default From email address. Must be authorized to send on the chosen domain.

Example: "noreply@acme.com"

Response · 200

workspace_id*string · uuid
domain_id*string

Null for the workspace-default config, set for a per-domain override.

host*string
port*number
secure*boolean
username*string
password*string

Always null on the wire — the stored ciphertext is never returned.

password_set*boolean
from_name*string
from_email*string
status*enum (3)
last_verified_at*string · date-time
last_verify_error*string
bounce_webhook_secret*string

Per-workspace bounce-webhook secret. Embedded in the URL the user pastes into provider dashboards.

bounce_webhook_url*string

Fully-qualified webhook URL the user pastes into their provider.

created_at*string · date-time
updated_at*string · date-time

Example

Request

PUT /v1/workspaces/{workspace_id}/domains/{domain_id}/smtp-config
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "host": "smtp.sendgrid.net",
  "port": 587,
  "secure": false,
  "username": "string",
  "password": "string",
  "from_name": "Acme Notifications",
  "from_email": "noreply@acme.com"
}

Response

{
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "domain_id": "string",
  "host": "string",
  "port": 0,
  "secure": false,
  "username": "string",
  "password": "string",
  "password_set": false,
  "from_name": "string",
  "from_email": "string",
  "status": "pending",
  "last_verified_at": "2026-01-01T00:00:00.000Z",
  "last_verify_error": "string",
  "bounce_webhook_secret": "string",
  "bounce_webhook_url": "string",
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z"
}
delete/v1/workspaces/{workspace_id}/domains/{domain_id}/smtp-configAuth

Drop the per-domain override; sends from this domain fall back to the workspace default (or shared relay).

Path parameters

workspace_id*string
domain_id*string

Response · 204

Per-domain override deleted.

post/v1/workspaces/{workspace_id}/domains/{domain_id}/smtp-config/verify-savedAuth

Test the per-domain SMTP override; updates status

Path parameters

workspace_id*string
domain_id*string

Response · 200

ok*boolean
errorobject

Example

Request

POST /v1/workspaces/{workspace_id}/domains/{domain_id}/smtp-config/verify-saved
Authorization: Bearer YOUR_TOKEN

Response

{
  "ok": false,
  "error": {}
}

Webhooks

post/v1/webhooks/bounces/{workspace_id}

Inbound bounce webhook (header-auth, preferred). Authenticates via the `X-Envoi-Bounce-Secret` header + provider signature. Public — no bearer / cookie auth.

Path parameters

workspace_id*string

Headers

x-envoi-bounce-secret*string

Response · 200

ok*boolean

Always true on a 200; providers retry on non-2xx.

Example: true

accepted*number

Count of bounces accepted and queued for downstream handling.

Example: 1

ignored*number

Count of bounces dropped (e.g. envelope-to did not match any mailbox).

Example: 0

Example

Request

POST /v1/webhooks/bounces/{workspace_id}

Response

{
  "ok": true,
  "accepted": 1,
  "ignored": 0
}
post/v1/webhooks/bounces/{workspace_id}/d/{domain_id}

Inbound bounce webhook for a per-domain SMTP override (header-auth, preferred). Header secret + provider signature; tagged with the domain so audit attributes correctly.

Path parameters

workspace_id*string
domain_id*string

Headers

x-envoi-bounce-secret*string

Response · 200

ok*boolean

Always true on a 200; providers retry on non-2xx.

Example: true

accepted*number

Count of bounces accepted and queued for downstream handling.

Example: 1

ignored*number

Count of bounces dropped (e.g. envelope-to did not match any mailbox).

Example: 0

Example

Request

POST /v1/webhooks/bounces/{workspace_id}/d/{domain_id}

Response

{
  "ok": true,
  "accepted": 1,
  "ignored": 0
}
post/v1/webhooks/bounces/{workspace_id}/{secret}

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.

Path parameters

workspace_id*string
secret*string

Response · 200

ok*boolean

Always true on a 200; providers retry on non-2xx.

Example: true

accepted*number

Count of bounces accepted and queued for downstream handling.

Example: 1

ignored*number

Count of bounces dropped (e.g. envelope-to did not match any mailbox).

Example: 0

Example

Request

POST /v1/webhooks/bounces/{workspace_id}/{secret}

Response

{
  "ok": true,
  "accepted": 1,
  "ignored": 0
}
post/v1/webhooks/bounces/{workspace_id}/{secret}/d/{domain_id}

DEPRECATED per-domain variant of the path-style route. Use the header-auth variant (`POST /webhooks/bounces/:workspaceId/d/:domainId` + `X-Envoi-Bounce-Secret`).

Path parameters

workspace_id*string
secret*string
domain_id*string

Response · 200

ok*boolean

Always true on a 200; providers retry on non-2xx.

Example: true

accepted*number

Count of bounces accepted and queued for downstream handling.

Example: 1

ignored*number

Count of bounces dropped (e.g. envelope-to did not match any mailbox).

Example: 0

Example

Request

POST /v1/webhooks/bounces/{workspace_id}/{secret}/d/{domain_id}

Response

{
  "ok": true,
  "accepted": 1,
  "ignored": 0
}

Mailboxes

get/v1/workspaces/{workspace_id}/mailboxesAuth

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.

Path parameters

workspace_id*string

Query parameters

limitnumber
cursorstring

Opaque pagination cursor

Response · 200

data*array
pagination*object
next_cursor*string

Opaque cursor for the next page. `null` when no more pages are available.

Example: "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wMVQwMDowMDowMFoifQ"

has_more*boolean

True iff another page exists; if true, pass `next_cursor` on the next request.

Example: false

Example

Request

GET /v1/workspaces/{workspace_id}/mailboxes
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "seq_id": "7",
      "workspace_id": "00000000-0000-0000-0000-000000000000",
      "domain_id": "00000000-0000-0000-0000-000000000000",
      "local_part": "string",
      "display_name": "string",
      "owner_account_id": "00000000-0000-0000-0000-000000000000",
      "retention_days": 14,
      "status": "active",
      "created_at": "2026-01-01T00:00:00.000Z"
    }
  ],
  "pagination": {
    "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wMVQwMDowMDowMFoifQ",
    "has_more": false
  }
}
post/v1/workspaces/{workspace_id}/mailboxesAuth

Create a mailbox under a verified domain. Local-part + domain combination must be globally unique.

Path parameters

workspace_id*string

Request body

local_part*string

Local part of the address (portion before @)

Example: "claude-qa-alpha"

domain_id*string

Domain ID (uuid)

retention_daysnumber

Retention period in days (1–30)

display_namestring

Human-readable display name

Response · 201

id*string · uuid
seq_id*string

Sequential id per workspace (stable, monotonic).

Example: "7"

workspace_id*string · uuid
domain_id*string · uuid
local_part*string
display_name*string
owner_account_id*string · uuid
retention_days*number

Retention period in days.

Example: 14

status*enum (2)

Example: "active"

created_at*string · date-time

Example

Request

POST /v1/workspaces/{workspace_id}/mailboxes
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "local_part": "claude-qa-alpha",
  "domain_id": "string",
  "retention_days": 0,
  "display_name": "string"
}

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "seq_id": "7",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "domain_id": "00000000-0000-0000-0000-000000000000",
  "local_part": "string",
  "display_name": "string",
  "owner_account_id": "00000000-0000-0000-0000-000000000000",
  "retention_days": 14,
  "status": "active",
  "created_at": "2026-01-01T00:00:00.000Z"
}
get/v1/workspaces/{workspace_id}/mailboxes/{id}Auth

Get one mailbox by id.

Path parameters

workspace_id*string
id*string

Response · 200

id*string · uuid
seq_id*string

Sequential id per workspace (stable, monotonic).

Example: "7"

workspace_id*string · uuid
domain_id*string · uuid
local_part*string
display_name*string
owner_account_id*string · uuid
retention_days*number

Retention period in days.

Example: 14

status*enum (2)

Example: "active"

created_at*string · date-time

Example

Request

GET /v1/workspaces/{workspace_id}/mailboxes/{id}
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "seq_id": "7",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "domain_id": "00000000-0000-0000-0000-000000000000",
  "local_part": "string",
  "display_name": "string",
  "owner_account_id": "00000000-0000-0000-0000-000000000000",
  "retention_days": 14,
  "status": "active",
  "created_at": "2026-01-01T00:00:00.000Z"
}
patch/v1/workspaces/{workspace_id}/mailboxes/{id}Auth

Update mailbox display name, retention, or status. Status transitions are restricted (active ↔ suspended; archived is terminal).

Path parameters

workspace_id*string
id*string

Request body

display_namestring

Display name

retention_daysnumber

Retention period in days (1–30)

statusenum (2)

Mailbox status

Response · 200

id*string · uuid
seq_id*string

Sequential id per workspace (stable, monotonic).

Example: "7"

workspace_id*string · uuid
domain_id*string · uuid
local_part*string
display_name*string
owner_account_id*string · uuid
retention_days*number

Retention period in days.

Example: 14

status*enum (2)

Example: "active"

created_at*string · date-time

Example

Request

PATCH /v1/workspaces/{workspace_id}/mailboxes/{id}
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "display_name": "string",
  "retention_days": 0,
  "status": "active"
}

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "seq_id": "7",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "domain_id": "00000000-0000-0000-0000-000000000000",
  "local_part": "string",
  "display_name": "string",
  "owner_account_id": "00000000-0000-0000-0000-000000000000",
  "retention_days": 14,
  "status": "active",
  "created_at": "2026-01-01T00:00:00.000Z"
}
delete/v1/workspaces/{workspace_id}/mailboxes/{id}Auth

Soft-delete a mailbox. The row is marked deleted and stops accepting new mail; existing messages are kept until retention purges them.

Path parameters

workspace_id*string
id*string

Response · 204

Mailbox soft-deleted.


Messages

get/v1/workspaces/{workspace_id}/mailboxes/{id}/messagesAuth

List messages in a mailbox

Path parameters

workspace_id*string
id*string

Query parameters

fromstring

Filter by From address (substring match)

subjectstring

Filter by subject (substring match)

sincestring

Only include messages received on or after this ISO date

untilstring

Only include messages received on or before this ISO date

unreadboolean

Filter to only unread (true) or only read (false) messages

limitnumber
cursorstring

Opaque pagination cursor

Response · 200

data*array
pagination*object
next_cursor*string

Opaque cursor for the next page. `null` when no more pages are available.

Example: "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wMVQwMDowMDowMFoifQ"

has_more*boolean

True iff another page exists; if true, pass `next_cursor` on the next request.

Example: false

Example

Request

GET /v1/workspaces/{workspace_id}/mailboxes/{id}/messages
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "mailbox_id": "00000000-0000-0000-0000-000000000000",
      "seq_id": "42",
      "raw_object_key": "string",
      "envelope_to": "string",
      "from_address": "string",
      "from_local": "string",
      "from_domain": "string",
      "to_addresses": [
        "string"
      ],
      "cc_addresses": [
        "string"
      ],
      "subject": "string",
      "body_preview": "string",
      "rfc822_message_id": "string",
      "size_bytes": 0,
      "spam_score": "string",
      "spam_verdict": "string",
      "spf_result": "string",
      "dkim_result": "string",
      "dmarc_result": "string",
      "quarantined": false,
      "received_at": "2026-01-01T00:00:00.000Z",
      "read_at": "2026-01-01T00:00:00.000Z",
      "deleted_at": "2026-01-01T00:00:00.000Z"
    }
  ],
  "pagination": {
    "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wMVQwMDowMDowMFoifQ",
    "has_more": false
  }
}
get/v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}Auth

Get a single message (metadata)

Path parameters

workspace_id*string
id*string
mid*string

Response · 200

id*string · uuid
mailbox_id*string · uuid
seq_id*string

Sequential ID per mailbox.

Example: "42"

raw_object_key*string

Storage object key for the raw RFC5322 .eml.

envelope_to*string
from_address*string
from_local*string
from_domain*string
to_addresses*array
cc_addresses*array
subject*string
body_preview*string
rfc822_message_id*string
size_bytes*number
spam_score*string
spam_verdict*string
spf_result*string
dkim_result*string
dmarc_result*string
quarantined*boolean
received_at*string · date-time
read_at*string · date-time
deleted_at*string · date-time
attachments*array

Example

Request

GET /v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "mailbox_id": "00000000-0000-0000-0000-000000000000",
  "seq_id": "42",
  "raw_object_key": "string",
  "envelope_to": "string",
  "from_address": "string",
  "from_local": "string",
  "from_domain": "string",
  "to_addresses": [
    "string"
  ],
  "cc_addresses": [
    "string"
  ],
  "subject": "string",
  "body_preview": "string",
  "rfc822_message_id": "string",
  "size_bytes": 0,
  "spam_score": "string",
  "spam_verdict": "string",
  "spf_result": "string",
  "dkim_result": "string",
  "dmarc_result": "string",
  "quarantined": false,
  "received_at": "2026-01-01T00:00:00.000Z",
  "read_at": "2026-01-01T00:00:00.000Z",
  "deleted_at": "2026-01-01T00:00:00.000Z",
  "attachments": [
    {
      "position": 0,
      "filename": "string",
      "content_id": "string",
      "content_type": "string",
      "size_bytes": 0,
      "sha256": "string"
    }
  ]
}
delete/v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}Auth

Soft-delete a message

Path parameters

workspace_id*string
id*string
mid*string

Response · 204

Message soft-deleted

get/v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}/rawAuth

Stream the raw RFC5322 .eml for a message

Path parameters

workspace_id*string
id*string
mid*string

Response · 200

Raw message stream

get/v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}/attachments/{position}Auth

Stream a single attachment by its position

Path parameters

workspace_id*string
id*string
mid*string
position*number

Response · 200

Attachment stream

post/v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}/readAuth

Mark a message as read

Path parameters

workspace_id*string
id*string
mid*string

Response · 204

Marked read

get/v1/workspaces/{workspace_id}/messages/timelineAuth

Day-bucketed message volume for a workspace

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.

Path parameters

workspace_id*string

Query parameters

granularity*enum (1)

Bucket size. Only `day` is supported today; the param is here so future hour/week additions don’t break clients.

since*string

Inclusive start of the window (ISO-8601). Treated in UTC for bucket alignment.

until*string

Exclusive end of the window (ISO-8601).

metricenum (2)

Which counter to bucket. `sent` (default) reads from received messages; `bounced` reads from MX-time rejections matching this workspace's mailbox addresses.

Response · 200

granularity*enum (1)
metric*enum (2)
points*array

Dense day-bucketed counts for the requested window. Days with zero messages still appear with `count: 0`.

Example

Request

GET /v1/workspaces/{workspace_id}/messages/timeline
Authorization: Bearer YOUR_TOKEN

Response

{
  "granularity": "day",
  "metric": "sent",
  "points": [
    {
      "date": "2026-04-02",
      "count": 42
    }
  ]
}

Outbound-Messages

get/v1/workspaces/{workspace_id}/messagesAuth

List outbound messages with cursor pagination

Path parameters

workspace_id*string

Query parameters

limitnumber

Page size, 1–100

cursorstring

Opaque cursor from previous page.

recipientstring

Filter by exact recipient address.

templatestring

Filter by template name.

statusenum (7)

Filter by status.

subject_containsstring

Substring filter on subject (case-insensitive, max 200 chars).

Response · 200

data*array
pagination*object
has_more*boolean

True iff another page exists; if true, pass `next_cursor` on the next request.

next_cursor*string

Opaque cursor for the next page; null when no more pages.

Example

Request

GET /v1/workspaces/{workspace_id}/messages
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "workspace_id": "00000000-0000-0000-0000-000000000000",
      "domain_id": "00000000-0000-0000-0000-000000000000",
      "from_address": "string",
      "to_address": "string",
      "subject": "string",
      "template_name": "string",
      "status": "queued",
      "smtp_transcript": "string",
      "spf_result": "string",
      "dkim_result": "string",
      "dmarc_result": "string",
      "bounce_code": "string",
      "bounce_detail": "string",
      "created_at": "2026-01-01T00:00:00.000Z",
      "delivered_at": "2026-01-01T00:00:00.000Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": "string"
  }
}
delete/v1/workspaces/{workspace_id}/messagesAuth

RTBF: delete every message log row for `recipient` (case-insensitive). Returns the deleted-row count.

Path parameters

workspace_id*string

Query parameters

recipient*string

Response · 200

deleted*number

Number of rows deleted.

Example

Request

DELETE /v1/workspaces/{workspace_id}/messages
Authorization: Bearer YOUR_TOKEN

Response

{
  "deleted": 0
}
get/v1/workspaces/{workspace_id}/messages/exportAuth

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.

Path parameters

workspace_id*string

Query parameters

recipientstring

Filter by exact recipient address.

templatestring

Filter by template name.

statusenum (7)

Filter by status.

subject_containsstring

Substring filter on subject (case-insensitive, max 200 chars).

Response · 200

text/csv stream with one header row + one row per message. Content-Disposition: attachment.

get/v1/workspaces/{workspace_id}/messages/settingsAuth

Read this workspace's outbound-message settings. Absent → defaults (redact_bodies: false).

Path parameters

workspace_id*string

Response · 200

workspace_id*string · uuid
redact_bodies*boolean

Example

Request

GET /v1/workspaces/{workspace_id}/messages/settings
Authorization: Bearer YOUR_TOKEN

Response

{
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "redact_bodies": false
}
patch/v1/workspaces/{workspace_id}/messages/settingsAuth

Update this workspace's outbound-message settings. Gated on `envoi.message.delete` — same privilege bar as RTBF, since both touch the PII boundary.

Path parameters

workspace_id*string

Request body

redact_bodies*boolean

When true, /messages/:id/body returns null body_html + body_text even to callers with envoi.message.body.read. Defaults to false.

Response · 200

workspace_id*string · uuid
redact_bodies*boolean

Example

Request

PATCH /v1/workspaces/{workspace_id}/messages/settings
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "redact_bodies": false
}

Response

{
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "redact_bodies": false
}
get/v1/workspaces/{workspace_id}/messages/{id}Auth

Get a single outbound message (metadata + delivery verdicts; body not included). Use /:id/body for cleartext content.

Path parameters

workspace_id*string
id*string

Response · 200

id*string · uuid
workspace_id*string · uuid
domain_id*string · uuid
from_address*string
to_address*string
subject*string
template_name*string
status*enum (7)
smtp_transcript*string
spf_result*string
dkim_result*string
dmarc_result*string
bounce_code*string
bounce_detail*string
created_at*string · date-time
delivered_at*string · date-time

Example

Request

GET /v1/workspaces/{workspace_id}/messages/{id}
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "domain_id": "00000000-0000-0000-0000-000000000000",
  "from_address": "string",
  "to_address": "string",
  "subject": "string",
  "template_name": "string",
  "status": "queued",
  "smtp_transcript": "string",
  "spf_result": "string",
  "dkim_result": "string",
  "dmarc_result": "string",
  "bounce_code": "string",
  "bounce_detail": "string",
  "created_at": "2026-01-01T00:00:00.000Z",
  "delivered_at": "2026-01-01T00:00:00.000Z"
}
get/v1/workspaces/{workspace_id}/messages/{id}/bodyAuth

Get the cleartext body of an outbound message. Gated by `envoi.message.body.read` (admin+ by default; not granted to plain members).

Path parameters

workspace_id*string
id*string

Response · 200

id*string · uuid
workspace_id*string · uuid
domain_id*string · uuid
from_address*string
to_address*string
subject*string
template_name*string
status*enum (7)
smtp_transcript*string
spf_result*string
dkim_result*string
dmarc_result*string
bounce_code*string
bounce_detail*string
created_at*string · date-time
delivered_at*string · date-time
body_html*string

Cleartext HTML body. Null when the workspace has `redact_bodies = true`.

body_text*string

Cleartext text body. Null when the workspace has `redact_bodies = true`.

Example

Request

GET /v1/workspaces/{workspace_id}/messages/{id}/body
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "domain_id": "00000000-0000-0000-0000-000000000000",
  "from_address": "string",
  "to_address": "string",
  "subject": "string",
  "template_name": "string",
  "status": "queued",
  "smtp_transcript": "string",
  "spf_result": "string",
  "dkim_result": "string",
  "dmarc_result": "string",
  "bounce_code": "string",
  "bounce_detail": "string",
  "created_at": "2026-01-01T00:00:00.000Z",
  "delivered_at": "2026-01-01T00:00:00.000Z",
  "body_html": "string",
  "body_text": "string"
}

Outbound-Webhooks

get/v1/workspaces/{workspace_id}/webhooksAuth

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).

Path parameters

workspace_id*string

Response · 200

Array of object

id*string · uuid
workspace_id*string · uuid
domain_id*string · uuid

Null for the workspace-default config; set for a per-domain override.

url*string
event_types*array
signing_secret_hint*string

Last 4 chars of the active signing secret.

prev_signing_secret_active*boolean

True while a previous signing secret is still being honoured (24h grace after rotation).

prev_signing_secret_expires_at*string · date-time
disabled_at*string · date-time

Set when auto-disabled after sustained failure.

disabled_reason*string
consecutive_failures*number
created_at*string · date-time
updated_at*string · date-time

Example

Request

GET /v1/workspaces/{workspace_id}/webhooks
Authorization: Bearer YOUR_TOKEN

Response

[
  {
    "id": "00000000-0000-0000-0000-000000000000",
    "workspace_id": "00000000-0000-0000-0000-000000000000",
    "domain_id": "00000000-0000-0000-0000-000000000000",
    "url": "string",
    "event_types": [
      "string"
    ],
    "signing_secret_hint": "string",
    "prev_signing_secret_active": false,
    "prev_signing_secret_expires_at": "2026-01-01T00:00:00.000Z",
    "disabled_at": "2026-01-01T00:00:00.000Z",
    "disabled_reason": "string",
    "consecutive_failures": 0,
    "created_at": "2026-01-01T00:00:00.000Z",
    "updated_at": "2026-01-01T00:00:00.000Z"
  }
]
get/v1/workspaces/{workspace_id}/webhooks/defaultAuth

Get the workspace-default webhook config (returns null if unset).

Path parameters

workspace_id*string

Response · 200 Workspace-default config, or null if not configured.

id*string · uuid
workspace_id*string · uuid
domain_id*string · uuid

Null for the workspace-default config; set for a per-domain override.

url*string
event_types*array
signing_secret_hint*string

Last 4 chars of the active signing secret.

prev_signing_secret_active*boolean

True while a previous signing secret is still being honoured (24h grace after rotation).

prev_signing_secret_expires_at*string · date-time
disabled_at*string · date-time

Set when auto-disabled after sustained failure.

disabled_reason*string
consecutive_failures*number
created_at*string · date-time
updated_at*string · date-time

Example

Request

GET /v1/workspaces/{workspace_id}/webhooks/default
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "domain_id": "00000000-0000-0000-0000-000000000000",
  "url": "string",
  "event_types": [
    "string"
  ],
  "signing_secret_hint": "string",
  "prev_signing_secret_active": false,
  "prev_signing_secret_expires_at": "2026-01-01T00:00:00.000Z",
  "disabled_at": "2026-01-01T00:00:00.000Z",
  "disabled_reason": "string",
  "consecutive_failures": 0,
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z"
}
put/v1/workspaces/{workspace_id}/webhooks/defaultAuth

Create or replace the workspace-default outbound webhook. Returns the plaintext signing secret ONCE — store it now.

Path parameters

workspace_id*string

Request body

url*string

Destination URL. Must be absolute https:// with a public TLD.

Example: "https://api.acme.com/webhooks/mail"

event_types*array

Events the destination subscribes to.

Example: ["message.delivered","message.bounced"]

Response · 200

id*string · uuid
workspace_id*string · uuid
domain_id*string · uuid

Null for the workspace-default config; set for a per-domain override.

url*string
event_types*array
signing_secret_hint*string

Last 4 chars of the active signing secret.

prev_signing_secret_active*boolean

True while a previous signing secret is still being honoured (24h grace after rotation).

prev_signing_secret_expires_at*string · date-time
disabled_at*string · date-time

Set when auto-disabled after sustained failure.

disabled_reason*string
consecutive_failures*number
created_at*string · date-time
updated_at*string · date-time
signing_secret*string

Plaintext signing secret. Returned once on create / rotate; never reconstructable.

Example

Request

PUT /v1/workspaces/{workspace_id}/webhooks/default
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "url": "https://api.acme.com/webhooks/mail",
  "event_types": [
    "message.delivered",
    "message.bounced"
  ]
}

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "domain_id": "00000000-0000-0000-0000-000000000000",
  "url": "string",
  "event_types": [
    "string"
  ],
  "signing_secret_hint": "string",
  "prev_signing_secret_active": false,
  "prev_signing_secret_expires_at": "2026-01-01T00:00:00.000Z",
  "disabled_at": "2026-01-01T00:00:00.000Z",
  "disabled_reason": "string",
  "consecutive_failures": 0,
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z",
  "signing_secret": "string"
}
delete/v1/workspaces/{workspace_id}/webhooks/defaultAuth

Delete the workspace-default webhook config.

Path parameters

workspace_id*string

Response · 204

Workspace-default webhook deleted.

post/v1/workspaces/{workspace_id}/webhooks/default/rotate-secretAuth

Rotate the workspace-default webhook signing secret. Previous secret remains valid for 24h. Plaintext returned once.

Path parameters

workspace_id*string

Response · 200

id*string · uuid
workspace_id*string · uuid
domain_id*string · uuid

Null for the workspace-default config; set for a per-domain override.

url*string
event_types*array
signing_secret_hint*string

Last 4 chars of the active signing secret.

prev_signing_secret_active*boolean

True while a previous signing secret is still being honoured (24h grace after rotation).

prev_signing_secret_expires_at*string · date-time
disabled_at*string · date-time

Set when auto-disabled after sustained failure.

disabled_reason*string
consecutive_failures*number
created_at*string · date-time
updated_at*string · date-time
signing_secret*string

Plaintext signing secret. Returned once on create / rotate; never reconstructable.

Example

Request

POST /v1/workspaces/{workspace_id}/webhooks/default/rotate-secret
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "domain_id": "00000000-0000-0000-0000-000000000000",
  "url": "string",
  "event_types": [
    "string"
  ],
  "signing_secret_hint": "string",
  "prev_signing_secret_active": false,
  "prev_signing_secret_expires_at": "2026-01-01T00:00:00.000Z",
  "disabled_at": "2026-01-01T00:00:00.000Z",
  "disabled_reason": "string",
  "consecutive_failures": 0,
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z",
  "signing_secret": "string"
}
get/v1/workspaces/{workspace_id}/webhooks/domains/{domain_id}Auth

Get the per-domain webhook override (returns null if unset).

Path parameters

workspace_id*string
domain_id*string

Response · 200 Per-domain override, or null if not configured.

id*string · uuid
workspace_id*string · uuid
domain_id*string · uuid

Null for the workspace-default config; set for a per-domain override.

url*string
event_types*array
signing_secret_hint*string

Last 4 chars of the active signing secret.

prev_signing_secret_active*boolean

True while a previous signing secret is still being honoured (24h grace after rotation).

prev_signing_secret_expires_at*string · date-time
disabled_at*string · date-time

Set when auto-disabled after sustained failure.

disabled_reason*string
consecutive_failures*number
created_at*string · date-time
updated_at*string · date-time

Example

Request

GET /v1/workspaces/{workspace_id}/webhooks/domains/{domain_id}
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "domain_id": "00000000-0000-0000-0000-000000000000",
  "url": "string",
  "event_types": [
    "string"
  ],
  "signing_secret_hint": "string",
  "prev_signing_secret_active": false,
  "prev_signing_secret_expires_at": "2026-01-01T00:00:00.000Z",
  "disabled_at": "2026-01-01T00:00:00.000Z",
  "disabled_reason": "string",
  "consecutive_failures": 0,
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z"
}
put/v1/workspaces/{workspace_id}/webhooks/domains/{domain_id}Auth

Create or replace the per-domain webhook override. Returns the plaintext signing secret ONCE — store it now.

Path parameters

workspace_id*string
domain_id*string

Request body

url*string

Destination URL. Must be absolute https:// with a public TLD.

Example: "https://api.acme.com/webhooks/mail"

event_types*array

Events the destination subscribes to.

Example: ["message.delivered","message.bounced"]

Response · 200

id*string · uuid
workspace_id*string · uuid
domain_id*string · uuid

Null for the workspace-default config; set for a per-domain override.

url*string
event_types*array
signing_secret_hint*string

Last 4 chars of the active signing secret.

prev_signing_secret_active*boolean

True while a previous signing secret is still being honoured (24h grace after rotation).

prev_signing_secret_expires_at*string · date-time
disabled_at*string · date-time

Set when auto-disabled after sustained failure.

disabled_reason*string
consecutive_failures*number
created_at*string · date-time
updated_at*string · date-time
signing_secret*string

Plaintext signing secret. Returned once on create / rotate; never reconstructable.

Example

Request

PUT /v1/workspaces/{workspace_id}/webhooks/domains/{domain_id}
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "url": "https://api.acme.com/webhooks/mail",
  "event_types": [
    "message.delivered",
    "message.bounced"
  ]
}

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "domain_id": "00000000-0000-0000-0000-000000000000",
  "url": "string",
  "event_types": [
    "string"
  ],
  "signing_secret_hint": "string",
  "prev_signing_secret_active": false,
  "prev_signing_secret_expires_at": "2026-01-01T00:00:00.000Z",
  "disabled_at": "2026-01-01T00:00:00.000Z",
  "disabled_reason": "string",
  "consecutive_failures": 0,
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z",
  "signing_secret": "string"
}
delete/v1/workspaces/{workspace_id}/webhooks/domains/{domain_id}Auth

Delete the per-domain webhook override.

Path parameters

workspace_id*string
domain_id*string

Response · 204

Per-domain webhook override deleted.

post/v1/workspaces/{workspace_id}/webhooks/domains/{domain_id}/rotate-secretAuth

Rotate the per-domain webhook signing secret. Previous secret remains valid for 24h. Plaintext returned once.

Path parameters

workspace_id*string
domain_id*string

Response · 200

id*string · uuid
workspace_id*string · uuid
domain_id*string · uuid

Null for the workspace-default config; set for a per-domain override.

url*string
event_types*array
signing_secret_hint*string

Last 4 chars of the active signing secret.

prev_signing_secret_active*boolean

True while a previous signing secret is still being honoured (24h grace after rotation).

prev_signing_secret_expires_at*string · date-time
disabled_at*string · date-time

Set when auto-disabled after sustained failure.

disabled_reason*string
consecutive_failures*number
created_at*string · date-time
updated_at*string · date-time
signing_secret*string

Plaintext signing secret. Returned once on create / rotate; never reconstructable.

Example

Request

POST /v1/workspaces/{workspace_id}/webhooks/domains/{domain_id}/rotate-secret
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "domain_id": "00000000-0000-0000-0000-000000000000",
  "url": "string",
  "event_types": [
    "string"
  ],
  "signing_secret_hint": "string",
  "prev_signing_secret_active": false,
  "prev_signing_secret_expires_at": "2026-01-01T00:00:00.000Z",
  "disabled_at": "2026-01-01T00:00:00.000Z",
  "disabled_reason": "string",
  "consecutive_failures": 0,
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z",
  "signing_secret": "string"
}
get/v1/workspaces/{workspace_id}/webhooks/attemptsAuth

Recent delivery attempts across all of this workspace's webhook configs. Includes status code, latency, response body (truncated to 1KB), and error if any.

Path parameters

workspace_id*string

Query parameters

limit*string

Response · 200

Array of object

id*string · uuid
event_id*string
event_type*string
attempt_number*number
status_code*number
error_message*string
latency_ms*number
attempted_at*string · date-time
url*string
domain_id*string · uuid

Example

Request

GET /v1/workspaces/{workspace_id}/webhooks/attempts
Authorization: Bearer YOUR_TOKEN

Response

[
  {
    "id": "00000000-0000-0000-0000-000000000000",
    "event_id": "string",
    "event_type": "string",
    "attempt_number": 0,
    "status_code": 0,
    "error_message": "string",
    "latency_ms": 0,
    "attempted_at": "2026-01-01T00:00:00.000Z",
    "url": "string",
    "domain_id": "00000000-0000-0000-0000-000000000000"
  }
]

Templates

get/v1/workspaces/{workspace_id}/templatesAuth

List templates / layouts / partials in a workspace. Filter via ?kind=template|layout|partial (default: all). Filter by tags via ?tags=foo,bar (comma-separated).

Path parameters

workspace_id*string

Query parameters

tags*string
kind*string

Response · 200

data*array

Example

Request

GET /v1/workspaces/{workspace_id}/templates
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "workspace_id": "00000000-0000-0000-0000-000000000000",
      "name": "string",
      "kind": "template",
      "subject": "string",
      "body_html": "string",
      "body_text": "string",
      "variables_schema": {},
      "tags": [
        "string"
      ],
      "created_at": "2026-01-01T00:00:00.000Z",
      "updated_at": "2026-01-01T00:00:00.000Z"
    }
  ]
}
post/v1/workspaces/{workspace_id}/templatesAuth

Create or update a template, layout, or partial. Idempotent on (workspace_id, name); `kind` cannot change after the first upsert.

Path parameters

workspace_id*string

Request body

name*string

Unique template name within the app (lowercase letters, digits, underscore, hyphen)

Example: "order-confirmation"

kindenum (3)

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.

subject*string

Subject line (Handlebars). Layouts and partials ignore this field — they are not standalone-sendable. Pass an empty string for those.

body_html*string

HTML body (Handlebars)

body_textstring

Plain-text body (Handlebars). Auto-derived if omitted.

variables_schemaobject

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.

tagsarray

Workspace-scoped labels for organizing templates. Free-form strings; the editor surfaces existing tags as autocomplete.

Response · 201

id*string · uuid
workspace_id*string · uuid
name*string
kind*enum (3)
subject*string
body_html*string
body_text*string
variables_schema*object

Declared variable schema; arbitrary JSON or null if not configured.

tags*array
created_at*string · date-time
updated_at*string · date-time

Example

Request

POST /v1/workspaces/{workspace_id}/templates
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "name": "order-confirmation",
  "kind": "template",
  "subject": "string",
  "body_html": "string",
  "body_text": "string",
  "variables_schema": {},
  "tags": [
    "string"
  ]
}

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "name": "string",
  "kind": "template",
  "subject": "string",
  "body_html": "string",
  "body_text": "string",
  "variables_schema": {},
  "tags": [
    "string"
  ],
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z"
}
get/v1/workspaces/{workspace_id}/templates/tagsAuth

Distinct tag list across all templates in this workspace (for autocomplete)

Path parameters

workspace_id*string

Response · 200

data*array

Example

Request

GET /v1/workspaces/{workspace_id}/templates/tags
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    "string"
  ]
}
get/v1/workspaces/{workspace_id}/templates/{name}Auth

Get a template, layout, or partial by name.

Path parameters

workspace_id*string
name*string

Response · 200

id*string · uuid
workspace_id*string · uuid
name*string
kind*enum (3)
subject*string
body_html*string
body_text*string
variables_schema*object

Declared variable schema; arbitrary JSON or null if not configured.

tags*array
created_at*string · date-time
updated_at*string · date-time

Example

Request

GET /v1/workspaces/{workspace_id}/templates/{name}
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "name": "string",
  "kind": "template",
  "subject": "string",
  "body_html": "string",
  "body_text": "string",
  "variables_schema": {},
  "tags": [
    "string"
  ],
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z"
}
delete/v1/workspaces/{workspace_id}/templates/{name}Auth

Delete a template, layout, or partial by name.

Path parameters

workspace_id*string
name*string

Response · 204

Template deleted.

post/v1/workspaces/{workspace_id}/templates/{name}/renderAuth

Render a saved template against a data payload. Returns subject + html + text without sending.

Path parameters

workspace_id*string
name*string

Request body

data*object

Data object to render into the template

Example: {"name":"Alice","order_id":"ord_123"}

Response · 200

subject*string
html*string

Fully-rendered HTML body.

text*string

Plain-text body (derived if not authored).

Example

Request

POST /v1/workspaces/{workspace_id}/templates/{name}/render
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "data": {
    "name": "Alice",
    "order_id": "ord_123"
  }
}

Response

{
  "subject": "string",
  "html": "string",
  "text": "string"
}
post/v1/workspaces/{workspace_id}/templates/previewAuth

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.

Path parameters

workspace_id*string

Request body

subjectstring

Subject template (Handlebars).

body_html*string

HTML body to render (Handlebars).

dataobject

Sample data to render variables with.

Response · 200

subject*string
html*string

Fully-rendered HTML body.

text*string

Plain-text body (derived if not authored).

Example

Request

POST /v1/workspaces/{workspace_id}/templates/preview
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "subject": "string",
  "body_html": "string",
  "data": {}
}

Response

{
  "subject": "string",
  "html": "string",
  "text": "string"
}
post/v1/workspaces/{workspace_id}/templates/lintAuth

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.

Path parameters

workspace_id*string

Request body

subjectstring

Subject template (Handlebars).

body_html*string

HTML body to render (Handlebars).

dataobject

Sample data to render variables with.

Response · 200

score*number

0-100, higher is better. ≥90 "looks great"; <70 "likely to spam-folder".

findings*array

Example

Request

POST /v1/workspaces/{workspace_id}/templates/lint
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "subject": "string",
  "body_html": "string",
  "data": {}
}

Response

{
  "score": 0,
  "findings": [
    {
      "id": "string",
      "severity": "error",
      "message": "string",
      "suggestion": "string"
    }
  ]
}
get/v1/workspaces/{workspace_id}/templates/{name}/variablesAuth

Variables auto-detected from the saved template body + subject (Handlebars expressions, dotted paths preserved, dedup'd in first-occurrence order).

Path parameters

workspace_id*string
name*string

Response · 200

data*array

Variables auto-detected from the saved template body + subject (Handlebars expressions, dotted paths preserved).

Example

Request

GET /v1/workspaces/{workspace_id}/templates/{name}/variables
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    "string"
  ]
}
post/v1/workspaces/{workspace_id}/templates/{name}/test-sendAuth

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.

Path parameters

workspace_id*string
name*string

Request body

from*string

From email address. Must be on a verified domain owned by the workspace, same as a normal send.

to*string

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.

dataobject

Variable values to render into the template. Keys correspond to the dotted paths returned by the /variables endpoint.

Example: {"name":"Alice","code":"123456"}

Response · 202

accepted*boolean

Always true on a 202.

Example: true

from*string
to*string
subject*string

Example

Request

POST /v1/workspaces/{workspace_id}/templates/{name}/test-send
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "from": "string",
  "to": "string",
  "data": {
    "name": "Alice",
    "code": "123456"
  }
}

Response

{
  "accepted": true,
  "from": "string",
  "to": "string",
  "subject": "string"
}
post/v1/workspaces/{workspace_id}/templates/{name}/sendAuth

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.

Path parameters

workspace_id*string
name*string

Headers

Idempotency-Keystring

Opaque client-generated string (1–256 chars, `[A-Za-z0-9_-:]`) used to deduplicate retries within a 24h window.

Request body

data*object

Data object to render into the template

Example: {"name":"Alice","order_id":"ord_123"}

from*string

From email address

Example: "hello@acme.com"

to*string

To email address or list of addresses

Example: "user@example.com"

subjectstring

Subject override (optional; uses template subject otherwise)

Response · 202 Send accepted; queued for delivery.

accepted*boolean

Always true on a 202.

Example: true

from*string
to*string
subject*string

Example

Request

POST /v1/workspaces/{workspace_id}/templates/{name}/send
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "data": {
    "name": "Alice",
    "order_id": "ord_123"
  },
  "from": "hello@acme.com",
  "to": "user@example.com",
  "subject": "string"
}

Response

{
  "accepted": true,
  "from": "string",
  "to": "string",
  "subject": "string"
}
post/v1/workspaces/{workspace_id}/templates/{name}/send-batchAuth

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.

Path parameters

workspace_id*string
name*string

Request body

messages*array

Up to 100 messages to render and enqueue in a single call. Each is processed independently — partial successes are allowed.

Response · 200

data*array
summary*object
accepted*number
rejected*number

Example

Request

POST /v1/workspaces/{workspace_id}/templates/{name}/send-batch
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "messages": [
    {
      "from": "noreply@acme.com",
      "to": "alice@example.com",
      "subject": "string",
      "data": {
        "name": "Alice",
        "code": "A1B2C3"
      },
      "idempotency_key": "string"
    }
  ]
}

Response

{
  "data": [
    {
      "index": 0,
      "id": "string",
      "status": "accepted",
      "error": {}
    }
  ],
  "summary": {
    "accepted": 0,
    "rejected": 0
  }
}

Suppression

get/v1/workspaces/{workspace_id}/suppressionAuth

List suppressed recipients in the workspace (most-recent first, capped at 500).

Path parameters

workspace_id*string

Response · 200

data*array

Example

Request

GET /v1/workspaces/{workspace_id}/suppression
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "workspace_id": "00000000-0000-0000-0000-000000000000",
      "email": "user@example.com",
      "reason": "string",
      "source": "manual",
      "added_at": "2026-01-01T00:00:00.000Z"
    }
  ]
}
post/v1/workspaces/{workspace_id}/suppressionAuth

Add an email to the workspace suppression list

Path parameters

workspace_id*string

Request body

Response · 201

id*string · uuid

Suppression row id.

workspace_id*string · uuid

Workspace this suppression belongs to.

email*string

Suppressed recipient (lowercased on insert).

Example: "user@example.com"

reason*string

Free-text reason; null when unspecified.

source*enum (3)

Where the row originated.

added_at*string · date-time

Insertion (or last-touch) timestamp.

Example

Request

POST /v1/workspaces/{workspace_id}/suppression
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{}

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "email": "user@example.com",
  "reason": "string",
  "source": "manual",
  "added_at": "2026-01-01T00:00:00.000Z"
}
delete/v1/workspaces/{workspace_id}/suppression/{email}Auth

Remove an address from the workspace suppression list.

Path parameters

workspace_id*string
email*string

Response · 204

Suppression removed.