API
The GitPanda REST API behind the command line, with API keys, scopes, the device sign-in flow and every endpoint.
Everything gitpanda does goes through a small REST API, which you can call yourself with the same
keys.
- Base URL:
https://okaypanda.com/api/v1 - Format: JSON in and out, except raw file downloads.
- Auth:
Authorization: Bearer opk_…on every request except sign-in. - Errors: a status code and
{ "error": "…" }with a plain-language message.
curl -s https://okaypanda.com/api/v1/me -H "Authorization: Bearer $GITPANDA_KEY"Keys and scopes#
A key (opk_ followed by 48 hex characters) acts as one person in one company. Get one from
browser sign-in (device flow) or make one in the app (GitPanda → Command line →
Use an API key instead).
| Scope | Allows |
|---|---|
read |
Every GET endpoint. Always included. |
work |
Pushing documents, asking rooms, starting and messaging goals, code. |
approve |
Deciding approvals and auto mode (owners and admins). |
The person's role is checked on every request: a viewer's key can only read, and approving needs an
owner or admin whatever the scopes. Revoked keys stop at once (401).
Rate limit: 240 requests a minute per key (429 beyond that).
| Status | Meaning |
|---|---|
400 |
The request isn't valid (the message says what). |
401 |
Missing, invalid or revoked key. |
403 |
The key lacks a scope, or the person's role doesn't allow it. |
404 |
Not found, or not visible to this key (other companies' things are always 404). |
409 |
A conflict: changed since you pulled, already decided, a precondition not met. |
429 |
Too many requests. |
Account#
GET /v1/me#
{
"user": { "email": "you@acme.com", "name": "You" },
"company": { "id": "1a2b…", "name": "Acme" },
"role": "owner",
"scopes": ["read", "work"],
"key": "gitpanda on my-mac",
"repo": { "slug": "acme", "visibility": "company", "url": "https://okaypanda.com/git/acme" }
}Repository#
GET /v1/repo#
The file tree, built live.
{
"slug": "acme",
"at": 1760720000000,
"tree": [
{ "path": "marketing/docs/launch-plan.md", "kind": "md", "size": 1834, "updated_at": 1760719000000, "editable": true },
{ "path": "marketing/README.md", "kind": "md", "size": 2210, "updated_at": 1760719500000, "editable": false },
{ "path": "marketing/files/pricing-report.pdf", "kind": "file", "size": 48211, "updated_at": 1760710000000, "editable": false }
]
}kind is md, file or image. updated_at is milliseconds since the epoch; use it as base
when pushing. Locked rooms are included only for owners and admins.
GET /v1/repo/raw?path=<path>#
One file's contents, with its real content type and an x-updated-at header. Markdown is text; files
and images are bytes.
GET /v1/repo/log?path=<path>&limit=<n>#
{ "commits": [
{ "sha": "7b72458", "message": "Add Spring sale plan", "author": "Sol", "ai": true, "at": 1760719000000, "path": "marketing/docs/spring-sale-plan.md" }
] }path filters to a file or folder. limit is at most 200.
POST /v1/repo/push (work)#
{ "path": "marketing/docs/launch-plan.md", "content": "# Launch plan\n\n…", "base": 1760719000000, "message": "Lead with the price", "force": false }- An existing document: saved as a new version. If it changed after
base, the answer is409with"conflict": true, unlessforceis true. Answers{ "ok": true, "path", "updated_at", "version" }, or"unchanged": trueif nothing changed. - A new document at
docs/<name>.mdor<room>/docs/<name>.md: created with its first# Headingas the title. Answers201with"created": trueand thepaththe repository gives it. - READMEs, generated files, files, images and paths outside
docs/are refused with400. - Content is at most 60,000 characters.
AI#
All of these read only what the key can see (locked rooms only for owners and admins), use the company's AI budget, and are limited to 20 requests a minute and 300 a day per key. Long answers arrive after a few keep-alive spaces; parse the trimmed body.
GET /v1/repo/search?q=<words>&limit=<n>#
Full-text search (no AI): { "results": [ { "path", "title", "snippet", "updated_at" } ] }. Matches in
snippet are wrapped in **.
POST /v1/repo/ask#
{ "question": "What's our refund policy?" } →
{ "answer": "Refunds within **30 days**…", "confident": true,
"sources": [ { "path": "marketing/docs/refund-policy.md", "why": "The policy itself" } ],
"considered": ["README.md", "marketing/docs/refund-policy.md"] }sources are always among considered, the files the AI read.
POST /v1/ai/commit-message#
{ "changes": [ { "path", "before": "…" | null, "after": "…" } ] } (up to 20 files) → { "message" }.
POST /v1/ai/review#
The same changes →
{ "verdict": "looks_good" | "suggestions" | "needs_changes", "summary": "…",
"comments": [ { "path", "severity": "issue" | "suggestion" | "praise", "quote": "…", "note": "…" } ],
"compared_with": ["sales/docs/price-list.md"] }POST /v1/ai/edit (work)#
{ "path": "marketing/docs/plan.md", "content": "…current text…", "instruction": "Cut it to 200 words" } →
{ "content": "…the whole edited document…", "summary": "…" }. Nothing is saved; push it yourself.
GET /v1/repo/explain?path=<path>#
{ "path", "summary", "points": [], "ask": [] }. 404 if there's no such file.
GET /v1/repo/recap?days=<1–90>#
{ "days", "summary", "highlights": [], "commits", "contributors": [ { "name", "ai", "changes", "last" } ] }
POST /v1/ai/intent#
{ "text": "make the launch plan lead with the price" } →
{ "command": "edit", "args": ["marketing/docs/launch-plan.md", "lead with the price"], "explain": "…" }.
command is one of ask, search, edit, request, goal, check, recap, why, explain, save, get, status, now.
Nothing runs; the client shows it and runs it.
Team in the repository#
POST /v1/repo/check#
{ "score": 76, "checked": 42, "issues": [ { "id", "kind": "contradiction" | "stale" | "thin" | "broken_link" | "no_docs", "severity": "high" | "medium" | "low", "paths": [], "note", "fix" } ] }. Uses the AI for contradictions.
POST /v1/repo/check/fix (work)#
{ "issues": [ …from check… ] } (up to 10) → { "started": [ { "goal": { "id", "seq" }, "room", "who", "paths" } | { "error", "paths" } ] }.
POST /v1/repo/request (work; auto needs approve)#
{ "target": "marketing/docs/launch-plan.md" | "marketing", "text": "…", "auto": false } → 201
{ "goal": { "id", "seq", "title" }, "room", "who" }. A document is updated in place as a new
version; a room gets a new document. READMEs and files can't be requested (400); unknown paths or
rooms are 404.
GET /v1/repo/why?path=<path>#
{ "path", "title", "version", "versions": [ { "version", "at", "author", "ai", "role", "message", "task": { "seq", "title", "status" }, "goal": { "seq", "title" }, "summary" } ] }, newest first.
GET /v1/repo/now#
{ "working": [ { "who", "room", "task": { "seq", "title" }, "goal", "writing": "the document's title" | null } ] }.
GET /v1/repo/contributors#
{ "contributors": [ { "name", "ai", "changes", "last" } ] } (no AI).
Team#
GET /v1/rooms#
{ "rooms": [ { "id": "…", "name": "Marketing", "slug": "marketing", "locked": 0, "lead": "Sol", "staff": 2, "running": 1, "open": 3 } ] }POST /v1/ask (work)#
{ "room": "marketing", "text": "How did the launch go?" }{ "room": "Marketing", "lead": "Sol", "reply": "…", "task": { "id": "…", "seq": 48, "title": "…" } }task is set when the lead queued work. This call can take several seconds.
Goals#
POST /v1/goals (work; auto needs approve)#
{ "text": "Plan the spring sale", "room": "marketing", "auto": false }Answers 201 with { "goal": { "id", "seq", "title", "status" } }. Without room, the Coordinator
plans it. Without auto, the company's mode applies.
GET /v1/goals?all=1#
Open goals (or all of them), with total and done task counts.
GET /v1/goals/<id or G-n>#
{
"goal": { "id": "…", "seq": 35, "title": "…", "status": "running", "auto": 0, "summary": "…" },
"tasks": [ { "id": "…", "seq": 36, "title": "…", "status": "completed", "summary": "…", "waiting_for": null, "who": "Sol", "room": "Marketing" } ],
"messages": [ { "role": "agent", "kind": "result", "body": "…", "who": "Sol", "created_at": 1760719000000 } ],
"approvals": [ { "id": "…", "kind": "action", "action": "send_email", "reasoning": "…", "risk": "medium" } ],
"files": [ { "id": "…", "name": "subject-lines.pdf", "type": "application/pdf", "size": 18211 } ],
"pull_requests": ["https://github.com/acme/website/pull/12"]
}GET /v1/goals/<id or G-n>/log?after=<ms>#
The live work log: rows updated after after, oldest first, and at to pass next time. Poll every
one to three seconds to stream it.
{ "goal": { "id": "…", "seq": 35, "status": "running" },
"log": [ { "id": "…", "kind": "tool", "label": "write_document", "body": "…", "live": 0, "who": "Sol", "meta": "{\"decision\":\"allowed\"}", "updated_at": 1760719000000 } ],
"at": 1760719000000 }kind is thinking, text, tool, result or search. live is 1 while a row is still being
written.
POST /v1/goals/<id or G-n>/message (work)#
{ "text": "Make it shorter" } → { "reply", "kind", "task" }. kind says how it was taken: a
question, an update or new work.
GET /v1/files/<id>#
Download a file a goal made (an attachment).
Approvals#
GET /v1/approvals#
Pending approvals: id, kind, action, reasoning, expected_outcome, risk, the task and the
employee.
POST /v1/approvals/<id> (approve; owners and admins)#
{ "decision": "approved" | "rejected", "note": "…" }. 409 if it was already decided.
Code#
POST /v1/code (work; auto needs approve)#
{ "text": "Add a size guide page", "repo": "acme/website", "room": "engineering", "auto": false }409 if GitHub isn't connected or no one may open pull requests; 400 if repo isn't one the
connection can use. Answers 201 with the goal; follow it with the goals endpoints, and read
pull_requests when it's done.
Device sign-in#
How gitpanda login signs in without a key, like GitHub's device flow. No Authorization header.
1. POST /v1/device#
{ "client": "my-tool on my-mac" } (shown to the person approving) →
{ "device_code": "<64 hex, keep it secret>", "user_code": "WDJB-MJHT",
"verification_uri": "https://okaypanda.com/app/device",
"verification_uri_complete": "https://okaypanda.com/app/device?code=WDJB-MJHT",
"interval": 3, "expires_in": 900 }Show user_code and open verification_uri_complete. 30 requests an hour per IP address.
2. The person approves#
At /app/device, signed in, they check the code, pick companies and scopes, and approve or cancel.
3. POST /v1/device/token#
Poll every interval seconds with { "device_code": "…" }:
status |
Meaning |
|---|---|
pending |
Not approved yet. Keep polling. |
slow_down |
Poll less often. |
denied |
They cancelled. Stop. |
expired |
The code expired or was already used. Start again. |
approved |
Done, once. |
{ "status": "approved", "user": { "email": "you@acme.com" },
"accounts": [ { "org": "…", "company": "Acme", "slug": "acme", "url": "https://okaypanda.com/git/acme", "role": "owner", "scopes": ["read", "work"], "key": "opk_…" } ] }The keys are created at this moment and returned once; the server never stores them.
Public repository pages#
Public repositories (and any you can see, with a session) have simple read endpoints, used by the web page:
GET /api/git/<slug>: the tree and recent commitsGET /api/git/<slug>/raw?path=<path>: one fileGET /api/git/<slug>/zip: the whole repository as a .zipGET /api/git/<slug>/search?q=: search (anyone who can see it)GET /api/git/<slug>/contributors: contributors (anyone who can see it)POST /api/git/<slug>/ask,GET /api/git/<slug>/recap?days=,GET /api/git/<slug>/explain?path=: the AI, for signed-in members and people it's shared with (401signed out,403otherwise)GET /api/git/<slug>/why?path=,GET /api/git/<slug>/now: the story and who's working (anyone who can see it)POST /api/git/<slug>/check: the health check (members and shares)POST /api/orgs/<org>/repo/request,POST /api/orgs/<org>/repo/fix: requests and fixes from the page (signed-in members)