> ## Documentation Index
> Fetch the complete documentation index at: https://docs.baseten.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Billing events

> Meter Frontier Gateway usage from per-request billing events: fetch them on demand with the events API, or receive them as signed webhooks.

For each inference request through Frontier Gateway, Baseten records a billing event with token counts, the model slug, the calling group's external identifier, and the API key that made the request. Consume these events to meter usage in Stripe, Orb, or your own billing system without sitting in the request path.

There are two ways to get billing events:

* **Fetch them on demand** with [`GET /v1/gateway/events`](/reference/gateway/events/list-gateway-events). Nothing to provision: the endpoint works with your workspace API key and you poll on your schedule.
* **Receive them as signed webhooks** pushed to your endpoint as requests happen. Baseten configures webhook delivery per workspace during managed onboarding: your Baseten team provisions the target URL and webhook signing secret before your first event ships. Choose webhooks when you want near-real-time push delivery or need the request's `requestMetadata` passthrough, which fetched events don't include.

## Fetch events

Call the events API with a time window. `start_time` is required and inclusive, `end_time` defaults to the current time and is exclusive, and events come back in ascending timestamp order, cursor-paginated:

<CodeGroup>
  ```bash Request theme={"system"}
  curl --request GET \
    --url "https://api.baseten.co/v1/gateway/events?start_time=2026-08-01T00:00:00Z&end_time=2026-08-02T00:00:00Z" \
    --header "Authorization: Bearer $BASETEN_API_KEY"
  ```

  ```json Output theme={"system"}
  {
    "items": [
      {
        "type": "API_BILLING_USAGE",
        "idempotencyKey": "01J9X7Y0Z3K4M5N6P7Q8R9S0T1",
        "timestamp": "2026-08-01T12:34:56.789Z",
        "requestId": "5e4a8c1a-2b3c-4d5e-9f0a-1b2c3d4e5f6a",
        "modelSlug": "your-org/your-model",
        "externalEntityId": "cust_42",
        "apiKeyPrefix": "sky_abcdefgh",
        "tokens": {
          "inputTokens": 2500,
          "outputTokens": 384,
          "cachedInputTokens": 2000
        }
      }
    ],
    "pagination": {
      "has_more": false,
      "cursor": null
    }
  }
  ```
</CodeGroup>

Plan around two behaviors: events become queryable within about a minute of the request (Baseten holds back the most recent minute so out-of-order ingest can't skip events), and overlapping windows can return the same event twice, so deduplicate on `idempotencyKey`. For the full request, response, and error reference, see [List gateway events](/reference/gateway/events/list-gateway-events).

## Webhook payload

Baseten POSTs a JSON body to your configured webhook URL. Every payload uses the standard Baseten envelope, where `type` is the discriminator and `data` holds the event-specific fields. Frontier Gateway emits the `API_BILLING_USAGE` event type; future event types may share the same envelope.

The `data.events` array can contain one or more events per delivery. Each event corresponds to a single inference request. Payloads may carry fields beyond those documented here; ignore them, as they're internal and can change without notice.

```json theme={"system"}
{
  "type": "API_BILLING_USAGE",
  "data": {
    "events": [
      {
        "idempotencyKey": "01J9X7Y0Z3K4M5N6P7Q8R9S0T1",
        "timestamp": "2025-07-07T23:40:35.905Z",
        "requestId": "5e4a8c1a-2b3c-4d5e-9f0a-1b2c3d4e5f6a",
        "requestMetadata": {},
        "modelSlug": "your-org/your-model",
        "externalEntityId": "cust_42",
        "apiKeyPrefix": "sky_sCqhBwEy4kPd",
        "tokens": {
          "inputTokens": 100,
          "outputTokens": 200,
          "cachedInputTokens": 300
        }
      }
    ]
  }
}
```

Fields on each event:

<ParamField body="idempotencyKey" type="string" required>
  Stable identifier for the event. Use this to deduplicate on your side.
</ParamField>

<ParamField body="timestamp" type="string" required>
  ISO 8601 UTC timestamp of the inference request.
</ParamField>

<ParamField body="requestId" type="string" required>
  Per-request identifier, useful for correlating billing events with platform logs.
</ParamField>

<ParamField body="requestMetadata" type="object | null" required>
  Freeform JSON object passed through from the inference request. May be `null` when no metadata is supplied.
</ParamField>

<ParamField body="modelSlug" type="string" required>
  The model slug invoked, in `your-org/your-model` form.
</ParamField>

<ParamField body="externalEntityId" type="string" required>
  The `metadata.external_entity_id` you set on the group that owns the key used for the request, the same value you write when you [create or update the group](/frontier-gateway/api-keys#create-a-group).
</ParamField>

<ParamField body="apiKeyPrefix" type="string" required>
  Prefix of the federated API key that made the request (the substring before the `.` in the full key string). The group identifies your customer; the prefix identifies which of that customer's [keys](/frontier-gateway/api-keys) drove the usage.
</ParamField>

<ParamField body="tokens" type="object" required>
  Token counts for the request.

  * **inputTokens** (`integer`, required): Prompt tokens.
  * **outputTokens** (`integer`, required): Generated tokens.
  * **cachedInputTokens** (`integer`, required): Prompt tokens served from cache, when applicable.
</ParamField>

## Headers

Baseten sets two headers on every delivery:

* `X-Baseten-Signature`: HMAC signature of the raw request body. For more information, see [Verify the signature](#verify-the-signature).
* `X-Baseten-Request-ID`: UUID generated per outbound delivery. Log this on your receiver as a correlation ID for debugging against Baseten platform logs. Use `idempotencyKey`, not this header, to dedupe events on your side; the same `requestId` is reused across retry attempts of a single delivery.

## Verify the signature

The `X-Baseten-Signature` header has the format `v1=<hex>`, where `<hex>` is the HMAC-SHA256 of the raw request body computed with your workspace's webhook signing secret. Verify the signature on every request before trusting the payload.

Two requirements:

* Verify against the **raw bytes** of the request body, not a re-serialized version. JSON re-serialization changes whitespace and field order and breaks the HMAC.
* Use a constant-time comparison (`hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node.js) to avoid timing attacks.

<Tabs>
  <Tab title="Python">
    **To verify the signature**:

    ```python verify.py theme={"system"}
    import hmac
    import hashlib
    import os

    def verify_signature(request) -> bool:
        signing_secret = os.getenv("BASETEN_WEBHOOK_SIGNING_SECRET")
        signature = request.headers.get("X-Baseten-Signature")
        body = request.data

        mac = hmac.new(signing_secret.encode("utf-8"), body, hashlib.sha256)
        expected_signature = f"v1={mac.hexdigest()}"
        return hmac.compare_digest(expected_signature, signature)
    ```
  </Tab>

  <Tab title="Node.js">
    **To verify the signature**:

    ```javascript verify.js theme={"system"}
    import crypto from "node:crypto";

    export function verifySignature(rawBody, signatureHeader) {
      const signingSecret = process.env.BASETEN_WEBHOOK_SIGNING_SECRET;
      const mac = crypto.createHmac("sha256", signingSecret);
      mac.update(rawBody);
      const expected = `v1=${mac.digest("hex")}`;

      const expectedBuf = Buffer.from(expected);
      const actualBuf = Buffer.from(signatureHeader ?? "");
      if (expectedBuf.length !== actualBuf.length) {
        return false;
      }
      return crypto.timingSafeEqual(expectedBuf, actualBuf);
    }
    ```
  </Tab>
</Tabs>

Webhook signing secrets are a general Baseten primitive shared across products that emit signed webhooks. Baseten provisions your Frontier Gateway secret during onboarding. For rotation behavior, see [Secure webhooks](/inference/async#secure-webhooks).

## Webhook delivery semantics

Baseten retries failed deliveries with exponential backoff so a transient blip on your endpoint doesn't drop billing events. Use these numbers to size your endpoint SLOs and to know when a failure is terminal.

* **Per-attempt timeout**: 10 seconds. If your endpoint doesn't respond within this window, Baseten cancels the attempt and treats it as a failure.
* **Backoff**: Exponential, starting at 1 second between attempts and capping at 5 seconds.
* **Maximum elapsed time**: 15 seconds. After this, Baseten stops retrying and routes the event to a dead-letter queue. The retry window is tight: the realistic budget is one or two attempts.
* **4xx responses are terminal**: Any 4xx status from your endpoint stops retries immediately. Only 5xx responses, network errors, and timeouts trigger a retry.

Events that exhaust retries land in the dead-letter queue, and Baseten doesn't redeliver them automatically. Contact your Baseten team to recover events from the DLQ.

## Recommended webhook consumption pattern

Treat the webhook handler as an ingestion endpoint, not a billing pipeline. The handler's job is to durably accept the event and return as fast as possible:

1. Verify the signature.
2. Persist the event to your own queue or database, keyed on `idempotencyKey`.
3. Return a 2xx response.
4. Process and forward to your billing provider asynchronously.

This separates two failure modes: receiving the event from Baseten, and reconciling it with your billing provider. If your billing provider is slow or down, you don't drop events or block the gateway's retry timer.

<Warning>
  Acknowledge fast. If your handler runs billing logic inline and exceeds the 10-second per-attempt timeout, Baseten retries the delivery and you risk double-billing your customer. The total retry window is only 15 seconds, so a slow handler that survives the first timeout often misses the retry budget entirely and lands in the DLQ. Always return 2xx before doing slow work, and dedupe on `idempotencyKey` to handle the at-least-once delivery guarantee.
</Warning>

## Next steps

* **[List gateway events](/reference/gateway/events/list-gateway-events)**: Fetch billing events on demand instead of running a webhook receiver.
* **[Manage groups and API keys](/frontier-gateway/api-keys)**: Create groups, build a hierarchy, mint and revoke keys, and delete groups.
* **[Rate and usage limits](/frontier-gateway/rate-limits)**: Cap per-group, per-model token and request volume.
