TypeScript SDK
@poststack.dev/sdk is a typed client for the PostStack REST API: seventeen resource namespaces, per-attempt timeouts, automatic retries and idempotent sends. This page lists every method it ships in version 0.15.0, and the request each one makes.
Everything the SDK does, you can also do with plain HTTP — the Emails API and the other reference pages document the same endpoints. What the SDK adds is the part every integration ends up writing by hand: typed request and response objects, a retry loop that backs off on network errors and retryable statuses, a timeout on each attempt, and an Idempotency-Key on sends so a retried request cannot deliver the same email twice. Failures arrive as one error class, PostStackError, carrying the HTTP status, the API's error message and a stable error code.
If you are on Python, Go, Ruby, PHP or another language, the framework guides show the same calls over HTTP or SMTP. For AI agents rather than application code, use the MCP server, which wraps this SDK.
Install#
npm install @poststack.dev/sdkbun add, pnpm add and yarn add work the same way. The package ships ES module and CommonJS builds with bundled type declarations and has no runtime dependencies. It calls the global fetch, AbortSignal.timeout and Web Crypto (crypto.randomUUID, crypto.subtle), so it runs on Bun, Deno, edge runtimes and any maintained Node.js release.
Create a client#
import { PostStack } from '@poststack.dev/sdk';
const poststack = new PostStack(process.env.POSTSTACK_API_KEY!, {
// All three options are optional; these are the defaults.
baseUrl: 'https://api.poststack.dev',
timeoutMs: 30_000,
maxRetries: 3,
});
const { id } = await poststack.emails.send({
from: 'Acme <hello@yourdomain.com>',
to: ['user@example.com'],
subject: 'Hello from PostStack',
html: '<p>Welcome!</p>',
});apiKeystringrequired- An
sk_live_…orsk_test_…key, sent asAuthorization: Beareron every request. A test key records sends but never delivers them — see Test mode. options.baseUrlstringdefaulthttps://api.poststack.dev- API origin. Paths are appended without an
/apiprefix (/emails, not/api/emails). A trailing slash is stripped. options.timeoutMsnumberdefault30000- Timeout for each individual attempt, not for the call as a whole. A timed-out attempt counts as a network error and is retried.
options.maxRetriesnumberdefault3- Extra attempts after the first, so one call makes at most
maxRetries + 1requests.0disables retrying.
Every request also carries User-Agent: PostStack-TypeScript-SDK/0.15.0. Each resource is a property of the client (poststack.emails, poststack.domains, …); the full list is in Method reference.
Retries and idempotency#
The client retries a request when the network fails, the attempt times out, or the API answers 408 or any 5xx. When the response carries Retry-After it waits that long; otherwise it waits a random delay between 0 and 250 ms × 2^attempt (full-jitter exponential backoff).
A 429 is retried only when it carries Retry-After, which the per-minute request limit always sends. The monthly plan limit, the daily sending cap and the new-account sending pause (monthly_limit_exceeded, daily_limit_exceeded, send_limit_exceeded) also answer 429, and are thrown at once whatever the headers say, because waiting a few seconds will not change the answer. So is any Retry-After longer than 60 seconds: the error carries it as retryAfter and you decide whether to wait. Any other error status is thrown at once.
Every POST gets a fresh Idempotency-Key header (a random UUID), generated once per call so all retries of that call share it. The API honours the header on POST /emails, POST /emails/send and POST /emails/batch: a retry after a lost response replays the original send instead of delivering a second copy. Other POST endpoints ignore it, so a retried create elsewhere can still produce a duplicate.
Errors#
Any non-2xx response that is not retried (or is still failing when retries run out) is thrown as a PostStackError. Network failures and timeouts that exhaust the retries are rethrown as the underlying error (a TypeError from fetch, or a TimeoutError DOMException), not wrapped.
import { PostStack, PostStackError } from '@poststack.dev/sdk';
try {
await poststack.emails.send({
from: 'you@unverified-domain.com',
to: ['user@example.com'],
subject: 'Hello',
html: '<p>Hi!</p>',
});
} catch (error) {
if (error instanceof PostStackError) {
error.statusCode; // 422
error.code; // "unprocessable_entity"
error.message; // "Domain 'unverified-domain.com' is not verified"
error.requestId; // value of X-Request-Id — quote it to support
}
throw error;
}statusCodenumber- The HTTP status.
messagestring- The API's
errorfield; the status text if the body was not JSON, orUnknown errorif it was JSON without anerrorfield. codestring | undefined- The stable machine-readable code, e.g.
invalid_request,unauthorized,forbidden,not_found,conflict,unprocessable_entity,rate_limit_exceeded. Branch on this, not onmessage. requestIdstring | undefined- The
X-Request-Idresponse header. retryAfternumber | undefined- Seconds from the
Retry-Afterheader, when the response had one. headersHeaders | undefined- All response headers, e.g.
X-RateLimit-LimitandX-RateLimit-Reset.
Every code and what triggers it is on Errors.
IDs#
Most resources are addressed by a prefixed public ID string (em_…, con_…, seg_…, tpl_…, …). Domains, webhooks, API keys, contact properties, inbound emails, mailbox aliases and webhook deliveries take a numeric id instead, and the method signatures below are typed accordingly.
Pagination#
Methods that return PaginatedResponse<T> accept page (default 1) and per_page (default 20, maximum 100) and resolve to { data: T[], meta: { page, perPage, total, totalPages } }. Three paginated methods keep their endpoint's own envelope: webhooks.getDeliveries() resolves to { deliveries, pagination }, domains.getDmarcReports() to { reports, pagination } (and takes perPage, not per_page), and notificationChannels.getDeliveries() to { deliveries, meta }. Unpaginated methods take no parameters and return the whole collection under a named key ({ domains }, { segments }, { webhooks }, …) exactly as the table shows.
let page = 1;
for (;;) {
const { data, meta } = await poststack.contacts.list({ page, per_page: 100 });
for (const contact of data) handle(contact);
if (page >= meta.totalPages) break;
page++;
}Common tasks#
Send and inspect email
// One email, with an attachment (base64 content, 10 MB per file, 25 MB per email)
const res = await poststack.emails.send({
from: 'Acme <hello@yourdomain.com>',
to: ['user@example.com'],
subject: 'Your invoice',
html: '<p>Attached.</p>',
tags: ['invoice'],
attachments: [{ filename: 'invoice.pdf', content: pdfBase64, content_type: 'application/pdf' }],
});
if (res.test_mode) console.warn('Sent with a test key — nothing was delivered');
// From a published template
await poststack.emails.send({
from: 'Acme <hello@yourdomain.com>',
to: ['user@example.com'],
template_id: 'tpl_abc123',
variables: { first_name: 'Alice' },
});
// Up to 100 per batch; each element succeeds or fails on its own
const { data } = await poststack.emails.batch({ emails });
const failed = data.filter((r): r is { error: string } => 'error' in r);
// Status and event timeline — pass the em_ publicId
const email = await poststack.emails.get('em_abc123');
const events = await poststack.emails.getEvents('em_abc123');Verify a webhook
PostStack.Webhooks.verify is static: it needs no client and no API key, only the raw request body, the X-PostStack-Signature header and the signing secret returned by webhooks.create() or webhooks.rotateSecret(). It resolves to false (never throws) on a missing, malformed or non-matching header. During a secret-rotation grace window the header carries two signatures and either one passes.
import { PostStack } from '@poststack.dev/sdk';
export async function POST(req: Request) {
const body = await req.text(); // the raw body — do not JSON.parse and re-stringify
const ok = await PostStack.Webhooks.verify(
body,
req.headers.get('x-poststack-signature') ?? '',
process.env.POSTSTACK_WEBHOOK_SECRET!,
);
if (!ok) return new Response('invalid signature', { status: 401 });
const event = JSON.parse(body);
// …
return new Response(null, { status: 204 });
}The same function is exported as WebhooksResource.verify for code that only receives webhooks.
Build a workflow
const workflow = await poststack.workflows.create({
name: 'Onboarding',
trigger_type: 'contact.created',
});
// putGraph replaces the whole graph atomically — it is not a merge.
await poststack.workflows.putGraph(workflow.publicId, {
nodes: [
{ public_id: 'n_trigger', type: 'trigger', config: { triggerType: 'contact.created' }, canvas_x: 0, canvas_y: 0 },
{ public_id: 'n_welcome', type: 'send_email', config: { templateId: 'tpl_welcome' }, canvas_x: 0, canvas_y: 120 },
{ public_id: 'n_wait', type: 'wait', config: { duration: 2, unit: 'days' }, canvas_x: 0, canvas_y: 240 },
],
edges: [
{ from_public_id: 'n_trigger', to_public_id: 'n_welcome' },
{ from_public_id: 'n_welcome', to_public_id: 'n_wait' },
],
});
const check = await poststack.workflows.validateGraph(workflow.publicId);
if (check.valid) await poststack.workflows.activate(workflow.publicId);
// Enrol a contact into every active "custom" workflow listening for an event
const { matched } = await poststack.workflows.postEvent({ event: 'order.placed', email: 'user@example.com' });
if (matched === 0) console.warn('No active workflow listens for order.placed');Method reference#
Every public method in 0.15.0, the request it makes (relative to baseUrl) and what the promise resolves to. Where the API wraps a single object ({ domain }, { contact }, …) the SDK unwraps it, so domains.get() resolves to the Domain itself. { success } is { success: boolean }.
poststack.emails
| Method | Request | Resolves to |
|---|---|---|
send(input: SendEmailInput) | POST /emails | { id, replayed?, test_mode? } |
batch(input: BatchSendInput) | POST /emails/batch | { data: BatchSendResult[] } |
list(params?: ListEmailsParams) | GET /emails | PaginatedResponse<Email> |
get(id: string) | GET /emails/{id} | Email |
getEvents(id: string) | GET /emails/{id}/events | EmailEvent[] |
getInsights(id: string) | GET /emails/{id}/insights | EmailInsightWarning[] |
reschedule(id: string, scheduledAt: string) | PATCH /emails/{id} | { success } |
cancel(id: string) | POST /emails/{id}/cancel | { success } |
preview(input: PreviewEmailInput) | POST /emails/preview | EmailPreview |
spamPreview(input: SpamPreviewInput) | POST /emails/spam-preview | SpamCheckResult |
poststack.domains
| Method | Request | Resolves to |
|---|---|---|
create(input: CreateDomainInput) | POST /domains | Domain |
list() | GET /domains | { domains: Domain[] } |
get(id: number) | GET /domains/{id} | Domain |
verify(id: number) | POST /domains/{id}/verify | Domain |
update(id: number, input: UpdateDomainInput) | PATCH /domains/{id} | Domain |
delete(id: number) | DELETE /domains/{id} | { success } |
assignIp(id: number, ipAddressId: number) | POST /domains/{id}/ip | { success } |
unassignIp(id: number) | DELETE /domains/{id}/ip | { success } |
startDkimRotation(id: number) | POST /domains/{id}/dkim/rotate | DkimRotation |
activateDkimRotation(id: number, options?: { force?: boolean }) | POST /domains/{id}/dkim/rotate/activate | DkimRotation |
cancelDkimRotation(id: number) | DELETE /domains/{id}/dkim/rotate | DkimRotation |
getDmarcReports(id: number, params?: { page?, perPage? }) | GET /domains/{id}/dmarc/reports | { reports: DmarcReport[], pagination } |
getDmarcStats(id: number, days?: number) | GET /domains/{id}/dmarc/stats | DmarcStats |
getDmarcSources(id: number, days?: number) | GET /domains/{id}/dmarc/sources | { sources: DmarcSource[] } |
getInboxPlacement(id: number, days?: number) | GET /domains/{id}/placement | InboxPlacement |
poststack.contacts
| Method | Request | Resolves to |
|---|---|---|
create(input: CreateContactInput) | POST /contacts | Contact |
list(params?: ListContactsParams) | GET /contacts | PaginatedResponse<Contact> |
get(id: string) | GET /contacts/{id} | Contact |
getByEmail(email: string) | GET /contacts/by-email/{email} | Contact |
update(id: string, input: UpdateContactInput) | PATCH /contacts/{id} | Contact |
delete(id: string) | DELETE /contacts/{id} | { success } |
unsubscribe(id: string) | POST /contacts/{id}/unsubscribe | Contact |
import(input: ImportContactsInput) | POST /contacts/import | ImportContactsResult |
exportCsv(params?: { segment_id?: string }) | GET /contacts/export | string (the CSV text) |
poststack.contactProperties
| Method | Request | Resolves to |
|---|---|---|
list() | GET /contact-properties | { properties: ContactProperty[] } |
create(input: CreateContactPropertyInput) | POST /contact-properties | ContactProperty |
update(id: number, input: UpdateContactPropertyInput) | PATCH /contact-properties/{id} | ContactProperty |
delete(id: number) | DELETE /contact-properties/{id} | { success } |
optionUsage(id: number) | GET /contact-properties/{id}/option-usage | { usage: Record<string, number> } |
poststack.segments
| Method | Request | Resolves to |
|---|---|---|
create(input: CreateSegmentInput) | POST /segments | Segment |
list() | GET /segments | { segments: Segment[] } |
get(id: string) | GET /segments/{id} | Segment |
update(id: string, input: UpdateSegmentInput) | PATCH /segments/{id} | Segment |
delete(id: string) | DELETE /segments/{id} | { success } |
addContacts(id: string, input: AddContactsInput) | POST /segments/{id}/contacts | { added } |
removeContact(id: string, contactId: string) | DELETE /segments/{id}/contacts/{contactId} | { success } |
previewRules(input: SegmentPreviewInput) | POST /segments/preview | SegmentPreviewResult |
members(id: string, params?: ListParams) | GET /segments/{id}/members | PaginatedResponse<Contact> |
growth(id: string, days?: number) | GET /segments/{id}/growth | SegmentGrowth |
broadcasts(id: string) | GET /segments/{id}/broadcasts | { broadcasts: SegmentBroadcastSummary[] } |
poststack.templates
| Method | Request | Resolves to |
|---|---|---|
create(input: CreateTemplateInput) | POST /templates | Template |
list(params?: ListTemplatesParams) | GET /templates | PaginatedResponse<Template> |
get(id: string) | GET /templates/{id} | Template |
update(id: string, input: UpdateTemplateInput) | PATCH /templates/{id} | Template |
delete(id: string) | DELETE /templates/{id} | { success } |
publish(id: string) | POST /templates/{id}/publish | Template |
unpublish(id: string) | POST /templates/{id}/unpublish | Template |
duplicate(id: string) | POST /templates/{id}/duplicate | Template |
render(id: string, variables?: Record<string, string>) | POST /templates/{id}/render | RenderTemplateResult |
getPresets() | GET /templates/presets | TemplatePreset[] |
usePreset(presetId: string) | POST /templates/presets/{presetId}/use | Template |
poststack.broadcasts
| Method | Request | Resolves to |
|---|---|---|
create(input: CreateBroadcastInput) | POST /broadcasts | Broadcast |
list(params?: ListBroadcastsParams) | GET /broadcasts | PaginatedResponse<Broadcast> |
get(id: string) | GET /broadcasts/{id} | Broadcast |
update(id: string, input: UpdateBroadcastInput) | PATCH /broadcasts/{id} | Broadcast |
send(id: string) | POST /broadcasts/{id}/send | { success } |
cancel(id: string) | POST /broadcasts/{id}/cancel | { success } |
sendTest(id: string, input: TestBroadcastInput) | POST /broadcasts/{id}/test | { success } |
getVariants(id: string) | GET /broadcasts/{id}/variants | BroadcastVariant[] |
getVariantStats(id: string) | GET /broadcasts/{id}/variant-stats | BroadcastVariantStats |
endAbTest(id: string) | POST /broadcasts/{id}/end-ab-test | { success } |
performance(id: string) | GET /broadcasts/{id}/performance | BroadcastPerformance |
listPerformance(params?: ListBroadcastPerformanceParams) | GET /broadcasts/performance | BroadcastPerformanceLeaderboard |
nonOpeners(id: string, options?: { limit?: number }) | GET /broadcasts/{id}/non-openers | BroadcastNonResponders |
nonClickers(id: string, options?: { limit?: number }) | GET /broadcasts/{id}/non-clickers | BroadcastNonResponders |
resend(id: string, input?: { target?: 'non_openers' | 'non_clickers'; subject?: string }) | POST /broadcasts/{id}/resend | { broadcast, segment, recipientCount } |
poststack.webhooks
| Method | Request | Resolves to |
|---|---|---|
create(input: CreateWebhookInput) | POST /webhooks | { webhook, signingSecret } |
list() | GET /webhooks | { webhooks: Webhook[] } |
get(id: number) | GET /webhooks/{id} | Webhook |
update(id: number, input: UpdateWebhookInput) | PATCH /webhooks/{id} | Webhook |
delete(id: number) | DELETE /webhooks/{id} | { success } |
test(id: number) | POST /webhooks/{id}/test | { success } |
getDeliveries(id: number, params?: ListParams) | GET /webhooks/{id}/deliveries | { deliveries: WebhookDelivery[], pagination } |
replay(id: number, deliveryId: number) | POST /webhooks/{id}/deliveries/{deliveryId}/replay | { success } |
batchReplay(id: number, options?: BatchReplayOptions) | POST /webhooks/{id}/deliveries/batch-replay | { replayed, remaining } |
rotateSecret(id: number, options?: { graceHours?: number }) | POST /webhooks/{id}/rotate-secret | { webhook, signingSecret } |
PostStack.Webhooks.verify(payload: string, signatureHeader: string, secret: string) | none (local) | boolean |
poststack.suppressions
| Method | Request | Resolves to |
|---|---|---|
list(params?: ListParams) | GET /suppressions | PaginatedResponse<Suppression> |
add(input: AddSuppressionInput) | POST /suppressions | { success } |
remove(email: string) | DELETE /suppressions/{email} | { success } |
poststack.apiKeys
| Method | Request | Resolves to |
|---|---|---|
create(input: CreateApiKeyInput) | POST /api-keys | ApiKey (with key, once) |
list() | GET /api-keys | { keys: ApiKey[] } |
get(id: number) | GET /api-keys/{id} | ApiKey |
rotate(id: number) | POST /api-keys/{id}/rotate | ApiKey (with key, once) |
revoke(id: number) | DELETE /api-keys/{id} | { success } |
poststack.mailboxes
| Method | Request | Resolves to |
|---|---|---|
create(input: CreateMailboxInput) | POST /mailboxes | Mailbox |
list(params?: ListMailboxesParams) | GET /mailboxes | PaginatedResponse<Mailbox> |
get(id: string) | GET /mailboxes/{id} | Mailbox |
update(id: string, input: UpdateMailboxInput) | PATCH /mailboxes/{id} | Mailbox |
delete(id: string) | DELETE /mailboxes/{id} | { success } |
changePassword(id: string, password: string) | POST /mailboxes/{id}/password | { success } |
createAlias(input: CreateMailboxAliasInput) | POST /mailboxes/aliases | MailboxAlias |
listAliases(params?: ListParams) | GET /mailboxes/aliases | PaginatedResponse<MailboxAlias> |
deleteAlias(id: number) | DELETE /mailboxes/aliases/{id} | { success } |
listFilters(id: string) | GET /mailboxes/{id}/filters | MailboxFilterRule[] |
setFilters(id: string, rules: MailboxFilterRuleInput[]) | PUT /mailboxes/{id}/filters | MailboxFilterRule[] |
getSignature(id: string) | GET /webmail/{id}/signature | MailboxSignature |
setSignature(id: string, signature: Partial<MailboxSignature>) | PUT /webmail/{id}/signature | MailboxSignature |
listShares(id: string) | GET /mailboxes/{id}/shares | { shares: MailboxShare[] } |
grantShare(id: string, input: GrantMailboxShareInput) | POST /mailboxes/{id}/shares | { share } |
revokeShare(id: string, targetId: string) | DELETE /mailboxes/{id}/shares/{targetId} | { success } |
unreadSummary() | GET /mailboxes/unread-summary | MailboxUnreadSummary |
poststack.inboundEmails
| Method | Request | Resolves to |
|---|---|---|
list(params?: ListInboundEmailsParams) | GET /inbound | PaginatedResponse<InboundEmail> |
get(id: number) | GET /inbound/{id} | InboundEmail |
listAttachments(id: number) | GET /inbound/{id}/attachments | InboundAttachment[] |
downloadAttachment(id: number, attachmentId: number) | GET /inbound/{id}/attachments/{attachmentId} | Uint8Array |
draftReply(id: number, input?: DraftReplyInput) | POST /inbound/{id}/draft-reply | InboundReplyDraft |
reply(id: number, input: ReplyInboundInput) | POST /inbound/{id}/reply | { id } |
forward(id: number, input: ForwardInboundInput) | POST /inbound/{id}/forward | { id } |
poststack.subscriptionTopics
| Method | Request | Resolves to |
|---|---|---|
list() | GET /subscription-topics | { topics: SubscriptionTopic[] } |
create(input: CreateSubscriptionTopicInput) | POST /subscription-topics | { topic: SubscriptionTopic } |
get(id: string) | GET /subscription-topics/{id} | SubscriptionTopicDetail |
update(id: string, input: UpdateSubscriptionTopicInput) | PATCH /subscription-topics/{id} | { topic: SubscriptionTopic } |
delete(id: string) | DELETE /subscription-topics/{id} | { success } |
listSubscribers(id: string, params?: ListTopicSubscribersParams) | GET /subscription-topics/{id}/subscribers | PaginatedResponse<TopicSubscriber> |
getContactSubscriptions(contactId: string) | GET /subscription-topics/contacts/{contactId}/subscriptions | { subscriptions: ContactSubscription[] } |
subscribe(contactId: string, topicId: string) | POST /subscription-topics/contacts/{contactId}/subscriptions | { subscription } |
unsubscribe(contactId: string, topicId: string) | DELETE /subscription-topics/contacts/{contactId}/subscriptions/{topicId} | { subscription } |
poststack.workflows
| Method | Request | Resolves to |
|---|---|---|
list() | GET /workflows | Workflow[] |
create(input: CreateWorkflowInput) | POST /workflows | Workflow |
get(id: string) | GET /workflows/{id} | Workflow |
update(id: string, input: UpdateWorkflowInput) | PATCH /workflows/{id} | Workflow |
delete(id: string) | DELETE /workflows/{id} | { success } |
getGraph(id: string) | GET /workflows/{id}/graph | WorkflowGraph |
putGraph(id: string, input: PutWorkflowGraphInput) | PUT /workflows/{id}/graph | WorkflowGraph |
validateGraph(id: string) | POST /workflows/{id}/validate | WorkflowGraphValidation |
activate(id: string) | POST /workflows/{id}/activate | Workflow |
pause(id: string) | POST /workflows/{id}/pause | Workflow |
trigger(id: string, input: TriggerWorkflowInput) | POST /workflows/{id}/trigger | { run: WorkflowRun } |
postEvent(input: WorkflowEventInput) | POST /workflows/events | { event, matched } |
poststack.signupForms
| Method | Request | Resolves to |
|---|---|---|
list(params?: ListParams) | GET /signup-forms | PaginatedResponse<SignupForm> |
create(input: CreateSignupFormInput) | POST /signup-forms | SignupForm |
get(id: string) | GET /signup-forms/{id} | SignupForm |
update(id: string, input: UpdateSignupFormInput) | PATCH /signup-forms/{id} | SignupForm |
delete(id: string) | DELETE /signup-forms/{id} | { success } |
submit(id: string, input: SubmitSignupFormInput) | POST /signup-forms/{id}/submit | { success, message, redirect_url? } |
poststack.emailValidations
| Method | Request | Resolves to |
|---|---|---|
validate(email: string) | POST /email-validations | EmailValidation |
validateBatch(emails: string[]) | POST /email-validations/batch | { results: EmailValidation[] } |
poststack.notificationChannels
Slack, Discord and Telegram destinations for account events, addressed by their nc_… id. Needs a full_access key. Secrets never come back: responses carry a masked configSummary.
| Method | Request | Resolves to |
|---|---|---|
create(input: CreateNotificationChannelInput) | POST /notification-channels | NotificationChannel |
list() | GET /notification-channels | { channels: NotificationChannel[] } |
get(id: string) | GET /notification-channels/{id} | NotificationChannel |
update(id: string, input: UpdateNotificationChannelInput) | PUT /notification-channels/{id} | NotificationChannel |
delete(id: string) | DELETE /notification-channels/{id} | { success } |
test(id: string) | POST /notification-channels/{id}/test | NotificationTestResult |
getDeliveries(id: string, params?: ListParams) | GET /notification-channels/{id}/deliveries | { deliveries, meta } |
replayDelivery(id: string, deliveryId: number) | POST /notification-channels/{id}/deliveries/{deliveryId}/replay | { success } |
Types#
Every request and response type named in the tables is exported from the package root as a type-only export:
import type {
SendEmailInput, BatchSendResult, Email, EmailEvent, EmailStatus,
Domain, DkimRotation, DmarcStats, InboxPlacement,
Contact, ContactProperty, Segment, Template,
Broadcast, BroadcastVariantStats, Webhook, WebhookDelivery,
Mailbox, MailboxFilterRule, InboundEmail, Suppression, ApiKey,
SubscriptionTopic, ContactSubscription, Workflow, WorkflowGraph,
WorkflowRun, SignupForm, EmailValidation, EmailPreview, NotificationChannel,
PaginatedResponse, ListParams,
} from '@poststack.dev/sdk';Python and Go SDKs#
PostStack also publishes a Python SDK (pip install poststack, version 0.9.0, Python 3.10+) and a Go SDK (go get github.com/getpoststack/go-sdk, version 0.8.0, Go 1.22+). Both decode the response shapes described on this page (numeric id plus publicId on contacts and segments, { deliveries, pagination }, { reports, pagination }, a trigger's run), send the same Idempotency-Key, verify webhook signatures, and follow the same retry rules: Retry-After is honoured up to 60 seconds and a quota 429 is raised at once. Both export contacts as CSV text, optionally for one segment.
Both also have DKIM rotation, inbox placement, webhook batch replay, workflow events and every method added in TypeScript 0.13.0: email preview and spam preview, broadcast performance and non-opener / non-clicker lists, subscription topic get / update / subscribers, contact property option usage, mailbox shares and the unread summary, and the notification channels resource (notification_channels in Python, client.NotificationChannels in Go).
from poststack import PostStack, PostStackError
client = PostStack(api_key="sk_live_...") # also: base_url=, timeout=30.0, max_retries=3
try:
result = client.emails.send({
"from": "Acme <hello@yourdomain.com>",
"to": ["user@example.com"],
"subject": "Hello from PostStack",
"html": "<p>Welcome!</p>",
})
print(result["id"])
except PostStackError as err:
print(err.status_code, err.code, err.error, err.request_id)
# AsyncPostStack has the same resources, with awaitable methods.client := poststack.NewClient("sk_live_...",
poststack.WithTimeout(30*time.Second), // also WithBaseURL, WithHTTPClient, WithMaxRetries, WithUserAgent
)
res, err := client.Emails.Send(ctx, &poststack.SendEmailInput{
From: "Acme <hello@yourdomain.com>",
To: []string{"user@example.com"},
Subject: "Hello from PostStack",
Html: "<p>Welcome!</p>",
})
var apiErr *poststack.Error
if errors.As(err, &apiErr) {
log.Printf("%d %s: %s (request %s)", apiErr.StatusCode, apiErr.Code, apiErr.Message, apiErr.RequestID)
if apiErr.IsQuotaExceeded() {
// monthly limit or daily cap: retrying will not help
}
}Method names follow each language's conventions: client.contact_properties.list() in Python, client.ContactProperties.List(ctx) in Go. Python methods return the response as a dict; the pydantic models in poststack.types are there to validate it if you want to. Python's PostStackError has status_code, error, code, request_id, headers and retry_after; Go's *poststack.Error has RetryAfter, Headers, IsRateLimited() and IsQuotaExceeded().