Skip to main content
Webhooks allow you to receive real-time notifications about email events in your application. This guide covers creating, managing, and verifying webhooks with the Resend Go SDK.

Overview

Resend webhooks notify your application when events occur, such as:
  • Emails being sent, delivered, or bounced
  • Contacts being created, updated, or deleted
  • Domains being created, updated, or deleted
The Go SDK provides built-in webhook signature verification using HMAC-SHA256 to ensure webhook authenticity.

Webhook Events

Resend supports the following webhook events:

Email Events

email.sent
string
Triggered when an email is successfully accepted for delivery
email.delivered
string
Triggered when an email is successfully delivered to the recipient’s mail server
email.delivery_delayed
string
Triggered when email delivery is temporarily delayed
email.complained
string
Triggered when a recipient marks an email as spam
email.bounced
string
Triggered when an email bounces (hard or soft bounce)
email.opened
string
Triggered when a recipient opens an email (requires open tracking)
email.clicked
string
Triggered when a recipient clicks a link in an email (requires click tracking)
email.received
string
Triggered when an inbound email is received
email.failed
string
Triggered when email sending fails

Contact Events

contact.created
string
Triggered when a new contact is created
contact.updated
string
Triggered when a contact is updated
contact.deleted
string
Triggered when a contact is deleted

Domain Events

domain.created
string
Triggered when a new domain is created
domain.updated
string
Triggered when a domain is updated
domain.deleted
string
Triggered when a domain is deleted

Creating a Webhook

Create a webhook to start receiving events at your endpoint.
import (
    "github.com/resend/resend-go/v3"
)

client := resend.NewClient("re_123456789")

params := &resend.CreateWebhookRequest{
    Endpoint: "https://your-app.com/webhooks/resend",
    Events: []string{
        resend.EventEmailSent,
        resend.EventEmailDelivered,
        resend.EventEmailBounced,
    },
}

webhook, err := client.Webhooks.Create(params)
if err != nil {
    panic(err)
}

fmt.Println("Webhook ID:", webhook.Id)
fmt.Println("Signing Secret:", webhook.SigningSecret)
Save the signing secret! The SigningSecret is only returned when creating a webhook. Store it securely - you’ll need it to verify webhook signatures.

Retrieving a Webhook

Get details about a specific webhook.
Get Webhook
webhook, err := client.Webhooks.Get("wh_123456")
if err != nil {
    panic(err)
}

fmt.Println("Status:", webhook.Status)
fmt.Println("Endpoint:", webhook.Endpoint)
fmt.Println("Events:", webhook.Events)
fmt.Println("Created:", webhook.CreatedAt)

Listing Webhooks

Retrieve all webhooks in your account with optional pagination.
webhooks, err := client.Webhooks.List()
if err != nil {
    panic(err)
}

fmt.Printf("You have %d webhook(s)\n", len(webhooks.Data))

for i, wh := range webhooks.Data {
    fmt.Printf("[%d] %s - %s\n", i+1, wh.Id, wh.Status)
    fmt.Printf("    Endpoint: %s\n", wh.Endpoint)
    fmt.Printf("    Events: %v\n", wh.Events)
}

Updating a Webhook

Update webhook configuration including endpoint, events, and status.
newEndpoint := "https://new-api.example.com/webhook"

updateParams := &resend.UpdateWebhookRequest{
    Endpoint: &newEndpoint,
}

updated, err := client.Webhooks.Update("wh_123456", updateParams)
if err != nil {
    panic(err)
}

fmt.Println("Updated webhook:", updated.Id)
Disable webhooks temporarily instead of deleting them if you need to pause event notifications.

Deleting a Webhook

Remove a webhook from your account.
Delete Webhook
deleted, err := client.Webhooks.Remove("wh_123456")
if err != nil {
    panic(err)
}

fmt.Printf("Webhook %s deleted: %v\n", deleted.Id, deleted.Deleted)

Receiving and Verifying Webhooks

Webhooks are sent as POST requests to your endpoint. Always verify webhook signatures to ensure they’re from Resend.

Webhook Headers

Resend includes three important headers with each webhook:
svix-id
string
Unique identifier for the webhook message
svix-timestamp
string
Unix timestamp when the webhook was sent
svix-signature
string
HMAC-SHA256 signature for verifying authenticity

Verification Method

The SDK provides a Verify method that implements HMAC-SHA256 signature verification:
Webhook Receiver
import (
    "encoding/json"
    "io"
    "log"
    "net/http"
    
    "github.com/resend/resend-go/v3"
)

func webhookHandler(client *resend.Client, webhookSecret string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 1. Read the raw request body
        body, err := io.ReadAll(r.Body)
        if err != nil {
            log.Printf("Error reading body: %v", err)
            http.Error(w, "Failed to read request body", http.StatusBadRequest)
            return
        }
        defer r.Body.Close()

        // 2. Extract webhook headers
        headers := resend.WebhookHeaders{
            Id:        r.Header.Get("svix-id"),
            Timestamp: r.Header.Get("svix-timestamp"),
            Signature: r.Header.Get("svix-signature"),
        }

        // 3. Verify the webhook signature
        err = client.Webhooks.Verify(&resend.VerifyWebhookOptions{
            Payload:       string(body),
            Headers:       headers,
            WebhookSecret: webhookSecret,
        })

        if err != nil {
            log.Printf("Webhook verification failed: %v", err)
            http.Error(w, "Webhook verification failed", http.StatusBadRequest)
            return
        }

        // 4. Parse the verified payload
        var payload map[string]any
        if err := json.Unmarshal(body, &payload); err != nil {
            log.Printf("Error parsing JSON: %v", err)
            http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
            return
        }

        // 5. Process the webhook event
        eventType := payload["type"].(string)
        log.Printf("✓ Verified webhook: %s", eventType)
        
        // Handle different event types
        switch eventType {
        case resend.EventEmailSent:
            handleEmailSent(payload)
        case resend.EventEmailDelivered:
            handleEmailDelivered(payload)
        case resend.EventEmailBounced:
            handleEmailBounced(payload)
        default:
            log.Printf("Unhandled event type: %s", eventType)
        }

        // 6. Respond with success
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(map[string]bool{"success": true})
    }
}

How Verification Works

The Verify method implements the following security checks:
1

Timestamp Validation

Verifies the webhook timestamp is within 5 minutes to prevent replay attacks.
// Checks if timestamp is within tolerance window
diff := now - timestamp
if diff > 300 seconds {
    return error
}
2

Signature Construction

Constructs the signed content from the webhook data:
signedContent = "{id}.{timestamp}.{payload}"
3

HMAC-SHA256 Calculation

Computes the expected signature using your webhook secret:
h := hmac.New(sha256.New, decodedSecret)
h.Write([]byte(signedContent))
expectedSignature := base64.StdEncoding.EncodeToString(h.Sum(nil))
4

Constant-Time Comparison

Safely compares signatures to prevent timing attacks:
if subtle.ConstantTimeCompare(expected, received) == 1 {
    return nil // Valid
}

Complete HTTP Server Example

Here’s a complete example of a webhook receiver server:
Webhook Server
package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    
    "github.com/resend/resend-go/v3"
)

func main() {
    webhookSecret := os.Getenv("RESEND_WEBHOOK_SECRET")
    apiKey := os.Getenv("RESEND_API_KEY")
    
    client := resend.NewClient(apiKey)

    http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
            return
        }

        // Read raw body (required for signature verification)
        body, err := io.ReadAll(r.Body)
        if err != nil {
            log.Printf("Error reading body: %v", err)
            http.Error(w, "Failed to read request body", http.StatusBadRequest)
            return
        }
        defer r.Body.Close()

        // Extract Svix headers
        headers := resend.WebhookHeaders{
            Id:        r.Header.Get("svix-id"),
            Timestamp: r.Header.Get("svix-timestamp"),
            Signature: r.Header.Get("svix-signature"),
        }

        // Verify the webhook
        err = client.Webhooks.Verify(&resend.VerifyWebhookOptions{
            Payload:       string(body),
            Headers:       headers,
            WebhookSecret: webhookSecret,
        })

        if err != nil {
            log.Printf("Webhook verification failed: %v", err)
            http.Error(w, "Webhook verification failed", http.StatusBadRequest)
            return
        }

        // Parse the verified payload
        var payload map[string]any
        if err := json.Unmarshal(body, &payload); err != nil {
            log.Printf("Error parsing JSON: %v", err)
            http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
            return
        }

        log.Printf("✓ Webhook verified successfully!")
        log.Printf("Event Type: %v", payload["type"])
        
        // Extract common fields
        eventType, _ := payload["type"].(string)
        data, _ := payload["data"].(map[string]any)
        
        // Handle specific events
        switch eventType {
        case resend.EventEmailSent:
            emailId, _ := data["email_id"].(string)
            log.Printf("Email sent: %s", emailId)
            
        case resend.EventEmailDelivered:
            emailId, _ := data["email_id"].(string)
            log.Printf("Email delivered: %s", emailId)
            
        case resend.EventEmailBounced:
            emailId, _ := data["email_id"].(string)
            bounceType, _ := data["bounce_type"].(string)
            log.Printf("Email bounced: %s (type: %s)", emailId, bounceType)
            
        case resend.EventContactCreated:
            contactId, _ := data["contact_id"].(string)
            email, _ := data["email"].(string)
            log.Printf("Contact created: %s (%s)", contactId, email)
            
        default:
            log.Printf("Unhandled event: %s", eventType)
        }

        // Respond with success
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(map[string]bool{"success": true})
    })

    port := ":5000"
    fmt.Printf("🚀 Webhook receiver listening on http://localhost%s/webhook\n", port)
    
    if err := http.ListenAndServe(port, nil); err != nil {
        log.Fatal(err)
    }
}

Event Payload Examples

Here are examples of webhook payloads for different events:
{
  "type": "email.sent",
  "created_at": "2024-01-15T10:30:00.000Z",
  "data": {
    "email_id": "4ef2e6e9-8e0a-4c3e-9c42-3c1e3b7a4f3e",
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Welcome to our platform"
  }
}
{
  "type": "email.delivered",
  "created_at": "2024-01-15T10:31:00.000Z",
  "data": {
    "email_id": "4ef2e6e9-8e0a-4c3e-9c42-3c1e3b7a4f3e",
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Welcome to our platform"
  }
}
{
  "type": "email.bounced",
  "created_at": "2024-01-15T10:31:30.000Z",
  "data": {
    "email_id": "4ef2e6e9-8e0a-4c3e-9c42-3c1e3b7a4f3e",
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Welcome to our platform",
    "bounce_type": "hard",
    "bounce_reason": "Mailbox does not exist"
  }
}
{
  "type": "contact.created",
  "created_at": "2024-01-15T10:32:00.000Z",
  "data": {
    "contact_id": "c4f8e2a1-7b3c-4d5e-9f8a-1b2c3d4e5f6a",
    "email": "[email protected]",
    "first_name": "John",
    "last_name": "Doe"
  }
}

Error Handling

Robust Webhook Verification
func verifyWebhook(client *resend.Client, secret string, r *http.Request) error {
    body, err := io.ReadAll(r.Body)
    if err != nil {
        return fmt.Errorf("failed to read body: %w", err)
    }

    headers := resend.WebhookHeaders{
        Id:        r.Header.Get("svix-id"),
        Timestamp: r.Header.Get("svix-timestamp"),
        Signature: r.Header.Get("svix-signature"),
    }

    // Verify all required headers are present
    if headers.Id == "" || headers.Timestamp == "" || headers.Signature == "" {
        return fmt.Errorf("missing required webhook headers")
    }

    // Verify the signature
    err = client.Webhooks.Verify(&resend.VerifyWebhookOptions{
        Payload:       string(body),
        Headers:       headers,
        WebhookSecret: secret,
    })

    if err != nil {
        return fmt.Errorf("verification failed: %w", err)
    }

    return nil
}

Best Practices

Always Verify Signatures

Never process webhooks without verifying their signature to prevent spoofing attacks.

Use HTTPS Endpoints

Always use HTTPS endpoints for webhooks to ensure data is encrypted in transit.

Respond Quickly

Return a 200 OK response immediately and process events asynchronously.

Handle Retries

Implement idempotency to handle duplicate webhook deliveries gracefully.

Store Signing Secrets Securely

Store webhook secrets in environment variables or secret management systems.

Monitor Webhook Health

Log webhook events and set up alerts for verification failures.

Testing Webhooks

1

Use ngrok for Local Testing

Expose your local server to the internet:
ngrok http 5000
Then create a webhook with the ngrok URL:
webhook, err := client.Webhooks.Create(&resend.CreateWebhookRequest{
    Endpoint: "https://your-ngrok-url.ngrok.io/webhook",
    Events:   []string{resend.EventEmailSent},
})
2

Send Test Emails

Trigger webhook events by sending test emails:
_, err := client.Emails.Send(&resend.SendEmailRequest{
    From:    "[email protected]",
    To:      []string{"[email protected]"},
    Subject: "Test Webhook",
    Html:    "<p>Testing webhooks</p>",
})
3

Monitor Logs

Watch your server logs to see webhook events arriving:
 Webhook verified successfully!
Event Type: email.sent
Email sent: 4ef2e6e9-8e0a-4c3e-9c42-3c1e3b7a4f3e

Next Steps

Email Events

Learn about sending emails and triggering events

Broadcasts

Send campaigns and receive broadcast events

Webhook Example

View the complete webhook receiver example

API Reference

View the complete Webhooks API reference

Build docs developers (and LLMs) love