Introduction
The Kula API is a REST API that takes and returns JSON.
Base URL
https://api.kulamail.com/v1
HTTPS only. Send JSON bodies with Content-Type: application/json.
Authentication
Pass an API key in the Authorization header:
curl https://api.kulamail.com/v1/me \
-H "Authorization: Bearer ml_your_api_key"
A key starts with ml_ and is shown once, at creation: store it in a secret manager, never in front-end code. The dashboard then shows only its prefix (ml_a1b2c3d4).
Space keys and account keys
| Space key | Account key | |
|---|---|---|
| Created in | Settings › API keys | Account › Account API keys |
| Acts on | Its own space | Any space of the account, chosen with X-Space-Id |
Manages spaces (/v1/spaces) | No | Yes |
| Who can create it | Admins and owners | Admins and owners |
Most integrations need a space key. Use an account key when your product creates one space per customer or brand: see Spaces.
With an account key, every route outside /v1/spaces acts on the space named in the X-Space-Id header:
curl https://api.kulamail.com/v1/contacts \
-H "Authorization: Bearer ml_your_account_key" \
-H "X-Space-Id: 3f1c9a52-7b0e-4d8a-9c61-2e5f8b7a4d10"
Without the header the API answers 400 space_required. A space key sent with another space's id answers 403 forbidden.
Permissions
Each key has one of two permissions:
- Full access: every route.
- Sending access: only the routes below, for servers that just send emails and events.
| Route | Sending access |
|---|---|
POST /v1/emails | Yes |
POST /v1/emails/batch | Yes |
POST /v1/events | Yes |
GET /v1/me | Yes |
| Anything else | No: 403 forbidden |
Test your key
GET /v1/me
Returns the key making the request, its account, and the space it acts on.
curl https://api.kulamail.com/v1/me \
-H "Authorization: Bearer ml_your_api_key"
{
"key": {
"name": "Production server",
"permission": "SENDING_ACCESS",
"prefix": "ml_a1b2c3d4"
},
"account": {
"id": "b7d2e8f1-4c3a-4e9b-8f60-1a2b3c4d5e6f",
"name": "Acme"
},
"space": {
"id": "3f1c9a52-7b0e-4d8a-9c61-2e5f8b7a4d10",
"name": "Acme"
}
}
space is absent when an account key calls without X-Space-Id.
Errors
Errors use HTTP status codes and always the same body:
{
"error": "validation_error",
"message": "email: Email is required",
"status": 400,
"fields": {
"email": "Email is required"
}
}
error is a stable code to test in your code; message is for humans and may change. fields only appears on validation errors.
| Status | Codes | What happened |
|---|---|---|
| 400 | bad_request, validation_error, invalid_body, space_required, invalid_to, invalid_from, domain_not_verified, invalid_url | The request is invalid. Read message |
| 401 | unauthorized | Missing, unknown or revoked API key |
| 402 | quota_exceeded | The monthly sending limit of your plan is reached. See Billing |
| 403 | forbidden, account_suspended | The key cannot use this route or this space, or the account is suspended |
| 404 | not_found | The resource does not exist in this space |
| 405 | method_not_allowed | Wrong HTTP method for this route |
| 409 | conflict, contact_unreachable, alias_taken, template_not_published, nothing_to_publish | The request conflicts with the current state, such as a duplicate alias |
| 413 | payload_too_large | The file is over 50 MB |
| 429 | rate_limited | Too many requests. Wait for Retry-After |
| 500 | internal_error | Something broke on our side. Retry later |
| 503 | unavailable | A dependency (DNS, sending provider) is unavailable. Retry later |
Rate limits
Each key can make 20 requests per second and 10,000 requests per day (the day resets at midnight UTC).
Over a limit, the API answers 429 rate_limited with a Retry-After header in seconds (1 for the per-second limit, the time until midnight UTC for the daily one).
POST /v1/emails/batch, 100 emails count as one request.Pagination
List routes that can grow large take a page parameter, starting at 0, and return the totals:
{
"contacts": [ ... ],
"page": 0,
"size": 50,
"total": 1284,
"totalPages": 26
}
The page size depends on the resource: contacts take a size parameter (50 by default, 200 at most), suppressions too (50 by default, 100 at most), campaigns come 25 per page.
Idempotency
To retry a send without risking a duplicate email, add an Idempotency-Key header:
curl https://api.kulamail.com/v1/emails \
-H "Authorization: Bearer ml_your_api_key" \
-H "Idempotency-Key: password-reset-7f3a9c" \
-H "Content-Type: application/json" \
-d '{ "to": "ada@example.com", "template": "password-reset" }'
- The key is any string up to 200 characters, unique within the space. Use an id from your own system, such as
order-1042-receipt. - If an email already exists with that key, the API sends nothing and returns the first email with
200and the headerIdempotent-Replayed: true, instead of201. - Two concurrent requests with the same key are safe: the second waits for the first and replays it.
Idempotency applies to POST /v1/emails and POST /v1/emails/batch.
Resources
| Resource | Routes |
|---|---|
| Emails | Send transactional emails, alone or in batches, and read their events |
| Templates | Transactional templates, with a draft and a published version |
| Contacts | Contacts, list subscriptions and bulk imports |
| Events | Record what contacts do in your product |
| Lists and segments | Subscription lists and saved filters |
| Campaigns and flows | Create and schedule campaigns, read flows |
| Domains and suppressions | Sending domains and the suppression list |
| Spaces | Spaces of the account, for account keys |
| Webhooks | Endpoints notified of deliveries, opens, clicks and unsubscribes |