Usage tracking
Every job your account runs is counted, whether it came from an API key, an embedded
editor or the website. The dashboard's Usage page is the human view;
GET /usage is the same data, and the SDK and MCP server are thin wrappers over it.
What is tracked
| Counter | What it counts |
| --- | --- |
| jobs | Every job accepted for the account, whatever its outcome. |
| credits | Credits actually billed. A free operation contributes 0, a cache hit contributes 0, and a job that fails terminally is refunded and contributes 0. |
| cacheHits | Jobs answered from the content-addressed cache — the same image, the same operation and the same params had already been run. No model ran and no credits were spent. |
| free | Jobs for a zero-credit operation, such as resize-image. |
| failed | Jobs that reached a terminal failure. Their credits were refunded. |
| delivered | Results written into one of your storage destinations. |
| deliveryFailed | Results whose delivery to your bucket failed. A delivery failure never fails the job — the bytes are still downloadable from snapnedit unless you asked for delete-after-delivery. |
| sessions | Embedded-editor sessions started (one per POST /embed/sessions or host-minted token exchange). |
| activeSessions | Sessions whose editor was last seen at or after from — the "still open" count for the window. It has no per-bucket meaning, so it appears only on totals. |
Jobs are attributed to the API key that ran them, the embed origin they came from, the operation, and the source:
| Source | What it is |
| --- | --- |
| api | A secret sk_… key. Billed. |
| embed | An embedded editor session — a publishable key or a host-minted token. Billed. |
| session | A signed-in visitor on snapnedit.com. Free. |
| anonymous | A visitor with no account. Free. |
Free sources are still counted; they simply contribute 0 to credits.
GET /usage
Authenticate with a secret sk_… key, a website session token, or an embed token (which
sees only its own key's usage). See API Authentication.
GET /usage?from=2026-08-15&to=2026-09-13&groupBy=operation&source=embed
Authorization: Bearer sk_live_...
{
"range": { "from": "2026-08-15T00:00:00.000Z", "to": "2026-09-13T23:59:59.999Z" },
"groupBy": "operation",
"totals": {
"jobs": 412, "credits": 968, "cacheHits": 57, "free": 31,
"failed": 4, "delivered": 380, "deliveryFailed": 1,
"sessions": 96, "activeSessions": 3
},
"series": [
{ "key": "upscale", "label": "upscale", "jobs": 210, "credits": 630,
"cacheHits": 22, "free": 0, "failed": 2, "delivered": 200,
"deliveryFailed": 0, "sessions": 0 }
],
"keys": [
{ "id": "key_2f…", "name": "Site widget", "kind": "publishable",
"dailyCreditLimit": 500, "usedToday": 431 }
]
}
Parameters
| Parameter | Default | Notes |
| --- | --- | --- |
| from, to | The last 30 days | A YYYY-MM-DD (read as midnight UTC) or a full ISO timestamp. A range wider than 366 days, or one that runs backwards, is refused with 400 invalid_input. The response echoes the resolved window back as ISO instants — from at the start of its day, to at the end. |
| groupBy | day | day, key, origin, operation or source — how series is bucketed. |
| keyId | — | Only jobs billed to this API key id. |
| origin | — | Only jobs from this embed origin: a site origin (https://acme.example) or native:<app id>. |
| operation | — | Only jobs for this operation id. |
| source | — | Only api, embed, session or anonymous. |
Filters narrow what is counted; groupBy decides how what is left is bucketed. They
compose: ?source=embed&groupBy=origin is "embed credits per customer site", and
?operation=upscale&groupBy=day is "how upscaling trends".
groupBy semantics
| Value | One row per | key is | label is |
| --- | --- | --- | --- |
| day | Every calendar day (UTC) in the range — zero-filled, so a chart has a point per day rather than gaps | YYYY-MM-DD | The same date |
| key | API key that ran something | The key's id | The key's name — the one grouping where label differs from key |
| origin | Embed origin | The origin, or native:<app id> | The same value |
| operation | Operation id | The operation id | The same value |
| source | One of the four sources above | The source | The same value |
A bucket whose dimension has no value — a website job has no API key, an sk_ job has no
origin — is grouped under the key "none". It is real usage, but it is not something you
can filter to.
sessions is per-bucket only where the dimension exists: it is always 0 on a
groupBy=operation or groupBy=source row, because a session belongs to a key and an
origin, not to an operation. totals.sessions is correct either way.
keys is the same on every response, and is not filtered by the range: it is your
current (non-revoked) key roster with each key's spend so far today against its
dailyCreditLimit (null means uncapped — only publishable keys carry a cap). It is the
number to watch when a cap is about to bite; the dashboard colors the gauge amber at 80%
and red at 100%. It is omitted entirely for an embed-token caller, which may not
enumerate the account's keys — the SDK normalizes that absence to [].
Embed session counting
A session is opened when a token is minted — POST /embed/sessions (a publishable key
exchanged by the editor frame) or POST /embed/tokens (a token your server minted). Both
return a sessionId.
A long edit re-mints its token silently, and that must not count as a new session. The
editor frame handles this for you: it keeps the sessionId from its first exchange and
echoes it on every re-mint, so POST /embed/sessions touches that session's lastSeenAt
instead of opening another one. If you mint tokens from your own server with
POST /embed/tokens you own the continuity yourself — that route opens a session per
call.
activeSessions counts sessions last seen at or after from — still open in the window,
rather than merely started in it. Sessions are attributed to the key and the embed origin,
which is what makes groupBy=origin a per-customer-site view; groupBy=operation and
groupBy=source have no session dimension and report 0.
Hosts that need to bill their own end users should meter from the frame's job event
rather than from this endpoint — it fires per job, carries credits, cached and
deliveryOnly, and can be attributed with endUserId. See
Metering your own end users.
Retention
Raw per-job usage rows are read directly for the most recent 2 days. Older days are folded into daily rollups by a worker sweeper — a day is only rolled up once it is two whole days in the past and every job in it is terminal, so a job that fails hours after midnight is still counted. Rollups are kept indefinitely. A range that reaches back past the raw window is answered from both, so:
groupBy=dayis accurate for the whole range;- the finest resolution available for older data is one day — there is no hour-by-hour view of last month;
- a job that ran minutes ago is already counted (rows are written as jobs settle).
CSV export
The Usage page exports whatever is on screen as CSV — the daily series
from the chart card, or the active breakdown table from its tab bar. Columns are key,
label, jobs, credits, cacheHits, free, failed, delivered, deliveryFailed,
sessions, and the file is named for the range it covers. It is built in the browser from
the response you are already looking at, so the CSV and the page can never disagree.
Every filter is in the URL, so a filtered view is shareable:
/dashboard/usage?from=2026-09-01&to=2026-09-13&source=embed&groupBy=origin.
From the SDK
import { createClient } from '@snapnedit/sdk';
const client = createClient({ baseUrl: 'https://snapnedit.com/api', apiKey: process.env.SNAPNEDIT_API_KEY! });
const usage = await client.getUsage(); // last 30 days, by day
const byOrigin = await client.getUsage({ source: 'embed', groupBy: 'origin' });
// `range` comes back as ISO instants; `.slice(0, 10)` is the day.
console.log(usage.range.from.slice(0, 10), usage.range.to.slice(0, 10));
for (const key of usage.keys) {
if (key.dailyCreditLimit !== null && key.usedToday / key.dailyCreditLimit > 0.8) {
console.warn(`${key.name}: ${key.usedToday}/${key.dailyCreditLimit} today`);
}
}
See Using the SDK.
From an agent (MCP)
The @snapnedit/mcp server exposes one read-only
tool, get_usage, taking from, to, group_by, source, operation, key_id and
origin — the same filters in snake_case. It returns the report with the key roster
trimmed to { id, name, usedToday, dailyCreditLimit }, so no other account detail enters
the agent's transcript.
How many credits did upscaling cost last week, and is any key close to its daily cap?