Test mode
Send with an sk_test_ key and PostStack runs the real request checks, records the email as delivered and fires its webhooks — without handing anything to a mail server.
Get a test key#
Create the key
In the dashboard, open Configuration → API keys, choose Create API key and set Mode to Test — simulates delivery, nothing is sent. Over the API, pass
"mode": "test"toPOST /api-keys.Use it where you would use a live key
Test keys start with
sk_test_. There is no flag to set in the SDKs or the request: the key decides. Read it from the environment so CI and staging get a test key and production a live one without a code change.
import { PostStack } from '@poststack.dev/sdk';
// sk_test_… in CI and staging, sk_live_… in production
const poststack = new PostStack(process.env.POSTSTACK_API_KEY!);
const res = await poststack.emails.send({
from: 'hello@acme.com',
to: ['user@example.com'],
subject: 'Test email',
html: '<p>This is a test.</p>',
});
res.test_mode; // true with a test key, absent with a live keycurl -X POST https://api.poststack.dev/emails \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"from": "hello@acme.com",
"to": ["user@example.com"],
"subject": "Test email",
"html": "<p>This is a test.</p>"
}'
# HTTP/1.1 202 Accepted
# { "id": "em_a2r19yq5nah5fx3ziepm3ehu", "test_mode": true }import os
from poststack import PostStack
ps = PostStack(api_key=os.environ["POSTSTACK_API_KEY"]) # sk_test_… in CI
result = ps.emails.send({
"from": "hello@acme.com",
"to": ["user@example.com"],
"subject": "Test email",
"html": "<p>This is a test.</p>",
})
assert result.get("test_mode") is Trueclient := poststack.NewClient(os.Getenv("POSTSTACK_API_KEY")) // sk_test_… in CI
res, err := client.Emails.Send(ctx, &poststack.SendEmailInput{
From: "hello@acme.com",
To: []string{"user@example.com"},
Subject: "Test email",
Html: "<p>This is a test.</p>",
})
if err != nil {
return err
}
// res.TestMode is true with a test keyWhat a test send checks#
A test send goes through the same request pipeline as a live one, up to the moment the message would be queued. A request that fails here with a test key fails the same way with a live key:
- Authentication, permissions and rate limits.
- Request validation — addresses, subject, body, attachment count and size.
- The
fromdomain is added and verified, and the key is allowed to send from it. - The template exists and is published; variables render.
- Suppressed recipients are removed (an
email.suppressedwebhook fires), and a send to only suppressed recipients fails with422. - The spam scan: content at the block threshold fails with
422exactly as it would live. - Idempotency keys replay and conflict exactly as they do live.
What it does not do#
| Live send | Test send |
|---|---|
| Queued, then handed to the mail server | Stored straight away as delivered; no connection to any mail server |
scheduled_at delays it | Ignored — marked delivered immediately, so it cannot be cancelled or rescheduled |
| Counts toward your monthly email allowance | Never counts |
| New and Free accounts: counts toward the daily cap on sends to external addresses | Never counts |
| Open pixel and click tracking added to the HTML | HTML stored as sent; no tracking |
| Real delivered, bounced, opened, clicked and complained events | Always queued → sent → delivered; nothing after |
Response has spam_score | Response has test_mode: true and no spam_score |
Webhooks in test mode#
Every test send fires email.sent and then email.delivered to your webhook endpoints, in that order, so you can develop a handler end to end before sending real mail. Both payloads carry "test_mode": true in their data:
{
"type": "email.delivered",
"created_at": "2026-09-29T10:00:00.912Z",
"data": {
"email_id": "em_a2r19yq5nah5fx3ziepm3ehu",
"from": "hello@acme.com",
"to": ["user@example.com"],
"subject": "Test email",
"test_mode": true
}
}In production, ignore or log separately any event whose data has test_mode: true.
Test mode over SMTP#
A test key works as an SMTP password exactly like a live one: authentication succeeds and the server accepts the message with 250. Since an SMTP client has no other way to learn that nothing was delivered, the reply says so:
250 Accepted em_a2r19yq5nah5fx3ziepm3ehu in TEST MODE - simulated only, nothing was delivered. Use a live (sk_live_) API key to send real mail.Frameworks and platforms that only take SMTP settings (Supabase, Rails, Django, WordPress) usually hide that line. Switch them to an sk_live_ key as soon as you want real deliveries.
Telling test emails apart#
- Send responses carry
test_mode: true(absent on a live send), and so does every accepted element of a batch response. - Email objects from
GET /emailsandGET /emails/:idhavetestMode: true. - The dashboard shows a Test mode badge in place of the delivery status.
Test sends are left out of billing, but not out of analytics: they appear in the email list and count toward the dashboard's sent and delivered totals and its provider and geography breakdowns. If your test volume would distort those numbers, tag test sends and filter on the tag — the list has no test-mode filter.
await poststack.emails.send({
from: 'hello@acme.com',
to: ['user@example.com'],
subject: 'Test email',
html: '<p>This is a test.</p>',
tags: ['env-test'],
});
const { data } = await poststack.emails.list({ tag: 'env-test' });Going live#
- Production reads an
sk_live_key from its secret store; no test key is baked into a build or container image. - The sending domain is verified — test mode already enforced this, so a send that passed with a test key will not fail on it live.
- New accounts are limited in how many sends to external addresses they can make per day while they warm up. A test key never meets that limit, so plan your first live volume accordingly.
- Your webhook handler ignores, or logs separately, events with
test_mode: true. - Assertions on
test_moderun only in CI and staging.
Troubleshooting#
Everything returns 202 but no email arrives
Check the key prefix. An sk_test_ key never delivers; the response's test_mode: true and the dashboard's Test mode badge confirm it.
422 Domain is not verified with a test key
Test mode applies the same domain checks as live mode. Verify the domain under Domains first.
Cancelling a scheduled test email returns 422
Test sends are marked delivered immediately, even with scheduled_at, so there is nothing left to cancel or reschedule. Test scheduling logic with a live key.