Skip to content

Powered by Grav CMS + Helios Theme

Postmark

This page is for a store sending through Postmark that wants delivered, bounced and complained figures on its campaign screens. Read Delivery reports and providers first for what a provider webhook is and what the store does with each event.

Before you begin

  • A Postmark account with at least one server. Tokens, message streams and webhooks all belong to a server.
  • A confirmed sender signature, or a verified sending domain. Postmark refuses to send from an address that is neither.
  • The grav-plugin-email-postmark plugin at 1.2.2 or later, with the Grav Email plugin at 5.2.0 or later on PHP 8.1 or later.
  • The Email plugin's Mail Engine set to Postmark.

Which token each feature needs

Postmark has no granular permission scopes. It has two token types, and each is refused where the other belongs.

Feature Token Header it is sent as Where it is found
Sending Server API token X-Postmark-Server-Token The server's own API Tokens tab
Creating and updating the webhook Server API token. Postmark puts no separate webhook permission on it. X-Postmark-Server-Token The same tab
Reading the sending domain's DKIM selector and return path for the Health tab Account API token X-Postmark-Account-Token Account → API Tokens

The server token is available to account owners, account admins and users with server-admin rights on that server. The account token is available to the account owner and account admins.

Important

An account token is refused where a server token belongs, and the other way round, with a 401. The Set up button adds a line about it whenever Postmark's refusal mentions a token.

The account token is optional. Without it the Health tab uses whatever selector you configured by hand.

Configure the transport

The transport is a Grav plugin, so its own settings live in the Grav admin's plugin list. The webhook that reports deliveries back is set up from the Providers card in the KahunaCart admin, in the next section.

  1. 1

    In the Grav admin's plugin list, open Email Postmark and set Transport to API (fastest) or SMTP.

  2. 2

    Paste the server token into API Token.

    Postmark has two kinds of token and they are not interchangeable. The server token sends mail and manages that server's webhooks. The account token is a different string, used only for reading your sender signatures, and it goes in its own field.

  3. 3

    Set Message Stream if you send on something other than outbound, then open Email in the same plugin list and set Mail Engine to Postmark.

Note

A new Postmark account is approval-pending, and pending accounts can only mail their own domain. Sending anywhere else is refused with Postmark's own sentence about it, which the campaign records as a failure rather than a send. If your first test lands as failed, read the reason before changing anything.

Senders are verified for the whole account, not per server. A sender signature is an address or a domain, added once under Sender Signatures, and every server can then send as it.

Field Default What it does
enabled on The plugin's own switch.
transport api api or smtp.
api_token empty The server token. Required.
message_stream outbound The stream messages leave on, and the stream the webhook is created on.
basic_user empty Webhook Username: the basic-auth username set on the webhook and checked on every report that arrives.
basic_password empty Webhook Password: the matching password.
account_token empty Account Token, for the deliverability checks only.

message_stream is appended to the transport DSN only when it is neither empty nor outbound. A store that never touched the field gets the DSN it had before.

Both transports carry custom headers, so List-Unsubscribe and List-Unsubscribe-Post reach the wire either way.

Set the webhook up in one click

  1. 1

    Go to KahunaCart → Add-ons → Newsletters → Settings → Providers and click Generate a secret on the Postmark card.

  2. 2

    Click Set up in Postmark.

The button lists the webhooks already on the stream, then either creates one or updates the one already pointed at that address. Postmark posts every event to every webhook on a stream, so a second press updates rather than duplicates.

The Set up button does not appear until an API token is configured.

The triggers it sets

Every trigger is named on the request, because an update leaves out what it does not mention.

Trigger Set to Why
Delivery on The store's delivered figure.
Bounce on With IncludeContent off.
SpamComplaint on With IncludeContent off.
Open on With PostFirstOpenOnly off, so a reload is not silently dropped.
Click on
SubscriptionChange off Postmark's own suppression list changing, not something that happened to a message.

If you have set Webhook Username and Webhook Password, the button sets the same pair as the webhook's basic auth. Setting both ends from one config value stops them drifting apart. With nothing configured, no basic auth is set at all.

Nothing deletes a webhook. Remove one in Postmark's dashboard.

Do it by hand instead

  1. 1

    In Postmark, open the server this store sends through, then the message stream, then its Webhooks tab, and click Add webhook.

  2. 2

    Paste the webhook address from the Providers card into the Webhook URL box.

  3. 3

    Tick Delivery, Bounce, Spam complaint, Open and Click.

    Leave Subscription change unticked. That one is Postmark's own suppression list rather than the store's.

  4. 4

    If you set a username and password for basic auth on that screen, put the same pair into Webhook Username and Webhook Password in the Email Postmark plugin's settings.

    Both are optional. The secret already in the address is what proves an event came from Postmark, so basic auth is a second lock rather than the only one.

  5. 5

    Test the webhook before saving, then save.

There is no header to add. Postmark returns no headers on any outbound webhook.

What each event means

Postmark names the event in RecordType.

Postmark RecordType Becomes Notes
Delivery delivered
Bounce with a permanent type code hard bounce Type codes 1 (HardBounce), 100000 (BadEmailAddress) and 100009 (DMARCPolicy).
Bounce with anything else soft bounce Including 2 Transient, 256 DnsError, 512 SpamNotification and 4096 SoftBounce. Three in thirty days are treated as a hard one.
SpamComplaint complaint The address is suppressed and the subscriber is unsubscribed.
Open open Stamped where the store's own pixel has not already.
Click click The same.
SubscriptionChange skipped The trigger is left off.
anything else skipped with a note Logged, not acted on.

There is no boolean in the payload saying hard or soft. The type code is the answer.

An address Postmark reports with Inactive set and CanActivate false is treated as a hard bounce whatever its code says, so the store's suppression list matches Postmark's.

The reason a merchant reads is Postmark's Name and Details joined, falling back to Description.

Postmark never reports a refused message

Postmark has no event for a refused message. A send to an address Postmark has suppressed is refused by the API call itself, with HTTP 422 and ErrorCode 406, "Inactive recipient." No message is created and no webhook ever arrives. It reaches the store as a failed send on that person's row rather than as a delivery report.

SubscriptionChange is not a substitute. It is Postmark's suppression list changing, not something that happened to a message.

Provider quirks

Postmark echoes no headers, on any webhook. No outbound Postmark webhook carries a headers array, and there is no setting that turns one on. What it does return on every one is Metadata.

X-PM-Metadata-Grav-Send-Id is an SMTP-only convention. Postmark turns a header with the X-PM-Metadata- prefix into metadata when the message arrives over SMTP. On this plugin's API transport, that header goes into the payload's Headers array and Postmark treats it as an ordinary header.

Important

A store on the API transport, this plugin's default, gets no send id back from Postmark. Delivery, bounce and complaint events still find the person by address, so suppressions and subscriber statuses are right, but per-campaign figures cannot be joined to the send row. Switch to the SMTP transport if you want bounces tied to a particular send.

Postmark strips the metadata header before the message reaches the recipient. Its limits are twenty characters on a key, eighty on a value, and ten fields per message, which is why the send header's metadata key is capped at twenty characters.

MessageID is Postmark's own UUID, not your Message-ID. The parser does not use it for correlation.

Postmark does not sign its webhooks. It offers basic auth and IP allowlisting instead. The path secret in the address, and the basic auth pair if you set one, protect the address.

Postmark drops an event on most 4xx answers. It retries a 5xx, a 408 and a 429 across six attempts escalating from one minute to fifteen. Any other 4xx is permanent, which is why the store answers an unreadable payload with 200 and logs it.

DNS for the Health tab

Record Value Notes
SPF include:spf.mtasv.net A domain may only have one SPF TXT record. Add Postmark to the existing one.
DKIM a TXT record at a host Postmark generates per domain Not a CNAME into a Postmark zone, so the selector cannot be guessed.
Return path a CNAME, by default at pm_bounces, pointing at pm.mtasv.net What lets Postmark's mail pass SPF for your domain.

With an Account Token configured, the Health tab reads the live selector, the pending one (the record Postmark is currently asking you to publish) and the return path off your account. Without it, put the part before ._domainkey into deliverability.dkim_selector on KahunaCart → Add-ons → Newsletters → Settings → Health.

Postmark also asks for a DMARC record as part of full domain verification. See Health.

Checking it actually works

Important

The numbers on a campaign screen are not evidence that Postmark is reporting anything. Opens and clicks are counted by the store's own tracking pixel and click redirect, and those run whether or not a webhook has ever arrived. A campaign can show delivered, opened and clicked while Postmark has said nothing.

The Providers card is the honest answer. If it says nothing has been received, nothing has.

Send a one-recipient campaign to an address you can read, open it, press a link, then check the Postmark card.

Troubleshooting

Symptom Cause
Every report refused The basic auth pair on the webhook and the pair in the plugin's settings have drifted apart.
The Set up button says Postmark refused it, mentioning a token An account token is in the API Token field, or a server token from a different server.
No Set up button on the card No API token is configured yet.
"None of the events asked for is one Postmark can report" Nothing requested maps to a Postmark trigger.
Two of everything recorded Two webhooks on one stream pointed at the same address. Remove one in Postmark.
Delivered and bounced figures never join to a campaign The store is on the API transport. See the metadata note above.
The Health tab says it could not look up the selector No Account Token is configured.
"Sender Signature not defined for From address" The From address is not a confirmed sender signature and its domain is not verified.
Events vanish with nothing in the log Postmark treats most 4xx answers as permanent and drops the event. Check the store answered 200.