3 min read
Postmark inbound webhook (platform-level)
Handles bounces, complaints, and inbound parsing sent from Postmark to Formspring. This is a server-wide integration, not per-form. The endpoint lives at POST /webhooks/postmark and is processed by App\Http\Controllers\Webhooks\PostmarkWebhookController.
Distinct from the outbound Postmark driver which sends transactional email per submission.
What it does
- Bounce events mark recipient addresses as suppressed in
email_suppressionsso we don't keep sending to dead addresses. - Complaint / spam events do the same plus log a flag (a future feature can use this to alert form owners).
- Inbound parsing (optional) routes inbound email to a form-defined address into a submission - only useful if you've configured an inbound stream.
Step 1 - Configure the inbound webhook URL in Postmark
- Sign in to https://account.postmarkapp.com/servers.
- Open the Server you use for outbound mail (per postmark.md).
- Default Transactional Stream (or whichever stream is sending) → Settings tab → scroll to Webhooks.
- Set Bounce webhook:
https://formspring.io/webhooks/postmark. - Set Spam complaint webhook: same URL.
- Save.
For inbound parsing (optional):
- Inbound stream (create one if absent).
- Webhook:
https://formspring.io/webhooks/postmark.
Step 2 - Configure the inbound secret in Formspring
To prevent unauthorised POSTs, Formspring validates a shared secret in the request. Set in .env:
POSTMARK_INBOUND_SECRET=<a long random string>
Then in Postmark, add the same secret to the webhook URL as a query parameter:
https://formspring.io/webhooks/postmark?secret=<the same value>
The controller (PostmarkWebhookController) validates request('secret') === config('services.postmark.inbound_secret') and 401s otherwise.
Step 3 - Verify the wiring
- In Postmark, send a test bounce: Servers → your server → Activity → click any sent message → Send test bounce → fill in the recipient.
- Tail the application logs.
- Confirm a row appears in
email_suppressionswith the bounced address.
Where the credential lives
- Server:
.env→POSTMARK_INBOUND_SECRET→config/services.phppostmark.inbound_secret. - Controller:
app/Http/Controllers/Webhooks/PostmarkWebhookController.php. - Route:
routes/web.php→POST /webhooks/postmark(no CSRF token required since Postmark is the caller). - Suppression model:
app/Models/EmailSuppression.php+ listenerapp/Listeners/SuppressMailToSuppressedAddresses.php.
Postmark webhook event types we handle
| Event type | Action |
|---|---|
Bounce |
Add address to email_suppressions with reason. |
SpamComplaint |
Same as bounce + record reason spam. |
SubscriptionChange |
(Currently a no-op - placeholder for future suppression-list-from-Postmark sync.) |
Inbound |
Optional, parses an inbound email into a submission for the configured form. |
Security
- Rotate the inbound secret by generating a new value, updating
.env, restarting the app, then updating each Postmark webhook URL's?secret=query param. - The secret is sent as a query string, which means it appears in Postmark's webhook delivery logs. That's acceptable - Postmark restricts log access to your account.
- For extra defence, add Postmark's IP allowlist to your firewall (Postmark publishes their IPs at https://postmarkapp.com/support/article/800-ips-for-firewalls).
Troubleshooting
| Symptom | Cause |
|---|---|
| 401 on every Postmark webhook delivery | Secret mismatch or query param missing in Postmark URL. |
| Bounces don't add to suppression list | Check the application log for parsing errors. The handler can log + continue silently. |
| Forms still send to a known-bounced address | Suppression listener (SuppressMailToSuppressedAddresses) only fires on MessageSending events. If a driver doesn't go through the framework mailer (Postmark/Resend/SendGrid drivers send via the HTTP client directly), the listener doesn't catch it. Pre-check against the suppression list in those drivers if needed. |
Provider docs
- Postmark webhooks: https://postmarkapp.com/developer/webhooks/webhooks-overview
- Bounce webhook: https://postmarkapp.com/developer/webhooks/bounce-webhook
- Spam complaint webhook: https://postmarkapp.com/developer/webhooks/spam-complaint-webhook
- Inbound webhook: https://postmarkapp.com/developer/webhooks/inbound-webhook