← Owner Map / API
Get a token

Driving Owner Map over HTTP

Everything the page does with a model, a script can do. One endpoint does the work; the rest is authentication and polling. The app takes an aggregate measured from a git history plus a task field naming which of three lanes to run, and returns a single JSON envelope whose body shape depends on that lane.

Read one thing first: Owner Map never runs git and has no access to your repository. It has no clone step, no forge credentials and no webhook. You run git log yourself, and you send the numbers. The browser app computes those numbers locally; over the API you compute them and put them in facts.

Base URL

https://api.skillsafe.ai/v1/app-api

Every request carries Authorization: Bearer <token> and Content-Type: application/json — and nothing else. There is no X-App-Slug header: the token is bound to this app when it is minted, and the slug appears in exactly one place, the JSON body of POST /guest. A bogus slug header is accepted and ignored, so do not use one as a check that anything is wired correctly. Get a token from the token page without opening a developer console.

The response envelope

Every endpoint returns the same wrapper. Success carries data; failure carries error. Nothing returns a bare value, so a client can branch on the presence of error alone. The model's own answer is a JSON string nested inside that wrapper, at data.output.output — parse the wrapper, then parse that string.

{"ok": true, "data": {"job_id": "job_...", "status": "succeeded",
  "charged_credits": 1131, "truncated": false,
  "output": {"output": "{\"lane\": \"risk\", \"posture\": \"critical\", ...}"}}}

{"ok": false, "error": {"code": "payment_required", "message": "...", "details": {}}}

Errors worth branching on

codeHTTPwhat happenedwhat to do
unauthorized401No token, an expired token, or a token minted for another app.Mint a new one with POST /guest, or sign in again for a personal token.
payment_required402Balance below the run's minimum, or below what the hold needs.Call POST /estimate first and compare min_credits and hold_credits against GET /me. The page disables its own button rather than submitting into this.
validation_error400The body was not a JSON object, or a field had the wrong type.Check you sent the input object itself - see the warning in step 3.
rate_limited429Too many requests, or 30/min exceeded on similarity search.Back off and retry; never tight-loop.
not_found404Unknown job id, or a collection this release does not declare.Check the id; a job id is only valid for the account that created it.

A run that finishes with "truncated": true is not an error: the balance sat between min_credits and hold_credits, so the output cap was reduced and the reply is cut short. Treat it as incomplete rather than as an answer.

1. Get a token

A guest token is enough for GET /me and POST /estimate. Running a lane is metered, so it needs a personal token, which comes from signing in - the token page reveals and copies the one this browser holds without a developer console. Every POST /guest mints a new guest identity, so reuse one token across calls: a fresh guest cannot see the previous guest's saved runs.

This is the only call that names the slug, and it names it in the body.

2. Check the session and the balance

GET /me tells you whether the token is a person or a guest, and what the balance is. Compare it against the hold from step 4 before you run anything: a 402 after submitting is a failure of the client, not of the user.

3. Build the run input

The input object is the same for every lane except for two fields. task is "risk", "handover" or "split"; changed_files is the raw change-set paste and belongs only to the split lane, whose grouped form also appears inside facts.change_set.

The body IS the input object.

Do not wrap it as {"input": {...}}. A wrapped body is accepted: it returns 200 and a plausible-looking hold, because the wrapper is priced as text. The model then never sees your task or your facts, and a real run is billed against a payload the prompt cannot read, with no error anywhere to catch it. Assert the shape with a free POST /estimate and sanity-check the hold against a run you have seen before.

facts is the aggregate the app measures in the browser. Everything in it is optional - the model works from what is present - but nothing outside it may be asserted by the reply, so a thin facts gets a thin answer. The shape below is the useful minimum; the full shape, including the directory rollup, the CODEOWNERS reality check, per-file top contributors and the commit sample, is described in llms.txt.

Produce the history the app expects with:

git log --no-merges --numstat --date=short --pretty=format:"::%H|%an|%ae|%ad|%s"

Flags carry stable ids and are the accountability mechanism: every flag you send with severity critical or high must come back in coverage_check. If you send no flags, nothing is checked.

4. Price it - free, and the only proof the wiring is right

POST /estimate takes the same body as a run, creates no job and charges nothing. Use it for three separate things:

5. Run it, and poll the job

POST /run returns a job_id immediately; poll GET /jobs/{id} until status is terminal (succeeded or failed). Always send an Idempotency-Key: a hash over the payload including the task, plus an attempt counter. A retry of the same attempt is then free of the risk of double billing, while a deliberate second attempt uses a new key.

6. Stream it instead

POST /run-stream is the same body over Server-Sent Events. Frames arrive as event: delta with a {"text": "..."} payload, then one event: job with the terminal job, or event: error. The final job frame is authoritative: deltas can drop the tail, so parse the reply from the job, not from the accumulated stream. The page uses the deltas only to advance its progress stages.

7. Read the reply, and check it against your own flags

The model's answer is one JSON object, the same envelope for every lane. The per-lane part is body:

Two checks are worth writing into any client, because they are the two failures a reader will not notice: every critical or high flag you sent must appear in coverage_check, and in the split lane every path you sent must be placed in exactly one pull request or explicitly listed in leftovers. The page performs both and displays the difference rather than hiding it.

{"lane": "risk", "lane_inferred": false, "title": "atlas-pay: ownership and bus-factor review",
 "posture": "critical", "verdict": "...", "summary": "...",
 "headline_numbers": [{"label": "sensitive files with bus factor 1", "value": "7"}],
 "findings": [{"id": "OM-1", "title": "...", "severity": "critical",
               "path": "services/auth/mfa.py", "owner": "Wei Zhang",
               "evidence": "...", "why": "...", "action": "...", "effort": "days"}],
 "coverage_check": [{"flag_id": "ORPH-1", "status": "confirmed", "finding_id": "OM-1", "note": ""}],
 "artifact": {"kind": "markdown", "filename": "CODEOWNERS.draft", "content": "..."},
 "next_lane": {"lane": "handover", "reason": "..."},
 "assumptions": ["..."], "open_questions": ["..."],
 "body": {"owner_notes": [...], "codeowners_actions": [...]}}

Rate limits, cost and idempotency in one paragraph

POST /estimate is free and creates no job. A run is metered: the hold prices the full output cap and the charge is what the run actually used, usually far less. Send an Idempotency-Key on every run - a content hash of the payload including the task, plus an attempt counter - so a retried request cannot bill twice. Note that the platform answers a replayed key with the original job even when the body differs, so a deliberate second attempt needs a different key.

What the app itself sends

The browser app computes facts from your history export with the algorithm described in llms.txt - a one-year half-life on each commit's weight, ownership shares, bus factor, HHI, a sensitive-path match, a CODEOWNERS reality check and a bounded low-discrepancy sample of commit subjects. It sends the aggregate and never the log. If you build your own aggregate, you decide what to include; the reply's quality tracks what you send, and anything absent from facts is something the model is forbidden to assert.

Owner Map is a derived work built on three published agent skills: @openai/security-ownership-map, @sickn33/git-pr-workflows-onboard and @nvidia/mcore-split-pr. Not affiliated with those skills' authors, nor with Git or GitHub.