Destinations
Destinations are where webhooks get delivered after processing. Hookbase supports two categories of destinations: HTTP endpoints for real-time delivery and warehouse destinations for batch ingestion into object storage.
Destination Types
| Type | Description | Use Case |
|---|---|---|
| HTTP | Delivers webhooks to an HTTP(S) endpoint | Real-time integrations, API triggers |
| Amazon S3 | Uploads batched events to an S3 bucket | AWS data lakes, Athena queries |
| Cloudflare R2 | Uploads batched events to an R2 bucket | Zero-egress archival, S3-compatible storage |
| Google Cloud Storage | Uploads batched events to a GCS bucket | BigQuery external tables, GCP pipelines |
| Azure Blob Storage | Uploads batched events to an Azure container | Azure Data Lake, Synapse Analytics |
HTTP Destinations
HTTP destinations deliver each webhook event to an endpoint in real time. This is the default destination type.
Creating an HTTP Destination
Required Fields
| Field | Description |
|---|---|
name | Human-readable name |
slug | URL-safe identifier: lowercase letters, numbers and hyphens only, max 50 characters |
url | The HTTP(S) URL to deliver webhooks to |
Optional Fields
| Field | Description |
|---|---|
description | Optional description |
headers | Custom headers to include in requests |
authType | Authentication type (none, bearer, basic, api_key) |
authConfig | Authentication configuration |
timeoutMs | Request timeout in milliseconds, 1000-60000 (default: 30000) |
isActive | Whether the destination is active (default: true) |
What Headers a Delivery Carries
A delivery is a fresh HTTP request, not a forwarded copy of the one the provider sent. Hookbase builds its headers from scratch, in this order:
Content-Type— from the transform's output, or the event's own content type when no transform runsUser-Agent: Hookbase/1.0X-Delivery-ID— this delivery attempt's id, stable across retries of the same deliveryX-Event-ID— the event id, shared by every delivery fanned out from one event- The destination's own
headersobject (below), which can override any of the above - The destination's auth header —
Authorizationforbearer/basic, or your chosen header name forapi_key/custom_header - Any one-shot overrides from a replay-with-edit, applied last
No header the provider sent is included. X-GitHub-Event, Stripe-Signature,
X-Shopify-Topic and every other inbound header stop at ingest — a handler that reads them off a
Hookbase delivery gets nothing. Read what you need out of the body, or set it yourself on the
destination.
Of the inbound headers, five are stored on the event and so remain visible in the dashboard, the
events API and a payload export: content-type, user-agent, x-github-event, x-gitlab-event
and stripe-signature. Everything else is dropped rather than stored, deliberately — it keeps
secrets and arbitrary vendor headers out of the datastore.
Custom Headers
Add custom headers to every webhook delivery:
{
"headers": {
"Authorization": "Bearer your-api-key",
"X-Custom-Header": "custom-value",
"X-Source": "hookbase"
}
}Common use cases:
- Authentication tokens
- API keys
- Tracking headers
- Environment identifiers
Retry Schedule
Retries are not configurable per destination. Every route delivery gets the same five attempts:
| Attempt | Sent after |
|---|---|
| 1 | immediately |
| 2 | 10 seconds |
| 3 | 30 seconds |
| 4 | 2 minutes |
| 5 | 10 minutes |
After the fifth attempt the delivery is marked failed and moves to the dead letter queue.
A 5xx, a 429, a network error, or a timeout is retried. Any other 4xx is permanent — the
delivery stops immediately rather than using its remaining attempts, so a destination answering
401 during a credential rotation needs the credential fixed and the delivery replayed.
The one delivery-timing field a destination has is timeoutMs, which decides when an attempt counts
as a timeout.
Info
Retry schedules are configurable on the sending side. If you fan webhooks out to your own
customers' endpoints, each endpoint takes its own backoffType and retryDelays. See
the Send API reference.
Success Criteria
A delivery is considered successful when:
- HTTP status code is 2xx (200-299)
- Response is received within timeout (30 seconds)
Failure Handling
After all retries are exhausted:
- The delivery is marked as
failed - The event is moved to the dead letter queue
- You can manually replay from the dashboard
Example: Creating an HTTP Destination
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Production API",
"slug": "production-api",
"type": "http",
"url": "https://api.yourapp.com/webhooks/handler",
"headers": {
"Authorization": "Bearer sk_live_xxx",
"X-Webhook-Source": "hookbase"
},
"timeoutMs": 15000
}'Warehouse Destinations
Warehouse destinations batch webhook events and upload them as structured files (JSONL or JSON) to object storage. This is useful for analytics, compliance archival, and batch processing pipelines.
Tip
Plan Requirement
Warehouse destinations are available on Pro and Business plans.
How Batching Works
When events arrive, Hookbase queues them for batch delivery. The warehouse queue accumulates up to 100 events or waits 30 seconds (whichever comes first), then uploads a single file to your bucket.
Events for different destinations are grouped separately, so each destination receives its own files.
File Path Pattern
Files are stored using a predictable path structure:
{prefix}/{partition}/{timestamp}-{destination_id}.{format}The partition segment depends on your configuration:
| Partition Strategy | Example Path |
|---|---|
| Date (default) | webhooks/stripe/2026-02-21/1740150000-a1b2c3d4.jsonl |
| Hour | webhooks/stripe/2026-02-21/14/1740150000-a1b2c3d4.jsonl |
| Source | webhooks/stripe/2026-02-21/my-stripe-source/1740150000-a1b2c3d4.jsonl |
File Formats
Each row written to a warehouse destination is the webhook payload itself. Hookbase does not
wrap it in an envelope — there is no event_id or received_at field in the file. If you need the
event id alongside the data, project it in with a field mapping (below) or keep an HTTP destination
in parallel.
JSONL (Recommended)
One JSON object per line. Works well with AWS Athena, BigQuery external tables, and streaming processors:
{"type":"payment_intent.succeeded","data":{"amount":2500}}
{"type":"customer.created","data":{"email":"user@example.com"}}JSON Array
Set "fileFormat": "json" to get a single pretty-printed array containing the batch instead:
[
{
"type": "payment_intent.succeeded",
"data": { "amount": 2500 }
},
{
"type": "customer.created",
"data": { "email": "user@example.com" }
}
]Amazon S3
{
"type": "s3",
"config": {
"bucket": "my-data-lake",
"region": "us-east-1",
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
"secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"prefix": "webhooks/stripe/",
"fileFormat": "jsonl",
"partitionBy": "date"
}
}| Field | Required | Description |
|---|---|---|
bucket | Yes | S3 bucket name |
region | Yes | AWS region (e.g., us-east-1) |
accessKeyId | Yes | AWS access key ID |
secretAccessKey | Yes | AWS secret access key (encrypted at rest) |
prefix | No | Path prefix inside the bucket |
fileFormat | No | jsonl (default) or json |
partitionBy | No | date (default), hour, or source |
Cloudflare R2
R2 destinations connect via R2's S3-compatible API using AWS4-HMAC-SHA256 signing. You'll need an R2 API token with read/write access to your bucket.
{
"type": "r2",
"config": {
"bucket": "my-webhook-archive",
"endpoint": "https://ACCOUNT_ID.r2.cloudflarestorage.com",
"accessKeyId": "your-r2-access-key-id",
"secretAccessKey": "your-r2-secret-access-key",
"prefix": "webhooks/",
"fileFormat": "jsonl",
"partitionBy": "date"
}
}| Field | Required | Description |
|---|---|---|
bucket | Yes | R2 bucket name (3-63 chars) |
endpoint | Yes | R2 S3-compatible API endpoint (https://{account_id}.r2.cloudflarestorage.com) |
accessKeyId | Yes | R2 API token access key ID |
secretAccessKey | Yes | R2 API token secret access key (encrypted at rest) |
prefix | No | Path prefix inside the bucket |
fileFormat | No | jsonl (default) or json |
partitionBy | No | date (default), hour, or source |
Google Cloud Storage
{
"type": "gcs",
"config": {
"bucket": "my-gcs-bucket",
"projectId": "my-gcp-project",
"serviceAccountKey": "{\"type\":\"service_account\",\"project_id\":\"...\"}",
"prefix": "webhooks/",
"fileFormat": "jsonl",
"partitionBy": "date"
}
}| Field | Required | Description |
|---|---|---|
bucket | Yes | GCS bucket name |
projectId | Yes | Google Cloud project ID |
serviceAccountKey | Yes | Service account key JSON (encrypted at rest) |
prefix | No | Path prefix inside the bucket |
fileFormat | No | jsonl (default) or json |
partitionBy | No | date (default), hour, or source |
Azure Blob Storage
{
"type": "azure_blob",
"config": {
"accountName": "myaccount",
"accountKey": "base64-encoded-key",
"containerName": "webhook-data",
"prefix": "webhooks/",
"fileFormat": "jsonl",
"partitionBy": "date"
}
}| Field | Required | Description |
|---|---|---|
accountName | Yes | Azure storage account name |
accountKey | Yes | Storage account key (encrypted at rest) |
containerName | Yes | Blob container name |
prefix | No | Path prefix inside the container |
fileFormat | No | jsonl (default) or json |
partitionBy | No | date (default), hour, or source |
Credential Security
Sensitive fields (secretAccessKey, serviceAccountKey, accountKey) are encrypted at rest using AES-256-GCM with organization-scoped keys. When you retrieve a destination via the API or dashboard, credentials are redacted and displayed as •••• followed by the last 4 characters.
When updating a destination, sending back the redacted value preserves the existing credential. You only need to provide a new value if you are rotating keys.
Field Mapping
By default each row is the raw webhook payload. Set a fieldMapping to write a flat, columnar
schema instead — each row becomes a projection built only from the mappings you list:
{
"fieldMapping": [
{ "source": "data.amount", "target": "amount", "type": "number" },
{ "source": "data.currency", "target": "currency", "type": "string" },
{ "source": "type", "target": "event_type", "type": "string" },
{ "source": "created", "target": "created_at", "type": "timestamp" }
]
}| Mapping Field | Description |
|---|---|
source | Dot path into the payload, e.g. data.amount. A leading $. is accepted and ignored. |
target | Output column name |
type | string, number, boolean, timestamp, or json |
default | Value used when the source path is missing (optional) |
Warning
source is a plain dot path, not full JSONPath: array indexing, wildcards and filter expressions
are not supported. It is also resolved against the payload itself, so do not prefix it with
payload. — $.payload.data.amount looks for a literal payload key inside the webhook body and
almost always resolves to nothing.
timestamp runs the value through new Date(value).toISOString(); json stringifies anything that
is not already a string. A mapping whose path is missing and which has no default is left out of
the row entirely, so rows in the same file can have different keys.
With a field mapping set, the row contains only your mapped columns:
{"amount":2500,"currency":"usd","event_type":"payment_intent.succeeded","created_at":"2026-02-21T14:29:58.000Z"}Warning
There are no _event_id or _received_at metadata columns — nothing is added to a mapped row.
If you need the Hookbase event id in the file, there is no path that yields it; keep an HTTP
destination alongside the warehouse one to capture it.
Testing a Warehouse Destination
Use the Test Connection action (in the dashboard or via API) to verify credentials and permissions. Hookbase uploads a small test file to your bucket and returns the file key, size, and event count.
curl -X POST https://api.hookbase.app/api/destinations/{destinationId}/test \
-H "Authorization: Bearer whr_your_api_key"{
"success": true,
"result": {
"key": "hookbase/2026-02-21/test-1740150000000.jsonl",
"size": 142,
"count": 1
}
}Managing Destinations
List Destinations
curl https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key"Get Destination Details
curl https://api.hookbase.app/api/destinations/{destinationId} \
-H "Authorization: Bearer whr_your_api_key"Update Destination
curl -X PATCH https://api.hookbase.app/api/destinations/{destinationId} \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.yourapp.com/webhooks/v2/handler"
}'Delete Destination
curl -X DELETE https://api.hookbase.app/api/destinations/{destinationId} \
-H "Authorization: Bearer whr_your_api_key"Destination Health
Monitor destination health in the dashboard:
- Success Rate: Percentage of successful deliveries
- Average Latency: Mean response time
- Last Delivery: Timestamp of most recent delivery
- Status: Active, degraded, or failing
Best Practices
-
Use HTTPS: Always use HTTPS URLs for HTTP destinations
-
Set appropriate timeouts: Ensure your endpoint responds within 30 seconds
-
Return 2xx quickly: Acknowledge receipt quickly, process asynchronously if needed
-
Handle duplicates: Webhooks may be delivered more than once; implement idempotency
-
Monitor health: Set up alerts for destinations with high failure rates
-
Use staging destinations: Test changes with a staging destination before production
-
Choose the right partition strategy: Use
datefor low-volume sources,hourfor high-volume sources, andsourcewhen you need to query by origin -
Use JSONL for analytics: JSONL is better suited for data lake queries than JSON arrays
Troubleshooting
Destination shows high failure rate
- Check if the destination URL is accessible
- Verify authentication headers are correct
- Check your application logs for errors
- Use the Event Debugger to inspect request/response
Webhooks not being delivered
- Verify the destination is enabled
- Check if routes are configured correctly
- Ensure filters aren't blocking events
- Check the dead letter queue for failed deliveries
Warehouse test connection fails
- Verify bucket/container name is correct
- Check that credentials have write permissions
- For S3, ensure the region matches the bucket's actual region
- For GCS, verify the service account key JSON is valid
- For Azure, confirm the account key is base64-encoded
See Also
- Destinations API — Full API reference
- Routes — Connect sources to destinations
- Failover Destinations — Configure backup endpoints
- Circuit Breaker — Automatic protection for unhealthy destinations