HTTP API
49 operations under /v1, 27 of them also exposed as MCP tools. Generated from the same registry the router matches against.
JSON in, JSON out, one bearer credential. Unknown fields are rejected rather than ignored — a typo in a body is a 400, not a silent default.
curl https://app.trunk.dev/v1/me \
-H "Authorization: Bearer $AGENT_KEY"
Live events are the one thing not in the registry: GET /v1/stream is server-sent events, with ?since=<event-id> to replay from a point. This UI uses it so it never polls.
Open the authenticated API consoleWho the credential belongs to, which services and paths it may touch, what it has left to spend today, and how its changes have fared. An agent should read this before it proposes anything.
Call this first. Returns your scopes (which services and paths you may change), your remaining budget for today, and your measured track record per service — the same numbers policy uses to decide whether your next change lands unattended.
The whole lifecycle: propose, verify, decide, land, revert. A change is the only thing that reaches trunk — there is no branch to push and no pull request to open.
POST/v1/changes
trunk_propose_changeSubmits a complete change object: your stated intent, the spec you worked to, the diff, the results you observed, and your session trace. Trunk verifies the diff itself, scores risk against your track record in this service, and applies repo policy. Configured required external checks are created and dispatched automatically for the exact immutable candidate revision; callers do not need to start Trunk-managed CI manually. If policy says auto_land and every required external check is green for the current revision, an ordinary Change lands. A Draft-controlled revision remains open until that exact revision is marked Ready. Human-routed changes still require a decision, and neither approval nor Ready bypasses required gates.
Body
| repo_idrequired | string | Repository id or name. |
| titlerequired | string | Imperative one-liner, as it will read on trunk. at most 120 characters |
| intentrequired | string | Why this change exists, in your own words: the problem, the evidence it is real, and what you decided not to do. This is what a human reads first. at least 20 characters |
| diffrequired | string | Unified diff (git format). Trunk derives its own evidence from this — it does not take your word for what changed. at least 10 characters |
| service | string | Owning service. Inferred from the changed paths when omitted. |
| branch | string | The branch this change lives on. Naming one that still carries an open change revises that change; omit it and Trunk opens a new change on a free branch derived from your handle and the title. at most 200 characters |
| spec | object | |
| goal | string | |
| non_goals | string[] | |
| acceptance_criteria | string[] | |
| constraints | string[] | |
| base_commit | string | Trunk commit you worked from. Defaults to current head. |
| reported | object | Results you observed. Recorded as self-reported and weighted as such. |
| suites | object[] | |
| namerequired | string | |
| passedrequired | integer | |
| failedrequired | integer | |
| skipped | integer | |
| duration_ms | integer | |
| coverage_delta | number | |
| benchmarks | object[] | |
| namerequired | string | |
| before_msrequired | number | |
| after_msrequired | number | |
| trace | object[] | Your session trace: how you got here. |
| kindrequired | string | What the agent was doing at this step. one of read · search · reason · edit · test · bench · tool · note |
| summaryrequired | string | One line, written for a human skimming the trace. at most 300 characters |
| detail | string | Optional longer output: command, result, reasoning. |
| ts | string | ISO timestamp. Defaults to now. |
| tokens | integer | Tokens consumed by this step. |
| usd | number | Cost of this step in USD. |
| cost_usd | number | What this change cost to produce. Counts against your budget. |
| draft | boolean | When true, all normal verification/CI/Preview work continues but this Change enters explicit revision-bound Draft/Ready landing control. |
GET/v1/changes
trunk_list_changesQuery
| repo_id | string | |
| service | string | |
| branch | string | Only changes on this branch. |
| author | string | Actor handle or id. |
| status | string | Comma-separated: verifying, awaiting_decision, approved, landed, reverted, blocked, rejected. |
| route | string | one of auto_land · human_review · owner_review · block |
| limit | integer | 1–200 defaults to 25 |
GET/v1/changes/:change_id
trunk_get_changePath
| change_idrequired | string | The change id to act on. |
Query
| include | string | Comma-separated: diff, evidence, trace, decisions, track. defaults to "evidence,trace,decisions,track" |
POST/v1/changes/:change_id/verify
trunk_verify_changeRebuilds the evidence bundle from scratch, re-scores risk against the current track record, and re-applies policy. Any approval bound to an older evidence digest is invalidated. After verification materializes the current immutable candidate, configured required external checks are dispatched automatically. Re-running verification safely reuses/reconciles the same durable first-attempt CI identity.
Path
| change_idrequired | string | The change id to act on. |
POST/v1/changes/:change_id/decisions
human onlyHuman-only. request_key is the caller-stable idempotency boundary: retry the exact same request with the same key after any lost or ambiguous response; reusing a key with different input fails closed. With verification evidence present, a decision is bound to that evidence. Before verification evidence exists, only reject is accepted and no Decision row is recorded. The response distinguishes the durable Decision outcome from the follow-on landing outcome; approval does not bypass required external checks.
Path
| change_idrequired | string | The change id to act on. |
Body
| request_keyrequired | string | Caller-stable idempotency token for this logical decision request. Retry with the same key after any lost or ambiguous response. 1–128 characters |
| verdictrequired | string | one of approve · reject · request_changes |
| evidence_digest | string | The digest you read for an evidence-bound decision. With evidence present, omitting it accepts whatever evidence is current; before evidence exists, only reject is allowed and no Decision row is recorded. at least 1 characters |
| note | string | What convinced you, or what is missing. |
POST/v1/changes/:change_id/land
trunk_land_changePath
| change_idrequired | string | The change id to act on. |
Body
| reason | string | |
| force | boolean | Admin override. Recorded on the change. |
POST/v1/changes/:change_id/ready
trunk_mark_change_readyOnly the Change author may open landing readiness for the exact numerical Change revision supplied by the caller. It creates no evidence and bypasses no policy, approval, check, Preview, freshness, source or publication fence. A stale revision is rejected.
Path
| change_idrequired | string | The change id to act on. |
Body
| revisionrequired | integer | Exact current numerical Change revision being released from Draft. 1 or more |
POST/v1/changes/:change_id/revert
trunk_revert_changeReverts on trunk and attributes the outcome to the change's author. This is what moves an agent's track record, which is what changes how its next change routes.
Path
| change_idrequired | string | The change id to act on. |
Body
| reasonrequired | string | What went wrong. Shown on the author's record. at least 5 characters |
| detected_by | string | Alert, on-call, customer report, canary… |
POST/v1/changes/:change_id/trace
trunk_append_tracePath
| change_idrequired | string | The change id to act on. |
Body
| stepsrequired | object[] | |
| kindrequired | string | What the agent was doing at this step. one of read · search · reason · edit · test · bench · tool · note |
| summaryrequired | string | One line, written for a human skimming the trace. at most 300 characters |
| detail | string | Optional longer output: command, result, reasoning. |
| ts | string | ISO timestamp. Defaults to now. |
| tokens | integer | Tokens consumed by this step. |
| usd | number | Cost of this step in USD. |
What policy could not resolve on evidence alone. If this is empty, everything that landed today landed unattended.
GET/v1/queue
trunk_decision_queueA repository owns services, the routing policy and the trunk log. Services are matched from changed paths, which is how a change finds its owner.
POST/v1/repos/:repo_id/git-credentials
human onlyIssues a short-lived credential for the repository’s Trunk-owned HTTPS Git remote. Read credentials permit clone/fetch. Write credentials additionally return the authenticated actor’s allowed Human Work ref prefix; Trunk landing remains the only normal canonical publication path.
Path
| repo_idrequired | string | The repo id to act on. |
Body
| accessrequired | string | Requested Git authority. one of read · write |
POST/v1/repos/:repo_id/import
trunk_import_repositoryRequires repository-admin authority. Reserves one durable import operation using bounded commit/source/digest/size metadata, or reconciles the same operation after an interrupted raw-pack upload. Bulk Git-pack bytes are uploaded separately to the returned Trunk-owned binary transfer URL and never travel through JSON or MCP.
Path
| repo_idrequired | string | The repo id to act on. |
Body
| commitrequired | string | Pinned Git SHA-1 commit to import. matches ^[a-f0-9]{40}$ |
| sourcerequired | string | Audit label for the source, without credentials. Never fetched by the server. 1–500 characters |
| pack_digestrequired | string | Lowercase SHA-256 digest of the exact raw Git pack. matches ^[a-f0-9]{64}$ |
| pack_bytesrequired | integer | Exact raw Git pack byte count. 32 or more |
GET/v1/repos/:repo_id/import
trunk_get_repository_importRequires repository-admin authority. Returns bounded metadata and the durable import state. This operation never carries Git-pack bytes or performs provider mutation.
Path
| repo_idrequired | string | The repo id to act on. |
GET/v1/repos
trunk_list_reposCreates a repository, its services, and version 1 of its routing policy. Services are how Trunk assigns a change to an owner and a criticality, and they are what track records are measured against — a repository with no services still works, but every change lands as "unassigned" at medium criticality. Human-only: registering repositories is not an agent operation. The creating human becomes repository administrator and check administrator in the durable preparation transaction before provider provisioning begins. CI is disabled by default. A repository admin can enable Trunk-managed CI from Integrations.
Body
| namerequired | string | A single lowercase slug. Repositories are not namespaced. at most 40 characters |
| trunk_branch | string | The branch changes land on. Defaults to "main". at most 60 characters |
| services | object[] | Path globs decide which service a change belongs to. |
| namerequired | string | at most 40 characters |
| criticality | string | one of low · medium · high · critical |
| owner_team | string | at most 60 characters |
| path_globs | string[] | e.g. services/payments/** |
| rules | object[] | Routing policy. Defaults to the built-in rules. |
GET/v1/repos/:repo_id
trunk_get_repoPath
| repo_idrequired | string | The repo id to act on. |
The rules that decide what lands unattended. Policy is versioned, and a candidate can be replayed over historical changes before it is adopted.
GET/v1/repos/:repo_id/policy
trunk_get_policyPath
| repo_idrequired | string | The repo id to act on. |
PUT/v1/repos/:repo_id/policy
human onlyPath
| repo_idrequired | string | The repo id to act on. |
Body
| rulesrequired | object[] | |
| id | string | |
| when | object | |
| then | string | one of auto_land · human_review · owner_review · block |
| reason | string | |
POST/v1/repos/:repo_id/policy/simulate
trunk_simulate_policyAnswers the only question that matters before changing policy: how many of the last N changes would this have auto-landed, and which ones would it have stopped?
Path
| repo_idrequired | string | The repo id to act on. |
Body
| rulesrequired | object[] | |
| limit | integer | 200 or less defaults to 50 |
The code itself, read from whichever Git provider holds it. Trunk lands on one branch and creates no others, so the branch list is the provider’s, not Trunk’s — and a read is only served for a branch that is actually there.
POST/v1/repos/:repo_id/files/read
trunk_read_sourcesReads up to 12 repository files in one call. The named branch is resolved once and every file is read at that immutable head commit. Missing paths are reported separately. Aggregate returned text is capped at 2097152 bytes.
Path
| repo_idrequired | string | The repo id to act on. |
Body
| pathsrequired | string[] | Repository-relative file paths. At most 12. |
| ref | string | Branch name. Defaults to the trunk branch. |
POST/v1/repos/:repo_id/search
trunk_search_sourceBounded literal text search over the provider-neutral repository browser. The named branch is resolved once and the complete traversal reads its immutable head commit. Binary files are ignored. Results may be marked truncated when the result cap, provider truncation, or hard scan bounds are reached.
Path
| repo_idrequired | string | The repo id to act on. |
Body
| queryrequired | string | Literal text to find; regular expressions are not evaluated. 1–200 characters |
| ref | string | Branch name. Defaults to the trunk branch. |
| path | string | Optional repository-relative subtree to search. at most 1024 characters |
| case_sensitive | boolean | defaults to false |
| max_results | integer | 1–50 defaults to 20 |
POST/v1/repos/:repo_id/patch/preflight
trunk_preflight_patchRead-only exact applicability check for an existing unified Git text diff. Trunk captures its current recorded commit SHA once, reads affected paths at that immutable commit, and verifies supported add/modify/delete hunks without fuzzy relocation. The result is convenience evidence for a later trunk_propose_change call using the returned base_commit; it does not reserve, authorize, materialize, or mutate anything. Diff input is capped at 524288 bytes and source reads reuse the bounded multi-read limits. Rename/copy, binary, mode-only and no-trailing-newline forms fail closed.
Path
| repo_idrequired | string | The repo id to act on. |
Body
| diffrequired | string | 1–524288 characters |
GET/v1/repos/:repo_id/branches
trunk_list_branchesTrunk lands on one branch and creates no others, so this is whatever the underlying Git provider actually has. The trunk branch is flagged and listed first. Reads are only served for a branch that appears here.
Path
| repo_idrequired | string | The repo id to act on. |
GET/v1/repos/:repo_id/tree
trunk_list_sourceDirectories first, then files, each alphabetically. Omit `path` for the repository root, and `ref` for the trunk branch. A `ref` naming no branch is a 404 rather than a silent fall back to trunk.
Path
| repo_idrequired | string | The repo id to act on. |
Query
| ref | string | Branch name. Defaults to the trunk branch. |
| path | string | Directory to list. Defaults to the repository root. at most 1024 characters |
GET/v1/repos/:repo_id/file
trunk_read_sourceText arrives as `text`; a blob that is not decodable text comes back with `text: null` and `binary: true`. Large files are truncated to whole lines with `truncated: true`, and `bytes` is always the size of the whole blob.
Path
| repo_idrequired | string | The repo id to act on. |
Query
| pathrequired | string | File to read, from the repository root. at most 1024 characters |
| ref | string | Branch name. Defaults to the trunk branch. |
What landed, in order, with the change and the evidence behind each commit.
GET/v1/repos/:repo_id/trunk
trunk_logPath
| repo_idrequired | string | The repo id to act on. |
Query
| limit | integer | 500 or less defaults to 50 |
The fleet, its scopes and its measured record. Registering an agent and granting it scope are human acts and stay human-only.
GET/v1/agents
trunk_list_agentsGET/v1/agents/:agent_id
trunk_get_agentPath
| agent_idrequired | string | The agent id to act on. |
Human-only. Returns the API key once; it is not retrievable afterwards.
Body
| handlerequired | string | matches ^[a-z][a-z0-9-]{1,30}$ |
| displayrequired | string | |
| model | string | Which model drives this agent. |
| operator | string | The team accountable for it. |
DELETE/v1/agents/:agent_id
human onlyHuman-only. Removes the identity along with its scopes, budget and API keys. Refused with a 409 once the agent has a record — a proposed change, a revert, an attested check run. That history is signed by this actor and policy reads it, so it outlives any wish to tidy the fleet up; revoke the agent's keys instead, which stops it working and leaves what it did readable.
Path
| agent_idrequired | string | The agent id to act on. |
POST/v1/agents/:agent_id/scopes
human onlyPath
| agent_idrequired | string | The agent id to act on. |
Body
| repo_idrequired | string | |
| service | string | defaults to "*" |
| path_globs | string[] | Glob patterns the agent may modify, e.g. services/payments/** |
| ops | string[] | |
PUT/v1/agents/:agent_id/budget
human onlyPath
| agent_idrequired | string | The agent id to act on. |
Body
| changes_per_day | integer | 0 or more |
| usd_per_day | number | 0 or more |
| max_diff_lines | integer | 1 or more |
| max_files | integer | 1 or more |
An agent key is shown once at issue and stored only as a digest. Human-only: an agent cannot widen its own credentials.
GET/v1/agents/:agent_id/keys
human onlyNames and previews only. A key itself is readable exactly once, when it is issued.
Path
| agent_idrequired | string | The agent id to act on. |
POST/v1/agents/:agent_id/keys
human onlyReturns the key once. Only its digest is stored, so it cannot be recovered afterwards — reissue instead. An agent may hold several keys at once, which is how you rotate without downtime: issue the new one, move the agent across, then revoke the old one.
Path
| agent_idrequired | string | The agent id to act on. |
Body
| name | string | What this key is for, e.g. "ci" or "laptop". at most 60 characters |
DELETE/v1/agents/:agent_id/keys/:key_id
human onlyThe key stops authenticating immediately. The record of it is kept, so a key that was used and then taken away stays visible.
Path
| agent_idrequired | string | The agent id to act on. |
| key_idrequired | string | The key id to act on. |
The platform event stream — every proposal, routing decision, landing and revert.
GET/v1/events
trunk_recent_eventsQuery
| limit | integer | 500 or less defaults to 50 |
| since | integer | defaults to 0 |
| change_id | string | |
Throughput and autonomy for a repository: what was proposed, what landed, and how much of it needed a person.
GET/v1/repos/:repo_id/stats
trunk_statsPath
| repo_idrequired | string | The repo id to act on. |
Human sign-up, sign-in and sign-out. Not MCP tools: registering people is not an agent operation.
POST/v1/auth/signup
no credentialhuman onlyCreates a human identity and opens a session. Agents cannot call this: registering people is not an agent operation.
Body
| emailrequired | string | at most 254 characters |
| passwordrequired | string | At least 10 characters. 10–512 characters |
| namerequired | string | Display name, shown on decisions you record. at most 120 characters |
| handle | string | Optional; derived from your name otherwise. matches ^[a-z][a-z0-9-]{1,30}$ |
POST/v1/auth/signin
no credentialhuman onlyBody
| emailrequired | string | |
| passwordrequired | string | |
POST/v1/auth/signout
human onlyBody
| everywhere | boolean | End every session for this account. |
POST/v1/changes/:changeId/ci-dispatches
human onlyManual/admin handoff for a configured Trunk-managed required check after Trunk has materialized and verified an immutable candidate. Normal first-attempt CI is orchestrated automatically; this surface remains human-attester-only for explicit recovery/bootstrap and terminal retry. The requested provider/check is revalidated as a required repository check under the same repository lock that creates the queued CheckRun and durable dispatch intent. The stable managed-CI provider identity is trunk-ci; the backing execution provider is intentionally not part of this public contract. The caller supplies the exact candidate SHA it observed; Trunk binds the request to the current numbered Change revision and uses the same rolling-safe first-attempt identity selection as automatic orchestration, so manual/automatic races converge while distinct numbered revisions can never collapse merely because they resolve to the same candidate SHA. To rerun a terminal non-success provider-owned check on the same revision, retryOf names the exact prior durable dispatch; Core derives exactly one child request identity from that parent, so replay is idempotent and concurrent retries cannot fan out.
Path
| changeIdrequired | string | The changeId to act on. |
Body
| providerrequired | string | Stable Trunk-managed CI provider identity. Use trunk-ci. one of trunk-ci |
| checkNamerequired | string | 1–120 characters |
| revisionrequired | string | 40–64 characters · matches ^[0-9a-fA-F]+$ |
| retryOf | string | 1–200 characters |
POST/v1/repos/:repoId/deployments
human onlyServer-only deployment handoff for an explicitly authorized deployer. Durable deployment intent is committed before provider handling. New Vercel Preview creates resolve the repository's durable Vercel installation/project connection, capture the exact immutable Trunk candidate source proof, and stay durably NOT_STARTED while read-only preflight positively proves that project already has a Production deployment. Only then may Trunk publish STARTED and transport the exact candidate directly to Vercel without a GitHub bridge or Production target. Existing STARTED attempts continue to reconcile against their pinned provider target, including the recovery-only legacy target format used before repository-scoped integrations, and require authoritative provider read-back. Historical unsupported SHA-256 Preview attempts that are still positively pre-mutation are retired to cancelled on replay rather than remaining queued forever; STARTED and terminal historical attempts retain their existing recovery/replay semantics. Terminal idempotent replays return durable state without requiring historical provider credentials. Production remains intent-only and therefore cannot invoke Vercel here.
Path
| repoIdrequired | string | The repoId to act on. |
Body
| revisionrequired | string | matches ^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$ |
| environmentrequired | string | one of preview · production |
| providerrequired | string | matches ^[a-z0-9][a-z0-9._-]{0,63}$ |
| requestKeyrequired | string | 1–200 characters |
| changeId | string | 1–200 characters |
PUT/v1/deployments/:deploymentId
human onlyServer-only provider-neutral result handoff. Any currently authorized deployer for the repository may reconcile the attempt, which permits credential rotation and failover. The deployment domain still enforces monotonic state, immutable terminal outcomes, immutable provider identity/URL and idempotent replay.
Path
| deploymentIdrequired | string | The deploymentId to act on. |
Body
| statusrequired | string | one of queued · in_progress · ready · failed · cancelled |
| externalId | string | at most 200 characters |
| url | string | at most 2000 characters |
| detailsUrl | string | at most 2000 characters |
GET/v1/repos/:repoId/check-requirements
human onlyReturns stable public check identities. Trunk-managed CI is identified as trunk-ci; customer-selected external providers retain their provider identity.
Path
| repoIdrequired | string | The repoId to act on. |
PUT/v1/repos/:repoId/check-requirements
human onlyRepository check-admin policy. Providers report check state; they never decide whether their own check is required for landing. Public signup alone grants no check-admin authority. Trunk-managed CI uses stable trunk-ci/<workflowName> identities; provider-specific managed execution remains internal. Policy storage commits before reconciliation. New managed requirements are activated for already-verified candidate revisions, while only already-approved changes may resume landing. If post-commit managed-CI activation is unavailable, the request reports a provider-neutral retryable error and the saved policy remains authoritative.
Path
| repoIdrequired | string | The repoId to act on. |
Body
| requiredrequired | object[] | |
| namerequired | string | 1–120 characters |
| providerrequired | string | matches ^[a-z0-9][a-z0-9._-]{0,63}$ |
GET/v1/changes/:changeId/checks
human onlyPath
| changeIdrequired | string | The changeId to act on. |
POST/v1/changes/:changeId/checks
human onlyBootstrap surface for human-attested or non-integrated check providers. The Trunk-managed CI identity is reserved for its authenticated handoff/result path, so a human cannot manufacture a later managed attempt that supersedes provider-owned evidence. Genuine customer-selected or non-managed providers retain their own provider identity. Other checks require repository check-attester authority and reject stale revisions.
Path
| changeIdrequired | string | The changeId to act on. |
Body
| namerequired | string | 1–120 characters |
| providerrequired | string | matches ^[a-z0-9][a-z0-9._-]{0,63}$ |
| revisionrequired | string | 1–128 characters |
| externalId | string | Provider run id for idempotent creation/replay. at most 200 characters |
| detailsUrl | string | at most 2000 characters |
PUT/v1/checks/:checkId
human onlyCheck state is monotonic. A completed conclusion is immutable; replaying the same update is idempotent and emits no duplicate event. Only the explicitly repository-authorized actor that created the attestation may update it, and CheckRuns bound to external-CI dispatches are provider-owned and rejected by this human surface.
Path
| checkIdrequired | string | The checkId to act on. |
Body
| statusrequired | string | one of queued · in_progress · completed |
| conclusion | string | one of success · failure · cancelled · timed_out · skipped |
| detailsUrl | string | at most 2000 characters |