Send your first email
From a new account to a delivered message you can see in the dashboard: verify a domain, create an API key, send with the SDK, cURL, Python, Go or SMTP, and check the result.
Before you start#
You need three things, and the send is refused until all three are in place:
- A confirmed account. Sign up at poststack.dev/register and click the link in the confirmation email. Until you do, every send answers
403withVerify your email address before sending email. - A verified sending domain. The address in
frommust be on a domain your team has added and verified. There is no shared sandbox sender — this is true for test-mode keys too. - An API key. Every request authenticates with
Authorization: Bearer sk_live_…(orsk_test_…); the SMTP relay takes the same key as its password.
The whole path usually takes 10–20 minutes, most of it waiting for DNS.
Set up#
Add your domain
In the dashboard open Configuration → Domains and click Add domain, or call
POST /domainswith{ "name": "yourdomain.com" }. Use a domain (or subdomain, such asmail.yourdomain.com) whose DNS you control. The Free plan allows one domain.Publish the DNS records
The domain page lists the records to create at your DNS provider. Only the SPF and DKIM records gate verification; the others (DMARC, return path, MX for inbound) are recommended and reported separately. Enter each record exactly as shown — the most common mistake is typing the full name (
_dmarc.yourdomain.com) into a provider that appends the domain itself.Verify
Click Verify DNS records on the domain page (or call
POST /domains/:id/verify). Pending domains are also re-checked automatically, so you can walk away. DNS usually propagates in 5–15 minutes but can take a few hours. The domain's status must readverifiedbefore you can send from it.Full record-by-record instructions are on the Domains page.
Create an API key
Open Configuration → API keys and click Create API key:
Permission- Sending only is enough for this guide: it can send and read back the emails it sent. Full access can manage every resource.
Mode- Live sends real mail (
sk_live_…). Test runs the whole pipeline and records the email as delivered, but never hands it to a mail server (sk_test_…) — see Test mode. Restrict to domain- Optional. A restricted key can only send from that domain.
Copy the key when it is shown. PostStack stores only a hash, so it can't show it to you again. Put it in an environment variable rather than in code:
bashexport POSTSTACK_API_KEY="sk_live_…"
Send the email#
Send to an address you own first. Replace yourdomain.com in from with your verified domain. to is always an array, and you need subject plus at least one of html or text.
// bun add @poststack.dev/sdk (or npm install / pnpm add)
import { PostStack } from '@poststack.dev/sdk';
const poststack = new PostStack(process.env.POSTSTACK_API_KEY!);
const result = await poststack.emails.send({
from: 'Acme <hello@yourdomain.com>',
to: ['you@yourdomain.com'],
subject: 'Hello from PostStack',
html: '<p>It works.</p>',
text: 'It works.',
});
console.log(result.id); // "em_k3v9x2m8q1w7r4t6y0p5n2bz"curl -X POST https://api.poststack.dev/emails \
-H "Authorization: Bearer $POSTSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@yourdomain.com>",
"to": ["you@yourdomain.com"],
"subject": "Hello from PostStack",
"html": "<p>It works.</p>",
"text": "It works."
}'# pip install poststack
import os
from poststack import PostStack
client = PostStack(api_key=os.environ["POSTSTACK_API_KEY"])
result = client.emails.send({
"from": "Acme <hello@yourdomain.com>",
"to": ["you@yourdomain.com"],
"subject": "Hello from PostStack",
"html": "<p>It works.</p>",
"text": "It works.",
})
print(result["id"]) # "em_k3v9x2m8q1w7r4t6y0p5n2bz"// go get github.com/getpoststack/go-sdk
package main
import (
"context"
"fmt"
"log"
"os"
poststack "github.com/getpoststack/go-sdk"
)
func main() {
client := poststack.NewClient(os.Getenv("POSTSTACK_API_KEY"))
res, err := client.Emails.Send(context.Background(), &poststack.SendEmailInput{
From: "Acme <hello@yourdomain.com>",
To: []string{"you@yourdomain.com"},
Subject: "Hello from PostStack",
Html: "<p>It works.</p>",
Text: "It works.",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(res.ID) // "em_k3v9x2m8q1w7r4t6y0p5n2bz"
}# Any SMTP client works. Username: poststack · Password: your API key
cat > message.eml <<'EOF'
From: Acme <hello@yourdomain.com>
To: you@yourdomain.com
Subject: Hello from PostStack
It works.
EOF
curl --url 'smtps://smtp.poststack.dev:465' --ssl-reqd \
--user "poststack:$POSTSTACK_API_KEY" \
--mail-from hello@yourdomain.com \
--mail-rcpt you@yourdomain.com \
--upload-file message.emlRead the response#
POST /emails answers 202 Accepted as soon as the message is validated, stored and queued. Delivery happens afterwards, so a 202 means “accepted”, not “in the inbox”.
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"id": "em_k3v9x2m8q1w7r4t6y0p5n2bz",
"spam_score": 0.4
}idstring- The email's ID, prefixed
em_. Use it to look the email up, and to match webhook events (data.email_id). spam_scorenumber- The content filter's score for this message. Omitted when the scan was skipped.
spam_warnings(the rules that fired) appears alongside it when there are any. test_modetrue- Present only when you sent with an
sk_test_key. Nothing was delivered. replayedtrue- Present only when the request carried an idempotency key that had already been used: nothing was sent this time and
idis the original email's. See Idempotency.
A test-mode send returns no spam score:
{
"id": "em_k3v9x2m8q1w7r4t6y0p5n2bz",
"test_mode": true
}Over SMTP there is no JSON: the relay answers 250 once the message is accepted. With a test key the 250 line says Accepted em_… in TEST MODE - simulated only, nothing was delivered.
See it arrive#
In the dashboard
Open Sending → Emails. The newest email is at the top; click it to see its status and the event timeline — queued, sent, then delivered once the recipient's server accepts it (or deferred, bounced, failed if it doesn't).
From the API
const email = await poststack.emails.get('em_k3v9x2m8q1w7r4t6y0p5n2bz');
console.log(email.status); // "delivered"curl https://api.poststack.dev/emails/em_k3v9x2m8q1w7r4t6y0p5n2bz \
-H "Authorization: Bearer $POSTSTACK_API_KEY"res = client.emails.get("em_k3v9x2m8q1w7r4t6y0p5n2bz")
print(res["email"]["status"]) # "delivered"email, err := client.Emails.Get(ctx, "em_k3v9x2m8q1w7r4t6y0p5n2bz")The response wraps the email in email and includes its events (abridged here; the full field list is on the Emails page). The TypeScript SDK unwraps email for you.
{
"email": {
"id": 48213,
"publicId": "em_k3v9x2m8q1w7r4t6y0p5n2bz",
"fromAddress": "hello@yourdomain.com",
"fromName": "Acme",
"toAddresses": ["you@yourdomain.com"],
"subject": "Hello from PostStack",
"status": "delivered",
"testMode": false,
"sentAt": "2026-09-29T10:14:03.512Z",
"createdAt": "2026-09-29T10:14:02.981Z",
"events": [
{ "eventType": "queued", "createdAt": "2026-09-29T10:14:02.990Z" },
{ "eventType": "sent", "createdAt": "2026-09-29T10:14:03.512Z" },
{ "eventType": "delivered", "createdAt": "2026-09-29T10:14:04.108Z" }
]
}
}By webhook
To be told instead of polling, create a webhook for email.delivered (and email.bounced) — see Webhooks. Each event is a signed POST to your URL:
POST /webhooks/poststack HTTP/1.1
Content-Type: application/json
User-Agent: PostStack-Webhook/1.0
X-PostStack-Signature: …
{
"type": "email.delivered",
"created_at": "2026-09-29T10:14:04.120Z",
"data": {
"email_id": "em_k3v9x2m8q1w7r4t6y0p5n2bz",
"from": "hello@yourdomain.com",
"to": ["you@yourdomain.com"],
"subject": "Hello from PostStack"
}
}Test-mode sends fire email.sent and email.delivered too, with "test_mode": true in data, so you can build the handler before sending real mail.
If it doesn't work#
Errors come back as { "error": "…" } with an HTTP status. The ones you can hit on a first send:
| Status | Error | What to do |
|---|---|---|
401 | Invalid API key | The key is mistyped, truncated or revoked. |
401 | Unauthorized | No usable Authorization header — check it reads "Bearer sk_…". |
403 | Verify your email address before sending email. | Click the link in the confirmation email PostStack sent when you signed up. |
422 | Domain 'yourdomain.com' not found for this team | The from domain isn’t on your team. Add it, or fix the typo in from. |
422 | Domain 'yourdomain.com' is not verified | The SPF or DKIM record isn’t visible yet. Check the domain page. |
403 | API key is restricted to a different domain | The key is locked to another domain. Use a key for this domain or an unrestricted one. |
400 | subject: Subject is required when not using a template | Validation failed; the message names the field. |
The API said 202 but nothing is in my inbox
Check the email's status in Emails. If it says delivered, the recipient's server accepted it — look in spam, and make sure your DMARC record exists. If the response had "test_mode": true, you used an sk_test_ key and nothing was sent by design. If the status is deferred, the receiving server asked us to retry later and PostStack is retrying.
The domain has been pending for over an hour
Look up the record yourself from outside your network, for example dig TXT yourdomain.com for SPF. If the answer doesn't match the dashboard exactly, the record is wrong rather than slow. The Domains page covers each record.
Every other error is listed on the Errors page.
Next steps
Emails API
Attachments, scheduling, templates, tags, batch sends and every field of the send request.
Webhooks
Get delivery, bounce, open and click events pushed to your app, and verify their signatures.
Idempotency
Retry a send safely without delivering the same email twice.
Rate limits and quotas
How many requests and emails you can send, and what a 429 means.