Email

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's fromDomains. Otherwise 400.
  • subject and body may 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.