Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Skills for receiving and verifying webhooks from specific providers. Each includ
| GitLab | [`gitlab-webhooks`](skills/gitlab-webhooks/) | Verify GitLab webhook tokens, handle push, merge_request, issue, and pipeline events |
| OpenAI | [`openai-webhooks`](skills/openai-webhooks/) | Verify OpenAI webhooks for fine-tuning, batch, and realtime async events |
| Paddle | [`paddle-webhooks`](skills/paddle-webhooks/) | Verify Paddle webhook signatures, handle subscription and billing events |
| Postmark | [`postmark-webhooks`](skills/postmark-webhooks/) | Authenticate Postmark webhooks (Basic Auth/Token), handle email delivery, bounce, open, click, and spam events |
| Replicate | [`replicate-webhooks`](skills/replicate-webhooks/) | Verify Replicate webhook signatures, handle ML prediction lifecycle events |
| Resend | [`resend-webhooks`](skills/resend-webhooks/) | Verify Resend webhook signatures, handle email delivery and bounce events |
| SendGrid | [`sendgrid-webhooks`](skills/sendgrid-webhooks/) | Verify SendGrid webhook signatures (ECDSA), handle email delivery events |
Expand Down
15 changes: 15 additions & 0 deletions providers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,21 @@ providers:
- email.delivered
- email.bounced

- name: postmark
displayName: Postmark
docs:
webhooks: https://postmarkapp.com/developer/webhooks/webhooks-overview
events: https://postmarkapp.com/developer/webhooks/webhooks-overview
notes: >
Email delivery platform. Uses Basic Auth or token-based authentication in the webhook URL
(not signature verification). Configure webhook URL with credentials like
https://username:password@yourdomain.com/webhooks/postmark or use a token query parameter.
Events: Bounce, Delivery, Open, Click, SpamComplaint, SubscriptionChange.
testScenario:
events:
- Bounce
- Delivery

- name: sendgrid
displayName: SendGrid
docs:
Expand Down
191 changes: 191 additions & 0 deletions skills/postmark-webhooks/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
---
name: postmark-webhooks
description: >
Receive and process Postmark webhooks. Use when setting up Postmark webhook
handlers, handling email delivery events, processing bounces, opens, clicks,
spam complaints, or subscription changes.
license: MIT
metadata:
author: hookdeck
version: "0.1.0"
repository: https://github.com/hookdeck/webhook-skills
---

# Postmark Webhooks

## When to Use This Skill

- Setting up Postmark webhook handlers for email event tracking
- Processing email delivery events (bounce, delivered, open, click)
- Handling spam complaints and subscription changes
- Implementing email engagement analytics
- Troubleshooting webhook authentication issues

## Essential Code

### Authentication

Postmark does NOT use signature verification. Instead, webhooks are authenticated by including credentials in the webhook URL itself.

```javascript
// Express - Basic Auth in URL
// Configure webhook URL in Postmark as:
// https://username:password@yourdomain.com/webhooks/postmark

app.post('/webhooks/postmark', express.json(), (req, res) => {
// Basic auth is handled by your web server or proxy
// Additional validation can check expected payload structure

const event = req.body;

// Validate expected fields exist
if (!event.RecordType || !event.MessageID) {
return res.status(400).send('Invalid payload structure');
}

// Process event
console.log(`Received ${event.RecordType} event for ${event.Email}`);

res.sendStatus(200);
});

// Alternative: Token in URL
// Configure webhook URL as:
// https://yourdomain.com/webhooks/postmark?token=your-secret-token

app.post('/webhooks/postmark', express.json(), (req, res) => {
const token = req.query.token;

if (token !== process.env.POSTMARK_WEBHOOK_TOKEN) {
return res.status(401).send('Unauthorized');
}

const event = req.body;
console.log(`Received ${event.RecordType} event`);

res.sendStatus(200);
});
```

### Handling Multiple Events

```javascript
// Postmark sends one event per request (not batched)
app.post('/webhooks/postmark', express.json(), (req, res) => {
const event = req.body;

switch (event.RecordType) {
case 'Bounce':
console.log(`Bounce: ${event.Email} - ${event.Type} - ${event.Description}`);
// Update contact as undeliverable
break;

case 'SpamComplaint':
console.log(`Spam complaint: ${event.Email}`);
// Remove from mailing list
break;

case 'Open':
console.log(`Email opened: ${event.Email} at ${event.ReceivedAt}`);
// Track engagement
break;

case 'Click':
console.log(`Link clicked: ${event.Email} - ${event.OriginalLink}`);
// Track click-through rate
break;

case 'Delivery':
console.log(`Delivered: ${event.Email} at ${event.DeliveredAt}`);
// Confirm delivery
break;

case 'SubscriptionChange':
console.log(`Subscription change: ${event.Email} - ${event.ChangedAt}`);
// Update subscription preferences
break;

case 'Inbound':
console.log(`Inbound email from: ${event.Email} - Subject: ${event.Subject}`);
// Process incoming email
break;

case 'SMTP API Error':
console.log(`SMTP API error: ${event.Email} - ${event.Error}`);
// Handle API error, maybe retry
break;

default:
console.log(`Unknown event type: ${event.RecordType}`);
}

res.sendStatus(200);
});
```

## Common Event Types

| Event | RecordType | Description | Key Fields |
|-------|------------|-------------|------------|
| Bounce | `Bounce` | Hard/soft bounce or blocked email | Email, Type, TypeCode, Description |
| Spam Complaint | `SpamComplaint` | Recipient marked as spam | Email, BouncedAt |
| Open | `Open` | Email opened (requires open tracking) | Email, ReceivedAt, Platform, UserAgent |
| Click | `Click` | Link clicked (requires click tracking) | Email, ClickedAt, OriginalLink |
| Delivery | `Delivery` | Successfully delivered | Email, DeliveredAt, Details |
| Subscription Change | `SubscriptionChange` | Unsubscribe/resubscribe | Email, ChangedAt, SuppressionReason |
| Inbound | `Inbound` | Incoming email received | Email, FromFull, Subject, TextBody, HtmlBody |
| SMTP API Error | `SMTP API Error` | SMTP API call failed | Email, Error, ErrorCode, MessageID |

## Environment Variables

```bash
# For token-based authentication
POSTMARK_WEBHOOK_TOKEN="your-secret-token-here"

# For basic auth (if not using URL-embedded credentials)
WEBHOOK_USERNAME="your-username"
WEBHOOK_PASSWORD="your-password"
```

## Security Best Practices

1. **Always use HTTPS** - Never configure webhooks with HTTP URLs
2. **Use strong credentials** - Generate long, random tokens or passwords
3. **Validate payload structure** - Check for expected fields before processing
4. **Implement IP allowlisting** - Postmark publishes their IP ranges
5. **Consider using a webhook gateway** - Like Hookdeck for additional security layers

## Local Development

For local webhook testing, use Hookdeck CLI:

```bash
brew install hookdeck/hookdeck/hookdeck
hookdeck listen 3000 --path /webhooks/postmark
```

No account required. Provides local tunnel + web UI for inspecting requests.

## Resources

- [overview.md](references/overview.md) - What Postmark webhooks are, common event types
- [setup.md](references/setup.md) - Configure webhooks in Postmark dashboard
- [verification.md](references/verification.md) - Authentication methods and security best practices
- [examples/](examples/) - Complete implementations for Express, Next.js, and FastAPI

## Recommended: webhook-handler-patterns

For production-ready webhook handling, also install the webhook-handler-patterns skill:

- [Handler sequence](https://github.com/hookdeck/webhook-skills/blob/main/skills/webhook-handler-patterns/references/handler-sequence.md) - Webhook processing flow
- [Idempotency](https://github.com/hookdeck/webhook-skills/blob/main/skills/webhook-handler-patterns/references/idempotency.md) - Prevent duplicate processing
- [Error handling](https://github.com/hookdeck/webhook-skills/blob/main/skills/webhook-handler-patterns/references/error-handling.md) - Graceful error recovery
- [Retry logic](https://github.com/hookdeck/webhook-skills/blob/main/skills/webhook-handler-patterns/references/retry-logic.md) - Handle transient failures

## Related Skills

- [sendgrid-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/sendgrid-webhooks) - SendGrid webhook handling with ECDSA verification
- [resend-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/resend-webhooks) - Resend webhook handling with Svix signatures
- [stripe-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/stripe-webhooks) - Stripe webhook handling with HMAC-SHA256
- [webhook-handler-patterns](https://github.com/hookdeck/webhook-skills/tree/main/skills/webhook-handler-patterns) - Idempotency, error handling, retry logic
- [hookdeck-event-gateway](https://github.com/hookdeck/webhook-skills/tree/main/skills/hookdeck-event-gateway) - Production webhook infrastructure
22 changes: 22 additions & 0 deletions skills/postmark-webhooks/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# TODO - Known Issues and Improvements

*Last updated: 2026-02-05*

These items were identified during automated review but are acceptable for merge.
Contributions to address these items are welcome.

## Issues

### Critical

None - Postmark correctly uses URL-based authentication instead of signature verification, which matches the official documentation.

### Major

None - Inbound and SMTP API Error event types are documented in overview.md.

### Minor

- [ ] **skills/postmark-webhooks/examples/nextjs/package.json**: Next.js version 16.1.6 doesn't exist. Latest stable is 15.x
- Suggested fix: Change next version to ^15.1.6 or latest 15.x version

10 changes: 10 additions & 0 deletions skills/postmark-webhooks/examples/express/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Postmark webhook authentication token
# Generate with: openssl rand -base64 32
POSTMARK_WEBHOOK_TOKEN=your-secret-webhook-token

# Alternative: Basic auth credentials
# WEBHOOK_USERNAME=webhook-user
# WEBHOOK_PASSWORD=webhook-password

# Server port
PORT=3000
80 changes: 80 additions & 0 deletions skills/postmark-webhooks/examples/express/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Postmark Webhooks - Express Example

Minimal example of receiving Postmark webhooks with authentication in Express.js.

## Prerequisites

- Node.js 18+
- Postmark account with webhook configuration access

## Setup

1. Install dependencies:
```bash
npm install
```

2. Copy environment variables:
```bash
cp .env.example .env
```

3. Generate a secure webhook token:
```bash
openssl rand -base64 32
```

4. Add the token to your `.env` file

## Run

Start the server:

```bash
npm start
```

Server runs on http://localhost:3000

## Configure Postmark

1. Log in to your [Postmark account](https://account.postmarkapp.com)
2. Select your Server → Webhooks → Add webhook
3. Set the webhook URL with your token:
```
https://yourdomain.com/webhooks/postmark?token=your-secret-token
```
4. Select the events you want to receive
5. Save and test the webhook

## Test Locally

Use the Hookdeck CLI for local testing:

```bash
# Install Hookdeck CLI
brew install hookdeck/hookdeck/hookdeck

# Create tunnel to your local server
hookdeck listen 3000 --path /webhooks/postmark
```

Then use the provided URL in Postmark's webhook configuration.

## Test

Run the test suite:

```bash
npm test
```

## Authentication Methods

This example uses token-based authentication. For basic auth, configure your webhook URL as:

```
https://username:password@yourdomain.com/webhooks/postmark
```

And update the handler to validate basic auth headers instead of the token parameter.
19 changes: 19 additions & 0 deletions skills/postmark-webhooks/examples/express/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "postmark-webhooks-express",
"version": "1.0.0",
"description": "Express.js example for handling Postmark webhooks",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js",
"test": "jest"
},
"dependencies": {
"express": "^5.2.1",
"dotenv": "^16.4.7"
},
"devDependencies": {
"jest": "^30.2.0",
"supertest": "^7.0.0"
}
}
Loading