openapi: 3.1.0 info: title: HookBridge API version: 1.0.0 description: | HookBridge is a webhook delivery reliability platform that provides: - Guaranteed delivery with intelligent retries - Full observability through logs and metrics - HMAC signing for security - Idempotency guarantees - Replay capabilities for failed deliveries ## Authentication All API requests require authentication via API key. Include your API key in the Authorization header: ``` Authorization: Bearer hb_live_xxxxxxxxxxxxxxxxxxxx ``` API keys can be created and managed via the `/v1/api-keys` endpoints or through the web console. ## Rate Limits - Project-level: 1000 requests per minute - Endpoint-level: 10 requests per second ## Webhook Delivery When HookBridge delivers webhooks to your customers' endpoints, it includes: - `X-Webhook-Signature`: HMAC-SHA256 signature for payload verification - `X-Webhook-Id`: Unique message ID for tracking - `X-Webhook-Timestamp`: Unix timestamp of the original send request - Custom headers you specify in the send request contact: name: HookBridge Support email: support@hookbridge.io url: https://hookbridge.io/docs license: name: Proprietary servers: - url: https://api.hookbridge.io description: Production security: - BearerAuth: [] tags: - name: Health description: Service health checks - name: Webhooks description: Send webhooks for delivery - name: Endpoints description: Manage webhook endpoints - name: Messages description: Query and manage message delivery - name: DLQ description: Dead Letter Queue management for failed messages - name: Observability description: Logs and metrics - name: API Keys description: Manage API keys for authentication - name: Projects description: Project management - name: Billing description: Subscription management and Stripe integration - name: Inbound Endpoints description: Manage inbound webhook endpoints for receiving webhooks from external sources - name: Pull Endpoints description: Manage pull mode endpoints for consumer-controlled event retrieval - name: Pull Events description: Retrieve and acknowledge events stored at pull endpoints - name: Pull Observability description: Logs and metrics for pull endpoint events - name: Inbound Messages description: Query, replay, and monitor inbound webhook messages - name: Exports description: CSV export jobs for webhook delivery data paths: /health: get: tags: - Health summary: Health check description: Returns the health status of the API service operationId: healthCheck security: [] responses: '200': description: Service is healthy content: application/json: schema: type: object properties: status: type: string example: "healthy" service: type: string example: "hookbridge-api" version: type: string example: "1.0.0" timestamp: type: string format: date-time example: "2025-12-06T01:30:00Z" /v1/webhooks/send: post: tags: - Webhooks summary: Send a webhook description: | Enqueue a webhook for delivery. The webhook will be stored durably and delivered with automatic retries on failure. Returns immediately with a message ID for tracking. **Retry Behavior:** - Fast retries (for 429 responses): 30s, 60s, 120s, 240s, 300s - Slow retries (for other errors): 30m, 2h, 6h, 12h, 24h, 48h, 72h, 96h - Maximum 8 total attempts before moving to DLQ operationId: sendWebhook servers: - url: https://send.hookbridge.io description: Send service (separate host from the main API) requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SendWebhookRequest' examples: basic: summary: Basic webhook value: endpoint_id: "ep_550e8400e29b41d4a716446655440000" payload: event: "order.created" order_id: "ord_12345" amount: 99.99 with_headers: summary: Webhook with custom headers value: endpoint_id: "ep_550e8400e29b41d4a716446655440000" payload: event: "order.created" order_id: "ord_12345" headers: X-Tenant-Id: "tenant_abc" X-Event-Type: "order.created" with_idempotency: summary: Webhook with idempotency key value: endpoint_id: "ep_550e8400e29b41d4a716446655440000" payload: event: "order.created" order_id: "ord_12345" idempotency_key: "order-12345-created-v1" responses: '200': description: Webhook successfully enqueued content: application/json: schema: $ref: '#/components/schemas/SendWebhookResponse' example: data: message_id: "01935abc-def0-7123-4567-890abcdef012" status: "queued" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '409': description: Idempotency key conflict content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "IDEMPOTENCY_MISMATCH" message: "Idempotency key already used with different payload" meta: request_id: "req_xyz123" '413': description: Payload too large content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "PAYLOAD_TOO_LARGE" message: "Payload exceeds maximum size of 1 MB" meta: request_id: "req_xyz123" '429': $ref: '#/components/responses/RateLimitExceeded' /v1/messages/{id}: get: tags: - Messages summary: Get message details description: Retrieve the status, metadata, and delivery information for a specific message operationId: getMessage parameters: - $ref: '#/components/parameters/MessageId' responses: '200': description: Message details retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetMessageResponse' example: data: id: "01935abc-def0-7123-4567-890abcdef012" project_id: "proj_abc123" endpoint_id: "ep_def456" status: "succeeded" attempt_count: 2 replay_count: 0 content_type: "application/json" size_bytes: 256 payload_sha256: "abc123..." response_status: 200 response_latency_ms: 245 created_at: "2025-12-06T12:00:00Z" updated_at: "2025-12-06T12:01:30Z" meta: request_id: "req_xyz123" '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' delete: tags: - Messages summary: Delete a message description: | Soft-delete a single outbound message. The payload becomes immediately unreachable; the S3 object is removed by the sweeper on its next run. Metadata and delivery-attempt history are preserved. The original status is kept unchanged. Idempotent: deleting an already-deleted message returns 200 with `already_deleted: true` and the original `deleted_at` timestamp. operationId: deleteMessage parameters: - $ref: '#/components/parameters/MessageId' responses: '200': description: Message deleted (or already deleted) content: application/json: schema: $ref: '#/components/schemas/DeleteMessageResponse' example: data: message_id: "01935abc-def0-7123-4567-890abcdef012" deleted_at: "2026-04-05T14:23:11.123Z" already_deleted: false meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' /v1/messages/delete-batch: post: tags: - Messages summary: Batch delete outbound messages by ID description: | Soft-delete a batch of outbound messages by their IDs. Returns per-ID outcomes: `deleted`, `already_deleted`, or `not_found`. Maximum 100 IDs per request. operationId: deleteMessagesBatch requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DeleteBatchRequest' example: message_ids: - "01935abc-def0-7123-4567-890abcdef012" - "01935abc-def0-7123-4567-890abcdef013" responses: '200': description: Batch delete results content: application/json: schema: $ref: '#/components/schemas/DeleteBatchResponse' example: data: results: - message_id: "01935abc-def0-7123-4567-890abcdef012" outcome: "deleted" deleted_at: "2026-04-05T14:23:11.123Z" - message_id: "01935abc-def0-7123-4567-890abcdef013" outcome: "not_found" deleted_count: 1 already_deleted_count: 0 not_found_count: 1 meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' /v1/messages/delete-all: post: tags: - Messages summary: Delete all outbound messages matching filters description: | Soft-delete outbound messages matching the specified filters. Processes in chunks for reliability. Returns the count and IDs of deleted messages. On error, the response includes an `error` object: `DELETE_FAILED` if no rows were deleted, or `PARTIAL_DELETE_FAILURE` if some rows were committed before the error. Maximum 1000 messages per request. operationId: deleteMessagesAll parameters: - name: status in: query description: Comma-separated list of statuses to match (e.g. `succeeded,failed_permanent`). schema: type: string - name: endpoint_id in: query description: Restrict to messages for this endpoint. schema: type: string format: uuid - name: created_after in: query description: Only delete messages created at or after this time (RFC 3339). schema: type: string format: date-time - name: created_before in: query description: Only delete messages created before this time (RFC 3339). schema: type: string format: date-time - name: limit in: query description: Maximum messages to delete (1–1000, default 1000). schema: type: integer minimum: 1 maximum: 1000 default: 1000 responses: '200': description: Delete-all results content: application/json: schema: $ref: '#/components/schemas/DeleteAllResponse' example: data: deleted: 42 deleted_message_ids: - "01935abc-def0-7123-4567-890abcdef012" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' /v1/messages/{id}/attempts: get: tags: - Messages summary: Get message delivery attempts description: | Retrieve all delivery attempts for a specific message, including timing breakdown, response details, and connection metadata. Useful for debugging delivery failures. Response body URLs are only available on Starter plans and above. **Note:** If the message ID does not exist or belongs to a different project, this endpoint returns 200 with an empty `data` array (not 404). Use `GET /v1/messages/{id}` first to verify the message exists. **Note:** Results are capped at 200 attempts. If `has_more` is true in the response, additional attempts exist but cannot be retrieved yet. Cursor-based pagination will be added in a future release. operationId: getMessageAttempts parameters: - $ref: '#/components/parameters/MessageId' responses: '200': description: | Attempts retrieved successfully. Returns an empty array if the message has no attempts yet or if the message ID is not found. content: application/json: schema: $ref: '#/components/schemas/GetMessageAttemptsResponse' example: data: - id: "01935abc-def0-7123-4567-890abcdef001" attempt_no: 1 response_status: 500 response_latency_ms: 245 error_text: "HTTP 500 Internal Server Error" retry_type: "slow" retry_after_seconds: 1800 dns_ms: 12 tcp_connect_ms: 25 tls_handshake_ms: 45 ttfb_ms: 200 transfer_ms: 8 conn_reused: false created_at: "2025-12-06T12:00:00Z" - id: "01935abc-def0-7123-4567-890abcdef002" attempt_no: 2 response_status: 200 response_latency_ms: 120 dns_ms: 0 tcp_connect_ms: 0 tls_handshake_ms: 0 ttfb_ms: 115 transfer_ms: 5 conn_reused: true created_at: "2025-12-06T12:30:00Z" meta: request_id: "req_xyz123" has_more: false '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' /v1/messages/{id}/replay: post: tags: - Messages summary: Replay a message description: | Re-enqueue a message for delivery. This creates a new delivery attempt using the original payload. Useful for recovering from transient failures, testing endpoints, or re-processing a successfully delivered message. Both `failed` and `succeeded` messages can be replayed, as long as the payload is still within its retention window. Retention is anchored to the original `delivered_at` timestamp for succeeded messages, so replaying does not extend the retention window. **Limits:** Maximum 10 replays per message. operationId: replayMessage parameters: - $ref: '#/components/parameters/MessageId' responses: '200': description: Message successfully re-enqueued content: application/json: schema: $ref: '#/components/schemas/ReplayMessageResponse' example: message: "Message queued for replay" data: message_id: "01935abc-def0-7123-4567-890abcdef012" status: "queued" meta: request_id: "req_xyz123" '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' '409': description: > Message cannot be replayed due to its current state. Possible error codes: `DELIVERY_IN_PROGRESS` (message is currently being delivered), `RETRY_IN_PROGRESS` (message is in active fast retry after a 429 response). content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: retry_in_progress: summary: Fast retry in progress value: error: code: "RETRY_IN_PROGRESS" message: "Message is in active fast retry; wait for retries to complete or fail before replaying" meta: request_id: "req_xyz123" delivery_in_progress: summary: Delivery in progress value: error: code: "DELIVERY_IN_PROGRESS" message: "Message is currently being delivered; wait for delivery to complete before replaying" meta: request_id: "req_xyz123" '410': description: > Message payload is no longer available for replay. Possible error codes: `RETENTION_EXPIRED` (the message's retention window has elapsed), `PAYLOAD_UNAVAILABLE` (the payload was cleaned up by the sweeper before retention expired), `MESSAGE_DELETED` (the message was explicitly deleted). content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: retention_expired: summary: Retention window elapsed value: error: code: "RETENTION_EXPIRED" message: "Message retention period has expired and payload is no longer available for replay" meta: request_id: "req_xyz123" payload_unavailable: summary: Payload already cleaned up value: error: code: "PAYLOAD_UNAVAILABLE" message: "Message payload has already been cleaned up and cannot be replayed" meta: request_id: "req_xyz123" message_deleted: summary: Message explicitly deleted value: error: code: "MESSAGE_DELETED" message: "Message has been deleted and cannot be replayed" meta: request_id: "req_xyz123" '429': description: Replay limit exceeded content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "REPLAY_LIMIT_EXCEEDED" message: "Message has already been replayed 10 times (limit: 10)" meta: request_id: "req_xyz123" /v1/messages/{id}/cancel: post: tags: - Messages summary: Cancel a pending retry description: | Cancel a message that is waiting for a scheduled retry. The message will be marked as `failed_permanent` and moved to the DLQ. Only works for messages in `pending_retry` status. operationId: cancelRetry parameters: - $ref: '#/components/parameters/MessageId' responses: '200': description: Retry cancelled successfully content: application/json: schema: type: object properties: data: type: object properties: message_id: type: string status: type: string enum: [failed_permanent] meta: $ref: '#/components/schemas/ResponseMeta' example: data: message_id: "01935abc-def0-7123-4567-890abcdef012" status: "failed_permanent" meta: request_id: "req_xyz123" '400': description: Message is not in pending_retry status content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "INVALID_STATUS" message: "Message is not in pending_retry status (current: succeeded)" meta: request_id: "req_xyz123" '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' /v1/messages/{id}/retry-now: post: tags: - Messages summary: Trigger immediate retry description: | Trigger an immediate retry for a message that is waiting for a scheduled retry. The message will be picked up by the scheduler on its next run (typically within 1 minute). Only works for messages in `pending_retry` status. operationId: retryNow parameters: - $ref: '#/components/parameters/MessageId' responses: '200': description: Immediate retry scheduled content: application/json: schema: type: object properties: message: type: string data: type: object properties: message_id: type: string status: type: string enum: [pending_retry] meta: $ref: '#/components/schemas/ResponseMeta' example: message: "Retry scheduled for immediate pickup" data: message_id: "01935abc-def0-7123-4567-890abcdef012" status: "pending_retry" meta: request_id: "req_xyz123" '400': description: Message is not in pending_retry status content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' /v1/messages/replay-all: post: tags: - Messages summary: Replay all matching outbound messages description: | Replay multiple outbound messages matching the specified filters. Messages are re-enqueued for delivery to their configured endpoints. operationId: replayAllMessages parameters: - name: status in: query required: true description: Filter messages by status (only replayable statuses allowed) schema: type: string enum: [failed_permanent, pending_retry] example: "failed_permanent" - name: endpoint_id in: query description: Filter by specific endpoint schema: type: string example: "ep_01935abc" - name: limit in: query description: Maximum number of messages to replay schema: type: integer minimum: 1 maximum: 1000 default: 100 example: 100 responses: '200': description: Messages replayed successfully content: application/json: schema: $ref: '#/components/schemas/ReplayAllMessagesResponse' example: message: "5 messages queued for replay" data: replayed: 5 failed: 0 stuck: 0 replayed_message_ids: - "01935abc-def0-7123-4567-890abcdef012" - "01935abc-def0-7123-4567-890abcdef013" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /v1/messages/replay-batch: post: tags: - Messages summary: Replay specific outbound messages by ID description: | Replay specific outbound messages by providing their IDs. Messages are re-enqueued for delivery to their configured endpoints. operationId: replayBatchMessages requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ReplayBatchRequest' example: message_ids: - "01935abc-def0-7123-4567-890abcdef012" - "01935abc-def0-7123-4567-890abcdef013" responses: '200': description: Messages replayed successfully content: application/json: schema: $ref: '#/components/schemas/ReplayBatchMessagesResponse' example: message: "2 messages replayed" data: replayed: 2 failed: 0 stuck: 0 results: - message_id: "01935abc-def0-7123-4567-890abcdef012" status: "replayed" - message_id: "01935abc-def0-7123-4567-890abcdef013" status: "replayed" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /v1/logs: get: tags: - Observability summary: Query delivery logs description: | Retrieve a paginated list of webhook messages with optional filters. Useful for debugging, monitoring, and auditing webhook deliveries. operationId: getLogs parameters: - name: endpoint_id in: query description: Filter by specific endpoint schema: type: string pattern: '^ep_[a-f0-9]{32}$' example: "ep_550e8400e29b41d4a716446655440000" - name: status in: query description: Filter by message status schema: type: string enum: [queued, delivering, succeeded, pending_retry, failed_permanent] example: "failed_permanent" - name: start_time in: query description: Filter messages created after this timestamp (ISO 8601) schema: type: string format: date-time example: "2025-12-01T00:00:00Z" - name: end_time in: query description: Filter messages created before this timestamp (ISO 8601) schema: type: string format: date-time example: "2025-12-06T23:59:59Z" - name: limit in: query description: Maximum number of results to return schema: type: integer minimum: 1 maximum: 500 default: 50 example: 100 - name: cursor in: query description: Pagination cursor from previous response schema: type: string example: "eyJpZCI6Im1zZ18xMjMiLCJ0cyI6MTczMzQ1Njc4OX0=" responses: '200': description: Logs retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetLogsResponse' example: data: - message_id: "01935abc-def0-7123-4567-890abcdef012" endpoint: "https://customer.app/webhooks" status: "succeeded" attempt_count: 1 created_at: "2025-12-06T12:00:00Z" delivered_at: "2025-12-06T12:00:05Z" response_status: 200 response_latency_ms: 120 - message_id: "01935abc-def0-7123-4567-890abcdef013" endpoint: "https://customer.app/webhooks" status: "pending_retry" attempt_count: 3 created_at: "2025-12-06T11:00:00Z" next_attempt_at: "2025-12-06T17:00:00Z" last_error: "connection timeout" meta: next_cursor: "eyJpZCI6Im1zZ18xMjMiLCJ0cyI6MTczMzQ1Njc4OX0=" has_more: true request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /v1/metrics: get: tags: - Observability summary: Get delivery metrics description: | Retrieve aggregated metrics for webhook deliveries including success rates, latency, and retry counts. operationId: getMetrics parameters: - name: window in: query description: Time window for aggregation schema: type: string enum: [1h, 24h, 7d, 30d] default: 24h example: "24h" - name: endpoint_id in: query description: Filter metrics by specific endpoint schema: type: string pattern: '^ep_[a-f0-9]{32}$' example: "ep_550e8400e29b41d4a716446655440000" responses: '200': description: Metrics retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetMetricsResponse' example: data: window: "24h" total_messages: 10000 succeeded: 9950 failed: 50 retries: 150 success_rate: 0.995 avg_latency_ms: 185 meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /v1/dlq/messages: get: tags: - DLQ summary: List DLQ messages description: | Retrieve messages that have permanently failed delivery (DLQ). These messages have exhausted all retry attempts and require manual intervention. operationId: getDLQMessages parameters: - name: limit in: query description: Maximum number of results to return schema: type: integer minimum: 1 maximum: 1000 default: 100 example: 100 - name: cursor in: query description: Pagination cursor from previous response schema: type: string responses: '200': description: DLQ messages retrieved successfully content: application/json: schema: type: object properties: data: type: object properties: messages: type: array items: $ref: '#/components/schemas/MessageSummary' has_more: type: boolean next_cursor: type: string meta: $ref: '#/components/schemas/ResponseMeta' '401': $ref: '#/components/responses/Unauthorized' /v1/dlq/replay/{id}: post: tags: - DLQ summary: Replay message from DLQ description: | Re-enqueue a message from the DLQ for delivery. Unlike regular replays, DLQ replays have no limit - you can always recover failed messages. operationId: replayFromDLQ parameters: - $ref: '#/components/parameters/MessageId' responses: '200': description: Message replayed from DLQ content: application/json: schema: type: object properties: data: type: object properties: message_id: type: string status: type: string enum: [replayed] meta: $ref: '#/components/schemas/ResponseMeta' example: data: message_id: "01935abc-def0-7123-4567-890abcdef012" status: "replayed" meta: request_id: "req_xyz123" '400': description: Message is not in DLQ content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "INVALID_REQUEST" message: "message is not in DLQ (must have failed_permanent status)" meta: request_id: "req_xyz123" '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' /v1/api-keys: get: tags: - API Keys summary: List API keys description: | List all API keys for the authenticated project. Only key metadata is returned; the actual key values are never exposed after creation. operationId: listAPIKeys responses: '200': description: API keys retrieved successfully content: application/json: schema: $ref: '#/components/schemas/ListAPIKeysResponse' example: data: - key_id: "key_abc123" label: "Production backend" prefix: "hb_live_abc" created_at: "2025-12-01T10:00:00Z" last_used_at: "2025-12-06T01:30:00Z" - key_id: "key_def456" label: "Staging backend" prefix: "hb_test_def" created_at: "2025-12-05T14:00:00Z" meta: request_id: "req_xyz123" '401': $ref: '#/components/responses/Unauthorized' post: tags: - API Keys summary: Create API key description: | Create a new API key for the project. The full key value is only returned once in this response - store it securely as it cannot be retrieved again. **Important:** The plaintext key in the response is the only time you will see the full key. Store it securely immediately. operationId: createAPIKey requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateAPIKeyRequest' examples: with_label: summary: Create an API key with label value: label: "Production backend" minimal: summary: Create an API key without label value: {} responses: '201': description: API key created successfully content: application/json: schema: $ref: '#/components/schemas/CreateAPIKeyResponse' example: data: key_id: "key_abc123" key: "hb_live_abcdefghijklmnopqrstuvwxyz123456" prefix: "hb_live_abc" label: "Production backend" created_at: "2025-12-06T01:30:00Z" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '409': description: API key limit exceeded content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "KEY_LIMIT_EXCEEDED" message: "Maximum of 20 API keys per project" meta: request_id: "req_xyz123" /v1/api-keys/{id}: delete: tags: - API Keys summary: Delete API key description: | Permanently deactivate an API key. The key will immediately stop working for authentication. This action cannot be undone. operationId: deleteAPIKey parameters: - name: id in: path required: true description: API key ID schema: type: string example: "key_abc123" responses: '200': description: API key deleted successfully content: application/json: schema: type: object properties: data: type: object properties: deleted: type: boolean meta: $ref: '#/components/schemas/ResponseMeta' example: data: deleted: true meta: request_id: "req_xyz123" '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' /v1/endpoints: get: tags: - Endpoints summary: List endpoints description: | List all endpoints for the authenticated project. Supports cursor-based pagination. operationId: listEndpoints parameters: - name: limit in: query description: Maximum number of results to return schema: type: integer minimum: 1 maximum: 100 default: 50 example: 50 - name: cursor in: query description: Pagination cursor from previous response schema: type: string responses: '200': description: Endpoints retrieved successfully content: application/json: schema: $ref: '#/components/schemas/ListEndpointsResponse' example: data: - id: "ep_550e8400e29b41d4a716446655440000" url: "https://customer.app/webhooks" description: "Main production webhook" created_at: "2025-12-01T10:00:00Z" meta: request_id: "req_xyz123" next_cursor: null '401': $ref: '#/components/responses/Unauthorized' post: tags: - Endpoints summary: Create endpoint description: | Create a new webhook endpoint. The endpoint URL must be HTTPS and publicly accessible. A signing secret is automatically generated and returned only in this response. **Important:** The `signing_secret` is only returned once. Store it securely for verifying webhook signatures. operationId: createEndpoint requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateEndpointRequest' examples: basic: summary: Basic endpoint value: url: "https://customer.app/webhooks" with_options: summary: Endpoint with rate limiting value: url: "https://customer.app/webhooks" description: "Main production webhook" rate_limit_rps: 10 burst: 20 headers: X-Custom-Header: "value" responses: '201': description: Endpoint created successfully content: application/json: schema: $ref: '#/components/schemas/CreateEndpointResponse' example: data: id: "ep_550e8400e29b41d4a716446655440000" url: "https://customer.app/webhooks" description: "Main production webhook" signing_key_id: "sk_550e8400e29b41d4a716446655440001" signing_secret: "whsec_abcdefghijklmnopqrstuvwxyz123456" key_hint: "abcd" created_at: "2025-12-06T12:00:00Z" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /v1/endpoints/{id}: get: tags: - Endpoints summary: Get endpoint description: | Retrieve details of a specific endpoint. The signing secret is not included. operationId: getEndpoint parameters: - $ref: '#/components/parameters/EndpointId' responses: '200': description: Endpoint details retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetEndpointResponse' example: data: id: "ep_550e8400e29b41d4a716446655440000" url: "https://customer.app/webhooks" description: "Main production webhook" rate_limit_rps: 10 burst: 20 headers: X-Custom-Header: "value" created_at: "2025-12-01T10:00:00Z" updated_at: "2025-12-06T12:00:00Z" meta: request_id: "req_xyz123" '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' patch: tags: - Endpoints summary: Update endpoint description: | Update an existing endpoint. Only provided fields are updated; omitted fields retain their current values. operationId: updateEndpoint parameters: - $ref: '#/components/parameters/EndpointId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateEndpointRequest' examples: update_url: summary: Update URL only value: url: "https://new-customer.app/webhooks" update_rate_limit: summary: Update rate limiting value: rate_limit_rps: 20 burst: 40 responses: '200': description: Endpoint updated successfully content: application/json: schema: $ref: '#/components/schemas/UpdateEndpointResponse' example: data: id: "ep_550e8400e29b41d4a716446655440000" updated: true meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' delete: tags: - Endpoints summary: Delete endpoint description: | Soft-delete an endpoint. The endpoint will no longer accept new messages, and any in-flight messages will be marked as permanently failed. operationId: deleteEndpoint parameters: - $ref: '#/components/parameters/EndpointId' responses: '200': description: Endpoint deleted successfully content: application/json: schema: type: object properties: data: type: object properties: deleted: type: boolean meta: $ref: '#/components/schemas/ResponseMeta' example: data: deleted: true meta: request_id: "req_xyz123" '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' /v1/endpoints/{id}/pause: post: tags: - Endpoints summary: Pause endpoint description: | Pause an endpoint. While paused, messages sent to this endpoint will be accepted and stored but not delivered. Messages queued during pause are transitioned to `paused` status. Use the resume endpoint to re-enable delivery and requeue paused messages. operationId: pauseEndpoint parameters: - $ref: '#/components/parameters/EndpointId' responses: '200': description: Endpoint paused successfully content: application/json: schema: type: object properties: data: type: object properties: id: type: string description: Endpoint identifier paused: type: boolean description: Whether the endpoint is paused (always true on success) meta: $ref: '#/components/schemas/ResponseMeta' example: data: id: "ep_550e8400e29b41d4a716446655440000" paused: true meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '409': description: Endpoint is already paused content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "ALREADY_PAUSED" message: "Endpoint is already paused" meta: request_id: "req_xyz123" /v1/endpoints/{id}/resume: post: tags: - Endpoints summary: Resume endpoint description: | Resume a paused endpoint. Delivery to this endpoint will restart, and any messages that were paused will be scheduled for re-delivery. The retry scheduler picks up these messages within its polling interval (typically 1–5 minutes). The response includes the count of messages that were scheduled. operationId: resumeEndpoint parameters: - $ref: '#/components/parameters/EndpointId' responses: '200': description: Endpoint resumed successfully content: application/json: schema: type: object properties: data: type: object properties: id: type: string description: Endpoint identifier paused: type: boolean description: Whether the endpoint is paused (always false on success) messages_requeued: type: integer format: int64 description: Number of paused messages scheduled for re-delivery by the retry scheduler meta: $ref: '#/components/schemas/ResponseMeta' example: data: id: "ep_550e8400e29b41d4a716446655440000" paused: false messages_requeued: 42 meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '409': description: Endpoint is not paused content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "NOT_PAUSED" message: "Endpoint is not paused" meta: request_id: "req_xyz123" /v1/endpoints/{id}/signing-keys: post: tags: - Endpoints summary: Create signing key description: | Create an additional signing key for this endpoint. Maximum 2 active keys per endpoint. **Important:** The new `signing_secret` is only returned once. Store it securely. operationId: createEndpointSigningKey parameters: - $ref: '#/components/parameters/EndpointId' responses: '201': description: Signing key created successfully content: application/json: schema: $ref: '#/components/schemas/CreateSigningKeyResponse' example: data: id: "sk_550e8400e29b41d4a716446655440001" signing_secret: "whsec_newabcdefghijklmnopqrstuvwxyz12" key_hint: "wxyz" created_at: "2025-12-06T12:10:00Z" meta: request_id: "req_xyz123" '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' get: tags: - Endpoints summary: List signing keys description: List active signing keys for an endpoint. operationId: listEndpointSigningKeys parameters: - $ref: '#/components/parameters/EndpointId' responses: '200': description: Signing keys retrieved successfully content: application/json: schema: $ref: '#/components/schemas/ListSigningKeysResponse' example: data: - id: "sk_550e8400e29b41d4a716446655440001" key_hint: "wxyz" created_at: "2025-12-06T12:10:00Z" - id: "sk_550e8400e29b41d4a716446655440000" key_hint: "abcd" created_at: "2025-12-01T10:00:00Z" meta: request_id: "req_xyz123" '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' /v1/endpoints/{id}/signing-keys/{keyId}: delete: tags: - Endpoints summary: Delete signing key description: | Delete an endpoint signing key. You cannot delete the last remaining key. operationId: deleteEndpointSigningKey parameters: - $ref: '#/components/parameters/EndpointId' - name: keyId in: path required: true schema: type: string description: Signing key ID in sk_ format responses: '204': description: Signing key deleted successfully '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' '409': $ref: '#/components/responses/Conflict' /v1/billing/subscription: get: tags: - Billing summary: Get subscription details description: | Retrieve the current subscription status, plan limits, and usage. Returns free tier limits if no paid subscription exists. operationId: getSubscription security: - BearerAuth: [] responses: '200': description: Subscription details retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetSubscriptionResponse' example: data: plan: "pro" status: "active" limits: plan: "pro" messages_per_month: 50000 max_projects: 100 max_endpoints: 10000 retention_days: 7 usage: messages_used: 12500 period_start: "2025-12-01T00:00:00Z" period_end: "2025-12-31T23:59:59Z" cancel_at_period_end: false current_period_end: "2025-12-31T23:59:59Z" meta: request_id: "req_xyz123" '401': $ref: '#/components/responses/Unauthorized' /v1/billing/usage-history: get: tags: - Billing summary: Get usage history description: | Retrieve historical monthly usage metrics for the authenticated tenant. Periods are calendar-month aligned (UTC). operationId: getUsageHistory security: - BearerAuth: [] parameters: - name: limit in: query description: Number of months to return schema: type: integer minimum: 1 maximum: 24 default: 12 - name: offset in: query description: Number of months to skip schema: type: integer minimum: 0 default: 0 responses: '200': description: Usage history retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetUsageHistoryResponse' example: data: - period_start: "2026-02-01" period_end: "2026-02-28" message_count: 6102 overage_count: 1102 plan_limit: 5000 meta: request_id: "req_xyz123" total: 6 limit: 12 offset: 0 has_more: false '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /v1/billing/invoices: get: tags: - Billing summary: Get invoice history description: | Retrieve Stripe invoice history for the authenticated tenant. Free/no-billing tenants return an empty list. operationId: getInvoices security: - BearerAuth: [] parameters: - name: limit in: query description: Number of invoices to return schema: type: integer minimum: 1 maximum: 99 default: 12 - name: starting_after in: query description: Stripe pagination cursor (invoice id from a previous response) schema: type: string responses: '200': description: Invoice history retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetInvoicesResponse' example: data: - id: "in_1abc..." status: "paid" amount_due: 1000 amount_paid: 1000 currency: "usd" period_start: "2026-02-05T00:00:00Z" period_end: "2026-03-05T00:00:00Z" created: "2026-03-05T06:00:00Z" invoice_pdf: "https://pay.stripe.com/invoice/..." hosted_invoice_url: "https://invoice.stripe.com/..." lines: - description: "Starter Plan (Monthly)" amount: 1000 quantity: 1 meta: request_id: "req_xyz123" has_more: false '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /v1/billing/webhook: post: tags: - Billing summary: Stripe webhook handler (internal) description: | **Internal endpoint - not for direct API use.** Handles Stripe webhook events to sync subscription status. Authentication is via Stripe signature verification. operationId: handleStripeWebhook security: [] requestBody: required: true content: application/json: schema: type: object description: Stripe webhook event payload responses: '200': description: Webhook processed successfully content: application/json: schema: type: object properties: received: type: boolean example: true '400': description: Invalid webhook signature or payload content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /v1/metrics/timeseries: get: tags: - Observability summary: Get time-series metrics description: | Retrieve time-bucketed delivery metrics for charting and analysis. Returns data points at intervals appropriate for the selected window. operationId: getTimeSeriesMetrics parameters: - name: window in: query description: Time window for metrics schema: type: string enum: [1h, 24h, 7d, 30d] default: 24h example: "24h" - name: endpoint_id in: query description: Optional filter by endpoint ID schema: type: string pattern: '^ep_[a-f0-9]{32}$' example: "ep_550e8400e29b41d4a716446655440000" responses: '200': description: Time-series metrics retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetTimeSeriesMetricsResponse' example: data: window: "24h" buckets: - timestamp: "2025-12-06T00:00:00Z" succeeded: 450 failed: 5 retrying: 10 total: 465 avg_latency_ms: 180 - timestamp: "2025-12-06T01:00:00Z" succeeded: 520 failed: 3 retrying: 8 total: 531 avg_latency_ms: 165 meta: request_id: "req_xyz123" '400': description: Invalid window or endpoint_id parameter content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': $ref: '#/components/responses/Unauthorized' # ── Inbound Endpoints ────────────────────────────────────────────── /v1/inbound-endpoints: post: tags: - Inbound Endpoints summary: Create inbound endpoint description: | Create a new inbound webhook endpoint for receiving webhooks from external sources. The endpoint URL must be HTTPS and publicly accessible. A secret token and ingest URL are returned only in this response. If `signing_enabled=true`, the initial delivery signing key is also created and its `signing_secret` is returned only in this response. **Important:** The `secret_token`, `ingest_url`, and `signing_secret` are only returned once. Store them securely. operationId: createInboundEndpoint requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateInboundEndpointRequest' examples: basic: summary: Basic inbound endpoint value: url: "https://myapp.com/webhooks/stripe" name: "Stripe webhooks" with_verification: summary: Endpoint with HMAC verification value: url: "https://myapp.com/webhooks/stripe" name: "Stripe webhooks" verify_hmac: true hmac_header_name: "Stripe-Signature" hmac_secret: "whsec_abc123" with_ip_allowlist: summary: Endpoint with IP allowlist value: url: "https://myapp.com/webhooks/partner" name: "Partner webhooks" verify_ip_allowlist: true allowed_cidrs: - "203.0.113.0/24" - "198.51.100.0/24" cli_mode: summary: CLI-mode endpoint (for local development) value: name: "Local dev webhooks" mode: "cli" responses: '201': description: Inbound endpoint created successfully content: application/json: schema: $ref: '#/components/schemas/CreateInboundEndpointResponse' example: data: id: "01935abc-def0-7123-4567-890abcdef012" name: "Stripe webhooks" url: "https://myapp.com/webhooks/stripe" ingest_url: "https://receive.hookbridge.io/v1/webhooks/receive/01935abc-def0-7123-4567-890abcdef012/a1b2c3d4e5f6" secret_token: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" signing_key_id: "sk_550e8400e29b41d4a716446655440001" signing_secret: "whsec_newabcdefghijklmnopqrstuvwxyz12" key_hint: "wxyz" created_at: "2025-12-06T12:00:00Z" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '409': description: Duplicate URL conflict content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "DUPLICATE_URL" message: "An inbound endpoint with this URL already exists in this project" meta: request_id: "req_xyz123" get: tags: - Inbound Endpoints summary: List inbound endpoints description: | List all inbound endpoints for the authenticated project. Supports cursor-based pagination. operationId: listInboundEndpoints parameters: - name: limit in: query description: Maximum number of results to return schema: type: integer minimum: 1 maximum: 100 default: 50 example: 50 - name: cursor in: query description: Pagination cursor from previous response schema: type: string responses: '200': description: Inbound endpoints retrieved successfully content: application/json: schema: $ref: '#/components/schemas/ListInboundEndpointsResponse' example: data: - id: "01935abc-def0-7123-4567-890abcdef012" name: "Stripe webhooks" url: "https://myapp.com/webhooks/stripe" active: true paused: false created_at: "2025-12-01T10:00:00Z" meta: request_id: "req_xyz123" next_cursor: null '401': $ref: '#/components/responses/Unauthorized' /v1/inbound-endpoints/{id}: get: tags: - Inbound Endpoints summary: Get inbound endpoint description: | Retrieve full details of a specific inbound endpoint including verification configuration and status. operationId: getInboundEndpoint parameters: - $ref: '#/components/parameters/InboundEndpointId' responses: '200': description: Inbound endpoint details retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetInboundEndpointResponse' example: data: id: "01935abc-def0-7123-4567-890abcdef012" name: "Stripe webhooks" description: "Receives Stripe payment events" url: "https://myapp.com/webhooks/stripe" active: true paused: false verify_static_token: false verify_hmac: true verify_ip_allowlist: false ingest_response_code: 202 idempotency_header_names: [] signing_enabled: false created_at: "2025-12-01T10:00:00Z" updated_at: "2025-12-06T12:00:00Z" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' patch: tags: - Inbound Endpoints summary: Update inbound endpoint description: | Update an existing inbound endpoint. Only provided fields are updated; omitted fields retain their current values. operationId: updateInboundEndpoint parameters: - $ref: '#/components/parameters/InboundEndpointId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateInboundEndpointRequest' examples: update_name: summary: Update name only value: name: "Updated endpoint name" enable_hmac: summary: Enable HMAC verification value: verify_hmac: true hmac_header_name: "X-Signature" hmac_secret: "my-secret-key" responses: '200': description: Inbound endpoint updated successfully content: application/json: schema: $ref: '#/components/schemas/UpdateInboundEndpointResponse' example: data: id: "01935abc-def0-7123-4567-890abcdef012" updated: true meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '409': description: Duplicate URL conflict content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': $ref: '#/components/responses/Unauthorized' delete: tags: - Inbound Endpoints summary: Delete inbound endpoint description: | Soft-delete an inbound endpoint. The endpoint will no longer accept new webhooks, and any in-flight messages will be marked as permanently failed. operationId: deleteInboundEndpoint parameters: - $ref: '#/components/parameters/InboundEndpointId' responses: '200': description: Inbound endpoint deleted successfully content: application/json: schema: type: object properties: data: type: object properties: id: type: string deleted: type: boolean meta: $ref: '#/components/schemas/ResponseMeta' example: data: id: "01935abc-def0-7123-4567-890abcdef012" deleted: true meta: request_id: "req_xyz123" '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' /v1/inbound-endpoints/{id}/signing-keys: post: tags: - Inbound Endpoints summary: Create inbound signing key description: | Create a new delivery signing key for an inbound forward endpoint. A maximum of 2 active keys is allowed to support safe rotation. operationId: createInboundSigningKey parameters: - $ref: '#/components/parameters/InboundEndpointId' responses: '201': description: Inbound signing key created successfully content: application/json: schema: $ref: '#/components/schemas/CreateSigningKeyResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '409': description: Maximum signing keys reached content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '500': $ref: '#/components/responses/InternalServerError' get: tags: - Inbound Endpoints summary: List inbound signing keys description: List active delivery signing keys for an inbound endpoint. operationId: listInboundSigningKeys parameters: - $ref: '#/components/parameters/InboundEndpointId' responses: '200': description: Inbound signing keys retrieved successfully content: application/json: schema: $ref: '#/components/schemas/ListSigningKeysResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /v1/inbound-endpoints/{id}/signing-keys/{keyId}: delete: tags: - Inbound Endpoints summary: Delete inbound signing key description: | Delete an active inbound delivery signing key. At least one key must remain. operationId: deleteInboundSigningKey parameters: - $ref: '#/components/parameters/InboundEndpointId' - $ref: '#/components/parameters/SigningKeyId' responses: '204': description: Inbound signing key deleted successfully '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '409': description: Cannot delete the last signing key content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '500': $ref: '#/components/responses/InternalServerError' /v1/inbound-endpoints/{id}/pause: post: tags: - Inbound Endpoints summary: Pause inbound endpoint description: | Pause an inbound endpoint. While paused, incoming webhooks will be rejected. operationId: pauseInboundEndpoint parameters: - $ref: '#/components/parameters/InboundEndpointId' responses: '200': description: Inbound endpoint paused successfully content: application/json: schema: type: object properties: data: type: object properties: id: type: string paused: type: boolean meta: $ref: '#/components/schemas/ResponseMeta' example: data: id: "01935abc-def0-7123-4567-890abcdef012" paused: true meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' /v1/inbound-endpoints/{id}/resume: post: tags: - Inbound Endpoints summary: Resume inbound endpoint description: | Resume a paused inbound endpoint. The endpoint will begin accepting webhooks again. operationId: resumeInboundEndpoint parameters: - $ref: '#/components/parameters/InboundEndpointId' responses: '200': description: Inbound endpoint resumed successfully content: application/json: schema: type: object properties: data: type: object properties: id: type: string paused: type: boolean meta: $ref: '#/components/schemas/ResponseMeta' example: data: id: "01935abc-def0-7123-4567-890abcdef012" paused: false meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' /v1/inbound-endpoints/{id}/listen: get: tags: - Inbound Endpoints summary: Poll for inbound webhooks (CLI fallback) description: | Polling fallback for the CLI tool when a WebSocket connection to the streaming service is unavailable. Returns inbound messages received after the `after` cursor, ordered by UUIDv7 (time-ordered). Only available for `cli`-mode endpoints. Returns `400` if the endpoint is in `forward` mode. Up to 50 messages are returned per request. Use `next_cursor` from the response `meta` to paginate through additional messages. operationId: listenInboundEndpoint parameters: - $ref: '#/components/parameters/InboundEndpointId' - name: after in: query required: false description: | Cursor for pagination. Only messages received after this message ID (UUIDv7, time-ordered) are returned. Omit to start from the earliest available message. schema: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" responses: '200': description: Messages retrieved successfully content: application/json: schema: $ref: '#/components/schemas/ListenInboundEndpointResponse' example: data: - message_id: "01935abc-def0-7123-4567-890abcdef012" content_type: "application/json" headers: X-Stripe-Signature: "t=1234,v1=abc..." Content-Type: "application/json" size_bytes: 512 received_at: "2026-03-20T10:30:00Z" body: type: "checkout.session.completed" id: "cs_test_abc123" - message_id: "01935abc-def0-7123-4567-890abcdef099" content_type: "application/xml" headers: Content-Type: "application/xml" size_bytes: 1024 received_at: "2026-03-20T10:31:00Z" body: "PD94bWwgdmVyc2lvbj0iMS4wIj8+..." body_encoding: "base64" meta: request_id: "req_xyz123" next_cursor: "01935abc-def0-7123-4567-890abcdef099" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' # ── Pull Endpoints ───────────────────────────────────────────────── /v1/pull-endpoints: post: tags: - Pull Endpoints summary: Create a pull endpoint description: | Creates a new pull mode endpoint. Pull endpoints receive webhooks from external providers and store events for consumer-controlled retrieval via the API, instead of forwarding them. operationId: createPullEndpoint requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreatePullEndpointRequest' responses: '201': description: Pull endpoint created content: application/json: schema: $ref: '#/components/schemas/CreatePullEndpointResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' get: tags: - Pull Endpoints summary: List pull endpoints description: List all pull endpoints for the authenticated project. operationId: listPullEndpoints parameters: - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 20 - name: cursor in: query schema: type: string responses: '200': description: List of pull endpoints content: application/json: schema: $ref: '#/components/schemas/ListPullEndpointsResponse' '401': $ref: '#/components/responses/Unauthorized' /v1/pull-endpoints/{id}: get: tags: - Pull Endpoints summary: Get pull endpoint description: Retrieve a pull endpoint with event counts. operationId: getPullEndpoint parameters: - $ref: '#/components/parameters/PullEndpointId' responses: '200': description: Pull endpoint details content: application/json: schema: $ref: '#/components/schemas/GetPullEndpointResponse' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' patch: tags: - Pull Endpoints summary: Update pull endpoint description: Update pull endpoint configuration. Only provided fields are updated. operationId: updatePullEndpoint parameters: - $ref: '#/components/parameters/PullEndpointId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdatePullEndpointRequest' responses: '200': description: Pull endpoint updated content: application/json: schema: $ref: '#/components/schemas/GetPullEndpointResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' delete: tags: - Pull Endpoints summary: Delete pull endpoint description: Soft-deletes a pull endpoint. Associated events become inaccessible. operationId: deletePullEndpoint parameters: - $ref: '#/components/parameters/PullEndpointId' responses: '200': description: Pull endpoint deleted content: application/json: schema: type: object properties: data: type: object properties: id: type: string deleted: type: boolean meta: $ref: '#/components/schemas/ResponseMeta' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' /v1/pull-endpoints/{id}/pause: post: tags: - Pull Endpoints summary: Pause pull endpoint description: | Pauses the pull endpoint. When paused, the ingestion URL returns 503 to webhook providers (triggering provider retry). No new events are stored. Existing events remain retrievable. operationId: pausePullEndpoint parameters: - $ref: '#/components/parameters/PullEndpointId' responses: '200': description: Pull endpoint paused content: application/json: schema: type: object properties: data: type: object properties: id: type: string paused: type: boolean example: true meta: $ref: '#/components/schemas/ResponseMeta' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' /v1/pull-endpoints/{id}/resume: post: tags: - Pull Endpoints summary: Resume pull endpoint description: Resumes a paused pull endpoint. New events will be accepted again. operationId: resumePullEndpoint parameters: - $ref: '#/components/parameters/PullEndpointId' responses: '200': description: Pull endpoint resumed content: application/json: schema: type: object properties: data: type: object properties: id: type: string paused: type: boolean example: false meta: $ref: '#/components/schemas/ResponseMeta' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' # ── Pull Events ─────────────────────────────────────────────────── /v1/pull-endpoints/{id}/events: get: tags: - Pull Events summary: List events description: | List events stored at a pull endpoint. Returns metadata only (no payloads). Use GET /v1/pull-endpoints/{id}/events/{event_id} to fetch the full payload. operationId: listPullEvents parameters: - $ref: '#/components/parameters/PullEndpointId' - name: status in: query description: Filter by event status. Default is "stored" (unacknowledged events). schema: type: string enum: [stored, fetched, delivered, all] default: stored - name: event_type in: query description: Filter by exact event type match (e.g., "payment_intent.succeeded"). schema: type: string - name: since in: query description: Return events received after this timestamp (inclusive). RFC 3339 format. schema: type: string format: date-time - name: before in: query description: Return events received before this timestamp (exclusive). RFC 3339 format. schema: type: string format: date-time - name: limit in: query description: Page size (1–1000). schema: type: integer minimum: 1 maximum: 1000 default: 100 - name: cursor in: query description: Opaque cursor for next page. schema: type: string responses: '200': description: List of events content: application/json: schema: $ref: '#/components/schemas/ListPullEventsResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /v1/pull-endpoints/{id}/events/{event_id}: get: tags: - Pull Events summary: Get event with payload description: | Retrieve a single event including the full payload fetched from storage and the original request headers. JSON payloads are embedded directly; non-JSON payloads are base64-encoded. operationId: getPullEvent parameters: - $ref: '#/components/parameters/PullEndpointId' - $ref: '#/components/parameters/PullEventId' - name: preview in: query description: | When `true`, the event payload is returned without transitioning the message status from `stored` to `fetched`. Useful for inspecting payloads without affecting the event lifecycle. schema: type: boolean default: false required: false responses: '200': description: Event with full payload content: application/json: schema: $ref: '#/components/schemas/GetPullEventResponse' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '410': description: Event payload has been cleaned up by the retention sweeper content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' delete: tags: - Pull Events summary: Delete a pull event description: | Soft-delete a single pull event. The payload becomes immediately unreachable; the S3 object is removed by the sweeper on its next run. Metadata is preserved. Idempotent: deleting an already-deleted event returns 200 with `already_deleted: true`. operationId: deletePullEvent parameters: - $ref: '#/components/parameters/PullEndpointId' - $ref: '#/components/parameters/PullEventId' responses: '200': description: Event deleted (or already deleted) content: application/json: schema: $ref: '#/components/schemas/DeleteEventResponse' example: data: event_id: "01935abc-def0-7123-4567-890abcdef099" deleted_at: "2026-04-05T14:23:11.123Z" already_deleted: false meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' /v1/pull-endpoints/{id}/events/delete-batch: post: tags: - Pull Events summary: Batch delete pull events by ID description: | Soft-delete a batch of pull events by their IDs. Returns per-ID outcomes: `deleted`, `already_deleted`, or `not_found`. Maximum 100 IDs per request. Note: the request body uses `message_ids` (shared schema across all message families). In this context, each entry is a pull event ID. operationId: deletePullEventsBatch parameters: - $ref: '#/components/parameters/PullEndpointId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DeleteBatchRequest' example: message_ids: - "01935abc-def0-7123-4567-890abcdef012" - "01935abc-def0-7123-4567-890abcdef013" responses: '200': description: Batch delete results content: application/json: schema: $ref: '#/components/schemas/DeleteEventBatchResponse' example: data: results: - event_id: "01935abc-def0-7123-4567-890abcdef012" outcome: "deleted" deleted_at: "2026-04-05T14:23:11.123Z" - event_id: "01935abc-def0-7123-4567-890abcdef013" outcome: "not_found" deleted_count: 1 already_deleted_count: 0 not_found_count: 1 meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' /v1/pull-endpoints/{id}/events/delete-all: post: tags: - Pull Events summary: Delete all pull events matching filters description: | Soft-delete pull events matching the specified filters for the given pull endpoint. Processes in chunks for reliability. Returns the count and IDs of deleted events. On error, the response includes an `error` object: `DELETE_FAILED` if no rows were deleted, or `PARTIAL_DELETE_FAILURE` if some rows were committed before the error. Maximum 1000 events per request. operationId: deletePullEventsAll parameters: - $ref: '#/components/parameters/PullEndpointId' - name: status in: query description: Comma-separated list of statuses to match (`stored`, `fetched`, `delivered`). schema: type: string - name: event_type in: query description: Exact event type to match. schema: type: string - name: received_after in: query description: Only delete events received at or after this time (RFC 3339). schema: type: string format: date-time - name: received_before in: query description: Only delete events received before this time (RFC 3339). schema: type: string format: date-time - name: limit in: query description: Maximum events to delete (1–1000, default 1000). schema: type: integer minimum: 1 maximum: 1000 default: 1000 responses: '200': description: Delete-all results content: application/json: schema: $ref: '#/components/schemas/PullDeleteAllResponse' example: data: deleted: 42 deleted_event_ids: - "01935abc-def0-7123-4567-890abcdef012" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' /v1/pull-endpoints/{id}/events/ack: post: tags: - Pull Events summary: Acknowledge events description: | Mark events as delivered. Idempotent — acknowledging already-delivered events is a no-op. All event IDs must belong to the specified pull endpoint. operationId: ackPullEvents parameters: - $ref: '#/components/parameters/PullEndpointId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AckPullEventsRequest' responses: '200': description: Events acknowledged content: application/json: schema: $ref: '#/components/schemas/AckPullEventsResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' # ── Pull Observability ────────────────────────────────────────────── /v1/pull-logs: get: tags: - Pull Observability summary: Query pull event logs description: | Retrieve a paginated list of pull events across all pull endpoints in the project. Useful for monitoring and debugging event ingestion across endpoints. operationId: getPullLogs parameters: - name: pull_endpoint_id in: query description: Filter by specific pull endpoint schema: type: string format: uuid - name: status in: query description: Filter by event status schema: type: string enum: [stored, delivered] - name: event_type in: query description: Filter by exact event type match schema: type: string - name: start_time in: query description: Filter events received after this timestamp (ISO 8601) schema: type: string format: date-time - name: end_time in: query description: Filter events received before this timestamp (ISO 8601) schema: type: string format: date-time - name: limit in: query description: Maximum number of results to return schema: type: integer minimum: 1 maximum: 1000 default: 50 - name: cursor in: query description: Pagination cursor from previous response schema: type: string responses: '200': description: Pull logs retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetPullLogsResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /v1/pull-metrics: get: tags: - Pull Observability summary: Get pull event metrics description: | Retrieve aggregated metrics for pull events including counts of stored and delivered events. operationId: getPullMetrics parameters: - name: window in: query description: Time window for aggregation schema: type: string enum: [1h, 24h, 7d, 30d] default: 24h - name: pull_endpoint_id in: query description: Filter metrics by specific pull endpoint schema: type: string format: uuid responses: '200': description: Pull metrics retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetPullMetricsResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /v1/pull-metrics/timeseries: get: tags: - Pull Observability summary: Get pull event time-series metrics description: | Retrieve time-bucketed pull event metrics for charting and analysis. Returns data points at intervals appropriate for the selected window. operationId: getPullTimeSeriesMetrics parameters: - name: window in: query description: Time window for metrics schema: type: string enum: [1h, 24h, 7d, 30d] default: 24h - name: pull_endpoint_id in: query description: Filter by specific pull endpoint schema: type: string format: uuid responses: '200': description: Pull time-series metrics retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetPullTimeSeriesMetricsResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' # ── Inbound Messages ─────────────────────────────────────────────── /v1/inbound-messages/{id}: get: tags: - Inbound Messages summary: Get inbound message details description: Retrieve the status, metadata, and delivery information for a specific inbound message operationId: getInboundMessage parameters: - $ref: '#/components/parameters/InboundMessageId' responses: '200': description: Inbound message details retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetInboundMessageResponse' example: data: id: "01935abc-def0-7123-4567-890abcdef012" project_id: "proj_abc123" inbound_endpoint_id: "01935abc-def0-7123-4567-890abcdef099" status: "succeeded" attempt_count: 1 replay_count: 0 content_type: "application/json" size_bytes: 512 payload_sha256: "abc123..." response_status: 200 response_latency_ms: 120 received_at: "2025-12-06T12:00:00Z" updated_at: "2025-12-06T12:00:05Z" delivered_at: "2025-12-06T12:00:05Z" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - Inbound Messages summary: Delete an inbound message description: | Soft-delete a single inbound message. The payload becomes immediately unreachable; the S3 object is removed by the sweeper on its next run. Metadata and delivery-attempt history are preserved. Idempotent: deleting an already-deleted message returns 200 with `already_deleted: true`. operationId: deleteInboundMessage parameters: - $ref: '#/components/parameters/InboundMessageId' responses: '200': description: Message deleted (or already deleted) content: application/json: schema: $ref: '#/components/schemas/DeleteMessageResponse' example: data: message_id: "01935abc-def0-7123-4567-890abcdef012" deleted_at: "2026-04-05T14:23:11.123Z" already_deleted: false meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' /v1/inbound-messages/delete-batch: post: tags: - Inbound Messages summary: Batch delete inbound messages by ID description: | Soft-delete a batch of inbound messages by their IDs. Returns per-ID outcomes: `deleted`, `already_deleted`, or `not_found`. Maximum 100 IDs per request. operationId: deleteInboundMessagesBatch requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DeleteBatchRequest' example: message_ids: - "01935abc-def0-7123-4567-890abcdef012" - "01935abc-def0-7123-4567-890abcdef013" responses: '200': description: Batch delete results content: application/json: schema: $ref: '#/components/schemas/DeleteBatchResponse' example: data: results: - message_id: "01935abc-def0-7123-4567-890abcdef012" outcome: "deleted" deleted_at: "2026-04-05T14:23:11.123Z" - message_id: "01935abc-def0-7123-4567-890abcdef013" outcome: "not_found" deleted_count: 1 already_deleted_count: 0 not_found_count: 1 meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' /v1/inbound-messages/delete-all: post: tags: - Inbound Messages summary: Delete all inbound messages matching filters description: | Soft-delete inbound messages matching the specified filters. Processes in chunks for reliability. Returns the count and IDs of deleted messages. On error, the response includes an `error` object: `DELETE_FAILED` if no rows were deleted, or `PARTIAL_DELETE_FAILURE` if some rows were committed before the error. Maximum 1000 messages per request. operationId: deleteInboundMessagesAll parameters: - name: status in: query description: Comma-separated list of statuses to match. schema: type: string - name: inbound_endpoint_id in: query description: Restrict to messages for this inbound endpoint. schema: type: string format: uuid - name: received_after in: query description: Only delete messages received at or after this time (RFC 3339). schema: type: string format: date-time - name: received_before in: query description: Only delete messages received before this time (RFC 3339). schema: type: string format: date-time - name: limit in: query description: Maximum messages to delete (1–1000, default 1000). schema: type: integer minimum: 1 maximum: 1000 default: 1000 responses: '200': description: Delete-all results content: application/json: schema: $ref: '#/components/schemas/DeleteAllResponse' example: data: deleted: 42 deleted_message_ids: - "01935abc-def0-7123-4567-890abcdef012" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' /v1/inbound-messages/{id}/attempts: get: tags: - Inbound Messages summary: Get inbound message delivery attempts description: | Retrieve all delivery attempts for a specific inbound message, including timing breakdown, response details, and connection metadata. Response body URLs are only available on Starter plans and above. **Note:** If the message ID does not exist or belongs to a different project, this endpoint returns 200 with an empty `data` array (not 404). Use `GET /v1/inbound-messages/{id}` first to verify the message exists. **Note:** Results are capped at 200 attempts. If `has_more` is true in the response, additional attempts exist but cannot be retrieved yet. Cursor-based pagination will be added in a future release. The `processing_ms` field from the shared attempt schema is not populated for inbound attempts and will be absent from the response. operationId: getInboundMessageAttempts parameters: - $ref: '#/components/parameters/InboundMessageId' responses: '200': description: | Attempts retrieved successfully. Returns an empty array if the message has no attempts yet or if the message ID is not found. content: application/json: schema: $ref: '#/components/schemas/GetInboundMessageAttemptsResponse' example: data: - id: "01935abc-def0-7123-4567-890abcdef001" attempt_no: 1 response_status: 200 response_latency_ms: 120 dns_ms: 10 tcp_connect_ms: 20 tls_handshake_ms: 40 ttfb_ms: 110 transfer_ms: 5 conn_reused: false created_at: "2025-12-06T12:00:00Z" meta: request_id: "req_xyz123" has_more: false '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' /v1/inbound-messages/{id}/replay: post: tags: - Inbound Messages summary: Replay inbound message description: | Replay a single inbound message. The message will be re-enqueued for delivery to the configured forwarding URL. Both `failed` and `succeeded` messages can be replayed, as long as the payload is still within its retention window. Retention is anchored to the original `delivered_at` timestamp for succeeded messages, so replaying does not extend the retention window. operationId: replayInboundMessage parameters: - $ref: '#/components/parameters/InboundMessageId' responses: '200': description: Inbound message queued for replay content: application/json: schema: $ref: '#/components/schemas/ReplayInboundMessageResponse' example: message: "Inbound message queued for replay" data: message_id: "01935abc-def0-7123-4567-890abcdef012" status: "queued" meta: request_id: "req_xyz123" '404': $ref: '#/components/responses/NotFound' '409': description: > Message cannot be replayed due to its current state. Possible error codes: `DELIVERY_IN_PROGRESS` (message is currently being delivered), `RETRY_IN_PROGRESS` (message is in active fast retry after a 429 response). content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: retry_in_progress: summary: Fast retry in progress value: error: code: "RETRY_IN_PROGRESS" message: "Message is in active fast retry; wait for retries to complete or fail before replaying" meta: request_id: "req_xyz123" delivery_in_progress: summary: Delivery in progress value: error: code: "DELIVERY_IN_PROGRESS" message: "Message is currently being delivered; wait for delivery to complete before replaying" meta: request_id: "req_xyz123" '410': description: > Message payload is no longer available for replay. Possible error codes: `RETENTION_EXPIRED` (the message's retention window has elapsed), `PAYLOAD_UNAVAILABLE` (the payload was cleaned up by the sweeper before retention expired), `MESSAGE_DELETED` (the message was explicitly deleted). content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: retention_expired: summary: Retention window elapsed value: error: code: "RETENTION_EXPIRED" message: "Message retention period has expired and payload is no longer available for replay" meta: request_id: "req_xyz123" payload_unavailable: summary: Payload already cleaned up value: error: code: "PAYLOAD_UNAVAILABLE" message: "Message payload has already been cleaned up and cannot be replayed" meta: request_id: "req_xyz123" message_deleted: summary: Message explicitly deleted value: error: code: "MESSAGE_DELETED" message: "Message has been deleted and cannot be replayed" meta: request_id: "req_xyz123" '429': description: Replay limit exceeded content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "REPLAY_LIMIT_EXCEEDED" message: "Message has already been replayed the maximum number of times" meta: request_id: "req_xyz123" '401': $ref: '#/components/responses/Unauthorized' '503': description: Inbound queue not configured content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /v1/inbound-messages/replay-all: post: tags: - Inbound Messages summary: Replay all matching inbound messages description: | Replay multiple inbound messages matching the specified filters. Messages are re-enqueued for delivery to their configured forwarding URLs. operationId: replayAllInboundMessages parameters: - name: status in: query required: true description: Filter messages by status (only replayable statuses allowed) schema: type: string enum: [failed_permanent, pending_retry] example: "failed_permanent" - name: inbound_endpoint_id in: query description: Filter by specific inbound endpoint schema: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" - name: limit in: query description: Maximum number of messages to replay schema: type: integer minimum: 1 maximum: 1000 default: 100 example: 100 responses: '200': description: Messages replayed successfully content: application/json: schema: $ref: '#/components/schemas/ReplayAllInboundMessagesResponse' example: message: "5 inbound messages queued for replay" data: replayed: 5 failed: 0 stuck: 0 replayed_message_ids: - "01935abc-def0-7123-4567-890abcdef012" - "01935abc-def0-7123-4567-890abcdef013" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '503': description: Inbound queue not configured content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /v1/inbound-messages/replay-batch: post: tags: - Inbound Messages summary: Replay specific inbound messages by ID description: | Replay specific inbound messages by providing their IDs. Messages are re-enqueued for delivery to their configured forwarding URLs. operationId: replayBatchInboundMessages requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ReplayBatchRequest' example: message_ids: - "01935abc-def0-7123-4567-890abcdef012" - "01935abc-def0-7123-4567-890abcdef013" responses: '200': description: Messages replayed successfully content: application/json: schema: $ref: '#/components/schemas/ReplayBatchInboundMessagesResponse' example: message: "2 inbound messages replayed" data: replayed: 2 failed: 0 stuck: 0 results: - message_id: "01935abc-def0-7123-4567-890abcdef012" status: "replayed" - message_id: "01935abc-def0-7123-4567-890abcdef013" status: "replayed" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '503': description: Inbound queue not configured content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /v1/inbound-logs: get: tags: - Inbound Messages summary: Query inbound delivery logs description: | Retrieve delivery logs for inbound webhook messages. Supports filtering by status, endpoint, and time range with cursor-based pagination. operationId: getInboundLogs parameters: - name: status in: query description: Filter by message status schema: type: string enum: [queued, delivering, succeeded, pending_retry, failed_permanent] example: "succeeded" - name: inbound_endpoint_id in: query description: Filter by inbound endpoint ID schema: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" - name: start_time in: query description: Filter messages received after this time (ISO 8601) schema: type: string format: date-time example: "2025-12-01T00:00:00Z" - name: end_time in: query description: Filter messages received before this time (ISO 8601) schema: type: string format: date-time example: "2025-12-06T23:59:59Z" - name: limit in: query description: Maximum number of results to return schema: type: integer minimum: 1 maximum: 500 default: 50 example: 50 - name: cursor in: query description: Pagination cursor from previous response schema: type: string responses: '200': description: Inbound logs retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetInboundLogsResponse' example: data: - message_id: "01935abc-def0-7123-4567-890abcdef012" inbound_endpoint_id: "01935abc-def0-7123-4567-890abcdef099" endpoint: "https://myapp.com/webhooks/stripe" status: "succeeded" attempt_count: 1 received_at: "2025-12-06T12:00:00Z" delivered_at: "2025-12-06T12:00:05Z" response_status: 200 response_latency_ms: 120 total_delivery_ms: 5000 meta: request_id: "req_xyz123" has_more: false '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /v1/inbound-metrics: get: tags: - Inbound Messages summary: Get inbound metrics description: | Retrieve aggregated delivery metrics for inbound webhook messages. Optionally filter by a specific inbound endpoint. operationId: getInboundMetrics parameters: - name: window in: query description: Time window for metrics schema: type: string enum: [1h, 24h, 7d, 30d] default: 24h example: "24h" - name: inbound_endpoint_id in: query description: Optional filter by inbound endpoint ID schema: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" responses: '200': description: Inbound metrics retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetInboundMetricsResponse' example: data: window: "24h" total_messages: 5000 succeeded: 4900 failed: 20 retries: 80 success_rate: 0.98 avg_latency_ms: 150 avg_delivery_time_ms: 3200 meta: request_id: "req_xyz123" '400': description: Invalid window or endpoint_id parameter content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': $ref: '#/components/responses/Unauthorized' /v1/inbound-metrics/timeseries: get: tags: - Inbound Messages summary: Get inbound time-series metrics description: | Retrieve time-bucketed delivery metrics for inbound webhook messages. Returns data points at intervals appropriate for the selected window. operationId: getInboundTimeSeriesMetrics parameters: - name: window in: query description: Time window for metrics schema: type: string enum: [1h, 24h, 7d, 30d] default: 24h example: "24h" - name: inbound_endpoint_id in: query description: Optional filter by inbound endpoint ID schema: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" responses: '200': description: Inbound time-series metrics retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetInboundTimeSeriesMetricsResponse' example: data: window: "24h" buckets: - timestamp: "2025-12-06T00:00:00Z" succeeded: 200 failed: 2 retrying: 5 total: 207 avg_latency_ms: 145 - timestamp: "2025-12-06T01:00:00Z" succeeded: 220 failed: 1 retrying: 3 total: 224 avg_latency_ms: 130 meta: request_id: "req_xyz123" '400': description: Invalid window or endpoint_id parameter content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': $ref: '#/components/responses/Unauthorized' /v1/inbound-rejections: get: tags: - Inbound Messages summary: List inbound rejections description: | Retrieve a log of rejected inbound webhooks. Rejections occur when incoming requests fail verification (invalid token, HMAC mismatch, IP not allowed, etc.). operationId: listInboundRejections parameters: - name: inbound_endpoint_id in: query description: Filter by inbound endpoint ID schema: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" - name: start_time in: query description: Filter rejections after this time (ISO 8601) schema: type: string format: date-time example: "2025-12-01T00:00:00Z" - name: end_time in: query description: Filter rejections before this time (ISO 8601) schema: type: string format: date-time example: "2025-12-06T23:59:59Z" - name: limit in: query description: Maximum number of results to return schema: type: integer minimum: 1 maximum: 500 default: 50 example: 50 - name: cursor in: query description: Pagination cursor from previous response schema: type: string responses: '200': description: Inbound rejections retrieved successfully content: application/json: schema: $ref: '#/components/schemas/ListInboundRejectionsResponse' example: data: - id: "01935abc-def0-7123-4567-890abcdef050" reason_code: "hmac_failed" received_at: "2025-12-06T12:00:00Z" inbound_endpoint_id: "01935abc-def0-7123-4567-890abcdef012" reason_detail: "HMAC signature mismatch" source_ip: "203.0.113.42" meta: request_id: "req_xyz123" has_more: false '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' # ── Actor Lookup ───────────────────────────────────────────────── /v1/actors/lookup: get: tags: - Actors summary: Look up actor display names description: | Resolves user IDs and/or API key IDs to their display names (email or label). Used by the console to render "Deleted by" attribution on message detail pages. **At least one of `user_id` or `api_key_id` must be provided.** If neither is given, the endpoint returns 400. Both parameters accept comma-separated lists for batch lookups (up to 100 total IDs). IDs that do not exist or belong to a different tenant are silently omitted from the response. operationId: lookupActors parameters: - name: user_id in: query required: false description: One or more Kratos identity IDs (comma-separated) schema: type: string example: "01935abc-def0-7123-4567-890abcdef012" - name: api_key_id in: query required: false description: One or more API key IDs (comma-separated) schema: type: string example: "key_abc123" responses: '200': description: Actor display names resolved content: application/json: schema: $ref: '#/components/schemas/ActorLookupResponse' example: data: users: "01935abc-def0-7123-4567-890abcdef012": email: "alice@example.com" api_keys: "key_abc123": label: "Production Backend" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' # ── Exports ──────────────────────────────────────────────────────── /v1/exports: post: tags: - Exports summary: Create export description: | Create a CSV export job for webhook delivery data. The export runs asynchronously; poll GET /v1/exports/{id} to check status and use GET /v1/exports/{id}/download to retrieve the file when complete. operationId: createExport requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateExportRequest' examples: basic: summary: Export all messages in a time range value: start_time: "2025-12-01T00:00:00Z" end_time: "2025-12-06T23:59:59Z" filtered: summary: Export failed messages for a specific endpoint value: start_time: "2025-12-01T00:00:00Z" end_time: "2025-12-06T23:59:59Z" status: "failed_permanent" endpoint_id: "ep_550e8400e29b41d4a716446655440000" responses: '201': description: Export job created successfully content: application/json: schema: $ref: '#/components/schemas/CreateExportResponse' example: data: id: "01935abc-def0-7123-4567-890abcdef012" project_id: "proj_abc123" status: "pending" filter_start_time: "2025-12-01T00:00:00Z" filter_end_time: "2025-12-06T23:59:59Z" created_at: "2025-12-06T12:00:00Z" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '429': description: Export rate or concurrency limit exceeded content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "EXPORT_LIMIT_EXCEEDED" message: "An export is already in progress. Please wait for it to complete." meta: request_id: "req_xyz123" get: tags: - Exports summary: List exports description: | List recent export jobs for the authenticated project. Returns the 20 most recent exports. operationId: listExports responses: '200': description: Exports retrieved successfully content: application/json: schema: $ref: '#/components/schemas/ListExportsResponse' example: data: - id: "01935abc-def0-7123-4567-890abcdef012" project_id: "proj_abc123" status: "completed" filter_start_time: "2025-12-01T00:00:00Z" filter_end_time: "2025-12-06T23:59:59Z" row_count: 5000 file_size_bytes: 1024000 created_at: "2025-12-06T12:00:00Z" completed_at: "2025-12-06T12:05:00Z" expires_at: "2025-12-13T12:05:00Z" meta: request_id: "req_xyz123" '401': $ref: '#/components/responses/Unauthorized' /v1/exports/{id}: get: tags: - Exports summary: Get export description: | Retrieve details of a specific export job including its status, row count, and file size when completed. operationId: getExport parameters: - $ref: '#/components/parameters/ExportId' responses: '200': description: Export details retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GetExportResponse' example: data: id: "01935abc-def0-7123-4567-890abcdef012" project_id: "proj_abc123" status: "completed" filter_start_time: "2025-12-01T00:00:00Z" filter_end_time: "2025-12-06T23:59:59Z" filter_status: "failed_permanent" row_count: 5000 file_size_bytes: 1024000 started_at: "2025-12-06T12:00:30Z" completed_at: "2025-12-06T12:05:00Z" expires_at: "2025-12-13T12:05:00Z" created_at: "2025-12-06T12:00:00Z" meta: request_id: "req_xyz123" '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' delete: tags: - Exports summary: Delete export description: | Delete an export and its associated S3 file. Exports that are pending or currently processing cannot be deleted. If the export transitions between states during the request, the server may return a transient 409 — retry the request in that case. operationId: deleteExport parameters: - $ref: '#/components/parameters/ExportId' responses: '204': description: Export deleted successfully '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '409': description: Export is pending or in progress content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "EXPORT_ACTIVE" message: "Cannot delete an export that is pending or in progress. Please try again in a moment." meta: request_id: "req_xyz123" /v1/exports/{id}/download: get: tags: - Exports summary: Download export description: | Download a completed export file. Returns a 302 redirect to a presigned S3 URL for the CSV file. The export must be in "completed" status. operationId: downloadExport parameters: - $ref: '#/components/parameters/ExportId' responses: '302': description: Redirect to presigned download URL headers: Location: description: Presigned S3 URL for the export CSV file schema: type: string format: uri '400': description: Export not ready for download content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "EXPORT_NOT_READY" message: "Export is not yet completed" meta: request_id: "req_xyz123" '404': $ref: '#/components/responses/NotFound' '410': description: Export download link has expired content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "EXPORT_EXPIRED" message: "Export download link has expired" meta: request_id: "req_xyz123" '401': $ref: '#/components/responses/Unauthorized' components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: API Key description: | API key authentication. Include your API key in the Authorization header: ``` Authorization: Bearer hb_live_xxxxxxxxxxxxxxxxxxxx ``` API keys are prefixed with: - `hb_live_` for production keys - `hb_test_` for test/development keys parameters: MessageId: name: id in: path required: true description: Unique message identifier (UUIDv7) schema: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" EndpointId: name: id in: path required: true description: Endpoint identifier (ep_ prefix + 32 hex chars) schema: type: string pattern: '^ep_[a-f0-9]{32}$' example: "ep_550e8400e29b41d4a716446655440000" ProjectId: name: id in: path required: true description: Project identifier (UUIDv7) schema: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" InboundEndpointId: name: id in: path required: true description: Inbound endpoint identifier (UUIDv7) schema: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" InboundMessageId: name: id in: path required: true description: Inbound message identifier (UUIDv7) schema: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" SigningKeyId: name: keyId in: path required: true description: Signing key identifier in sk_ format schema: type: string example: "sk_550e8400e29b41d4a716446655440001" PullEndpointId: name: id in: path required: true description: Pull endpoint identifier (UUIDv7) schema: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" PullEventId: name: event_id in: path required: true description: Pull event identifier (UUIDv7) schema: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef099" ExportId: name: id in: path required: true description: Export job identifier (UUIDv7) schema: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" schemas: SendWebhookRequest: type: object required: - endpoint_id - payload properties: endpoint_id: type: string description: | ID of the endpoint to deliver to. Endpoints must be created first via POST /v1/endpoints. pattern: '^ep_[a-f0-9]{32}$' example: "ep_550e8400e29b41d4a716446655440000" payload: type: object description: JSON payload to send (max 1 MB) example: event: "order.created" order_id: "ord_12345" amount: 99.99 headers: type: object description: | Optional custom headers to include in the webhook request. Maximum 50 headers, 8KB total size. Some headers are forbidden (Host, Authorization, etc). additionalProperties: type: string example: X-Tenant-Id: "tenant_abc" X-Event-Type: "order.created" idempotency_key: type: string description: | Optional idempotency key to prevent duplicate sends. If the same key is used with the same payload, the original message ID is returned. If the same key is used with a different payload, a 409 error is returned. pattern: '^[a-zA-Z0-9_-]+$' maxLength: 255 example: "order-12345-created-v1" retention_hours: type: integer minimum: 0 description: | Optional override for payload retention period in hours. 0 means immediate delete after delivery. If not specified, the default retention for your plan is used. Maximum value depends on your subscription plan. example: 72 SendWebhookResponse: type: object properties: data: type: object properties: message_id: type: string format: uuid description: Unique identifier for the queued message (UUIDv7) example: "01935abc-def0-7123-4567-890abcdef012" status: type: string enum: [queued] description: Initial message status example: "queued" meta: $ref: '#/components/schemas/ResponseMeta' GetMessageResponse: type: object properties: data: $ref: '#/components/schemas/Message' meta: $ref: '#/components/schemas/ResponseMeta' DeleteMessageResponse: type: object properties: data: type: object properties: message_id: type: string description: The deleted message ID example: "01935abc-def0-7123-4567-890abcdef012" deleted_at: type: string format: date-time description: Timestamp when the message was deleted example: "2026-04-05T14:23:11.123Z" already_deleted: type: boolean description: True if the message was already deleted before this call example: false required: - message_id - deleted_at - already_deleted meta: $ref: '#/components/schemas/ResponseMeta' DeleteEventResponse: type: object properties: data: type: object properties: event_id: type: string description: The deleted event ID example: "01935abc-def0-7123-4567-890abcdef099" deleted_at: type: string format: date-time description: Timestamp when the event was deleted example: "2026-04-05T14:23:11.123Z" already_deleted: type: boolean description: True if the event was already deleted before this call example: false required: - event_id - deleted_at - already_deleted meta: $ref: '#/components/schemas/ResponseMeta' DeleteBatchRequest: type: object properties: message_ids: type: array items: type: string description: List of message IDs to delete (max 100) minItems: 1 maxItems: 100 example: - "01935abc-def0-7123-4567-890abcdef012" - "01935abc-def0-7123-4567-890abcdef013" required: - message_ids DeleteBatchResponse: type: object properties: data: type: object properties: results: type: array items: type: object properties: message_id: type: string description: The message ID outcome: type: string enum: [deleted, already_deleted, not_found] description: The outcome for this message deleted_at: type: string format: date-time description: Timestamp when the message was deleted (present for deleted and already_deleted outcomes) required: - message_id - outcome deleted_count: type: integer description: Number of messages newly deleted already_deleted_count: type: integer description: Number of messages that were already deleted not_found_count: type: integer description: Number of message IDs not found required: - results - deleted_count - already_deleted_count - not_found_count meta: $ref: '#/components/schemas/ResponseMeta' DeleteEventBatchResponse: type: object properties: data: type: object properties: results: type: array items: type: object properties: event_id: type: string description: The event ID outcome: type: string enum: [deleted, already_deleted, not_found] description: The outcome for this event deleted_at: type: string format: date-time description: Timestamp when the event was deleted (present for deleted and already_deleted outcomes) required: - event_id - outcome deleted_count: type: integer description: Number of events newly deleted already_deleted_count: type: integer description: Number of events that were already deleted not_found_count: type: integer description: Number of event IDs not found required: - results - deleted_count - already_deleted_count - not_found_count meta: $ref: '#/components/schemas/ResponseMeta' DeleteAllResponse: type: object properties: data: type: object properties: deleted: type: integer description: Number of messages deleted deleted_message_ids: type: array items: type: string description: IDs of deleted messages required: - deleted - deleted_message_ids error: type: object description: Present only on partial failure properties: code: type: string enum: [DELETE_FAILED, PARTIAL_DELETE_FAILURE] message: type: string meta: $ref: '#/components/schemas/ResponseMeta' PullDeleteAllResponse: type: object properties: data: type: object properties: deleted: type: integer description: Number of events deleted deleted_event_ids: type: array items: type: string description: IDs of deleted events required: - deleted - deleted_event_ids error: type: object description: Present only on partial failure properties: code: type: string enum: [DELETE_FAILED, PARTIAL_DELETE_FAILURE] message: type: string meta: $ref: '#/components/schemas/ResponseMeta' ActorLookupResponse: type: object properties: data: type: object properties: users: type: object additionalProperties: type: object properties: email: type: string required: - email description: Map of Kratos identity ID to user email. Only present when user_id was requested. api_keys: type: object additionalProperties: type: object properties: label: type: string required: - label description: > Map of API key ID to key label. Only present when api_key_id was requested. Keys that exist but have no label are included with an empty string; keys not found or belonging to another tenant are omitted entirely. meta: $ref: '#/components/schemas/ResponseMeta' ReplayMessageResponse: type: object properties: message: type: string example: "Message queued for replay" data: type: object properties: message_id: type: string example: "01935abc-def0-7123-4567-890abcdef012" status: type: string enum: [queued] example: "queued" meta: $ref: '#/components/schemas/ResponseMeta' GetLogsResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/MessageSummary' meta: allOf: - $ref: '#/components/schemas/ResponseMeta' - type: object properties: next_cursor: type: string description: Cursor for the next page of results example: "eyJpZCI6Im1zZ18xMjMiLCJ0cyI6MTczMzQ1Njc4OX0=" has_more: type: boolean description: Whether more results are available example: true GetMetricsResponse: type: object properties: data: $ref: '#/components/schemas/Metrics' meta: $ref: '#/components/schemas/ResponseMeta' CreateEndpointRequest: type: object required: - url properties: url: type: string format: uri description: | HTTPS URL of the webhook endpoint. Must be publicly accessible. Internal/private IP addresses are blocked for security. pattern: '^https://' minLength: 12 maxLength: 2048 example: "https://customer.app/webhooks" description: type: string maxLength: 500 description: Optional description of the endpoint example: "Main production webhook" rate_limit_rps: type: integer minimum: 0 maximum: 1000 default: 0 description: Rate limit in requests per second (0 = no limit) example: 10 burst: type: integer minimum: 0 maximum: 10000 default: 0 description: Maximum burst size for rate limiting example: 20 headers: type: object description: Custom headers to include in webhook requests additionalProperties: type: string example: X-Custom-Header: "value" ephemeral: type: boolean default: false description: | If true, creates an ephemeral endpoint for CI/test workflows. Ephemeral endpoints auto-expire after the TTL. ttl_minutes: type: integer minimum: 1 # must match config.MinEphemeralTTLMinutes maximum: 1440 # must match config.MaxEphemeralTTLMinutes default: 30 # must match config.DefaultEphemeralTTLMinutes description: | Time-to-live in minutes for ephemeral endpoints. Only used when ephemeral is true. After this duration, the endpoint is automatically deleted. example: 60 CreateEndpointResponse: type: object properties: data: type: object properties: id: type: string description: Unique endpoint identifier example: "ep_550e8400e29b41d4a716446655440000" url: type: string format: uri description: Webhook delivery URL example: "https://customer.app/webhooks" description: type: string description: Optional description example: "Main production webhook" signing_key_id: type: string description: Initial signing key identifier example: "sk_550e8400e29b41d4a716446655440001" signing_secret: type: string description: | **The signing secret - shown only once!** Store this securely for verifying webhook signatures. example: "whsec_abcdefghijklmnopqrstuvwxyz123456" key_hint: type: string description: Short identifier suffix for the initial signing key example: "abcd" ephemeral: type: boolean description: Whether this is an ephemeral (CI/test) endpoint example: false expires_at: type: string format: date-time description: When the ephemeral endpoint will auto-expire (only present for ephemeral endpoints) example: "2026-06-01T10:30:00Z" created_at: type: string format: date-time description: When the endpoint was created example: "2025-12-01T10:00:00Z" meta: $ref: '#/components/schemas/ResponseMeta' GetEndpointResponse: type: object properties: data: $ref: '#/components/schemas/Endpoint' meta: $ref: '#/components/schemas/ResponseMeta' ListEndpointsResponse: type: object description: | Paginated list of endpoints. Uses EndpointSummary (omits headers, rate limits) for performance. Use GET /endpoints/:id for full details. properties: data: type: array items: $ref: '#/components/schemas/EndpointSummary' meta: allOf: - $ref: '#/components/schemas/ResponseMeta' - type: object properties: next_cursor: type: string description: Cursor for the next page of results (opaque, do not parse) UpdateEndpointRequest: type: object properties: url: type: string format: uri pattern: '^https://' minLength: 12 maxLength: 2048 description: Updated endpoint URL example: "https://new-customer.app/webhooks" description: type: string maxLength: 500 description: Updated description rate_limit_rps: type: integer minimum: 0 maximum: 1000 description: Updated rate limit burst: type: integer minimum: 0 maximum: 10000 description: Updated burst size headers: type: object additionalProperties: type: string description: Updated custom headers UpdateEndpointResponse: type: object properties: data: type: object properties: id: type: string description: Endpoint ID example: "ep_550e8400e29b41d4a716446655440000" updated: type: boolean description: Whether the update was successful example: true meta: $ref: '#/components/schemas/ResponseMeta' SigningKey: type: object properties: id: type: string description: Signing key ID example: "sk_550e8400e29b41d4a716446655440001" key_hint: type: string description: Last 4 chars of key for identification example: "wxyz" created_at: type: string format: date-time example: "2025-12-06T12:10:00Z" CreateSigningKeyResponse: type: object properties: data: type: object properties: id: type: string description: Signing key ID example: "sk_550e8400e29b41d4a716446655440001" signing_secret: type: string description: | **The signing secret - shown only once!** Store this securely for verifying webhook signatures. example: "whsec_newabcdefghijklmnopqrstuvwxyz12" key_hint: type: string example: "wxyz" created_at: type: string format: date-time example: "2025-12-06T12:10:00Z" meta: $ref: '#/components/schemas/ResponseMeta' ListSigningKeysResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/SigningKey' meta: $ref: '#/components/schemas/ResponseMeta' Endpoint: type: object description: Full endpoint details (returned by GET /endpoints/:id) properties: id: type: string description: Unique endpoint identifier example: "ep_550e8400e29b41d4a716446655440000" url: type: string format: uri description: Webhook delivery URL example: "https://customer.app/webhooks" description: type: string description: Optional description example: "Main production webhook" rate_limit_rps: type: integer description: Rate limit in requests per second example: 10 burst: type: integer description: Maximum burst size example: 20 headers: type: object additionalProperties: type: string description: Custom headers for webhook requests paused: type: boolean description: Whether the endpoint is paused. When paused, messages are accepted but not delivered. example: false ephemeral: type: boolean description: Whether this is an ephemeral (CI/test) endpoint example: false expires_at: type: string format: date-time description: When the ephemeral endpoint will auto-expire (only present for ephemeral endpoints) example: "2026-06-01T10:30:00Z" created_at: type: string format: date-time description: When the endpoint was created example: "2025-12-01T10:00:00Z" updated_at: type: string format: date-time description: When the endpoint was last updated example: "2025-12-06T12:00:00Z" EndpointSummary: type: object description: Summary endpoint info for list responses (omits headers for performance) properties: id: type: string description: Unique endpoint identifier example: "ep_550e8400e29b41d4a716446655440000" url: type: string format: uri description: Webhook delivery URL example: "https://customer.app/webhooks" description: type: string description: Optional description example: "Main production webhook" paused: type: boolean description: Whether the endpoint is paused example: false ephemeral: type: boolean description: Whether this is an ephemeral (CI/test) endpoint example: false expires_at: type: string format: date-time description: When the ephemeral endpoint will auto-expire (only present for ephemeral endpoints) example: "2026-06-01T10:30:00Z" created_at: type: string format: date-time description: When the endpoint was created example: "2025-12-01T10:00:00Z" ListAPIKeysResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/APIKeyInfo' meta: $ref: '#/components/schemas/ResponseMeta' CreateAPIKeyRequest: type: object properties: label: type: string maxLength: 255 description: Optional human-readable label for the key example: "Production backend" CreateAPIKeyResponse: type: object properties: data: type: object properties: key_id: type: string description: Unique key identifier example: "key_abc123" key: type: string description: | **The full API key - shown only once!** Store this securely as it cannot be retrieved again. example: "hb_live_abcdefghijklmnopqrstuvwxyz123456" prefix: type: string description: First 11 characters of the key (for identification) example: "hb_live_abc" label: type: string description: Human-readable label example: "Production backend" created_at: type: string format: date-time example: "2025-12-06T01:30:00Z" meta: $ref: '#/components/schemas/ResponseMeta' APIKeyInfo: type: object properties: key_id: type: string description: Unique key identifier example: "key_abc123" label: type: string description: Human-readable label example: "Production backend" prefix: type: string description: First 11 characters of the key example: "hb_live_abc" created_at: type: string format: date-time example: "2025-12-01T10:00:00Z" last_used_at: type: string format: date-time description: Last time the key was used for authentication example: "2025-12-06T01:30:00Z" Message: type: object properties: id: type: string format: uuid description: Unique message identifier (UUIDv7) example: "01935abc-def0-7123-4567-890abcdef012" project_id: type: string description: Project that owns this message example: "proj_abc123" endpoint_id: type: string description: Endpoint ID for delivery example: "ep_def456" status: type: string enum: [queued, delivering, succeeded, pending_retry, failed_permanent] description: | Current message status: - `queued`: Waiting for initial delivery - `delivering`: Currently being delivered - `succeeded`: Successfully delivered - `pending_retry`: Waiting for scheduled retry - `failed_permanent`: All retries exhausted (in DLQ) example: "succeeded" attempt_count: type: integer description: Number of delivery attempts made example: 2 replay_count: type: integer description: Number of times this message has been manually replayed example: 0 content_type: type: string description: Content-Type header used for delivery example: "application/json" size_bytes: type: integer description: Payload size in bytes example: 256 payload_sha256: type: string description: SHA256 hash of the payload example: "abc123def456..." idempotency_key: type: string description: Idempotency key if provided example: "order-12345-created-v1" next_attempt_at: type: string format: date-time description: Scheduled time for next retry attempt (if pending_retry) example: "2025-12-06T17:00:00Z" last_error: type: string description: Error message from the last failed attempt example: "connection timeout" response_status: type: integer description: HTTP status code from the most recent delivery attempt example: 200 response_latency_ms: type: integer description: Latency in milliseconds for the most recent delivery attempt example: 245 first_attempt_at: type: string format: date-time nullable: true description: When the first delivery attempt was made delivered_at: type: string format: date-time nullable: true description: When the message was successfully delivered processing_ms: type: integer description: Time spent processing the delivery (signing, headers, etc.) total_delivery_ms: type: integer description: Total time from message creation to successful delivery queue_wait_ms: type: integer description: Time spent waiting in the queue before first attempt retention_hours: type: integer description: How long this message's payload is retained created_at: type: string format: date-time description: When the message was created example: "2025-12-06T12:00:00Z" updated_at: type: string format: date-time description: When the message was last updated example: "2025-12-06T12:01:30Z" deleted_at: type: string format: date-time nullable: true description: When the message was soft-deleted. Only present on deleted rows. example: "2026-04-09T00:22:45Z" deleted_by_user_id: type: string nullable: true description: User ID that deleted the message (mutually exclusive with `deleted_by_api_key_id`). deleted_by_api_key_id: type: string nullable: true description: API key ID that deleted the message (mutually exclusive with `deleted_by_user_id`). MessageSummary: type: object properties: message_id: type: string example: "01935abc-def0-7123-4567-890abcdef012" endpoint: type: string format: uri example: "https://customer.app/webhooks" status: type: string enum: [queued, delivering, succeeded, pending_retry, failed_permanent] example: "succeeded" attempt_count: type: integer example: 1 created_at: type: string format: date-time example: "2025-12-06T12:00:00Z" delivered_at: type: string format: date-time example: "2025-12-06T12:00:05Z" first_attempt_at: type: string format: date-time nullable: true description: When the first delivery attempt was made response_status: type: integer example: 200 response_latency_ms: type: integer example: 120 processing_ms: type: integer description: Time spent processing the delivery (signing, headers, etc.) total_delivery_ms: type: integer description: Total time from message creation to successful delivery queue_wait_ms: type: integer description: Time spent waiting in the queue before first attempt last_error: type: string example: "connection timeout" deleted_at: type: string format: date-time nullable: true description: When the message was soft-deleted. Only present on deleted rows; omitted for live messages. example: "2026-04-09T00:22:45Z" Metrics: type: object properties: window: type: string enum: [1h, 24h, 7d, 30d] description: Time window for these metrics example: "24h" total_messages: type: integer description: Total number of messages in the window example: 10000 succeeded: type: integer description: Number of successfully delivered messages example: 9950 failed: type: integer description: Number of permanently failed messages example: 50 retries: type: integer description: Total number of retry attempts example: 150 success_rate: type: number format: float description: Ratio of succeeded messages to total messages example: 0.995 avg_latency_ms: type: integer description: Average delivery latency in milliseconds example: 185 avg_delivery_time_ms: type: integer description: Average total delivery time (created_at to delivered_at) for succeeded messages avg_queue_wait_ms: type: integer description: Average queue wait time (created_at to first_attempt_at) for attempted messages # Billing Schemas CreateCheckoutRequest: type: object required: - plan - interval properties: plan: type: string enum: ["starter", "pro", "business"] description: Plan to subscribe to example: "pro" interval: type: string enum: ["monthly", "annual"] description: Billing interval example: "monthly" CreateCheckoutResponse: type: object properties: data: type: object properties: session_id: type: string description: Stripe checkout session ID example: "cs_test_abc123" checkout_url: type: string format: uri description: URL to redirect user to Stripe checkout example: "https://checkout.stripe.com/c/pay/cs_test_abc123" meta: $ref: '#/components/schemas/ResponseMeta' CreatePortalRequest: type: object properties: return_url: type: string format: uri description: URL to return to after portal session (must be from allowed origin) example: "https://app.hookbridge.io/billing" CreatePortalResponse: type: object properties: data: type: object properties: portal_url: type: string format: uri description: URL to redirect user to Stripe customer portal example: "https://billing.stripe.com/p/session/abc123" meta: $ref: '#/components/schemas/ResponseMeta' GetSubscriptionResponse: type: object properties: data: type: object properties: plan: type: string enum: ["free", "starter", "pro", "business", "enterprise"] description: Current subscription plan example: "pro" status: type: string enum: ["active", "canceled", "past_due", "trialing", "unpaid"] description: Subscription status example: "active" limits: type: object properties: plan: type: string example: "pro" messages_per_month: type: integer example: 50000 max_projects: type: integer example: 100 max_endpoints: type: integer example: 10000 retention_days: type: integer example: 7 overage_rate_per_message: type: number description: Cost per message over the plan limit example: 0 usage: type: object properties: messages_used: type: integer description: Messages used in current period example: 12500 overage_count: type: integer description: Number of messages over the plan limit example: 0 overage_rate: type: number description: Rate charged per overage message example: 0 estimated_overage_charge: type: number description: Estimated overage charge for current period example: 0 period_start: type: string format: date-time example: "2025-12-01T00:00:00Z" period_end: type: string format: date-time example: "2025-12-31T23:59:59Z" cancel_at_period_end: type: boolean description: Whether subscription will cancel at period end example: false current_period_end: type: string format: date-time description: When current billing period ends example: "2025-12-31T23:59:59Z" meta: $ref: '#/components/schemas/ResponseMeta' UsageHistoryRow: type: object properties: period_start: type: string format: date example: "2026-02-01" period_end: type: string format: date example: "2026-02-28" message_count: type: integer example: 6102 overage_count: type: integer example: 1102 plan_limit: type: integer nullable: true example: 5000 GetUsageHistoryResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/UsageHistoryRow' meta: allOf: - $ref: '#/components/schemas/ResponseMeta' - type: object properties: total: type: integer example: 6 limit: type: integer example: 12 offset: type: integer example: 0 has_more: type: boolean example: false InvoiceLine: type: object properties: description: type: string example: "Starter Plan (Monthly)" amount: type: integer example: 1000 quantity: type: integer example: 1 Invoice: type: object properties: id: type: string example: "in_1abc..." status: type: string example: "paid" amount_due: type: integer example: 1000 amount_paid: type: integer example: 1000 currency: type: string example: "usd" period_start: type: string format: date-time example: "2026-02-05T00:00:00Z" period_end: type: string format: date-time example: "2026-03-05T00:00:00Z" created: type: string format: date-time example: "2026-03-05T06:00:00Z" invoice_pdf: type: string format: uri nullable: true hosted_invoice_url: type: string format: uri nullable: true lines: type: array items: $ref: '#/components/schemas/InvoiceLine' GetInvoicesResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/Invoice' meta: allOf: - $ref: '#/components/schemas/ResponseMeta' - type: object properties: has_more: type: boolean example: false # Time Series Metrics Schemas TimeSeriesBucket: type: object properties: timestamp: type: string format: date-time description: Start time of this bucket example: "2025-12-06T00:00:00Z" succeeded: type: integer description: Successful deliveries in this bucket example: 450 failed: type: integer description: Failed deliveries in this bucket example: 5 retrying: type: integer description: Messages pending retry in this bucket example: 10 total: type: integer description: Total messages in this bucket example: 465 avg_latency_ms: type: integer description: Average delivery latency in milliseconds example: 180 GetTimeSeriesMetricsResponse: type: object properties: data: type: object properties: window: type: string enum: [1h, 24h, 7d, 30d] description: Time window for these metrics example: "24h" buckets: type: array items: $ref: '#/components/schemas/TimeSeriesBucket' meta: $ref: '#/components/schemas/ResponseMeta' # Inbound Endpoint Schemas CreateInboundEndpointRequest: type: object properties: name: type: string maxLength: 255 description: Friendly name for the inbound endpoint example: "Stripe webhooks" description: type: string maxLength: 500 description: Optional description example: "Receives Stripe payment events" mode: type: string enum: [forward, cli, pull] default: forward description: | Endpoint mode. `forward` delivers webhooks to the URL via HTTP. `cli` streams webhooks to a connected CLI tool (no HTTP delivery). example: "forward" url: type: string format: uri description: | HTTPS URL of your endpoint that will receive forwarded webhooks. Required when mode is `forward`. Optional when mode is `cli`. Must be publicly accessible. Internal/private IP addresses are blocked. pattern: '^https://' minLength: 12 maxLength: 2048 example: "https://myapp.com/webhooks/stripe" verify_static_token: type: boolean description: Enable static token verification on incoming webhooks default: false token_header_name: type: string description: Header name containing the static token (if verify_static_token is true) example: "X-Webhook-Token" token_query_param: type: string description: Query parameter name containing the static token (alternative to header) example: "token" token_value: type: string description: The static token value (will be hashed for storage) example: "my-secret-token" verify_hmac: type: boolean description: Enable HMAC signature verification on incoming webhooks default: false hmac_header_name: type: string description: Header name containing the HMAC signature (if verify_hmac is true) example: "X-Signature" hmac_secret: type: string description: HMAC secret key (will be encrypted for storage) example: "whsec_abc123" timestamp_header_name: type: string description: Header name containing the request timestamp for replay protection example: "X-Webhook-Timestamp" timestamp_ttl_seconds: type: integer minimum: 1 description: Maximum age in seconds for timestamp validation example: 300 verify_ip_allowlist: type: boolean description: Enable IP allowlist verification on incoming webhooks default: false allowed_cidrs: type: array items: type: string description: CIDR blocks allowed to send webhooks (if verify_ip_allowlist is true) example: ["203.0.113.0/24", "198.51.100.0/24"] idempotency_header_names: type: array items: type: string description: Ordered list of header names to derive idempotency keys from incoming requests example: ["X-Idempotency-Key"] ingest_response_code: type: integer minimum: 200 maximum: 299 default: 202 description: HTTP status code returned to senders on successful ingestion example: 202 signing_enabled: type: boolean description: Enable HMAC signing on forwarded deliveries (secret auto-generated) default: false ephemeral: type: boolean default: false description: | If true, creates an ephemeral inbound endpoint for CI/test workflows. Ephemeral endpoints auto-expire after the TTL. ttl_minutes: type: integer minimum: 1 # must match config.MinEphemeralTTLMinutes maximum: 1440 # must match config.MaxEphemeralTTLMinutes default: 30 # must match config.DefaultEphemeralTTLMinutes description: | Time-to-live in minutes for ephemeral endpoints. Only used when ephemeral is true. After this duration, the endpoint is automatically deleted. example: 60 CreateInboundEndpointResponse: type: object properties: data: type: object properties: id: type: string format: uuid description: Unique inbound endpoint identifier (UUIDv7) example: "01935abc-def0-7123-4567-890abcdef012" name: type: string description: Endpoint name example: "Stripe webhooks" url: type: string format: uri description: Forwarding URL (empty for cli-mode endpoints) example: "https://myapp.com/webhooks/stripe" mode: type: string enum: [forward, cli, pull] description: Endpoint mode example: "forward" ingest_url: type: string format: uri description: | **Shown only once!** The full URL that external senders should POST webhooks to. example: "https://receive.hookbridge.io/v1/webhooks/receive/01935abc-def0-7123-4567-890abcdef012/a1b2c3d4e5f6" secret_token: type: string description: | **Shown only once!** 32-character hex token embedded in the ingest URL. example: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" signing_key_id: type: string description: Initial signing key ID when delivery signing is enabled example: "sk_550e8400e29b41d4a716446655440001" signing_secret: type: string description: | **Shown only once!** Initial delivery signing secret when signing is enabled. example: "whsec_newabcdefghijklmnopqrstuvwxyz12" key_hint: type: string description: Last 4 chars of the signing key for identification example: "wxyz" ephemeral: type: boolean description: Whether this is an ephemeral (CI/test) endpoint example: false expires_at: type: string format: date-time description: When the ephemeral endpoint will auto-expire (only present for ephemeral endpoints) example: "2026-06-01T12:30:00Z" created_at: type: string format: date-time example: "2025-12-06T12:00:00Z" meta: $ref: '#/components/schemas/ResponseMeta' InboundEndpoint: type: object description: Full inbound endpoint details (returned by GET /inbound-endpoints/:id) properties: id: type: string format: uuid description: Unique inbound endpoint identifier example: "01935abc-def0-7123-4567-890abcdef012" name: type: string description: Endpoint name example: "Stripe webhooks" description: type: string description: Optional description example: "Receives Stripe payment events" url: type: string format: uri description: Forwarding URL (empty for cli-mode endpoints) example: "https://myapp.com/webhooks/stripe" mode: type: string enum: [forward, cli, pull] description: | Endpoint mode. `forward` delivers to URL. `cli` streams to CLI tool. example: "forward" ephemeral: type: boolean description: Whether this is an ephemeral (CI/test) endpoint example: false expires_at: type: string format: date-time description: When the ephemeral endpoint will auto-expire (only present for ephemeral endpoints) example: "2026-06-01T12:30:00Z" active: type: boolean description: Whether the endpoint is active example: true paused: type: boolean description: Whether the endpoint is paused example: false verify_static_token: type: boolean description: Static token verification enabled example: false verify_hmac: type: boolean description: HMAC verification enabled example: true verify_ip_allowlist: type: boolean description: IP allowlist verification enabled example: false ingest_response_code: type: integer description: HTTP status code returned to senders example: 202 idempotency_header_names: type: array items: type: string description: Header names used for idempotency example: [] signing_enabled: type: boolean description: Whether delivery signing is enabled example: false ingest_url: type: string format: uri description: | The URL that external senders should POST webhooks to. Includes the endpoint's secret token. Available on GET so the CLI and console can display it after initial creation. example: "https://receive.hookbridge.io/v1/webhooks/receive/01935abc-def0-7123-4567-890abcdef012/a1b2c3d4e5f6" created_at: type: string format: date-time example: "2025-12-01T10:00:00Z" updated_at: type: string format: date-time example: "2025-12-06T12:00:00Z" InboundEndpointSummary: type: object description: Summary inbound endpoint info for list responses properties: id: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" name: type: string example: "Stripe webhooks" url: type: string format: uri example: "https://myapp.com/webhooks/stripe" mode: type: string enum: [forward, cli, pull] description: | Endpoint mode. `forward` delivers to URL. `cli` streams to CLI tool. example: "forward" ephemeral: type: boolean description: Whether this is an ephemeral (CI/test) endpoint example: false expires_at: type: string format: date-time description: When the ephemeral endpoint will auto-expire (only present for ephemeral endpoints) example: "2026-06-01T12:30:00Z" active: type: boolean example: true paused: type: boolean example: false created_at: type: string format: date-time example: "2025-12-01T10:00:00Z" GetInboundEndpointResponse: type: object properties: data: $ref: '#/components/schemas/InboundEndpoint' meta: $ref: '#/components/schemas/ResponseMeta' ListInboundEndpointsResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/InboundEndpointSummary' meta: allOf: - $ref: '#/components/schemas/ResponseMeta' - type: object properties: next_cursor: type: string description: Cursor for the next page of results (opaque, do not parse) UpdateInboundEndpointRequest: type: object properties: name: type: string maxLength: 255 description: Updated endpoint name description: type: string maxLength: 500 description: Updated description mode: type: string enum: [forward, cli, pull] description: | Updated endpoint mode. When switching to `forward`, a URL is required — either provided in this request or already stored on the endpoint. url: type: string format: uri pattern: '^https://' minLength: 12 maxLength: 2048 description: Updated forwarding URL verify_static_token: type: boolean description: Enable/disable static token verification token_header_name: type: string description: Updated header name for static token token_query_param: type: string description: Updated query param for static token token_value: type: string description: Updated static token value verify_hmac: type: boolean description: Enable/disable HMAC verification hmac_header_name: type: string description: Updated HMAC header name hmac_secret: type: string description: Updated HMAC secret timestamp_header_name: type: string description: Updated timestamp header name timestamp_ttl_seconds: type: integer minimum: 1 description: Updated timestamp TTL verify_ip_allowlist: type: boolean description: Enable/disable IP allowlist verification allowed_cidrs: type: array items: type: string description: Updated CIDR allowlist idempotency_header_names: type: array items: type: string description: Updated idempotency header names ingest_response_code: type: integer minimum: 200 maximum: 299 description: Updated ingest response code signing_enabled: type: boolean description: Enable/disable delivery signing UpdateInboundEndpointResponse: type: object properties: data: type: object properties: id: type: string description: Inbound endpoint ID example: "01935abc-def0-7123-4567-890abcdef012" updated: type: boolean description: Whether the update was successful example: true meta: $ref: '#/components/schemas/ResponseMeta' ListenInboundEndpointResponse: type: object description: | Response from the CLI polling endpoint. Contains an array of inbound messages with their full payload and headers. Use `next_cursor` to paginate through additional messages. required: - data - meta properties: data: type: array items: type: object required: - message_id - content_type - headers - size_bytes - received_at properties: message_id: type: string format: uuid description: Unique inbound message identifier (UUIDv7) example: "01935abc-def0-7123-4567-890abcdef012" content_type: type: string description: Content-Type of the original inbound payload example: "application/json" headers: type: object additionalProperties: type: string description: HTTP headers from the original inbound request example: X-Stripe-Signature: "t=1234,v1=abc..." Content-Type: "application/json" size_bytes: type: integer description: Payload size in bytes example: 512 received_at: type: string format: date-time description: When the inbound message was received example: "2026-03-20T10:30:00Z" body: description: | Webhook payload body. JSON payloads are embedded directly as objects. Non-JSON payloads are base64-encoded strings (check `body_encoding` to distinguish). Omitted when the payload fetcher is not configured or the S3 key is missing. body_encoding: type: string enum: [base64] description: | Present only when body is base64-encoded (non-JSON payloads). Omitted for JSON payloads. Use this field to determine whether `body` is a raw JSON object or a base64-encoded string. example: "base64" body_error: type: string enum: [payload_unavailable] description: | Present only when the payload could not be fetched from storage. When set, `body` and `body_encoding` are omitted. example: "payload_unavailable" meta: allOf: - $ref: '#/components/schemas/ResponseMeta' - type: object properties: next_cursor: type: string format: uuid nullable: true description: | Cursor for the next page of results. Pass as the `after` query parameter to fetch subsequent messages. Null when the response data array is empty (no messages available after the cursor). example: "01935abc-def0-7123-4567-890abcdef099" # Inbound Message Schemas ReplayInboundMessageResponse: type: object properties: message: type: string example: "Inbound message queued for replay" data: type: object properties: message_id: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" status: type: string enum: [queued] example: "queued" meta: $ref: '#/components/schemas/ResponseMeta' ReplayAllInboundMessagesResponse: type: object properties: message: type: string example: "5 inbound messages queued for replay" data: type: object properties: replayed: type: integer description: Number of messages successfully re-enqueued example: 5 failed: type: integer description: Number of messages that failed to process example: 0 stuck: type: integer description: Number of messages updated in DB but failed to enqueue example: 0 replayed_message_ids: type: array items: type: string format: uuid description: IDs of successfully replayed messages example: ["01935abc-def0-7123-4567-890abcdef012"] stuck_message_ids: type: array items: type: string format: uuid description: IDs of messages that were updated but failed to enqueue (empty array when none) meta: $ref: '#/components/schemas/ResponseMeta' InboundLogEntry: type: object properties: message_id: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef012" inbound_endpoint_id: type: string format: uuid example: "01935abc-def0-7123-4567-890abcdef099" endpoint: type: string format: uri description: Forwarding URL example: "https://myapp.com/webhooks/stripe" status: type: string enum: [queued, delivering, succeeded, pending_retry, failed_permanent] example: "succeeded" attempt_count: type: integer example: 1 received_at: type: string format: date-time example: "2025-12-06T12:00:00Z" delivered_at: type: string format: date-time description: When the message was successfully delivered (if applicable) example: "2025-12-06T12:00:05Z" response_status: type: integer description: HTTP status code from the most recent delivery attempt example: 200 response_latency_ms: type: integer description: Latency in milliseconds for the most recent attempt example: 120 last_error: type: string description: Error message from the last failed attempt example: "connection timeout" total_delivery_ms: type: integer description: Total time in milliseconds from received to delivered example: 5000 deleted_at: type: string format: date-time nullable: true description: When the inbound message was soft-deleted. Only present on deleted rows; omitted for live messages. example: "2026-04-09T00:22:45Z" GetInboundLogsResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/InboundLogEntry' meta: allOf: - $ref: '#/components/schemas/ResponseMeta' - type: object properties: next_cursor: type: string description: Cursor for the next page of results has_more: type: boolean description: Whether more results are available GetInboundMetricsResponse: type: object properties: data: type: object properties: window: type: string enum: [1h, 24h, 7d, 30d] description: Time window for these metrics example: "24h" total_messages: type: integer description: Total number of inbound messages in the window example: 5000 succeeded: type: integer description: Successfully delivered messages example: 4900 failed: type: integer description: Permanently failed messages example: 20 retries: type: integer description: Total retry attempts example: 80 success_rate: type: number format: float description: Ratio of succeeded to total messages example: 0.98 avg_latency_ms: type: integer description: Average delivery latency in milliseconds example: 150 avg_delivery_time_ms: type: integer description: Average total time from received to delivered in milliseconds example: 3200 meta: $ref: '#/components/schemas/ResponseMeta' GetInboundTimeSeriesMetricsResponse: type: object properties: data: type: object properties: window: type: string enum: [1h, 24h, 7d, 30d] description: Time window for these metrics example: "24h" buckets: type: array items: $ref: '#/components/schemas/TimeSeriesBucket' meta: $ref: '#/components/schemas/ResponseMeta' InboundRejection: type: object properties: id: type: string description: Rejection record ID example: "01935abc-def0-7123-4567-890abcdef050" reason_code: type: string enum: - endpoint_not_found - endpoint_deleted - invalid_secret - static_token_failed - hmac_failed - ip_not_allowed - payload_too_large - invalid_method description: Machine-readable reason for rejection example: "hmac_failed" received_at: type: string format: date-time example: "2025-12-06T12:00:00Z" inbound_endpoint_id: type: string format: uuid nullable: true description: Inbound endpoint ID (null when reason_code is endpoint_not_found) example: "01935abc-def0-7123-4567-890abcdef012" reason_detail: type: string description: Additional detail about the rejection example: "HMAC signature mismatch" source_ip: type: string description: IP address of the sender example: "203.0.113.42" headers_redacted: type: string description: JSON string of request headers with values masked ListInboundRejectionsResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/InboundRejection' meta: allOf: - $ref: '#/components/schemas/ResponseMeta' - type: object properties: next_cursor: type: string description: Cursor for the next page of results has_more: type: boolean description: Whether more results are available # Export Schemas CreateExportRequest: type: object required: - start_time - end_time properties: start_time: type: string format: date-time description: Start of the export time range (RFC 3339) example: "2025-12-01T00:00:00Z" end_time: type: string format: date-time description: End of the export time range (RFC 3339). Must be after start_time. example: "2025-12-06T23:59:59Z" status: type: string enum: [queued, delivering, succeeded, pending_retry, failed_permanent] description: Optional filter by message status example: "failed_permanent" endpoint_id: type: string description: Optional filter by outbound endpoint ID (only for direction=outbound) example: "ep_550e8400e29b41d4a716446655440000" direction: type: string enum: [outbound, inbound] default: outbound description: | Export direction: - `outbound`: Export outbound webhook delivery data (default) - `inbound`: Export inbound webhook delivery data example: "outbound" inbound_endpoint_id: type: string format: uuid description: Optional filter by inbound endpoint ID (only for direction=inbound) example: "01935abc-def0-7123-4567-890abcdef012" ExportRecord: type: object properties: id: type: string format: uuid description: Export job identifier example: "01935abc-def0-7123-4567-890abcdef012" project_id: type: string description: Project that owns this export example: "proj_abc123" status: type: string enum: [pending, processing, completed, failed, expired] description: | Export job status: - `pending`: Queued for processing - `processing`: Currently generating the CSV - `completed`: Ready for download - `failed`: Generation failed - `expired`: Download link has expired example: "completed" filter_start_time: type: string format: date-time description: Start of the export time range example: "2025-12-01T00:00:00Z" filter_end_time: type: string format: date-time description: End of the export time range example: "2025-12-06T23:59:59Z" filter_status: type: string description: Message status filter (if specified) example: "failed_permanent" filter_endpoint_id: type: string format: uuid description: Outbound endpoint ID filter (if specified, stored as UUID) example: "550e8400-e29b-41d4-a716-446655440000" filter_inbound_endpoint_id: type: string format: uuid description: Inbound endpoint ID filter (if specified, when direction=inbound) example: "01935abc-def0-7123-4567-890abcdef012" direction: type: string enum: [outbound, inbound] description: Export direction example: "outbound" row_count: type: integer description: Number of rows in the export (populated after completion) example: 5000 file_size_bytes: type: integer description: File size in bytes (populated after completion) example: 1024000 error_message: type: string description: Error description (if failed) started_at: type: string format: date-time description: When processing started example: "2025-12-06T12:00:30Z" completed_at: type: string format: date-time description: When processing completed example: "2025-12-06T12:05:00Z" expires_at: type: string format: date-time description: When the download link expires example: "2025-12-13T12:05:00Z" created_at: type: string format: date-time description: When the export was requested example: "2025-12-06T12:00:00Z" CreateExportResponse: type: object properties: data: $ref: '#/components/schemas/ExportRecord' meta: $ref: '#/components/schemas/ResponseMeta' ListExportsResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/ExportRecord' meta: $ref: '#/components/schemas/ResponseMeta' GetExportResponse: type: object properties: data: $ref: '#/components/schemas/ExportRecord' meta: $ref: '#/components/schemas/ResponseMeta' # Project Schemas Project: type: object properties: id: type: string format: uuid description: Unique project identifier (UUIDv7) example: "01935abc-def0-7123-4567-890abcdef012" tenant_id: type: string description: Tenant that owns this project example: "tenant_abc123" name: type: string description: Project name example: "Production Webhooks" status: type: string enum: ["active", "suspended", "deleted"] description: Project status example: "active" rate_limit_default: type: integer description: Default rate limit for this project (requests per minute) example: 1000 created_at: type: string format: date-time example: "2025-12-01T10:00:00Z" ListProjectsResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/Project' meta: $ref: '#/components/schemas/ResponseMeta' GetProjectResponse: type: object properties: data: $ref: '#/components/schemas/Project' meta: $ref: '#/components/schemas/ResponseMeta' CreateProjectRequest: type: object required: - name properties: name: type: string minLength: 1 maxLength: 255 description: Project name example: "Production Webhooks" rate_limit_default: type: integer minimum: 0 description: Default rate limit (0 uses system default of 1000) example: 1000 UpdateProjectRequest: type: object properties: name: type: string minLength: 1 maxLength: 255 description: Updated project name rate_limit_default: type: integer minimum: 1 description: Updated rate limit in requests per minute # Outbound Replay Schemas ReplayAllMessagesResponse: type: object properties: message: type: string example: "5 messages queued for replay" data: type: object properties: replayed: type: integer description: Number of messages successfully re-enqueued example: 5 failed: type: integer description: Number of messages that failed to process example: 0 stuck: type: integer description: Number of messages updated in DB but failed to enqueue example: 0 replayed_message_ids: type: array items: type: string description: IDs of successfully replayed messages example: ["01935abc-def0-7123-4567-890abcdef012"] stuck_message_ids: type: array items: type: string description: IDs of messages that were updated but failed to enqueue (empty array when none) meta: $ref: '#/components/schemas/ResponseMeta' ReplayBatchMessagesResponse: type: object properties: message: type: string example: "2 messages replayed" data: type: object properties: replayed: type: integer description: Number of messages successfully re-enqueued example: 2 failed: type: integer description: Number of messages that failed to process example: 0 stuck: type: integer description: Number of messages updated in DB but failed to enqueue example: 0 results: type: array items: type: object properties: message_id: type: string description: The message ID example: "01935abc-def0-7123-4567-890abcdef012" status: type: string enum: [replayed, failed, stuck] description: Result status for this message example: "replayed" error: type: string description: Error message if the replay failed (only present for failed/stuck) description: Per-message replay results meta: $ref: '#/components/schemas/ResponseMeta' ReplayBatchInboundMessagesResponse: type: object properties: message: type: string example: "2 inbound messages replayed" data: type: object properties: replayed: type: integer description: Number of messages successfully re-enqueued example: 2 failed: type: integer description: Number of messages that failed to process example: 0 stuck: type: integer description: Number of messages updated in DB but failed to enqueue example: 0 results: type: array items: type: object properties: message_id: type: string description: The message ID example: "01935abc-def0-7123-4567-890abcdef012" status: type: string enum: [replayed, failed, stuck] description: Result status for this message example: "replayed" error: type: string description: Error message if the replay failed (only present for failed/stuck) description: Per-message replay results meta: $ref: '#/components/schemas/ResponseMeta' ReplayBatchRequest: type: object required: - message_ids properties: message_ids: type: array items: type: string minItems: 1 maxItems: 100 description: List of message IDs to replay example: - "01935abc-def0-7123-4567-890abcdef012" - "01935abc-def0-7123-4567-890abcdef013" # Attempt Schemas AttemptRecord: type: object description: A single delivery attempt for a webhook message required: - id - attempt_no - created_at properties: id: type: string format: uuid description: Unique attempt identifier (UUIDv7) example: "01935abc-def0-7123-4567-890abcdef001" attempt_no: type: integer description: Attempt number (1-based) example: 1 response_status: type: integer description: HTTP status code from the endpoint example: 200 response_latency_ms: type: integer description: Response latency in milliseconds example: 245 processing_ms: type: integer nullable: true description: Total processing time in milliseconds (outbound attempts only; not populated for inbound attempts) example: 280 error_text: type: string description: Error message if the attempt failed example: "connection timeout" response_headers: type: object additionalProperties: type: string description: Response headers from the endpoint retry_type: type: string description: Type of retry scheduling applied example: "slow" retry_after_seconds: type: integer description: Seconds until next retry was scheduled example: 1800 resolved_ip: type: string description: IP address the endpoint hostname resolved to example: "203.0.113.42" request_headers: type: object additionalProperties: type: string description: Request headers sent to the endpoint dns_ms: type: integer description: DNS resolution time in milliseconds example: 12 tcp_connect_ms: type: integer description: TCP connection time in milliseconds example: 25 tls_handshake_ms: type: integer description: TLS handshake time in milliseconds example: 45 ttfb_ms: type: integer description: Time to first byte in milliseconds example: 200 transfer_ms: type: integer description: Response transfer time in milliseconds example: 8 conn_reused: type: boolean description: Whether the TCP connection was reused example: false response_body_url: type: string format: uri description: Presigned URL to download the response body (Starter+ plans only, expires in 15 minutes) response_body_truncated: type: boolean default: false description: | Whether the stored response body was truncated. Only present in the response when true; absence means the body was not truncated. created_at: type: string format: date-time description: When the attempt was made example: "2025-12-06T12:00:00Z" GetMessageAttemptsResponse: type: object required: - data - meta properties: data: type: array items: $ref: '#/components/schemas/AttemptRecord' meta: allOf: - $ref: '#/components/schemas/ResponseMeta' - type: object required: - has_more properties: has_more: type: boolean description: Whether more attempts exist beyond the returned set # Inbound Message Schemas InboundMessage: type: object description: Full inbound message details required: - id - project_id - inbound_endpoint_id - status - attempt_count - replay_count - received_at - updated_at properties: id: type: string format: uuid description: Unique inbound message identifier (UUIDv7) example: "01935abc-def0-7123-4567-890abcdef012" project_id: type: string description: Project that owns this message example: "proj_abc123" inbound_endpoint_id: type: string format: uuid description: Inbound endpoint that received this message example: "01935abc-def0-7123-4567-890abcdef099" status: type: string enum: [queued, delivering, succeeded, pending_retry, failed_permanent] description: Current message delivery status example: "succeeded" attempt_count: type: integer description: Number of delivery attempts made example: 1 replay_count: type: integer description: Number of times this message has been manually replayed example: 0 content_type: type: string description: Content-Type of the original inbound payload example: "application/json" size_bytes: type: integer description: Payload size in bytes example: 512 payload_sha256: type: string description: SHA256 hash of the payload example: "abc123def456..." idempotency_key: type: string description: Derived idempotency key (if configured on the inbound endpoint) next_attempt_at: type: string format: date-time description: Scheduled time for next retry attempt (if pending_retry) last_error: type: string description: Error message from the last failed attempt example: "connection timeout" response_status: type: integer description: HTTP status code from the most recent delivery attempt example: 200 response_latency_ms: type: integer description: Latency in milliseconds for the most recent delivery attempt example: 120 queue_wait_ms: type: integer description: Time spent waiting in queue before first delivery attempt (ms) example: 50 total_delivery_ms: type: integer description: Total time from received to delivered (ms) example: 5000 received_at: type: string format: date-time description: When the inbound message was received example: "2025-12-06T12:00:00Z" updated_at: type: string format: date-time description: When the message was last updated example: "2025-12-06T12:00:05Z" delivered_at: type: string format: date-time description: When the message was successfully delivered example: "2025-12-06T12:00:05Z" failed_at: type: string format: date-time description: When the message permanently failed retention_hours: type: integer description: How long this message's payload is retained (hours) deleted_at: type: string format: date-time nullable: true description: When the inbound message was soft-deleted. Only present on deleted rows. example: "2026-04-09T00:22:45Z" deleted_by_user_id: type: string nullable: true description: User ID that deleted the inbound message (mutually exclusive with `deleted_by_api_key_id`). deleted_by_api_key_id: type: string nullable: true description: API key ID that deleted the inbound message (mutually exclusive with `deleted_by_user_id`). GetInboundMessageResponse: type: object required: - data - meta properties: data: $ref: '#/components/schemas/InboundMessage' meta: $ref: '#/components/schemas/ResponseMeta' GetInboundMessageAttemptsResponse: type: object required: - data - meta properties: data: type: array items: $ref: '#/components/schemas/AttemptRecord' description: Delivery attempts use the same schema as outbound attempts meta: allOf: - $ref: '#/components/schemas/ResponseMeta' - type: object required: - has_more properties: has_more: type: boolean description: Whether more attempts exist beyond the returned set # ── Pull Endpoint Schemas ────────────────────────────────────────── CreatePullEndpointRequest: type: object properties: name: type: string maxLength: 255 description: Human-readable name example: "Stripe Order Events" description: type: string maxLength: 500 description: Description of this endpoint's purpose retention_days: type: integer minimum: 1 description: Per-endpoint retention override (default is plan default) example: 30 event_type_source: type: string enum: [body, header] description: Where to extract the event type from incoming webhooks event_type_path: type: string maxLength: 256 description: JSON path for body extraction (e.g., "type") or header name (e.g., "X-GitHub-Event") example: "type" verify_static_token: type: boolean default: false token_header_name: type: string token_query_param: type: string token_value: type: string minLength: 16 verify_hmac: type: boolean default: false hmac_header_name: type: string hmac_secret: type: string timestamp_header_name: type: string timestamp_ttl_seconds: type: integer verify_ip_allowlist: type: boolean default: false allowed_cidrs: type: array items: type: string idempotency_header_names: type: array items: type: string ingest_response_code: type: integer minimum: 200 maximum: 299 default: 202 UpdatePullEndpointRequest: type: object description: Partial update — only provided fields are changed. properties: name: type: string maxLength: 255 description: type: string maxLength: 500 retention_days: type: integer minimum: 1 event_type_source: type: string enum: [body, header, ""] description: Set to empty string to clear event type extraction. event_type_path: type: string maxLength: 256 verify_static_token: type: boolean token_header_name: type: string token_query_param: type: string token_value: type: string minLength: 16 verify_hmac: type: boolean hmac_header_name: type: string hmac_secret: type: string timestamp_header_name: type: string timestamp_ttl_seconds: type: integer verify_ip_allowlist: type: boolean allowed_cidrs: type: array items: type: string idempotency_header_names: type: array items: type: string ingest_response_code: type: integer minimum: 200 maximum: 299 PullEndpoint: type: object required: - id - mode - ingest_url - active - paused - created_at - updated_at properties: id: type: string format: uuid name: type: string description: type: string mode: type: string enum: [pull] ingest_url: type: string format: uri description: URL to give to your webhook provider. Does not include the secret token suffix. active: type: boolean description: Whether the endpoint is active (not deleted). paused: type: boolean retention_days: type: integer nullable: true event_type_source: type: string enum: [body, header] event_type_path: type: string counts: type: object description: Event counts (only on single-endpoint GET, not list) properties: stored: type: integer fetched: type: integer delivered: type: integer total: type: integer verify_static_token: type: boolean token_header_name: type: string token_query_param: type: string verify_hmac: type: boolean hmac_header_name: type: string timestamp_header_name: type: string timestamp_ttl_seconds: type: integer nullable: true verify_ip_allowlist: type: boolean allowed_cidrs: type: array items: type: string ingest_response_code: type: integer idempotency_header_names: type: array items: type: string created_at: type: string format: date-time updated_at: type: string format: date-time CreatePullEndpointResponse: type: object properties: data: allOf: - $ref: '#/components/schemas/PullEndpoint' - type: object properties: secret_token: type: string description: Secret token for the ingestion URL. Shown only once at creation time. ingest_url: type: string description: Full ingestion URL including the secret token suffix. meta: $ref: '#/components/schemas/ResponseMeta' GetPullEndpointResponse: type: object properties: data: $ref: '#/components/schemas/PullEndpoint' meta: $ref: '#/components/schemas/ResponseMeta' ListPullEndpointsResponse: type: object properties: data: type: array items: type: object properties: id: type: string format: uuid name: type: string active: type: boolean paused: type: boolean ingest_url: type: string created_at: type: string format: date-time meta: allOf: - $ref: '#/components/schemas/ResponseMeta' - type: object properties: next_cursor: type: string nullable: true # ── Pull Event Schemas ────────────────────────────────────────── PullTimingBreakdown: type: object description: Timing breakdown for a pull event lifecycle. properties: ingest_processing_ms: type: integer nullable: true description: Server-side ingest processing time (validation, auth, S3 upload). Measured before the database write. Null for events ingested before timing was added. time_to_fetch_ms: type: integer nullable: true description: Time from receipt to first customer fetch (fetched_at - received_at). Includes ingest_processing_ms server processing time; subtract it to get pure queue wait time. Null if not yet fetched. time_to_ack_ms: type: integer nullable: true description: Time from fetch to acknowledgment (delivered_at - fetched_at). Null if not yet delivered. total_lifecycle_ms: type: integer nullable: true description: Total time from receipt to acknowledgment (delivered_at - received_at). Null if not yet delivered. PullEventSummary: type: object description: Event metadata returned in list responses (no payload). required: - id - status - size_bytes - received_at - timing properties: id: type: string format: uuid event_type: type: string nullable: true description: Extracted event type (null if extraction not configured or failed) example: "payment_intent.succeeded" status: type: string enum: [stored, fetched, delivered] size_bytes: type: integer received_at: type: string format: date-time fetched_at: type: string format: date-time nullable: true delivered_at: type: string format: date-time nullable: true timing: $ref: '#/components/schemas/PullTimingBreakdown' PullEventDetail: type: object description: | Full event including payload and original headers. For soft-deleted events, customer fetches (default) return `410 MESSAGE_DELETED`. Console preview requests (`?preview=true`) instead return a 200 with metadata only: `payload` and `headers` are `null`, and `deleted_at` / `deleted_by_user_id` / `deleted_by_api_key_id` identify the deletion. required: - id - status - content_type - size_bytes - received_at - timing properties: id: type: string format: uuid event_type: type: string nullable: true status: type: string enum: [stored, fetched, delivered] content_type: type: string example: "application/json" payload: nullable: true description: JSON payloads are embedded directly. Non-JSON payloads are base64-encoded strings. `null` for deleted events in preview mode. oneOf: - type: object - type: string headers: type: object nullable: true additionalProperties: type: string description: Original request headers from the webhook provider. `null` for deleted events in preview mode. size_bytes: type: integer received_at: type: string format: date-time fetched_at: type: string format: date-time nullable: true delivered_at: type: string format: date-time nullable: true timing: $ref: '#/components/schemas/PullTimingBreakdown' deleted_at: type: string format: date-time nullable: true description: When the event was soft-deleted. Only present (and only via `?preview=true`) on deleted rows. example: "2026-04-09T00:22:45Z" deleted_by_user_id: type: string nullable: true description: User ID that deleted the event (mutually exclusive with `deleted_by_api_key_id`). deleted_by_api_key_id: type: string nullable: true description: API key ID that deleted the event (mutually exclusive with `deleted_by_user_id`). ListPullEventsResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/PullEventSummary' meta: allOf: - $ref: '#/components/schemas/ResponseMeta' - type: object properties: has_more: type: boolean description: Whether more events exist beyond the returned page. next_cursor: type: string description: Opaque cursor for the next page. Empty string when no more results. GetPullEventResponse: type: object properties: data: $ref: '#/components/schemas/PullEventDetail' meta: $ref: '#/components/schemas/ResponseMeta' AckPullEventsRequest: type: object required: - event_ids properties: event_ids: type: array items: type: string format: uuid minItems: 1 maxItems: 1000 description: Event IDs to acknowledge as delivered. example: ["01935abc-def0-7123-4567-890abcdef012"] AckPullEventsResponse: type: object properties: data: type: object properties: acknowledged: type: integer description: Number of events newly acknowledged (already-delivered events are not counted). meta: $ref: '#/components/schemas/ResponseMeta' # ── Pull Observability Schemas ────────────────────────────────────── PullLogEntry: type: object required: - event_id - pull_endpoint_id - status - size_bytes - received_at - timing properties: event_id: type: string format: uuid pull_endpoint_id: type: string format: uuid endpoint_name: type: string description: Name of the pull endpoint (joined from endpoint record) event_type: type: string nullable: true status: type: string enum: [stored, fetched, delivered] size_bytes: type: integer received_at: type: string format: date-time fetched_at: type: string format: date-time nullable: true delivered_at: type: string format: date-time nullable: true deleted_at: type: string format: date-time nullable: true description: When the pull event was soft-deleted. Only present on deleted rows; omitted for live events. example: "2026-04-09T00:22:45Z" timing: $ref: '#/components/schemas/PullTimingBreakdown' GetPullLogsResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/PullLogEntry' meta: allOf: - $ref: '#/components/schemas/ResponseMeta' - type: object properties: has_more: type: boolean next_cursor: type: string GetPullMetricsResponse: type: object properties: data: $ref: '#/components/schemas/Metrics' meta: $ref: '#/components/schemas/ResponseMeta' PullTimeSeriesBucket: type: object properties: timestamp: type: string format: date-time description: Start time of this bucket example: "2025-12-06T00:00:00Z" succeeded: type: integer description: Delivered (acknowledged) messages in this bucket example: 450 stored: type: integer description: Messages received but not yet fetched by the customer example: 10 fetched: type: integer description: Messages fetched by the customer but not yet acknowledged example: 5 total: type: integer description: Total messages received in this bucket example: 465 GetPullTimeSeriesMetricsResponse: type: object properties: data: type: object properties: window: type: string enum: [1h, 24h, 7d, 30d] buckets: type: array items: $ref: '#/components/schemas/PullTimeSeriesBucket' meta: $ref: '#/components/schemas/ResponseMeta' ErrorResponse: type: object properties: error: type: object properties: code: type: string description: Machine-readable error code example: "INVALID_REQUEST" message: type: string description: Human-readable error message example: "endpoint must be a valid HTTPS URL" meta: $ref: '#/components/schemas/ResponseMeta' ResponseMeta: type: object properties: request_id: type: string description: Unique identifier for this request (useful for support) example: "req_xyz123" responses: BadRequest: description: Invalid request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: endpoint_not_found: summary: Endpoint not found value: error: code: "ENDPOINT_NOT_FOUND" message: "endpoint not found" meta: request_id: "req_xyz123" invalid_endpoint_id: summary: Invalid endpoint ID format value: error: code: "INVALID_REQUEST" message: "endpoint_id must be in format ep_xxx" meta: request_id: "req_xyz123" invalid_payload: summary: Invalid payload value: error: code: "INVALID_REQUEST" message: "payload must be valid JSON" meta: request_id: "req_xyz123" invalid_headers: summary: Invalid headers value: error: code: "INVALID_HEADERS" message: "header 'Host' is forbidden and cannot be overridden" meta: request_id: "req_xyz123" Unauthorized: description: Unauthorized - Invalid or missing API key content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "UNAUTHORIZED" message: "Invalid or missing API key" meta: request_id: "req_xyz123" NotFound: description: Resource not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "NOT_FOUND" message: "Message not found" meta: request_id: "req_xyz123" Conflict: description: Conflict - action cannot be completed content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: max_signing_keys: summary: Maximum signing keys reached value: error: code: "MAX_SIGNING_KEYS_REACHED" message: "Endpoint already has the maximum of 2 signing keys" meta: request_id: "req_xyz123" cannot_delete_last_key: summary: Cannot delete last signing key value: error: code: "CANNOT_DELETE_LAST_SIGNING_KEY" message: "Cannot delete the last signing key" meta: request_id: "req_xyz123" InternalServerError: description: Internal server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "INTERNAL_ERROR" message: "An internal error occurred" meta: request_id: "req_xyz123" RateLimitExceeded: description: Rate limit exceeded content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: "RATE_LIMIT_EXCEEDED" message: "Rate limit exceeded. Please try again later." meta: request_id: "req_xyz123" headers: Retry-After: description: Number of seconds to wait before retrying schema: type: integer example: 60