Skip to main content
This guide moves v1 API automation, such as submitting pull requests from CI, pausing queues, or building dashboards, to v2. Every v1 capability has a v2 equivalent, though a few take more requests. Most migrations come down to four changes: a new credential, a new base URL, IDs instead of repository coordinates, and camelCase enum values. Response parsing changes too.

Migrate with an agent

To have a coding agent do the migration, run it from the repository that calls v1 and give it this prompt:
You can also open this page in Claude or ChatGPT from the page menu, or copy it as Markdown. Every page on this site is available as Markdown by appending .md to its URL, and each endpoint’s Markdown includes its OpenAPI definition.

What changes

Step 1: Create an API key

An organization admin creates API keys in the Trunk app: navigate to Settings → Developer → API Keys and click the Create Key button. Store the key right away, since it’s shown only once. See Create an API key for details. Grant the permissions your v1 calls need: If CI only uploads impacted targets from GitHub Actions, it doesn’t need a key. Log in with the run’s own credential instead, which also works on fork pull requests.

Step 2: Switch the credential and base URL

Step 3: Resolve IDs once

v2 addresses queues and pull requests by ID. IDs don’t change, so look each one up once and store it. Queue and repository IDs. List your organization’s queues. Match each queue’s repositoryUrl and targetBranch against what your v1 code sends, and keep its id and repositoryId. Pull request lookups need the repositoryId:
If hasMore is true, follow nextCursor, or narrow the list to one repository. Get its ID from GET /v2/repositories?providerUrl=https://github.com/acme/widgets (.data[0].id), then call GET /v2/merge-queues?repositoryId=<id>&targetBranch=main. Pull request ID. Resolve one number, or up to 50 comma-separated numbers (set limit=50):
Submit takes the pull request’s number directly, and its response includes the id for polling.

Endpoint map

Examples

Automate queue actions

Submit a pull request with a priority, from CI:
v1’s priority took either a number or a name. In v2, send a number as priorityValue or a name as priorityName. Pause a queue during an incident, and resume it afterward:
Check whether a pull request merged:

Build dashboards

Prometheus. Change your scrape job’s URL and credential, and replace any repo/repoHost parameters with repositoryId. Metric names, labels, and label values are unchanged, including snake_case state values such as tests_passed, so dashboards and alerts keep working. Don’t apply this guide’s rename tables to PromQL queries or alert rules. Prefer one organization-wide target over one per queue (see Scrape configuration):
Completed testing over a time range. Page through testing-results with nextCursor. A request covers at most 45 days, which is also how long results are kept. Each row carries pullRequestNumber, testingDurationMs, outcome, and the verificationRunId for its details:
Pull requests that merged or failed in a window. concludedAfter and concludedBefore replace v1’s since and until:

Renamed values

v2 enum values are camelCase. Some drop or change words, so map them with these tables, not by changing case.

Queue entry states

v1 accepted either case. v2 accepts only the camelCase value and rejects any other spelling, including TESTS_PASSED and tests_passed, with 400.

Queues

State switching_modes becomes switchingModes; running, paused, and draining are unchanged. Mode single becomes queue, and parallel becomes graph. Sending a v1 mode spelling to create or switch-modes returns 400, but v2 can’t catch a swap. queue and graph are both valid, so a wrong mapping switches the queue to the other mode.

Pull request records

v1’s pull request records (getSubmittedPullRequest, and the entries of listPullRequests and getQueue) were flat. v2 puts the pull request’s fields at the top level, where id is the pull request’s ID, and its place in the queue under queueEntry. isCurrentlySubmittedToQueue is unchanged. state, stateChangedAt, priorityValue, priorityName, usedDefaultPriorityName, and readiness move under queueEntry, with the renames below. priorityValue and priorityName are optional in v2. v2’s top-level state is the pull request’s state on GitHub, not its queue state. The new queueEntry.readiness.mergeabilityDetails.reasons[] explains the mergeability. Its code values include all of blockReasons’ codes below, plus others such as draft and closed.

Timelines

v1’s timeline is now the queue-entry history. Its shape changes as well as its values, so update the parser field by field:

Testing results

Verification runs

v1’s testing details are a v2 verification run:
  • checkSuites[].checkRuns[] and statusChecks[] become one flat checks list. Each check’s kind says which it was, and GitHub’s raw values are in providerStatus and providerConclusion. A status check’s context is its name.
  • dependentPrs, a list of numbers, becomes dependentPullRequests, a list of objects with id, number, sha, title, and url.
  • Each of testedPullRequests carries id, number, sha, and url in place of v1’s prNumber and prUrl.
  • impactedTargets and impactedTargetsForTestedPrs become impactedTargets.all and impactedTargets.testedPullRequestsOnly. Each is an object with impactsAllTargets, impactedTargets, and totalImpactedTargets, not a list.
  • status and requiredStatusesSource values are camelCase.

Renamed settings

PATCH /v2/merge-queues/{id}/config takes the same settings as v1’s updateQueue, with these changes: The same names appear under config when you read a queue. v2 also bounds values v1 accepted without limit, such as concurrency (at most 1000) and list settings (at most 100 entries), so a queue configured past a bound through v1 can’t be written back unchanged.

Behavior changes

  • Resubmitting doesn’t update a queued pull request. v1’s submitPullRequest rewrote the priority and batching of a pull request already in the queue. v2 returns 409 PR_ALREADY_ENQUEUED and changes nothing. Use POST /v2/pull-requests/{id}/reprioritize or PATCH /v2/pull-requests/{id}/queue-entry instead.
  • requiredStatuses: [] clears the list. In v1 an empty list was ignored, and you cleared it with deleteRequiredStatuses: true.
  • The queue’s pull requests come in three lists. v1’s getQueue returned one enqueuedPullRequests array. /order returns:
    • batches, each with a verification run, a state, and its pullRequests.
    • pending, admitted entries in the order the queue will consider them, each holding its pull request in pullRequest.
    • awaitingAdmission, pull requests, truncated. awaitingAdmissionTotal is the full count.
    Take the pull requests from each, in that order, to rebuild v1’s list. Pull requests that aren’t ready yet are in none of them, as in v1. Each entry carries only the pull request’s id, number, title, sha, and url. For state, priority, or author, match by id against GET /v2/merge-queues/{id}/pull-requests?state=pending,testing,testsPassed,pendingFailure.
  • List order is not queue order. GET /v2/merge-queues/{id}/pull-requests is ordered by when each pull request was first submitted, newest first. Use /order for queue position.
  • Lookups don’t tell you “not in this queue”. v1’s getSubmittedPullRequest returned 404, and getSubmittedPullRequests a notFound list, for pull requests never submitted to that queue. GET /v2/pull-requests finds any pull request Trunk knows in the repository. Treat a result with no queueEntry, or whose queueEntry.mergeQueueId is another queue, as not found. For a batch, also compare the numbers you sent with the ones returned.
  • Timelines default to oldest first. Send direction=newest for v1’s order, where the current state comes first.
  • Errors have one shape, but not every response has it. Switch on error.code when the body has an error object. 401, 403 for some credential problems, 429, and 503 come from Trunk’s edge, and the API answers 413 and 504 as plain text. These may lack the error object, so branch on their status code. 403 INSUFFICIENT_PERMISSIONS means the key lacks a permission. See Errors.
  • Read back before retrying a write. v2 doesn’t honor Idempotency-Key, so a blind retry can, for example, restart a pull request’s tests twice. Reads are safe to retry. See Retrying.
  • Pull request reads have their own, lower limit. GET requests on paths starting with /v2/pull-requests are limited to 5 per second per API key or user. Most other requests share 100 per second per organization. A v1 loop that polls many pull requests may need to slow down. See Rate limits.

Finding a verification run

A testRunId stored from v1 is the same ID in v2. If it ends in v1’s -bisection suffix, remove the suffix, which v2 doesn’t accept. What remains is that bisection run’s own ID. Otherwise, take the ID from a response you already have:
  • verificationRunId on a testing result, a queue-entry history transition, or a batch in /order
  • queueEntry.verifiedByVerificationRunId on a merged pull request
  • id on each run from a pull request’s or a queue’s verification runs

Checklist

  • An API key exists with the permissions your calls need, stored as a secret.
  • No Merge Queue request uses api.trunk.io/v1 or the x-api-token header. Other v1 calls, such as Flaky Tests and test-result uploads, are unchanged.
  • Queue and pull request IDs are resolved once and stored, not looked up per request.
  • Every enum value comes from the tables above, including values stored in your own database or config.
  • Every list request sets limit and follows nextCursor until hasMore is false, with the other parameters unchanged, including batch lookups.
  • Response parsing reads queue fields from queueEntry, and no code stores the pull request’s id where it stored v1’s queue entry id.
  • Timeline and testing-result parsing uses the v2 field names above.
  • notReadyTimeoutHours values are converted to minutes.
  • Code that resubmitted a queued pull request to change its priority now calls reprioritize or PATCH …/queue-entry.
  • Code that sent requiredStatuses: [] meant to clear the list.
  • Error handling switches on error.code when the body has an error object, otherwise on the status code, and treats 404 as possibly “not yours”.