Webhook Integration Guide
This guide walks you through setting up webhook subscriptions with Hookbase to send events to your customers' endpoints.
Overview
Hookbase's outbound webhook system allows you to:
- Define event types that your application produces
- Create applications to represent your customers or tenants
- Register webhook endpoints for each application
- Subscribe endpoints to specific event types
- Send events that are automatically delivered to subscribed endpoints
Getting Started
Step 1: Create Event Types
Event types define the different kinds of events your application can produce. They follow a dot-notation naming convention.
curl -X POST "https://api.hookbase.app/api/organizations/{orgId}/event-types" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"name": "order.created",
"displayName": "Order Created",
"description": "Triggered when a new order is placed",
"category": "orders",
"schema": "{\"type\": \"object\", \"properties\": {\"orderId\": {\"type\": \"string\"}, \"amount\": {\"type\": \"number\"}}}",
"examplePayload": "{\"orderId\": \"ord_123\", \"amount\": 99.99, \"currency\": \"USD\"}"
}'Naming conventions:
- Use lowercase letters and dots only
- Start with a noun (the entity)
- End with a past-tense verb (the action)
- Examples:
order.created,payment.succeeded,user.subscription.upgraded
Step 2: Create an Application
Applications represent your customers or tenants. Each application can have multiple endpoints.
curl -X POST "https://api.hookbase.app/api/organizations/{orgId}/webhook-applications" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp",
"externalId": "customer_123",
"metadata": {
"plan": "enterprise",
"region": "us-east-1"
}
}'The externalId field allows you to reference applications by your own internal customer ID.
Step 3: Create Webhook Endpoints
Endpoints are the URLs where webhooks will be delivered. Each endpoint gets a unique signing secret.
curl -X POST "https://api.hookbase.app/api/organizations/{orgId}/webhook-endpoints" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"applicationId": "085fc909-649d-4820-9c73-6bf2dc484c48",
"url": "https://api.customer.com/webhooks",
"description": "Production webhook endpoint",
"timeoutSeconds": 30
}'Response:
{
"data": {
"id": "d80e9f49-2dbd-4203-bd12-502cfcd56a45",
"url": "https://api.customer.com/webhooks",
"secret": "whsec_abc123def456...",
"circuitState": "closed"
},
"warning": "Save the signing secret now. It will not be shown again."
}Important: Store the signing secret securely. It is only returned once at creation time.
To sign this endpoint's deliveries with Ed25519 (v1a) instead of HMAC, add "signatureScheme": "ed25519". The response then has no secret; it carries publicKey (whpk_...), which receivers verify with and which GET on the endpoint returns at any time. See Ed25519 signatures.
Step 4: Create Subscriptions
Subscriptions link endpoints to event types, determining which events are delivered to each endpoint.
curl -X POST "https://api.hookbase.app/api/organizations/{orgId}/webhook-subscriptions" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"endpointId": "d80e9f49-2dbd-4203-bd12-502cfcd56a45",
"eventTypeId": "2f41bb16-991e-4412-80fb-54d5a59583aa"
}'You can also subscribe to multiple event types at once:
curl -X POST "https://api.hookbase.app/api/organizations/{orgId}/webhook-subscriptions/bulk" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"endpointId": "d80e9f49-2dbd-4203-bd12-502cfcd56a45",
"eventTypeIds": ["b862fc8c-2298-4065-aba4-51745d13ab8c", "e847fdac-a158-4913-b277-d10ee037ad25", "adef9d21-744b-45bc-b3f5-96c4fd82c840"]
}'Step 5: Send Events
When something happens in your application, send an event to Hookbase:
curl -X POST "https://api.hookbase.app/api/organizations/{orgId}/send-event" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"eventType": "order.created",
"payload": {
"orderId": "ord_789",
"customerId": "cus_456",
"amount": 149.99,
"currency": "USD",
"items": [
{"sku": "WIDGET-001", "quantity": 2, "price": 74.99}
]
},
"idempotencyKey": "order-created-ord_789"
}'Response:
{
"data": {
"eventId": "0b3b6fd4-e9ae-4657-96a5-9e7553a9502a",
"messagesQueued": 3,
"endpoints": [
{"id": "bb1daf5c-2251-4883-ad56-37182fdaf7b8", "url": "https://api.customer1.com/webhooks"},
{"id": "ec4bf8a4-ef55-4020-b17f-8dad023fc2cb", "url": "https://api.customer2.com/webhooks"},
{"id": "97d2180c-be38-4f3d-8ab7-fcbd40e54286", "url": "https://api.customer3.com/webhooks"}
]
}
}Event Types
Creating Event Types
Event types should be created before sending events. They provide:
- Schema validation: Define a JSON Schema to validate payloads
- Documentation: Help consumers understand event structure
- Categorization: Group related events for easier management
curl -X POST "https://api.hookbase.app/api/organizations/{orgId}/event-types" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"name": "invoice.payment.succeeded",
"displayName": "Invoice Payment Succeeded",
"description": "Fired when an invoice payment is successfully processed",
"category": "billing",
"schema": "{\"type\": \"object\", \"required\": [\"invoiceId\", \"amount\"], \"properties\": {\"invoiceId\": {\"type\": \"string\"}, \"amount\": {\"type\": \"number\"}, \"paidAt\": {\"type\": \"string\", \"format\": \"date-time\"}}}",
"examplePayload": "{\"invoiceId\": \"inv_123\", \"amount\": 500.00, \"paidAt\": \"2024-01-15T10:30:00Z\"}",
"documentationUrl": "https://docs.example.com/webhooks/invoice-payment-succeeded"
}'Validating Payloads
A schema on its own only documents the payload: Hookbase shows it in the portal and the event catalog, but sends are not checked against it. Turn on validatePayloads to enforce it. Sends of that event type whose payload doesn't match the schema are then rejected with 422, before anything is stored or queued, so a bad payload reaches no endpoint and doesn't count toward your monthly event quota. It does still count toward the per-minute send rate limit.
validatePayloads is off by default, so adding or changing a schema never starts rejecting sends that already work. Set it when you create the event type, or later with PATCH:
curl -X PATCH "https://api.hookbase.app/api/organizations/{orgId}/event-types/{eventTypeId}" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{ "validatePayloads": true }'In the dashboard, the event type form shows Reject payloads that don't match the schema once the schema field has a value. Event types with it on show a Validates payloads badge.
How validation works:
- The schema is checked as JSON Schema draft 2020-12 (
$ref,oneOf,additionalProperties, type arrays and the rest of the spec are supported). - If the event type has no schema, there is nothing to enforce and every payload is accepted, even with
validatePayloadson. - A versioned event type checks the payload against the schema of the version it is sent in: the latest version by default, or the version named by
eventVersion. - The response lists at most 20 problems. Each one has a JSON Pointer
pathto the failing value (/for the payload itself) and amessage.
A rejected send returns:
{
"error": "Schema validation failed",
"message": "Payload does not match the schema for event type invoice.payment.succeeded",
"errors": [
{ "path": "/amount", "message": "Instance type \"string\" is invalid. Expected \"number\"." },
{ "path": "/couponCode", "message": "Property \"couponCode\" is not allowed" }
]
}For a versioned event type, message ends with the version (... version 2024-06-01) and the body also has "eventVersion": "2024-06-01". The same response comes from POST /api/organizations/{orgId}/send-event, POST /api/send-event and POST /v1/events.
Deprecating Event Types
When you need to remove an event type, deprecate it first to give consumers time to migrate:
curl -X PATCH "https://api.hookbase.app/api/organizations/{orgId}/event-types/{eventTypeId}" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"isDeprecated": true,
"deprecatedMessage": "Use invoice.paid instead. This event will be removed on 2024-06-01."
}'Label Filtering
Labels allow you to send events to specific subsets of subscribers based on metadata.
Adding Labels to Events
curl -X POST "https://api.hookbase.app/api/organizations/{orgId}/send-event" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"eventType": "order.shipped",
"payload": {"orderId": "ord_123", "carrier": "fedex"},
"labels": {
"region": "us-west",
"priority": "high",
"carrier": "fedex"
}
}'Configuring Subscription Filters
Subscriptions can filter events based on labels:
curl -X POST "https://api.hookbase.app/api/organizations/{orgId}/webhook-subscriptions" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"endpointId": "d80e9f49-2dbd-4203-bd12-502cfcd56a45",
"eventTypeId": "2f41bb16-991e-4412-80fb-54d5a59583aa",
"labelFilters": {
"region": "us-west",
"priority": ["high", "urgent"]
},
"labelFilterMode": "all"
}'Filter modes:
all: Event must match ALL filter conditions (AND logic)any: Event must match ANY filter condition (OR logic)
Filter values:
- Single value: Exact match required
- Array of values: Event label must match any value in the array
Retry Behavior
Hookbase automatically retries failed webhook deliveries using exponential backoff.
Retry Schedule
| Attempt | Delay After Failure |
|---|---|
| 1st retry | 10 seconds |
| 2nd retry | 30 seconds |
| 3rd retry | 2 minutes |
| 4th retry | 10 minutes |
After 5 total attempts (1 initial + 4 retries), the message is moved to the Dead Letter Queue (DLQ).
What Triggers a Retry
- Network errors (timeout, connection refused, DNS failure)
- HTTP 5xx responses (server errors)
- HTTP 429 responses (rate limited)
- Request timeouts
What Does NOT Retry
- HTTP 2xx responses (success)
- HTTP 4xx responses except 429 (client errors are not retried)
Circuit Breaker
Hookbase implements a circuit breaker pattern to prevent overwhelming failing endpoints.
Circuit States
| State | Description | Behavior |
|---|---|---|
| Closed | Normal operation | All requests are sent |
| Open | Endpoint is failing | Requests are skipped, message retried later |
| Half-Open | Testing recovery | Probe requests are sent to test endpoint health |
Default Thresholds
| Parameter | Default | Description |
|---|---|---|
| Failure threshold | 5 | Consecutive failures to open circuit |
| Success threshold | 2 | Consecutive successes to close circuit |
| Cooldown period | 60 seconds | Time before testing recovery |
Configuring Circuit Breaker
Set custom thresholds when creating or updating an endpoint:
curl -X PATCH "https://api.hookbase.app/api/organizations/{orgId}/webhook-endpoints/{endpointId}" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"circuitFailureThreshold": 10,
"circuitSuccessThreshold": 3,
"circuitCooldownSeconds": 120
}'Manually Resetting Circuit Breaker
If you've fixed an endpoint issue, you can manually reset the circuit:
curl -X POST "https://api.hookbase.app/api/organizations/{orgId}/webhook-endpoints/{endpointId}/reset-circuit" \
-H "Authorization: Bearer {token}"Best Practices for Endpoint Implementation
1. Respond Quickly
Acknowledge webhooks within 5-10 seconds. Process events asynchronously if needed.
// Good: Acknowledge immediately, process async
app.post('/webhooks', async (req, res) => {
// Verify signature
verifySignature(req);
// Queue for async processing
await queue.add('processWebhook', req.body);
// Respond immediately
res.status(200).json({ received: true });
});2. Implement Idempotency
Use the webhook-id header (the message ID, the same on every retry) to deduplicate webhooks:
app.post('/webhooks', async (req, res) => {
const messageId = req.headers['webhook-id'];
// Check if already processed
if (await redis.get(`webhook:${messageId}`)) {
return res.status(200).json({ received: true, duplicate: true });
}
// Process webhook
await processWebhook(req.body);
// Mark as processed (with TTL for cleanup)
await redis.set(`webhook:${messageId}`, '1', 'EX', 86400);
res.status(200).json({ received: true });
});3. Return Appropriate Status Codes
| Scenario | Status Code | Hookbase Behavior |
|---|---|---|
| Success | 200-299 (or the endpoint's custom success codes) | Mark as delivered |
| Endpoint gone | 410 | Disable the endpoint and move the message to the DLQ, no retry |
| Rate limited / unavailable | 429, 503 | Retry with backoff, never sooner than Retry-After |
| Any other response (including other 4xx) | - | Retry with backoff until attempts run out, then the DLQ |
| Timeout | - | Retry with backoff |
4. Log Webhook Receipts
Maintain an audit trail for debugging:
app.post('/webhooks', async (req, res) => {
const receipt = {
messageId: req.headers['webhook-id'],
eventType: req.headers['x-hookbase-event-type'],
timestamp: req.headers['webhook-timestamp'],
receivedAt: new Date().toISOString(),
payload: req.body,
};
await db.webhookReceipts.create(receipt);
// Process...
});5. Handle Signature Verification Failures Gracefully
app.post('/webhooks', (req, res) => {
try {
verifySignature(req.body, req.headers, secret);
} catch (error) {
// Log for security monitoring
console.error('Webhook signature verification failed:', {
error: error.message,
headers: req.headers,
ip: req.ip,
});
// Return 401 but don't leak details
return res.status(401).json({ error: 'Unauthorized' });
}
// Process verified webhook...
});Monitoring and Debugging
Viewing Delivery Status
List recent messages and their delivery status:
curl "https://api.hookbase.app/api/organizations/{orgId}/outbound-messages?status=failed&limit=20" \
-H "Authorization: Bearer {token}"Viewing Delivery Attempts
See all attempts for a specific message:
curl "https://api.hookbase.app/api/organizations/{orgId}/outbound-messages/{messageId}/attempts" \
-H "Authorization: Bearer {token}"Checking DLQ
View messages that have exhausted all retries:
curl "https://api.hookbase.app/api/organizations/{orgId}/outbound-messages/dlq/messages" \
-H "Authorization: Bearer {token}"Retrying Failed Messages
Replay a failed message:
curl -X POST "https://api.hookbase.app/api/organizations/{orgId}/outbound-messages/{messageId}/replay" \
-H "Authorization: Bearer {token}"Retry a DLQ message:
curl -X POST "https://api.hookbase.app/api/organizations/{orgId}/outbound-messages/dlq/{messageId}/retry" \
-H "Authorization: Bearer {token}"Next Steps
- Webhook Signature Verification - Implement signature verification in your endpoint
- Webhook API Reference - Complete API documentation