Postbox docs

Quickstart

From nothing to a domain, a live mailbox, a sent message, and a received one.

This walks the whole round-trip: verify a domain, provision a mailbox, send from it, receive into it, and read it from a browser without leaking your key.

You need a project API key (pb_live_…) from the console.

Every step shows the raw HTTP call and the same call through the TypeScript client. See SDKs for the client in full.

Authenticate

Every request is a bearer credential. The key is scoped to one project, so there is nothing else to pass.

import { Postbox } from "@postbox/sdk";

const pb = new Postbox({
  apiKey: process.env.PB_KEY!,
  projectId: process.env.PROJECT_ID!,
});

const mailboxes = await pb.mailboxes.list();
import os
from postbox import Postbox

pb = Postbox(api_key=os.environ["PB_KEY"], project_id=os.environ["PROJECT_ID"])

mailboxes = pb.mailboxes.list()
using Postbox;

var pb = new PostboxClient(
    new PostboxOptions
    {
        ApiKey = Environment.GetEnvironmentVariable("PB_KEY")!,
        ProjectId = Environment.GetEnvironmentVariable("PROJECT_ID")!,
    }
);

var mailboxes = await pb.Mailboxes.ListAsync();
import cloud.postboxapp.*;
import cloud.postboxapp.gen.*;
import java.util.*;

Postbox pb = new Postbox(PostboxOptions.builder().apiKey(System.getenv("PB_KEY")).projectId(System.getenv("PROJECT_ID")).build());

var mailboxes = pb.mailboxes.list();
import postbox "github.com/postboxapp/postbox-go"

pb, err := postbox.New(postbox.Options{APIKey: os.Getenv("PB_KEY"), ProjectID: os.Getenv("PROJECT_ID")})
ctx := context.Background()

mailboxes, err := pb.Mailboxes.List(ctx)
export PB_KEY="pb_live_..."

curl https://api.postboxapp.cloud/v1/mailboxes \
  -H "Authorization: Bearer $PB_KEY"

Register a domain

A mailbox lives on a domain you own. Register it once and Postbox returns the DNS records to publish. The records are Postbox-branded CNAMEs, so your customers never see a third-party provider in their DNS.

const domain = await pb.domains.create({
  domain: "mail.acme.com",
  receivingEnabled: true,
});
domain = pb.domains.create(
    {
        "domain": "mail.acme.com",
        "receivingEnabled": True,
    }
)
var domain = await pb.Domains.CreateAsync(
    new DomainsCreateBody { Domain = "mail.acme.com", ReceivingEnabled = true }
);
var domain = pb.domains.create(
    DomainsCreateBody.builder()
        .domain("mail.acme.com")
        .receivingEnabled(true)
        .build());
domain, err := pb.Domains.Create(ctx, &postbox.DomainsCreateBody{
    Domain:           "mail.acme.com",
    ReceivingEnabled: true,
})
curl -X POST https://api.postboxapp.cloud/v1/domains \
  -H "Authorization: Bearer $PB_KEY" \
  -H "X-Project-Id: $PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "mail.acme.com", "receivingEnabled": true }'

Fetch the records to publish, then ask Postbox to check them:

const records = await pb.domains.listRecords(domain.id);

const verified = await pb.domains.verify(domain.id);
records = pb.domains.list_records(domain["id"])

verified = pb.domains.verify(domain["id"])
var records = await pb.Domains.ListRecordsAsync(domain.Id);

var verified = await pb.Domains.VerifyAsync(domain.Id);
var records = pb.domains.listRecords(domain.id());

var verified = pb.domains.verify(domain.id());
records, err := pb.Domains.ListRecords(ctx, domain.ID)

verified, err := pb.Domains.Verify(ctx, domain.ID)
curl https://api.postboxapp.cloud/v1/domains/$DOMAIN_ID/records \
  -H "Authorization: Bearer $PB_KEY" -H "X-Project-Id: $PROJECT_ID"

curl -X POST https://api.postboxapp.cloud/v1/domains/$DOMAIN_ID/verify \
  -H "Authorization: Bearer $PB_KEY" -H "X-Project-Id: $PROJECT_ID"

A domain activates once ownership, DKIM, and MAIL FROM all pass. DMARC is reported but never blocks activation. Full detail in Domains.

Provision a mailbox

One call returns a real, sending and receiving address. It has folders and storage from the first message.

Note that you pass the domain's id, not its name, and the local part on its own rather than a full address.

const mailbox = await pb.mailboxes.create({
  domainId: domain.id,
  localPart: "support",
  displayName: "Acme Support",
});
// [email protected] is now live
mailbox = pb.mailboxes.create(
    {
        "domainId": domain["id"],
        "localPart": "support",
        "displayName": "Acme Support",
    }
)
# [email protected] is now live
var mailbox = await pb.Mailboxes.CreateAsync(
    new MailboxesCreateBody
    {
        DomainId = domain.Id,
        LocalPart = "support",
        DisplayName = "Acme Support",
    }
);
// [email protected] is now live
var mailbox = pb.mailboxes.create(
    MailboxesCreateBody.builder()
        .domainId(domain.id())
        .localPart("support")
        .displayName("Acme Support")
        .build());
// [email protected] is now live
mailbox, err := pb.Mailboxes.Create(ctx, &postbox.MailboxesCreateBody{
    DomainId:    domain.ID,
    LocalPart:   "support",
    DisplayName: "Acme Support",
})
// [email protected] is now live
curl -X POST https://api.postboxapp.cloud/v1/mailboxes \
  -H "Authorization: Bearer $PB_KEY" \
  -H "X-Project-Id: $PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{
        "domainId": "'"$DOMAIN_ID"'",
        "localPart": "support",
        "displayName": "Acme Support"
      }'

Send

Outbound is DKIM-signed and checked against rate limits and your plan allowance before it is accepted.

await pb.messages.send(
  {
    from: "[email protected]",
    to: [{ address: "[email protected]", name: "Jess" }],
    subject: "Welcome to Acme",
    bodyHtml: "<p>Glad you are here.</p>",
  },
  { idempotencyKey: "welcome-user-1042" },
);
pb.messages.send(
    {
        "from": "[email protected]",
        "to": [{"address": "[email protected]", "name": "Jess"}],
        "subject": "Welcome to Acme",
        "bodyHtml": "<p>Glad you are here.</p>",
    },
    idempotency_key="welcome-user-1042",
)
await pb.Messages.SendAsync(
    new MessagesSendBody
    {
        From = "[email protected]",
        To = new List<Dictionary<string, object?>>
        {
            new() { ["address"] = "[email protected]", ["name"] = "Jess" },
        },
        Subject = "Welcome to Acme",
        BodyHtml = "<p>Glad you are here.</p>",
    },
    idempotencyKey: "welcome-user-1042"
);
pb.messages.send(
    MessagesSendBody.builder()
        .from("[email protected]")
        .to(List.of(Map.of("address", "[email protected]", "name", "Jess")))
        .subject("Welcome to Acme")
        .bodyHtml("<p>Glad you are here.</p>")
        .build());
res, err := pb.Messages.Send(ctx, &postbox.MessagesSendBody{
    From:     "[email protected]",
    To:       []map[string]any{{"address": "[email protected]", "name": "Jess"}},
    Subject:  "Welcome to Acme",
    BodyHtml: "<p>Glad you are here.</p>",
})
curl -X POST https://api.postboxapp.cloud/v1/messages/send \
  -H "Authorization: Bearer $PB_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: welcome-user-1042" \
  -d '{
        "from": "[email protected]",
        "to": [{ "address": "[email protected]", "name": "Jess" }],
        "subject": "Welcome to Acme",
        "bodyHtml": "<p>Glad you are here.</p>"
      }'

A 200 here means Postbox accepted and queued the message. It does not mean the mail was delivered. Watch for message.sent, then message.delivered. See Sending.

Receive

Inbound mail is spam-scored, parsed, threaded, and stored, then a signed webhook fires. Subscribe once and handle every event type you care about.

await pb.webhooks.create({
  url: "https://acme.com/hooks/postbox",
  events: ["message.received", "message.bounced"],
});
pb.webhooks.create(
    {
        "url": "https://acme.com/hooks/postbox",
        "events": ["message.received", "message.bounced"],
    }
)
await pb.Webhooks.CreateAsync(
    new WebhooksCreateBody
    {
        Url = "https://acme.com/hooks/postbox",
        Events = new List<string> { "message.received", "message.bounced" },
    }
);
pb.webhooks.create(
    WebhooksCreateBody.builder()
        .url("https://acme.com/hooks/postbox")
        .events(List.of("message.received", "message.bounced"))
        .build());
_, err := pb.Webhooks.Create(ctx, &postbox.WebhooksCreateBody{
    Url:    "https://acme.com/hooks/postbox",
    Events: []string{"message.received", "message.bounced"},
})
curl -X POST https://api.postboxapp.cloud/v1/webhooks \
  -H "Authorization: Bearer $PB_KEY" \
  -H "X-Project-Id: $PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{
        "url": "https://acme.com/hooks/postbox",
        "events": ["message.received", "message.bounced"]
      }'

The response carries a signing secret (whsec_…). Store it. You must verify every delivery against it.

Read from a browser

Never ship a pb_live_ key to a client. Mint a short-lived, mailbox-scoped token server-side and hand that to the browser instead. It reaches exactly one inbox and nothing else.

// server
const { token } = await pb.tokens.create({
  mailboxId: mailbox.id,
  permissions: ["read"],
  expiresIn: 3600,
});

// browser
const client = new Postbox({ apiKey: token });
const messages = await client.mailboxes.listMessages(mailboxId);
# server
created = pb.tokens.create(
    {
        "mailboxId": mailbox["id"],
        "permissions": ["read"],
        "expiresIn": 3600,
    }
)
token = created["token"]

# browser
client = Postbox(api_key=token)
messages = client.mailboxes.list_messages(mailbox_id)
// server
var created = await pb.Tokens.CreateAsync(
    new TokensCreateBody
    {
        MailboxId = mailbox.Id,
        Permissions = new List<string> { "read" },
        ExpiresIn = 3600,
    }
);
var token = created.Token;

// browser
var client = new PostboxClient(new PostboxOptions { ApiKey = token });
var messages = await client.Mailboxes.ListMessagesAsync(mailboxId);
// server
var created = pb.tokens.create(
    TokensCreateBody.builder()
        .mailboxId(mailbox.id())
        .permissions(List.of("read"))
        .expiresIn(3600)
        .build());
var token = created.token();

// browser
Postbox client = new Postbox(token);
var messages = client.mailboxes.listMessages(mailbox.id());
// server
created, err := pb.Tokens.Create(ctx, &postbox.TokensCreateBody{
    MailboxId:   mailbox.ID,
    Permissions: []string{"read"},
    ExpiresIn:   3600,
})
token := created.Token

// browser
client, err := postbox.NewWithKey(token)
messages, err := client.Mailboxes.ListMessages(ctx, mailbox.ID)
curl -X POST https://api.postboxapp.cloud/v1/tokens \
  -H "Authorization: Bearer $PB_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "mailboxId": "'"$MAILBOX_ID"'",
        "permissions": ["read"],
        "expiresIn": 3600
      }'

Next

  • Authentication for the credential model and project scoping.
  • Events to stream inbound mail live instead of polling.
  • Errors before you write retry logic.