Skip to content

fix(postmark): accept numeric ID in webhook payloads; use BouncedAt f… - #445

Open
ckdev96 wants to merge 1 commit into
Notifuse:mainfrom
ckdev96:fix/postmark-webhook-numeric-id
Open

ckdev96 wants to merge 1 commit into
Notifuse:mainfrom
ckdev96:fix/postmark-webhook-numeric-id

Conversation

@ckdev96

@ckdev96 ckdev96 commented Oct 3, 2026

Copy link
Copy Markdown

…or complaints

Postmark sends "ID" as a JSON number in Bounce and SpamComplaint webhook payloads (e.g. "ID": 692560173), but PostmarkWebhookPayload.ID was a string, so json.Unmarshal failed and the endpoint returned 400.

Postmark now verifies webhooks on creation by posting sample events and rejects the webhook on any non-2xx (HTTP 422, ErrorCode 1364), so "Register webhook" fails for Postmark integrations. Real bounce/complaint events are rejected for the same reason.

  • PostmarkWebhookPayload.ID: string -> json.Number (accepts numbers and string-encoded numbers, so existing payloads keep working).
  • SpamComplaint: Postmark carries the event time in "BouncedAt"; fall back to it when "ComplainedAt" is absent instead of using time.Now().
  • Regression tests with the official Postmark sample payloads.

…or complaints

Postmark sends "ID" as a JSON number in Bounce and SpamComplaint webhook
payloads (e.g. "ID": 692560173), but PostmarkWebhookPayload.ID was a string,
so json.Unmarshal failed and the endpoint returned 400.

Postmark now verifies webhooks on creation by posting sample events and
rejects the webhook on any non-2xx (HTTP 422, ErrorCode 1364), so
"Register webhook" fails for Postmark integrations. Real bounce/complaint
events are rejected for the same reason.

- PostmarkWebhookPayload.ID: string -> json.Number (accepts numbers and
  string-encoded numbers, so existing payloads keep working).
- SpamComplaint: Postmark carries the event time in "BouncedAt"; fall back to
  it when "ComplainedAt" is absent instead of using time.Now().
- Regression tests with the official Postmark sample payloads.
@ckdev96

ckdev96 commented Oct 3, 2026

Copy link
Copy Markdown
Author

Some extra context and evidence for this fix:

What users see

In the Postmark integration, clicking Register webhook shows:

Failed to register webhooks: failed to register Postmark webhook: API returned non-OK status code 422

Server log:

failed to process webhook: failed to unmarshal Postmark webhook payload: json: cannot unmarshal number into Go struct field PostmarkWebhookPayload.ID of type string

Postmark's verification response (per trigger)

{"ErrorCode":1364,"Message":"Webhook verification failed; nothing was saved. Fix the endpoint and retry, or send ?verify=false to save it unverified.", ... "Results":[
 {"TriggerType":"Delivery","Success":true,"StatusCode":200},
 {"TriggerType":"Bounce","Success":false,"StatusCode":400,"Message":"{\"error\":\"Failed to process webhook\"}"},
 {"TriggerType":"SpamComplaint","Success":false,"StatusCode":400,"Message":"{\"error\":\"Failed to process webhook\"}"}]}

Delivery passes because its payload has no ID field. ?verify=false is not a workaround: real bounce and complaint events carry the same numeric ID and would also be rejected.

Reproduction (v41.0)

POST the official samples to /webhooks/email?provider=postmark&workspace_id=…&integration_id=…:

So ID is the only blocking field; the other field types match.

Verification of this PR

  • The two new subtests in TestProcessPostmarkWebhook fail on v41.0 without the fix and pass with it.
  • The full go test ./internal/domain/... ./internal/service/... suite passes, and gofmt is clean.
  • We run this exact commit (61c421b, on top of v41.0) in production. Postmark's webhook verification now succeeds, the Bounce and SpamComplaint samples return 200, and complaints are stored with Postmark's event time (BouncedAt) instead of the time they were received.

Thanks for Notifuse!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant