# Response headers

> Every billed SerpKite response tells you what it cost, your remaining balance, whether it came from cache, how long it took and roughly how many LLM tokens it contains.

## The headers

POST /v1/search · 200 OK:

```http
HTTP/2 200
content-type: application/json
x-request-id: req_01J8ZK4M6Q2V7
x-credits-used: 1
x-credits-remaining: 61499
x-cost-usd: 0.0008
x-cache: MISS
x-latency-ms: 942
x-tokens-estimate: 1184
```

| Header | Meaning |
| --- | --- |
| `X-Request-Id` | Unique ID for this request. Quote it to support. |
| `X-Credits-Used` | Credits charged for this request (0 if not billed). |
| `X-Credits-Remaining` | Your balance after this request. |
| `X-Cost-USD` | Effective cost of this request at your average pack price. |
| `X-Cache` | HIT if served from cache (max_age), otherwise MISS. |
| `X-Latency-Ms` | Server-side time to produce the response. |
| `X-Tokens-Estimate` | Approximate LLM tokens in the response body. |

Three more headers appear in specific cases:

| Header | When | Meaning |
| --- | --- | --- |
| `Retry-After` | `429` responses | Seconds to wait before retrying. See [Rate limits](https://serpkite.com/docs/rate-limits). |
| `Content-Type` | Always | `application/json`, or `text/markdown; charset=utf-8` with `format: "markdown"`. |

Values are plain numbers. Credits are decimals (`0.5`, `61499.5`), `X-Cost-USD` is a decimal dollar amount, `X-Latency-Ms` and `X-Tokens-Estimate` are integers.

## What each one is for

### Cost and balance

`X-Credits-Used` is the final cost of this call after any refund, so it is `0` for errors, empty results and failed page fetches. `X-Credits-Remaining` is your account balance right after the call. Together they let you track spend without ever calling [`/v1/account`](https://serpkite.com/docs/endpoints/account).

`X-Cost-USD` converts the credits used to dollars at the per-credit price of the largest pack you have bought. On a free account it is `0`. It is a convenience for per-request cost attribution (for example, per customer or per agent run), not an invoice.

### Cache

`X-Cache: HIT` means the result came from the cache because you sent `max_age` and a fresh-enough copy existed; the call cost half price. `MISS` means a live fetch. The body's `meta.cached` and `meta.cached_at` say the same thing. See [Caching](https://serpkite.com/docs/caching).

### Latency

`X-Latency-Ms` is the time SerpKite spent producing the response, from receiving the request to writing the body. It excludes network time between you and us.

### Token estimate

`X-Tokens-Estimate` is the approximate size of the response body in LLM tokens, computed as the number of characters divided by four. It's a quick way to compare `format: "json"`, `"compact"` and `"markdown"`, or to decide whether a result fits in a context window, without running a tokenizer. Real counts vary by model and language. See [Output formats](https://serpkite.com/docs/output-formats).

### Request ID

`X-Request-Id` identifies this call. It is also in the body as `meta.request_id` (or `error.request_id` on errors) and in the dashboard's request log. Include it in bug reports.

## Reading headers in code

The official SDKs parse the body for you; `res.meta.credits_used` (`res.Meta.CreditsUsed` in Go) carries the cost and `meta.request_id` the request ID. To read the raw headers, use any HTTP client:

cURL:

```bash
curl -sS -D - -o /dev/null https://api.serpkite.com/v1/search \
  -H "Authorization: Bearer $SERPKITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q":"best espresso machine 2026"}' | grep -i '^x-'
```

Python:

```python
import os
import httpx

res = httpx.post(
    "https://api.serpkite.com/v1/search",
    headers={"Authorization": f"Bearer {os.environ['SERPKITE_API_KEY']}"},
    json={"q": "best espresso machine 2026"},
    timeout=30,
)
used = float(res.headers["X-Credits-Used"])
left = float(res.headers["X-Credits-Remaining"])
print(f"{used} credits, {left} left, cache {res.headers['X-Cache']}, ~{res.headers['X-Tokens-Estimate']} tokens")
```

Node.js:

```javascript
const res = await fetch("https://api.serpkite.com/v1/search", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SERPKITE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ q: "best espresso machine 2026" }),
});
const h = res.headers;
console.log({
  used: Number(h.get("X-Credits-Used")),
  remaining: Number(h.get("X-Credits-Remaining")),
  costUsd: Number(h.get("X-Cost-USD")),
  cache: h.get("X-Cache"),
  tokens: Number(h.get("X-Tokens-Estimate")),
  requestId: h.get("X-Request-Id"),
});
```

Go:

```go
used, _ := strconv.ParseFloat(res.Header.Get("X-Credits-Used"), 64)
left, _ := strconv.ParseFloat(res.Header.Get("X-Credits-Remaining"), 64)
fmt.Printf("%v credits, %v left, request %s\n", used, left, res.Header.Get("X-Request-Id"))
```

## Browsers and CORS

The API sends `Access-Control-Allow-Origin: *` (without credentials) and exposes all of the headers above, including `Retry-After`, through `Access-Control-Expose-Headers`. Browser code can read them with `response.headers.get()`. Keep in mind that a key used from a browser is visible to anyone using that page; see [Authentication](https://serpkite.com/docs/authentication#keep-keys-secret).

## Body meta

The JSON body repeats the essentials in `meta`, which is handy when you only store the body, and adds parse information that has no header:

| Field | Type | Meaning |
| --- | --- | --- |
| `request_id` | string | Same as `X-Request-Id`. |
| `credits_used` | number | Same as `X-Credits-Used`. |
| `engine` | string | The provider that answered, e.g. `google` or `brave`. See [Search providers](https://serpkite.com/docs/providers). |
| `route` | array | Provider attempts behind a fresh result, in order: `{provider, outcome, ms}`. Absent on cache hits. |
| `cached` | boolean | `true` on a cache hit. |
| `cached_at` | string or null | When the cached copy was fetched (ISO 8601). |
| `resolved_urls` | boolean | `true` when every result link is the resolved destination, never a Google redirect. |
| `parse_quality` | string | `ok`, `partial` (some blocks couldn't be parsed) or `empty` (no results; not billed). |
| `latency_ms` | integer | Same as `X-Latency-Ms`. |

With `format: "markdown"` the body is plain Markdown and has no `meta`; rely on the headers there. With `fields`, `meta` and `request` are always kept.

## Related

- [Credits and billing](https://serpkite.com/docs/credits-and-billing): What each call costs and what is refunded.
- [Output formats](https://serpkite.com/docs/output-formats): Use X-Tokens-Estimate to pick a format.
- [Caching](https://serpkite.com/docs/caching): max_age, X-Cache and half-price hits.
- [Errors](https://serpkite.com/docs/errors): Error bodies carry request_id too.