NullSpend Docs

Webhook Event Types

NullSpend emits 20 event types. Each event is delivered as an HTTP POST with a JSON body.

NullSpend emits 20 event types. Each event is delivered as an HTTP POST with a JSON body.

Event Envelope

Full Event

{
  "id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "type": "cost_event.created",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": { }
  }
}

Thin Event

Used for cost_event.created on endpoints with payloadMode: "thin". All other event types always use the full envelope.

{
  "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"
  }
}
FieldTypeDescription
idstringUnique event ID (evt_ + UUID). Use for deduplication.
typestringOne of the 20 event types below.
api_versionstringAPI version ("2026-04-01").
created_atintegerUnix timestamp in seconds.
data.objectobjectEvent-specific payload (full mode).
related_objectobjectReference to fetchable object (thin mode).

Cost Events

cost_event.created

Fires when a cost event is recorded — once per proxied request.

data.object fields:

FieldTypeDescription
request_idstringUnique request identifier
event_typestringRequest type: "llm" (LLM API call), "tool" (MCP tool invocation), or "custom" (SDK-reported)
providerstring"openai", "anthropic", or "google"
modelstringModel name (e.g., gpt-4o)
input_tokensintegerTotal input tokens
output_tokensintegerOutput tokens
cached_input_tokensintegerCached input tokens
cost_microdollarsintegerTotal cost in microdollars
duration_msintegerRequest duration in milliseconds
upstream_duration_msinteger or nullTime spent waiting for the LLM provider
session_idstring or nullSession ID if set
trace_idstring or nullTrace ID
tool_namestring or nullMCP tool name
tool_serverstring or nullMCP tool server
tool_calls_requestedarray or nullArray of {name, id} objects representing tool calls, or null
tool_definition_tokensintegerToken count for tool definitions (defaults to 0)
api_key_idstringAPI key that made the request
sourcestring"proxy", "sdk", "api", or "mcp"
tagsobjectKey-value pairs from X-NullSpend-Tags
created_atstringISO 8601 timestamp

Example:

{
  "id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "type": "cost_event.created",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "request_id": "chatcmpl-abc123",
      "event_type": "llm",
      "provider": "openai",
      "model": "gpt-4o",
      "input_tokens": 1000,
      "output_tokens": 500,
      "cached_input_tokens": 200,
      "cost_microdollars": 7,
      "duration_ms": 1234,
      "upstream_duration_ms": 1180,
      "session_id": null,
      "trace_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
      "tool_name": null,
      "tool_server": null,
      "tool_calls_requested": null,
      "tool_definition_tokens": 0,
      "api_key_id": "ns_key_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "source": "proxy",
      "tags": { "team": "billing", "env": "production" },
      "created_at": "2026-03-21T12:00:00.000Z"
    }
  }
}

Budget Events

budget.threshold.warning

Fires when spend crosses a threshold percentage below 90% (e.g., 50%, 80%).

data.object fields:

FieldTypeDescription
budget_entity_typestring"user", "api_key", or "tag"
budget_entity_idstringEntity identifier
threshold_percentintegerThreshold crossed (e.g., 80)
budget_spend_microdollarsintegerCurrent spend
budget_limit_microdollarsintegerBudget ceiling
budget_remaining_microdollarsintegerRemaining budget (limit minus spend)
triggered_by_request_idstringRequest that triggered the crossing

Example:

{
  "id": "evt_b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "type": "budget.threshold.warning",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "budget_entity_type": "api_key",
      "budget_entity_id": "ns_key_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "threshold_percent": 80,
      "budget_spend_microdollars": 40500000,
      "budget_limit_microdollars": 50000000,
      "budget_remaining_microdollars": 9500000,
      "triggered_by_request_id": "chatcmpl-abc123"
    }
  }
}

budget.threshold.critical

Same structure as budget.threshold.warning. Fires when spend crosses a threshold ≥ 90%.

{
  "id": "evt_c3d4e5f6-a7b8-9012-cdef-123456789012",
  "type": "budget.threshold.critical",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "budget_entity_type": "user",
      "budget_entity_id": "user_12345",
      "threshold_percent": 95,
      "budget_spend_microdollars": 47800000,
      "budget_limit_microdollars": 50000000,
      "budget_remaining_microdollars": 2200000,
      "triggered_by_request_id": "chatcmpl-def456"
    }
  }
}

budget.exceeded

Fires when a request is blocked because the budget ceiling was hit.

data.object fields:

FieldTypeDescription
budget_entity_typestringEntity type
budget_entity_idstringEntity identifier
budget_limit_microdollarsintegerBudget ceiling
budget_spend_microdollarsintegerCurrent spend
estimated_request_cost_microdollarsintegerEstimated cost of the blocked request
modelstringRequested model
providerstringProvider name
blocked_atstringISO 8601 timestamp when blocked

Example:

{
  "id": "evt_d4e5f6a7-b8c9-0123-defa-234567890123",
  "type": "budget.exceeded",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "budget_entity_type": "api_key",
      "budget_entity_id": "ns_key_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "budget_limit_microdollars": 50000000,
      "budget_spend_microdollars": 49800000,
      "estimated_request_cost_microdollars": 500000,
      "model": "gpt-4o",
      "provider": "openai",
      "blocked_at": "2026-03-21T12:00:00.000Z"
    }
  }
}

budget.increased

Fires when a budget limit is increased via a HITL budget increase approval.

data.object fields:

FieldTypeDescription
budget_entity_typestringEntity type
budget_entity_idstringEntity identifier
previous_limit_microdollarsintegerBudget limit before the increase
new_limit_microdollarsintegerBudget limit after the increase
increased_by_microdollarsintegerAmount of the increase
approved_bystringUser who approved the increase
action_idstringHITL action ID that triggered the increase

Example:

{
  "id": "evt_d4e5f6a7-b8c9-0123-defa-234567890124",
  "type": "budget.increased",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "budget_entity_type": "user",
      "budget_entity_id": "user_12345",
      "previous_limit_microdollars": 50000000,
      "new_limit_microdollars": 100000000,
      "increased_by_microdollars": 50000000,
      "approved_by": "admin_user",
      "action_id": "ns_act_550e8400-e29b-41d4-a716-446655440000"
    }
  }
}

budget.reset

Fires when a budget period resets (daily, weekly, or monthly).

data.object fields:

FieldTypeDescription
budget_entity_typestringEntity type
budget_entity_idstringEntity identifier
budget_limit_microdollarsintegerBudget ceiling
previous_spend_microdollarsintegerSpend in the period that just ended
new_period_startstringISO 8601 timestamp of the new period
reset_intervalstring"daily", "weekly", or "monthly"

Example:

{
  "id": "evt_e5f6a7b8-c9d0-1234-efab-345678901234",
  "type": "budget.reset",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "budget_entity_type": "user",
      "budget_entity_id": "user_12345",
      "budget_limit_microdollars": 50000000,
      "previous_spend_microdollars": 42000000,
      "new_period_start": "2026-04-01T00:00:00.000Z",
      "reset_interval": "monthly"
    }
  }
}

Enforcement Events

request.blocked

Fires when a request is blocked for any reason.

data.object fields:

FieldTypeDescription
reasonstring"budget", "rate_limit", or "policy"
modelstringRequested model
providerstringProvider name
api_key_idstringAPI key
detailsobject or nullAdditional context (varies by reason)

Example:

{
  "id": "evt_f6a7b8c9-d0e1-2345-fabc-456789012345",
  "type": "request.blocked",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "reason": "budget",
      "model": "gpt-4o",
      "provider": "openai",
      "api_key_id": "ns_key_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "details": null
    }
  }
}

velocity.exceeded

Fires when a velocity limit trips the circuit breaker.

data.object fields:

FieldTypeDescription
budget_entity_typestringEntity type
budget_entity_idstringEntity identifier
velocity_limit_microdollarsintegerConfigured velocity limit
velocity_window_secondsintegerSliding window size
velocity_current_microdollarsintegerSpend in the current window
cooldown_secondsintegerHow long requests will be blocked
modelstringRequested model
providerstringProvider name
blocked_atstringISO 8601 timestamp when blocked

Example:

{
  "id": "evt_a7b8c9d0-e1f2-3456-abcd-567890123456",
  "type": "velocity.exceeded",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "budget_entity_type": "api_key",
      "budget_entity_id": "ns_key_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "velocity_limit_microdollars": 10000000,
      "velocity_window_seconds": 60,
      "velocity_current_microdollars": 10500000,
      "cooldown_seconds": 60,
      "model": "gpt-4o",
      "provider": "openai",
      "blocked_at": "2026-03-21T12:00:00.000Z"
    }
  }
}

velocity.recovered

Fires when the velocity circuit breaker closes after cooldown.

data.object fields:

FieldTypeDescription
budget_entity_typestringEntity type
budget_entity_idstringEntity identifier
velocity_limit_microdollarsintegerConfigured velocity limit
velocity_window_secondsintegerSliding window size
velocity_cooldown_secondsintegerCooldown duration that just ended
recovered_atstringISO 8601 timestamp when recovered

Example:

{
  "id": "evt_b8c9d0e1-f2a3-4567-bcde-678901234567",
  "type": "velocity.recovered",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "budget_entity_type": "api_key",
      "budget_entity_id": "ns_key_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "velocity_limit_microdollars": 10000000,
      "velocity_window_seconds": 60,
      "velocity_cooldown_seconds": 60,
      "recovered_at": "2026-03-21T12:01:00.000Z"
    }
  }
}

session.limit_exceeded

Fires when a session's cumulative spend exceeds the session limit.

data.object fields:

FieldTypeDescription
budget_entity_typestringEntity type
budget_entity_idstringEntity identifier
session_idstringThe session that was capped
session_spend_microdollarsintegerCumulative session spend
session_limit_microdollarsintegerConfigured session limit
modelstringRequested model
providerstringProvider name
blocked_atstringISO 8601 timestamp when blocked

Example:

{
  "id": "evt_c9d0e1f2-a3b4-5678-cdef-789012345678",
  "type": "session.limit_exceeded",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "budget_entity_type": "user",
      "budget_entity_id": "user_12345",
      "session_id": "conv_abc123",
      "session_spend_microdollars": 4800000,
      "session_limit_microdollars": 5000000,
      "model": "gpt-4o",
      "provider": "openai",
      "blocked_at": "2026-03-21T12:00:00.000Z"
    }
  }
}

tag_budget.exceeded

Fires when a tag-level budget is exceeded.

data.object fields:

FieldTypeDescription
budget_entity_typestring"tag"
budget_entity_idstringTag entity ID (key=value)
tag_keystringTag key
tag_valuestringTag value
budget_limit_microdollarsintegerTag budget ceiling
budget_spend_microdollarsintegerCurrent tag spend
estimated_request_cost_microdollarsintegerEstimated cost of the blocked request
modelstringRequested model
providerstringProvider name
blocked_atstringISO 8601 timestamp when blocked

Example:

{
  "id": "evt_d0e1f2a3-b4c5-6789-defa-890123456789",
  "type": "tag_budget.exceeded",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "budget_entity_type": "tag",
      "budget_entity_id": "team=billing",
      "tag_key": "team",
      "tag_value": "billing",
      "budget_limit_microdollars": 50000000,
      "budget_spend_microdollars": 49500000,
      "estimated_request_cost_microdollars": 500000,
      "model": "gpt-4o",
      "provider": "openai",
      "blocked_at": "2026-03-21T12:00:00.000Z"
    }
  }
}

customer_budget.exceeded

Fires when a customer-scoped budget is exceeded. Similar to budget.exceeded but specific to per-customer budgets.

Producers:

  • Proxy LLM calls tagged with the X-NullSpend-Customer header (provider: "openai", "anthropic", "google").
  • POST /v1/gate denials when the customer's bind cap is exhausted (provider: "gate", model: the feature field from the gate request, or empty string).

Fires at most once per logical denial — idempotent replays of the same Idempotency-Key do NOT re-fire the webhook, and 503 idempotency_unavailable paths defer the fire to the caller's retry.

data.object fields:

FieldTypeDescription
budget_entity_typestring"customer"
budget_entity_idstringCustomer identifier
customer_idstringCustomer identifier (same as budget_entity_id)
budget_limit_microdollarsintegerCustomer budget ceiling
budget_spend_microdollarsintegerCurrent customer spend
estimated_request_cost_microdollarsintegerEstimated cost of the blocked request
modelstringRequested model
providerstringProvider name
blocked_atstringISO 8601 timestamp when blocked

Example:

{
  "id": "evt_e1f2a3b4-c5d6-7890-efab-901234567891",
  "type": "customer_budget.exceeded",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "budget_entity_type": "customer",
      "budget_entity_id": "acme-corp",
      "customer_id": "acme-corp",
      "budget_limit_microdollars": 25000000,
      "budget_spend_microdollars": 24800000,
      "estimated_request_cost_microdollars": 500000,
      "model": "gpt-4o",
      "provider": "openai",
      "blocked_at": "2026-03-21T12:00:00.000Z"
    }
  }
}

loop.detected

Fires when the proxy detects a runaway agent loop — the same prompt or near-identical prompt fingerprint repeated more times than the configured threshold within a sliding window. The offending request is blocked.

data.object fields:

FieldTypeDescription
detection_typestring"exact" (identical content hash) or "semantic" (similar fingerprint)
modelstringModel the loop was detected against
providerstringProvider name
call_countintegerRepeated-call count observed in the window
window_secondsintegerSliding window size in seconds
max_callsintegerConfigured ceiling that was exceeded
blocked_atstringISO 8601 timestamp when blocked

The content hash that triggered the detection is intentionally omitted from the payload — surfacing it would leak prompt fingerprints to webhook subscribers.

Example:

{
  "id": "evt_a4b5c6d7-e8f9-0123-abcd-456789012347",
  "type": "loop.detected",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "detection_type": "exact",
      "model": "gpt-4o",
      "provider": "openai",
      "call_count": 50,
      "window_seconds": 60,
      "max_calls": 25,
      "blocked_at": "2026-03-21T12:00:00.000Z"
    }
  }
}

plan_limit.exceeded

Fires when an org exceeds its NullSpend plan-tier governed-request cap (Free tier hard-blocks today). Distinct from budget.exceeded, which is for org-configured budgets. The payload mirrors the 429 response body so subscribers can react identically to inline errors and out-of-band alerts.

data.object fields:

FieldTypeDescription
current_countintegerGoverned requests used in the current period
block_atintegerCap that triggered the block
tierstringCurrent plan tier (e.g., "free")
upgrade_urlstring or nullURL the caller should send users to upgrade
self_host_urlstring or nullURL with self-host instructions
modelstringModel the blocked request targeted
providerstringProvider the blocked request targeted ("openai", "anthropic", "google")
blocked_atstringISO 8601 timestamp when blocked

Example:

{
  "id": "evt_b5c6d7e8-f9a0-1234-bcde-567890123458",
  "type": "plan_limit.exceeded",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "current_count": 100000,
      "block_at": 100000,
      "tier": "free",
      "upgrade_url": "https://nullspend.dev/pricing",
      "self_host_url": "https://nullspend.dev/docs",
      "model": "gpt-4o",
      "provider": "openai",
      "blocked_at": "2026-03-21T12:00:00.000Z"
    }
  }
}

Margin Events

margin.threshold_crossed

Fires when a customer's margin health tier worsens (e.g., healthy to moderate, moderate to at_risk). Only fires on worsening transitions, not improvements. Requires an active Stripe connection.

data.object fields:

FieldTypeDescription
customer.stripeIdstringStripe customer ID
customer.namestring or nullCustomer name from Stripe
customer.tagValuestringNullSpend tag value mapped to this customer
margin.previousnumberPrevious margin as a decimal (e.g., 0.35 = 35%)
margin.currentnumberCurrent margin as a decimal
margin.previousTierstringPrevious health tier: "healthy", "moderate", "at_risk", or "critical"
margin.currentTierstringCurrent health tier
revenue_microdollarsintegerCurrent period revenue
cost_microdollarsintegerCurrent period cost
periodstringBilling period (e.g., "2026-04")

Example:

{
  "id": "evt_f2a3b4c5-d6e7-8901-fabc-012345678902",
  "type": "margin.threshold_crossed",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "customer": {
        "stripeId": "cus_abc123",
        "name": "Acme Corp",
        "tagValue": "acme-corp"
      },
      "margin": {
        "previous": 0.35,
        "current": 0.18,
        "previousTier": "healthy",
        "currentTier": "moderate"
      },
      "revenue_microdollars": 50000000,
      "cost_microdollars": 41000000,
      "period": "2026-04"
    }
  }
}

Health tier thresholds: healthy (≥ 50%), moderate (20%–49%), at_risk (0%–19%), critical (< 0%). See Margins — Health Tiers for the full breakdown.


HITL Action Events

See Human-in-the-Loop for the full approval workflow, state machine, and SDK integration.

action.created

Fires when a human-in-the-loop approval action is created.

data.object fields:

FieldTypeDescription
action_idstringAction identifier (ns_act_ + UUID)
action_typestringType of action
agent_idstringAgent that created the action
statusstring"pending"
payloadobjectAction payload (the data submitted for approval)
created_atstringISO 8601 timestamp
expires_atstring or nullWhen the action expires if not acted on

Example:

{
  "id": "evt_e1f2a3b4-c5d6-7890-efab-901234567890",
  "type": "action.created",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "action_id": "ns_act_550e8400-e29b-41d4-a716-446655440000",
      "action_type": "http_post",
      "agent_id": "my-agent",
      "status": "pending",
      "payload": { "amount": 500, "description": "Large purchase" },
      "created_at": "2026-03-21T12:00:00.000Z",
      "expires_at": "2026-03-21T13:00:00.000Z"
    }
  }
}

action.approved

Fires when an action is approved.

data.object fields:

FieldTypeDescription
action_idstringAction identifier
action_typestringType of action
agent_idstringAgent that created the action
statusstring"approved"
approved_bystring or nullUser who approved
approved_atstring or nullISO 8601 timestamp of approval

action.rejected

Fires when an action is rejected.

data.object fields:

FieldTypeDescription
action_idstringAction identifier
action_typestringType of action
agent_idstringAgent that created the action
statusstring"rejected"
rejected_bystring or nullUser who rejected
rejected_atstring or nullISO 8601 timestamp of rejection
reasonstring or nullRejection reason

action.expired

Fires when an action's TTL expires.

data.object fields:

FieldTypeDescription
action_idstringAction identifier
action_typestringType of action
agent_idstringAgent that created the action
statusstring"expired"
expired_atstring or nullISO 8601 timestamp of expiry

Test Events

test.ping

Sent when you click "Test" in the dashboard. Use it to verify your endpoint is reachable and signature verification works.

Example:

{
  "id": "evt_f2a3b4c5-d6e7-8901-fabc-012345678901",
  "type": "test.ping",
  "api_version": "2026-04-01",
  "created_at": 1711036800,
  "data": {
    "object": {
      "message": "Test webhook event"
    }
  }
}

On this page