# TypeScript / Node.js integration

Run these examples on your server. They use the language runtime HTTP client, not a platform SDK. No request is sent until you run a command. Models must be configured and enabled before use.

## Setup

Runtime: **Node.js 22.18+**. [Download complete source](/examples/typescript.ts).

Set your deployment base URL, including `/v1` exactly once, and a server-side API key. `https://xxxrouter.com` is the official service domain; for local testing, replace it with your deployment origin. The service is not yet deployed and integration certification is not complete.

```sh
export XXXROUTER_BASE_URL='https://xxxrouter.com/v1'
export XXXROUTER_API_KEY='YOUR_SERVER_SIDE_KEY'
```

## Run one path

`chat` reads SSE deltas and final usage; `image` receives a synchronous image; `async-image` and `video` handle HTTP 202 and query the returned task. Polling is bounded to approximately five minutes plus any in-flight read; a timeout preserves the task for later reads.

```sh
node --experimental-strip-types typescript.ts chat
# node --experimental-strip-types typescript.ts image
# node --experimental-strip-types typescript.ts async-image
# node --experimental-strip-types typescript.ts video
```

## Idempotency and failure handling

A new run creates a new idempotency key and can incur a new charge. In an application, persist that key with the exact body before submitting. Set `XXXROUTER_REQUEST_ID` only to resume the same logical request with the same model, payload, API key and source. Do not rerun with a new key after a transport failure. These clients perform one generation POST and never retry it automatically.

Record the response `X-Request-ID`. HTTP errors retain the response body for diagnosis. A task with `status=unknown` stops automatic polling and requires later read/reconciliation; it is not a successful or free request. A stream error event or missing `[DONE]` is an interruption. Missing usage is not zero cost. Stopping the connection does not cancel accepted work. Do not log API keys or signed asset URLs.

## Image inputs and result files

For image-to-image or image-to-video, pass a public HTTPS URL in `image` when the model supports reference images. The public inference API takes URLs, not file bytes. Playground can upload temporary input images or videos to private R2 storage; only signed-in, verified users can use it. Generated results are never downloaded, stored or rehosted by XXXRouter. Successful responses return the original generated-media URL unchanged. The original host controls availability, expiry and browser cross-origin behavior. Save results before expiry; querying a completed task returns the saved URL and does not renew it. Request metadata and result URLs may be retained for recovery; generated-media bytes are never retained.

## Complete example

The code is an integration example, not proof that a live model or payment channel is enabled. Do not place it in a browser bundle.

```typescript
// Node.js 22.18+; run: node --experimental-strip-types typescript.ts chat
// Uses native fetch, including explicit HTTP and SSE error handling.
import { randomUUID } from 'node:crypto';

type Result = { status?: string; task_id?: string; error?: unknown; [key: string]: any };
const base = process.env.XXXROUTER_BASE_URL?.replace(/\/$/, ''); // Include /v1.
const key = process.env.XXXROUTER_API_KEY;
if (!base || !key) throw new Error('Set XXXROUTER_BASE_URL and XXXROUTER_API_KEY');
const cases: Record<string, [string, Record<string, unknown>]> = {
  chat: ['chat/completions', { model: 'deepseek-v4-flash', messages: [{ role: 'user', content: 'Explain HTTP in one sentence.' }], max_tokens: 256, stream: true, stream_options: { include_usage: true } }],
  image: ['images/generations', { model: 'seedream-4.5', prompt: 'A ceramic cup in natural light', size: '2048x2048', n: 1 }],
  'async-image': ['images/generations', { model: 'nano-banana-pro', prompt: 'A ceramic cup in natural light', size: '1024x1024', n: 1 }],
  video: ['videos/generations', { model: 'kling-3.0', prompt: 'A slow camera move across a mountain landscape', duration: 5, aspect_ratio: '16:9' }],
};
async function request(path: string, payload?: unknown, idem?: string): Promise<Response> {
  const headers: Record<string, string> = { Authorization: `Bearer ${key}` };
  if (idem) headers['Idempotency-Key'] = idem;
  if (payload) headers['Content-Type'] = 'application/json';
  const response = await fetch(`${base}/${path}`, {
    method: payload ? 'POST' : 'GET', headers, redirect: 'error',
    body: payload ? JSON.stringify(payload) : undefined,
    signal: AbortSignal.timeout(payload ? 300_000 : 30_000),
  });
  console.error('request_id=', response.headers.get('X-Request-ID'));
  if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  return response;
}
async function stream(response: Response): Promise<void> {
  if (!response.body) throw new Error('Empty response body');
  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = '';
  let data: string[] = [];
  const consume = (): boolean => {
    const value = data.join('\n'); data = [];
    if (value === '[DONE]') return true;
    if (!value) return false;
    const chunk = JSON.parse(value);
    if (chunk.error) throw new Error(JSON.stringify(chunk.error));
    for (const choice of chunk.choices ?? []) process.stdout.write(choice.delta?.content ?? '');
    if (chunk.usage) console.log('\nusage:', chunk.usage);
    return false;
  };
  try {
    while (true) {
      const { value, done } = await reader.read();
      buffer += value ?? '';
      let end: number;
      while ((end = buffer.indexOf('\n')) !== -1) {
        const line = buffer.slice(0, end).replace(/\r$/, ''); buffer = buffer.slice(end + 1);
        if (!line && consume()) return;
        if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /, ''));
      }
      if (done) break;
    }
    if (buffer.startsWith('data:')) data.push(buffer.slice(5).trimStart());
    if (consume()) return;
    throw new Error('Stream interrupted. Keep request ID and idempotency key; do not resubmit.');
  } finally { await reader.cancel(); reader.releaseLock(); }
}
async function finish(result: Result): Promise<void> {
  const deadline = Date.now() + 300_000;
  while (result.task_id && !['succeeded', 'failed', 'unknown'].includes(result.status ?? '')) {
    if (Date.now() >= deadline) throw new Error(`Query task ${result.task_id} later; do not resubmit`);
    await new Promise(resolve => setTimeout(resolve, 2000));
    result = await (await request(`tasks/${encodeURIComponent(result.task_id)}`)).json();
  }
  console.log(result);
  if (result.status === 'unknown') throw new Error('Outcome unconfirmed; retain IDs and wait for reconciliation.');
  if (result.status === 'failed') throw new Error('Generation failed; inspect error and billing.');
}
const [path, payload] = cases[process.argv[2] ?? 'chat'];
const idem = process.env.XXXROUTER_REQUEST_ID || randomUUID();
console.error('idempotency_key=', idem); // Persist alongside the exact payload before submitting.
const response = await request(path, payload, idem);
if (response.headers.get('content-type')?.includes('text/event-stream')) await stream(response);
else await finish(await response.json());
```

## Related reference

[chat](/docs?page=chat&lang=en) · [images](/docs?page=images&lang=en) · [videos](/docs?page=videos&lang=en) · [tasks](/docs?page=tasks&lang=en) · [uploads](/docs?page=uploads&lang=en) · [errors](/docs?page=errors&lang=en)

[Runtime documentation](https://nodejs.org/api/globals.html#fetch) · 2026-10-26
