Skip to content

API client

@digize/sdk/api is a client for the public API at https://digize.com/api/v1. Every operation in the API is a method, typed from the API’s own OpenAPI document: its input, its answer, the permission its key needs, and its description on hover.

import { createApiClient } from "@digize/sdk/api";
const api = createApiClient({ apiKey: env.DIGIZE_API_KEY });
const contact = await api.crm.contacts.create({
firstName: "Maya",
lastName: "Chen",
email: "maya@example.com",
tags: ["wholesale"],
});
await api.crm.contacts.update({ id: contact.id, city: "Austin" });
const again = await api.crm.contacts.get({ id: contact.id });

Make the key in Settings › API keys with the permissions the calls need (Authentication), and keep it on a server: never in a browser.

Option
apiKey Required. Sent as Authorization: Bearer <key>.
baseUrl The API’s address. https://digize.com/api/v1 unless it says.
maxRetries How many times a call is sent again after a 429, a 5xx or no answer. 2 unless it says.
timeoutMs How long one try may take. 60 seconds unless it says.
fetch Used instead of the global fetch, such as in tests.
headers Sent with every call.

Each operation is named <module>.<resource>.<verb>, such as crm.contacts.list, and is a method by those parts. call(name, input) runs one by its name, which is handy when the name is in a variable:

await api.crm.contacts.list({ tag: ["vip"], stage: ["customer"] });
await api.call("crm.contacts.list", { tag: ["vip"], stage: ["customer"] });

The input is one object, as the operation declares it. Path parameters go into the path ({ id } for /contacts/{id}); the rest goes into the query string for GET and DELETE, with lists as repeated parameters (?tag=vip&tag=wholesale), or the JSON body for the others. An input with nothing required can be left out.

The types are exported for your own code:

import type { ApiInput, ApiName, ApiOutput } from "@digize/sdk/api";
type NewContact = ApiInput<"crm.contacts.create">;
type Contact = ApiOutput<"crm.contacts.get">;

ApiOperations has every operation’s method, path, permission, input and output, and API_ROUTES each one’s method and path. The API reference lists them all.

Each call also takes options, after the input:

await api.commerce.orders.get(
{ id: "ord_01JD7QK2X8V5N3M4P6R9T0W1YZ" },
{ signal: AbortSignal.timeout(5_000), headers: { "x-request-source": "nightly-sync" } },
);
Option
idempotencyKey A write’s Idempotency-Key; see below.
signal Cancels the call, retries included.
maxRetries This call’s own, over the client’s.
headers Sent with this call.

Every POST, PATCH, PUT and DELETE goes with an Idempotency-Key: a new UUID per call unless you give one, which the call’s retries reuse. For 24 hours the same key and request answer what the first one did, without making the change twice, so a write the client sends again after a timeout can’t happen twice.

To make a write safe to send again yourself, across restarts say, give it a key you keep:

await api.crm.invoices.send(
{ id: invoice.id },
{ idempotencyKey: `send-${invoice.id}` },
);

The same key with a different request throws invalid with status 422; one sent while the first is still running throws conflict. Pass idempotencyKey: null to send no key; such a write is never retried.

A call that gets a 429, a 5xx or no answer at all is sent again, twice unless maxRetries says otherwise. A read is always safe to send again; a write only with its idempotency key, which it has unless you passed null. The client waits as the answer’s retry-after says (at most 30 seconds), or else a random time that doubles each try, up to 8 seconds. Any other error is thrown at once.

The long lists (contacts, orders, a sending domain’s activity, the audit log and card taps) come a page at a time: each answer has rows and a nextCursor for the next page, null on the last. paginate follows the cursor and hands over each row; pages hands over each page:

for await (const order of api.paginate("commerce.orders.page", {
status: ["paid"],
limit: 100,
})) {
console.log(order.number, order.total);
}
for await (const page of api.pages("audit.page", { limit: 100 })) {
console.log(page.rows.length, "more entries");
}

Both take the operation’s name and the same input on every page; a cursor in the input starts from that page. Only operations with rows and nextCursor are allowed (the PagedName type).

request(method, path, { query, body }) calls a path under the base URL directly, with the key, the retries and the error handling, for anything the operations don’t cover:

const found = await api.request<{ rows: unknown[] }>("GET", "/contacts", {
query: { search: "maya" },
});

A call Digize turns down throws a DigizeError with the API’s code, the HTTP status, the message and details, with fields naming each field that wasn’t valid:

import { isDigizeError } from "@digize/sdk/api";
try {
await api.crm.contacts.create({ email: "not an email" });
} catch (error) {
if (isDigizeError(error, "invalid")) console.log(error.fields);
else throw error;
}

The client’s types and routes are generated from the API’s OpenAPI document (/api/v1/openapi.json) when the package is built, so each release of @digize/sdk matches the API as it was then, and API_VERSION says which version of the API that is. An operation added since needs a newer package, or request until then.