Skip to content

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#

bash
npm install @poststack.dev/sdk

bun 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#

typescript
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_… or sk_test_… key, sent as Authorization: Bearer on every request. A test key records sends but never delivers them — see Test mode.
options.baseUrlstringdefault https://api.poststack.dev
API origin. Paths are appended without an /api prefix (/emails, not /api/emails). A trailing slash is stripped.
options.timeoutMsnumberdefault 30000
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.maxRetriesnumberdefault 3
Extra attempts after the first, so one call makes at most maxRetries + 1 requests. 0 disables 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.

Deduplicating across calls

The automatic header protects one call's retries. To make separate calls collapse into one send (a job that may run twice, say), pass your own idempotency_key in the send body. A matching earlier send comes back with replayed: true and the original id; nothing is sent again. The key is honoured for your team's whole log retention period, and reusing it with a different payload is rejected with 422.

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.

typescript
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 error field; the status text if the body was not JSON, or Unknown error if it was JSON without an error field.
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 on message.
requestIdstring | undefined
The X-Request-Id response header.
retryAfternumber | undefined
Seconds from the Retry-After header, when the response had one.
headersHeaders | undefined
All response headers, e.g. X-RateLimit-Limit and X-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.

Email objects carry two ids

An Email from emails.list() or emails.get() has a numeric id (internal) and a publicId (em_…). Every email method takes the publicId; passing email.id returns 404. The same applies to broadcast.publicId, workflow.publicId, template.publicId, signupForm.publicId, mailbox.publicId, contact.publicId and segment.publicId; the numeric id on each is typed as number so the two cannot be confused.

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.

typescript
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

typescript
// 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.

typescript
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

typescript
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

MethodRequestResolves to
send(input: SendEmailInput)POST /emails{ id, replayed?, test_mode? }
batch(input: BatchSendInput)POST /emails/batch{ data: BatchSendResult[] }
list(params?: ListEmailsParams)GET /emailsPaginatedResponse<Email>
get(id: string)GET /emails/{id}Email
getEvents(id: string)GET /emails/{id}/eventsEmailEvent[]
getInsights(id: string)GET /emails/{id}/insightsEmailInsightWarning[]
reschedule(id: string, scheduledAt: string)PATCH /emails/{id}{ success }
cancel(id: string)POST /emails/{id}/cancel{ success }
preview(input: PreviewEmailInput)POST /emails/previewEmailPreview
spamPreview(input: SpamPreviewInput)POST /emails/spam-previewSpamCheckResult

poststack.domains

MethodRequestResolves to
create(input: CreateDomainInput)POST /domainsDomain
list()GET /domains{ domains: Domain[] }
get(id: number)GET /domains/{id}Domain
verify(id: number)POST /domains/{id}/verifyDomain
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/rotateDkimRotation
activateDkimRotation(id: number, options?: { force?: boolean })POST /domains/{id}/dkim/rotate/activateDkimRotation
cancelDkimRotation(id: number)DELETE /domains/{id}/dkim/rotateDkimRotation
getDmarcReports(id: number, params?: { page?, perPage? })GET /domains/{id}/dmarc/reports{ reports: DmarcReport[], pagination }
getDmarcStats(id: number, days?: number)GET /domains/{id}/dmarc/statsDmarcStats
getDmarcSources(id: number, days?: number)GET /domains/{id}/dmarc/sources{ sources: DmarcSource[] }
getInboxPlacement(id: number, days?: number)GET /domains/{id}/placementInboxPlacement

poststack.contacts

MethodRequestResolves to
create(input: CreateContactInput)POST /contactsContact
list(params?: ListContactsParams)GET /contactsPaginatedResponse<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}/unsubscribeContact
import(input: ImportContactsInput)POST /contacts/importImportContactsResult
exportCsv(params?: { segment_id?: string })GET /contacts/exportstring (the CSV text)

poststack.contactProperties

MethodRequestResolves to
list()GET /contact-properties{ properties: ContactProperty[] }
create(input: CreateContactPropertyInput)POST /contact-propertiesContactProperty
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

MethodRequestResolves to
create(input: CreateSegmentInput)POST /segmentsSegment
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/previewSegmentPreviewResult
members(id: string, params?: ListParams)GET /segments/{id}/membersPaginatedResponse<Contact>
growth(id: string, days?: number)GET /segments/{id}/growthSegmentGrowth
broadcasts(id: string)GET /segments/{id}/broadcasts{ broadcasts: SegmentBroadcastSummary[] }

poststack.templates

MethodRequestResolves to
create(input: CreateTemplateInput)POST /templatesTemplate
list(params?: ListTemplatesParams)GET /templatesPaginatedResponse<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}/publishTemplate
unpublish(id: string)POST /templates/{id}/unpublishTemplate
duplicate(id: string)POST /templates/{id}/duplicateTemplate
render(id: string, variables?: Record<string, string>)POST /templates/{id}/renderRenderTemplateResult
getPresets()GET /templates/presetsTemplatePreset[]
usePreset(presetId: string)POST /templates/presets/{presetId}/useTemplate

poststack.broadcasts

MethodRequestResolves to
create(input: CreateBroadcastInput)POST /broadcastsBroadcast
list(params?: ListBroadcastsParams)GET /broadcastsPaginatedResponse<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}/variantsBroadcastVariant[]
getVariantStats(id: string)GET /broadcasts/{id}/variant-statsBroadcastVariantStats
endAbTest(id: string)POST /broadcasts/{id}/end-ab-test{ success }
performance(id: string)GET /broadcasts/{id}/performanceBroadcastPerformance
listPerformance(params?: ListBroadcastPerformanceParams)GET /broadcasts/performanceBroadcastPerformanceLeaderboard
nonOpeners(id: string, options?: { limit?: number })GET /broadcasts/{id}/non-openersBroadcastNonResponders
nonClickers(id: string, options?: { limit?: number })GET /broadcasts/{id}/non-clickersBroadcastNonResponders
resend(id: string, input?: { target?: 'non_openers' | 'non_clickers'; subject?: string })POST /broadcasts/{id}/resend{ broadcast, segment, recipientCount }

poststack.webhooks

MethodRequestResolves 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

MethodRequestResolves to
list(params?: ListParams)GET /suppressionsPaginatedResponse<Suppression>
add(input: AddSuppressionInput)POST /suppressions{ success }
remove(email: string)DELETE /suppressions/{email}{ success }

poststack.apiKeys

MethodRequestResolves to
create(input: CreateApiKeyInput)POST /api-keysApiKey (with key, once)
list()GET /api-keys{ keys: ApiKey[] }
get(id: number)GET /api-keys/{id}ApiKey
rotate(id: number)POST /api-keys/{id}/rotateApiKey (with key, once)
revoke(id: number)DELETE /api-keys/{id}{ success }

poststack.mailboxes

MethodRequestResolves to
create(input: CreateMailboxInput)POST /mailboxesMailbox
list(params?: ListMailboxesParams)GET /mailboxesPaginatedResponse<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/aliasesMailboxAlias
listAliases(params?: ListParams)GET /mailboxes/aliasesPaginatedResponse<MailboxAlias>
deleteAlias(id: number)DELETE /mailboxes/aliases/{id}{ success }
listFilters(id: string)GET /mailboxes/{id}/filtersMailboxFilterRule[]
setFilters(id: string, rules: MailboxFilterRuleInput[])PUT /mailboxes/{id}/filtersMailboxFilterRule[]
getSignature(id: string)GET /webmail/{id}/signatureMailboxSignature
setSignature(id: string, signature: Partial<MailboxSignature>)PUT /webmail/{id}/signatureMailboxSignature
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-summaryMailboxUnreadSummary

poststack.inboundEmails

MethodRequestResolves to
list(params?: ListInboundEmailsParams)GET /inboundPaginatedResponse<InboundEmail>
get(id: number)GET /inbound/{id}InboundEmail
listAttachments(id: number)GET /inbound/{id}/attachmentsInboundAttachment[]
downloadAttachment(id: number, attachmentId: number)GET /inbound/{id}/attachments/{attachmentId}Uint8Array
draftReply(id: number, input?: DraftReplyInput)POST /inbound/{id}/draft-replyInboundReplyDraft
reply(id: number, input: ReplyInboundInput)POST /inbound/{id}/reply{ id }
forward(id: number, input: ForwardInboundInput)POST /inbound/{id}/forward{ id }

poststack.subscriptionTopics

MethodRequestResolves 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}/subscribersPaginatedResponse<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

MethodRequestResolves to
list()GET /workflowsWorkflow[]
create(input: CreateWorkflowInput)POST /workflowsWorkflow
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}/graphWorkflowGraph
putGraph(id: string, input: PutWorkflowGraphInput)PUT /workflows/{id}/graphWorkflowGraph
validateGraph(id: string)POST /workflows/{id}/validateWorkflowGraphValidation
activate(id: string)POST /workflows/{id}/activateWorkflow
pause(id: string)POST /workflows/{id}/pauseWorkflow
trigger(id: string, input: TriggerWorkflowInput)POST /workflows/{id}/trigger{ run: WorkflowRun }
postEvent(input: WorkflowEventInput)POST /workflows/events{ event, matched }

poststack.signupForms

MethodRequestResolves to
list(params?: ListParams)GET /signup-formsPaginatedResponse<SignupForm>
create(input: CreateSignupFormInput)POST /signup-formsSignupForm
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

MethodRequestResolves to
validate(email: string)POST /email-validationsEmailValidation
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.

MethodRequestResolves to
create(input: CreateNotificationChannelInput)POST /notification-channelsNotificationChannel
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}/testNotificationTestResult
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:

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

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().

Next steps

Was this page helpful?

Related

Framework guides