Test spaces
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_.
| Key | Mode |
|---|---|
| Space key | The mode of its space: a key created in a test space is a test key |
| Account key | Chosen 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.
| Address | Outcome |
|---|---|
delivered@test.kulamail.com | Delivered |
bounced@test.kulamail.com | Permanent bounce (HARD) |
soft-bounced@test.kulamail.com | Temporary bounce (SOFT) |
blocked@test.kulamail.com | Refused by the receiving server (BLOCKED) |
complained@test.kulamail.com | Delivered, then marked as spam |
delayed@test.kulamail.com | Delayed, then delivered |
opened@test.kulamail.com | Delivered, then opened |
clicked@test.kulamail.com | Delivered, opened, then the first link clicked |
| Any other address | Delivered |
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 | |
|---|---|
| Delivery | Nothing is delivered, except copies to account members |
| Copies to members | An email to the address of an account member is delivered for real, subject prefixed with [Test], 50 per day per account |
| Quota and billing | Not counted |
| Sending domains | Verified at once, no DNS check. The DNS records are still shown |
| Account review | Not required: campaigns can be scheduled right away |
| Suppressions | Always limited to the space, permanent bounces included |
| Free plan | Test 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 | |
|---|---|
| Emails | 1,000 per day per account, all test spaces together. Beyond, emails stay QUEUED until the next day |
| Contacts | 10,000 per test space. Beyond, 409 test_space_contact_limit |
| Retention | Emails, 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": { ... }
}