Tags
Tags let you attribute costs to teams, environments, features, or anything else. Attach a JSON object to any request and query costs by those dimensions in the
Tags let you attribute costs to teams, environments, features, or anything else. Attach a JSON object to any request and query costs by those dimensions in the dashboard, API, or webhooks.
Sending Tags
Add the X-NullSpend-Tags header with a JSON object:
TypeScript
const response = await openai.chat.completions.create(
{
model: "gpt-4o",
messages: [{ role: "user", content: "Hello" }],
},
{
headers: {
"X-NullSpend-Tags": JSON.stringify({
team: "billing",
env: "production",
feature: "summarizer",
}),
},
}
);Python
import json
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
extra_headers={
"X-NullSpend-Tags": json.dumps({
"team": "billing",
"env": "production",
"feature": "summarizer",
}),
},
)cURL
curl https://proxy.nullspend.dev/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "X-NullSpend-Key: $NULLSPEND_API_KEY" \
-H 'X-NullSpend-Tags: {"team":"billing","env":"production"}' \
-H "Content-Type: application/json" \
-d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}]}'Validation Rules
Tags are supplementary — they never cause a request to be rejected. Invalid tags are silently dropped.
| Rule | Limit |
|---|---|
| Max keys per request | 10 |
| Key pattern | [a-zA-Z0-9_-]+ |
| Key max length | 64 characters |
| Value max length | 256 characters |
| Reserved prefix | _ns_ — keys starting with this are silently dropped |
| Null bytes in values | Tag is silently dropped |
| Invalid JSON | All tags silently dropped (request proceeds with no tags) |
| Single invalid key/value | That key is dropped; valid keys are kept |
System Tags
NullSpend uses the _ns_ prefix for internal tags. You cannot set these — they are added automatically when applicable. User-supplied tags with the _ns_ prefix are silently dropped.
All providers
| Tag | Value | When |
|---|---|---|
_ns_estimated | "true" | Cost is an estimate (stream was cancelled before completion or no usage was reported) |
_ns_cancelled | "true" | The streaming response was cancelled by the client |
_ns_no_usage | "true" | The provider returned a response without usage metadata |
_ns_unpriced | "true" | The model isn't in the pricing catalog — cost recorded as 0 |
_ns_max_tokens | string | The max_tokens / maxOutputTokens value sent on the request |
_ns_temperature | string | The temperature value sent on the request |
_ns_tool_count | string | Number of tool definitions in the request |
OpenAI / Anthropic
| Tag | Value | When |
|---|---|---|
_ns_ratelimit_remaining_requests | string | Provider-reported requests-remaining (from x-ratelimit-remaining-requests) |
_ns_ratelimit_remaining_tokens | string | Provider-reported tokens-remaining (from x-ratelimit-remaining-tokens) |
Anthropic only
| Tag | Value | When |
|---|---|---|
_ns_cache_write_tokens | string | Tokens written to the prompt cache (cache_creation_input_tokens) |
_ns_cache_read_tokens | string | Tokens read from the prompt cache (cache_read_input_tokens) |
_ns_long_context | "true" | Total input exceeded 200K tokens — long-context multipliers applied |
Gemini only
| Tag | Value | When |
|---|---|---|
_ns_thinking_tokens | string | Thinking tokens consumed (thoughtsTokenCount, Gemini 2.5+) |
_ns_google_response_id | string | Google's responseId from the response body (proxy generates its own x-request-id header) |
Customer Attribution
For per-customer cost tracking and margin analysis, use the dedicated X-NullSpend-Customer header instead of (or in addition to) tags. The customer ID lands in the customer_id column of cost events, which the Margins dashboard and the Margins API read from directly.
If you cannot set a custom header (e.g., a third-party SDK that strips unknown headers), use the customer tag fallback:
X-NullSpend-Tags: {"customer":"acme-corp"}The proxy auto-elevates the customer tag value into the customer_id column, so margin tracking works the same either way. The header takes precedence when both are present.
Cost Attribution by Tags
The Attribution page lets you group costs by any tag value. Select a tag key from the dropdown and see a ranked breakdown of spend per value — with daily trends, model breakdowns, and CSV export.
This is the primary way to answer "how much does each customer cost me?" when you're tagging requests with customer_id or similar keys.
Querying by Tags
Dashboard
Filter cost events by tag key-value pairs in the analytics view, or use the Attribution page for aggregated per-tag-value breakdowns.
API
Use tag.* query parameters on GET /api/cost-events:
# All cost events tagged with team=billing (requires dashboard session)
curl "https://nullspend.dev/api/cost-events?tag.team=billing" \
-H "Cookie: session=..."
# Multiple tag filters (AND logic)
curl "https://nullspend.dev/api/cost-events?tag.team=billing&tag.env=production" \
-H "Cookie: session=..."Tag queries use PostgreSQL JSONB containment (@>), so they are indexed and fast.
Tags in Webhooks
Tags are included in the cost_event.created webhook payload under data.object.tags:
{
"id": "evt_abc123",
"type": "cost_event.created",
"api_version": "2026-04-01",
"created_at": 1711036800,
"data": {
"object": {
"request_id": "req_xyz",
"provider": "openai",
"model": "gpt-4o",
"cost_microdollars": 45,
"tags": {
"team": "billing",
"env": "production"
}
}
}
}See Webhook Event Types for the full payload.
Tag Budgets
You can create budgets scoped to a specific tag key-value pair. When spend for that tag exceeds the budget, the proxy blocks requests carrying that tag with 429:
{
"error": {
"code": "tag_budget_exceeded",
"message": "Request blocked: tag budget exceeded",
"details": {
"tag_key": "team",
"tag_value": "billing",
"budget_limit_microdollars": 50000000,
"budget_spend_microdollars": 49500000
}
}
}Tag budgets support the same features as user and API key budgets: reset intervals, threshold alerts, and velocity limits. See Budgets for configuration details.
Related
- Cost Attribution — group and drill into costs by any tag dimension
- Custom Headers Reference — header format and validation
- Cost Tracking — how costs are calculated
- Budgets — enforce spending limits including per-tag budgets
- Per-Customer Budgets — bind a customer to a plan + dollar cap and gate every action with
bind()/gate() - Webhook Event Types — tags in webhook payloads
Session Limits
Session limits cap how much a single agent conversation can spend, regardless of the overall budget. A runaway agent loop that stays under velocity limits can s
Tracing
Tracing links related LLM requests so you can see what a multi-step agent run cost as a whole. Every request through the proxy gets a trace ID — either one yo