Send Webhooks with Go: A Complete Tutorial
Learn how to send webhooks with Go using the standard library. This hands-on tutorial covers JSON marshaling, HMAC-SHA256 signatures, HTTP client configuration, timeouts, retries, and production-ready patterns.

Send Webhooks with Go: A Complete Tutorial
Go's standard library provides everything for building webhook delivery systems: HTTP clients, JSON marshaling, and cryptographic functions. This tutorial builds a complete system from scratch, then shows how Hook Mesh simplifies production workloads.
Setting Up a Basic Webhook Sender
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
type WebhookPayload struct {
Event string `json:"event"`
Timestamp time.Time `json:"timestamp"`
Data map[string]interface{} `json:"data"`
}
func sendWebhook(url string, payload WebhookPayload) error {
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload: %w", err)
}
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to send webhook: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("webhook failed with status: %d", resp.StatusCode)
}
return nil
}This basic implementation works but lacks critical features for production use. Let's enhance it step by step.
JSON Marshaling Best Practices
type OrderEvent struct {
Event string `json:"event"`
Timestamp time.Time `json:"timestamp"`
Data OrderData `json:"data"`
}
type OrderData struct {
OrderID string `json:"order_id"`
CustomerID string `json:"customer_id"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Status string `json:"status"`
}
func createOrderPayload(orderID, customerID string, amount float64) ([]byte, error) {
event := OrderEvent{
Event: "order.completed",
Timestamp: time.Now().UTC(),
Data: OrderData{
OrderID: orderID,
CustomerID: customerID,
Amount: amount,
Currency: "USD",
Status: "completed",
},
}
return json.Marshal(event)
}Use UTC timestamps and event types for routing.
Adding HMAC-SHA256 Signatures
HMAC-SHA256 signatures let recipients verify payload authenticity and integrity.
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func generateSignature(payload []byte, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
return hex.EncodeToString(mac.Sum(nil))
}
func sendSignedWebhook(url string, payload []byte, secret string) error {
signature := generateSignature(payload, secret)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Webhook-Signature", signature)
req.Header.Set("X-Webhook-Timestamp", fmt.Sprintf("%d", time.Now().Unix()))
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send webhook: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("webhook failed with status: %d", resp.StatusCode)
}
return nil
}Timestamp inclusion prevents replay attacks—reject old timestamps.
HTTP Client Configuration
Production systems need fine-tuned settings: connection pooling, timeouts, and keep-alives significantly impact performance at scale.
func createWebhookClient() *http.Client {
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
DisableCompression: true,
}
return &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
}Reuse clients—connection reuse reduces latency and resource use.
Context Usage for Timeouts
Use Go's context package for granular control over request lifecycles, timeouts, and cancellation.
import "context"
func sendWebhookWithContext(ctx context.Context, client *http.Client, url string, payload []byte, secret string) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
signature := generateSignature(payload, secret)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Webhook-Signature", signature)
resp, err := client.Do(req)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("webhook timed out")
}
return fmt.Errorf("failed to send webhook: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("webhook failed with status: %d", resp.StatusCode)
}
return nil
}Context timeouts prevent resource leaks in complex workflows.
Implementing Retries with Exponential Backoff
A robust retry mechanism with exponential backoff handles transient failures gracefully.
func sendWebhookWithRetry(ctx context.Context, client *http.Client, url string, payload []byte, secret string, maxRetries int) error {
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
backoff := time.Duration(1<<uint(attempt-1)) * time.Second
select {
case <-time.After(backoff):
case <-ctx.Done():
return fmt.Errorf("context cancelled during retry: %w", ctx.Err())
}
}
err := sendWebhookWithContext(ctx, client, url, payload, secret)
if err == nil {
return nil
}
lastErr = err
// Don't retry client errors (4xx)
if isClientError(err) {
return err
}
}
return fmt.Errorf("webhook failed after %d attempts: %w", maxRetries+1, lastErr)
}
func isClientError(err error) bool {
// Check if error message contains 4xx status codes
errStr := err.Error()
return len(errStr) > 0 &&
(contains(errStr, "status: 4") || contains(errStr, "status code: 4"))
}Limit retries to server errors and network failures. Client errors (4xx) indicate problems with the request itself and won't succeed on retry.
Async Webhook Delivery with Goroutines
Goroutines enable concurrent delivery without blocking your main application flow.
type WebhookJob struct {
URL string
Payload []byte
Secret string
}
func startWebhookWorkers(ctx context.Context, jobs <-chan WebhookJob, workers int) {
client := createWebhookClient()
for i := 0; i < workers; i++ {
go func(workerID int) {
for {
select {
case job, ok := <-jobs:
if !ok {
return
}
err := sendWebhookWithRetry(ctx, client, job.URL, job.Payload, job.Secret, 3)
if err != nil {
log.Printf("Worker %d: webhook failed: %v", workerID, err)
}
case <-ctx.Done():
return
}
}
}(i)
}
}Use buffered channels as job queues with worker pools to prevent overwhelming endpoints.
Error Handling Patterns
Categorize errors to determine appropriate responses and retry behavior.
type WebhookError struct {
Type string
StatusCode int
Message string
Retryable bool
}
func (e *WebhookError) Error() string {
return fmt.Sprintf("%s: %s (status: %d)", e.Type, e.Message, e.StatusCode)
}
func categorizeError(resp *http.Response, err error) *WebhookError {
if err != nil {
return &WebhookError{
Type: "network",
Message: err.Error(),
Retryable: true,
}
}
switch {
case resp.StatusCode >= 500:
return &WebhookError{
Type: "server",
StatusCode: resp.StatusCode,
Message: "recipient server error",
Retryable: true,
}
case resp.StatusCode >= 400:
return &WebhookError{
Type: "client",
StatusCode: resp.StatusCode,
Message: "invalid request",
Retryable: false,
}
default:
return nil
}
}Log errors and expose metrics for monitoring.
Using Hook Mesh for Production Webhooks
Hook Mesh provides retry queues, dead letter storage, monitoring, and rate limiting out of the box.
import "github.com/hookmesh/hookmesh-go"
func main() {
client := hookmesh.NewClient("your-api-key")
event := hookmesh.Event{
Type: "order.completed",
Data: map[string]interface{}{
"order_id": "ord_12345",
"amount": 99.99,
},
}
err := client.Send(context.Background(), event)
if err != nil {
log.Fatal(err)
}
}Hook Mesh handles signature generation, retries, and delivery tracking automatically. The dashboard provides visibility into delivery status, and failed webhooks are stored for replay.
Conclusion
Go's standard library provides everything for webhook delivery systems: JSON marshaling, HMAC-SHA256 signatures, HTTP client configuration, timeouts, retries, and async patterns. The DIY approach works well for development; Hook Mesh handles complexity at scale.
Learn how recipients verify webhooks in Go to understand the full integration flow. For more language tutorials, explore our webhook implementation guides.
Related Posts
Receive and Verify Webhooks in Go
Learn how to securely receive and verify webhooks in Go using HMAC-SHA256 signatures, timestamp validation, and timing-safe comparisons. Complete tutorial with production-ready code examples.
HMAC-SHA256 Webhook Signatures: Implementation Guide
Learn how to implement secure webhook signature verification using HMAC-SHA256. Complete guide with code examples for signing and verifying webhook payloads in Node.js and Python.
Webhook Retry Strategies: Linear vs Exponential Backoff
A technical deep-dive into webhook retry strategies, comparing linear and exponential backoff approaches, with code examples and best practices for building reliable webhook delivery systems.
Send Webhooks with Node.js: A Complete Tutorial
Learn how to send webhooks from your Node.js application. This hands-on tutorial covers payload structure, HMAC signatures, error handling, retry logic, and testing—with complete code examples using modern ES modules and fetch.
Webhook Implementation Guides by Language and Framework
The complete developer hub for webhook implementation tutorials. Find guides for sending, receiving, and verifying webhooks in Node.js, Python, Go, and popular frameworks like Next.js, Django, and Express.