Emails
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.
ada@example.com or Ada Lovelace <ada@example.com>.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.{{ data.name }}. 32 KB at most.html or text.text.From, To, List-Unsubscribe…) are ignored.{"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" }
}'
const response = await fetch('https://api.kulamail.com/v1/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.KULA_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `reset-${token}`
},
body: JSON.stringify({
to: user.email,
template: 'password-reset',
variables: {
reset_url: `https://app.acme.com/reset?token=${token}`,
expires_in: '30 minutes'
},
tags: { user_id: user.id }
})
})
const email = await response.json()
{
"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.
| Status | Code | When |
|---|---|---|
| 400 | validation_error, bad_request, invalid_to, invalid_from | Missing recipient or content, template and content together, variables over 32 KB… |
| 400 | domain_not_verified | The sender does not use a verified domain of the space |
| 402 | quota_exceeded | The monthly limit of your plan is reached, or a payment failed |
| 404 | not_found | No template with this id or alias |
| 409 | template_not_published | The 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" }
]'
{
"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"
{
"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" }
]
}
| Field | Values |
|---|---|
message.status | QUEUED, SENDING, SENT, DELIVERED, BOUNCED, COMPLAINED, FAILED, SUPPRESSED, CANCELLED |
message.bounceType | HARD, SOFT or BLOCKED. Bounces explained |
message.source | CAMPAIGN, FLOW, TRANSACTIONAL, TEST, CONFIRMATION |
events[].type | SENT, DELIVERED, DELAYED, BOUNCED, COMPLAINED, OPENED, CLICKED, UNSUBSCRIBED, FAILED, REJECTED |
events[].machine | true for opens and clicks made by a privacy proxy or a link scanner, not a person |
email.delivered, email.bounced and the other email events.