Docs / For agents
HTTP API
Everything an agent can do in Graft, as plain JSON over HTTPS: connect copies, start syncs, read reports and what stopped a sync, and hand in resolutions. The MCP server offers the same actions to MCP clients. Graft still moves a copy only when every change lands and the tests pass.
Authentication
Make a personal API token under Settings in your dashboard and send it as a bearer token. Tokens are shown once and scoped to read, sync and resolve; how tokens work has the details. The examples below read it from an environment variable:
export GRAFT_TOKEN=graft_... curl -s https://graftfork.com/api/v1/token -H "Authorization: Bearer $GRAFT_TOKEN"
The base URL is https://graftfork.com/api/v1. Requests and answers are JSON. The OpenAPI 3.1 description is public, for generating a client or loading into a tool.
Endpoints
| Route | What it does | Scope |
|---|---|---|
GET /token | The token's owner and scopes | any |
GET /copies | Your copies, each with its last sync explained | read |
POST /copies | Connect a repository | sync |
GET /copies/{id} | One copy and its five latest syncs | read |
POST /copies/{id}/sync | Sync now | sync |
GET /copies/{id}/runs | Its syncs, newest first | read |
GET /copies/{id}/explain | Why the last sync stayed, and what to do next | read |
GET /runs/{id} | A sync's report, tests, log and conflicts | read |
GET /conflicts | What's waiting: conflicting files and questions | read |
GET /conflicts/{id} | One conflict's three versions, or a question's options | read |
POST /conflicts/{id}/resolution | Hand in a resolved file | resolve |
POST /conflicts/{id}/answer | Answer a question with an option | resolve |
GET /usage | Plan, this month's syncs and agent spend | read |
Copies
GET/api/v1/copies
curl -s https://graftfork.com/api/v1/copies -H "Authorization: Bearer $GRAFT_TOKEN"
Each copy has its repository, branch, upstream, the release it sits on (base), how many conflicts and questions wait, its last sync and explain: a headline and what to do, in the words the dashboard uses.
POST/api/v1/copies
Graft's GitHub app has to be installed on the repository. With only repo, Graft detects the upstream, the release your branch sits on and the tag pattern, and starts the first sync when there is a newer release:
curl -s https://graftfork.com/api/v1/copies -H "Authorization: Bearer $GRAFT_TOKEN" \
-H "Content-Type: application/json" -d '{"repo": "you/your-fork"}'
To follow a branch other than the default, give all three of branch, upstream (owner/name on GitHub or a https git URL) and base (the upstream release tag the branch is on). Optional: tags (a glob such as v*), follow (all, minor or major):
curl -s https://graftfork.com/api/v1/copies -H "Authorization: Bearer $GRAFT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repo": "you/your-fork", "branch": "patched", "upstream": "owner/project", "base": "v2.3.1"}'
Answers 201 with copyId, run (the first sync, if one started) and a message.
POST/api/v1/copies/{id}/sync
curl -s -X POST https://graftfork.com/api/v1/copies/COPY_ID/sync -H "Authorization: Bearer $GRAFT_TOKEN"
Answers 202 with the run. A sync usually takes a few minutes; poll the copy or its runs. 409 when one is already queued or running, 429 with code limit_reached when this month's syncs are used.
GET/api/v1/copies/{id}/explain
curl -s https://graftfork.com/api/v1/copies/COPY_ID/explain -H "Authorization: Bearer $GRAFT_TOKEN"
Why the last sync didn't move the copy: a headline, the stuck changes, what's waiting, and next.do, one of sync_now, resolve, ask_the_user, review_pull_request, wait, user_action or nothing, with next.why in plain words.
Runs
GET/api/v1/copies/{id}/runs and /api/v1/runs/{id}
curl -s https://graftfork.com/api/v1/copies/COPY_ID/runs -H "Authorization: Bearer $GRAFT_TOKEN" curl -s https://graftfork.com/api/v1/runs/RUN_ID -H "Authorization: Bearer $GRAFT_TOKEN"
A run's status is queued, running, moved, stayed, waiting (conflicts or questions wait), failed or cancelled. The single run adds the report (each change kept, dropped, rewritten or stuck, and why), how the tests ran, the log, and its conflicts.
Conflicts and questions
GET/api/v1/conflicts
curl -s https://graftfork.com/api/v1/conflicts -H "Authorization: Bearer $GRAFT_TOKEN"
conflicts are files waiting for your agent (one per file of a change). questions are an agent's question that needs your own decision, with numbered options.
GET/api/v1/conflicts/{id}
curl -s https://graftfork.com/api/v1/conflicts/CONFLICT_ID -H "Authorization: Bearer $GRAFT_TOKEN"
For a conflict: base (the file before your change), ours (with it), theirs (in the new release), reason (from your commit) and instructions. null means the file doesn't exist in that version.
POST/api/v1/conflicts/{id}/resolution
The whole file as it should be on the new release, or {"delete": true}:
jq -Rs '{content: .}' resolved.c | curl -s https://graftfork.com/api/v1/conflicts/CONFLICT_ID/resolution \
-H "Authorization: Bearer $GRAFT_TOKEN" -H "Content-Type: application/json" -d @-
Graft stores it. When every file in that sync is in, it runs the sync again with them (resumedRun) and moves your copy only if every change lands and the tests pass. A resolution can't be changed once handed in; if the tests reject it, the copy stays and the next sync asks again.
POST/api/v1/conflicts/{id}/answer
curl -s https://graftfork.com/api/v1/conflicts/QUESTION_ID/answer -H "Authorization: Bearer $GRAFT_TOKEN" \
-H "Content-Type: application/json" -d '{"option": 2}'
Send only the option the person picked. The chosen file is checked by the tests like any rewrite.
Usage
curl -s https://graftfork.com/api/v1/usage -H "Authorization: Bearer $GRAFT_TOKEN"
Your plan, this month's syncs and copies against their limits, and agent spend. Counts reset on the 1st (UTC).
Errors
Every error has the same shape and a matching HTTP status:
{"error": {"code": "insufficient_scope", "message": "This token can't sync: it has read. ..."}}
| Status | Codes |
|---|---|
| 400 | invalid_request, invalid_json, app_not_installed, is_question |
| 401 | unauthorized: no token, or it's wrong or revoked |
| 403 | insufficient_scope, forbidden |
| 404 | not_found: doesn't exist, or isn't yours |
| 405 | method_not_allowed |
| 409 | conflict: already connected, already syncing, already resolved, or out of date |
| 413 | too_large |
| 429 | rate_limited (see Retry-After) or limit_reached (a monthly limit) |
| 503 | unavailable: hosted syncs are paused for now |
Rate limits
Per token: 120 requests a minute, of which 30 may change something (connect, sync, resolution, answer). Every answer carries RateLimit-Limit and RateLimit-Remaining; a 429 carries Retry-After in seconds. The monthly limits on syncs and copies are your plan's, the same as in the dashboard.
Every request made with a token is logged and shown in your dashboard's Settings.