Tracing Outbound Webhooks
Hookbase supports W3C Trace Context on outbound webhooks. A trace starts in your service when you send an event and continues through each delivery attempt to the endpoint that receives it:
- Your service calls
POST /v1/events(orPOST /api/send-event) with atraceparentheader. - Hookbase opens a
hookbase.outbound.sendspan as a child of your span. - Each delivery attempt to each endpoint gets its own
hookbase.outbound.deliveryspan, a child of the send span. Retries are included. - The receiving endpoint gets a
traceparentheader that names the attempt span, so its own server span joins the same trace.
If you've set up Observability Export, these spans go to the same backend as your inbound spans (Grafana, Honeycomb, New Relic, Axiom or any OTLP endpoint). The traceparent header is sent to endpoints whether or not you've set up an export.
Sending a trace context
Send your current span's context in the standard headers:
curl -X POST "https://api.hookbase.app/v1/events" \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-H "traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" \
-H "tracestate: vendor=opaque-value" \
-d '{ "eventType": "order.created", "payload": { "orderId": "ord_123" } }'Most OpenTelemetry SDKs add these headers for you when you make the request with an instrumented HTTP client (for example @opentelemetry/instrumentation-http in Node.js, or opentelemetry-instrumentation-requests in Python).
- No
traceparent: Hookbase starts a new trace for the send. - Invalid
traceparent: Hookbase ignores it and starts a new trace, as the W3C spec requires. Atracestatesent with an invalidtraceparentis dropped. tracestateis passed to your endpoints unchanged. It must be well formed: at most 32 members and 512 characters. Otherwise Hookbase drops it.
What your endpoint receives
Every delivery attempt carries the trace context in its request headers:
POST /webhooks HTTP/1.1
Content-Type: application/json
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-b7ad6b7169203331-01
tracestate: vendor=opaque-value
webhook-id: ...- The trace ID (
4bf92f…4736) is the trace you sent, or the new trace Hookbase started. - The span ID (
b7ad6b71…) is the delivery attempt span. A retry is a new attempt, so it sends a new span ID in the same trace. - If your endpoint is instrumented with OpenTelemetry, its server span becomes a child of the attempt span automatically.
- A custom endpoint header named
traceparentortracestateis replaced by the trace context.
The headers are also stored with each attempt, so you can see them in the attempt's request headers in the dashboard and through the attempts API.
Finding a trace
The outbound message detail page in the dashboard (Outbound Webhooks, then Delivery Logs, then a message) shows the message's Trace ID with a copy button. Search for that ID in your tracing backend. The API returns it too:
curl "https://api.hookbase.app/api/organizations/{orgId}/outbound-messages/{messageId}" \
-H "Authorization: Bearer {token}"
# { "data": { "id": "...", "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", ... } }Sampling
- Your sampling decision is kept. If your
traceparenthas the sampled flag off (-00), Hookbase still passes the context on with-00, but it doesn't export the spans. - The export config's sampling rate is decided by trace ID. Each trace is kept or dropped as a whole, so you never see a send span without its delivery attempts, or attempts without their send.
Replays and DLQ retries
A replayed message, or a message retried from the dead-letter queue, starts its own trace. Its first attempt is the root span, and later attempts of that message are its children.
Spans
hookbase.outbound.send (kind SERVER)
One per send request, including rejected ones (404, 413, 422, 429).
| Attribute | Description |
|---|---|
hookbase.org.id | Organization ID |
hookbase.event_type | Event type name |
hookbase.event.id | Outbound event ID (when accepted) |
hookbase.application.id | Target application, when the send names one |
hookbase.outbound.messages_queued | Number of messages queued |
http.request.method | POST |
http.response.status_code / http.status_code | Response status of the send call |
hookbase.outbound.delivery (kind CLIENT)
One per delivery attempt. Its parent is the send span.
| Attribute | Description |
|---|---|
hookbase.org.id | Organization ID |
hookbase.application.id | Application ID |
hookbase.endpoint.id | Endpoint ID |
hookbase.message.id | Message ID |
hookbase.event.id | Outbound event ID |
hookbase.event_type | Event type name |
hookbase.delivery.attempt | Attempt number (1-based) |
hookbase.delivery.outcome | success, failed or timeout |
hookbase.delivery.latency_ms | Round-trip time to the endpoint |
hookbase.delivery.retry_scheduled | Whether another attempt was scheduled |
hookbase.delivery.retry_delay_s | Delay before that attempt, when one was scheduled |
http.request.method | POST |
server.address | Endpoint host |
http.response.status_code / http.status_code | Endpoint response status (absent on a network error or timeout) |
error.type | http_error, timeout, network or auth on a failed attempt |
The span status is OK for a successful attempt and ERROR otherwise. Attempts skipped by an open circuit breaker don't send a request, so they don't get a span.
Metrics
| Metric | Description |
|---|---|
hookbase.outbound.delivery.latency_ms | Attempt round-trip time |
hookbase.outbound.delivery.success | Successful attempts |
hookbase.outbound.delivery.failure | Failed attempts |
hookbase.outbound.delivery.retry | Attempts that scheduled a retry |
Metrics are tagged with hookbase.org.id, hookbase.application.id and hookbase.endpoint.id.
Resource filters
An export config with a resource filter exports outbound spans only when the filter includes the application or endpoint ID they belong to. A config with no filter exports everything.
Reliability
Telemetry never affects delivery. Spans are exported in the background after the attempt finishes. A slow or failing collector doesn't delay, fail or retry a webhook. Without an export config, nothing is exported, and the only cost is the traceparent header on the request.