---
title: "Integration Recipes"
description: "Build grounded agents, GIS imports, warehouse syncs, and durable intelligence pipelines"
canonical: https://y2.dev/docs/api/integration-recipes/
---

# Integration Recipes

Build grounded agents, GIS imports, warehouse syncs, and durable intelligence pipelines

Use these patterns when you already have an API key and need to connect Y2 to another system. They use the canonical schemas in the [OpenAPI document](https://y2.dev/api/openapi.yaml). 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-types
```

External 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

Replace `warehouse.merge` with your database's idempotent upsert operation. This example uses Python and `httpx`.

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.

Implement `applyPageAtomically` with a database transaction. Use `requireSuccess` from the first recipe.

```
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.

Temporary webhook failures receive bounded retries. A paused destination requires testing before reactivation. Use the change feed to recover missed state. See [delivery recovery](https://y2.dev/docs/api/webhooks#handle-attempts-and-failures).

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

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

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

[Migration guide](https://y2.dev/docs/api/migration-v1-1)
