Build a TypeScript Client
Generate Y2 API types and add a small server-side fetch transport
Use generated OpenAPI types with native fetch when you want current schema coverage without
depending on release-specific SDK method names. This approach works in Node.js, Bun, serverless
functions, and workers with standards-compatible fetch APIs.
Keep the API key server-side
Do not instantiate a bearer client in browser-delivered code. Put Y2 calls behind your own
server, worker, or API route so Y2_API_KEY never enters a public bundle.
1. Generate declarations
Install the same generator used by the Y2 platform repository:
Commit the generated declaration or pin the OpenAPI input in your build pipeline. Regeneration from a moving production URL should be an intentional compatibility update, not an invisible deploy step.
Add a repeatable script:
{
"scripts": {
"generate:y2-types": "openapi-typescript https://y2.dev/api/openapi.yaml --immutable --alphabetize --output src/y2-api.d.ts"
}
}2. Create a JSON transport
Set the credential in the runtime environment:
export Y2_API_KEY="y2_..."Then create a transport that preserves Y2 Problem Details and rate-limit context:
import type { components } from "./y2-api";
const API_ORIGIN = "https://api.y2.dev";
const API_V1 = `${API_ORIGIN}/api/v1`;
export type Y2Problem = components["schemas"]["ErrorResponse"];
export class Y2ApiError extends Error {
constructor(
readonly status: number,
readonly problem: Y2Problem | null,
readonly retryAfter: string | null,
readonly requestId: string | null,
) {
super(problem?.detail ?? `Y2 request failed with HTTP ${status}`);
}
}
function isProblem(value: unknown): value is Y2Problem {
return (
typeof value === "object" &&
value !== null &&
"status" in value &&
"code" in value &&
"detail" in value
);
}
function resolveUrl(pathOrUrl: string): string {
if (pathOrUrl.startsWith("https://")) return pathOrUrl;
if (pathOrUrl.startsWith("/api/")) return `${API_ORIGIN}${pathOrUrl}`;
return `${API_V1}${pathOrUrl}`;
}
export async function y2Json<T>(
pathOrUrl: string,
init: RequestInit = {},
): Promise<T> {
const apiKey = process.env.Y2_API_KEY;
if (!apiKey) throw new Error("Y2_API_KEY is required");
const headers = new Headers(init.headers);
headers.set("Authorization", `Bearer ${apiKey}`);
if (!headers.has("Accept")) headers.set("Accept", "application/json");
if (init.body && !headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
const response = await fetch(resolveUrl(pathOrUrl), {
...init,
headers,
signal: init.signal ?? AbortSignal.timeout(30_000),
});
const text = await response.text();
let payload: unknown = null;
if (text) {
try {
payload = JSON.parse(text);
} catch {
payload = text;
}
}
if (!response.ok) {
throw new Y2ApiError(
response.status,
isProblem(payload) ? payload : null,
response.headers.get("Retry-After"),
response.headers.get("X-Request-Id"),
);
}
return payload as T;
}y2Json is intentionally JSON-only. Use separate functions for NDJSON, Markdown, plain text, audio
redirects, and Agent Y2 SSE so a content-type mismatch cannot be silently cast to a JSON model.
3. Type a read operation
Extract parameter and response types from the path entry:
import type { paths } from "./y2-api";
import { y2Json } from "./y2-client";
type ListNewsQuery = NonNullable<
paths["/news"]["get"]["parameters"]["query"]
>;
type ListNewsResponse =
paths["/news"]["get"]["responses"][200]["content"]["application/json"];
export async function listNews(query: ListNewsQuery): Promise<ListNewsResponse> {
const params = new URLSearchParams();
if (query.topics) params.set("topics", query.topics);
if (query.limit !== undefined) params.set("limit", String(query.limit));
if (query.cursor) params.set("cursor", query.cursor);
if (query.format) params.set("format", query.format);
return y2Json<ListNewsResponse>(`/news?${params}`);
}
const page = await listNews({ topics: "markets,macro", limit: 10 });
for (const item of page.data) {
console.log(item.title, item.url);
}Use GET /news/feeds to discover the current 40-topic catalog. Current items use title, summary,
and a nested sentiment.label/sentiment.value object—not older top-level signal and sentiment
scalar fields.
4. Type a mutation
Profile creation returns a PublicProfile directly in data. Its ID is data.id; profile creation
also creates a subscription, but that sub_... ID is obtained from GET /profiles.
import type { paths } from "./y2-api";
import { y2Json } from "./y2-client";
type CreateProfileBody =
paths["/profiles"]["post"]["requestBody"]["content"]["application/json"];
type CreateProfileResponse =
paths["/profiles"]["post"]["responses"][201]["content"]["application/json"];
export async function createProfile(
body: CreateProfileBody,
idempotencyKey: string,
): Promise<CreateProfileResponse> {
return y2Json<CreateProfileResponse>("/profiles", {
method: "POST",
headers: { "Idempotency-Key": idempotencyKey },
body: JSON.stringify(body),
});
}
const created = await createProfile(
{
name: "Critical Supplier Risk",
topic: "Monitor disruption affecting strategic semiconductor suppliers.",
frequency: "daily",
scheduleTimeOfDay: "08:00",
},
"supplier-risk-2026-07-21",
);
console.log(created.data.id); // prf_...Use PATCH /profiles/{profileId} for partial changes and PUT only for full replacement. Preserve
the ETag returned by create or update when you need If-Match concurrency protection.
5. Paginate reports
Follow the response link instead of decoding or modifying an opaque cursor:
import type { paths } from "./y2-api";
import { y2Json } from "./y2-client";
type ReportsPage =
paths["/reports"]["get"]["responses"][200]["content"]["application/json"];
export async function* reportsForProfile(profileId: string) {
let next: string | null = `/reports?profileId=${encodeURIComponent(profileId)}&limit=20`;
while (next) {
const page: ReportsPage = await y2Json<ReportsPage>(next);
yield* page.data;
next = page.links.next;
}
}
for await (const report of reportsForProfile("prf_0123456789abcdef01234567")) {
console.log(report.id, report.summary);
}The list and default detail response are compact. Request include=content,sources or
Accept: text/markdown only when the consumer needs those representations.
Call Intel v2 correctly
The OpenAPI path key includes the full /api/v2 prefix, and the operation overrides the document
server with https://api.y2.dev:
type IncidentList =
paths["/api/v2/incidents"]["get"]["responses"][200]["content"]["application/json"];
const incidents = await y2Json<IncidentList>(
"https://api.y2.dev/api/v2/incidents?limit=20",
);Do not compose Intel URLs by adding /api/v1 to the path.
Handle special representations
Request application/x-ndjson, read response.body as a stream, split complete newline-delimited
records, and retain X-Y2-Next-Cursor. Do not call response.json() on the whole stream.
Production checklist
- Pin or review generated declaration changes.
- Give the key only the required scopes.
- Use idempotency keys on supported create operations.
- Follow
Retry-Afteron429and retry only safe or idempotent requests. - Log
X-Request-Idwithout logging credentials or full payment payloads. - Treat
404as non-disclosing for tenant-owned resources. - Test special media types separately from JSON.