Skip to content

fix(growth): accept Resend webhook payloads that carry a headers array; log every outcome - #991

Merged
blove merged 2 commits into
mainfrom
blove/resend-webhook-headers
Sep 3, 2026
Merged

fix(growth): accept Resend webhook payloads that carry a headers array; log every outcome#991
blove merged 2 commits into
mainfrom
blove/resend-webhook-headers

Conversation

@blove

@blove blove commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #989. Resend's webhook event log (GET /webhooks/{id}/events/{event}) showed the real recipient payload: every recipient message carries List-Unsubscribe, List-Unsubscribe-Post, X-Threadplane-Job-ID, Bcc, and Reply-To, and Resend echoes them as a data.headers array. The closed parser rejected the key, the route answered 400, and Resend marked those events failed with next_attempt_at: null. Founder notifications carry no custom headers, which is why only those events succeeded.

Change

  • libs/growth/src/lib/webhooks.ts: validate the known keys plus a bounded headers array of {name, value}; ignore unknown keys within a 32-key bound instead of failing the event.
  • apps/website/src/app/api/webhooks/resend/route.ts: one structured console.info line per call with status, applied, and reason, so attempts are visible in Vercel runtime logs.

Tests: the captured production payload shape applies; unknown keys are ignored; an unbounded key count still fails closed. Growth lint/test pass (42 webhook tests); the website route spec passes (10).

After deploy the failed events need replaying from Resend (they will not retry on their own).

🤖 Generated with Claude Code

…y; log every outcome

Resend's webhook event log showed why recipient delivery state never
left "submitted": every recipient message carries List-Unsubscribe, the
job id, Bcc, and Reply-To, and Resend echoes them back as a data.headers
array. The closed parser rejected the key, the route answered 400, and
Resend marked the events failed with no further attempts. Founder
notifications have no custom headers, which is why only those succeeded.

The parser now validates the known keys and a bounded headers array, and
ignores unknown keys within a bounded key count instead of failing the
event. Two drifts in one day showed the closed set was hurting more than
it protected. The route now logs one structured line per call with the
status and reason so future attempts are visible in Vercel logs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@blove
blove enabled auto-merge (squash) September 3, 2026 21:12
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
threadplane Ready Ready Preview Sep 3, 2026 9:23pm UTC

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove merged commit e64d6e9 into main Sep 3, 2026
31 checks passed
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