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

# Settle usage

> Tell ZeroClick what a served request actually used: the zc-usage header for synchronous settlement, and asynchronous reports for work that finishes later.

An allowed request still has to settle: the payment finalizes against what the request actually used. Your revenue, credit burn, and analytics are computed from usage. The primary path is synchronous: stamp a `zc-usage` header on the successful response. ZeroClick records the usage as it returns the response to the agent. Reserve the [asynchronous report](#report-usage) for work that finishes after the response is gone.

## Settle on the response

`zc-usage` is a response header carrying a JSON array of what the request used:

```json theme={null}
[{ "serviceSlug": "product-watch", "meterSlug": "requests", "quantity": 1 }]
```

The SDK helpers build and validate it:

```ts TypeScript theme={null}
const response = Response.json({ watching: true });
return zeroClick.withUsage(response, [
  { serviceSlug: "product-watch", meterSlug: "requests", quantity: 1 },
]);
```

```python Python theme={null}
return to_fastapi(
    zeroclick.with_usage(
        ZcResponse.json({"watching": True}),
        [SyncUsageItem(service_slug="product-watch", meter_slug="requests", quantity=1)],
    )
)
```

```go Go theme={null}
// The Meter middleware attaches zc-usage for the quantities it declared,
// and only on a 2xx; nothing to write in the handler.
mux.Handle("/v1/product-watch", seller.Meter(sellers.PerRequest("requests", 1))(
	http.HandlerFunc(productWatch)))
```

Three rules govern the header:

* **2xx only.** A 4xx is the buyer's bad input and a 5xx is your failure; neither delivered anything, so neither bills. The Go middleware enforces this. In TypeScript and Python, call `withUsage` only on the success path.
* **ZeroClick strips it.** The agent never sees `zc-usage`: the proxy consumes it and removes it from the response.
* **Settle the actual quantity.** For a fixed `quantity`, that is what you declared. For a `maxQuantity` ceiling, it is anywhere from zero up to the ceiling. See [charge up to a maximum](/docs/integrate/charge-up-to-a-maximum.md). The platform accepts `0` (a delivered response that produced nothing settles the ceiling at \$0). The TypeScript and Python helpers validate quantities as at least 1, so settle a zero-output ceiling by omitting that item.

A paid, delivered response always settles, even if your header is missing or malformed: fixed items settle at the authorized quantity, ceilings settle at zero. That guarantee protects the buyer's payment from a garbage header. It does not excuse the report: an unreported ceiling settles as if nothing was produced. "Delivered" means a 2xx with a body, as the next section explains.

## Empty responses do not bill

A 2xx that carries nothing counts as a failed delivery. ZeroClick settles it exactly like a 4xx or 5xx, even if you attached a `zc-usage` header, and passes your response through to the agent unchanged. A response is empty when its body is:

* missing, or only whitespace
* JSON that is exactly `null`, `{}`, `[]`, or `""`

Everything else bills as usual, including:

* any other JSON, even `{"results": []}` or an object whose fields are all `null`
* plain text
* `204 No Content`, which declares a successful action with nothing to return
* a response to a `HEAD` request
* a compressed body (any `content-encoding` other than `identity`), which ZeroClick does not decode to inspect

What this means for your API:

* **An action with nothing to return** (send a message, start a job): respond `204`, or describe what happened, such as `{"sent": true, "id": "msg_123"}`. A bare `200 {}` does not bill.
* **A lookup that finds nothing**: a shaped answer like `{"results": []}` bills, because your API did the work. If the agent should not pay for a miss, return a 4xx such as `404` instead.
* **An error**: return the error status, not a `200` with an error body. A `200` with an error message in it is not empty, so it bills.

This applies to settlement on the response. An [asynchronous report](#report-usage) you send later bills as reported. What a failed delivery costs on each payment rail is covered in [payment protocols](/docs/concepts/payment-protocols.md).

## Report usage

Some work outlives the response: a job that keeps extracting after you return `202`, tokens streamed over a connection that has already closed, a worker that settles from a queue. Report that usage asynchronously with `POST /v1/usage`, through `reportUsage`, `report_usage`, or `ReportUsage`, using the `usage:write` key ([split keys](/docs/integrate/keys-and-secrets.md#split-read-and-write-keys) put it in the worker):

```ts TypeScript theme={null}
const result = await zeroClick.reportUsage({
  zcAgentId: "agt_x7f2kq93bh0d",
  idempotencyKey: "zcreq_8h2m4x0q9k1f_output_tokens", // derived, never random
  serviceSlug: "product-watch",
  meterSlug: "output_tokens",
  quantity: 4200,
});
console.log(result.recorded, result.duplicate);
```

```python Python theme={null}
result = await zeroclick.report_usage(
    zc_agent_id="agt_x7f2kq93bh0d",
    idempotency_key="zcreq_8h2m4x0q9k1f_output_tokens",  # derived, never random
    service_slug="product-watch",
    meter_slug="output_tokens",
    quantity=4200,
)
```

```go Go theme={null}
_, err := seller.ReportUsage(sellers.BackgroundContext(r), sellers.ReportUsageInput{
	AgentID:        zc.AgentID,
	ServiceSlug:    "product-watch",
	MeterSlug:      "output_tokens",
	Quantity:       4200,
	IdempotencyKey: zc.RequestID + "_output_tokens", // derived, never random
})
if err != nil {
	// Delivered but unbilled. Log it; do not fail a request the buyer already has.
	log.Printf("usage not reported: %v", err)
}
```

> [!WARNING]
>
> In Go, report with `sellers.BackgroundContext(r)`, not `r.Context()`. Go cancels the request context the moment your handler returns, so a report on it is silently dropped. The work goes unbilled, with nothing in the logs.

**Derive the idempotency key.** Reports are idempotent per service on `idempotencyKey`, and the key is yours to construct: the SDKs never generate one. Derive it from stable facts, such as the request id plus the meter (`zcreq_8h2m4x0q9k1f_output_tokens`). A random key turns every retry into a double bill. `duplicate: true` means the key already landed and the stored event was replayed: a success, not an error. There are no automatic retries. Retry yourself with the same key.

On the wire, the call and its response:

```http Request theme={null}
POST /v1/usage HTTP/1.1
Host: api.zeroclick.io
Authorization: Bearer zc_…
Content-Type: application/json

{
  "zcAgentId": "agt_x7f2kq93bh0d",
  "idempotencyKey": "zcreq_8h2m4x0q9k1f_output_tokens",
  "serviceSlug": "product-watch",
  "meterSlug": "output_tokens",
  "quantity": 4200,
  "occurredAt": "2026-07-28T17:04:05.000Z"
}
```

```json Response theme={null}
{
  "recorded": true,
  "duplicate": false,
  "usageEvent": {
    "id": "use_2j8fq0w7xk4m",
    "zcAgentId": "agt_x7f2kq93bh0d",
    "zcRequestId": null,
    "idempotencyKey": "zcreq_8h2m4x0q9k1f_output_tokens",
    "meterSlug": "output_tokens",
    "quantity": 4200,
    "totalCostUsd": "0.042000",
    "source": "async",
    "occurredAt": "2026-07-28T17:04:05.000Z"
  }
}
```

`quantity` is an integer of at least 1 (maximum 2,147,483,647); `occurredAt` is an optional ISO timestamp for when the usage happened. A report can fail with `402` (`access_inactive`, `plan_expired`, `usage_exhausted`), `409` (`meter_not_priced`), or otherwise `404` for a missing resource. The SDKs surface these as typed errors carrying the status and reason. See [errors](/docs/resources/errors.md). A failed report after delivery means work went unbilled. Log it with the reason rather than failing a request the buyer already received.

## Choose a path

* **Settle on the response** when you know the quantity before you respond: most requests, including ceilings whose actual you know by the end of the handler.
* **Report asynchronously** when the work finishes after the response, when settlement happens in a separate worker, or when there is no HTTP response to stamp (a connection upgraded away from HTTP, a queue consumer).
* **Combine them** for work you can't size up front: a fixed part settled on the response, the variable part reported when you know it. That is the pattern in [charge up to a maximum](/docs/integrate/charge-up-to-a-maximum.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.