postmark
semver
>=2.0.0postconditions12functions12last verified2026-06-25coverage score100%Postconditions: what we check
- sendEmail · api-errorerrorWhenAny HTTP or network failure: invalid API token (401 InvalidAPIKeyError), validation error (422 ApiInputError — invalid sender, missing fields), inactive recipients (422/406 InactiveRecipientsError), rate limit exceeded (429 RateLimitExceededError), server error (500 InternalServerError), service unavailable (503 ServiceUnavailablerError), or network/DNS failure.Throws
InvalidAPIKeyError (401 — bad server token), ApiInputError (422 — validation failures), InactiveRecipientsError (422/406 — bounced recipients, extends ApiInputError), InvalidEmailRequestError (422/300 — malformed email request, extends ApiInputError), RateLimitExceededError (429 — too many requests), InternalServerError (500 — Postmark server error), ServiceUnavailablerError (503 — Postmark unavailable), UnknownError (other HTTP status codes). All extend PostmarkError with code and statusCode properties.Required handlingCaller MUST wrap client.sendEmail() in try-catch. Email delivery failures must be detected — users expect confirmation emails, receipts, and notifications to arrive. Unhandled rejection crashes the process. Minimum handling: try { await client.sendEmail({ From, To, Subject, TextBody }); } catch (error) { if (error instanceof Errors.InactiveRecipientsError) { // Handle bounced recipient } console.error('Email send failed:', error.message); throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - sendEmailBatch · api-errorerrorWhenAny HTTP or network failure: authentication, validation, rate limit, server error, or network failure. Partial batch failures may occur where some emails succeed and others fail within the response.Throws
Same error hierarchy as sendEmail. InvalidAPIKeyError (401), ApiInputError (422), RateLimitExceededError (429), InternalServerError (500), ServiceUnavailablerError (503).Required handlingCaller MUST wrap client.sendEmailBatch() in try-catch. Batch email failures affect multiple recipients simultaneously. try { const results = await client.sendEmailBatch(messages); } catch (error) { console.error('Batch send failed:', error.message); throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - sendEmailWithTemplate · api-errorerrorWhenAny HTTP or network failure: authentication, template not found, validation error (missing template model variables), rate limit, server error, or network failure.Throws
Same error hierarchy as sendEmail. Additionally, ApiInputError (422) is thrown when TemplateId/TemplateAlias is invalid or required TemplateModel variables are missing.Required handlingCaller MUST wrap client.sendEmailWithTemplate() in try-catch. try { await client.sendEmailWithTemplate({ TemplateAlias: 'welcome', TemplateModel: { name: user.name }, From: 'hello@example.com', To: user.email, }); } catch (error) { console.error('Template email failed:', error.message); throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - sendEmailBatchWithTemplates · api-errorerrorWhenAny HTTP or network failure: authentication, template not found, validation error, rate limit, server error, or network failure.Throws
Same error hierarchy as sendEmail and sendEmailWithTemplate.Required handlingCaller MUST wrap client.sendEmailBatchWithTemplates() in try-catch. try { const results = await client.sendEmailBatchWithTemplates(messages); } catch (error) { console.error('Batch template send failed:', error.message); throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - activateBounce · activate-bounce-no-try-catcherrorWhenactivateBounce() called in async context without surrounding try/catch. Throws PostmarkError subclasses on any HTTP or network failure including invalid bounce ID (ApiInputError), revoked token (InvalidAPIKeyError), or bounce not eligible for reactivation (CanActivate: false).Throws
PostmarkError (InvalidAPIKeyError 401, ApiInputError 422, UnknownError)Required handlingCaller MUST wrap activateBounce() in try-catch: try { const result = await client.activateBounce(bounceId); // result.Bounce.CanActivate tells you if it worked } catch (error) { if (error instanceof Errors.InvalidAPIKeyError) { // Auth token invalid } console.error('Bounce activation failed:', error.message); }costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - createSuppressions · create-suppressions-no-try-catcherrorWhencreateSuppressions() called in async context without surrounding try/catch, OR called without checking per-item Status in the response. The API returns 200 OK with Status: "Failed" for individual addresses on validation or authorization failure — not just on HTTP 4xx/5xx errors.Throws
PostmarkError (InvalidAPIKeyError 401, InactiveRecipientsError 406, RateLimitExceededError 429)Required handlingCaller MUST wrap createSuppressions() in try-catch AND check item statuses: try { const result = await client.createSuppressions('outbound', { Suppressions: [{ EmailAddress: email }] }); const failed = result.Suppressions.filter(s => s.Status === 'Failed'); if (failed.length > 0) { console.error('Failed to suppress:', failed.map(s => s.EmailAddress)); } } catch (error) { console.error('Suppression API failed:', error.message); }costmediumin prodsilent failureusers seelost datavisibilitysilent - deleteSuppressions · delete-suppressions-no-try-catcherrorWhendeleteSuppressions() called in async context without surrounding try/catch, OR called without checking per-item Status in the response. SpamComplaint suppressions always return Status: "Failed" with HTTP 200 OK — this is not an exception, it is a silent API-level constraint that callers must check.Throws
PostmarkError (InvalidAPIKeyError 401, RateLimitExceededError 429, ServerError 5xx)Required handlingCaller MUST wrap deleteSuppressions() in try-catch AND check item statuses: try { const result = await client.deleteSuppressions('outbound', { Suppressions: [{ EmailAddress: email }] }); const failed = result.Suppressions.filter(s => s.Status === 'Failed'); if (failed.length > 0) { // SpamComplaint suppressions cannot be deleted console.warn('Could not remove suppressions:', failed); } } catch (error) { console.error('Delete suppressions failed:', error.message); }costmediumin prodsilent failureusers seelost datavisibilitysilent - createTemplate · create-template-no-try-catcherrorWhencreateTemplate() called in async context without surrounding try/catch. Throws ApiInputError (422) for invalid alias format, missing required fields, or 100-template server limit exceeded. Throws InvalidAPIKeyError (401) for bad server token. Throws RateLimitExceededError (429) under high load.Throws
PostmarkError (ApiInputError 422, InvalidAPIKeyError 401, RateLimitExceededError 429)Required handlingCaller MUST wrap createTemplate() in try-catch: try { const template = await client.createTemplate({ Name: 'Welcome Email', Subject: 'Welcome to {{product_name}}', HtmlBody: '<p>Hello, {{name}}!</p>', TextBody: 'Hello, {{name}}!', Alias: 'welcome-v1', }); console.log('Template ID:', template.TemplateId); } catch (error) { console.error('Template creation failed:', error.message); // Check if 100-template limit exceeded }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - editTemplate · edit-template-no-try-catcherrorWheneditTemplate() called in async context without surrounding try/catch. Throws ApiInputError (422) when template ID or alias is not found on the server, or when the alias format is invalid. Throws InvalidAPIKeyError (401) for auth failures. Throws RateLimitExceededError (429) and InternalServerError (500) under API failure conditions.Throws
PostmarkError (ApiInputError 422, InvalidAPIKeyError 401, RateLimitExceededError 429)Required handlingCaller MUST wrap editTemplate() in try-catch: try { const updated = await client.editTemplate('welcome-v1', { Subject: 'Welcome to {{company_name}}!', HtmlBody: '<p>Hi {{name}}, welcome aboard!</p>', }); console.log('Template updated:', updated.TemplateId); } catch (error) { if (error instanceof Errors.ApiInputError) { console.error('Template not found or validation failed:', error.message); } else { console.error('Template update failed:', error.message); } throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - deleteTemplate · delete-template-no-try-catcherrorWhendeleteTemplate() called in async context without surrounding try/catch. Two distinct destructive-failure paths: (1) template not found — server returns HTTP 404 mapped to a generic PostmarkError (NOT ApiInputError — there is no 404 mapping in errors/ErrorHandler.js, so callers that only catch ApiInputError still crash on missing templates); (2) layout template with dependent standard templates — server returns ErrorCode 1130 on HTTP 422 mapped to ApiInputError ("The layout template cannot be deleted because it has dependent templates using it.").Throws
PostmarkError (404 not-found generic PostmarkError, 422 ApiInputError ErrorCode 1130 layout-cascade, 401 InvalidAPIKeyError, 429 RateLimitExceededError, 500 InternalServerError)Required handlingCaller MUST wrap deleteTemplate() in try-catch AND handle both the generic PostmarkError (for 404 not-found) and ApiInputError (for the ErrorCode 1130 layout-cascade case) explicitly. Tear-down scripts must order layout deletion AFTER all dependent standard templates, OR catch the 1130 ApiInputError and re-queue the layout for a second pass: try { const result = await client.deleteTemplate('welcome-v1'); console.log('Deleted:', result.Message); } catch (error) { if (error instanceof Errors.ApiInputError) { // ErrorCode 1130: layout still has dependent standard templates console.error('Layout cascade blocked:', error.message); // Re-queue or delete dependent templates first } else if (error instanceof Errors.PostmarkError) { // 404 template-not-found arrives here (no specialized class) console.warn('Template already absent:', error.message); } else { throw error; } }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - createWebhook · create-webhook-no-try-catcherrorWhencreateWebhook() called in async context without surrounding try/catch. Throws ApiInputError (422 ErrorCode 606) when the webhook URL is invalid, unreachable, or contains a private/internal IP range (Postmark blocks localhost, 127.0.0.1, 10.x.x.x, 192.168.x.x). Throws InvalidAPIKeyError (401) on auth failure. Throws RateLimitExceededError (429) under load.Throws
PostmarkError (ApiInputError 422 ErrorCode 606 invalid-url, InvalidAPIKeyError 401, RateLimitExceededError 429, InternalServerError 500)Required handlingCaller MUST wrap createWebhook() in try-catch. Webhook provisioning code typically runs at server startup or first-install — any thrown error must be caught and surfaced to the operator: try { const webhook = await client.createWebhook({ Url: process.env.WEBHOOK_URL!, HttpAuth: { Username: 'user', Password: process.env.WEBHOOK_SECRET! }, Triggers: { Delivery: { Enabled: true }, Bounce: { Enabled: true, IncludeContent: false }, }, }); console.log('Webhook registered:', webhook.ID); } catch (error) { if (error instanceof Errors.ApiInputError) { // ErrorCode 606: URL invalid or contains internal IP range console.error('Webhook URL rejected by Postmark:', error.message); // Alert ops — email events will not be delivered until resolved } else { console.error('Webhook creation failed:', error.message); } throw error; // Re-throw — startup should fail if webhook not provisioned }costhighin prodimmediate exceptionusers seelost datavisibilitysilent - createMessageStream · create-message-stream-no-try-catcherrorWhencreateMessageStream() called in async context without surrounding try/catch. Throws ApiInputError (422) with unique stream-provisioning error codes: 1225 (max 10 streams per server), 1227 (invalid ID format — must be lowercase letters/numbers/hyphens/underscores, start with letter, max 30 chars), 1228 (already have one inbound stream), 1230 (ID already exists), 1233 (ID starts with reserved 'pm-' prefix), 1237 (ID is reserved). These error codes are DISTINCT from sendEmail error codes — callers catching generic PostmarkError still see them but cannot distinguish the provisioning error from a runtime email error without checking the error code.Throws
PostmarkError (ApiInputError 422 ErrorCodes 1221/1223/1224/1225/1227/1228/1230/1233/1237, InvalidAPIKeyError 401, RateLimitExceededError 429, InternalServerError 500)Required handlingCaller MUST wrap createMessageStream() in try-catch with error-code checking for the unique stream-provisioning codes. Provisioning scripts that call this at deploy time should surface ALL errors to the operator: try { const stream = await client.createMessageStream({ ID: 'marketing-outbound', Name: 'Marketing Outbound', MessageStreamType: 'Broadcasts', }); console.log('Stream created:', stream.ID); } catch (error) { if (error instanceof Errors.ApiInputError) { const code = (error as any).code; if (code === 1225) { console.error('Max 10 message streams reached — delete unused streams first'); } else if (code === 1227) { console.error('Invalid stream ID format — use lowercase letters/numbers/hyphens only, start with letter, max 30 chars'); } else if (code === 1228) { console.error('Only one inbound stream is allowed per server'); } else if (code === 1230) { console.error('Stream ID already exists — choose a different ID'); } else { console.error('Message stream creation failed:', error.message); } } else { console.error('Stream creation failed:', error.message); } throw error; // Re-throw — all downstream email for this channel will fail }costhighin prodimmediate exceptionusers seeservice unavailablevisibilitysilent
Sources
Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.
Official documentation
- [1]postmarkapp.com/developer/api/email-apiEmail Api
- [3]postmarkapp.com/developer/api/email-apiEmail Api
- [5]postmarkapp.com/developer/api/templates-apiTemplates Api
- [6]postmarkapp.com/developer/api/templates-apiTemplates Api
- [7]postmarkapp.com/developer/api/bounce-apiBounce Api
- [8]postmarkapp.com/developer/api/suppressions-apiSuppressions Api
- [9]postmarkapp.com/developer/api/templates-apiTemplates Api
- [10]postmarkapp.com/developer/api/overviewOverview
- [12]postmarkapp.com/developer/api/webhooks-apiWebhooks Api
- [13]postmarkapp.com/developer/api/overviewOverview
- [14]postmarkapp.com/developer/api/message-streams-apiMessage Streams Api
Source code
- [2]github.com/ActiveCampaign/postmark.js/blobActiveCampaign/postmark.js · Errors.ts
- [4]github.com/ActiveCampaign/postmark.js/blobActiveCampaign/postmark.js · ServerClient.ts
- [11]github.com/ActiveCampaign/postmark.js/blobActiveCampaign/postmark.js · ErrorHandler.ts
Research notes
Curator notes from SOURCES.md captured when the profile was written so you can verify the reasoning, not just the rules.
Sources: postmark
Official Documentation
- Email API Reference: https://postmarkapp.com/developer/api/email-api
- Documents sendEmail, sendEmailBatch endpoints and response format
- Templates API: https://postmarkapp.com/developer/api/templates-api
- Documents sendEmailWithTemplate, sendEmailBatchWithTemplates
- Error Codes: https://postmarkapp.com/developer/api/overview#error-codes
- Complete list of API error codes and their meanings
- API Overview: https://postmarkapp.com/developer/api/overview
- Authentication, rate limits, general API behavior
SDK Source Code
- Error Classes: https://github.com/ActiveCampaign/postmark.js/blob/main/src/client/errors/Errors.ts
- Complete error hierarchy: PostmarkError → HttpError → {InvalidAPIKeyError, InternalServerError, etc.}
- ApiInputError subclasses: InactiveRecipientsError (406), InvalidEmailRequestError (300)
- Error Handler: https://github.com/ActiveCampaign/postmark.js/blob/main/src/client/errors/ErrorHandler.ts
- HTTP status code → error class mapping (401, 404, 422, 429, 500, 503)
- ServerClient: https://github.com/ActiveCampaign/postmark.js/blob/main/src/client/ServerClient.ts
- All public methods, parameter types, return types
- BaseClient: https://github.com/ActiveCampaign/postmark.js/blob/main/src/client/BaseClient.ts
- HTTP request pipeline, error propagation flow
Evidence Quality: partial
- Limited local test-repo coverage (blitz template, n8n node — not direct SDK usage)
- GitHub search unavailable during onboarding (auth expired)
- Contract based on SDK source code analysis — high confidence in error behavior
Need a different package?
Request a profile