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 livemailbox = pb.mailboxes.create(
{
"domainId": domain["id"],
"localPart": "support",
"displayName": "Acme Support",
}
)
# [email protected] is now livevar mailbox = await pb.Mailboxes.CreateAsync(
new MailboxesCreateBody
{
DomainId = domain.Id,
LocalPart = "support",
DisplayName = "Acme Support",
}
);
// [email protected] is now livevar mailbox = pb.mailboxes.create(
MailboxesCreateBody.builder()
.domainId(domain.id())
.localPart("support")
.displayName("Acme Support")
.build());
// [email protected] is now livemailbox, err := pb.Mailboxes.Create(ctx, &postbox.MailboxesCreateBody{
DomainId: domain.ID,
LocalPart: "support",
DisplayName: "Acme Support",
})
// [email protected] is now livecurl -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.