Skip to content

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 403 with Verify your email address before sending email.
  • A verified sending domain. The address in from must 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_… (or sk_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#

  1. Add your domain

    In the dashboard open Configuration → Domains and click Add domain, or call POST /domains with { "name": "yourdomain.com" }. Use a domain (or subdomain, such as mail.yourdomain.com) whose DNS you control. The Free plan allows one domain.

  2. 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.

  3. 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 read verified before you can send from it.

    Full record-by-record instructions are on the Domains page.

  4. 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:

    bash
    export 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"

New accounts start with a small external allowance

A send that has a recipient outside the domains you've added counts against a cap: 10 in your account's first hour, then 50 per 24 hours on the Free plan, and on paid plans during the first 72 hours. Sending to addresses on a domain you've added doesn't count. The details are under Sending quotas.

Read 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”.

json
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 id is the original email's. See Idempotency.

A test-mode send returns no spam score:

json
{
  "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"

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.

json
{
  "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:

json
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:

StatusErrorWhat to do
401Invalid API keyThe key is mistyped, truncated or revoked.
401UnauthorizedNo usable Authorization header — check it reads "Bearer sk_…".
403Verify your email address before sending email.Click the link in the confirmation email PostStack sent when you signed up.
422Domain 'yourdomain.com' not found for this teamThe from domain isn’t on your team. Add it, or fix the typo in from.
422Domain 'yourdomain.com' is not verifiedThe SPF or DKIM record isn’t visible yet. Check the domain page.
403API key is restricted to a different domainThe key is locked to another domain. Use a key for this domain or an unrestricted one.
400subject: Subject is required when not using a templateValidation 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

Was this page helpful?

Related