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
| 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/markdownto a report endpoint that advertises canonical Markdown. - Send
format=ndjsonorAccept: application/x-ndjsononly to collections whose OpenAPI operation advertises NDJSON. - Send
format=geojsonorAccept: application/geo+jsononly 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. PUTreplaces mutable state;PATCHchanges only supplied fields.- Updates and deletes accept
If-Match; a stale value returns412 PRECONDITION_FAILEDwith the currentETag. - A successful delete returns
204with 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.