NullSpend Docs

Cost Events API

Create, list, and analyze cost events. Cost events are the core data model — every AI API call tracked by NullSpend produces one.

Create, list, and analyze cost events. Cost events are the core data model — every AI API call tracked by NullSpend produces one.

See API Overview for authentication, pagination, errors, and ID formats.


Ingest Single Event

POST /api/cost-events

Record a cost event from your agent or SDK. The proxy creates these automatically; use this endpoint for custom integrations.

Authentication

API key

Parameters

NameInTypeRequiredDescription
providerbodystringYesProvider name (e.g., "openai", "anthropic"). 1–100 chars.
modelbodystringYesModel identifier (e.g., "gpt-4o", "claude-sonnet-4-5-20250514"). 1–200 chars.
inputTokensbodyintegerYesInput token count. Min 0.
outputTokensbodyintegerYesOutput token count. Min 0.
costMicrodollarsbodyintegerYesCost in microdollars (1 microdollar = $0.000001). Min 0.
cachedInputTokensbodyintegerNoCached input tokens. Default 0.
reasoningTokensbodyintegerNoReasoning/thinking tokens. Default 0.
durationMsbodyintegerNoRequest duration in milliseconds.
sessionIdbodystringNoSession identifier. Max 200 chars.
traceIdbodystringNo128-bit hex trace ID (^[0-9a-f]{32}$).
eventTypebodystringNo"llm", "tool", or "custom". Default "custom". Stored but not returned in list/detail responses — used for internal filtering and analytics.
toolNamebodystringNoTool name for tool-use events. Max 200 chars.
toolServerbodystringNoTool server name. Max 200 chars.
tagsbodyobjectNoKey-value metadata. Max 10 keys, key 1–64 chars (^[a-zA-Z0-9_-]+$), value max 256 chars.
idempotencyKeybodystringNoDeduplication key. Max 200 chars. Alternative to Idempotency-Key header.
Idempotency-KeyheaderstringNoDeduplication key. Takes priority over body field.

Request

const res = await fetch("https://nullspend.dev/api/cost-events", {
  method: "POST",
  headers: {
    "X-NullSpend-Key": "ns_live_sk_abc123...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    provider: "openai",
    model: "gpt-4o",
    inputTokens: 1200,
    outputTokens: 350,
    costMicrodollars: 5250,
    tags: { environment: "production", agent: "support-bot" },
  }),
});
import requests

resp = requests.post(
    "https://nullspend.dev/api/cost-events",
    headers={"X-NullSpend-Key": "ns_live_sk_abc123..."},
    json={
        "provider": "openai",
        "model": "gpt-4o",
        "inputTokens": 1200,
        "outputTokens": 350,
        "costMicrodollars": 5250,
        "tags": {"environment": "production", "agent": "support-bot"},
    },
)
curl -X POST https://nullspend.dev/api/cost-events \
  -H "X-NullSpend-Key: ns_live_sk_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "openai",
    "model": "gpt-4o",
    "inputTokens": 1200,
    "outputTokens": 350,
    "costMicrodollars": 5250,
    "tags": {"environment": "production", "agent": "support-bot"}
  }'

Response

201 Created (new event) or 200 OK (deduplicated):

{
  "data": {
    "id": "ns_evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "createdAt": "2026-03-20T14:30:00.000Z"
  }
}

Idempotency

Deduplication key resolution (first match wins):

  1. Idempotency-Key header
  2. idempotencyKey body field
  3. Auto-generated sdk_<uuid>

Duplicate detection uses the (requestId, provider) unique index. If a duplicate is found, the endpoint returns 200 with the original event's ID and timestamp.

Errors

CodeHTTPWhen
validation_error400Invalid or missing required fields
invalid_json400Malformed JSON body
unsupported_media_type415Content-Type is not application/json
payload_too_large413Body exceeds 1 MB
authentication_required401Missing or invalid API key
rate_limit_exceeded429Per-key rate limit exceeded

Batch Ingest

POST /api/cost-events/batch

Insert up to 100 cost events in a single request. Duplicates are silently skipped.

Authentication

API key

Parameters

NameInTypeRequiredDescription
eventsbodyarrayYesArray of cost event objects (same schema as single ingest). 1–100 items.

Request

const res = await fetch("https://nullspend.dev/api/cost-events/batch", {
  method: "POST",
  headers: {
    "X-NullSpend-Key": "ns_live_sk_abc123...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    events: [
      {
        provider: "openai",
        model: "gpt-4o",
        inputTokens: 500,
        outputTokens: 200,
        costMicrodollars: 2100,
      },
      {
        provider: "anthropic",
        model: "claude-sonnet-4-5-20250514",
        inputTokens: 800,
        outputTokens: 300,
        costMicrodollars: 4950,
      },
    ],
  }),
});
import requests

resp = requests.post(
    "https://nullspend.dev/api/cost-events/batch",
    headers={"X-NullSpend-Key": "ns_live_sk_abc123..."},
    json={
        "events": [
            {
                "provider": "openai",
                "model": "gpt-4o",
                "inputTokens": 500,
                "outputTokens": 200,
                "costMicrodollars": 2100,
            },
            {
                "provider": "anthropic",
                "model": "claude-sonnet-4-5-20250514",
                "inputTokens": 800,
                "outputTokens": 300,
                "costMicrodollars": 4950,
            },
        ]
    },
)
curl -X POST https://nullspend.dev/api/cost-events/batch \
  -H "X-NullSpend-Key: ns_live_sk_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {"provider":"openai","model":"gpt-4o","inputTokens":500,"outputTokens":200,"costMicrodollars":2100},
      {"provider":"anthropic","model":"claude-sonnet-4-5-20250514","inputTokens":800,"outputTokens":300,"costMicrodollars":4950}
    ]
  }'

Response

201 Created:

{
  "inserted": 2,
  "ids": [
    "ns_evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "ns_evt_b2c3d4e5-f6a7-8901-bcde-f12345678901"
  ]
}

inserted reflects only newly created events — duplicates are silently skipped via ON CONFLICT DO NOTHING.

Errors

CodeHTTPWhen
validation_error400Empty array, >100 events, or invalid event fields
invalid_json400Malformed JSON body
unsupported_media_type415Content-Type is not application/json
payload_too_large413Body exceeds 1 MB
authentication_required401Missing or invalid API key
rate_limit_exceeded429Per-key rate limit exceeded

List Cost Events

GET /api/cost-events

Retrieve cost events for the current organization with filtering and pagination.

Authentication

Session (dashboard)

Parameters

NameInTypeRequiredDescription
limitqueryintegerNoPage size. 1–100, default 25.
cursorquerystringNoJSON-encoded cursor from a previous response.
requestIdquerystringNoFilter by request ID. 1–200 chars.
apiKeyIdquerystringNoFilter by API key (ns_key_*).
modelquerystringNoFilter by model name.
providerquerystringNoFilter by provider.
sourcequerystringNoFilter by source: "proxy", "api", or "mcp".
traceIdquerystringNoFilter by trace ID (32 hex chars).
sessionIdquerystringNoFilter by session ID. 1–200 chars. Returns events for a specific session.
tag.*querystringNoJSONB containment filter. Example: tag.environment=production.

Request

# Requires dashboard session cookie
curl https://nullspend.dev/api/cost-events?limit=10&provider=openai \
  -H "Cookie: session=..."

Response

200 OK:

{
  "data": [
    {
      "id": "ns_evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "requestId": "sdk_f8e7d6c5-b4a3-2190-fedc-ba0987654321",
      "apiKeyId": "ns_key_11223344-5566-7788-99aa-bbccddeeff00",
      "provider": "openai",
      "model": "gpt-4o",
      "inputTokens": 1200,
      "outputTokens": 350,
      "cachedInputTokens": 0,
      "reasoningTokens": 0,
      "costMicrodollars": 5250,
      "durationMs": 1340,
      "createdAt": "2026-03-20T14:30:00.000Z",
      "source": "proxy",
      "traceId": "a1b2c3d4e5f67890a1b2c3d4e5f67890",
      "sessionId": "research-task-47",
      "tags": { "environment": "production" },
      "keyName": "production-key"
    }
  ],
  "cursor": {
    "createdAt": "2026-03-20T14:30:00.000Z",
    "id": "ns_evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Headers: NullSpend-Version: 2026-04-01

Errors

CodeHTTPWhen
validation_error400Invalid query parameters
authentication_required401No valid session

Get Single Event

GET /api/cost-events/:id

Retrieve a single cost event by ID. The event must belong to the authenticated user's organization.

Authentication

Session (dashboard)

Parameters

NameInTypeRequiredDescription
idpathstringYesEvent ID. Accepts ns_evt_* or raw UUID.

Request

# Requires dashboard session cookie
curl https://nullspend.dev/api/cost-events/ns_evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "Cookie: session=..."

Response

200 OK:

{
  "data": {
    "id": "ns_evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "requestId": "sdk_f8e7d6c5-b4a3-2190-fedc-ba0987654321",
    "apiKeyId": "ns_key_11223344-5566-7788-99aa-bbccddeeff00",
    "provider": "openai",
    "model": "gpt-4o",
    "inputTokens": 1200,
    "outputTokens": 350,
    "cachedInputTokens": 0,
    "reasoningTokens": 0,
    "costMicrodollars": 5250,
    "durationMs": 1340,
    "createdAt": "2026-03-20T14:30:00.000Z",
    "source": "proxy",
    "traceId": "a1b2c3d4e5f67890a1b2c3d4e5f67890",
    "sessionId": "research-task-47",
    "tags": { "environment": "production" },
    "keyName": "production-key"
  }
}

Errors

CodeHTTPWhen
validation_error400Invalid ID format
authentication_required401No valid session
not_found404Event not found or not in user's organization

Get Session

GET /api/cost-events/sessions/:sessionId

Retrieve all cost events for a session, in chronological order, with aggregate summary stats. Use this to build session replay views.

Authentication

Session (dashboard)

Parameters

NameInTypeRequiredDescription
sessionIdpathstringYesSession ID. 1–200 chars.

Request

# Requires dashboard session cookie
curl https://nullspend.dev/api/cost-events/sessions/research-task-47 \
  -H "Cookie: session=..."

Response

200 OK:

{
  "sessionId": "research-task-47",
  "summary": {
    "eventCount": 12,
    "totalCostMicrodollars": 43000,
    "totalInputTokens": 15200,
    "totalOutputTokens": 4800,
    "totalDurationMs": 8340,
    "startedAt": "2026-03-20T14:21:05.000Z",
    "endedAt": "2026-03-20T14:23:39.000Z"
  },
  "events": [
    {
      "id": "ns_evt_...",
      "requestId": "req-001",
      "provider": "openai",
      "model": "gpt-4o",
      "inputTokens": 1200,
      "outputTokens": 350,
      "costMicrodollars": 5250,
      "durationMs": 680,
      "createdAt": "2026-03-20T14:21:05.000Z",
      "sessionId": "research-task-47",
      "tags": {},
      "keyName": "production-key"
    }
  ]
}

Events are ordered chronologically (oldest first). Maximum 200 events per session. If a session has no events, events is an empty array and summary.startedAt/endedAt are null.

Errors

CodeHTTPWhen
validation_error400Invalid or empty session ID
authentication_required401No valid session

Cost Analytics

GET /api/cost-events/summary

Aggregate cost breakdown by day, model, provider, key, tool, source, and trace.

Authentication

Session (dashboard)

Parameters

NameInTypeRequiredDescription
periodquerystringNo"7d", "30d", or "90d". Default "30d".
excludeEstimatedquerystringNo"true" or "false". Default "false".

Request

# Requires dashboard session cookie
curl "https://nullspend.dev/api/cost-events/summary?period=7d" \
  -H "Cookie: session=..."

Response

200 OK:

KeyDescription
dailyCost per day over the period
modelsCost, token counts, and request count per model
providersCost and request count per provider
keysCost and request count per API key
toolsCost, request count, and avg duration per tool
sourcesCost and request count per source (proxy/api/mcp)
tracesCost and request count per trace ID
totalsTotal cost, total requests, and the period value
costBreakdownInput, output, cached, and reasoning cost breakdown
{
  "daily": [
    { "date": "2026-03-20", "totalCostMicrodollars": 125000 },
    { "date": "2026-03-19", "totalCostMicrodollars": 98500 }
  ],
  "models": [
    {
      "provider": "openai",
      "model": "gpt-4o",
      "totalCostMicrodollars": 85000,
      "requestCount": 42,
      "inputTokens": 50000,
      "outputTokens": 15000,
      "cachedInputTokens": 0,
      "reasoningTokens": 0
    }
  ],
  "providers": [
    { "provider": "openai", "totalCostMicrodollars": 85000, "requestCount": 42 }
  ],
  "keys": [
    {
      "apiKeyId": "ns_key_11223344-5566-7788-99aa-bbccddeeff00",
      "keyName": "production-key",
      "totalCostMicrodollars": 85000,
      "requestCount": 42
    }
  ],
  "tools": [
    {
      "model": "gpt-4o",
      "totalCostMicrodollars": 12000,
      "requestCount": 8,
      "avgDurationMs": 1250
    }
  ],
  "sources": [
    { "source": "proxy", "totalCostMicrodollars": 80000, "requestCount": 38 }
  ],
  "traces": [
    {
      "traceId": "a1b2c3d4e5f67890a1b2c3d4e5f67890",
      "totalCostMicrodollars": 15000,
      "requestCount": 5
    }
  ],
  "totals": {
    "totalCostMicrodollars": 223500,
    "totalRequests": 84,
    "period": "7d"
  },
  "costBreakdown": {
    "inputCost": 120000,
    "outputCost": 95000,
    "cachedCost": 3500,
    "reasoningCost": 5000
  }
}

Headers: NullSpend-Version: 2026-04-01

Errors

CodeHTTPWhen
validation_error400Invalid period or excludeEstimated value
authentication_required401No valid session

Export Cost Events

GET /api/cost-events/export

Export cost events as a CSV file. Returns up to 10,000 rows sorted by createdAt DESC. Supports the same filters as the list endpoint.

Authentication

Session (dashboard)

Parameters

NameInTypeRequiredDescription
providerquerystringNoFilter by provider.
modelquerystringNoFilter by model name.
apiKeyIdquerystringNoFilter by API key (ns_key_*).
sourcequerystringNoFilter by source: "proxy", "api", or "mcp".
sessionIdquerystringNoFilter by session ID.
traceIdquerystringNoFilter by trace ID (32 hex chars).
tag.*querystringNoJSONB containment filter. Example: tag.environment=production.

Request

# Requires dashboard session cookie
curl "https://nullspend.dev/api/cost-events/export?provider=openai" \
  -H "Cookie: session=..." \
  -o cost-events.csv

Response

200 OK — CSV file download.

Headers: Content-Type: text/csv; charset=utf-8, Content-Disposition: attachment; filename="nullspend-cost-events-2026-03-28.csv"

CSV columns: id, request_id, provider, model, input_tokens, output_tokens, cached_input_tokens, reasoning_tokens, cost_microdollars, cost_usd, duration_ms, source, session_id, trace_id, key_name, created_at.

The cost_usd column is a convenience conversion (cost_microdollars / 1,000,000).

Errors

CodeHTTPWhen
authentication_required401No valid session
forbidden403User lacks viewer role

Cost Attribution

GET /api/cost-events/attribution

Group cost events by API key or any tag value. Returns ranked groups with total cost, request count, and average cost per request. Supports JSON and CSV export.

See Cost Attribution for the feature overview and common patterns.

Authentication

Session (dashboard)

Parameters

NameInTypeRequiredDescription
groupByquerystringYes"api_key" for API key grouping, or any tag key name (e.g., "customer_id"). 1–100 chars.
periodquerystringNo"7d", "30d", or "90d". Default "30d".
limitqueryintegerNoMax groups returned. 1–500, default 100.
excludeEstimatedquerystringNo"true" or "false". Default "false". Excludes cancelled stream estimates.
formatquerystringNo"json" (default) or "csv". CSV returns a downloadable file.

Request

# Group by API key (requires dashboard session)
curl "https://nullspend.dev/api/cost-events/attribution?groupBy=api_key&period=30d" \
  -H "Cookie: session=..."

# Group by customer_id tag
curl "https://nullspend.dev/api/cost-events/attribution?groupBy=customer_id&period=30d" \
  -H "Cookie: session=..."

# CSV export
curl "https://nullspend.dev/api/cost-events/attribution?groupBy=api_key&format=csv" \
  -H "Cookie: session=..." \
  -o attribution.csv

Response

200 OK (JSON):

{
  "data": {
    "groups": [
      {
        "key": "production-key",
        "keyId": "ns_key_11223344-5566-7788-99aa-bbccddeeff00",
        "totalCostMicrodollars": 8500000,
        "requestCount": 4200,
        "avgCostMicrodollars": 2024
      },
      {
        "key": "staging-key",
        "keyId": "ns_key_aabbccdd-eeff-0011-2233-445566778899",
        "totalCostMicrodollars": 1200000,
        "requestCount": 800,
        "avgCostMicrodollars": 1500
      }
    ],
    "period": "30d",
    "groupBy": "api_key",
    "totalGroups": 2,
    "hasMore": false,
    "totals": {
      "totalCostMicrodollars": 9700000,
      "totalRequests": 5000
    }
  }
}

When groupBy is a tag key, keyId is null for all groups:

{
  "data": {
    "groups": [
      {
        "key": "acme-corp",
        "keyId": null,
        "totalCostMicrodollars": 6000000,
        "requestCount": 3000,
        "avgCostMicrodollars": 2000
      }
    ],
    "period": "30d",
    "groupBy": "customer_id",
    "totalGroups": 1,
    "hasMore": false,
    "totals": {
      "totalCostMicrodollars": 9700000,
      "totalRequests": 5000
    }
  }
}

totals contains org-wide aggregates for the period (not just the visible groups). hasMore is true when more groups exist beyond the limit.

200 OK (CSV, when format=csv):

key,key_id,total_cost_microdollars,total_cost_usd,request_count,avg_cost_microdollars,avg_cost_usd
production-key,ns_key_11223344-5566-7788-99aa-bbccddeeff00,8500000,8.500000,4200,2024,0.002024
staging-key,ns_key_aabbccdd-eeff-0011-2233-445566778899,1200000,1.200000,800,1500,0.001500

Headers: Content-Type: text/csv; charset=utf-8, Content-Disposition: attachment; filename="nullspend-attribution-api_key-2026-03-28.csv"

Errors

CodeHTTPWhen
validation_error400Missing groupBy, invalid period, limit out of range, invalid format
authentication_required401No valid session
forbidden403User lacks viewer role

Attribution Detail

GET /api/cost-events/attribution/:key

Retrieve daily spend trend and model breakdown for a single attribution group.

Authentication

Session (dashboard)

Parameters

NameInTypeRequiredDescription
keypathstringYesAPI key ID (ns_key_*) or tag value. Use (no key) for events without an API key.
groupByquerystringYesMust match the groupBy used in the list endpoint. "api_key" or a tag key name.
periodquerystringNo"7d", "30d", or "90d". Default "30d".
excludeEstimatedquerystringNo"true" or "false". Default "false".

Request

# API key detail (requires dashboard session)
curl "https://nullspend.dev/api/cost-events/attribution/ns_key_11223344-5566-7788-99aa-bbccddeeff00?groupBy=api_key" \
  -H "Cookie: session=..."

# Tag value detail
curl "https://nullspend.dev/api/cost-events/attribution/acme-corp?groupBy=customer_id" \
  -H "Cookie: session=..."

Response

200 OK:

{
  "data": {
    "key": "ns_key_11223344-5566-7788-99aa-bbccddeeff00",
    "totalCostMicrodollars": 8500000,
    "requestCount": 4200,
    "avgCostMicrodollars": 2024,
    "daily": [
      { "date": "2026-03-27", "cost": 285000, "count": 140 },
      { "date": "2026-03-26", "cost": 310000, "count": 155 }
    ],
    "models": [
      { "model": "gpt-4o", "cost": 7200000, "count": 3600 },
      { "model": "gpt-4o-mini", "cost": 1300000, "count": 600 }
    ]
  }
}

daily is sorted by date ascending. models is sorted by cost descending.

Errors

CodeHTTPWhen
invalid_key400Key contains / or .. (path traversal), or invalid ns_key_* format
validation_error400Missing groupBy or invalid period
authentication_required401No valid session
forbidden403User lacks viewer role

Tag Keys

GET /api/cost-events/tag-keys

Returns distinct non-internal tag key names from the last 7 days of cost events. Used to populate the Attribution page's groupBy dropdown.

Authentication

Session (dashboard)

Request

# Requires dashboard session
curl "https://nullspend.dev/api/cost-events/tag-keys" \
  -H "Cookie: session=..."

Response

200 OK:

{
  "data": ["customer_id", "environment", "feature", "team"]
}

Keys starting with _ns_ (internal tags) are excluded. Maximum 50 keys returned. Sorted alphabetically.

Errors

CodeHTTPWhen
authentication_required401No valid session
forbidden403User lacks viewer role

On this page