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

# Migrate from v1 to v2

> Every change between /api/v1 and /api/v2 that affects your integration, and what to update.

v2 is a new version of the API, served under `/api/v2` next to `/api/v1`. Nothing changes for a request you keep sending to `/api/v1`. Your API token works on both versions, and you can move one endpoint at a time.

This page lists what you need to change when you move a call from v1 to v2. Each endpoint page in the [API reference](https://api.docs.originone.ai/api-reference/orders/create.md) documents the full v2 contract.

## Before you start

**1. Check your token's scopes**

v2 splits some v1 scopes and adds new ones. See [Scopes](#scopes) and request any you are missing from Origin One.

**2. Treat every id as a string**

All v2 `id` values are strings. See [Identifiers](#identifiers).

**3. Update your error handling**

A missing scope returns `403`, not `401`, and error `code` values are new. See [Errors](#errors).

**4. Move each endpoint**

Change the path from `/api/v1` to `/api/v2` and apply the changes for that endpoint below.

**5. Move your webhook callbacks**

v2 deliveries have a new body and signature. See [Migrate webhooks to v2](https://api.docs.originone.ai/migrate-webhooks.md).

## Endpoints

Most v1 endpoints have a v2 equivalent at the same path under `/api/v2`. Two are removed:

| v1 | v2 |
| - | - |
| `POST /api/v1/orders/search` | Removed. Use [List orders](https://api.docs.originone.ai/api-reference/orders/list.md): see [Searching orders](#searching-orders). |
| `GET /api/v1/users` | Removed, with no replacement. |

v2 adds these endpoints:

| Endpoint | Purpose |
| - | - |
| [`GET /api/v2/orders`](https://api.docs.originone.ai/api-reference/orders/list) | List and filter orders. |
| [`GET /api/v2/agencies`](https://api.docs.originone.ai/api-reference/agencies/list) | List the regulatory agencies an order can name, with their job categories. |
| [`GET /api/v2/agencies/{id}`](https://api.docs.originone.ai/api-reference/agencies/get) | Get an agency. |
| [`GET /api/v2/agencies/{agencyId}/jobCategories/{id}`](https://api.docs.originone.ai/api-reference/job-categories/get) | Get a job category. |
| [`GET /api/v2/tenants`](https://api.docs.originone.ai/api-reference/tenants/list) | List the child tenants your token can act in. |
| [`GET /api/v2/tenants/{id}`](https://api.docs.originone.ai/api-reference/tenants/get) | Get a child tenant. |
| `/api/v2/tenants/{tenantId}/…` | The company, location, package, service, clinic, and order endpoints, acting in a child tenant. See [Tenants](https://api.docs.originone.ai/tenants.md). |

## Scopes

Scope names keep the `resource.action` convention. These differ between versions:

| v1 scope | v2 scope | What changes |
| - | - | - |
| `company.read` | `company.list` and `company.read` | [List companies](https://api.docs.originone.ai/api-reference/companies/list.md) requires `company.list`. `company.read` covers only [Get a company](https://api.docs.originone.ai/api-reference/companies/get.md). |
| `order.search` | `order.list` | Covers [List orders](https://api.docs.originone.ai/api-reference/orders/list.md), which replaces the search endpoint. |
| `user.search` | — | The users endpoint is removed. |
| — | `order.participant.read` | Required to see the participant's `identifiers` on an order. Without it, `participant.identifiers` is [omitted](https://api.docs.originone.ai/responses.md#null-versus-omitted). |
| — | `agency.list`, `agency.read`, `agency.jobCategories.read` | Cover the new agency endpoints. |
| — | `tenant.list`, `tenant.read` | Cover the new tenant endpoints. Routes under `/tenants/{tenantId}` use the same scopes as their top-level endpoints. |

Every other scope keeps its name. See [Scopes](https://api.docs.originone.ai/scopes.md) for the full table.

## Errors

A token that lacks the scope an endpoint requires gets `403 Forbidden` in v2. v1 returns `401 Unauthorized` for the same request. In v2, `401` means only that the bearer token is missing, invalid, or expired.

A resource outside your token's access no longer returns `403`. Where v1 returns `403` for a location or package the token isn't linked to, v2 returns `404 Not Found` from a get endpoint and an empty page from a list filter such as `locationId`.

Every error a v2 endpoint returns, including `401`, `403`, and `404`, has the same body as a validation error:

```json theme={null}
{
  "statusCode": 403,
  "error": "Forbidden",
  "errors": [
    {
      "code": "insufficientScope",
      "detail": "This operation requires the `order.list` scope."
    }
  ]
}
```

What changes in the body:

* **`code` values are new.** v1 returns validation constraint names such as `isNotEmpty` and `isString`. v2 uses its own vocabulary, such as `required`, `invalidType`, `invalidValue`, and `tooSmall`. Don't map v1 codes to v2 codes. Rely on `statusCode`, `source.pointer`, and `detail` instead.
* **`source` can be omitted.** It is present only on `400` errors that a specific request field caused.
* **The top-level `meta` field is removed.**

A request that fails before it reaches an endpoint, such as one with a malformed JSON body or an unknown path, still returns the v1 error body.

[Create an order](https://api.docs.originone.ai/api-reference/orders/create.md) can also return `409 Conflict` when a package `name` matches more than one package.

## Identifiers

Every v2 `id` is a string. In v1, company, location, package, and clinic ids are JSON numbers. The value stays the same: a location with `"id": 456` in v1 has `"id": "456"` in v2. Order and service ids were already strings and don't change.

The change applies wherever an id appears: response bodies, path parameters, query filters such as `companyId` and `locationId`, and request bodies such as `locationId` in [Search clinics](https://api.docs.originone.ai/api-reference/clinics/search.md). Store ids as opaque strings. See [Identifiers](https://api.docs.originone.ai/responses.md#identifiers).

## Response conventions

v2 applies the same rules to every resource. These change what v1 returns:

* **Every resource carries `object`.** Nested resources carry it too, including `address`. See [Resource type](https://api.docs.originone.ai/responses.md#resource-type).
* **Every resource you can fetch carries `url`**, its canonical path. Follow it instead of building paths from `id`. See [Resource URLs](https://api.docs.originone.ai/responses.md#resource-urls).
* **Related resources are nested references**, not flat fields. A location returns `company: { object, id, name, url }` instead of `companyId` and `companyName`.
* **Empty values are `null`.** v1 can return an empty string for an absent address field. v2 returns `null`, and a field your token can't see is omitted. See [Null versus omitted](https://api.docs.originone.ai/responses.md#null-versus-omitted).
* **Addresses have one shape**: `object`, `line1`, `line2`, `city`, `state`, `postalCode`, and `country`. Each value can be `null`.
* **Response enums are open.** Values such as an order's `status` can gain new members. Handle values you don't recognize. See [Forward compatibility](https://api.docs.originone.ai/responses.md#forward-compatibility).

Pagination is unchanged: list endpoints take `page` and `limit` and return the `{ "data", "total", "page", "limit" }` envelope. See [Pagination](https://api.docs.originone.ai/pagination.md).

## Orders

### Create an order

[Create an order](https://api.docs.originone.ai/api-reference/orders/create.md) takes a reorganized body. Here is the same order in each version:

```json v1 request body theme={null}
{
  "location": { "name": "Acme Downtown" },
  "package": { "object": "package", "name": "5 Panel Urine" },
  "reasonForTest": "preEmployment",
  "schedulingOption": "link",
  "deliveryMethod": { "type": "mobile", "phoneNumber": "+12065550100" },
  "requester": {
    "email": "jo.smith@acme.example",
    "firstName": "Jo",
    "lastName": "Smith",
    "phoneNumber": "+12065550142"
  },
  "participant": {
    "firstName": "Ana",
    "lastName": "Reis",
    "dateOfBirth": "1990-04-12",
    "phoneNumber": "+12065550100",
    "address": {
      "street": "123 Main St",
      "city": "Seattle",
      "region": "WA",
      "postalCode": "98101",
      "country": "US"
    }
  }
}
```

```json v2 request body theme={null}
{
  "location": { "name": "Acme Downtown" },
  "package": { "name": "5 Panel Urine" },
  "testReason": "preEmployment",
  "settings": { "schedulingOption": "link" },
  "deliveryMethods": [{ "type": "mobile", "phone": "+12065550100" }],
  "requester": {
    "email": "jo.smith@acme.example",
    "firstName": "Jo",
    "lastName": "Smith",
    "phone": "+12065550142"
  },
  "participant": {
    "firstName": "Ana",
    "lastName": "Reis",
    "birthDate": "1990-04-12",
    "phone": "+12065550100",
    "address": {
      "line1": "123 Main St",
      "city": "Seattle",
      "state": "WA",
      "postalCode": "98101",
      "country": "US"
    }
  }
}
```

> [!WARNING]
>
> v2 ignores request fields it doesn't define, so a v1 field name left in a v2 request is dropped without an error. A required field then fails validation, but an optional one, such as `clinicSettings.selectionMode`, silently falls back to its default. Rename every field in the table below.

| v1 field | v2 field | Notes |
| - | - | - |
| `reasonForTest` | `testReason` | Same values. |
| — | `otherTestReason` | Detail for a `testReason` of `other`, up to 30 characters. |
| `schedulingOption` | `settings.schedulingOption` | `settings` is now required. |
| `clinicSettings.selectionMode` | `settings.clinicSelectionMode` | Same values. |
| `deliveryMethod` | `deliveryMethods` | An array with exactly one entry. |
| `deliveryMethod.phoneNumber` | `deliveryMethods[].phone` | |
| `settings.regulatoryAgency` | `agency` | An object with the agency's `id` or its `code`. Required when the package includes a DOT service. |
| — | `jobCategory` | An object with the job category's `id` or `name`, from [List agencies](https://api.docs.originone.ai/api-reference/agencies/list.md). Accepted only for a DOT service under FAA, FRA, or FTA, and required there when the package includes a DOT urine test. v1 accepts these orders without it. |
| `location.name` | `location.id` or `location.name` | Send exactly one. |
| `package.object`, `package.name` | `package.id` or `package.name` | Send exactly one. `object` is no longer part of the request. |
| `requester.phoneNumber` | `requester.phone` | |
| `participant.dateOfBirth` | `participant.birthDate` | |
| `participant.phoneNumber` | `participant.phone` | |
| `participant.address.street` | `participant.address.line1` | `line2` is available for a second address line. |
| `participant.address.region` | `participant.address.state` | |
| `participant.address.country` | `participant.address.country` | Now optional, defaults to `US`. |

The `callback`, `orderMatching`, and the remaining `settings` fields keep their names. `settings.observedCollection` must be `true` for a return-to-duty or follow-up test with a DOT urine service: v1 skips that check when the field is omitted, and v2 rejects the order. Registering a callback through v2 changes what its deliveries look like: see [Migrate webhooks to v2](https://api.docs.originone.ai/migrate-webhooks.md).

### Order responses

[Create an order](https://api.docs.originone.ai/api-reference/orders/create.md), [Get an order](https://api.docs.originone.ai/api-reference/orders/get.md), and [List orders](https://api.docs.originone.ai/api-reference/orders/list.md) return the same v2 order. [Cancel an order](https://api.docs.originone.ai/api-reference/orders/cancel.md) returns it too, without the participant's `identifiers`.

| v1 field | v2 field |
| - | - |
| `orderCode` | `code` |
| `services[].statusHistory[].timestamp` | `services[].statusHistory[].occurredAt` |

The v2 order also adds the order's context, so you no longer need a second call for it: `url`, `agency`, `clinic`, `company`, `location`, `jobCategory`, `packages`, `participant`, `testReason`, `otherTestReason`, `createdAt`, `updatedAt`, `statusUpdatedAt`, and `expiresAt`. Each service adds `id`, `isDot`, `attributes`, `lab`, `specimenId`, `trackingNumber`, `courierServiceName`, and `screenHistory`, and each `statusHistory` entry adds `groupType`.

`participant.identifiers` is present only for a token with the `order.participant.read` scope. `services[].statusUpdatedAt` can be `null`. See [Order lifecycle](https://api.docs.originone.ai/order-lifecycle.md) for statuses and `isFinal`.

### Searching orders

`POST /api/v1/orders/search` is replaced by [`GET /api/v2/orders`](https://api.docs.originone.ai/api-reference/orders/list), which takes query-string filters instead of a filter body:

| v1 search | v2 list |
| - | - |
| `filter` on `orderCode` | `code` (exact match) |
| `filter` on `specimenId` | `specimenId` (exact match) |
| `filter` on `status` | `status` (exact match) |
| `filter` on `companyName` | `companyId` |
| `filter` on `createdAt` | `createdFrom` and `createdTo` |
| — | `locationId` |
| `limit` and `offset` | `page` and `limit` |
| `{ "orders", "total", "limit", "offset" }` | `{ "data", "total", "page", "limit" }` |
| `order` | Removed. Results are always newest first. |

v2 can't filter orders by participant details (name, email, phone, date of birth, SSN, or driver license) or by `updatedAt`. To follow status changes, register a `callback` when you create the order instead of polling.

The list has no implicit date window: it returns every order your token can see unless `createdFrom` or `createdTo` narrow it.

### Cancel an order

[Cancel an order](https://api.docs.originone.ai/api-reference/orders/cancel.md) keeps its path, its body, and its `422` cases. It returns the v2 order without the participant's `identifiers`: get the order for them.

## Clinics

### Search clinics

[Search clinics](https://api.docs.originone.ai/api-reference/clinics/search.md) returns only active clinics owned by your tenant.

| v1 field | v2 field |
| - | - |
| `clinicTypes` | Removed. |
| `lat`, `long` | `coordinates.lat`, `coordinates.long` |
| `address.addressLine` | `address.line1` |
| `locationId` (number) | `locationId` (string) |

Send `coordinates` or `address`, not both. An `address` requires `state` and `postalCode`. In the response, `clinicType` and `vendors` are removed and `isActive` is added.

### Clinic responses

[List clinics](https://api.docs.originone.ai/api-reference/clinics/list.md), [Get a clinic](https://api.docs.originone.ai/api-reference/clinics/get.md), and search results list a clinic's services as references (`object`, `id`, `name`, `type`, `url`). `services[].isDot` and `services[].attributes` are removed from the clinic; fetch the service's `url` for them.

## Locations, companies, packages, and services

| Resource | Change |
| - | - |
| [Locations](https://api.docs.originone.ai/api-reference/locations/list.md) | `companyId` and `companyName` are replaced by a `company` reference. `isActive` is removed: v2 returns only active locations. `createdAt` is added. |
| [Companies](https://api.docs.originone.ai/api-reference/companies/list.md) | `isActive` is removed. |
| [Packages](https://api.docs.originone.ai/api-reference/packages/list.md) | `isActive` is removed: v2 returns only active packages. Services are references, without `isDot` or `specimenType`. [Get a package](https://api.docs.originone.ai/api-reference/packages/get.md) removes `capabilities` and adds `locations`, the locations that offer the package. |
| [Services](https://api.docs.originone.ai/api-reference/services/list.md) | `attributes` is always present, and `null` when the service has no testing category or specimen type. |

## Webhooks

A callback receives the deliveries of the version it was registered through. v2 deliveries carry an event instead of the order, and sign it with a timestamp. See [Migrate webhooks to v2](https://api.docs.originone.ai/migrate-webhooks.md) for the switch-over steps.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.