Y2 Elite workspaces are rolling out for teams
Y2Y2Docs

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.

Responses identify the route and schema contracts with X-Y2-API-Version and X-Y2-Schema-Version. Current shared v1 response helpers emit API version 1.0 and schema version 1.1; v2 operations emit API version 2.0.

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

ResourcePrefix
Reportrpt_
Profileprf_
Subscriptionsub_
Webhookwhk_
Observationobs_
Incidentinc_
Entityent_
Marketmkt_
Financial indicator rowfin_
Placeplc_
Relationrel_
Signalsig_
Sourcesrc_

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.

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

StateMeaning
Field omittedOptional enrichment or bounded relation was not requested or not present
Field is nullThe 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

SurfaceDeprecatedCanonical replacement
Regional OSINT querysincedatetime={RFC3339 start}/..
Regional OSINT queryuntildatetime=../{RFC3339 end}
Profile report metadatareportGeneratedAtreportGeneratedAtISO
Profile delivery metadatageneratedAtgeneratedAtISO
Report audio metadatadurationdurationSeconds
Collection metadatameta.countmeta.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.

Delivery failure is not an automatic retry queue

Y2 does not run an independent webhook retry loop. A failed attempt increments the consecutive failure count, and five consecutive failures disable the webhook. Use the change feed to repair missed 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.