API reference

Introduction

Base URL, API keys, errors, rate limits, pagination and idempotency

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 keyAccount key
Created inSettings › API keysAccount › Account API keys
Acts onIts own spaceAny space of the account, chosen with X-Space-Id
Manages spaces (/v1/spaces)NoYes
Who can create itAdmins and ownersAdmins 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.
RouteSending access
POST /v1/emailsYes
POST /v1/emails/batchYes
POST /v1/eventsYes
GET /v1/meYes
Anything elseNo: 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.

StatusCodesWhat happened
400bad_request, validation_error, invalid_body, space_required, invalid_to, invalid_from, domain_not_verified, invalid_urlThe request is invalid. Read message
401unauthorizedMissing, unknown or revoked API key
402quota_exceededThe monthly sending limit of your plan is reached. See Billing
403forbidden, account_suspendedThe key cannot use this route or this space, or the account is suspended
404not_foundThe resource does not exist in this space
405method_not_allowedWrong HTTP method for this route
409conflict, contact_unreachable, alias_taken, template_not_published, nothing_to_publishThe request conflicts with the current state, such as a duplicate alias
413payload_too_largeThe file is over 50 MB
429rate_limitedToo many requests. Wait for Retry-After
500internal_errorSomething broke on our side. Retry later
503unavailableA 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).

With 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 200 and the header Idempotent-Replayed: true, instead of 201.
  • 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

ResourceRoutes
EmailsSend transactional emails, alone or in batches, and read their events
TemplatesTransactional templates, with a draft and a published version
ContactsContacts, list subscriptions and bulk imports
EventsRecord what contacts do in your product
Lists and segmentsSubscription lists and saved filters
Campaigns and flowsCreate and schedule campaigns, read flows
Domains and suppressionsSending domains and the suppression list
SpacesSpaces of the account, for account keys
WebhooksEndpoints notified of deliveries, opens, clicks and unsubscribes