> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trunk.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from the v1 API

> Move Merge Queue automation and dashboards from the v1 API to v2, endpoint by endpoint.

This guide moves [v1 API](https://docs.trunk.io/merge-queue/reference/merge) automation, such as submitting pull requests from CI, pausing queues, or building dashboards, to [v2](https://docs.trunk.io/merge-queue/reference/api-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:

```text theme={"system"}
Migrate this codebase's Trunk Merge Queue API calls from v1 (https://api.trunk.io/v1) to v2. v2 requests go to https://api.trunk.io, and every v2 path already starts with /v2, so never add /v2 to the host as well.

1. Read the migration guide, https://docs.trunk.io/merge-queue/reference/api-v2/migrate-from-v1.md, and the API overview, https://docs.trunk.io/merge-queue/reference/api-v2.md, which covers errors, retrying, rate limits, and polling.
2. Find every v1 Merge Queue call in this codebase. Look for api.trunk.io/v1, the x-api-token header, and the v1 endpoint names in the guide's endpoint map. Leave other v1 calls, such as /v1/flaky-tests/..., and the Trunk Analytics CLI's organization API token as they are. They aren't part of this migration.
3. Port each call using the endpoint map. Before writing each v2 request, read that endpoint's reference page as Markdown for the exact parameters and response fields. For a link to docs.trunk.io, whether relative (/merge-queue/...) or absolute (https://docs.trunk.io/...), remove any #fragment, add .md unless the path already ends in .md, and fetch it from https://docs.trunk.io. A link that is only #… is a section of the page you're reading. Links to other hosts, such as app.trunk.io or github.com, aren't documentation, so don't fetch them. If a docs.trunk.io fetch doesn't return Markdown with status 200, or returns "Page Not Found", the path is wrong.
4. Port response parsing, not just requests. v1's flat pull request record is split in v2: pull request fields at the top level, queue fields under queueEntry, and id now identifies the pull request. Timelines and testing results also change shape. Use the guide's "Renamed values" section.
5. Use the guide's tables for enum values and renamed fields. Never derive a v2 enum value by changing the case of a v1 value.
6. Every v2 list is paginated with a default page size of 25. Set limit explicitly (up to 100) and follow nextCursor until hasMore is false, including for batch lookups of up to 50 IDs.
7. Resolve IDs once and reuse them, rather than looking them up on every request. For a fixed queue, read its ID from configuration, or resolve it once at startup with GET /v2/merge-queues?repositoryId=…&targetBranch=…. The organization API token works only with the v1 API, so read an API key from a new secret, such as TRUNK_API_KEY.
8. When you're done, tell the user to create that API key, list the permissions it needs from the guide's permissions table, and work through the guide's checklist.
```

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

| | v1 | v2 |
| - | - | - |
| URLs | `https://api.trunk.io/v1/<endpoint>`, e.g. `/v1/getQueue` | `https://api.trunk.io` plus a path that already starts with `/v2`, e.g. `/v2/merge-queues/{id}` |
| Credential | The organization API token, in the `x-api-token` header | An API key, sent as `Authorization: Bearer <key>`. The organization API token works only with the v1 API. |
| Addressing | `repo` + `targetBranch` + `pr.number` in every request body | IDs in the path: a queue ID, a pull request ID |
| Methods | `POST` for everything except metrics | `GET` to read, `POST`/`PATCH`/`PUT`/`DELETE` to change |
| Pull request records | One flat record whose `id` is the queue entry | Pull request fields at the top level, whose `id` is the pull request; queue fields under `queueEntry` |
| Enum values | `snake_case` (`tests_passed`, `merge_commit`), and uppercase was accepted | camelCase only (`testsPassed`, `mergeCommit`) |
| Lists | Endpoint-specific `take`, `offset`, or `cursor` | One envelope: `data`, `nextCursor`, `hasMore`, with `limit` (default 25, up to 100) and `cursor` |
| Errors | Varies by endpoint | `{ "error": { "code", "message" } }` from the API; responses from Trunk's edge may differ |

## Step 1: Create an API key

An organization admin creates API keys in the [Trunk app](https://app.trunk.io): 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](https://docs.trunk.io/merge-queue/reference/api-v2/authentication.md#create-an-api-key) for details.

Grant the permissions your v1 calls need:

| If your code calls | The key needs |
| - | - |
| Only reads: `getSubmittedPullRequest(s)`, `listPullRequests`, `getQueue`, `getMergeQueueTestingDetails`, `listMergeQueueTestingResults`, `getMergeQueueMetrics` | No permissions |
| `submitPullRequest`, `cancelPullRequest`, `restartTestsOnPullRequest`, `setImpactedTargets` | **Merge and cancel PRs** |
| `submitPullRequest` with `priority`; in v2, also `reprioritize`, or `PATCH …/queue-entry` with a priority | **Set priority on PRs** |
| `submitPullRequest` with `noBatch: true`; in v2, also `PATCH …/queue-entry` with `allowBatching` | **Skip batching** |
| `direct` on a v2 submit or queue-entry update (new in v2) | **Merge PRs with direct to queue** |
| `createQueue`, `updateQueue`, `deleteQueue` | **Update Merge Queue settings** |

If CI only uploads impacted targets from GitHub Actions, it doesn't need a key. [Log in with the run's own credential](https://docs.trunk.io/merge-queue/reference/api-v2/github-actions.md) instead, which also works on fork pull requests.

## Step 2: Switch the credential and base URL

```bash v1 theme={"system"}
curl -X POST https://api.trunk.io/v1/getQueue \
  -H "Content-Type: application/json" \
  -H "x-api-token: $TRUNK_TOKEN" \
  -d '{ "repo": { "host": "github.com", "owner": "acme", "name": "widgets" }, "targetBranch": "main" }'
```

```bash v2 theme={"system"}
curl https://api.trunk.io/v2/merge-queues/xK9mP2nQ \
  -H "Authorization: Bearer $TRUNK_API_KEY"
```

## 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`:

```bash theme={"system"}
curl "https://api.trunk.io/v2/merge-queues?limit=100" \
  -H "Authorization: Bearer $TRUNK_API_KEY" |
  jq '.data[] | select(.repositoryUrl == "https://github.com/acme/widgets" and .targetBranch == "main") | {id, 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`](https://docs.trunk.io/merge-queue/reference/api-v2/repositories/resolve-a-repository-by-its-provider-url) (`.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`):

```bash theme={"system"}
curl "https://api.trunk.io/v2/pull-requests?repositoryId=$REPOSITORY_ID&number=4901" \
  -H "Authorization: Bearer $TRUNK_API_KEY" |
  jq -r '.data[0].id'
```

Submit takes the pull request's `number` directly, and its response includes the `id` for polling.

## Endpoint map

| v1 | v2 | Notes |
| - | - | - |
| `POST /submitPullRequest` | [`POST /v2/merge-queues/{id}/pull-requests`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/submit-a-pull-request-to-a-merge-queue) | Body: `number`, plus optional `priorityValue` or `priorityName`, `allowBatching`, `direct`. Resubmitting a queued pull request returns `409 PR_ALREADY_ENQUEUED` instead of updating it (see [behavior changes](#behavior-changes)). |
| `POST /cancelPullRequest` | [`POST /v2/pull-requests/{id}/dequeue`](https://docs.trunk.io/merge-queue/reference/api-v2/pullrequests/dequeue-a-pull-request) | An entry that already merged, failed, or was cancelled returns `409 PR_NOT_IN_ACTIVE_STATE`. |
| `POST /restartTestsOnPullRequest` | [`POST /v2/pull-requests/{id}/restart-tests`](https://docs.trunk.io/merge-queue/reference/api-v2/pullrequests/restart-a-queued-pull-requests-tests) | |
| `POST /getSubmittedPullRequest` | [`GET /v2/pull-requests?repositoryId=…&number=…`](https://docs.trunk.io/merge-queue/reference/api-v2/pullrequests/resolve-pull-requests-by-repository-coordinates), then [`GET /v2/pull-requests/{id}`](https://docs.trunk.io/merge-queue/reference/api-v2/pullrequests/get-a-pull-request) | By head branch: `?repositoryId=…&branch=…&baseBranch=…`. The queue state is `queueEntry.state`. A pull request that was never submitted is still found, without `queueEntry` (see [behavior changes](#behavior-changes)). |
| `POST /getSubmittedPullRequest` with `includeTimeline` | [`GET /v2/pull-requests/{id}/queue-entry-history?direction=newest`](https://docs.trunk.io/merge-queue/reference/api-v2/pullrequests/list-a-pull-requests-queue-entry-state-history) | `direction=newest` gives v1's order. The default is oldest first. |
| `POST /getSubmittedPullRequest` with `includeEstimatedTimeToMerge` | [`GET /v2/pull-requests/{id}/queue-entry-estimate`](https://docs.trunk.io/merge-queue/reference/api-v2/pullrequests/estimate-when-a-queued-pull-request-will-merge) | Values are camelCase (`pendingFailureResolution`, `pastTypical`). A queue without estimates answers `404 ESTIMATE_NOT_ENABLED` (v1: `status: unavailable`, `not_enabled`). A merged, failed, or cancelled pull request answers `409 PR_NOT_IN_ACTIVE_STATE` (v1: `concluded`). |
| `POST /getSubmittedPullRequests` | [`GET /v2/pull-requests?repositoryId=…&number=1,2,3`](https://docs.trunk.io/merge-queue/reference/api-v2/pullrequests/resolve-pull-requests-by-repository-coordinates) | Up to 50 numbers. Set `limit=50` (the default page is 25) and still follow `nextCursor`. v1 also took head branches in the same batch; v2 takes one `branch` per request. There is no `notFound` list (see [behavior changes](#behavior-changes)). |
| `POST /getSubmittedPullRequests` with `includeTimeline` | [`GET /v2/queue-entry-history?pullRequestIds=…&direction=newest`](https://docs.trunk.io/merge-queue/reference/api-v2/pullrequests/list-queue-entry-state-history-for-several-pull-requests) | Up to 50 pull request IDs, each with its complete timeline. `limit` counts pull requests and defaults to 25, so set `limit=50` to get them in one call. |
| `POST /listPullRequests` | [`GET /v2/merge-queues/{id}/pull-requests`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/list-a-merge-queues-pull-requests) | `state` takes comma-separated camelCase values. `since`/`until` become `concludedAfter`/`concludedBefore`. `take` becomes `limit`. |
| `POST /getQueue` (state and configuration) | [`GET /v2/merge-queues/{id}`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/get-a-merge-queue) | Configuration is under `config`. |
| `POST /getQueue` (`enqueuedPullRequests`) | [`GET /v2/merge-queues/{id}/order`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/get-a-merge-queues-dispatch-order) | Split into `batches`, `pending`, and `awaitingAdmission`, with only each pull request's ID, number, title, head commit, and URL (see [behavior changes](#behavior-changes)). |
| `POST /updateQueue` (settings) | [`PATCH /v2/merge-queues/{id}/config`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/update-a-merge-queues-configuration) | Send only the settings you change; any other key, including `repo` or `targetBranch`, is a `400`. See [renamed settings](#renamed-settings). |
| `POST /updateQueue` with `state` | [`POST /v2/merge-queues/{id}/pause`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/pause-a-merge-queue), [`/resume`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/resume-a-merge-queue), or [`/drain`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/drain-a-merge-queue) | `running` is `/resume`. |
| `POST /updateQueue` with `mode` | [`POST /v2/merge-queues/{id}/switch-modes`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/switch-a-merge-queues-mode) | `single` is `queue`; `parallel` is `graph`. |
| `POST /createQueue` | [`POST /v2/merge-queues`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/create-a-merge-queue) | Body: `repositoryId`, `targetBranch`, and optional settings such as `mode` (`queue` or `graph`) and `concurrency`. |
| `POST /deleteQueue` | [`DELETE /v2/merge-queues/{id}`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/delete-a-merge-queue) | The queue must still be empty. |
| `POST /getMergeQueueTestingDetails` | [`GET /v2/merge-queue-verification-runs/{id}`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeueverificationruns/get-a-verification-run) | v1's test run is a verification run, with the same ID (see [finding a verification run](#finding-a-verification-run)). |
| `POST /listMergeQueueTestingResults` | [`GET /v2/merge-queues/{id}/testing-results?since=…&until=…`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/list-a-merge-queues-completed-testing) | At most 45 days per request. `offset`/`take` become `cursor`/`limit`. Result fields are renamed (see [testing results](#testing-results)). |
| `GET /getMergeQueueMetrics` | [`GET /v2/merge-queues/metrics`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/get-prometheus-metrics-for-every-merge-queue) or [`GET /v2/merge-queues/{id}/metrics`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/get-prometheus-metrics-for-a-merge-queue) | Same metric names, labels, and label values. The `repo` and `repoHost` query parameters become `repositoryId`. Metrics have their own limit of 5 requests per second per organization. |
| `POST /setImpactedTargets` | [`PUT /v2/impacted-targets`](https://docs.trunk.io/merge-queue/reference/api-v2/impactedtargets/upload-a-pull-requests-impacted-targets) | Body: `repositoryId`, `targetBranch`, `number`, `sha`, and `impactedTargets`, or `impactsAllTargets: true` instead of `"ALL"`. Returns `202`. |
| `POST /setImpactedTargets` from a fork, with `x-forked-workflow-run-id` | [GitHub Actions login](https://docs.trunk.io/merge-queue/reference/api-v2/github-actions.md) | No header or secret: `trunk-io/login` authenticates the fork's run. |

## Examples

### Automate queue actions

Submit a pull request with a priority, from CI:

```bash v1 theme={"system"}
curl -X POST https://api.trunk.io/v1/submitPullRequest \
  -H "Content-Type: application/json" \
  -H "x-api-token: $TRUNK_TOKEN" \
  -d '{
    "repo": { "host": "github.com", "owner": "acme", "name": "widgets" },
    "targetBranch": "main",
    "pr": { "number": 4901 },
    "priority": "high"
  }'
```

```bash v2 theme={"system"}
curl -X POST https://api.trunk.io/v2/merge-queues/$QUEUE_ID/pull-requests \
  -H "Authorization: Bearer $TRUNK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "number": 4901, "priorityName": "high" }'
```

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:

```bash v1 theme={"system"}
curl -X POST https://api.trunk.io/v1/updateQueue \
  -H "Content-Type: application/json" \
  -H "x-api-token: $TRUNK_TOKEN" \
  -d '{ "repo": { "host": "github.com", "owner": "acme", "name": "widgets" }, "targetBranch": "main", "state": "paused" }'
```

```bash v2 theme={"system"}
curl -X POST https://api.trunk.io/v2/merge-queues/$QUEUE_ID/pause \
  -H "Authorization: Bearer $TRUNK_API_KEY"

curl -X POST https://api.trunk.io/v2/merge-queues/$QUEUE_ID/resume \
  -H "Authorization: Bearer $TRUNK_API_KEY"
```

Check whether a pull request merged:

```bash theme={"system"}
curl https://api.trunk.io/v2/pull-requests/$PULL_REQUEST_ID \
  -H "Authorization: Bearer $TRUNK_API_KEY" |
  jq -r '.queueEntry.state'   # notReady, pending, testing, testsPassed, pendingFailure, merged, failed, or cancelled
```

### 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](https://docs.trunk.io/merge-queue/administration/metrics.md#scrape-configuration)):

```yaml theme={"system"}
scrape_configs:
  - job_name: trunk-merge-queue
    scrape_interval: 60s
    scheme: https
    static_configs:
      - targets: ['api.trunk.io']
    metrics_path: /v2/merge-queues/metrics
    authorization:
      type: Bearer
      credentials_file: /etc/prometheus/trunk-api-key
```

**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:

```bash theme={"system"}
curl "https://api.trunk.io/v2/merge-queues/$QUEUE_ID/testing-results?since=2026-09-01T00:00:00Z&until=2026-09-08T00:00:00Z&limit=100" \
  -H "Authorization: Bearer $TRUNK_API_KEY"
```

**Pull requests that merged or failed in a window.** `concludedAfter` and `concludedBefore` replace v1's `since` and `until`:

```bash theme={"system"}
curl "https://api.trunk.io/v2/merge-queues/$QUEUE_ID/pull-requests?state=merged,failed&concludedAfter=2026-09-01T00:00:00Z&limit=100" \
  -H "Authorization: Bearer $TRUNK_API_KEY"
```

## 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 | v2 |
| - | - |
| `not_ready` | `notReady` |
| `pending` | `pending` |
| `testing` | `testing` |
| `tests_passed` | `testsPassed` |
| `pending_failure` | `pendingFailure` |
| `merged` | `merged` |
| `failed` | `failed` |
| `cancelled` | `cancelled` |

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.

| v1 | v2 |
| - | - |
| `id` | `queueEntry.id` |
| `prNumber` | `number` |
| `prTitle` | `title` |
| `prSha` | `sha` |
| `prBaseBranch` | `baseBranch` |
| `prAuthor` | `author` |
| `skipTheLine` | `queueEntry.skipsTheLine`, read-only. Send `priorityValue: 0` to skip the line |
| `forceEnqueued` | `queueEntry.direct` |
| `verifiedByTestRun.testRunId` (`verifiedByTestRun` was an object) | `queueEntry.verifiedByVerificationRunId`, a string ID. v1's `testBranchSha` has no equivalent here; read it from the verification run |
| `noBatch` | `queueEntry.allowBatching`, inverted: `noBatch: true` is `allowBatching: false` |
| `readiness.gitHubMergeability` | `queueEntry.readiness.mergeability`: `unspecified`, `inProgress`, `mergeable`, or `notMergeable` |

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](https://docs.trunk.io/merge-queue/reference/api-v2/pullrequests/list-a-pull-requests-queue-entry-state-history.md). Its shape changes as well as its values, so update the parser field by field:

| v1 | v2 |
| - | - |
| `timestamp` | `changedAt` |
| `failure` | `failed` |
| Each state's `reason` and `actor` (`notReady.reason`, `cancellation.reason`, `merged.actor`, …) | `reason` and `actor` on the transition itself. The `cancellation` and `merged` objects are gone. |
| `testRun` (branch, sha, status, required statuses, tested pull requests) and `testsPassed.testRunId`, `testBranchSha`, `testBranchUrl` | `verificationRunId` on the transition. Read the run with [`GET /v2/merge-queue-verification-runs/{id}`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeueverificationruns/get-a-verification-run), one request per run. |
| `failureCausingStatuses` | `failureCausingChecks` |
| `notReady.noBatch` | `notReady.allowBatching` (inverted) |
| `prNumber` and `prUrl` in `waitingFor`, `otherTestedPullRequests`, `prerequisitePullRequests`, `conflictCulprit` | `number` and `url`, plus the pull request's `pullRequestId` |
| `blockReasons[].kind`: `behind_base`, `merge_conflict`, `failing_required_check`, `awaiting_required_check`, `review_required`, `changes_requested`, `stack_needs_rebase`, `undetermined` | `blockReasons[].code`: `behind`, `conflict`, `failingCheck`, `awaitingCheck`, `reviewRequired`, `changesRequested`, `stackNeedsRebase`, `other`. Each reason also has a `message`. `have`, `need`, `check`, and `ontoPrNumber` keep their names; v1's `status` and `state` are gone |
| `conflictCulprit.type`: `queue_pr`, `base_branch`, `stacked_pr` | `conflictCulprit.kind`: `queuePullRequest`, `baseBranch`, `stackedPullRequest` |
| `testsPassed.successorMergeItemId` | `testsPassed.successorPullRequestId`, a pull request ID, not a queue entry ID |
| `reason` values, lowercase (`prerequisite_merge_item_failed`) | Uppercase (`PREREQUISITE_MERGE_ITEM_FAILED`) |

### Testing results

| v1 `listMergeQueueTestingResults` | v2 `testing-results` |
| - | - |
| `testingResults` | `data` |
| `prNumber` | `pullRequestNumber` |
| `cycleConclusion` | `entryConclusion` |
| `bisection: "in_bisection"` | `isBisection: true` |
| `bisection: "entered_bisection"` | `startedBisection: true` (`isBisection` may also be `true`) |
| `testRunId` | `verificationRunId` |
| `totalItems` | `total`, only when you send `includeTotal=true` |

### 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:

| v1 | v2 |
| - | - |
| `batch` | `allowBatching` |
| `notReadyTimeoutHours: n` | `notReadyTimeoutMinutes: n * 60`, a multiple of 60 |
| `commentsEnabled`, `commandsEnabled` | `areCommentsEnabled`, `areCommandsEnabled` |
| `labelCommandsEnabled`, `stateLabelsEnabled` | `areLabelCommandsEnabled`, `areStateLabelsEnabled` |
| `statusCheckEnabled`, `extensionEnabled` | `isStatusCheckEnabled`, `isExtensionEnabled` |
| `createPrsForTestingBranches` | `createsPrsForTestingBranches` |
| `deleteRequiredStatuses: true` | `requiredStatuses: []` |
| `mergeMethod`: `merge_commit` | `mergeCommit` |
| `optimizationMode`: `bisection_skip_redundant_tests` | `bisectionSkipRedundantTests` |
| `testBranchConstructionMode`: `rename_temp_branch`, `push_new_branch_from_temp` | `renameTempBranch`, `pushNewBranchFromTemp` |
| `state`, `mode` | Their own endpoints (see the [endpoint map](#endpoint-map)) |

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`](https://docs.trunk.io/merge-queue/reference/api-v2/pullrequests/reprioritize-a-queued-pull-request) or [`PATCH /v2/pull-requests/{id}/queue-entry`](https://docs.trunk.io/merge-queue/reference/api-v2/pullrequests/update-a-queued-pull-requests-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`](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/list-a-merge-queues-pull-requests).
* **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](https://docs.trunk.io/merge-queue/reference/api-v2#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](https://docs.trunk.io/merge-queue/reference/api-v2#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](https://docs.trunk.io/merge-queue/reference/api-v2#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](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/list-a-merge-queues-completed-testing.md), 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](https://docs.trunk.io/merge-queue/reference/api-v2/pullrequests/list-a-pull-requests-verification-runs.md) or [a queue's](https://docs.trunk.io/merge-queue/reference/api-v2/mergequeues/list-a-merge-queues-verification-runs.md) verification runs

## Checklist

* [ ] An API key exists with the [permissions](#step-1-create-an-api-key) 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".
