graft

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

RouteWhat it doesScope
GET /tokenThe token's owner and scopesany
GET /copiesYour copies, each with its last sync explainedread
POST /copiesConnect a repositorysync
GET /copies/{id}One copy and its five latest syncsread
POST /copies/{id}/syncSync nowsync
GET /copies/{id}/runsIts syncs, newest firstread
GET /copies/{id}/explainWhy the last sync stayed, and what to do nextread
GET /runs/{id}A sync's report, tests, log and conflictsread
GET /conflictsWhat's waiting: conflicting files and questionsread
GET /conflicts/{id}One conflict's three versions, or a question's optionsread
POST /conflicts/{id}/resolutionHand in a resolved fileresolve
POST /conflicts/{id}/answerAnswer a question with an optionresolve
GET /usagePlan, this month's syncs and agent spendread

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. ..."}}
StatusCodes
400invalid_request, invalid_json, app_not_installed, is_question
401unauthorized: no token, or it's wrong or revoked
403insufficient_scope, forbidden
404not_found: doesn't exist, or isn't yours
405method_not_allowed
409conflict: already connected, already syncing, already resolved, or out of date
413too_large
429rate_limited (see Retry-After) or limit_reached (a monthly limit)
503unavailable: 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.