SkillsMay 11, 2026·4 min read

Resend Webhooks — Email Delivery Events for AI Agents

Resend webhooks fire on delivery, open, click, bounce, complaint. Route to your agent for follow-ups, deliverability monitoring, cleanup.

Agent ready

Safe staging for this asset

This asset is staged first. The copied prompt tells the agent to inspect the staged files and ask before activating scripts, MCP config, or global config.

Stage only · 29/100Policy: stage
Agent surface
Any MCP/CLI agent
Kind
Skill
Install
Stage only
Trust
Trust: Community
Entrypoint
Asset
Safe staging command
npx -y tokrepo@latest install e60ca183-5be1-4d16-ae96-2b7b3bedba46 --target codex

Stages files first; activation requires review of the staged README and plan.

Intro

Resend fires webhooks for every email lifecycle event — delivered, opened, clicked, bounced, complained, delivery_delayed. Wire them to your AI agent and you can trigger automated follow-ups when an email opens, suppress recipients on hard bounce, or alert when complaint rates spike. Best for: cold outreach agents, transactional pipelines that need bounce hygiene, marketing automations with engagement-based branching. Works with: any HTTPS endpoint, Resend ≥ 2024. Setup time: 15 minutes.


FastAPI webhook endpoint

import os, hmac, hashlib, json
from fastapi import FastAPI, Request, HTTPException
from anthropic import Anthropic

app = FastAPI()
SIGNING_SECRET = os.environ["RESEND_WEBHOOK_SECRET"].encode()
claude = Anthropic()

def verify(payload: bytes, sig_header: str) -> bool:
    expected = hmac.new(SIGNING_SECRET, payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig_header)

EVENT_HANDLERS = {
    "email.opened":    "schedule_followup",
    "email.clicked":   "log_engagement",
    "email.bounced":   "suppress_recipient",
    "email.complained":"alert_compliance",
}

@app.post("/resend/webhook")
async def resend_webhook(request: Request):
    payload = await request.body()
    sig = request.headers.get("svix-signature") or ""
    if not verify(payload, sig.split(",")[-1]):
        raise HTTPException(401, "bad signature")

    event = json.loads(payload)
    handler = EVENT_HANDLERS.get(event["type"])
    if not handler:
        return {"status": "ignored"}

    # Hand off to Claude for next-step decision
    claude.messages.create(
        model="claude-3-5-haiku-20241022",
        max_tokens=512,
        tools=[{"name": handler, "input_schema": {"type": "object", "properties": {
            "to": {"type": "string"}, "subject": {"type": "string"}, "campaign_id": {"type": "string"},
        }}}],
        messages=[{"role": "user", "content": f"Resend event: {event['type']}\n{event['data']}"}],
    )
    return {"status": "handled"}

Configure the webhook

In Resend dashboard → Webhooks → Add endpoint:

  • URL: https://your-app.com/resend/webhook
  • Events: select email.* (delivered, opened, clicked, bounced, complained, delivery_delayed)
  • Save and copy the signing secret → put in RESEND_WEBHOOK_SECRET env var

Event payload structure

{
  "type": "email.opened",
  "created_at": "2026-05-11T14:23:00Z",
  "data": {
    "email_id": "8d22a0b1-...-...-...",
    "to": ["user@example.com"],
    "from": "TokRepo <hi@tokrepo.com>",
    "subject": "Welcome to TokRepo",
    "tags": [{"name": "campaign", "value": "welcome-2026-05"}]
  }
}

Production hygiene

  • Hard bounces → mark recipient suppressed immediately. Sending again hurts your domain reputation.
  • Complaints → suppress AND lower send volume for the source campaign.
  • Delivery delays → usually transient. Alert only on >24h delay or repeating recipients.
  • Idempotency → store event['data']['email_id'] + event['type'] to dedupe retries.

FAQ

Q: Open tracking — is it accurate? A: Opens fire on tracking-pixel load. Image-blocking clients (Apple Mail Privacy Protection, many corporate clients) inflate or null open counts. Use opens as a directional signal, not a precise metric. Clicks are far more reliable.

Q: Why use webhooks vs polling the API? A: Webhooks are real-time; polling burns rate limit and adds latency. The Resend Emails API is fine for status-on-demand (emails.retrieve) but webhooks are the right primitive for event-driven agent flows.

Q: How do I test webhooks locally? A: Use ngrok http 8000 to tunnel; paste the ngrok URL into Resend webhook config. Resend dashboard has a 'Send test event' button to replay payloads without sending real email.


Quick Use

  1. Add endpoint at resend.com/webhooks, select email.* events, copy signing secret
  2. Build /resend/webhook handler that verifies svix-signature
  3. Route by event.type to suppress / followup / alert

Intro

Resend fires webhooks for every email lifecycle event — delivered, opened, clicked, bounced, complained, delivery_delayed. Wire them to your AI agent and you can trigger automated follow-ups when an email opens, suppress recipients on hard bounce, or alert when complaint rates spike. Best for: cold outreach agents, transactional pipelines that need bounce hygiene, marketing automations with engagement-based branching. Works with: any HTTPS endpoint, Resend ≥ 2024. Setup time: 15 minutes.


FastAPI webhook endpoint

import os, hmac, hashlib, json
from fastapi import FastAPI, Request, HTTPException
from anthropic import Anthropic

app = FastAPI()
SIGNING_SECRET = os.environ["RESEND_WEBHOOK_SECRET"].encode()
claude = Anthropic()

def verify(payload: bytes, sig_header: str) -> bool:
    expected = hmac.new(SIGNING_SECRET, payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig_header)

EVENT_HANDLERS = {
    "email.opened":    "schedule_followup",
    "email.clicked":   "log_engagement",
    "email.bounced":   "suppress_recipient",
    "email.complained":"alert_compliance",
}

@app.post("/resend/webhook")
async def resend_webhook(request: Request):
    payload = await request.body()
    sig = request.headers.get("svix-signature") or ""
    if not verify(payload, sig.split(",")[-1]):
        raise HTTPException(401, "bad signature")

    event = json.loads(payload)
    handler = EVENT_HANDLERS.get(event["type"])
    if not handler:
        return {"status": "ignored"}

    # Hand off to Claude for next-step decision
    claude.messages.create(
        model="claude-3-5-haiku-20241022",
        max_tokens=512,
        tools=[{"name": handler, "input_schema": {"type": "object", "properties": {
            "to": {"type": "string"}, "subject": {"type": "string"}, "campaign_id": {"type": "string"},
        }}}],
        messages=[{"role": "user", "content": f"Resend event: {event['type']}\n{event['data']}"}],
    )
    return {"status": "handled"}

Configure the webhook

In Resend dashboard → Webhooks → Add endpoint:

  • URL: https://your-app.com/resend/webhook
  • Events: select email.* (delivered, opened, clicked, bounced, complained, delivery_delayed)
  • Save and copy the signing secret → put in RESEND_WEBHOOK_SECRET env var

Event payload structure

{
  "type": "email.opened",
  "created_at": "2026-05-11T14:23:00Z",
  "data": {
    "email_id": "8d22a0b1-...-...-...",
    "to": ["user@example.com"],
    "from": "TokRepo <hi@tokrepo.com>",
    "subject": "Welcome to TokRepo",
    "tags": [{"name": "campaign", "value": "welcome-2026-05"}]
  }
}

Production hygiene

  • Hard bounces → mark recipient suppressed immediately. Sending again hurts your domain reputation.
  • Complaints → suppress AND lower send volume for the source campaign.
  • Delivery delays → usually transient. Alert only on >24h delay or repeating recipients.
  • Idempotency → store event['data']['email_id'] + event['type'] to dedupe retries.

FAQ

Q: Open tracking — is it accurate? A: Opens fire on tracking-pixel load. Image-blocking clients (Apple Mail Privacy Protection, many corporate clients) inflate or null open counts. Use opens as a directional signal, not a precise metric. Clicks are far more reliable.

Q: Why use webhooks vs polling the API? A: Webhooks are real-time; polling burns rate limit and adds latency. The Resend Emails API is fine for status-on-demand (emails.retrieve) but webhooks are the right primitive for event-driven agent flows.

Q: How do I test webhooks locally? A: Use ngrok http 8000 to tunnel; paste the ngrok URL into Resend webhook config. Resend dashboard has a 'Send test event' button to replay payloads without sending real email.


Source & Thanks

Built by Resend. Webhook docs at resend.com/docs/dashboard/webhooks/introduction.

resend/resend-node — official SDK

🙏

Source & Thanks

Built by Resend. Webhook docs at resend.com/docs/dashboard/webhooks/introduction.

resend/resend-node — official SDK

Discussion

Sign in to join the discussion.
No comments yet. Be the first to share your thoughts.

Related Assets