API reference

Test spaces

Staging and integration tests without delivering a single email, with simulated bounces, complaints, opens and clicks

A test space works like any other space: API, templates, campaigns, flows, forms, webhooks and statistics. But no email is delivered. Each email is rendered, then kept: the activity log of the dashboard shows its HTML, its text version, its headers and its events.

Use it for:

  • Staging: point the staging environment of your app at a test space.
  • Integration tests: trigger bounces, complaints, opens and clicks on purpose, and check how your code reacts to the webhooks.

Create a test space

In the dashboard: Account › Spaces › New space, mode Test. With the API: POST /v1/spaces with an account key in test mode.

The mode is set at creation and never changes. Test spaces show a Test badge and a banner on every page.

Test keys

Test keys start with ml_test_.

KeyMode
Space keyThe mode of its space: a key created in a test space is a test key
Account keyChosen at creation (Account › Account API keys), live by default

A test account key only sees, creates and reaches test spaces; a live key, only live spaces. An X-Space-Id of the other mode answers 404, like an unknown space.

externalId is unique per account and per mode: your staging can reuse the ids of production.

# Staging: same code as production, a test key
curl https://api.kulamail.com/v1/spaces \
  -H "Authorization: Bearer ml_test_your_account_key" \
  -H "Content-Type: application/json" \
  -d '{ "name": "The Daily Fern", "externalId": "blog_42" }'

Simulated addresses

Send to an address of test.kulamail.com to choose the outcome. Add a +suffix to get distinct contacts: bounced+alice@test.kulamail.com.

AddressOutcome
delivered@test.kulamail.comDelivered
bounced@test.kulamail.comPermanent bounce (HARD)
soft-bounced@test.kulamail.comTemporary bounce (SOFT)
blocked@test.kulamail.comRefused by the receiving server (BLOCKED)
complained@test.kulamail.comDelivered, then marked as spam
delayed@test.kulamail.comDelayed, then delivered
opened@test.kulamail.comDelivered, then opened
clicked@test.kulamail.comDelivered, opened, then the first link clicked
Any other addressDelivered

Events arrive a few seconds after the send and have the usual effects: statuses, suppressions, flow conditions and webhooks. Simulated opens and clicks count as human.

In a live space, an address of test.kulamail.com is never sent: the email fails with "Test addresses only work in test spaces".

What differs

Test space
DeliveryNothing is delivered, except copies to account members
Copies to membersAn email to the address of an account member is delivered for real, subject prefixed with [Test], 50 per day per account
Quota and billingNot counted
Sending domainsVerified at once, no DNS check. The DNS records are still shown
Account reviewNot required: campaigns can be scheduled right away
SuppressionsAlways limited to the space, permanent bounces included
Free planTest spaces do not count in its single space

Domain names

A sending domain is unique across all spaces, live and test. A test space cannot add the domain of your live space: it answers 409 domain_taken.

Use another name for staging, such as staging.dailyfern.com. Any name works, since it is not checked in DNS.

Limits

Limit
Emails1,000 per day per account, all test spaces together. Beyond, emails stay QUEUED until the next day
Contacts10,000 per test space. Beyond, 409 test_space_contact_limit
RetentionEmails, events and content kept 30 days

An account keeps at least one live space. A test space can always be deleted.

Webhooks

Every webhook carries livemode: false in a test space, true in a live space. Webhooks are delivered for real, with the same signature. Payload.

{
  "id": "evt_5f0c7d2b9a8e4f1c8b3a6d9e2f1c0b7a",
  "type": "email.bounced",
  "created_at": "2026-10-17T09:30:04Z",
  "space_id": "9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
  "livemode": false,
  "data": { ... }
}