> ## Documentation Index
> Fetch the complete documentation index at: https://modelrunner.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# JavaScript Client

> Install, configure, and use the ModelRunner JavaScript/TypeScript SDK.

## Overview

ModelRunner provides a unified JavaScript/TypeScript SDK to call any supported model with a consistent interface. Use it in Node.js, serverless runtimes, and—via a proxy—in the browser.

<Callout>
  For client-side apps, never expose secrets. Use the proxy pattern shown below to safely forward requests.
</Callout>

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm i @modelrunner/client
  ```

  ```bash yarn theme={null}
  yarn add @modelrunner/client
  ```

  ```bash pnpm theme={null}
  pnpm add @modelrunner/client
  ```
</CodeGroup>

## Configure credentials

Configure the client with a single key. Environment variables are recommended on the server.

```ts theme={null}
import { modelrunner } from "@modelrunner/client";

modelrunner.config({
  credentials: process.env.MODELRUNNER_KEY,
});
```

<Tip>
  Get your credentials from your ModelRunner account. Keep them server-only.
</Tip>

## Call a model

Leverage the queue for long-running tasks. Optionally listen to queue updates.

```ts theme={null}
import { modelrunner } from "@modelrunner/client";

const result = await modelrunner.subscribe("bytedance/sdxl-lightning-4step", {
  input: { "prompt": "two friends cooking together" },
  onQueueUpdate(update) {
    if (update.status === "IN_QUEUE") {
      console.log(`Position in queue: ${update.position}`);
    }
  },
});

console.log(result.data.output);
```

<Warning>
  `run` and `subscribe` resolve to a wrapper, not the model record itself — so `result.output` is always `undefined`. The model's own fields live one level down, under `result.data`.
</Warning>

```ts theme={null}
type Result<T> = {
  data: T;           // the model record — `output`, `logs`, and the rest
  requestId: string; // useful for logs and support
};
```

## Tag requests with metadata

Attach your own flat string map to a request — job ids, environments, batch labels — by passing `metadata` next to `input`. It's supported on `run`, `subscribe`, `queue.submit`, and `stream`:

```ts theme={null}
const result = await modelrunner.subscribe("bytedance/sdxl-lightning-4step", {
  input: { prompt: "two friends cooking together" },
  metadata: { project: "onboarding-demo", env: "prod" },
});
```

`metadata` is sent as a reserved **top-level sibling** of your input fields — never nested inside `input`, and never forwarded to the model. It's stored on the request so you can filter your history by it later; the client itself has no read-back or filtering API, so read tags back through the [request lifecycle metadata filter](/docs/guides/request-lifecycle#tagging-requests-with-metadata) or the MCP `list_my_requests` tool.

The client validates `metadata` before dispatching, mirroring the API:

| Constraint | Value                         |
| ---------- | ----------------------------- |
| Max keys   | 16                            |
| Key length | 1–64 characters               |
| Values     | strings only, ≤512 characters |

A violation throws a `ValidationError` (`"Invalid metadata"`, status `400`) before any request is sent — every offending key is reported at once and is addressable with `error.getFieldErrors("<key>")`. Omitting `metadata` leaves the body untouched; an explicit `{}` is valid and is sent as-is.

<Warning>
  `metadata` is reserved at the top level of the request body. If a model's own input schema declares a field named `metadata`, sending it this way is rejected by that model's validation — it's treated as a request tag, not model input.
</Warning>

## Get called back with webhooks

Instead of holding a `subscribe` open, pass a `webhookUrl` and ModelRunner POSTs the result to you when the request settles. Nothing is lost if your process restarts mid-request, which is what makes this the right choice for long video and training jobs.

```ts theme={null}
const { request_id } = await modelrunner.queue.submit("bytedance/sdxl-lightning-4step", {
  input: { prompt: "two friends cooking together" },
  webhookUrl: "https://example.com/hooks/modelrunner",
  webhookEvents: ["completed"], // optional — this is the default
});
```

`webhookUrl` also works on `subscribe`, if you want both a callback and in-process updates. See the [webhooks guide](/docs/guides/webhooks) for the events, the retry schedule, and the full payload shape.

<Warning>
  **Requires `@modelrunner/client` 1.2.0 or newer.** In earlier versions this option was accepted and silently ignored — no callback was ever sent, and no error was raised. Upgrading also means the URL is now validated, so a bad value that used to be quietly dropped will fail the submit with a `400`.
</Warning>

### Verify a delivery

Every delivery is signed. Fetch your signing secret **once** and keep it in your server environment — never in a browser:

```ts theme={null}
const { key } = await modelrunner.webhooks.getSecret();
```

Then verify each delivery against the **raw** request body:

```ts theme={null}
import express from "express";
import { modelrunner, WebhookVerificationError } from "@modelrunner/client";

app.post(
  "/hooks/modelrunner",
  // The signature covers the delivered bytes, so the raw body is required.
  // express.json() would parse and discard it.
  express.raw({ type: "application/json" }),
  async (req, res) => {
    let payload;
    try {
      payload = await modelrunner.webhooks.verify({
        secret: process.env.MODELRUNNER_WEBHOOK_SECRET,
        headers: req.headers,
        body: req.body,
      });
    } catch (error) {
      // WebhookVerificationError — a missing header, a stale timestamp, or a
      // signature that doesn't match. Treat every case the same way.
      return res.sendStatus(401);
    }

    res.sendStatus(200); // acknowledge first, then do the work
    await handleResult(payload);
  },
);
```

`verify` is async because it uses the Web Crypto API, which is what lets the same code run in Node, serverless runtimes and edge workers. It returns the parsed payload and throws `WebhookVerificationError` on any failure.

<Warning>
  **Read `billingStatus`, not `status`.** A generation that failed at the provider still arrives as `status: "COMPLETED"`, with `billingStatus: "failed"` and a populated `error`. Code that treats `status` alone as success will report every failure as a success.

  ```ts theme={null}
  if (payload.billingStatus === "charged" || payload.billingStatus === "partial") {
    console.log(payload.output);
  }
  ```
</Warning>

Two more things your endpoint must do, both easy to get wrong:

* **Return `2xx` directly.** Redirects are not followed, so a `301` — a missing trailing slash, an `http`→`https` upgrade — is recorded as a failed attempt and you see nothing but silence.
* **Deduplicate on the `webhook-id` header.** Delivery is at-least-once and that id is stable across retries of the same delivery.

### Rotate the secret

```ts theme={null}
const { key } = await modelrunner.webhooks.rotateSecret();
```

The previous secret keeps verifying for **24 hours**, and deliveries are signed with both during that window — so you can roll the new value out without dropping anything. Pass both to bridge the gap:

```ts theme={null}
await modelrunner.webhooks.verify({
  secrets: [process.env.WEBHOOK_SECRET_NEXT, process.env.WEBHOOK_SECRET_CURRENT],
  headers: req.headers,
  body: req.body,
});
```

<Note>
  Rotating twice inside that window ends it early and breaks receivers still holding the original secret, so this call is never retried automatically. Rotate once, deploy, then rotate again if you need to.
</Note>

## Upload files

Upload local files to ModelRunner storage and receive a temporary URL you can pass to model inputs (for example, image or audio URLs).

<CodeGroup>
  ```ts Node.js (ESM) theme={null}
  import { modelrunner } from "@modelrunner/client";
  import fs from "node:fs";

  const fileBuffer = fs.readFileSync("./image.jpeg");
  const url = await modelrunner.storage.upload(new Blob([fileBuffer]));
  console.log(url);
  ```
</CodeGroup>

You can then pass the returned `url` to your model input:

```ts theme={null}
const result = await modelrunner.subscribe("swook/inspyrenet", {
  input: { image_path: url,},
});
```

<Tip>
  In Node.js, `Blob` is available in modern runtimes (Node 18+). If you use an older version, consider upgrading or using a compatible polyfill.

  The storage service accepts any binary file type (images, audio, video, documents).
</Tip>

## Using the client in browsers (via proxy)

To keep secrets safe, use the official server proxy so credentials stay on your server.

<CodeGroup>
  ```ts server (Next.js Page Router) theme={null}
  // /pages/api/modelrunner/proxy.ts
  export { handler as default } from "@modelrunner/server-proxy/nextjs";
  ```

  ```ts server (Next.js App Router) theme={null}
  // /app/api/modelrunner/proxy/route.ts
  import { route } from "@modelrunner/server-proxy/nextjs";
  export const { GET, POST, PUT } = route;
  ```

  ```ts client (Frontend) theme={null}
  import { modelrunner } from "@modelrunner/client";

  modelrunner.config({
    proxyUrl: "/api/modelrunner/proxy",
  });

  const result = await modelrunner.subscribe("bytedance/sdxl-lightning-4step", {
    input: { prompt: "two friends cooking together" },
  });
  ```
</CodeGroup>

<Warning>
  Set `MODELRUNNER_KEY` in your server environment. The proxy reads this value to authenticate requests. Never expose it in the browser.
</Warning>
