Throttle Receiving API
Stream usage snapshots from the browser extension into a private, append-only archive you control. All endpoints live under https://your-throttle.app/api/public. Every endpoint accepts CORS preflight, so the extension can call them directly from a content/background script without a proxy.
1. Pair the extension
The user generates a pairing code in the dashboard. The extension exchanges that single-use code for a long-lived API key. There are three ways to pick up the code:
- Manual paste — user copies the code from the dashboard into the extension popup.
- Deep link — user clicks
https://your-throttle.app/pair#code=…(also accepts?code=). The page broadcasts the code viawindow.postMessageon the same origin. - DOM scrape — the same page exposes the code on a
[data-throttle-pair-code]element.
Redeem the code
POST https://your-throttle.app/api/public/pair
content-type: application/json
{
"code": "PASTE_FROM_DASHBOARD",
"label": "Chrome desktop" // optional, ≤ 60 chars, shown in the dashboard
}
→ 200
{
"ok": true,
"api_key": "tk_live_…", // FULL key — store securely, only returned once
"prefix": "tk_live_…", // first 12 chars, safe to display
"user_id": "uuid"
}Codes expire after 15 minutes and are single-use. Failure modes: 404 invalid_code, 410 expired, 410 already_redeemed.
Content script: auto-capture from the deep link
// content_script.js — runs on https://your-throttle.app/pair*
window.addEventListener("message", (e) => {
if (e.origin !== "https://your-throttle.app") return;
if (e.data?.type !== "throttle:pair") return;
chrome.runtime.sendMessage({ type: "PAIR", code: e.data.code });
});
// fallback: read DOM if the message arrived before the listener attached
const el = document.querySelector("[data-throttle-pair-code]");
if (el) chrome.runtime.sendMessage({ type: "PAIR", code: el.dataset.throttlePairCode });Background service worker: redeem and persist
// background.js
chrome.runtime.onMessage.addListener(async (msg) => {
if (msg.type !== "PAIR") return;
const res = await fetch("https://your-throttle.app/api/public/pair", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ code: msg.code, label: "Chrome desktop" }),
});
const json = await res.json();
if (!json.ok) throw new Error(json.error);
await chrome.storage.local.set({
throttle_api_key: json.api_key,
throttle_user_id: json.user_id,
});
});Required manifest.json entries: "host_permissions": ["https://your-throttle.app/*"], "permissions": ["storage"], and a content script matching "https://your-throttle.app/pair*".
2. Authentication
Every endpoint except /pair requires a bearer token:
Authorization: Bearer tk_live_…
Keys are hashed at rest (SHA-256) and scoped to the issuing user. Revoking a key from the dashboard invalidates it immediately. Use GET /api/public/whoami to confirm the key still works after pairing.
3. Upload snapshots
Snapshots are append-only points-in-time. Identify the upstream account with provider + provider_id; Throttle upserts the account on first upload. Re-uploads with the same (account_id, t) are silently deduped.
Request schema
POST https://your-throttle.app/api/public/snapshots
authorization: Bearer tk_live_…
content-type: application/json
{
"provider": "claude" | "lovable" | "other", // required
"provider_id": "user@example.com", // required, ≤ 255 chars, stable per upstream account
"label": "Personal", // optional display name, ≤ 120 chars
"plan": "pro", // optional, ≤ 60 chars
"snapshots": [
{
"t": 1738305600000, // ms since epoch, required, unique per account
"data": { /* arbitrary JSON object */ } // optional, anything serialisable
}
]
}
→ 200
{
"ok": true,
"accepted": 2, // newly inserted rows
"duplicates": 0, // rows skipped because (account_id, t) already existed
"account_id": "uuid" // upserted account id — cache it client-side to skip re-resolution
}Constraints
- 1 – 2,000 snapshots per request.
- Body capped at 1 MB (
413 payload_too_largeotherwise). - Each
dataobject is stored verbatim as JSONB. provider_idis the unique key per user — pick a stable identifier (email, account UUID).
Extension example
async function upload(snapshots) {
const { throttle_api_key } = await chrome.storage.local.get("throttle_api_key");
const res = await fetch("https://your-throttle.app/api/public/snapshots", {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${throttle_api_key}`,
},
body: JSON.stringify({
provider: "claude",
provider_id: "user@example.com",
snapshots, // [{ t: Date.now(), data: {...} }, ...]
}),
});
const json = await res.json();
if (!json.ok) throw new Error(`${res.status} ${json.error}`);
return json;
}Recommended client behaviour: batch every N seconds, retry with exponential backoff on 5xx and network errors, drop the batch on 400, and prompt the user to re-pair on 401.
4. Read snapshots back
GET https://your-throttle.app/api/public/snapshots?provider=claude&since=1738305600000&limit=500
authorization: Bearer tk_live_…
→ 200
{
"ok": true,
"snapshots": [
{ "id": 42, "account_id": "uuid", "t": 1738305660000, "data": {…}, "uploaded_at": "ISO" }
],
"next_cursor": "1738305660000" // pass as ?cursor=… for the next page; null when exhausted
}Query params (all optional): account_id, provider, provider_id, since (ms), until (ms), cursor (ms, exclusive), limit (1–1000, default 100). Results are sorted by t descending.
5. List accounts
GET https://your-throttle.app/api/public/accounts
authorization: Bearer tk_live_…
→ 200
{
"ok": true,
"accounts": [
{
"id": "uuid",
"provider": "claude",
"provider_id": "user@example.com",
"label": "Personal",
"plan": "pro",
"retention_days": null,
"created_at": "ISO",
"snapshot_count": 1234,
"latest_t": 1738305660000
}
]
}6. Export everything (NDJSON)
GET https://your-throttle.app/api/public/snapshots/export?account_id=uuid # account_id optional
authorization: Bearer tk_live_…
→ 200 application/x-ndjson
one snapshot per line:
{"id":42,"account_id":"uuid","t":1738305660000,"data":{…},"uploaded_at":"ISO"}
…Streamed in pages of 1,000 newest-first. Suitable for piping straight into jq or DuckDB.
7. Whoami
Cheap auth probe — call after pairing or when a saved key may have been revoked.
GET https://your-throttle.app/api/public/whoami
authorization: Bearer tk_live_…
→ 200 { "ok": true, "user_id": "uuid", "key_id": "uuid" }8. Errors
All endpoints return a uniform envelope. ok: true on success, ok: false + error code on failure. Treat unknown error codes as fatal.
{ "ok": false, "error": "unauthorized", "detail": "optional human-readable string" }| Status | error | When |
|---|---|---|
| 400 | invalid_body | Body failed schema validation |
| 401 | unauthorized | Missing/invalid/revoked bearer key — re-pair |
| 404 | invalid_code | Pairing code not found |
| 410 | expired | Pairing code older than 15 min |
| 410 | already_redeemed | Pairing code used once already |
| 413 | payload_too_large | Body exceeds 1 MB |
| 500 | query_failed / insert_failed | Server-side DB error — retry with backoff |
9. End-to-end extension flow
- User opens the dashboard, hits Generate, copies the pair URL.
- Extension opens the URL (or user clicks it). The content script picks up the code via
postMessage/ DOM. - Background worker
POSTs to/api/public/pairand storesapi_keyinchrome.storage.local. - Extension calls
GET /api/public/whoamito confirm. - On a timer, extension batches snapshots and
POSTs to/api/public/snapshots. - Dashboard / scripts read with
GET /api/public/snapshotsor stream/snapshots/export.