Ingest API reference and Python client
Xero and QuickBooks Online only report usage for your app as a whole. To see which of your tenants drives the bill, you send Tierguard one small event per outbound API call, from the code that makes the call. This page documents that endpoint and the Python client that wraps it.
Overview
There is one endpoint. Base URL: https://tierguard.dev.
POST https://tierguard.dev/v1/ingest
Authorization: Bearer <ingest token>
Content-Type: application/json
The body is a JSON array of events, or a single event object. Each event says which tenant a call was for, which endpoint was called and how much it cost in bytes and calls.
Authentication
Send an ingest token as a bearer token: Authorization: Bearer <token>. A token belongs to one project, and every event you send with it is recorded under that project.
- Create a token on the project's setup page after you sign up. The token is shown once, when it is created; store it then.
- A project can have up to 5 active tokens, so you can rotate without downtime. Revoking a token stops it working immediately.
- A missing, malformed, unknown or revoked token gets
401.
Request
Each event has these fields. Unknown fields are rejected.
| Field | Type | Required | Description |
|---|---|---|---|
tenant_ref | string, max 256 chars | yes | Your identifier for the tenant (connected Xero organisation or QuickBooks company) the call was made for. |
endpoint | string, max 256 chars | yes | The upstream API endpoint that was called, for example /Invoices. |
bytes_egress | integer, 0 to 1015 | one of the two metrics | Response size in bytes. |
call_count | integer, 0 to 1015 | one of the two metrics | Number of API calls this event represents, usually 1. |
occurred_at | ISO 8601 date-time | yes | When the call happened. A timestamp with an offset is converted to UTC; a timestamp without an offset is stored as given and treated as UTC. |
event_id | string, max 256 chars | yes | Idempotency key, unique per project. See Idempotency. |
At least one of bytes_egress and call_count must be present. A missing metric is stored as 0.
Response
A successful request returns 200 with the number of newly stored events and the number of events that were already known:
{"accepted": 2, "duplicates": 0}
Errors
Errors are JSON with a detail field.
| Status | Meaning | Body | Retry? |
|---|---|---|---|
401 | Missing or malformed Authorization header, unknown token or revoked token. | {"detail": "Invalid or revoked token"} or {"detail": "Missing or invalid Authorization header"} | No. Fix the token. |
422 | The body failed validation: a missing or unknown field, a value out of range, neither metric present, or more than 1000 events. | For too many events: {"detail": "Batch size exceeds maximum of 1000"}. For field errors, detail is a list of validation errors, each with loc and msg. | No. The whole request is rejected and nothing is stored. |
5xx | Server-side failure. | Not specified. | Yes, with backoff. Re-sending is safe. |
Body validation runs before the token is checked, so a malformed body gets 422 even when the token is missing or wrong.
Idempotency
event_id is unique per project. If you send an event whose event_id was already stored for the project, it is not stored again and is counted in duplicates instead of accepted. Use a stable id per logical API call, such as the upstream request id, and a retry after a timeout or a 5xx cannot double-count.
Events in a batch are stored one at a time. If the token is revoked or its project deleted while a batch is being stored, the events stored before that point stay stored and the request answers 401; sending the same batch again with a valid token counts them in duplicates.
Limits
- At most 1000 events per request.
tenant_ref,endpointandevent_id: at most 256 characters each.bytes_egressandcall_count: integers from 0 to 1,000,000,000,000,000.
curl examples
One event:
curl -X POST https://tierguard.dev/v1/ingest \
-H "Authorization: Bearer $TIERGUARD_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tenant_ref": "org-123", "endpoint": "/Invoices", "bytes_egress": 48211, "call_count": 1, "occurred_at": "2026-10-06T09:30:00Z", "event_id": "req-0001"}'
A batch:
curl -X POST https://tierguard.dev/v1/ingest \
-H "Authorization: Bearer $TIERGUARD_TOKEN" \
-H "Content-Type: application/json" \
-d '[
{"tenant_ref": "org-123", "endpoint": "/Invoices", "bytes_egress": 48211, "occurred_at": "2026-10-06T09:30:00Z", "event_id": "req-0001"},
{"tenant_ref": "org-456", "endpoint": "/Contacts", "call_count": 3, "occurred_at": "2026-10-06T09:30:02Z", "event_id": "req-0002"}
]'
Python client
You can call the endpoint directly, or use tierguard_client.py, a single file that buffers events and sends them in the background. It needs Python 3.9 or later and httpx, nothing else.
Verify the download with shasum -a 256 tierguard_client.py; the output must match the SHA-256 above.
Create one client per process, at module level, and reuse it:
from tierguard_client import TierguardClient
tg = TierguardClient("https://tierguard.dev", "YOUR_INGEST_TOKEN")
resp = xero.get(f"/connections/{org_id}/Invoices")
tg.record(tenant_ref=org_id, endpoint="/Invoices",
bytes_egress=len(resp.content), call_count=1)
record() returns the event's event_id. If you do not pass one, a random id is generated; pass your own to make retries idempotent. record_response(tenant_ref, response) does the same from an httpx.Response, taking the endpoint path and body size from it.
Constructor options
| Option | Default | Meaning |
|---|---|---|
base_url, token | required | Tierguard origin and your ingest token. |
batch_size | 100 | Send as soon as this many events are waiting (capped at 1000). |
flush_interval | 5.0 | Seconds between background sends of whatever is buffered. |
timeout | 5.0 | HTTP timeout per request, in seconds. |
max_buffer | 10000 | Events kept in memory. Beyond this the oldest are dropped. |
backoff_base, backoff_max | 1.0, 300.0 | Seconds of the first and the longest pause after a failed send. The pause doubles on each consecutive failure and is jittered. |
shutdown_timeout | 5.0 | Longest wait, in seconds, for the final flush in close() and at interpreter exit. |
Read-only properties: pending (events waiting to be sent) and dropped (events discarded because the buffer overflowed or the server rejected them).
What the client guarantees
record()never does network I/O, never blocks on the network and never raises. An invalid event is logged and dropped.- A background thread sends batches of up to 1000 events, so a slow or unreachable Tierguard does not slow your Xero or QuickBooks calls.
- The buffer is bounded by
max_buffer. On overflow the oldest events are dropped and one warning is logged. - On a network error,
408,429or5xxthe batch is kept and retried after an exponential backoff. On another4xx(for example401for a revoked token) the batch is dropped, counted indropped, and the client backs off as well. - Remaining events are flushed on
close()and when the interpreter exits. A client created before afork(gunicorn, uvicorn workers) starts clean in the child process. flush()sends synchronously and returns the number of events the server accepted.
Serverless: the process can be frozen as soon as your handler returns, before the background thread runs. Call tg.flush() before returning.