Skip to content

Webhooks

Webhooks provide a way to receive HTTP callbacks when events occur in your inbox. Instead of polling or maintaining SSE connections, your application receives push notifications automatically.

Create a webhook for an inbox to receive notifications when emails arrive:

inbox, err := client.CreateInbox(ctx)
if err != nil {
log.Fatal(err)
}
webhook, err := inbox.CreateWebhook(ctx, "https://your-app.com/webhook/emails",
vaultsandbox.WithWebhookEvents(vaultsandbox.WebhookEventEmailReceived),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Webhook ID: %s\n", webhook.ID)
fmt.Printf("Secret: %s\n", webhook.Secret) // Save this for signature verification
OptionDescription
WithWebhookEvents(events...)Events that trigger the webhook
WithWebhookTemplate(name)Built-in template (slack, discord, teams, etc.)
WithWebhookCustomTemplate(body, contentType)Custom payload template
WithWebhookFilter(filter)Filter which emails trigger the webhook
WithWebhookDescription(desc)Human-readable description
EventConstantDescription
email.receivedWebhookEventEmailReceivedEmail received by the inbox
email.storedWebhookEventEmailStoredEmail successfully stored
email.deletedWebhookEventEmailDeletedEmail deleted from the inbox
response, err := inbox.ListWebhooks(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total webhooks: %d\n", response.Total)
for _, wh := range response.Webhooks {
status := "disabled"
if wh.Enabled {
status = "enabled"
}
fmt.Printf("- %s: %s (%s)\n", wh.ID, wh.URL, status)
}
webhook, err := inbox.GetWebhook(ctx, "webhook-id")
if err != nil {
log.Fatal(err)
}
fmt.Printf("URL: %s\n", webhook.URL)
fmt.Printf("Events: %v\n", webhook.Events)
fmt.Printf("Created: %s\n", webhook.CreatedAt.Format(time.RFC3339))
if webhook.Stats != nil {
fmt.Printf("Deliveries: %d/%d\n",
webhook.Stats.SuccessfulDeliveries,
webhook.Stats.TotalDeliveries)
}
updated, err := inbox.UpdateWebhook(ctx, "webhook-id",
vaultsandbox.WithUpdateURL("https://your-app.com/webhook/v2/emails"),
vaultsandbox.WithUpdateEnabled(true),
vaultsandbox.WithUpdateDescription("Updated webhook endpoint"),
)
if err != nil {
log.Fatal(err)
}
OptionDescription
WithUpdateURL(url)Update the webhook endpoint URL
WithUpdateEvents(events...)Update the event types that trigger the webhook
WithUpdateTemplate(name)Update the built-in template
WithUpdateCustomTemplate(body, contentType)Update with a custom payload template
WithUpdateFilter(filter)Update the filter configuration
WithClearFilter()Remove the filter from the webhook
WithUpdateDescription(desc)Update the description
WithUpdateEnabled(enabled)Enable or disable the webhook
err := inbox.DeleteWebhook(ctx, "webhook-id")
if err != nil {
log.Fatal(err)
}
fmt.Println("Webhook deleted")

Use filters to control which emails trigger webhooks:

webhook, err := inbox.CreateWebhook(ctx, "https://your-app.com/webhook/emails",
vaultsandbox.WithWebhookEvents(vaultsandbox.WebhookEventEmailReceived),
vaultsandbox.WithWebhookFilter(&vaultsandbox.FilterConfig{
Rules: []vaultsandbox.FilterRule{
{Field: "from", Operator: vaultsandbox.FilterOperatorDomain, Value: "example.com"},
{Field: "subject", Operator: vaultsandbox.FilterOperatorContains, Value: "Invoice"},
},
Mode: vaultsandbox.FilterModeAll, // All rules must match
}),
)
OperatorConstantDescription
equalsFilterOperatorEqualsExact match
containsFilterOperatorContainsContains substring
starts_withFilterOperatorStartsWithStarts with string
ends_withFilterOperatorEndsWithEnds with string
domainFilterOperatorDomainEmail domain match
regexFilterOperatorRegexRegular expression match
existsFilterOperatorExistsField exists and is non-empty
ModeConstantDescription
allFilterModeAllAll rules must match (AND)
anyFilterModeAnyAt least one rule must match (OR)
webhook, err := inbox.CreateWebhook(ctx, "https://your-app.com/webhook/emails",
vaultsandbox.WithWebhookEvents(vaultsandbox.WebhookEventEmailReceived),
vaultsandbox.WithWebhookFilter(&vaultsandbox.FilterConfig{
Rules: []vaultsandbox.FilterRule{
{
Field: "subject",
Operator: vaultsandbox.FilterOperatorContains,
Value: "urgent",
CaseSensitive: false, // Case-insensitive match
},
},
Mode: vaultsandbox.FilterModeAll,
}),
)

Only trigger webhooks for authenticated emails:

webhook, err := inbox.CreateWebhook(ctx, "https://your-app.com/webhook/verified-emails",
vaultsandbox.WithWebhookEvents(vaultsandbox.WebhookEventEmailReceived),
vaultsandbox.WithWebhookFilter(&vaultsandbox.FilterConfig{
Rules: []vaultsandbox.FilterRule{},
Mode: vaultsandbox.FilterModeAll,
RequireAuth: true, // Only emails passing SPF/DKIM/DMARC
}),
)

Templates control the webhook payload format.

// Slack-formatted payload
slackWebhook, err := inbox.CreateWebhook(ctx, "https://hooks.slack.com/services/...",
vaultsandbox.WithWebhookEvents(vaultsandbox.WebhookEventEmailReceived),
vaultsandbox.WithWebhookTemplate("slack"),
)
// Discord-formatted payload
discordWebhook, err := inbox.CreateWebhook(ctx, "https://discord.com/api/webhooks/...",
vaultsandbox.WithWebhookEvents(vaultsandbox.WebhookEventEmailReceived),
vaultsandbox.WithWebhookTemplate("discord"),
)
// Microsoft Teams
teamsWebhook, err := inbox.CreateWebhook(ctx, "https://outlook.office.com/webhook/...",
vaultsandbox.WithWebhookEvents(vaultsandbox.WebhookEventEmailReceived),
vaultsandbox.WithWebhookTemplate("teams"),
)
customBody := `{
"email_id": "{{.Email.ID}}",
"sender": "{{.Email.From}}",
"subject_line": "{{.Email.Subject}}",
"received_timestamp": "{{.Email.ReceivedAt}}"
}`
webhook, err := inbox.CreateWebhook(ctx, "https://your-app.com/webhook/emails",
vaultsandbox.WithWebhookEvents(vaultsandbox.WebhookEventEmailReceived),
vaultsandbox.WithWebhookCustomTemplate(customBody, "application/json"),
)

Retrieve the list of available built-in templates:

templates, err := client.GetWebhookTemplates(ctx)
if err != nil {
log.Fatal(err)
}
for _, tmpl := range templates {
fmt.Printf("- %s (%s)\n", tmpl.Label, tmpl.Value)
}
result, err := inbox.TestWebhook(ctx, "webhook-id")
if err != nil {
log.Fatal(err)
}
if result.Success {
fmt.Println("Test successful!")
fmt.Printf("Status: %d\n", result.StatusCode)
fmt.Printf("Response time: %dms\n", result.ResponseTime)
} else {
fmt.Printf("Test failed: %s\n", result.Error)
}
type TestWebhookResponse struct {
Success bool // Whether the test was successful
StatusCode int // HTTP status code returned
ResponseTime int // Response time in milliseconds
Error string // Error message if test failed
RequestID string // Unique identifier for the test request
}

Rotate webhook secrets periodically for security:

result, err := inbox.RotateWebhookSecret(ctx, "webhook-id")
if err != nil {
log.Fatal(err)
}
fmt.Printf("New secret: %s\n", result.Secret)
if result.PreviousSecretValidUntil != nil {
fmt.Printf("Old secret valid until: %s\n", result.PreviousSecretValidUntil.Format(time.RFC3339))
}
// Update your application with the new secret
// The old secret remains valid during the grace period

Always verify webhook signatures in your endpoint. Webhooks include the following headers:

HeaderDescription
X-Vault-SignatureHMAC-SHA256 signature
X-Vault-TimestampUnix timestamp
X-Vault-EventEvent type
X-Vault-DeliveryUnique delivery ID

The signature is computed over ${timestamp}.${raw_request_body}:

package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"strings"
)
func verifyWebhookSignature(rawBody []byte, signature, timestamp, secret string) bool {
signedPayload := fmt.Sprintf("%s.%s", timestamp, string(rawBody))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedPayload))
expectedSignature := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expectedSignature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-Vault-Signature")
timestamp := r.Header.Get("X-Vault-Timestamp")
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
if !verifyWebhookSignature(rawBody, signature, timestamp, webhookSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process the webhook
fmt.Printf("Received webhook: %s\n", string(rawBody))
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
import (
"errors"
"github.com/vaultsandbox/client-go"
)
webhook, err := inbox.GetWebhook(ctx, "webhook-id")
if err != nil {
if errors.Is(err, vaultsandbox.ErrWebhookNotFound) {
fmt.Println("Webhook not found")
} else if errors.Is(err, vaultsandbox.ErrInboxNotFound) {
fmt.Println("Inbox not found")
} else {
var apiErr *vaultsandbox.APIError
if errors.As(err, &apiErr) {
fmt.Printf("API error (%d): %s\n", apiErr.StatusCode, apiErr.Message)
}
}
}
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/vaultsandbox/client-go"
)
func main() {
ctx := context.Background()
client, err := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithBaseURL(os.Getenv("VAULTSANDBOX_URL")),
)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Create inbox
inbox, err := client.CreateInbox(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Inbox: %s\n", inbox.EmailAddress())
// Create webhook with filter
webhook, err := inbox.CreateWebhook(ctx, "https://your-app.com/webhook/emails",
vaultsandbox.WithWebhookEvents(
vaultsandbox.WebhookEventEmailReceived,
vaultsandbox.WebhookEventEmailStored,
),
vaultsandbox.WithWebhookDescription("Production email webhook"),
vaultsandbox.WithWebhookFilter(&vaultsandbox.FilterConfig{
Rules: []vaultsandbox.FilterRule{
{Field: "from", Operator: vaultsandbox.FilterOperatorDomain, Value: "example.com"},
},
Mode: vaultsandbox.FilterModeAll,
}),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Webhook created: %s\n", webhook.ID)
fmt.Printf("Secret: %s\n", webhook.Secret)
// Test the webhook
testResult, err := inbox.TestWebhook(ctx, webhook.ID)
if err != nil {
log.Fatal(err)
}
if testResult.Success {
fmt.Println("Webhook test successful!")
} else {
fmt.Printf("Webhook test failed: %s\n", testResult.Error)
}
// List all webhooks
response, err := inbox.ListWebhooks(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total webhooks: %d\n", response.Total)
// Update webhook
_, err = inbox.UpdateWebhook(ctx, webhook.ID,
vaultsandbox.WithUpdateDescription("Updated description"),
)
if err != nil {
log.Fatal(err)
}
// Cleanup
// err = inbox.DeleteWebhook(ctx, webhook.ID)
// err = inbox.Delete(ctx)
}

The Go client includes support for global webhooks via the Admin() interface:

admin := client.Admin()
// Create a global webhook
webhook, err := admin.CreateWebhook(ctx, "https://your-app.com/webhook/all-emails",
vaultsandbox.WithWebhookEvents(vaultsandbox.WebhookEventEmailReceived),
)
// List global webhooks
response, err := admin.ListWebhooks(ctx)
// Other operations mirror inbox webhooks
webhook, err := admin.GetWebhook(ctx, "webhook-id")
updated, err := admin.UpdateWebhook(ctx, "webhook-id", vaultsandbox.WithUpdateEnabled(false))
result, err := admin.TestWebhook(ctx, "webhook-id")
rotated, err := admin.RotateWebhookSecret(ctx, "webhook-id")
err := admin.DeleteWebhook(ctx, "webhook-id")

Global webhooks use the same options and response types as inbox webhooks. The main difference is they have WebhookScopeGlobal and receive events from all inboxes rather than a specific one.

Monitor webhook health and delivery statistics across your account:

metrics, err := client.GetWebhookMetrics(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total webhooks: %d (active: %d)\n", metrics.TotalWebhooks, metrics.ActiveWebhooks)
fmt.Printf("Deliveries: %d successful, %d failed (%.1f%% success rate)\n",
metrics.SuccessfulDeliveries,
metrics.FailedDeliveries,
metrics.SuccessRate,
)
// Breakdown by scope
for scope, count := range metrics.ByScope {
fmt.Printf(" %s: %d\n", scope, count)
}
// Breakdown by event type
for event, count := range metrics.ByEvent {
fmt.Printf(" %s: %d\n", event, count)
}
type WebhookMetrics struct {
TotalWebhooks int // Total registered webhooks
ActiveWebhooks int // Enabled webhooks
TotalDeliveries int // All delivery attempts
SuccessfulDeliveries int // Successful deliveries
FailedDeliveries int // Failed deliveries
SuccessRate float64 // Success percentage
ByScope map[string]int // Counts by scope (global/inbox)
ByEvent map[string]int // Counts by event type
}
FeatureWebhooksSSEPolling
DeliveryPush to your serverPush to clientPull from client
ConnectionNone requiredPersistentRepeated requests
LatencyNear real-timeReal-timeDepends on interval
Server requiredYes (webhook endpoint)NoNo
Firewall friendlyYesUsuallyYes
Best forServer-to-serverBrowser/client appsSimple integrations