Send CRM emails from a record. Outbound messages land as comments with type=email_out; inbound replies arrive via the do-rmail webhook and are inserted as email_in comments.
All endpoints require a session or Bearer token with write access on the target table.
POST /api/email/send
Queue an outbound email and create the corresponding comment row.
Request
{
"tableId": 123,
"recordId": 456,
"smtpIntegrationId": 7,
"fromIdentity": { "name": "Sales", "email": "sales@acme.com" },
"to": ["lead@example.org"],
"cc": [],
"bcc": [],
"subject": "Welcome {{field:title}}",
"body": "<p>Hi {{field:title}},</p>",
"signatureId": 12,
"attachments": [
{ "name": "brochure.pdf", "path": "/space-1-table-2-brochure.pdf", "type": "application/pdf", "size": 12345, "host": "..." }
]
}
fromIdentity.email— the domain (right of@) MUST be in the SMTP integration'sfromDomains. Otherwise400.subjectandbodymay contain{{field:slug}}tokens — resolved server-side at send time against the current record state.attachments[]follow the same shape as comment attachments and must already be uploaded under a path beginning{spaceId}-{tableId}-.
Response (202-style)
{
"id": 789,
"status": "queued",
"comment": { "id": 789, "type": "email_out", "metadata": { ... } }
}
The actual SMTP transmission runs asynchronously. Poll /api/table/{tid}/record/{rid}/comments/get to observe metadata.status flipping to sent, read, bounced, or dropped.
POST /api/email/recordContext
Return the record's email-type field values + non-key, non-calc field list — used by the compose modal to pre-populate the To suggestions and the {{field:}} picker.
{ "tableId": 123, "recordId": 456 }
→
{
"emails": ["lead@example.org"],
"fields": [
{"id": "title", "name": "Title"},
{"id": "company", "name": "Company"}
]
}
POST /api/email/smtpOptions
Returns the account's SMTP integrations that have at least one configured From-domain.
[
{
"id": 7,
"label": "smtp@acme.com",
"fromDomains": ["acme.com"],
"fromIdentities": [{"name": "Sales", "email": "sales@acme.com"}],
"disablePixel": false
}
]
Email signatures
Per-user, account-wide HTML signatures.
| Method | Path | Body | Notes |
|---|---|---|---|
| POST | /api/email/signatures/list |
{} |
Returns the current user's signatures |
| POST | /api/email/signatures/create |
{name, html, is_default} |
Returns the created row |
| POST | /api/email/signature/{id}/update |
{name, html, is_default} |
404 unless owned by current user |
| POST | /api/email/signature/{id}/delete |
{} |
html is capped at 200 KB.
Tracking pixel
GET /email/pixel/{token}.png is public — embedded in outbound emails when the SMTP integration's disablePixel flag is off. First hit flips the comment's metadata.status from sent to read and stamps metadata.readAt. Subsequent hits are silently ignored. Always returns a 1×1 transparent PNG with Cache-Control: no-store so token validity is never leaked.
Inbound webhook
POST /webhooks/email_receive accepts raw RFC822 bytes (Content-Type: text/raw). Designed to be called by email2hook; no signing or auth header — that matches email2hook's wire contract.
Trust gate is the 16-hex reply-{token}@catchDomain token in the recipient address. Tokens are opaque, scoped to one specific record + parent comment, and looked up against the email_tokens table. Unknown or absent tokens are dropped silently with a 200 (so the endpoint doesn't leak token validity to scanners).
Not a customer endpoint.