Skip to content

Webhook SDK Specification

A language-agnostic specification for implementing webhook management in VaultSandbox client libraries. This document provides all necessary information to build webhook functionality in any programming language.


  1. Overview
  2. Authentication
  3. API Reference
  4. Data Structures
  5. Event Types
  6. Filtering
  7. Payload Templates
  8. Signature Verification
  9. Error Handling
  10. Limits & Quotas
  11. Utility Endpoints
  12. Implementation Checklist

The VaultSandbox webhook system enables real-time notifications when email events occur. Webhooks support two scopes:

  • Global webhooks: Receive events for all inboxes
  • Inbox webhooks: Receive events for a specific inbox only
  • Multiple event types: Subscribe to email received, stored, or deleted events
  • Flexible filtering: Filter events by sender, subject, headers, and more
  • Payload templates: Built-in templates for Slack, Discord, Teams, or custom formats
  • Signature verification: HMAC-SHA256 signatures for secure delivery
  • Automatic retries: Exponential backoff for failed deliveries
TypePrefixExample
Webhookwhk_whk_a1b2c3d4e5f6...
Secretwhsec_whsec_abc123def456...
Eventevt_evt_xyz789abc...
Deliverydlv_dlv_123abc456...
Messagemsg_msg_def789ghi...

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

GET /api/webhooks HTTP/1.1
Host: api.example.com
X-API-Key: your-api-key

Global webhooks receive events for all inboxes associated with the API key.

Creates a new global webhook.

Request Body: CreateWebhookDto

Response: 201 Created - WebhookResponse (includes secret)

Errors:

StatusDescription
400Invalid request body
409Webhook limit reached

Lists all global webhooks.

Response: 200 OK - WebhookListResponse

Note: The secret field is excluded from list responses.


Retrieves a specific global webhook.

Response: 200 OK - WebhookResponse (includes secret and stats)

Errors:

StatusDescription
404Webhook not found

Updates a global webhook.

Request Body: UpdateWebhookDto

Response: 200 OK - WebhookResponse

Errors:

StatusDescription
400Invalid request body
404Webhook not found

Deletes a global webhook.

Response: 204 No Content


Sends a test event to verify webhook connectivity.

Response: 200 OK - TestWebhookResponse

Errors:

StatusDescription
404Webhook not found

Generates a new signing secret. The old secret remains valid for 1 hour.

Response: 200 OK - RotateSecretResponse

Errors:

StatusDescription
404Webhook not found

Inbox webhooks are scoped to a specific inbox and follow the same patterns as global webhooks.

Creates a new inbox webhook.

Request Body: CreateWebhookDto

Response: 201 Created - WebhookResponse

Errors:

StatusDescription
400Invalid request body
404Inbox not found
409Webhook limit reached

Lists all webhooks for the specified inbox.

Response: 200 OK - WebhookListResponse


Retrieves a specific inbox webhook.

Response: 200 OK - WebhookResponse


Updates an inbox webhook.

Request Body: UpdateWebhookDto

Response: 200 OK - WebhookResponse


Deletes an inbox webhook.

Response: 204 No Content


POST /api/inboxes/{email}/webhooks/{id}/test

Section titled “POST /api/inboxes/{email}/webhooks/{id}/test”

Sends a test event to the inbox webhook endpoint.

Response: 200 OK - TestWebhookResponse


POST /api/inboxes/{email}/webhooks/{id}/rotate-secret

Section titled “POST /api/inboxes/{email}/webhooks/{id}/rotate-secret”

Generates a new signing secret for the inbox webhook.

Response: 200 OK - RotateSecretResponse


interface CreateWebhookDto {
/** Target URL (HTTPS required in production) */
url: string;
/** Event types to subscribe to (max 10) */
events: WebhookEventType[];
/** Optional payload template */
template?: 'slack' | 'discord' | 'teams' | 'simple' | 'notification' | 'zapier' | 'default' | CustomTemplateDto;
/** Optional event filter configuration */
filter?: FilterConfigDto;
/** Optional description (max 500 chars) */
description?: string;
}
FieldTypeRequiredDescription
urlstringYesTarget URL (HTTPS required in prod)
eventsWebhookEventType[]YesEvent types to subscribe to
templatestring | CustomTemplateDtoNoPayload template configuration
filterFilterConfigDtoNoEvent filter rules
descriptionstringNoHuman-readable description

Example:

{
"url": "https://example.com/webhook",
"events": ["email.received"],
"description": "Notify when new emails arrive"
}

All fields are optional. Set template or filter to null to remove them.

interface UpdateWebhookDto {
url?: string;
events?: WebhookEventType[];
template?: string | CustomTemplateDto | null;
filter?: FilterConfigDto | null;
description?: string;
enabled?: boolean;
}

Example - Disable webhook:

{
"enabled": false
}

Example - Remove filter:

{
"filter": null
}

interface CustomTemplateDto {
/** Must be 'custom' */
type: 'custom';
/** JSON template with {{variable}} placeholders (max 10000 chars) */
body: string;
/** Optional Content-Type header override */
contentType?: string;
}

Example:

{
"template": {
"type": "custom",
"body": "{\"email_from\": \"{{data.from.address}}\", \"subject\": \"{{data.subject}}\"}"
}
}

interface FilterConfigDto {
/** Filter rules (max 10) */
rules: FilterRuleDto[];
/** 'all' = AND logic, 'any' = OR logic */
mode: 'all' | 'any';
/** Require email to pass SPF/DKIM/DMARC checks */
requireAuth?: boolean;
}
interface FilterRuleDto {
/** Field to filter on */
field: FilterableField;
/** Comparison operator */
operator: FilterOperator;
/** Value to match (max 1000 chars) */
value: string;
/** Case-sensitive matching (default: false) */
caseSensitive?: boolean;
}

Example:

{
"filter": {
"mode": "all",
"requireAuth": true,
"rules": [
{
"field": "from.address",
"operator": "domain",
"value": "example.com"
},
{
"field": "subject",
"operator": "contains",
"value": "urgent"
}
]
}
}

interface WebhookResponse {
/** Webhook ID (whk_ prefix) */
id: string;
/** Target URL */
url: string;
/** Subscribed event types */
events: WebhookEventType[];
/** 'global' or 'inbox' */
scope: 'global' | 'inbox';
/** Inbox email (inbox webhooks only) */
inboxEmail?: string;
/** Inbox hash (inbox webhooks only) */
inboxHash?: string;
/** Whether webhook is active */
enabled: boolean;
/** Signing secret (whsec_ prefix) - only on create/get, not list */
secret?: string;
/** Template configuration */
template?: WebhookTemplate;
/** Filter configuration */
filter?: FilterConfigDto;
/** Human-readable description */
description?: string;
/** ISO timestamp */
createdAt: string;
/** ISO timestamp of last update */
updatedAt?: string;
/** ISO timestamp of last delivery attempt */
lastDeliveryAt?: string;
/** Status of last delivery */
lastDeliveryStatus?: 'success' | 'failed';
/** Delivery statistics (only on get, not list) */
stats?: WebhookStatsResponse;
}

interface WebhookListResponse {
webhooks: WebhookResponse[];
total: number;
}

interface TestWebhookResponse {
/** Whether test succeeded */
success: boolean;
/** HTTP status code from endpoint */
statusCode?: number;
/** Response time in milliseconds */
responseTime?: number;
/** Response body (truncated to 1KB) */
responseBody?: string;
/** Error message if failed */
error?: string;
/** The test payload that was sent */
payloadSent?: unknown;
}

interface RotateSecretResponse {
/** Webhook ID */
id: string;
/** New signing secret */
secret: string;
/** ISO timestamp - old secret valid until this time (1 hour grace period) */
previousSecretValidUntil: string;
}

interface WebhookStatsResponse {
totalDeliveries: number;
successfulDeliveries: number;
failedDeliveries: number;
}

type WebhookEventType =
| 'email.received' // Email arrived at inbox
| 'email.stored' // Email persisted to storage
| 'email.deleted'; // Email deleted

All events are wrapped in a standard envelope:

interface WebhookEventEnvelope<T> {
/** Event ID (evt_ prefix) */
id: string;
/** Always 'event' */
object: 'event';
/** Unix timestamp */
createdAt: number;
/** Event type */
type: WebhookEventType;
/** Event-specific data */
data: T;
}

Fired when an email arrives at an inbox.

interface EmailReceivedData {
/** Email ID (msg_ prefix) */
id: string;
/** Inbox hash */
inboxId: string;
/** Inbox email address */
inboxEmail: string;
/** Sender */
from: EmailAddress;
/** Recipients */
to: EmailAddress[];
/** CC recipients */
cc?: EmailAddress[];
/** Email subject */
subject: string;
/** First 200 chars of text body */
snippet: string;
/** Full text body (optional) */
textBody?: string;
/** Full HTML body (optional) */
htmlBody?: string;
/** Selected headers (lowercase keys) */
headers: Record<string, string>;
/** Attachment metadata (no content) */
attachments: AttachmentMeta[];
/** SPF/DKIM/DMARC results */
auth?: EmailAuthResults;
/** ISO timestamp */
receivedAt: string;
}
interface EmailAddress {
address: string;
name?: string;
}
interface AttachmentMeta {
filename: string;
contentType: string;
size: number;
}

Example payload:

{
"id": "evt_abc123",
"object": "event",
"createdAt": 1705420800,
"type": "email.received",
"data": {
"id": "msg_xyz789",
"inboxId": "a1b2c3d4",
"inboxEmail": "[email protected]",
"from": {
"address": "[email protected]",
"name": "John Doe"
},
"to": [
{
"address": "[email protected]"
}
],
"subject": "Hello World",
"snippet": "This is the beginning of the email...",
"headers": {
"message-id": "<[email protected]>",
"date": "Wed, 16 Jan 2026 12:00:00 +0000"
},
"attachments": [],
"receivedAt": "2026-01-16T12:00:00.000Z"
}
}

Fired when an email is persisted to storage.

interface EmailStoredData {
id: string;
inboxId: string;
inboxEmail: string;
storedAt: string;
}

Fired when an email is deleted.

interface EmailDeletedData {
id: string;
inboxId: string;
inboxEmail: string;
reason: 'manual' | 'ttl' | 'eviction';
deletedAt: string;
}

Filter webhooks to only receive events matching specific criteria.

FieldDescription
subjectEmail subject line
from.addressSender email address
from.nameSender display name
to.addressFirst recipient email address
to.nameFirst recipient display name
body.textPlain text body (first 5KB)
body.htmlHTML body (first 5KB)
header.X-CustomAny email header (case-insensitive)
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 match (supports subdomains)from.address domain "example.com"
regexRegular expressionsubject regex "^(RE|FW):"
existsField presence checkheader.X-Priority exists
  • all: ALL rules must match (AND logic)
  • any: AT LEAST ONE rule must match (OR logic)

Set requireAuth: true to only trigger for emails that pass SPF/DKIM/DMARC checks.


Transform webhook payloads using built-in or custom templates.

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

Example - Using Slack template:

{
"url": "https://hooks.slack.com/services/xxx",
"events": ["email.received"],
"template": "slack"
}

Create custom payloads using {{variable}} placeholders with dot notation.

Available Variables:

  • {{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
  • {{data.inboxEmail}} - Inbox address
  • Any other nested field in the event data

Example:

{
"template": {
"type": "custom",
"body": "{\"text\": \"New email from {{data.from.address}}: {{data.subject}}\"}",
"contentType": "application/json"
}
}

All webhook deliveries are signed using HMAC-SHA256. Always verify signatures to ensure requests are authentic.

HeaderDescription
X-Vault-Signaturesha256=<hex_signature>
X-Vault-EventEvent type
X-Vault-DeliveryDelivery ID (dlv_ prefix)
X-Vault-TimestampUnix timestamp
signed_payload = ${timestamp}.${raw_body}
expected_signature = HMAC-SHA256(signed_payload, webhook_secret)
import crypto from 'crypto';
function verifyWebhookSignature(rawBody: string, signature: string, timestamp: string, secret: string): boolean {
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
const actualSignature = signature.replace('sha256=', '');
return crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(actualSignature));
}
// Express middleware example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-vault-signature'] as string;
const timestamp = req.headers['x-vault-timestamp'] as string;
const rawBody = req.body.toString();
if (!verifyWebhookSignature(rawBody, signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(rawBody);
// Process event...
res.status(200).send('OK');
});
import hmac
import hashlib
def verify_webhook_signature(raw_body: str, signature: str, timestamp: str, secret: str) -> bool:
signed_payload = f"{timestamp}.{raw_body}"
expected_signature = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
actual_signature = signature.replace("sha256=", "")
return hmac.compare_digest(expected_signature, actual_signature)
# Flask example
@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Vault-Signature')
timestamp = request.headers.get('X-Vault-Timestamp')
raw_body = request.get_data(as_text=True)
if not verify_webhook_signature(raw_body, signature, timestamp, WEBHOOK_SECRET):
return 'Invalid signature', 401
event = request.get_json()
# Process event...
return 'OK', 200
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func verifyWebhookSignature(rawBody, signature, timestamp, secret string) bool {
signedPayload := fmt.Sprintf("%s.%s", timestamp, rawBody)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedPayload))
expectedSignature := hex.EncodeToString(mac.Sum(nil))
actualSignature := signature[7:] // Remove "sha256=" prefix
return hmac.Equal([]byte(expectedSignature), []byte(actualSignature))
}

To prevent replay attacks, validate that the timestamp is recent (e.g., within 5 minutes):

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

When rotating secrets via /rotate-secret:

  1. A new secret is generated immediately
  2. The old secret remains valid for 1 hour (grace period)
  3. Update your verification code before the grace period expires
  4. During the grace period, verify against both secrets

StatusDescription
200Success (GET, PATCH, POST)
201Webhook created successfully
204Webhook deleted successfully
400Invalid request body
401Missing or invalid API key
404Webhook or inbox not found
409Webhook limit reached
interface ErrorResponse {
statusCode: number;
message: string | string[];
error: string;
}

Example:

{
"statusCode": 400,
"message": ["url must be a valid HTTPS URL"],
"error": "Bad Request"
}

The server retries failed deliveries with exponential backoff:

AttemptDelay
1Immediate
230 seconds
35 minutes
430 minutes
54 hours

A delivery is considered successful if your endpoint returns a 2xx status code.


LimitDefault Value
Global webhooks100
Webhooks per inbox50
Events per webhook10
Filter rules per webhook10
Max retry attempts5
Delivery timeout10 seconds
Custom template size10,000 chars
Description length500 chars
Filter value length1,000 chars

The server info endpoint includes webhook-related configuration fields.

Webhook-related fields:

interface ServerInfoResponse {
// ... other fields ...
/** Whether the webhook system is enabled on this server */
webhookEnabled: boolean;
/** Default value for webhook requireAuth filter when not specified */
webhookRequireAuthDefault: boolean;
}

Usage:

  • Check webhookEnabled before showing webhook UI
  • Use webhookRequireAuthDefault as the default value for the requireAuth filter toggle

Retrieves available payload templates for dropdown/select UI components.

Response: 200 OK

interface WebhookTemplateOption {
/** Display label */
label: string;
/** Template identifier to use in CreateWebhookDto */
value: string;
}
interface WebhookTemplatesResponse {
templates: WebhookTemplateOption[];
}

Example Response:

{
"templates": [
{ "label": "Default (Raw JSON)", "value": "default" },
{ "label": "Slack", "value": "slack" },
{ "label": "Discord", "value": "discord" },
{ "label": "Microsoft Teams", "value": "teams" },
{ "label": "Simple", "value": "simple" },
{ "label": "Notification", "value": "notification" },
{ "label": "Zapier/Automation", "value": "zapier" }
]
}

Retrieves aggregated webhook statistics across all webhooks.

Response: 200 OK

interface WebhookMetricsResponse {
webhooks: {
/** Number of global webhooks */
global: number;
/** Number of inbox-scoped webhooks */
inbox: number;
/** Number of currently enabled webhooks */
enabled: number;
/** Total webhooks (global + inbox) */
total: number;
};
deliveries: {
/** Total delivery attempts across all webhooks */
total: number;
/** Successful deliveries */
successful: number;
/** Failed deliveries */
failed: number;
};
}

Example Response:

{
"webhooks": {
"global": 3,
"inbox": 12,
"enabled": 14,
"total": 15
},
"deliveries": {
"total": 1250,
"successful": 1180,
"failed": 70
}
}

Usage:

  • Display in a dashboard or summary view
  • Calculate success rate: (deliveries.successful / deliveries.total) * 100

Note: This endpoint aggregates stats by iterating all webhooks. Use sparingly (e.g., on dashboard load, not in real-time polling).


  • Create global webhook
  • List global webhooks
  • Get global webhook by ID
  • Update global webhook
  • Delete global webhook
  • Test global webhook
  • Rotate global webhook secret
  • Create inbox webhook
  • List inbox webhooks
  • Get inbox webhook by ID
  • Update inbox webhook
  • Delete inbox webhook
  • Test inbox webhook
  • Rotate inbox webhook secret
  • HMAC-SHA256 signature generation
  • Constant-time signature comparison
  • Timestamp validation (replay attack prevention)
  • Dual-secret verification during rotation grace period
  • All filter operators (equals, contains, starts_with, ends_with, domain, regex, exists)
  • Filter mode (all/any)
  • Authentication requirement filter
  • Built-in template selection
  • Custom template with variable substitution
  • Template retrieval from utility endpoint
  • All HTTP error codes
  • Error response parsing
  • Retry behavior documentation for consumers
  • Unit tests for signature verification
  • Integration tests for CRUD operations
  • Filter rule evaluation tests
  • Template rendering tests

VersionDateChanges
0.8.02026-01-18Initial webhook SDK specification