NullSpend Docs

Webhook Delivery

How NullSpend delivers webhook events, including transport, retries, and failure handling.

How NullSpend delivers webhook events, including transport, retries, and failure handling.

Two Delivery Paths

NullSpend has two independent systems that dispatch webhooks, depending on where the event originates:

PathEventsTransportRetries
Proxy-sidecost_event.created, all budget events, velocity.*, session.*, tag_budget.*, request.blockedCloudflare Queue5 retries, exponential backoff
Dashboard-sideaction.*, test.ping, dashboard-originated cost_event.createdDirect HTTP POSTNone (fire-and-forget)

Both paths sign payloads identically — your verification code works the same regardless of which path delivered the event.

Proxy-Side Delivery

Events from the proxy worker are delivered via a Cloudflare Queue-based dispatch pipeline.

How it works:

  1. The proxy builds the event payload and enqueues it to the webhook delivery queue
  2. The queue consumer fetches the endpoint metadata, signs the payload, and delivers the HTTP POST
  3. If your endpoint returns a non-2xx response or doesn't respond within 5 seconds, the consumer retries with exponential backoff

Retry behavior:

  • 5 retry attempts with exponential backoff
  • Your endpoint must return a 2xx status code within 5 seconds
  • After all retries are exhausted, the event goes to the dead-letter queue (DLQ)

Event type filtering: Each endpoint can subscribe to specific event types. If an endpoint's eventTypes array is empty, it receives all events. If it lists specific types, only matching events are dispatched.

Dashboard-Side Delivery

Events from the Next.js dashboard (action lifecycle events, test pings) are delivered directly via HTTP POST.

How it works:

  1. The dashboard queries the database for your active webhook endpoints
  2. Each matching endpoint receives a signed HTTP POST
  3. The request has a 5-second timeout — if your endpoint doesn't respond in time, the attempt is abandoned
  4. No retries. Dashboard-side delivery is fire-and-forget.

Secret rotation cleanup: After dispatching, the dashboard checks for endpoints with expired rotation windows (older than 24 hours) and clears the previous signing secret. This cleanup is lazy and fire-and-forget — it doesn't affect delivery.

Headers

Every webhook POST — from both paths — includes these headers:

HeaderValuePurpose
Content-Typeapplication/jsonAlways JSON
X-NullSpend-SignatureHMAC-SHA256 signatureVerify authenticity (see Security)
X-NullSpend-Webhook-IdEvent ID (e.g., evt_a1b2c3d4-...)Deduplicate deliveries
X-NullSpend-Webhook-TimestampUnix timestamp (seconds)Detect replay attacks
User-AgentNullSpend-Webhooks/1.0Identify NullSpend traffic

Payload Modes

Endpoints can be configured with a payload mode that controls how cost_event.created events are delivered:

ModeBehavior
full (default)Complete event data in data.object
thinReference only in related_object — fetch full data from the API

Payload mode only affects cost_event.created events. All other event types always use full payloads regardless of the endpoint's mode setting.

Thin payload example:

{
  "id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "type": "cost_event.created",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "related_object": {
    "id": "req_xyz",
    "type": "cost_event",
    "url": "/api/cost-events?requestId=req_xyz&provider=openai"
  }
}

Fetch the full event data using the url field with your API key. See Webhooks Overview for more on payload modes.

Failure Handling

Webhook delivery is fail-open — errors are logged but never block the operation that triggered the event.

  • A failed webhook dispatch never prevents a cost event from being written
  • A failed webhook dispatch never blocks or delays an API response
  • If the endpoint lookup fails (cache miss + DB error), the event is silently dropped

DLQ: On the proxy side, events that fail all 5 retry attempts land in the dead-letter queue. These are not automatically retried.

Dashboard side: No DLQ. Failed deliveries are logged server-side but not retried or stored.

Endpoint Caching

Proxy

The proxy caches endpoint metadata (ID, URL, event types) to avoid querying the database on every request:

  • Workers KV with a 5-minute TTL
  • Signing secrets are never cached — they are always fetched from the database at dispatch time

Cache invalidation happens when you create, update, or delete an endpoint via the dashboard API.

Dashboard

The dashboard queries the database directly for each dispatch — no caching layer.

Best Practices

  • Return 200 fast. Do your processing asynchronously. Proxy-side events retry on timeout; dashboard-side events are lost.
  • Deduplicate by event ID. Use the X-NullSpend-Webhook-Id header (same as event.id) to skip duplicate deliveries from retries.
  • Don't rely on ordering. Events may arrive out of order, especially with retries. Use created_at to determine sequence.
  • Use thin mode for high volume. If you process hundreds of cost events per minute, thin mode reduces bandwidth and latency. Fetch full details on demand.
  • Monitor for DLQ entries. If your endpoint has persistent failures, events accumulate in the dead-letter queue.

For expanded best practices with code examples, see Best Practices.

On this page