Call the API from a Script
This guide calls the tRPC Platform API from a standalone Node or Bun script. It
covers getting a token, a raw fetch version (to see the superjson wire format), the typed
@trpc/client version, paginating a list, and handling the error envelope.
Get a token
Programmatic callers authenticate with a session JWT in the Authorization header:
Authorization: Bearer <session JWT>
The token is your session JWT, the same value your session cookie carries. The simplest way to get it is Sign in with Inklura in the API Explorer (see Authentication). Keep it out of source; read it from the environment:
export INKLURA_TOKEN="<session JWT>"
The script acts as the token's user, with that user's permissions. The token names that account's home tenant and site; the scope of each call is resolved on the request and checked against the account's access (see Sessions & Tokens).
Check that the token is accepted before doing real work by calling auth.getSession (shown
below). An invalid or expired token is not reported as such: the request is treated as
anonymous and the call fails with UNAUTHORIZED (401). auth.getSession requires the
auth:read permission: an account without it gets FORBIDDEN (403), even with a valid token.
Raw fetch
Useful for seeing the wire format. Queries are GET with the input URL-encoded into
?input={"json":<value>}; mutations are POST with the body {"json":<value>}. The result
is nested at result.data.json, and an error at error.json.
const BASE = "https://manage.inklura.fr/api/trpc";
const auth = { Authorization: `Bearer ${process.env.INKLURA_TOKEN}` };
// Read a response: return the value, or throw the tRPC error.
async function unwrap(res: Response) {
const body = await res.json();
if (body.error) {
const { message, data } = body.error.json;
throw new Error(`${data?.code} (${data?.httpStatus}): ${message}`);
}
return body.result.data.json;
}
// Query (GET) without input: who am I?
const me = await unwrap(await fetch(`${BASE}/auth.getSession`, { headers: auth }));
console.log("session:", me.user.email);
// Query (GET) with input: first page of products
const input = encodeURIComponent(JSON.stringify({ json: { page: 0, limit: 10 } }));
const firstPage = await unwrap(
await fetch(`${BASE}/products.list?input=${input}`, { headers: auth }),
);
console.log("products:", firstPage.total);
// Mutation (POST): create a client
const created = await unwrap(
await fetch(`${BASE}/clients.create`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ json: { name: "Acme SARL", email: "contact@acme.example" } }),
}),
);
console.log("created client:", created.id);
A procedure that takes no input, such as auth.getSession, is called without ?input=:
sending an input to it fails validation.
With raw fetch, values that JSON cannot represent (dates in particular) arrive as strings in
json, with their original type described alongside in meta. The typed client below
converts them back for you.
Typed @trpc/client
For a TypeScript caller, @trpc/client (v11) handles the superjson wrapping and batching for
you. Configure the superjson transformer on an httpBatchLink and set the Authorization
header there:
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import superjson from "superjson";
// AppRouter: the API's router type if you have it; `any` works too, without type checking.
const client = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: "https://manage.inklura.fr/api/trpc",
transformer: superjson,
headers() {
return { Authorization: `Bearer ${process.env.INKLURA_TOKEN}` };
},
}),
],
});
const session = await client.auth.getSession.query();
const created = await client.clients.create.mutate({
name: "Acme SARL",
email: "contact@acme.example",
});
With the transformer configured, you pass and receive plain values, with no manual
{"json":…} wrapping. See tRPC Procedures for the calling conventions and
batching details.
clients.create requires name (non-empty); email is optional but must be a valid address
when given. Your account also needs permission to edit clients: otherwise the call fails with
FORBIDDEN.
Paginate a list
products.list takes { page, limit }, with page starting at 0 (limit defaults to
10), and returns { items, total, page, limit, pages }. It also accepts optional filters:
search (name, description, reference or SKU), active, categoryId, and the
sortBy / sortOrder sort (newest first by default). Walk every page:
async function listAllProducts() {
const all = [];
const limit = 50;
for (let page = 0; ; page += 1) {
const res = await client.products.list.query({ page, limit });
all.push(...res.items);
if (page + 1 >= res.pages || res.items.length === 0) break;
}
return all;
}
Pagination is a convention rather than a framework guarantee: parameter names and the first page number vary between procedures, so confirm the shape for the procedure you call. See Pagination & Filtering.
Handle errors
Failures come back as the standard tRPC error envelope; the useful fields are in data
(code, httpStatus, path), and input validation failures add data.zodError. The typed
client throws a TRPCClientError whose data carries them:
import { TRPCClientError } from "@trpc/client";
try {
await client.clients.create.mutate({ name: "", email: "not-an-email" });
} catch (err) {
if (err instanceof TRPCClientError) {
const data = err.data; // { code, httpStatus, path, zodError }
console.error(data?.code, data?.httpStatus); // BAD_REQUEST 400
if (data?.zodError) {
console.error("field errors:", data.zodError.fieldErrors);
}
} else {
throw err;
}
}
With raw fetch, read the same fields off error.json.data in the parsed body (as unwrap
does above). See Errors & Rate Limits for the code-to-status
mapping and the zodError shape.
Related
- tRPC Procedures: calling conventions, batching, router catalog
- Sessions & Tokens: identity resolution and tenant/site scope
- Errors & Rate Limits: the error envelope and Zod errors
- Pagination & Filtering: pagination conventions