Observability
Every Ringside call leaves a trail you can follow. A correlation id on the response. A structured row in the request log. A set of metering headers that report exactly what was charged and which model served the call. Rollup endpoints that aggregate it all by day, model, customer, tag or property. This page shows how to wire those signals into your own logging, dashboards and alerting so a production incident is a lookup, not a guess.
Why this matters
You are running someone else's inference through your account. When a customer reports a slow response, a wrong answer or a surprise bill, you need to know what actually happened on that one call. Ringside answers that at four levels. The response itself carries a request id and a set of metering headers. The request log keeps a per-call row you can pull back by id. The usage and margin endpoints aggregate those rows into cost and revenue rollups. And webhooks push the events you cannot afford to poll for.
Wire all four. The response headers give you live correlation, the log gives you forensic depth, the rollups give you trends and the webhooks give you alerts. Skip any one of them and you end up reconstructing state you could have captured for free.
one call
|
|--> response headers X-Request-Id, X-Ringside-Model-Used, X-Ringside-Wallet-Deducted ...
|--> request log row GET /v1/requests/{id} (full request + response preview, tokens, cost, status)
|--> usage rollup GET /v1/usage?group_by=... (cost + tokens by day/model/customer/tag/property)
|--> margin rollup GET /v1/customers/{id}/margin (billable - cost)
'--> webhook event request.failed_after_retry, wallet.low, customer.budget_exceeded, batch.* ...Capture the request id on every call
The correlation id is the X-Request-Id response header. It is never a body field. Log it on success and on failure, store it next to your own trace id and put it in any support ticket. With it you can pull the exact log row back later, and you can quote it to us if you need to.
If you send an X-Request-Id request header matching ^[a-zA-Z0-9_-]{1,64}$, Ringside echoes it back instead of minting one. That lets you carry a distributed-tracing id straight through the call. Anything outside that pattern is ignored and a fresh req_<16hex> id is generated.
curl -sS -D - -o /dev/null https://api.fightclub.pro/v1/chat/completions \
-H "Authorization: Bearer ko_<key>" \
-H "Content-Type: application/json" \
-d '{"model":"fc:openai/gpt-4o-mini","messages":[{"role":"user","content":"ping"}]}' \
| grep -i '^x-request-id:'
# x-request-id: req_9f3c1a77b2e40af1The error body is { "error": { "type", "code", "message", "param"?, "retry_after_seconds"? } }. type, code and message are always present. Drive your handling off code (a stable machine string), never off message (human-facing, may change). param appears on field-validation errors; retry_after_seconds appears on rate-limit errors.
The request log
Every call writes one row to the request log. Pull a single row by id with GET /v1/requests/{id}, or page the list with GET /v1/requests. The single-record fetch returns full request and response previews; the list view truncates both previews to 200 characters to keep the payload small. Cross-tenant access returns 404, not 403, so ids stay non-enumerable.
In the dashboard the same data is at ringside.fightclub.pro/app/logs, with a per-request detail view at /app/logs/{request_id}. Paste a req_ id from a customer report straight into that path.
Fields on a log row
| Field | Meaning |
|---|---|
id | The req_ id, same value as the X-Request-Id header. |
endpoint | Which endpoint served it, e.g. chat_completions, conversation_message. |
status | Terminal status of the call. ok on success; non-ok for blocked or errored calls. |
error_type / error_message | The error type and human message when status is not ok; both null on success. |
model_requested / model_resolved / provider_used | The ref you sent, the concrete model it resolved to and the upstream provider that actually served it. |
input_tokens / output_tokens / cached_input_tokens / reasoning_tokens | Token accounting, including prompt-cache reads and reasoning tokens where the model reports them. |
cost_raw_usd / cost_charged_usd / billable_amount_usd | Raw provider cost, what your developer wallet was charged and the resale amount you set (null if unset). |
customer_id / conversation_id / message_id / session_id | Attribution links. session_id is your FC-Session-Id; customer_id is the attributed cus_. |
properties / tags | The FC-Property-* map and FC-Tag list you attached, used for slicing. |
idempotency_key | The idempotency-key you sent on the call, if any. |
duration_ms / retry_count | Wall-clock duration and how many upstream retries the call took before settling. |
request_preview / response_preview | Truncated to 200 chars on the list, full on the single-record GET. |
created_at | ISO 8601 timestamp. |
Filters on the list
GET /v1/requests accepts customer_id, conversation_id, session_id, endpoint, status, model (matches either requested or resolved), tag, property_key + property_value (both required together) and a from / to date window. Paginate with limit (default 20, max 100) and after (pass the previous next_cursor). The list returns { "data": [...], "has_more": boolean, "next_cursor": string | null }. There is no object: "list" field.
curl -sS "https://api.fightclub.pro/v1/requests?customer_id=cus_42&session_id=checkout-7f2&status=error&from=2026-06-23T00:00:00Z&limit=50" \
-H "Authorization: Bearer ko_<key>"
# {
# "data": [ { "id": "req_9f3c1a77b2e40af1", "status": "error",
# "error_type": "wallet_error", "model_resolved": "gpt-4o-mini",
# "duration_ms": 812, "retry_count": 2, ... } ],
# "has_more": false,
# "next_cursor": null
# }Send FC-Session-Id to group a multi-call workflow, FC-Property-<name> for arbitrary key/value dimensions (lowercased, 256 chars per value, 1 KB total across all properties) and FC-Tag (repeatable, max 20 per call) for flat labels. They land on the log row and become the group_by keys on the usage rollup. Decide your slicing scheme before you go live; you cannot backfill these onto calls already made.
Metering headers
On a chat completion the response carries the billing and routing outcome inline, so you can record it without a second lookup. Two headers report money. X-Ringside-Wallet-Balance is the wallet balance after the call and X-Ringside-Wallet-Deducted is the exact charge, both to 7 decimal places. When the call is attributed to a customer wallet, these report that wallet; otherwise they report your developer wallet.
| Header | Reports |
|---|---|
X-Request-Id | Correlation id (req_), present on every response. |
X-Ringside-Wallet-Balance | Balance after this call, 7 dp. |
X-Ringside-Wallet-Deducted | Exact amount charged for this call, 7 dp. |
X-Ringside-Model-Resolved | The concrete model a match: / slot: / dyn: ref resolved to. |
X-Ringside-Model-Used | The provider/model that actually served the response (set on a cascade). |
X-Ringside-Models-Tried | Comma-separated chain attempted, in order (on a cascade). |
X-Ringside-Fallback-Triggered | true if a fallback past the first model served the call, else false. |
X-Ringside-Model-Newer-Available | Set when a newer model than the resolved one exists, so you can plan a pin bump. |
X-Ringside-Dynamic-Profile / X-Ringside-Dynamic-Reason | The chosen profile and routing reason for a dyn: ref. |
X-RateLimit-Remaining | Requests left in the current window (the tightest of the four enforcement layers). |
FC-Cache-Status / FC-Cache-Key / FC-Cache-Age | hit, miss or bypass; the first 8 chars of the cache key; and, on a hit, the cached entry's age in seconds. |
There is no X-RateLimit-Limit and no X-RateLimit-Reset. On a 429, read Retry-After (integer seconds) instead. Rate limiting has four layers and the lowest wins: edge per-IP, per-client-token, per-developer and per-customer. A 429 from any layer looks the same on the wire.
The customer-wallet debit is fire-and-forget so it never blocks your response. On a customer-attributed call the X-Ringside-Wallet-* headers reflect that debit only if it settled before the response flushed. For the authoritative number, read cost_charged_usd / billable_amount_usd off the log row or the usage rollup, which are written transactionally.
Usage and margin rollups
For trends, not single calls, aggregate the log. GET /v1/usage rolls up cost and tokens over a from / to window (default last 30 days). Set group_by to day, model, customer, tag or property. With group_by=property you must also pass property_key. Add customer_id to scope to one customer, or tag to filter to a single tag before aggregating.
The response carries a total block (cost, input and output tokens, cached and reasoning tokens, request count, message count, conversation count) plus a buckets array, each bucket a { key, llm_cost_usd, input_tokens, output_tokens, request_count }. An invalid group_by returns 400 invalid_group_by; a missing property_key returns 400 missing_property_key; an unparseable date returns 400 invalid_date with the offending param.
curl -sS "https://api.fightclub.pro/v1/usage?group_by=model&from=2026-06-01T00:00:00Z" \
-H "Authorization: Bearer ko_<key>"
# {
# "total": { "llm_cost_usd": 412.88, "input_tokens": 19284113,
# "output_tokens": 3120554, "request_count": 88241, ... },
# "buckets": [
# { "key": "gpt-4o-mini", "llm_cost_usd": 201.40, "request_count": 61002, ... },
# { "key": "claude-sonnet-4", "llm_cost_usd": 187.10, "request_count": 24109, ... }
# ],
# "from": "2026-06-01T00:00:00.000Z", "to": "...", "group_by": "model"
# }Margin is revenue minus cost, and it lives on the customer. GET /v1/customers/{id}/margin returns cost_charged_usd, billable_amount_usd, margin_usd and margin_pct (null when revenue is zero, to avoid a noisy 0/0) per bucket and in total. group_by is day, customer or model. Only status=ok calls contribute; blocked and errored calls carry neither revenue nor cost. An unknown customer id returns 404, same as everywhere else.
curl -sS "https://api.fightclub.pro/v1/customers/cus_42/margin?group_by=day&from=2026-06-01T00:00:00Z" \
-H "Authorization: Bearer ko_<key>"
# {
# "customer_id": "cus_42",
# "total": { "cost_charged_usd": 18.40, "billable_amount_usd": 31.00,
# "margin_usd": 12.60, "margin_pct": 40.64, "request_count": 4120 },
# "buckets": [ { "key": "2026-06-23", "margin_usd": 1.92, "margin_pct": 41.2, ... } ],
# "group_by": "day"
# }Operational webhooks
Some conditions you cannot poll for fast enough. Register an HTTPS endpoint with POST /v1/webhooks and subscribe to the operational slice of the taxonomy. The events below are the ones an on-call rotation actually wants to wake up for.
- wallet.low / wallet.empty
- Your developer wallet is running down or out. Top up before calls start failing with 402
wallet_empty. - customer.budget_exceeded
- An attributed customer crossed its
monthly_budget_usd; further calls for that customer return 402customer_budget_exceeded. - customer.rate_limit_exceeded
- A customer hit its
rpm_limit/tpm_limit. Pairs with the 429s you will see on that customer's traffic. - request.failed_after_retry
- A call exhausted its retry budget and gave up. This is your upstream-degradation signal.
- batch.created / batch.completed / batch.failed / batch.cancelled
- Async batch lifecycle, so you act on completion instead of polling
GET /v1/batches/{id}on a timer. - moderation.flagged
- Inline moderation flagged content on a chat call that used the
moderationparam.
Delivery carries X-FC-Signature: t=<unix>,v1=<hex>, where v1 is the HMAC-SHA256 of the raw request body keyed by the endpoint secret (whsec_, returned once on create and on rotate_secret). Verify constant-time, reject stale timestamps and, during a secret rotation, accept either of the two v1= digests in the header. The same evt_ id rides every retry and every parallel fan-out, so dedupe on it. A sustained delivery-failure streak auto-disables the endpoint (status: disabled_after_failures); watch for that, because a dead endpoint means missed alerts.
GET /v1/webhooks/{id}/deliveries lists attempts and POST /v1/webhooks/{id}/deliveries/{delivery_id}/retry replays one. POST /v1/webhooks/{id}/test fires a webhook.test event so you can confirm your verifier and routing without waiting for a real event.
A logging checklist
- 01Record
X-Request-Idon every response, success and failure, next to your own trace id. - 02On error, log
error.typeanderror.codefrom the body, and branch oncode, nevermessage. - 03Record
X-Ringside-Model-UsedandX-Ringside-Fallback-Triggeredso a silent fallback to a pricier model is visible. - 04Treat the per-call
X-Ringside-Wallet-*headers as live signal, but reconcile spend againstcost_charged_usdon the log row or usage rollup, which are written transactionally. - 05Attach
FC-Session-Id,FC-TagandFC-Property-*at call time so usage rolls up by the dimensions you care about; you cannot add them after the fact. - 06Subscribe to
wallet.low,customer.budget_exceededandrequest.failed_after_retry, verify the signature constant-time and alert ondisabled_after_failures. - 07Run a daily
GET /v1/usage?group_by=modeland per-customer margin pull to catch cost drift before the invoice does.