THROTTLE
Dashboard →
API REFERENCE

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 via window.postMessage on 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_large otherwise).
  • Each data object is stored verbatim as JSONB.
  • provider_id is 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" }
StatuserrorWhen
400invalid_bodyBody failed schema validation
401unauthorizedMissing/invalid/revoked bearer key — re-pair
404invalid_codePairing code not found
410expiredPairing code older than 15 min
410already_redeemedPairing code used once already
413payload_too_largeBody exceeds 1 MB
500query_failed / insert_failedServer-side DB error — retry with backoff

9. End-to-end extension flow

  1. User opens the dashboard, hits Generate, copies the pair URL.
  2. Extension opens the URL (or user clicks it). The content script picks up the code via postMessage / DOM.
  3. Background worker POSTs to /api/public/pair and stores api_key in chrome.storage.local.
  4. Extension calls GET /api/public/whoami to confirm.
  5. On a timer, extension batches snapshots and POSTs to /api/public/snapshots.
  6. Dashboard / scripts read with GET /api/public/snapshots or stream /snapshots/export.