Tokens
Calls take a Jog personal access token: Authorization: Bearer jog_pat_…. A token acts for one person and reads advice only, of their accounts. It reads nothing else on Jog: no money, settings, logs or other pages. Advice is for an account's owners for now.
- For an agent:
POST https://usejog.com/api/devicewith{"name", "access": "advice", "org"}(org, an account's login, is optional: without it the token reads all the person's accounts). The person approves the code in their browser; a token for one account can be approved only by its owners. PollPOST /api/device/tokenwith{"deviceCode"}everyintervalseconds until it answerskey, handed over once. Until then it answers an error:authorization_pending(not approved yet),logins_busy_here(too many asks at once from that network: ask again afterretryAfterseconds),busy(ask again in a few seconds),access_denied(declined) orexpired_token(expired, or the key was already collected: ask for a new code). Asking for a code can also answerlogins_busyorlogins_busy_here, withretryAfter. Each approval makes a new token, which lasts 7 days; none replaces an older one, even of the same name, so revoke those no longer used. - For CI: the person makes one at https:/
/ , for all their accounts or one, for 7, 30 or 90 days. It is shown once.usejog.com/ account/ -/ tokens
Revoke a token there, or with POST https://usejog.com/api/key/revoke and the token itself. Jog keeps only a token's hash. Its text is jog_pat_, 64 hex digits and 8 of check: the first 4 bytes of the SHA-256 of the rest.
GET/api/orgs/{org}/advice
Headers
| Name | Type |
|---|---|
Authorization | Bearer jog_pat_… |
Jog-Route-Version | 3 |
Query
| Name | Type |
|---|---|
goaloptional | "balanced" | "cost" | "speed" |
untiloptional | time |
Every job's advice over the last 30 days. Send Jog-Route-Version: 3: this answer is at version 3, and a call naming an older version, or none, gets 409 updated. Query: goal (cost, balanced or speed; the account's own otherwise, balanced until it chooses) and until (the window's end, at most now, as 2026-10-24T23:36:10Z). Pass the first answer's window.end as until to every later read, of either route, so they all read the same window. A run Jog hears of late, or whose measure comes after its charge, can still join it: if two reads disagree about a job, read both again.
| Field | What it is |
|---|---|
goal"balanced" | "cost" | "speed" | |
nowtime | |
window (start, end).object | |
jobsobject[] | one each, what is breaking first (failing, more-memory, go-back), then the changes to make, the most at stake first. |
jobs[].kindstring | today smaller, larger, more-memory, worked, go-back, failing (a move that fails and can't safely go back), few or right. |
jobs[].reasonstring | null | why, as a code: for right, cpu, cost, memory, growth, killed, disk, smallest, largest ("The right size") or unknown, tried, drift, hardware ("No change advised": Jog isn't sure, or for hardware, can't estimate the job's time on Jog Max's 16 or 32 vCPUs, which can run on slower cores); for few, runs, unusable, none-passed, mixed, moved, max-runs (moved to Jog Max, whose runs alone count there), mixed-tier (on Jog and Jog Max lately) or failures (failing since it moved, not yet shown to be the size's fault); for a move, failing, killed, slower, cost, pace, room, unknown. Read both as text: a kind or reason added later comes with its own title and detail. |
jobs[].title, detail | the advice in words, with its figures; each figure is a field below or worked out from the runs. |
jobs[].jobobject | repo (owner/name), repoId, workflow, workflowFile, name (GitHub's job name; a matrix leg's has its values). |
jobs[].sizeInNameboolean | the name holds a Jog size, as a leg named by its size does; changing its size renames it, and Jog can't follow the move. |
jobs[].lastRunAttime | when its newest run, on any size, was charged. |
jobs[].sizestring | the runner the advice is about: of its runs on the tier its newest run used (Jog or Jog Max), since its last stretch on the other, the one most of the newest 10 used; or for a fresh move that fails, the one it moved to. |
jobs[].tostring | null | the runner to move to, or null. A job is advised from those runs alone, so size and to are both Jog's labels or both Jog Max's (jog-max, jog-max-4vcpu and so on); only memory, disk and kills count from both, since a size's machines have the same memory and disk on either. A job on both tiers lately (mixed-tier) gets no to. |
jobs[].nowobject | its runs on size since it came from the size before (the window, without one): runs (the runs the advice counts), ran (every run), passed and failed (a run Jog ended at its job limit failed; any other cancelled run is neither), medianSeconds (the sample's median; 0 with no counted run), cost (micro-dollars spent, at the price each run was charged), costPerRun (a run that passed or failed, on average, at its tier's price now (Jog's, or Jog Max's); null with none), costPerPass (a passing run with its share of failed runs, on average, at its tier's price now, which a move compares; null with none passed), from (its first run). |
jobs[].sampleobject | null | the counted runs every time figure is of: runs, from, to. |
jobs[].afterobject | null | the other size judged and what it would make of the sample: size, medianSeconds (estimated; for a bigger size, at best), slowestTenth (for a smaller size, the most nine in ten of its runs would take there, in thousandths of their time now), cost, costMost. Set for smaller and larger, and on a right for the size it judged. |
jobs[].beforeobject | null | the size before (for a move, a failing size, or a size it tried), its runs as now has them, and since (when the runs on size began). |
jobs[].memoryPeakBytes, memoryP95Bytes, diskPeakBytes | of every run measured in the window on any size, each run's memory at most its own size's; null with none measured. |
jobs[].memoryGrowthBytesinteger | null | how much its memory grew a week lately on size, beside sizeMemoryPeakBytes, the most a run on size used. |
jobs[].limitsobject | null | the size the words hold the job against (size: to, after's size, or the size a kept or failing move can't go back to) and its memoryBytes and diskBytes. All in bytes. |
jobs[].killedinteger | runs of now killed for memory; |
jobs[].killobject | null | the one the words name (jobId, at, size); |
jobs[].largestKillobject | null | for more-memory with no to below Jog's largest size, the run killed there, which leaves no size to name. |
jobs[].unmeasuredinteger | passing runs of now Jog couldn't use, so passed is runs plus unmeasured. |
jobs[].unwholeinteger | runs on size since the sample began that Jog didn't wholly measure. |
Costs are over the last 30 days, or since now.from. Sizes and sentences write GB for GiB, 2^30 bytes. | |
curl -G https://usejog.com/api/orgs/ACCOUNT/advice \
-H "Authorization: Bearer $JOG_TOKEN" \
-H "Jog-Route-Version: 3"{
"goal": "balanced",
"jobs": [
{
"after": {},
"before": {},
"detail": "Its CPU stayed under half of 8 vCPUs in most runs.",
"diskPeakBytes": 2147483648,
"job": {},
"kill": {},
"killed": 12,
"kind": "smaller",
"largestKill": {},
"lastRunAt": "2026-10-24T23:36:10Z",
"limits": {},
"loadUsec": [],
"memoryGrowthBytes": 2147483648,
"memoryP95Bytes": 2147483648,
"memoryPeakBytes": 2147483648,
"now": {},
"reason": "cpu",
"sample": {},
"size": "jog-8vcpu",
"sizeInName": false,
"sizeMemoryPeakBytes": 2147483648,
"title": "Smaller and about as fast",
"to": "jog-4vcpu",
"unmeasured": 12,
"unwhole": 12
}
],
"now": "2026-10-24T23:36:10Z",
"window": {
"end": "2026-10-24T23:36:10Z",
"start": "2026-10-24T23:36:10Z"
}
}GET/api/orgs/{org}/advice/runs
Headers
| Name | Type |
|---|---|
Authorization | Bearer jog_pat_… |
Jog-Route-Version | 2 |
Query
| Name | Type |
|---|---|
beforeAtoptional | time |
beforeJobIdoptional | integer |
joboptional | string |
repoIdoptional | integer |
untiloptional | time |
workflowFileoptional | string |
Every run in the window, on Jog or Jog Max, or one job's (repoId, workflowFile and job together), newest first, 1,000 a page. Send Jog-Route-Version: 2: this answer is at version 2, whose size can be Jog Max's, and a call naming an older version, or none, gets 409 updated. While next isn't null, ask again with beforeAt set to its at and beforeJobId to its jobId; the last page can be empty. Take the same until as the advice to read the same window. Each run: jobId, job, at, size, seconds, cost, counted (counted, not_passed, unmeasured), outcome (passed, failed, neither), cpuUsec, cpuPeakMillicpus, memoryPeakBytes, diskPeakBytes, ioReadBytes, ioWriteBytes, networkRxBytes, networkTxBytes, memoryKills. A job's passed and failed are counts of its runs' outcomes.
curl -G https://usejog.com/api/orgs/ACCOUNT/advice/runs \
-H "Authorization: Bearer $JOG_TOKEN" \
-H "Jog-Route-Version: 2"{
"next": {
"at": "2026-10-24T23:36:10Z",
"jobId": 31415926
},
"now": "2026-10-24T23:36:10Z",
"runs": [
{
"at": "2026-10-24T23:36:10Z",
"cost": 4180,
"counted": "counted",
"cpuPeakMillicpus": 12,
"cpuUsec": 12,
"diskPeakBytes": 2147483648,
"ioReadBytes": 2147483648,
"ioWriteBytes": 2147483648,
"job": {},
"jobId": 31415926,
"loadUsec": [],
"memoryKills": 12,
"memoryPeakBytes": 2147483648,
"networkRxBytes": 2147483648,
"networkTxBytes": 2147483648,
"outcome": "passed",
"seconds": 12,
"size": "jog-8vcpu"
}
],
"window": {
"end": "2026-10-24T23:36:10Z",
"start": "2026-10-24T23:36:10Z"
}
}Errors
{"error": code, "message": words}
| Status | Error |
|---|---|
| 400 | bad_request |
| 401 | bad_key (ended, revoked or malformed) |
| 403 | key_not_allowed (a route a token can't call), not_owner |
| 404 | not_found (an account the token doesn't reach is not found, as a stranger's is) |
| 503 | busy (ask again in a few seconds) |
The same runs always give the same advice under a goal.


