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 = await client.create_inbox()
webhook = await inbox.create_webhook(
url="https://your-app.com/webhook/emails",
events=["email.received"],
)
print(f"Webhook ID: {webhook.id}")
print(f"Secret: {webhook.secret}") # Save this for signature verification
webhook = await inbox.create_webhook(
url="https://your-app.com/webhook/emails",
events=["email.received"],
template="slack", # Optional: payload format
filter=FilterConfig(...), # Optional: filter events
description="Production email notifications", # Optional
)
ParameterTypeRequiredDescription
urlstrYesThe URL to send webhook requests to
eventslist[str]YesEvents that trigger the webhook
templatestr | CustomTemplateNoPayload format template
filterFilterConfigNoFilter which emails trigger the webhook
descriptionstrNoHuman-readable description
EventDescription
email.receivedEmail received by the inbox
email.storedEmail successfully stored
email.deletedEmail deleted from the inbox
webhooks = await inbox.list_webhooks()
print(f"Total webhooks: {len(webhooks)}")
for webhook in webhooks:
status = "enabled" if webhook.enabled else "disabled"
print(f"- {webhook.id}: {webhook.url} ({status})")
webhook = await inbox.get_webhook("whk_abc123")
print(f"URL: {webhook.url}")
print(f"Events: {', '.join(webhook.events)}")
print(f"Created: {webhook.created_at}")
print(f"Last delivery: {webhook.last_delivery_at or 'Never'}")
if webhook.stats:
print(f"Deliveries: {webhook.stats.successful_deliveries}/{webhook.stats.total_deliveries}")
webhook = await inbox.get_webhook("whk_abc123")
await webhook.update(
url="https://your-app.com/webhook/v2/emails",
enabled=True,
description="Updated webhook endpoint",
)

All available update parameters:

await webhook.update(
url="https://new-url.com/webhook", # New target URL
events=["email.received", "email.stored"], # New event subscriptions
template="slack", # New template
remove_template=True, # Remove template (set instead of template)
filter=FilterConfig(...), # New filter config
remove_filter=True, # Remove filter (set instead of filter)
description="New description", # New description
enabled=True, # Enable/disable
)
webhook = await inbox.get_webhook("whk_abc123")
# Disable webhook temporarily
await webhook.disable()
# Re-enable webhook
await webhook.enable()
webhook = await inbox.get_webhook("whk_abc123")
# Refresh webhook data from server
await webhook.refresh()
print(f"Latest stats: {webhook.stats.successful_deliveries} successful")
await inbox.delete_webhook("whk_abc123")
print("Webhook deleted")

Use filters to control which emails trigger webhooks:

from vaultsandbox import FilterConfig, FilterRule
webhook = await inbox.create_webhook(
url="https://your-app.com/webhook/emails",
events=["email.received"],
filter=FilterConfig(
rules=[
FilterRule(field="from.address", operator="domain", value="example.com"),
FilterRule(field="subject", operator="contains", value="Invoice"),
],
mode="all", # "all" = AND, "any" = OR
),
)
FieldDescription
subjectEmail subject line
from.addressSender email address
from.nameSender display name
to.addressRecipient email address
to.nameRecipient display name
body.textPlain text body
body.htmlHTML body
header.X-*Custom email headers (see below)
OperatorDescriptionExample
equalsExact matchFilterRule(field="from.address", operator="equals", value="noreply@example.com")
containsContains substringFilterRule(field="subject", operator="contains", value="Reset")
starts_withStarts with stringFilterRule(field="subject", operator="starts_with", value="RE:")
ends_withEnds with stringFilterRule(field="from.address", operator="ends_with", value="@company.com")
domainEmail domain matchFilterRule(field="from.address", operator="domain", value="example.com")
regexRegular expression matchFilterRule(field="subject", operator="regex", value=r"Order #\d+")
existsField exists and is non-emptyFilterRule(field="attachments", operator="exists", value="true")

Filter on custom email headers using the header.X-* format:

webhook = await inbox.create_webhook(
url="https://your-app.com/webhook/emails",
events=["email.received"],
filter=FilterConfig(
rules=[
# Filter by custom header
FilterRule(
field="header.X-Priority",
operator="equals",
value="1",
),
# Filter by mailing list header
FilterRule(
field="header.List-Unsubscribe",
operator="exists",
value="true",
),
],
mode="any",
),
)
webhook = await inbox.create_webhook(
url="https://your-app.com/webhook/emails",
events=["email.received"],
filter=FilterConfig(
rules=[
FilterRule(
field="subject",
operator="contains",
value="urgent",
case_sensitive=False, # Case-insensitive match
),
],
mode="all",
),
)

Only trigger webhooks for authenticated emails:

webhook = await inbox.create_webhook(
url="https://your-app.com/webhook/verified-emails",
events=["email.received"],
filter=FilterConfig(
rules=[],
mode="all",
require_auth=True, # Only emails passing SPF/DKIM/DMARC
),
)

Templates control the webhook payload format:

TemplateDescription
defaultFull email data in standard JSON format
slackSlack-compatible message blocks
discordDiscord webhook embed format
teamsMicrosoft Teams adaptive card
simpleMinimal payload with essential fields only
notificationPush notification-friendly compact format
zapierZapier-optimized flat structure
# Slack-formatted payload
slack_webhook = await inbox.create_webhook(
url="https://hooks.slack.com/services/...",
events=["email.received"],
template="slack",
)
# Discord-formatted payload
discord_webhook = await inbox.create_webhook(
url="https://discord.com/api/webhooks/...",
events=["email.received"],
template="discord",
)
# Microsoft Teams
teams_webhook = await inbox.create_webhook(
url="https://outlook.office.com/webhook/...",
events=["email.received"],
template="teams",
)
# Simple payload for lightweight integrations
simple_webhook = await inbox.create_webhook(
url="https://your-app.com/webhook/emails",
events=["email.received"],
template="simple",
)
# Zapier integration
zapier_webhook = await inbox.create_webhook(
url="https://hooks.zapier.com/...",
events=["email.received"],
template="zapier",
)
from vaultsandbox import CustomTemplate
import json
webhook = await inbox.create_webhook(
url="https://your-app.com/webhook/emails",
events=["email.received"],
template=CustomTemplate(
body=json.dumps({
"email_id": "{{email.id}}",
"sender": "{{email.from}}",
"subject_line": "{{email.subject}}",
"received_timestamp": "{{email.receivedAt}}",
}),
content_type="application/json",
),
)
webhook = await inbox.get_webhook("whk_abc123")
result = await webhook.test()
if result.success:
print(f"Test successful!")
print(f"Status: {result.status_code}")
print(f"Response time: {result.response_time}ms")
else:
print(f"Test failed: {result.error}")
@dataclass
class TestWebhookResult:
success: bool
status_code: int | None = None
response_time: int | None = None
response_body: str | None = None
error: str | None = None
payload_sent: Any | None = None

Rotate webhook secrets periodically for security:

webhook = await inbox.get_webhook("whk_abc123")
result = await webhook.rotate_secret()
print(f"New secret: {result.secret}")
print(f"Old secret valid until: {result.previous_secret_valid_until}")
# 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 (format: sha256=<hex>)
X-Vault-TimestampUnix timestamp
X-Vault-EventEvent type
X-Vault-DeliveryUnique delivery ID

The signature is computed over {timestamp}.{raw_request_body} and sent with a sha256= prefix:

from vaultsandbox import verify_webhook_signature, WebhookSignatureVerificationError
WEBHOOK_SECRET = "whsec_..."
# Flask example
@app.route("/webhook/emails", methods=["POST"])
def handle_webhook():
raw_body = request.get_data(as_text=True)
signature = request.headers.get("X-Vault-Signature")
timestamp = request.headers.get("X-Vault-Timestamp")
try:
verify_webhook_signature(raw_body, signature, timestamp, WEBHOOK_SECRET)
except WebhookSignatureVerificationError:
return "Invalid signature", 401
# Process the webhook
event = request.get_json()
print(f"Email received: {event}")
return "OK", 200
from fastapi import FastAPI, Request, HTTPException
from vaultsandbox import verify_webhook_signature, WebhookSignatureVerificationError
app = FastAPI()
WEBHOOK_SECRET = "whsec_..."
@app.post("/webhook/emails")
async def handle_webhook(request: Request):
raw_body = await request.body()
signature = request.headers.get("X-Vault-Signature")
timestamp = request.headers.get("X-Vault-Timestamp")
try:
verify_webhook_signature(raw_body, signature, timestamp, WEBHOOK_SECRET)
except WebhookSignatureVerificationError:
raise HTTPException(status_code=401, detail="Invalid signature")
# Process the webhook
event = await request.json()
print(f"Email received: {event}")
return {"status": "ok"}
import json
from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from vaultsandbox import verify_webhook_signature, WebhookSignatureVerificationError
WEBHOOK_SECRET = "whsec_..."
@csrf_exempt
def handle_webhook(request):
raw_body = request.body.decode("utf-8")
signature = request.headers.get("X-Vault-Signature")
timestamp = request.headers.get("X-Vault-Timestamp")
try:
verify_webhook_signature(raw_body, signature, timestamp, WEBHOOK_SECRET)
except WebhookSignatureVerificationError:
return HttpResponseForbidden("Invalid signature")
# Process the webhook
event = json.loads(raw_body)
print(f"Email received: {event}")
return HttpResponse("OK")

The verify_webhook_signature function accepts an optional tolerance_seconds parameter:

verify_webhook_signature(
raw_body,
signature,
timestamp,
WEBHOOK_SECRET,
tolerance_seconds=300, # Default: 5 minutes
)

Set tolerance_seconds=0 to disable timestamp validation (not recommended for production).

Check if a webhook timestamp is within the tolerance window without verifying the full signature:

from vaultsandbox import is_timestamp_valid
timestamp = request.headers.get("X-Vault-Timestamp")
if not is_timestamp_valid(timestamp):
return "Timestamp expired", 401
if not is_timestamp_valid(timestamp, tolerance_seconds=60):
return "Timestamp outside 60-second window", 401

Parse and validate the webhook payload structure:

import json
from vaultsandbox import verify_webhook_signature, construct_webhook_event
# After verifying signature
verify_webhook_signature(raw_body, signature, timestamp, WEBHOOK_SECRET)
# Parse and validate payload structure
event = construct_webhook_event(json.loads(raw_body))
# Event structure:
# {
# "id": "evt_...",
# "object": "event",
# "createdAt": 1705420800,
# "type": "email.received",
# "data": { ... }
# }
if event["type"] == "email.received":
email_data = event["data"]
print(f"New email from {email_data['from']['address']}")
from vaultsandbox import (
WebhookNotFoundError,
WebhookLimitReachedError,
InboxNotFoundError,
ApiError,
)
try:
webhook = await inbox.get_webhook("whk_abc123")
except WebhookNotFoundError:
print("Webhook not found")
except InboxNotFoundError:
print("Inbox not found")
except ApiError as e:
print(f"API error ({e.status_code}): {e.message}")
import asyncio
import os
from vaultsandbox import (
VaultSandboxClient,
FilterConfig,
FilterRule,
WebhookNotFoundError,
)
async def setup_webhooks():
async with VaultSandboxClient(
api_key=os.environ["VAULTSANDBOX_API_KEY"],
) as client:
# Create inbox
inbox = await client.create_inbox()
print(f"Inbox: {inbox.email_address}")
# Create webhook with filter
webhook = await inbox.create_webhook(
url="https://your-app.com/webhook/emails",
events=["email.received", "email.stored"],
description="Production email webhook",
filter=FilterConfig(
rules=[
FilterRule(
field="from.address",
operator="domain",
value="example.com",
),
],
mode="all",
),
)
print(f"Webhook created: {webhook.id}")
print(f"Secret: {webhook.secret}")
# Test the webhook
result = await webhook.test()
if result.success:
print("Webhook test successful!")
else:
print(f"Webhook test failed: {result.error}")
# List all webhooks
webhooks = await inbox.list_webhooks()
print(f"Total webhooks: {len(webhooks)}")
# Update webhook
await webhook.update(description="Updated description")
# Rotate secret after some time
# new_secret = await webhook.rotate_secret()
# Cleanup
# await webhook.delete()
# await inbox.delete()
asyncio.run(setup_webhooks())
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