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
Triggered when an email is successfully accepted for delivery
Triggered when an email is successfully delivered to the recipient’s mail server
Triggered when email delivery is temporarily delayed
Triggered when a recipient marks an email as spam
Triggered when an email bounces (hard or soft bounce)
Triggered when a recipient opens an email (requires open tracking)
Triggered when a recipient clicks a link in an email (requires click tracking)
Triggered when an inbound email is received
Triggered when email sending fails
Triggered when a new contact is created
Triggered when a contact is updated
Triggered when a contact is deleted
Domain Events
Triggered when a new domain is created
Triggered when a domain is updated
Triggered when a domain is deleted
Creating a Webhook
Create a webhook to start receiving events at your endpoint.
Basic Webhook
All Email Events
Contact and Domain Events
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.
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.
List All Webhooks
With 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.
Update Endpoint
Update Events
Disable Webhook
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.
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.
Resend includes three important headers with each webhook:
Unique identifier for the webhook message
Unix timestamp when the webhook was sent
HMAC-SHA256 signature for verifying authenticity
Verification Method
The SDK provides a Verify method that implements HMAC-SHA256 signature verification:
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:
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
}
Signature Construction
Constructs the signed content from the webhook data: signedContent = "{id}.{timestamp}.{payload}"
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 ))
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:
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"
}
}
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
Use ngrok for Local Testing
Expose your local server to the internet: 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 },
})
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>" ,
})
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