Skip to content

TypeScript client

@re-zip/api-client is the official TypeScript client for this API. It is a thin wrapper around fetch that carries the full type surface of the API with it: paths, request bodies, query parameters, and every response body a route can return.

The types are not hand-written. They are printed from the API server’s own route definitions and republished whenever those definitions change, so the client cannot drift from the API the way a maintained-by-hand SDK does.

You do not need it. Every endpoint in this reference is a plain HTTP call and the OpenAPI definitions are there if you would rather generate your own client. Use this one if you are on TypeScript and want the API’s shapes checked at compile time.

Terminal window
npm install @re-zip/api-client

Requires Node 20.19 or newer (or any runtime with a global fetch). The package has one runtime dependency and ships no client-side validation.

import { createClient } from "@re-zip/api-client"
const api = createClient({
url: "https://api.sandbox.re-zip.com",
token: process.env.REZIP_TOKEN,
})
OptionDescription
urlAPI base URL. Use https://api.sandbox.re-zip.com while integrating, https://api.re-zip.com when live.
tokenAccess token. A bare token is sent as Bearer <token>; prefix it yourself with Pat or Basic to use those schemes.
headersExtra headers merged into every request.
statusHandlersCallbacks keyed by status code, run as a side effect when a response carries that status. Handy for a global 401 reaction in a frontend.

Every request the client sends carries Accept-Version: 2.0, a Content-Type, an X-Session-UUID that is stable for the life of the client, and a fresh X-Request-UUID. Quote those two IDs if you contact support about a specific call.

Authentication is functional: there is no mutable client to re-authenticate. To use a different token, build another client.

const asShop = createClient({ ...config, token: shopToken })

Keep tokens server-side. The client works in a browser, but a shop or partner token in browser JavaScript is a leaked token.

One function, called with a method and a path:

const res = await api("post", `/shops/${shopId}/orders`, {
body: {
currency: "DKK",
lines: [
{ sku: "REZIP-BAG-M", quantity: 2 },
{ sku: "REZIP-BOX-S", quantity: 1 },
],
},
})

Paths are interpolated, not templated — you pass the real URL, and the client resolves it back to the route segment by segment. A path that matches no route is a compile error. A route that requires a body makes the third argument required, and body is typed to that route’s declared wire shape: the example above will not compile without currency, or with a line missing quantity.

A typo inside an interpolated segment is not a compile error, because ${string} matches any segment. It shows up as a result type you cannot use.

Nothing throws on a non-2xx. The result is a discriminated union, so check res.ok:

if (res.ok) {
// res.status is 201, res.body is the created order
console.log(res.body.id, res.body.number)
} else if (res.body?.type === "validation_error") {
// res.body.errors is Record<string, string[]>
console.error(res.body.errors)
} else {
console.error(res.status, res.body?.message)
}

There is one ok: true variant per 2xx status the route declares, with status and body correlated — a route that answers 200 or 204 gives you a union you have to narrow before reading the body, rather than a body that is secretly nullable.

The error side has one variant per error the handler can actually return, plus a fallback covering the failures any request can hit: validation, auth, rate limiting, and 500s. res.body is null when the response has no body or a non-JSON one.

Narrow errors on body.type. Three shapes exist, and they match what The HTTP Responses describes on the wire:

body.typeShapeWhen
"validation_error"{ message, type, errors: Record<string, string[]> }A 400 from input validation. errors is keyed by field.
A domain error name{ message, type, ...fields }A named failure the route declares, carrying its own fields.
undefined{ message }A plain 404/403/409/400 with no discriminator.

Domain errors are the ones worth branching on, because they carry the detail you need to react. POST /partners/$PARTNER_ID/orders for example declares:

if (!res.ok && res.body?.type === "skus_not_in_assortment") {
// res.body.skus is readonly string[] — exactly which SKUs were rejected
console.error("not in assortment:", res.body.skus.join(", "))
}

Which domain errors a route can return is part of its type, so autocomplete on body.type lists them for the route you actually called.

Pass query as an object. The client encodes nested objects in the bracket style the API expects, so filter[state]=1 is written as:

const orders = await api("get", `/shops/${shopId}/orders`, {
query: {
filter: { state: 1 },
sort_by: "created_at",
sort_dir: "desc",
page_size: 100,
},
})

Where a route declares a query schema, its parameters are typed and required ones are enforced. Where it does not, query accepts a string/number record — the pagination and filter parameters in The HTTP Request work on every listing endpoint regardless of what the route declares.

One known gap: GET /drop_points/locations is also matched by GET /drop_points/:id, so the client resolves it to both and its declared query parameters are not enforced. It is the only such collision in the API. The request still works; only the compile-time check is missing.

Pass a FormData or Blob body and the client drops its default Content-Type so fetch can set the multipart boundary itself:

const form = new FormData()
form.append("file", blob, "logo.png")
await api("post", `/shops/${shopId}/files`, { body: form })

If you set Content-Type yourself in options.headers, your value wins.

Useful when typing a wrapper function or storing a response:

import type {
ApiFetch,
ApiResponseFor,
RequestOptionsFor,
Routes,
ApiError,
} from "@re-zip/api-client"
type OrderResult = ApiResponseFor<"post", `/shops/${string}/orders`>
type RouteKeys = keyof Routes

ApiFetch is the type of the client itself, so you can accept one as a parameter rather than reaching for a global.

The declarations need Headers, FormData, Blob, and ReadableStream to exist as globals, and nothing else. Either lib: ["dom"] or @types/node on Node 20.19+ satisfies that — a Node backend does not need the DOM lib.

Five endpoints do not declare a typed response and are therefore absent from the client. Call them with plain fetch:

  • GET /ping
  • GET /health
  • GET /tools/ulids
  • POST /
  • GET /me/rewards/:id/voucher.pdf

Versions are 2.YYYYMMDD.N. The leading 2 is the API major version — the one every request sends as Accept-Version: 2.0 — and the rest is a release date, not semver. Because releases are generated automatically from the API’s route definitions, no human judges whether a given change is breaking, so a semver minor/patch split would be a claim the process cannot back.

"@re-zip/api-client": "^2" // always current, API v2 — recommended
"@re-zip/api-client": "2.20260917.0" // frozen until you bump it

^2 tracks every future release for API v2, breaking type changes included. That is the intended default: a changed type fails at compile time, when you choose to build, and it cannot surprise you at runtime — the runtime is a fetch wrapper with no behaviour of its own. Pin exactly if you would rather adopt changes deliberately.

If the API ships a v3, this package’s major becomes 3, so a ^2 range keeps resolving to v2 types instead of silently retargeting you at an API you have not migrated to.

Integrating and stuck? Email helpdesk@re-zip.com.