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

# Migrate to API v2

> Endpoint mappings, field changes, and behavior differences for moving v1 read integrations to Public API v2.

This guide will help you understand how to migrate from Private API v1 to API v2. You can also share the prompt below with your AI agent to make the migration smoother.

<Prompt description="Migrate this project's Nonce API v1 integration to API v2" actions={["copy"]}>
  Migrate the Nonce Private API v1 read integration in \[client or module] to Public API v2. If no target is specified, inspect the entire project.

  References:

  * Migration guide: [https://docs.nonce.app/api-guide/v2/migration](https://docs.nonce.app/api-guide/v2/migration)
  * Compatibility notes: [https://docs.nonce.app/api-guide/v2/migration-compatibility](https://docs.nonce.app/api-guide/v2/migration-compatibility)
  * API Reference: [https://docs.nonce.app/api-reference/v2/workspaces/list-workspaces](https://docs.nonce.app/api-reference/v2/workspaces/list-workspaces)
  * OpenAPI contract: [https://docs.nonce.app/api-reference/openapi-v2.json](https://docs.nonce.app/api-reference/openapi-v2.json)
    Read the guide, compatibility notes, and available endpoint contracts before changing code. Verify the fields and behavior the existing code actually consumes. If an endpoint has no full Reference, use fields and behavior explicitly documented in the migration guide; flag only required details that remain unconfirmed. If a documentation page cannot be read, retry its URL with .md appended. Resolve field questions from the endpoint Reference or OpenAPI contract before asking the developer. If the needed information is still unavailable, report what is missing and do not change code that depends on it.

  Find every v1 call site across the HTTP client, shared wrappers, proxies, configuration, call sites, and data handling code. Search for /private-api/v1 and dynamically built paths, including /miner/latest and /miners/latest. List the purpose, filters, pagination, and consumed response fields for each call; include writes and other calls not covered by this read-migration guide as unresolved items.

  Apply the documented endpoint and field mappings while preserving existing behavior. Check workspace and farm scope, archived farms, complete pagination, exact MAC matching, legacy filters that were ignored, and strict last\_updated\_at.gt/lt boundaries. For historical queries, preserve time zones, explicit granularity, half-open windows, range limits, result order, and null values. For task batches, retrieve all task pages and distinguish unsuccessful\_count from failed\_count. Keep metric units unchanged.

  Use existing tests and fixtures to verify the affected behavior, adding focused tests where coverage is missing. Cover empty results, multiple pages, matching and boundary cases relevant to the integration, and 401/403/404 responses; errors must not become empty successes. Run the project's relevant checks and report any checks that could not run. Do not operate real devices, expose credentials, or deploy.

  Use only documented replacement fields and endpoints. Note which beta endpoints are used. Where a required field, permission, or business meaning has no confirmed replacement, record the affected feature and continue with independent work. Do not remove features, silently leave a v1 fallback, or substitute empty values to hide differences.

  Finally, search again for v1 paths, shared base URLs, and old response-field usage. Classify every remaining match as non-runtime material or an unresolved migration item. Report changed files, migrated calls, actual validation results, and open items. End with an explicit status: complete or incomplete. Use incomplete if required behavior is missing, runtime v1 dependencies remain, any required contract detail is unconfirmed, or relevant checks have not passed. Do not describe an incomplete migration as "complete pending confirmation".
</Prompt>

The resulting code still needs review.

This guide covers moving Private API v1 read integrations to Public API v2. Resource paths, query scope, pagination, and some response fields change. Valid workspace API keys, Bearer authentication, and the outer `success`, `data`, and `error` response fields stay the same.

<Warning>
  Private API v1 is a legacy interface and will be retired later. In v2, the workspace, classification metrics, and historical metrics endpoints are beta and may still receive breaking changes.
</Warning>

Examples use `https://api.nonce.app` as the base URL. The v1 paths below omit the `/private-api/v1/{workspace_id}` prefix; `/me` is the root path `/private-api/v1/me`.

| v1 call                                                     | v2 endpoint                                               |
| ----------------------------------------------------------- | --------------------------------------------------------- |
| `GET /me`                                                   | [`ListWorkspaces`](#workspaces)                           |
| `GET /farms`                                                | [`ListFarms`](#farms)                                     |
| `GET /agents`                                               | [`ListAgents`](#agents)                                   |
| `GET /farms/{farm_id}/miner/latest` (also `/miners/latest`) | [`ListMiners`](#miners), [`SearchMiners`](#filters)       |
| `POST /farms/{farm_id}/miners/search`                       | [`SearchMiners`](#filters)                                |
| `GET /farms/{farm_id}/miners/stats`                         | [`QueryFarmMinerMetrics`](#classification-metrics)        |
| `GET /farms/{farm_id}/metrics/history`                      | [`QueryFarmMetrics`](#historical-metrics)                 |
| `GET /farms/{farm_id}/tasks/batches`                        | [`ListTaskBatches`](#task-batches)                        |
| `POST /farms/{farm_id}/tasks/search`                        | [`SearchTaskBatches`](#task-batches)                      |
| `GET /farms/{farm_id}/tasks/batches/{batch_id}`             | [`GetTaskBatch`, `ListTaskBatchTasks`](#detail-and-tasks) |

## Authentication

Valid workspace API keys continue to work with `Authorization: Bearer <workspace_api_key>`. In v2, 401 indicates an authentication failure, 403 indicates a permission failure, and 404 indicates an inaccessible or missing scoped resource, such as a farm outside the key's scope. None of these responses represent an empty list.

## Pagination

Lists paginate through query parameters and searches through the JSON request body. Both use `page` and `page_size`. Both parameters are optional and default to `page=1` and `page_size=20`. Historical metrics do not paginate; they are queried by time window.

| Behavior                 | v1                | v2                   |
| ------------------------ | ----------------- | -------------------- |
| Page size (GET)          | `pageSize`        | `page_size`          |
| Page size (search)       | `limit`           | `page_size`          |
| Default page size        | 10                | 20                   |
| Maximum page size        | 10,000            | 10,000               |
| Position in the response | `limit`, `offset` | `page`, `page_size`  |
| Next page exists         | `hasNext`         | `page < total_pages` |
| Previous page exists     | `hasPrevious`     | `page > 1`           |

Pagination metadata is returned in `pagination`, alongside `success`, `data`, and `error`.

Read from page 1 and request the next page while `page < total_pages`; stop when `total` is 0. Data can change between requests, so results across pages are not a point-in-time snapshot.

The v1 GET endpoints did not define `limit` or `offset`. Integrations that sent those parameters need to check whether their original pagination worked as intended.

## Workspaces

Integrations that already hold a `workspace_id` keep using it. Integrations that resolved workspace details through `/me` use `ListWorkspaces` instead:

```http theme={null}
GET /public-api/v2/workspaces
Authorization: Bearer <workspace_api_key>
```

`data` is an unpaginated array, and the target workspace is selected by `id`. On each item, `workspace_id`, `workspace_name`, and `workspace_slug` become `id`, `name`, and `slug`.

With an API key, the list contains the key's own workspace (`relations` includes `member`) and workspaces with valid grants (`relations` includes `grantee`). With OAuth, it contains the workspaces the current user has joined. Each item also carries a `permissions` field, which describes effective permissions only.

`ListFarms` lists the farms accessible with the same credentials. The `permissions` values in `ListWorkspaces` describe allowed API actions, not a list of farm IDs. No v2 endpoint currently returns the full `farm_ids` and grant-expiration record from `/me/grants`; integrations that depend on those details need a confirmed data source before migration can be completed.

## Farms

v1:

```http theme={null}
GET /private-api/v1/{workspace_id}/farms?page=1&pageSize=100
```

v2:

```http theme={null}
GET /public-api/v2/workspaces/{workspace_id}/farms?page=1&page_size=100
```

v2 includes archived farms that have not been deleted; filtering out `archived: true` restores the v1 scope. The sort order also changes, so a v1 page number cannot be relied on to identify the same records in v2.

## Agents

v1 listed Agents across the whole workspace:

```http theme={null}
GET /private-api/v1/{workspace_id}/agents?page=1&pageSize=100
```

v2 lists them per farm:

```http theme={null}
GET /public-api/v2/workspaces/{workspace_id}/farms/{farm_id}/agents?page=1&page_size=100
```

A workspace-wide view iterates over the accessible farms and pages through each farm's Agents; the workspace count is the sum of `total` across the farms in scope.

List items provide status, version, uptime, and update time. The nested `farm` object is replaced by `farm_id`. Host details are not part of the list summary; see the [compatibility notes](/api-guide/v2/migration-compatibility).

## Miners

`ListMiners` returns unfiltered lists; `SearchMiners` applies filters and returns the same summary objects.

```http theme={null}
GET /public-api/v2/workspaces/{workspace_id}/farms/{farm_id}/miners?page=1&page_size=1000
```

| v1 field      | v2                   |
| ------------- | -------------------- |
| `temp`        | `temperature`        |
| `stale`       | `status === "stale"` |
| Nested `farm` | `farm_id`            |

Units are unchanged: `hashrate` and `expected_hashrate` in H/s, `power` in W, `efficiency` in J/TH.

Summaries do not include every field of the v1 full object. Integrations that read `errors`, `hashboards`, `pools`, `psus`, `hashrate_24h`, or reboot counts need a separate data-access solution. The [compatibility notes](/api-guide/v2/migration-compatibility) describe these limitations; renaming fields alone is not enough.

### Filters

`SearchMiners` takes filters in a JSON body. Top-level conditions are combined with AND; for example, "online with a fan or power anomaly":

```http theme={null}
POST /public-api/v2/workspaces/{workspace_id}/farms/{farm_id}/miners/search
Content-Type: application/json
```

```json theme={null}
{
  "status": { "in": ["online"] },
  "anomaly_flags": { "in": ["fan", "power"] },
  "page": 1,
  "page_size": 1000
}
```

A condition that combines fields with OR, such as "stale or anomalous", requires two queries whose results are merged by miner ID. `anomaly_filters` becomes `anomaly_flags.in`; `ip_ranges` remains an array.

`status` accepts only `online` and `stale`. v1 converted `offline` to `stale` and ignored deprecated health values such as `error` and `low_hashrate`. A migrated request reproduces that behavior: `offline` becomes `stale`, and the ignored values are dropped rather than converted into an `anomaly_flags` condition, which would filter results that v1 did not. The [compatibility notes](/api-guide/v2/migration-compatibility) explain this in detail.

For an exact MAC lookup, set `search.field` to `"mac"` and provide the complete address in `search.value`. Field search matches substrings, so read all candidate pages, compare full addresses case-insensitively, and count only exact matches.

The `gt` and `lt` conditions on `last_updated_at` are strict; records equal to either boundary are excluded.

## Classification metrics

`QueryFarmMinerMetrics` (beta) returns a snapshot of current miner classifications and distributions. The request changes from GET to POST and carries no body.

v1:

```http theme={null}
GET /private-api/v1/{workspace_id}/farms/{farm_id}/miners/stats
```

v2:

```http theme={null}
POST /public-api/v2/workspaces/{workspace_id}/farms/{farm_id}/miner-metrics/query
```

| v1 response          | v2 response                  |
| -------------------- | ---------------------------- |
| `by_type`            | `classification`             |
| `by_abnormal_type`   | `anomaly_breakdown`          |
| `mining_mode`        | `mining_modes`               |
| `mining_mode[].mode` | `mining_modes[].mining_mode` |
| `miner_model`        | `miner_models`               |

`theo` and `total` are unchanged. `period` is new and gives the observation time, and `mining_modes` entries gain `firmwares`. Classifications can overlap, so the overall count comes from `total` rather than from summing categories. The endpoint reports live state, so counts vary between calls.

For a farm that does not exist, v1 could return empty stats; v2 returns 404. Time series come from [historical metrics](#historical-metrics).

## Historical metrics

`QueryFarmMetrics` (beta) replaces paged history with time-window queries: one request returns the buckets for the requested window. For daily metrics on 2026-09-01 UTC:

v1:

```http theme={null}
GET /private-api/v1/{workspace_id}/farms/{farm_id}/metrics/history?from_date=2026-09-01&to_date=2026-09-01&granularity=day
```

v2:

```http theme={null}
POST /public-api/v2/workspaces/{workspace_id}/farms/{farm_id}/metrics/query
Content-Type: application/json
```

```json theme={null}
{
  "from_time": "2026-09-01T00:00:00Z",
  "to_time": "2026-09-02T00:00:00Z",
  "granularity": "day"
}
```

The window is half-open: `from_time` is included and `to_time` is excluded, so a full end date ends at midnight of the following day. For non-UTC dates, the boundaries are built in the business time zone and then converted to ISO timestamps.

`granularity` should always be sent explicitly. v1 defaulted to `day`; v2 defaults to `hour`.

### Response

The record array moves from `data` to `data.snapshots`, and `data` also carries the window information. Daily buckets keep the `pool`, `agent`, `finance`, and `electricity` field groups.

`snapshots` are in ascending order. Buckets with no data are present and contain `null` metrics rather than 0. Because empty buckets are present, the latest N buckets and the latest N buckets with data are different selections. Snapshots no longer include `id` or `workspace_id`; farm, granularity, and `period` identify a point. `period` is the start of the metric bucket, expressed as an ISO 8601 string.

### Range limits

A single query covers at most:

| Granularity | Maximum range |
| ----------- | ------------- |
| `10min`     | 1 day         |
| `hour`      | 7 days        |
| `day`       | 90 days       |
| `week`      | 365 days      |

Longer ranges are split at bucket boundaries into adjacent, non-overlapping windows and merged by `period`, keeping the original time-zone meaning. A v1 request for 7 days at `10min` or 60 days at `hour` therefore becomes several v2 queries.

## Task batches

Lists use query parameters; searches use a JSON body.

```http theme={null}
GET /public-api/v2/workspaces/{workspace_id}/farms/{farm_id}/task-batches?task_name=miner.system.reboot&page=1&page_size=50
POST /public-api/v2/workspaces/{workspace_id}/farms/{farm_id}/task-batches/search
```

```json theme={null}
{
  "task_name": { "eq": "miner.system.reboot" },
  "page": 1,
  "page_size": 50
}
```

`batch_id` becomes `id`. In list items, `failed_count` becomes `unsuccessful_count` and still counts failures, timeouts, and cancellations together.

### Detail and tasks

v1 embedded `tasks` in the task batch detail. v2 splits them: `GetTaskBatch` returns the summary, and `ListTaskBatchTasks` returns the tasks as a paginated list.

```http theme={null}
GET /public-api/v2/workspaces/{workspace_id}/farms/{farm_id}/task-batches/{task_batch_id}
GET /public-api/v2/workspaces/{workspace_id}/farms/{farm_id}/task-batches/{task_batch_id}/tasks?page=1&page_size=100
```

<Warning>
  In the detail response, `failed_count` counts execution failures only. It is not the same figure as the list's `unsuccessful_count`.
</Warning>
