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.
Install
Section titled “Install”npm install @re-zip/api-clientRequires Node 20.19 or newer (or any runtime with a global fetch). The
package has one runtime dependency and ships no client-side validation.
Create a client
Section titled “Create a client”import { createClient } from "@re-zip/api-client"
const api = createClient({ url: "https://api.sandbox.re-zip.com", token: process.env.REZIP_TOKEN,})| Option | Description |
|---|---|
url | API base URL. Use https://api.sandbox.re-zip.com while integrating, https://api.re-zip.com when live. |
token | Access token. A bare token is sent as Bearer <token>; prefix it yourself with Pat or Basic to use those schemes. |
headers | Extra headers merged into every request. |
statusHandlers | Callbacks 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.
Make a request
Section titled “Make a request”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.
Read the response
Section titled “Read the response”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.
Error bodies
Section titled “Error bodies”Narrow errors on body.type. Three shapes exist, and they match what
The HTTP Responses describes on the wire:
body.type | Shape | When |
|---|---|---|
"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.
Query parameters
Section titled “Query parameters”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.
Upload files
Section titled “Upload files”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.
Import the types
Section titled “Import the types”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 RoutesApiFetch is the type of the client itself, so you can accept one as a
parameter rather than reaching for a global.
TypeScript setup
Section titled “TypeScript setup”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.
Endpoints the client does not cover
Section titled “Endpoints the client does not cover”Five endpoints do not declare a typed response and are therefore absent from
the client. Call them with plain fetch:
GET /pingGET /healthGET /tools/ulidsPOST /GET /me/rewards/:id/voucher.pdf
Versioning
Section titled “Versioning”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.
Next steps
Section titled “Next steps”- Webshop integration — the end-to-end flow,
written in
curl; every call in it maps one-to-one onto this client - Voucher integration — on-demand voucher pools
- Webhooks — signature verification and retries
Integrating and stuck? Email helpdesk@re-zip.com.