Integration Recipes
Build grounded agents, GIS imports, warehouse syncs, and durable intelligence pipelines
Use these recipes when you already have a Y2 API key and need a production-shaped integration.
They use the canonical schemas in the OpenAPI document. Keep
Y2_API_KEY and webhook secrets in server-side environment variables.
| Goal | Start with |
|---|---|
| Ground an agent answer | Report metadata and Markdown |
| Import data into a map or spatial analysis tool | GeoJSON FeatureCollection |
| Synchronize an analytics warehouse | Paginated Intel v2 JSON |
| Keep another system current | Change feed and webhooks |
Persist public IDs such as rpt_... and prf_.... Legacy Convex document IDs are not a
stable integration contract.
Ground an agent with a report and its sources
Fetch the report's structured source metadata and canonical Markdown representation in parallel.
Retry only rate-limit and server failures, and retain requestId when logging a Problem Details
response.
const authorization = `Bearer ${process.env.Y2_API_KEY}`;
const reportId = "rpt_0123456789abcdef01234567";
const reportUrl = `https://api.y2.dev/api/v1/reports/${reportId}`;
async function requireSuccess(response: Response) {
if (response.ok) return response;
const problem = await response.json();
const error = new Error(
`${problem.code}: ${problem.detail} (request ${problem.requestId})`,
);
Object.assign(error, {
retryable: problem.status === 429 || problem.status >= 500,
problem,
});
throw error;
}
const [metadataResponse, markdownResponse] = await Promise.all([
fetch(`${reportUrl}?include=sources&view=agent`, {
headers: { Authorization: authorization },
}),
fetch(reportUrl, {
headers: { Authorization: authorization, Accept: "text/markdown" },
}),
]);
const [metadata, markdown] = await Promise.all([
requireSuccess(metadataResponse).then((response) => response.json()),
requireSuccess(markdownResponse).then((response) => response.text()),
]);
answerWithCitations(
markdown,
metadata.data.sources.map((source: { url: string }) => source.url),
);The repository checks its generated TypeScript definitions with:
bun run generate:api-types
bun run check:api-typesExternal TypeScript projects can generate their own types from
https://y2.dev/api/openapi.yaml with an OpenAPI 3.1-compatible generator.
Import regional intelligence into a GIS
Request GeoJSON from the regional OSINT endpoint. Coordinates use WGS 84 longitude-first order:
[longitude, latitude].
import os
import requests
response = requests.get(
"https://api.y2.dev/api/v1/osint/regional",
headers={
"Authorization": f"Bearer {os.environ['Y2_API_KEY']}",
"Accept": "application/geo+json",
},
params={
"bbox": "-122.5,32.5,-96.8,49.2",
"datetime": "2026-07-01T00:00:00Z/2026-07-21T23:59:59Z",
"limit": 200,
},
timeout=30,
)
response.raise_for_status()
feature_collection = response.json()
assert feature_collection["type"] == "FeatureCollection"
assert all(
feature["type"] == "Feature"
for feature in feature_collection["features"]
)
next_page = next(
(
link["href"]
for link in feature_collection.get("links", [])
if link.get("rel") == "next"
),
None,
)The bounding box filter excludes records without matching coordinates. Open the returned
FeatureCollection directly in QGIS or ArcGIS, or load it with GeoPandas. Follow the item in
links whose rel is next to retrieve another page.
Sync financial intelligence into a warehouse
The semantic /api/v2/finint collection returns JSON with pagination in meta.page and
links.next. Merge on the canonical public id, and treat the returned next link as an opaque,
filter-bound continuation.
import os
from urllib.parse import urljoin
import requests
origin = "https://api.y2.dev"
url = f"{origin}/api/v2/finint?limit=500"
headers = {"Authorization": f"Bearer {os.environ['Y2_API_KEY']}"}
while url:
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
page = response.json()
warehouse.merge("y2_finint_facts", page["data"], key="id")
next_link = page["links"]["next"]
url = urljoin(origin, next_link) if next_link else None/api/v2/finint does not offer NDJSON. If a row-oriented stream is required, use
/api/v1/osint/finint?format=ndjson; that endpoint returns at most 100 rows per request and
exposes continuation in X-Y2-Next-Cursor.
Model semantic fields as facts and dimensions. Do not make deprecated epoch aliases or raw provider payloads part of the warehouse contract.
Build a durable change pipeline
Use webhooks for low-latency report notifications and /api/v2/changes to detect missed or
updated resources. A bearer API key receives global intelligence changes plus changes belonging
to its own user or workspace; it does not receive another tenant's records.
async function ingestChanges(savedWatermark?: string) {
const first = new URL("https://api.y2.dev/api/v2/changes");
first.searchParams.set("limit", "500");
if (savedWatermark) first.searchParams.set("watermark", savedWatermark);
let next: URL | null = first;
while (next) {
const response = await requireSuccess(
await fetch(next, {
headers: { Authorization: `Bearer ${process.env.Y2_API_KEY}` },
}),
);
const page = await response.json();
// Implement this as one transaction: apply every resource change or
// tombstone, then persist the page watermark as the final write.
await applyPageAtomically(page.data, page.meta.watermark);
next = page.links.next
? new URL(page.links.next, "https://api.y2.dev")
: null;
}
}The watermark is an exclusive checkpoint. Reusing the last committed value after a failure is safe; saving it before resource writes commit can lose changes. A tombstone instructs the consumer to remove or retire the named resource.
For webhook requests, verify HMAC-SHA256 over the exact raw request bytes before parsing JSON.
Deduplicate on the CloudEvent id (also sent as Idempotency-Key and X-Y2-Event-Id) before
performing side effects, and acknowledge a duplicate with a 2xx response.
Y2 does not run an automatic retry loop for failed webhook deliveries. Use the change feed as the repair path; five consecutive delivery failures disable the webhook configuration.