Skip to content

Webhooks

VaultSandbox provides a webhooks system that enables real-time HTTP notifications for email events. Configure webhooks to trigger CI/CD pipelines, send notifications to Slack/Discord, or integrate with any HTTP endpoint.

Webhooks support two scopes:

  • Global Webhooks: Receive events from all inboxes
  • Inbox Webhooks: Receive events from a specific inbox only

All webhook deliveries are cryptographically signed (HMAC-SHA256) and include automatic retry logic with exponential backoff.

Event TypeTriggerUse Case
email.receivedEmail arrives at inboxReal-time notifications, CI/CD triggers
email.storedEmail persisted to storageAudit logging, metrics collection
email.deletedEmail removed (manual, TTL, or eviction)Compliance tracking, cleanup verification

All endpoints require authentication via the X-API-Key header.

MethodEndpointDescription
POST/api/webhooksCreate webhook
GET/api/webhooksList webhooks
GET/api/webhooks/:idGet webhook details
PATCH/api/webhooks/:idUpdate webhook
DELETE/api/webhooks/:idDelete webhook
POST/api/webhooks/:id/testSend test event
POST/api/webhooks/:id/rotate-secretRotate signing secret
GET/api/webhooks/templatesGet available templates
GET/api/webhooks/metricsGet aggregated metrics
MethodEndpointDescription
POST/api/inboxes/:email/webhooksCreate webhook
GET/api/inboxes/:email/webhooksList webhooks
GET/api/inboxes/:email/webhooks/:idGet webhook details
PATCH/api/inboxes/:email/webhooks/:idUpdate webhook
DELETE/api/inboxes/:email/webhooks/:idDelete webhook
POST/api/inboxes/:email/webhooks/:id/testSend test event
POST/api/inboxes/:email/webhooks/:id/rotate-secretRotate signing secret
Terminal window
curl -X POST https://your-gateway/api/webhooks \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-endpoint.com/webhook",
"events": ["email.received"],
"description": "Notify on new emails"
}'

Response:

{
"id": "whk_a1b2c3d4e5f6...",
"url": "https://your-endpoint.com/webhook",
"events": ["email.received"],
"description": "Notify on new emails",
"enabled": true,
"secret": "whsec_abc123def456...",
"createdAt": "2024-05-11T10:00:00.000Z"
}

All webhook events follow a standard envelope:

{
"id": "evt_abc123def456",
"object": "event",
"createdAt": 1715421234,
"type": "email.received",
"data": { ... }
}

Triggered when an email successfully arrives at an inbox.

{
"id": "evt_abc123",
"object": "event",
"createdAt": 1715421234,
"type": "email.received",
"data": {
"id": "msg_xyz789",
"inboxId": "abc123def456",
"inboxEmail": "[email protected]",
"from": {
"address": "[email protected]",
"name": "John Sender"
},
"to": [
{ "address": "[email protected]", "name": "Test Inbox" }
],
"cc": [],
"subject": "Welcome to Our Service!",
"snippet": "Thank you for signing up...",
"textBody": "Full text body content",
"htmlBody": "<html>...</html>",
"headers": {
"message-id": "<[email protected]>",
"date": "Sat, 11 May 2024 10:30:00 +0000"
},
"attachments": [
{
"filename": "welcome.pdf",
"contentType": "application/pdf",
"size": 15234
}
],
"auth": {
"spf": "pass",
"dkim": "pass",
"dmarc": "pass"
},
"receivedAt": "2024-05-11T10:30:34.567Z"
}
}

Triggered when an email is persisted to storage.

{
"id": "evt_abc123",
"object": "event",
"createdAt": 1715421235,
"type": "email.stored",
"data": {
"id": "msg_xyz789",
"inboxId": "abc123def456",
"inboxEmail": "[email protected]",
"storedAt": "2024-05-11T10:30:35.123Z"
}
}

Triggered when an email is removed from an inbox.

{
"id": "evt_abc123",
"object": "event",
"createdAt": 1715425000,
"type": "email.deleted",
"data": {
"id": "msg_xyz789",
"inboxId": "abc123def456",
"inboxEmail": "[email protected]",
"reason": "manual",
"deletedAt": "2024-05-11T11:30:00.000Z"
}
}

Deletion reasons:

  • manual: Deleted via API
  • ttl: Expired due to inbox TTL
  • eviction: Removed due to storage limits

Transform webhook payloads using built-in or custom templates.

TemplateDescription
defaultRaw event JSON
slackSlack Block Kit format
discordDiscord embed format
teamsMicrosoft Teams MessageCard
simpleMinimal fields (from, to, subject, preview)
notificationSimple text message
zapierComprehensive fields for Zapier

Example with Slack template:

Terminal window
curl -X POST https://your-gateway/api/webhooks \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.slack.com/services/...",
"events": ["email.received"],
"template": { "type": "slack" }
}'

Create custom payloads using {{variable}} placeholders:

{
"url": "https://your-endpoint.com/webhook",
"events": ["email.received"],
"template": {
"type": "custom",
"body": "{\"email_from\": \"{{data.from.address}}\", \"subject\": \"{{data.subject}}\", \"event\": \"{{type}}\"}"
}
}

Available variables:

VariableDescription
{{id}}Event ID
{{type}}Event type
{{createdAt}}Unix timestamp
{{timestamp}}ISO 8601 timestamp
{{data.from.address}}Sender email
{{data.from.name}}Sender name
{{data.subject}}Email subject
{{data.snippet}}Email preview (first 200 chars)
{{data.inboxEmail}}Inbox address
{{data.textBody}}Full text body
{{data.htmlBody}}Full HTML body
{{data.auth.spf}}SPF result
{{data.auth.dkim}}DKIM result
{{data.auth.dmarc}}DMARC result

Reduce noise by filtering which events trigger webhook deliveries.

{
"url": "https://your-endpoint.com/webhook",
"events": ["email.received"],
"filter": {
"mode": "all",
"requireAuth": true,
"rules": [
{
"field": "from.address",
"operator": "domain",
"value": "github.com"
},
{
"field": "subject",
"operator": "contains",
"value": "pull request"
}
]
}
}

This webhook fires only for authenticated emails from @github.com with “pull request” in the subject.

FieldDescription
subjectEmail subject line
from.addressSender email address
from.nameSender display name
to.addressRecipient email
to.nameRecipient name
body.textPlain text body (first 5KB)
body.htmlHTML body (first 5KB)
header.X-CustomAny email header
OperatorDescriptionExample
equalsExact matchfrom.address equals "[email protected]"
containsSubstring matchsubject contains "urgent"
starts_withPrefix matchsubject starts_with "Re:"
ends_withSuffix matchfrom.address ends_with "@example.com"
domainEmail domain (with subdomains)from.address domain "example.com"
regexRegular expressionsubject regex "^(RE|FW):"
existsField presence checkheader.X-Priority exists
  • all (AND): All rules must match
  • any (OR): At least one rule must match

Set requireAuth: true to only trigger webhooks for emails that pass SPF, DKIM, and/or DMARC authentication. This prevents webhooks from firing for spoofed or unauthenticated senders.

All webhooks are cryptographically signed using HMAC-SHA256. Always verify signatures to ensure deliveries are authentic.

HeaderDescription
Content-Typeapplication/json
User-AgentVaultSandbox-Webhook/1.0
X-Vault-SignatureHMAC-SHA256 signature
X-Vault-EventEvent type
X-Vault-DeliveryUnique delivery ID
X-Vault-TimestampUnix timestamp
X-Vault-Signature: sha256=<hex-encoded-hmac>

The signature is computed over a signed payload:

signed_payload = ${timestamp}.${raw_request_body}
expected_signature = HMAC-SHA256(signed_payload, webhook_secret)
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signature, timestamp, secret) {
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js middleware example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-vault-signature'];
const timestamp = req.headers['x-vault-timestamp'];
const rawBody = req.body.toString();
if (!verifyWebhookSignature(rawBody, signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the webhook
const event = JSON.parse(rawBody);
console.log('Received event:', event.type);
res.status(200).send('OK');
});
import hmac
import hashlib
def verify_signature(raw_body: str, signature: str, timestamp: str, secret: str) -> bool:
signed_payload = f"{timestamp}.{raw_body}"
expected = 'sha256=' + hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)

Validate timestamps to prevent replay attacks (recommended tolerance: 5 minutes):

function isTimestampValid(timestamp, toleranceSeconds = 300) {
const webhookTime = parseInt(timestamp, 10);
const currentTime = Math.floor(Date.now() / 1000);
return Math.abs(currentTime - webhookTime) <= toleranceSeconds;
}

Rotate secrets with zero-downtime using the /rotate-secret endpoint:

Terminal window
curl -X POST https://your-gateway/api/webhooks/whk_abc123/rotate-secret \
-H "X-API-Key: your-api-key"

Response:

{
"id": "whk_abc123",
"secret": "whsec_new_secret_here...",
"previousSecretValidUntil": "2024-05-11T15:00:00.000Z"
}

The previous secret remains valid for 1 hour during the grace period. Update your verification code before the grace period expires.

Webhook endpoints must respond within 10 seconds. Return a 2xx status code to indicate success.

Failed deliveries automatically retry with exponential backoff:

AttemptDelayCumulative Time
1Immediate0
230 seconds30s
35 minutes5m 30s
430 minutes35m 30s
54 hours4h 35m 30s

After 5 consecutive failures, the webhook is automatically disabled. Re-enable it manually via the API.

LimitValue
Concurrent deliveries per webhook10
Total concurrent deliveries (global)100
SettingLimit
Global webhooks per account100
Webhooks per inbox50
Events per webhook10
Filter rules per webhook10
Custom template size10,000 characters
Description length500 characters
Filter rule value1,000 characters
URL length2,048 characters

Server administrators can configure webhook behavior:

VariableDefaultDescription
VSB_WEBHOOK_ENABLEDtrueEnable/disable webhook system
VSB_WEBHOOK_MAX_GLOBAL100Max global webhooks
VSB_WEBHOOK_MAX_INBOX50Max webhooks per inbox
VSB_WEBHOOK_TIMEOUT10000Delivery timeout (ms)
VSB_WEBHOOK_MAX_RETRIES5Max retry attempts
VSB_WEBHOOK_MAX_RETRIES_PER_WEBHOOK100Max retries queued per webhook
VSB_WEBHOOK_ALLOW_HTTPfalseAllow HTTP URLs (dev only)
VSB_WEBHOOK_REQUIRE_AUTH_DEFAULTfalseDefault requireAuth value
VSB_WEBHOOK_MAX_HEADERS50Max headers included in payload
VSB_WEBHOOK_MAX_HEADER_VALUE_LEN1000Max header value length (chars)
TypePrefixExample
Webhookwhk_whk_a1b2c3d4e5f6...
Signing Secretwhsec_whsec_abc123def456...
Eventevt_evt_xyz789abc...
Deliverydlv_dlv_123abc456...
  1. Always verify signatures using the X-Vault-Signature header
  2. Check timestamp freshness to prevent replay attacks
  3. Handle duplicates idempotently using event.id for deduplication
  4. Respond quickly within 10 seconds to avoid retries
  5. Process asynchronously if heavy work is needed
  6. Use requireAuth: true when filtering on sender identity
  7. Rotate secrets periodically for security
  8. Monitor for disabled webhooks and re-enable as needed
StatusDescription
200 OKSuccess
201 CreatedWebhook created
204 No ContentWebhook deleted
400 Bad RequestInvalid request or URL
401 UnauthorizedMissing or invalid API key
404 Not FoundWebhook or inbox not found
409 ConflictWebhook limit reached
422 Unprocessable EntityInvalid filter regex or template