Destinations API
Destinations are delivery targets for webhooks. Hookbase supports HTTP endpoints and warehouse destinations (S3, R2, GCS, Azure Blob).
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/destinations | List destinations |
| POST | /api/destinations | Create destination |
| GET | /api/destinations/{id} | Get destination |
| PATCH | /api/destinations/{id} | Update destination |
| DELETE | /api/destinations/{id} | Delete destination |
| POST | /api/destinations/{id}/test | Test destination |
| GET | /api/destinations/export | Export destinations as JSON |
| POST | /api/destinations/import | Import destinations from JSON |
| DELETE | /api/destinations/bulk | Bulk delete destinations |
Destination Object
HTTP Destination
{
"id": "22222222-2222-4222-8222-222222222222",
"name": "Production API",
"type": "http",
"url": "https://api.yourapp.com/webhooks",
"method": "POST",
"headers": {
"X-Custom-Header": "value"
},
"authType": "bearer",
"authConfig": {
"token": "sk_live_xxx"
},
"timeoutMs": 30000,
"throttle": {
"mode": "rate",
"rateLimit": 500,
"rateUnit": "minute",
"maxConcurrency": null,
"queueLimit": null
},
"isActive": true,
"useStaticIp": false,
"routeCount": 3,
"deliveryCount": 5230,
"successCount": 5152,
"failureCount": 78,
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-15T10:30:00Z"
}Warehouse Destination (S3 example)
{
"id": "22222222-2222-4222-8222-222222220005",
"name": "Analytics Data Lake",
"type": "s3",
"url": "",
"config": {
"bucket": "my-data-lake",
"region": "us-east-1",
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
"secretAccessKey": "••••EKEY"
},
"fieldMapping": [
{ "source": "$.payload.type", "target": "event_type", "type": "string" },
{ "source": "$.payload.data.amount", "target": "amount", "type": "number" }
],
"isActive": true,
"createdAt": "2026-02-21T10:30:00Z",
"updatedAt": "2026-02-21T10:30:00Z"
}Info
Credential Redaction
Sensitive fields (secretAccessKey, serviceAccountKey, accountKey) are returned as •••• followed by the last 4 characters. Credentials are encrypted at rest using AES-256-GCM.
List Destinations
GET /api/destinationsQuery Parameters
| Parameter | Type | Description |
|---|---|---|
| page | number | Page number (default: 1) |
| pageSize | number | Items per page (default: 20, max: 100) |
| enabled | boolean | Filter by enabled status |
| search | string | Search by name or URL |
Example
curl https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key"Response
{
"destinations": [
{
"id": "22222222-2222-4222-8222-222222222222",
"name": "Production API",
"type": "http",
"url": "https://api.yourapp.com/webhooks",
"isActive": true,
"routeCount": 3,
"deliveryCount": 5230,
"successCount": 5152,
"failureCount": 78
},
{
"id": "22222222-2222-4222-8222-222222220005",
"name": "Analytics Data Lake",
"type": "s3",
"url": "",
"config": {
"bucket": "my-data-lake",
"region": "us-east-1",
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
"secretAccessKey": "••••EKEY"
},
"isActive": true
}
],
"pagination": {
"total": 5,
"page": 1,
"pageSize": 20
}
}Create Destination
POST /api/destinationsCommon Fields
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Display name |
| slug | string | Yes | URL-safe identifier matching ^[a-z0-9-]+$, max 50 characters. Must be unique in the organization |
| type | string | No | http (default), s3, r2, gcs, or azure_blob |
| enabled | boolean | No | Active status (default: true) |
HTTP-Specific Fields
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | Delivery URL (HTTPS recommended) |
| method | string | No | HTTP method: GET, POST, PUT, PATCH, DELETE (default: POST). See Request bodies by method |
| headers | object | No | Custom headers to include |
| authType | string | No | Authentication type (see below) |
| authConfig | object | No | Authentication configuration |
| timeoutMs | number | No | Request timeout in milliseconds (min: 1000, default: 30000, max: 60000) |
| throttle | object | No | Throttle config for outbound delivery: { mode: "off" | "rate" | "concurrency", rateLimit?, rateUnit?: "second" | "minute" | "hour", maxConcurrency?, queueLimit? } (default mode: off, unlimited). rateLimit+rateUnit are required when mode is rate; maxConcurrency is required when mode is concurrency; queueLimit is optional in either mode. Requires a paid plan for modes other than off. |
Info
Retries are not configurable per destination. Every route delivery gets five attempts, 10s / 30s
/ 2m / 10m apart, and timeoutMs is the only delivery-timing field a destination has. See
Production Readiness.
Request bodies by method
Whether a delivery carries the event payload as a body depends on the verb and on the payload itself:
| Method | Body |
|---|---|
GET, HEAD | Never sent. A body on these verbs is invalid HTTP and some servers reject the request outright, so the payload is dropped and the request goes out bodiless. |
DELETE | Sent. A destination configured with DELETE may legitimately expect a body, so it is not stripped on the verb alone. |
POST, PUT, PATCH | Sent. |
An empty payload is dropped on every method: a query-less GET, HEAD or DELETE at ingest is
stored as {}, and relaying a literal {}, [] or empty string downstream is noise rather than
signal. Non-JSON bodies (raw text, form encoding, XML) and JSON scalars count as real content and
are always relayed.
Info
This applies to route deliveries, cron jobs and scheduled sends. It does not apply to outbound
messaging (endpoints and subscriptions), which is always POST and HMAC-signed
over the exact body — there an empty {} is a legitimate signed event, not noise.
Warehouse-Specific Fields
| Field | Type | Required | Description |
|---|---|---|---|
| config | object | Yes | Provider-specific configuration (see below) |
| fieldMapping | array | No | Field mapping rules for structured output |
Tip
Plan Requirement
Warehouse destinations (s3, r2, gcs, azure_blob) require a Pro or Business plan.
Authentication Types (HTTP)
| Type | Description | Auth Config |
|---|---|---|
none | No authentication | -- |
basic | HTTP Basic Auth | { "username": "...", "password": "..." } |
bearer | Bearer token | { "token": "..." } |
api_key | API key in header | { "headerName": "X-API-Key", "key": "..." } |
custom_header | Custom auth header | { "headerName": "...", "headerValue": "..." } |
Warehouse Config: S3
{
"bucket": "my-data-lake",
"region": "us-east-1",
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
"secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"prefix": "webhooks/",
"fileFormat": "jsonl",
"partitionBy": "date"
}| Field | Type | Required | Description |
|---|---|---|---|
| bucket | string | Yes | S3 bucket name (3-63 chars) |
| region | string | Yes | AWS region |
| accessKeyId | string | Yes | AWS access key ID |
| secretAccessKey | string | Yes | AWS secret access key |
| prefix | string | No | Path prefix |
| fileFormat | string | No | jsonl (default) or json |
| partitionBy | string | No | date (default), hour, or source |
Warehouse Config: R2
{
"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 | Type | Required | Description |
|---|---|---|---|
| bucket | string | Yes | R2 bucket name (3-63 chars) |
| endpoint | string | Yes | R2 S3-compatible API endpoint (https://{account_id}.r2.cloudflarestorage.com) |
| accessKeyId | string | Yes | R2 API token access key ID |
| secretAccessKey | string | Yes | R2 API token secret access key (encrypted at rest) |
| prefix | string | No | Path prefix |
| fileFormat | string | No | jsonl (default) or json |
| partitionBy | string | No | date (default), hour, or source |
Warehouse Config: GCS
{
"bucket": "my-gcs-bucket",
"projectId": "my-gcp-project",
"serviceAccountKey": "{...}",
"prefix": "webhooks/",
"fileFormat": "jsonl",
"partitionBy": "date"
}| Field | Type | Required | Description |
|---|---|---|---|
| bucket | string | Yes | GCS bucket name (3-63 chars) |
| projectId | string | Yes | Google Cloud project ID |
| serviceAccountKey | string | Yes | Service account key JSON |
| prefix | string | No | Path prefix |
| fileFormat | string | No | jsonl (default) or json |
| partitionBy | string | No | date (default), hour, or source |
Warehouse Config: Azure Blob
{
"accountName": "myaccount",
"accountKey": "base64-encoded-key",
"containerName": "webhook-data",
"prefix": "webhooks/",
"fileFormat": "jsonl",
"partitionBy": "date"
}| Field | Type | Required | Description |
|---|---|---|---|
| accountName | string | Yes | Storage account name (3-24 chars) |
| accountKey | string | Yes | Base64-encoded account key |
| containerName | string | Yes | Blob container name (3-63 chars) |
| prefix | string | No | Path prefix |
| fileFormat | string | No | jsonl (default) or json |
| partitionBy | string | No | date (default), hour, or source |
Field Mapping
| Field | Type | Required | Description |
|---|---|---|---|
| source | string | Yes | JSONPath expression (e.g., $.payload.amount) |
| target | string | Yes | Output column name |
| type | string | Yes | string, number, boolean, timestamp, or json |
| default | string | No | Default value if source path is missing |
Maximum 50 field mappings per destination.
Example: Create 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"
},
"timeoutMs": 15000
}'Example: Create S3 Warehouse Destination
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Analytics Data Lake",
"slug": "analytics-data-lake",
"type": "s3",
"config": {
"bucket": "my-data-lake",
"region": "us-east-1",
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
"secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"prefix": "webhooks/stripe/",
"fileFormat": "jsonl",
"partitionBy": "date"
},
"fieldMapping": [
{ "source": "$.payload.type", "target": "event_type", "type": "string" },
{ "source": "$.payload.data.amount", "target": "amount", "type": "number" },
{ "source": "$.payload.created", "target": "created_at", "type": "timestamp" }
]
}'Response
{
"destination": {
"id": "22222222-2222-4222-8222-22222222000a",
"name": "Analytics Data Lake",
"type": "s3",
"config": {
"bucket": "my-data-lake",
"region": "us-east-1",
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
"secretAccessKey": "••••EKEY"
},
"fieldMapping": [
{ "source": "$.payload.type", "target": "event_type", "type": "string" },
{ "source": "$.payload.data.amount", "target": "amount", "type": "number" },
{ "source": "$.payload.created", "target": "created_at", "type": "timestamp" }
],
"isActive": true,
"createdAt": "2026-02-21T10:30:00Z",
"updatedAt": "2026-02-21T10:30:00Z"
}
}Get Destination
GET /api/destinations/{id}Example
curl https://api.hookbase.app/api/destinations/22222222-2222-4222-8222-222222222222 \
-H "Authorization: Bearer whr_your_api_key"Response
Returns the full destination object. Sensitive credential fields are redacted.
Update Destination
PATCH /api/destinations/{id}Request Body
All fields are optional. Only provided fields are updated.
| Field | Type | Description |
|---|---|---|
| name | string | Display name |
| url | string | Delivery URL (HTTP destinations) |
| headers | object | Custom headers (replaces existing) |
| config | object | Warehouse configuration |
| fieldMapping | array | Field mapping rules |
| enabled | boolean | Active status |
Info
Updating Credentials
When updating a warehouse destination, redacted credential values (e.g., ••••EKEY) are automatically preserved. Only provide a new value if you are rotating keys.
Example
curl -X PATCH https://api.hookbase.app/api/destinations/22222222-2222-4222-8222-222222222222 \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.yourapp.com/webhooks/v2/handler",
"timeoutMs": 45000
}'Response
Returns the updated destination object.
Delete Destination
DELETE /api/destinations/{id}Warning
Deleting a destination removes it from all routes. Deliveries are preserved for history.
Example
curl -X DELETE https://api.hookbase.app/api/destinations/22222222-2222-4222-8222-222222222222 \
-H "Authorization: Bearer whr_your_api_key"Response
204 No ContentTest Destination
Send a test webhook (HTTP) or upload a test file (warehouse) to verify the destination is configured correctly.
POST /api/destinations/{id}/testRequest Body (Optional)
| Field | Type | Description |
|---|---|---|
| payload | object | Custom test payload (default: sample webhook event) |
Example
curl -X POST https://api.hookbase.app/api/destinations/22222222-2222-4222-8222-222222222222/test \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"payload": {
"event": "test",
"timestamp": "2024-01-15T10:30:00Z"
}
}'HTTP Destination Response
{
"success": true,
"statusCode": 200,
"latency": 245,
"responseBody": "{\"received\": true}",
"message": "Test webhook delivered successfully"
}Warehouse Destination Response
{
"success": true,
"result": {
"key": "hookbase/2026-02-21/test-1740150000000.jsonl",
"size": 142,
"count": 1
},
"message": "Test file uploaded successfully"
}Error Response
{
"success": false,
"statusCode": 503,
"latency": 30012,
"error": "Connection timeout after 30000ms",
"message": "Test webhook delivery failed"
}Destination Health
Per-destination health and latency metrics (success rate, p50/p95/p99 latency, recent failures) are available through the Analytics API:
GET /api/analytics/destinations/healthExport Destinations
Export destinations as a JSON file for backup or migration to another organization.
GET /api/destinations/exportQuery Parameters
| Parameter | Type | Description |
|---|---|---|
| ids | string | Comma-separated destination IDs to export (optional, exports all if not specified) |
| includeSensitive | boolean | Include auth credentials in export (default: false) |
Example
# Export all destinations (auth credentials redacted)
curl https://api.hookbase.app/api/destinations/export \
-H "Authorization: Bearer whr_your_api_key"
# Export specific destinations with credentials
curl "https://api.hookbase.app/api/destinations/export?ids=22222222-2222-4222-8222-222222220004,22222222-2222-4222-8222-222222220006&includeSensitive=true" \
-H "Authorization: Bearer whr_your_api_key"Response
{
"version": "1.0",
"exportedAt": "2024-01-15T10:30:00Z",
"organizationSlug": "myorg",
"destinations": [
{
"name": "Production API",
"slug": "production-api",
"type": "http",
"url": "https://api.yourapp.com/webhooks",
"method": "POST",
"headers": {
"X-Custom-Header": "value"
},
"authType": "bearer",
"authConfig": {
"token": "***REDACTED***"
},
"timeoutMs": 30000,
"throttle": {
"mode": "off",
"rateLimit": null,
"rateUnit": null,
"maxConcurrency": null,
"queueLimit": null
},
"isActive": true,
"config": null,
"batchSize": 1,
"batchWindowSeconds": 0,
"fieldMapping": null,
"useStaticIp": false
},
{
"name": "Event Warehouse",
"slug": "event-warehouse",
"type": "s3",
"url": "",
"method": "POST",
"headers": null,
"authType": null,
"authConfig": null,
"timeoutMs": 30000,
"throttle": {
"mode": "off",
"rateLimit": null,
"rateUnit": null,
"maxConcurrency": null,
"queueLimit": null
},
"isActive": true,
"config": {
"bucket": "acme-events",
"region": "us-east-1",
"accessKeyId": "••••MPLE",
"secretAccessKey": "••••f2Kx"
},
"batchSize": 500,
"batchWindowSeconds": 60,
"fieldMapping": [
{ "source": "$.id", "target": "event_id", "type": "string" }
],
"useStaticIp": true
}
]
}type decides what the rest of the entry means. An http destination carries a url and no
config; a queue or warehouse destination (s3, r2, gcs, azure_blob, sqs, eventbridge,
servicebus, pubsub, oci_queue) carries a config and an empty url.
Redaction
Credentials are redacted unless you pass includeSensitive=true, and the two kinds are marked
differently:
| Field | Default export | With includeSensitive=true |
|---|---|---|
headers | every value replaced with ***REDACTED*** | actual values |
authConfig | every value replaced with ***REDACTED*** | actual values |
config (queue/warehouse) | credential fields only, replaced with •••• plus the last 4 characters; bucket, region and other non-secret fields are untouched | actual values |
Warning
includeSensitive=true needs write access, not just export access. Export is
read-equivalent and every role including viewer holds it, so asking for credentials without write
access is refused with 403 rather than quietly downgraded to a redacted file.
Warning
A default export of a queue or warehouse destination cannot be imported as-is. Import rejects a
config whose credentials are still redacted, because storing ••••f2Kx as the
secret would produce a destination that looks configured and fails every upload. Either export with
includeSensitive=true or fill the credential fields in before importing. Redacted headers and
authConfig are a warning instead: headers are dropped and auth values blanked, so the destination
still imports and you re-add the credentials afterwards.
Import Destinations
Import destinations from a JSON export file.
POST /api/destinations/importRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
| destinations | array | Yes | Array of destination objects to import |
| conflictStrategy | string | Yes | How to handle slug conflicts: skip, rename, or overwrite |
| validateOnly | boolean | No | If true, validates without importing (default: false) |
Conflict Strategies
| Strategy | Description |
|---|---|
skip | Skip destinations that already exist (by slug) |
rename | Auto-rename conflicting destinations (e.g., api -> api-1) |
overwrite | Update existing destinations with imported data |
Example
curl -X POST https://api.hookbase.app/api/destinations/import \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"destinations": [
{
"name": "Staging API",
"slug": "staging-api",
"type": "http",
"url": "https://staging.yourapp.com/webhooks",
"method": "POST",
"timeoutMs": 30000,
"isActive": true
}
],
"conflictStrategy": "skip"
}'Response
{
"success": true,
"summary": {
"imported": 3,
"skipped": 1,
"overwritten": 0,
"failed": 0
},
"details": {
"imported": ["Staging API", "Development API", "QA Endpoint"],
"skipped": ["Production API"],
"overwritten": [],
"failed": []
}
}Each entry in details.failed is {"name": "...", "error": "..."}. A file with some valid and some
invalid entries imports the valid ones and reports the rest here — the request still returns 200.
Validation
Every entry is validated before anything is written, with the same rules POST /api/destinations
applies:
| Field | Rule |
|---|---|
name | Required |
slug | Required, lowercase alphanumeric and hyphens only |
type | One of the destination types; defaults to http when absent |
url | Required for http, and checked against the same SSRF guard as create (no private or link-local addresses, no non-standard ports). Ignored with a warning for other types |
method | One of the allowed methods |
timeoutMs | 1000–60000 |
authType | One of the allowed auth types |
config | Required for a queue or warehouse type, and validated against that type's schema |
batchSize | 1–1000 |
batchWindowSeconds | 0–300 |
fieldMapping | Up to 50 entries, each {source, target, type} |
Queue and warehouse types are also plan-gated, so importing one on a plan without
warehouse_destinations or sqs_destinations fails that entry.
Validating Without Importing
validateOnly: true returns a different shape — per-entry results instead of the imported/skipped
lists, and nothing is written:
{
"valid": false,
"validationResults": [
{
"index": 0,
"name": "Event Warehouse",
"slug": "event-warehouse",
"status": "error",
"errors": ["Configuration has redacted credentials (secretAccessKey). Re-export with includeSensitive=true or set them in the file before importing."],
"warnings": []
},
{
"index": 1,
"name": "Staging API",
"slug": "staging-api",
"status": "rename",
"errors": [],
"warnings": ["Destination with slug 'staging-api' already exists, will be renamed to 'staging-api-1'"],
"resolvedSlug": "staging-api-1"
}
],
"summary": {
"total": 2,
"toCreate": 1,
"toOverwrite": 0,
"toSkip": 0,
"errors": 1
}
}status is one of valid, error, skip, rename or overwrite, and reflects the
conflictStrategy you sent.
Bulk Delete Destinations
Delete multiple destinations in a single request.
DELETE /api/destinations/bulkRequest Body
{
"ids": ["22222222-2222-4222-8222-222222220002", "22222222-2222-4222-8222-222222220003", "22222222-2222-4222-8222-222222220008"]
}Example
curl -X DELETE https://api.hookbase.app/api/destinations/bulk \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"ids": ["22222222-2222-4222-8222-222222220002", "22222222-2222-4222-8222-222222220003"]}'Response
{
"success": true,
"deleted": 2
}Warning
Deleting destinations removes them from all routes. Delivery history is preserved.
Error Responses
400 Bad Request
Invalid URL:
{
"error": "Bad Request",
"message": "Invalid URL format",
"code": "INVALID_URL"
}Invalid warehouse config:
{
"error": "Bad Request",
"message": "Invalid warehouse configuration: bucket must be 3-63 characters"
}403 Forbidden
Plan restriction:
{
"error": "Forbidden",
"message": "Warehouse destinations require Pro or Business plan."
}404 Not Found
Destination not found:
{
"error": "Not Found",
"message": "Destination with ID 22222222-2222-4222-8222-22222222000b not found",
"code": "RESOURCE_NOT_FOUND"
}