Node.js server integration
Two patterns for a Node.js backend: mint session tokens for a browser frontend, or call the API directly from server-only code.
Node.js on the backend covers two different jobs, both on this page: (1) minting session tokens so a browser frontend (plain webpage or React) can call the API safely, and (2) calling the API directly from server-only code — a cron job, a webhook handler, a CLI, anything with no browser involved. Pick the section that matches what you’re building.
Minting tokens for a browser frontend
This is the pattern from Choose your auth mode made concrete: a token-minting endpoint on your server, and a browser page (see Plain webpage integration or React SPA integration for the browser side) that calls it before calling the SDK.
1. Add a token endpoint to your backend
The recommended way to mint a session JWT is to have your backend call the platform’s own
POST /auth/token endpoint, authenticating with the client_id/client_secret pair from Get
your API key. Your backend never signs anything itself — it just
forwards the request and hands the returned token to the browser.
const CLIENT_ID = process.env.QURAVIN_CLIENT_ID; // from the Console
const CLIENT_SECRET = process.env.QURAVIN_CLIENT_SECRET; // from the Console, one-time reveal
app.get("/ai-token", requireUserLogin, async (req, res) => {
const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");
const r = await fetch("https://api.quravin.com/auth/token", {
method: "POST",
headers: {
Authorization: `Basic ${basic}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
subject: req.user.id, // your app's user id — becomes the JWT `sub`
ttl_seconds: 900, // optional; 60-3600, default 900 (15 min)
// pipelines: ["translate-string"], // optional — omit to get your app's full allow-list
}),
});
if (!r.ok) return res.status(502).json({ error: "token_mint_failed" });
const { token, expires_in } = await r.json();
res.json({ token, expires_in });
});
POST /auth/token responds with:
{ "token": "eyJhbGciOi...", "token_type": "Bearer", "expires_in": 900 }
The pipelines you request are intersected with your Application’s allowed_pipelines (set in
the Console — see Get your API key); omit the field to get
whatever your app is allowed to call. requireUserLogin is your own auth middleware — the token
only ever goes to a user your server has already authenticated.
Alternate: self-signing a JWT (legacy)
Some apps instead hold their own per-app signing secret (AI_PLATFORM_SIGNING_SECRET) and mint
the JWT locally with a library like jsonwebtoken, instead of calling POST /auth/token. This is a
real, supported mechanism, but it’s the legacy path — prefer POST /auth/token above unless you
have a specific reason to self-sign (e.g. you already run this way and don’t want to change).
import jwt from "jsonwebtoken";
const SECRET = process.env.AI_PLATFORM_SIGNING_SECRET; // NOT the client_secret — see below
const APP_ID = "your-app-id"; // your app's registered id
app.get("/ai-token", requireUserLogin, (req, res) => {
const token = jwt.sign(
{
iss: APP_ID,
sub: req.user.id, // your app's user id
aud: "ai-platform",
pipelines: ["translate-string"], // which pipelines this user may call
rate_limit: { rpm: 30 },
},
SECRET,
{ algorithm: "HS256", expiresIn: "15m" },
);
res.json({ token, expires_in: 15 * 60 });
});
This uses a different credential than client_id/client_secret. The signing secret isn’t
issued by the Console — it’s provisioned out-of-band by the platform operator, over a secure
channel, specifically for apps that need to self-sign. If you don’t already have one, use the
POST /auth/token path above instead of requesting one.
iss differs between the two minting paths — don’t assume a JWT payload you’re debugging always
has the same issuer. A token minted by POST /auth/token always has iss: "ai-platform" (the
platform itself signs it). A self-signed token (this section) has iss: <your-app-id> instead —
the platform still accepts it, keyed by that app id’s registered signing secret. Both use
aud: "ai-platform".
Which secret is which
Three different secrets show up across these auth docs, and their names overlap enough to cause mix-ups. This is the full list:
| Secret | Env var | Who holds it | Used for |
|---|---|---|---|
| Client secret | QURAVIN_CLIENT_SECRET (yours to name) | Your backend | Authorization: Basic client_id:client_secret on POST /auth/token — the recommended path above. |
| Per-app signing secret | AI_PLATFORM_SIGNING_SECRET | Your backend (only if self-signing) | Locally signing JWTs yourself, bypassing POST /auth/token — the legacy path in this section. |
| Platform JWT signing secret | (platform-internal, not exposed) | The platform operator only, never you | What the platform itself uses to sign tokens it mints via POST /auth/token. You never see or configure this. |
If you’re only ever calling POST /auth/token (the recommended path), you only need to think about
the client secret — the other two are internal/legacy concerns.
2. Your frontend fetches the token and calls the SDK
fetch("/ai-token", { credentials: "include" }) is a same-origin call to your own backend
route (step 1) — no CORS involved, just your usual cookie-based session. The SDK’s own call to our
API is a separate, cross-origin request the SDK handles itself; you don’t configure CORS for it.
The full browser-side code (plain webpage or React) lives in Plain webpage
integration and React SPA
integration — this endpoint is the piece those two guides assume
already exists.
That’s the whole loop: your server mints tokens, the browser never sees a secret, and every call is attributed to the user who made it.
Calling the API directly from server-only code
No browser involved — a cron job, a webhook handler, a CLI, a batch script. Use apiKey mode
directly; there’s no token to mint, no per-request round trip to POST /auth/token.
There’s no window for a <script> tag to attach to here, and the SDK’s own import { Quravin } from "@quravin/sdk" ergonomics aren’t on the public npm registry yet (ask your platform operator
if you need that path) — so for server-only Node.js code today, call the API directly over HTTP.
Node 18+ ships fetch globally, no extra dependency needed:
const API_BASE = process.env.QURAVIN_API_BASE;
const API_KEY = process.env.QURAVIN_API_KEY; // from the Console — see "Get your API key"
async function runPipeline(pipelineId, inputs) {
const submit = await fetch(`${API_BASE}/tickets`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
body: JSON.stringify({ mode: "pipeline", pipeline_id: pipelineId, inputs }),
});
const { ticket_id } = await submit.json();
while (true) {
await new Promise((r) => setTimeout(r, 1000));
const poll = await fetch(`${API_BASE}/tickets/${ticket_id}`, {
headers: { "x-api-key": API_KEY },
});
const ticket = await poll.json();
if (ticket.status === "DONE") return ticket.result;
if (ticket.status === "FAILED") throw new Error(ticket.error);
}
}
const out = await runPipeline("translate-string", { text: "Hello", target_language: "de" });
console.log(out.translation); // "Hallo"
See Direct API integration for the full request/response shapes and error tables this loop is built on.
This is the same x-api-key from Get your API key’s “What about a
static x-api-key?” section — safe to hold in server-only code (an env var, a secrets manager)
since it never ships to a browser. Every call is attributed to the app, not a specific user; if you
need per-user attribution even from server-only code, mint a session JWT per user instead (same
POST /auth/token call as above, just triggered by your own server logic instead of an incoming
browser request — then send it as Authorization: Bearer <jwt> instead of x-api-key).
Handling errors
A quota or rate-limit hit returns a typed error — surface it however fits your context (a user message in a browser flow, a retry/alert in a batch job):
try {
const out = await runPipeline("translate-string", { text: "Hello", target_language: "de" });
} catch (err) {
console.error("Translation failed:", err.message);
}
See Tool reference’s Errors section for the full list of status codes and what each one means for the authenticated path.
Going further
The SDK also supports a few patterns not shown above:
ai.runMany(...)— submit a batch of inputs (e.g. every line of a file) with bounded concurrency and per-item progress, instead of oneai.run()call per input.ai.button(selector, config)— wire a button directly to a pipeline call without writing your own click handler, polling, or disabled-state logic.- Raw-prompt mode — run a custom system/user prompt instead of a named pipeline. See Raw prompt mode for the full request/response shape, model selection, and how it differs from a named pipeline.
- Troubleshooting table — the full list of SDK/HTTP error strings and what causes each one.
ai.runMany(...), ai.button(...), and the troubleshooting table are documented in the SDK’s
fuller Integration Guide — it isn’t public, so ask your platform operator for a copy if you need
it.