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

# Move a v2 buyer REST integration to V3

> Map v2 buyer REST calls for advertisers, campaigns and reporting to V3 HTTP operations, and the changes your client needs.

This page is for a backend job or service that calls the v2 buyer REST API,
such as a nightly reporting pull, and is moving to the V3 HTTP API. Your API
key works for both. What changes is the shape of each call: V3 has one fixed
route per operation, takes a JSON body, and returns typed results. The
[V3 HTTP API](https://docs.apostra.com/v3/http-api.md) page is the reference for requests, responses and
errors; this page covers what is different from v2.

## The calls, side by side

| You call in v2 | You call in V3 |
| - | - |
| `GET /api/v2/buyer/advertisers` | `POST /api/v3/tools/search` with `{"kind": "advertiser"}` |
| `GET /api/v2/buyer/advertisers/{id}` | `POST /api/v3/tools/get` with `{"kind": "advertiser", "id": "{id}"}` |
| `GET /api/v2/buyer/campaigns?advertiserId={id}` | `POST /api/v3/tools/search` with `{"kind": "campaign", "filter": {"advertiserId": "{id}"}}` |
| `GET /api/v2/buyer/reporting/metrics` | `POST /api/v3/tools/get_delivery` with `{"report": "campaign_delivery", ...}` |

Every V3 call is a `POST` to `https://api.apostra.com/api/v3/tools/<operation>`
with your key as a bearer token. A key that belongs to one account needs no
other header. See [Make a request](https://docs.apostra.com/v3/http-api.md#make-a-request).

## Read advertisers

```bash theme={null}
curl https://api.apostra.com/api/v3/tools/search \
  --request POST \
  --header "Authorization: Bearer $APOSTRA_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"kind": "advertiser", "limit": 200}'
```

The advertisers are in `data.objects.results`, one object per advertiser with
`advertiserId`, `name`, `primaryCurrency` and `status`. To read one advertiser,
call `get` with `kind: "advertiser"` and its `id`; the advertiser is in
`data.object`.

## Read an advertiser's campaigns

```bash theme={null}
curl https://api.apostra.com/api/v3/tools/search \
  --request POST \
  --header "Authorization: Bearer $APOSTRA_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"kind": "campaign", "filter": {"advertiserId": "5717"}, "limit": 200}'
```

Each result nests the campaign and its advertiser:
`data.objects.results[].campaign.campaignId` and
`data.objects.results[].advertiser.advertiserId`. Filters always go inside
`filter`; V3 HTTP does not read a filter from the top level of the body.

## Page through results

v2 paged with numbers (`limit` and `offset` for advertisers, `take` and `skip`
for campaigns). V3 pages with a cursor:

1. Send `limit` (up to 200 for `search`, up to 100 rows for `get_delivery`).
2. If the response has a `nextCursor` (`data.objects.nextCursor` for
   `search`, `data.page.nextCursor` for `get_delivery`), send the same request
   again with `cursor` set to it.
3. Stop when the response has no `nextCursor`.

A cursor belongs to the request that produced it. Send it back with the same
operation, filters and `limit`.

## Read delivery

v2 `GET /api/v2/buyer/reporting/metrics` becomes `get_delivery` with
`report: "campaign_delivery"`. For one advertiser and one day:

```bash theme={null}
curl https://api.apostra.com/api/v3/tools/get_delivery \
  --request POST \
  --header "Authorization: Bearer $APOSTRA_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "report": "campaign_delivery",
    "filters": {"advertiserId": "5717"},
    "range": {"startDate": "2026-10-29", "endDate": "2026-10-29"},
    "dimensions": ["date", "campaign", "media_buy", "package"],
    "metrics": ["impressions", "spend", "clicks", "cpm"]
  }'
```

* **Dates.** Send `range.startDate` and `range.endDate` (up to 90 days), or
  `range.lifetime: true`. There is no `days` parameter.
* **Grouping.** `dimensions` chooses the grouping, from `date`, `advertiser`,
  `campaign`, `media_buy`, `package` and others. Each row carries its
  dimension values in `rows[].dimensions`, for example
  `rows[].dimensions.campaign.id`.
* **Metric values.** Each metric is an object, not a bare number:
  `{"value": 1250, "status": "available"}`. When `status` is `unavailable`,
  `value` is `null` and `reason` says why. A missing value is never reported
  as zero.
* **Totals.** `data.totals.metrics` holds the total for each metric. When the
  matched delivery mixes provisional (Snapshot) and final (Official) figures,
  `totals` gives no number and `totalsByFinality` gives one total per class.
  See [Reporting](https://docs.apostra.com/v3/reporting-pipeline.md) for what each label means.
* **Paging.** Rows come in pages of up to 100 (`limit`); follow
  `data.page.nextCursor` as above.

### Metric names

V3 uses AdCP's delivery metric names.

| v2 field | V3 metric |
| - | - |
| `impressions`, `spend`, `clicks`, `views`, `conversions`, `leads`, `ctr`, `roas` | Same name |
| `completedViews` | `completed_views` |
| `videoCompletions` | `quartile_100` |
| `conversionValue` | `conversion_value` |
| `completionRate` | `completion_rate` |
| `ecpm` | `cpm` |
| `cpc` | `cost_per_click` |
| `cpa` | `cost_per_acquisition` |
| `viewability` | `viewable_impressions`, `measurable_impressions`, `viewable_rate` |

### The v2 summary view

v2 reporting returns a tree by default (`view=summary`): advertiser, then
campaign, media buy and package, with totals at each level. `get_delivery`
returns flat rows instead. To build the same tree, request
`dimensions: ["campaign", "media_buy", "package"]` and group the rows in your
client. For daily rows, add `date`.

These v2 reporting features have no V3 equivalent today: delivery pace and goal
progress in the summary view, the converted cross-advertiser total
(`displayCurrency`), outcome counts from your measurement uploads
(`outcomeType`), and CSV download (`download=true`). If you depend on one of
them, tell us before you move.

## Errors

V3 returns a non-2xx status with an AdCP error in `error`; `data` is `null`.

* A request body that does not match the operation's schema returns `422` with
  code `INVALID_REQUEST`, and `error.details.validation` names the field. v2
  returned `400` for invalid query parameters.
* A key cannot reach another account. Sending an `X-SCOPE3-CUSTOMER-ID` header
  that does not match the key's account returns `403`.

See [Errors](https://docs.apostra.com/v3/errors.md) for every code and what to do about it.

## Generate a client

The V3 OpenAPI document lists every operation, its body and its result. You can
generate a typed client from it rather than writing requests by hand. See
[Generate a client](https://docs.apostra.com/v3/http-api.md#generate-a-client), or use an
[official SDK](https://docs.apostra.com/v3/sdk-guide.md).


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