---
title: "Migrate to Canonical API Responses"
description: "Adopt public IDs, pagination, representations, mutation preconditions, and CloudEvents"
canonical: https://y2.dev/docs/api/migration-v1-1/
---

# Migrate to Canonical API Responses

Adopt public IDs, pagination, representations, mutation preconditions, and CloudEvents

Use this guide to migrate a client that depends on legacy persistence-shaped fields or pagination behavior. The v1 routes remain at `/api/v1`. Their response contract was widened additively. Intel v2 routes under `/api/v2` emit canonical public DTOs.

Read `X-Y2-API-Version` and `X-Y2-Schema-Version` to identify the response contract. See [current version headers](https://y2.dev/docs/api/responses#version-and-diagnostic-headers).

## Migration checklist

### Capture current client fixtures

Save representative success, empty, error, and paginated responses before changing parsers. Include response headers in the fixtures.

### Store public resource IDs

Replace saved Convex document IDs with the typed IDs emitted in current responses. Treat every ID as opaque.

### Adopt canonical fields

Read RFC 3339 semantic timestamps, typed outcome rows, Problem Details, and the documented missing-value states. Stop creating new dependencies on deprecated aliases.

### Replace collection traversal

Follow `links.next` or return `meta.page.nextCursor` unchanged. Do not infer another page from the number of rows.

### Harden writes and delivery consumers

Add idempotency keys, ETag preconditions, CloudEvent deduplication, and a durable change-feed checkpoint where those features apply.

## Replace persistence IDs with public IDs

| Resource                | Prefix |
| ----------------------- | ------ |
| Report                  | `rpt_` |
| Profile                 | `prf_` |
| Subscription            | `sub_` |
| Webhook                 | `whk_` |
| Observation             | `obs_` |
| Incident                | `inc_` |
| Entity                  | `ent_` |
| Market                  | `mkt_` |
| Financial indicator row | `fin_` |
| Place                   | `plc_` |
| Relation                | `rel_` |
| Signal                  | `sig_` |
| Source                  | `src_` |

Legacy Convex IDs remain accepted on migrated resource paths during the compatibility window, but responses and webhooks emit public IDs. Store the exact ID Y2 returns. Do not derive IDs, parse the hash, or depend on its current length.

## Traverse collections by link or cursor

Canonical collections return `data`, `meta`, and `links`. `meta.pageCount` is the number of rows in the current page, not the collection total. A total exists only where a response explicitly defines `totalCount`.

```
let url: string | null = "https://api.y2.dev/api/v2/incidents?limit=100";

while (url) {
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.Y2_API_KEY}` },
  });
  if (!response.ok) throw await response.json();

  const page = await response.json();
  await persist(page.data);
  url = page.links.next
    ? new URL(page.links.next, "https://api.y2.dev").toString()
    : null;
}
```

Cursors are opaque and filter-bound. Never edit them or reuse one with a different filter shape. Changing `limit` does not change the filter shape, but following `links.next` is the safest default because it preserves the server's continuation parameters.

## Distinguish omitted, null, and empty

| State           | Meaning                                                                  |
| --------------- | ------------------------------------------------------------------------ |
| Field omitted   | Optional enrichment or bounded relation was not requested or not present |
| Field is `null` | The schema permits the field, but its value is unknown or unavailable    |
| Field is `[]`   | The relationship or outcome set is known and empty                       |

Numeric measurements include their documented unit, currency, or provider basis. Prediction outcomes are objects with `label` and a `0–1` `probability`. Do not parse JSON-encoded outcome strings in a new client.

## Replace deprecated compatibility fields

| Surface                   | Deprecated          | Canonical replacement          |
| ------------------------- | ------------------- | ------------------------------ |
| Regional OSINT query      | `since`             | `datetime={RFC3339 start}/..`  |
| Regional OSINT query      | `until`             | `datetime=../{RFC3339 end}`    |
| Profile report metadata   | `reportGeneratedAt` | `reportGeneratedAtISO`         |
| Profile delivery metadata | `generatedAt`       | `generatedAtISO`               |
| Report audio metadata     | `duration`          | `durationSeconds`              |
| Collection metadata       | `meta.count`        | `meta.pageCount` for this page |

The regional `since` and `until` parameters are inclusive epoch-millisecond bounds. Do not combine either one with `datetime`. The compatibility fields remain additive on v1 for the published migration window. New integrations should use only the canonical fields.

## Parse Problem Details

JSON failures use `application/problem+json` with RFC 9457-style top-level fields: `type`, `title`, `status`, `detail`, `instance`, `code`, `requestId`, and `resolution`. Read `resolution` for a safe next step. No nested compatibility error object is returned.

```
if (!response.ok) {
  const problem = await response.json();
  logger.error({
    code: problem.code,
    status: problem.status,
    requestId: problem.requestId,
    resolution: problem.resolution,
  });
  throw new Error(problem.detail);
}
```

Retry only when the status and response headers indicate a retryable rate-limit or availability failure. A validation, authentication, scope, idempotency, or precondition failure requires a request change.

## Request specialized representations explicitly

- Send `Accept: text/markdown` to a report endpoint that advertises canonical Markdown.
- Send `format=ndjson` or `Accept: application/x-ndjson` only to collections whose OpenAPI operation advertises NDJSON.
- Send `format=geojson` or `Accept: application/geo+json` only to spatial operations that advertise a GeoJSON FeatureCollection.

Representation support is endpoint-specific

Do not infer support from another collection. For example, `/api/v1/osint/finint` offers NDJSON, while `/api/v2/finint` currently returns JSON only.

GeoJSON follows WGS 84 and uses `[longitude, latitude]`. For `/api/v1/osint/regional`, `bbox` is `west,south,east,north`. `datetime` accepts an RFC 3339 instant or `start/end` interval, with `..` as an open boundary.

## Make profile and webhook writes safe

`POST /profiles` and `POST /webhooks` return `201`, the created resource, `Location`, and `ETag`. Both accept an `Idempotency-Key` containing 8–200 allowed characters. The key is scoped to the tenant and operation and retained for 24 hours.

- Replaying the same key with the same canonical JSON body returns the original resource.
- Reusing the key with a different body returns `409 IDEMPOTENCY_CONFLICT`.
- `PUT` replaces mutable state. `PATCH` changes only supplied fields.
- Updates and deletes accept `If-Match`. A stale value returns `412 PRECONDITION_FAILED` with the current `ETag`.
- A successful delete returns `204` with no JSON body.

## Migrate webhook consumers to CloudEvents

Report notifications use `Content-Type: application/cloudevents+json` and event type `dev.y2.report.generated.v1`. The body contains compact report and subscription IDs, a bounded summary, intelligence counts, audio availability, and API links. It excludes report HTML, storage IDs, model names, prompts, and generation costs.

Deduplicate on the CloudEvent `id`, also sent as `Idempotency-Key` and `X-Y2-Event-Id`. When the same logical event is attempted again, its event ID and body remain stable. `X-Y2-Attempt-Id`, `X-Y2-Attempt`, and `X-Y2-Timestamp` identify the network attempt. Verify `X-Y2-Signature` over the exact raw bytes before parsing JSON.

Recover paused webhook delivery

Y2 retries temporary delivery failures within a fixed attempt limit. Paused destinations need a successful test before reactivation. See [webhook recovery](https://y2.dev/docs/api/webhooks#handle-attempts-and-failures). Use the change feed to recover missed resource state.

## Recover with the change feed

`GET https://api.y2.dev/api/v2/changes` returns append-only resource changes from an exclusive watermark. Process each page, commit every resource update or tombstone, and persist `meta.watermark` as the final write.

Deleted resources have `changeType: "deleted"` and `tombstone: true`. Authenticated keys receive global intelligence changes plus their user or workspace changes. Anonymous x402 requests receive global changes only. The current feed has no retention expiry, but consumers should still repair continuously instead of treating it as a permanent archive guarantee.

[Integration recipes](https://y2.dev/docs/api/integration-recipes)

[Authentication and errors](https://y2.dev/docs/api/authentication)

[Webhook contract](https://y2.dev/docs/api/webhooks)

[OpenAPI](https://y2.dev/docs/api/openapi)
