# How to Implement Custom Bounce Handling with Postmark Webhooks in Listmonk

> Learn to implement custom bounce handling with Postmark webhooks in Listmonk. Discover how to process bounce notifications and map them to internal records for effective email management.

- Repository: [Kailash Nadh/listmonk](https://github.com/knadh/listmonk)
- Tags: how-to-guide
- Published: 2026-05-19

---

**Listmonk ingests Postmark bounce notifications through the `/api/bounce/postmark` endpoint, authenticating requests via HTTP Basic Auth and mapping JSON payloads to internal bounce records using the `Postmark.ProcessBounce` method.**

Listmonk is an open-source newsletter and mailing list manager that supports webhook-based bounce processing from multiple email service providers. To implement custom bounce handling with Postmark webhooks, you configure credentials in [`config.toml`](https://github.com/knadh/listmonk/blob/main/config.toml) and point Postmark's webhook settings to Listmonk's bounce API endpoint.

## Configuring Postmark Bounce Handling

Before Postmark can deliver bounce notifications, you must enable and secure the webhook endpoint in Listmonk's configuration. The settings reside in the `[bounce]` section of your [`config.toml`](https://github.com/knadh/listmonk/blob/main/config.toml) file.

Set the following values:

```toml
[bounce]
postmark.enabled = true
postmark.username = "your-secure-username"
postmark.password = "your-secure-password"

```

When `postmark.enabled` is true, Listmonk initializes a `Postmark` struct via `NewPostmark(username, password)` defined in [`internal/bounce/webhooks/postmark.go`](https://github.com/knadh/listmonk/blob/main/internal/bounce/webhooks/postmark.go). This struct wraps the authentication handler and bounce processing logic required to validate and ingest Postmark payloads.

## Webhook Endpoint and Authentication Flow

Listmonk exposes a generic bounce webhook endpoint at `/api/bounce/:service`, where `:service` is the provider name. For Postmark, the full URL is:

```

https://YOUR_LISTMONK_HOST/api/bounce/postmark

```

In [`cmd/bounce.go`](https://github.com/knadh/listmonk/blob/main/cmd/bounce.go), the `BounceWebhook` handler extracts the service parameter and reads the raw request body:

```go
service := c.Param("service")
rawReq, err := io.ReadAll(c.Request().Body)

```

The Postmark implementation enforces security via HTTP Basic Auth. In [`internal/bounce/webhooks/postmark.go`](https://github.com/knadh/listmonk/blob/main/internal/bounce/webhooks/postmark.go), lines 43-49 construct the middleware using constant-time credential comparison to prevent timing attacks:

```go
authHandler: middleware.BasicAuth(makePostmarkAuthHandler(username, password))

```

When Postmark posts a bounce to your endpoint, Listmonk validates the username and password against your configuration using a secure constant-time comparison (lines 101-118) before processing the payload.

## Processing Postmark Bounce Payloads

Once authenticated, Listmonk delegates to `Postmark.ProcessBounce` to transform the Postmark JSON into internal `models.Bounce` objects.

### Payload Structure and Type Mapping

The method unmarshals the raw JSON into a `postmarkNotif` struct (lines 58-80). It then maps Postmark's bounce types—such as `HardBounce`, `SoftBounce`, `SpamComplaint`, and `Unsubscribe`—to Listmonk's internal `BounceType*` enum values.

### Campaign Attribution

To link a bounce to a specific campaign, include the custom header `X-Listmonk-Campaign` when sending mail through Postmark. Listmonk extracts this UUID from the `Metadata` field of the Postmark payload (lines 85-90):

```go
if campUUID, ok := n.Metadata["X-Listmonk-Campaign"]; ok {
    b.CampaignUUID = campUUID
}

```

### Recording the Bounce

The function constructs a `models.Bounce` object (defined in [`models/bounces.go`](https://github.com/knadh/listmonk/blob/main/models/bounces.go)) and returns it to the generic handler in [`cmd/bounce.go`](https://github.com/knadh/listmonk/blob/main/cmd/bounce.go). The handler then persists the record via `a.bounce.Record(b)` (lines 48-55), storing the bounce type, email address, timestamp, and raw metadata in the database.

## Practical Implementation Examples

### Example 1: Minimal Configuration

Enable Postmark bounce handling in your [`config.toml`](https://github.com/knadh/listmonk/blob/main/config.toml):

```toml
[bounce]
postmark.enabled = true
postmark.username = "listmonk-webhook-user"
postmark.password = "a-secure-random-password"

```

### Example 2: Registering the Webhook with Postmark

Configure the webhook in your Postmark dashboard or via their API. The webhook must use HTTP Basic Auth with the credentials defined above:

```bash
curl -X POST https://api.postmarkapp.com/webhooks \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Server-Token: YOUR_SERVER_TOKEN" \
  -d '{
        "Url": "https://listmonk.example.com/api/bounce/postmark",
        "Triggers": ["Bounce", "SpamComplaint"],
        "BasicAuthUsername": "listmonk-webhook-user",
        "BasicAuthPassword": "a-secure-random-password"
      }'

```

### Example 3: Sending Campaign-Tagged Email

When sending via Postmark's API, include the custom header to enable bounce attribution:

```bash
curl -X POST https://api.postmarkapp.com/email \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Server-Token: YOUR_SERVER_TOKEN" \
  -d '{
        "From": "newsletter@example.com",
        "To": "subscriber@example.com",
        "Subject": "Weekly Update",
        "HtmlBody": "<p>Hello!</p>",
        "Headers": [
          {
            "Name": "X-Listmonk-Campaign",
            "Value": "c1d2e3f4-5678-90ab-cdef-1234567890ab"
          }
        ]
      }'

```

### Example 4: Testing the Webhook Endpoint

Simulate a Postmark bounce notification to verify your implementation:

```bash
curl -X POST https://listmonk.example.com/api/bounce/postmark \
  -u listmonk-webhook-user:a-secure-random-password \
  -H "Content-Type: application/json" \
  -d '{
        "RecordType": "Bounce",
        "MessageStream": "outbound",
        "ID": 12345,
        "Type": "HardBounce",
        "Email": "bounced@example.com",
        "BouncedAt": "2024-05-01T12:00:00Z",
        "Metadata": {
          "X-Listmonk-Campaign": "c1d2e3f4-5678-90ab-cdef-1234567890ab"
        }
      }'

```

A successful request returns:

```json
{"status": true}

```

### Example 5: Querying Stored Bounces

Retrieve processed bounces via the Listmonk API:

```bash
curl https://listmonk.example.com/api/bounces?source=postmark \
  -H "Authorization: Bearer YOUR_API_TOKEN"

```

The response includes bounce objects containing the email address, mapped bounce type, campaign UUID, and the raw Postmark payload stored in the `meta` field.

## Summary

- **Endpoint**: Listmonk receives Postmark bounces at `/api/bounce/postmark` as implemented in [`cmd/bounce.go`](https://github.com/knadh/listmonk/blob/main/cmd/bounce.go).
- **Security**: HTTP Basic Auth with constant-time credential comparison protects the endpoint ([`internal/bounce/webhooks/postmark.go`](https://github.com/knadh/listmonk/blob/main/internal/bounce/webhooks/postmark.go), lines 101-118).
- **Configuration**: Enable `postmark.enabled` in [`config.toml`](https://github.com/knadh/listmonk/blob/main/config.toml) and supply unique username/password credentials.
- **Campaign Tracking**: Add the `X-Listmonk-Campaign` custom header when sending email to associate bounces with specific campaigns.
- **Persistence**: The `Postmark.ProcessBounce` method maps Postmark types to internal enums and stores records via `a.bounce.Record`.

## Frequently Asked Questions

### What bounce types does Listmonk support from Postmark?

Listmonk maps Postmark's `HardBounce`, `SoftBounce`, `SpamComplaint`, `Unsubscribe`, and other types to its internal bounce type enumeration defined in [`models/bounces.go`](https://github.com/knadh/listmonk/blob/main/models/bounces.go). Hard bounces typically result in automatic subscriber suppression, while soft bounces are recorded for monitoring.

### How do I secure the webhook endpoint?

Listmonk requires HTTP Basic Auth for all Postmark webhook requests. Configure a unique username and password in the `[bounce]` section of [`config.toml`](https://github.com/knadh/listmonk/blob/main/config.toml). The authentication handler uses constant-time comparison to prevent timing attacks, as seen in [`internal/bounce/webhooks/postmark.go`](https://github.com/knadh/listmonk/blob/main/internal/bounce/webhooks/postmark.go) lines 101-118. Never expose the endpoint without authentication or use default credentials.

### Can I track which campaign caused a bounce?

Yes. When sending email through Postmark, include the `X-Listmonk-Campaign` header with your campaign UUID as the value. Listmonk extracts this from the `Metadata` field of the Postmark payload and stores it in the `CampaignUUID` field of the bounce record, allowing you to analyze bounce rates per campaign.

### What happens if the webhook authentication fails?

If the username or password supplied by Postmark does not match your configuration, Listmonk returns an HTTP 401 Unauthorized error and aborts the request before processing the bounce payload. Failed authentication attempts are logged, and the bounce is not recorded in the database.