Y2 Elite workspaces are rolling out for teams
Y2Y2Docs

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:

package.json
{
  "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:

src/y2-client.ts
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:

src/news.ts
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.

src/profiles.ts
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:

src/reports.ts
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-After on 429 and retry only safe or idempotent requests.
  • Log X-Request-Id without logging credentials or full payment payloads.
  • Treat 404 as non-disclosing for tenant-owned resources.
  • Test special media types separately from JSON.