deeprelayDocs
UPDATED 2026.09.22READ 7 MINSUGGEST AN EDIT →
CH·09SDKS

TypeScript SDK.

@deeprelay/sdk is the official typed TypeScript client. It is generated from the same OpenAPI spec the public REST API is served against, so every endpoint has a typed method and every response has a generated interface. Node 22+.

It covers the serverless relay: inference (chat, embeddings, image and video generation), the model catalog, the subscription and billing lifecycle, usage reporting, webhooks, and long-running operations. Six API classes — InferenceApi, BillingApi, UsageApi, WebhooksApi, OperationsApi, MetaApi — and that is the whole client.

§ 09.1Install

SHELL
npm install @deeprelay/sdk

The package is CJS-primary: require() resolves under plain node with no bundler and no build step — that is the path the examples below use and the path CI exercises. Bundlers (webpack, vite, esbuild, …) automatically pick up the ESM build via the module field. Importing the ESM output directly under plain Node is not supported.

§ 09.2Base URL and authentication

Base URLhttps://api.deeprelay.ai/v1
AuthAuthorization: Bearer deeprelay_live_…

Get a key with deeprelay login (it stores a deeprelay_live_… key in ~/.config/deeprelay/credentials.json) or mint one in the dashboard. Pass it as accessToken on the Configuration and the SDK sets the Authorization header for you.

Only /v1/health and /v1/openapi.json are reachable without a key. Every other endpoint — including the model catalog — is account-scoped, so the quick start below reads the key from the environment rather than hard-coding one.

§ 09.3Quick start

Nothing billable: a keyless health check, then the model catalog. Save as quickstart.js and run it with node quickstart.js.

JS
const { Configuration, MetaApi, InferenceApi } = require('@deeprelay/sdk');

const BASE_URL = 'https://api.deeprelay.ai/v1';

async function main() {
  // /health takes no key.
  const anon = new Configuration({ basePath: BASE_URL });
  console.log((await new MetaApi(anon).getHealth()).status);

  const cfg = new Configuration({
    basePath: BASE_URL,
    accessToken: process.env.DEEPRELAY_API_KEY, // e.g. from `deeprelay login`
  });

  const models = await new InferenceApi(cfg).listModels({});
  for (const m of models.data.slice(0, 5)) {
    const plan = m.planCovered ? 'plan' : 'pay-as-you-go';
    console.log(`${m.id.padEnd(44)} ${m.modality.padEnd(10)} ${plan}`);
  }
}

main();
SHELL
export DEEPRELAY_API_KEY=deeprelay_live_...
node quickstart.js

listModels returns the OpenAI-shaped {object, data} envelope, not a cursor page. The genuinely paginated endpoints — listUsage, listDeposits, listWebhookEndpoints — return data plus a nextCursor that is null on the last page; pass it back as cursor to walk forward.

§ 09.4Your first completion

This one does spend money: on the plan's quota if the model is covered, and pay-as-you-go otherwise.

JS
const { Configuration, InferenceApi } = require('@deeprelay/sdk');

const cfg = new Configuration({
  basePath: 'https://api.deeprelay.ai/v1',
  accessToken: process.env.DEEPRELAY_API_KEY,
});

async function main() {
  const resp = await new InferenceApi(cfg).createChatCompletion({
    chatCompletionRequest: {
      model: 'deeprelay/llama-3.3-70b-instruct',
      messages: [{ role: 'user', content: 'Say hello in six words.' }],
      maxTokens: 64,
    },
  });
  console.log(resp.choices[0].message.content);
  console.log(`tokens: ${resp.usage.totalTokens}`);
}

main();

§ 09.5Subscription and billing

The flat tier covers a published list of models; anything outside it, and anything past the quota, bills pay-as-you-go at list rates. Four calls cover the whole lifecycle.

The snippets below are bodies, not whole programs: each one awaits, so it belongs inside an async function main() like the quick start above, with cfg already in scope. Plain CommonJS has no top-level await.

What does my org have? Read-only, any member of the org:

JS
const { Configuration, BillingApi } = require('@deeprelay/sdk');

const sub = await new BillingApi(cfg).getSubscription();

console.log(`subscribed=${sub.subscribed} status=${sub.status}`);
console.log(`plan: ${sub.plan.name}`);

// Absent until the billing provider has reported a period — so undefined for
// an org that has never subscribed. Guard before formatting.
if (sub.currentPeriodEnd) {
  console.log(`  period ends ${sub.currentPeriodEnd.toISOString().slice(0, 10)}`);
}
if (sub.cancelAtPeriodEnd) {
  console.log('  cancels at period end — coverage continues until then');
}

if (sub.usage) {  // undefined when the org is not subscribed
  const { quota: q, usage: u } = sub;
  console.log(`  input  ${u.weightedInputTokens} / ${q.inputTokensMonthly}`);
  console.log(`  output ${u.weightedOutputTokens} / ${q.outputTokensMonthly}`);
  console.log(`  weekly ${u.weeklyWeightedTokens} / ${q.weeklyTokens}`);
}

quota.paygDiscountBp is deprecated and always 0. There is no subscriber discount on pay-as-you-go spend; do not display it.

Will this call be covered? inferencePreflight answers "what happens if I call this model right now?" against the same gates that judge the real request, without sending, counting, charging, or reserving anything:

JS
const pre = await new InferenceApi(cfg).inferencePreflight({
  model: 'deeprelay/llama-3.3-70b-instruct',
});
console.log(`${pre.verdict}: ${pre.message}`);
console.log(`  planCovered=${pre.planCovered} subscribed=${pre.subscribed} funded=${pre.funded}`);

The case it exists for is verdict: 'warn' with reason: 'not_plan_covered': a subscriber calling a model outside the plan is served and is charged pay-as-you-go, and nothing in the completion response says so. funded is a boolean and never a figure — this route is on the inference read scope and must not disclose the balance. Use getBalance for the number.

Start a subscription. Requires billing:write and org-admin, because subscribing spends organization money:

JS
const session = await new BillingApi(cfg).createSubscriptionCheckout({});
console.log(`Open this to subscribe: ${session.checkoutUrl}`);

This does not subscribe anyone. Checkout is a hosted page that needs a browser and a card, so the caller's job is to put the returned URL in front of a human. The subscription becomes active when payment completes, which is not synchronous with this call — poll getSubscription to confirm. An empty request means "use every default"; pass subscriptionCheckoutRequest to override successUrl / cancelUrl, or to pin planKey to the plan you displayed so an unknown key 400s rather than silently buying a different tier.

Cancel, or fix a declined card. Both live in the hosted billing portal:

JS
const portal = await new BillingApi(cfg).createSubscriptionPortal({});
console.log(`Manage billing here: ${portal.portalUrl}`);

Cancelling in the portal ends the subscription at the close of the current period; coverage continues until then and getSubscription reports cancelAtPeriodEnd: true. An organization that has never paid for anything gets a 404 — there is no billing account to manage, and this endpoint deliberately does not create one as a side effect of looking.

The same client reaches getBalance, getSpendingLimit / updateSpendingLimit, and the stablecoin deposit methods. Each API class's methods are documented in the generated tree.

§ 09.6TypeScript and ESM

The package ships its own type declarations (types dist/index.d.ts), so the same entry point is fully typed. In a .ts file, TypeScript's typed-CJS import form keeps you on the same CommonJS path:

TS
import Deeprelay = require('@deeprelay/sdk');

async function main(): Promise<void> {
  const cfg = new Deeprelay.Configuration({
    basePath: 'https://api.deeprelay.ai/v1',
    accessToken: process.env.DEEPRELAY_API_KEY,
  });
  const sub: Deeprelay.Subscription = await new Deeprelay.BillingApi(cfg).getSubscription();
  console.log(`${sub.plan.name}: subscribed=${sub.subscribed}`);
}

main();

If your project is compiled by tsc or a bundler (vite, webpack, esbuild, …), the standard ESM syntax — import { Configuration } from '@deeprelay/sdk' — works too: tsc with module: commonjs emits the require above, and bundlers pick up the ESM build through the module field. What is not supported is loading dist/esm/ directly under plain Node with no build step; use the require() form there.

§ 09.7Errors

A non-2xx response rejects with ResponseError, which carries the raw fetch Response:

JS
const { Configuration, BillingApi, ResponseError } = require('@deeprelay/sdk');

try {
  // an org with no billing account has no portal to open
  await new BillingApi(cfg).createSubscriptionPortal({});
} catch (e) {
  if (e instanceof ResponseError) {
    console.log(`api error ${e.response.status}:`, await e.response.text());
  } else {
    throw e;
  }
}
OUTPUT
api error 404: {"type":"https://api.deeprelay.ai/errors/not_found","title":"Not Found",
"status":404,"detail":"no billing account for this organization","code":"not_found","request_id":"…"}

The body is an RFC 9457 problem document; request_id is what to quote in a support email. Inference endpoints are the exception — they return OpenAI's error envelope instead, so that OpenAI-compatible clients parse failures the way they already do.

§ 09.8Versioning: pre-1.0 convention

The SDK is versioned independently of the API (the API is v1 and stays v1). While the SDK is on 0.x, it follows this convention:

  • Breaking change → minor bump
  • Everything else (new endpoints, new fields, doc changes) → patch bump

npm's caret operator already enforces exactly this at 0.x: a caret range on a 0.x release (^0.<minor>.<patch>) accepts patch releases only and will not pull a minor bump, so the default npm install range keeps you on non-breaking updates. The Python package needs an explicit pin to get the same protection; pip has no equivalent rule.

Breaking releases are called out in a ⚠ Breaking section of the GitHub Release notes for the release tag. At 0.x the version number alone will not warn you, so the release notes are the channel to read.

1.0.0 is a deliberate stability promotion, made as a human call; it never happens automatically by rolling over from 0.x.