API reference

Emails

Send transactional emails, alone or in batches

These routes send transactional emails: one recipient each, triggered by something the person did (a password reset, a receipt, an invitation). Transactional emails explained.

Three content modes:

  • a template written in the dashboard, with variables (recommended);
  • markdown, wrapped in your space's email design;
  • raw html and/or text, sent as given.

Send an email

POST /v1/emails

Queues one email, usually sent within seconds. Allowed with a sending access key.

to
string required
One recipient: ada@example.com or Ada Lovelace <ada@example.com>.
from
string
Name <address> or an address. It must use a verified domain of the space. Defaults to the template sender, then to the space's default sender.
replyTo
string
Where replies go. Defaults to the template reply-to, then to the space's.
subject
string
Up to 500 characters. Required with raw content; with a template, it overrides the template subject.
template
string
Id or alias of a template. Its published version is sent, never its draft.
variables
object
Values for the template, read as {{ data.name }}. 32 KB at most.
markdown
string
Content in Markdown, wrapped in the space design. Cannot be combined with html or text.
html
string
Raw HTML, sent as given. Kula derives the plain-text part if you don't send text.
text
string
Plain-text version.
headers
object
Extra headers, 20 at most. Headers Kula manages (From, To, List-Unsubscribe…) are ignored.
tags
object
Up to 20 key/value pairs of your own, such as {"order_id": "1042"}. They come back in every webhook about this email.

Send either a template or content (markdown, or html and/or text), never both. Content is limited to 1 MB.

An Idempotency-Key header makes the request safe to retry: see Idempotency.

curl https://api.kulamail.com/v1/emails \
  -H "Authorization: Bearer ml_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reset-7f3a9c" \
  -d '{
    "to": "ada@example.com",
    "template": "password-reset",
    "variables": {
      "reset_url": "https://app.acme.com/reset?token=7f3a9c",
      "expires_in": "30 minutes"
    },
    "tags": { "user_id": "usr_1042" }
  }'
201 Created
{
  "id": "8f6c2a1e-3b4d-4e5f-9a0b-1c2d3e4f5a6b",
  "status": "QUEUED",
  "to": "ada@example.com",
  "createdAt": "2026-10-17T09:30:00Z"
}

status is QUEUED, or SUPPRESSED when the address hard bounced before: nothing is sent and nothing is counted. A replay of an earlier Idempotency-Key answers 200 with the header Idempotent-Replayed: true.

StatusCodeWhen
400validation_error, bad_request, invalid_to, invalid_fromMissing recipient or content, template and content together, variables over 32 KB…
400domain_not_verifiedThe sender does not use a verified domain of the space
402quota_exceededThe monthly limit of your plan is reached, or a payment failed
404not_foundNo template with this id or alias
409template_not_publishedThe template has never been published

Send a batch

POST /v1/emails/batch

Sends up to 100 emails in one request. The body is an array of emails, each with the same fields as Send an email. Allowed with a sending access key.

Each email is accepted or refused on its own: one invalid email does not stop the others. Results come back in the order of the request.

With an Idempotency-Key header, email number i (from 0) uses the key <key>-<i>. To control keys yourself, set an idempotencyKey field (200 characters at most) on each email instead.

curl https://api.kulamail.com/v1/emails/batch \
  -H "Authorization: Bearer ml_your_api_key" \
  -H "Content-Type: application/json" \
  -d '[
    { "to": "ada@example.com", "template": "invoice", "variables": { "amount": "$19.00" }, "idempotencyKey": "invoice-1042" },
    { "to": "not-an-address", "template": "invoice", "variables": { "amount": "$49.00" }, "idempotencyKey": "invoice-1043" }
  ]'
200 OK
{
  "data": [
    { "index": 0, "id": "8f6c2a1e-3b4d-4e5f-9a0b-1c2d3e4f5a6b", "status": "QUEUED" },
    { "index": 1, "error": "invalid_to", "message": "Enter a valid recipient address" }
  ]
}

The request itself fails with 400 only when the array is empty or holds more than 100 emails.

Get an email

GET /v1/emails/{id}

Returns any email of the space (transactional, campaign or flow) with its events. Requires a full access key.

curl https://api.kulamail.com/v1/emails/8f6c2a1e-3b4d-4e5f-9a0b-1c2d3e4f5a6b \
  -H "Authorization: Bearer ml_your_api_key"
200 OK
{
  "message": {
    "id": "8f6c2a1e-3b4d-4e5f-9a0b-1c2d3e4f5a6b",
    "toEmail": "ada@example.com",
    "subject": "Reset your password",
    "source": "TRANSACTIONAL",
    "stream": "TRANSACTIONAL",
    "status": "DELIVERED",
    "bounceType": null,
    "campaignId": null,
    "flowId": null,
    "templateId": "c41d7e2a-9b8f-4a6e-b3d2-5f1e0a9c8b7d",
    "createdAt": "2026-10-17T09:30:00Z",
    "sentAt": "2026-10-17T09:30:02Z",
    "deliveredAt": "2026-10-17T09:30:04Z",
    "bouncedAt": null,
    "complainedAt": null,
    "openedAt": "2026-10-17T09:41:17Z",
    "openCount": 1,
    "clickedAt": "2026-10-17T09:41:30Z",
    "clickCount": 1,
    "unsubscribedAt": null
  },
  "fromEmail": "hello@acme.com",
  "fromName": "Acme",
  "replyTo": null,
  "contactId": "0d9e8f7a-6b5c-4d3e-2f1a-0b9c8d7e6f5a",
  "statusDetail": null,
  "mailboxProvider": "GOOGLE",
  "importReview": false,
  "attempts": 1,
  "sendAfter": "2026-10-17T09:30:00Z",
  "tags": { "user_id": "usr_1042" },
  "events": [
    { "id": "1a2b…", "type": "SENT", "machine": false, "url": null, "userAgent": null, "detail": null, "occurredAt": "2026-10-17T09:30:02Z" },
    { "id": "3c4d…", "type": "DELIVERED", "machine": false, "url": null, "userAgent": null, "detail": null, "occurredAt": "2026-10-17T09:30:04Z" },
    { "id": "5e6f…", "type": "OPENED", "machine": false, "url": null, "userAgent": "Mozilla/5.0 …", "detail": null, "occurredAt": "2026-10-17T09:41:17Z" },
    { "id": "7a8b…", "type": "CLICKED", "machine": false, "url": "https://app.acme.com/reset?token=7f3a9c", "userAgent": "Mozilla/5.0 …", "detail": null, "occurredAt": "2026-10-17T09:41:30Z" }
  ]
}
FieldValues
message.statusQUEUED, SENDING, SENT, DELIVERED, BOUNCED, COMPLAINED, FAILED, SUPPRESSED, CANCELLED
message.bounceTypeHARD, SOFT or BLOCKED. Bounces explained
message.sourceCAMPAIGN, FLOW, TRANSACTIONAL, TEST, CONFIRMATION
events[].typeSENT, DELIVERED, DELAYED, BOUNCED, COMPLAINED, OPENED, CLICKED, UNSUBSCRIBED, FAILED, REJECTED
events[].machinetrue for opens and clicks made by a privacy proxy or a link scanner, not a person
Instead of polling, subscribe a webhook to email.delivered, email.bounced and the other email events.