TypeScript SDK
The official TypeScript client — typed methods for every operation, cursor pagination, idempotency keys, retries and webhook verification.
@processcart/sdk is the official TypeScript client for this API. It has one method per operation, named by the operation's operationId (the same name the reference shows), and its request and response types are typed from the OpenAPI document. It also walks cursor pages, sends an Idempotency-Key on every write, retries what is safe to retry, and verifies webhook signatures.
Install
npm install @processcart/sdkThe package is ESM with type declarations; it has no runtime dependencies.
Create a client
import { ProcessCart } from "@processcart/sdk";
const client = new ProcessCart({ apiKey: process.env.PROCESSCART_API_KEY! });An Admin creates keys under Settings → API (Authentication & API keys). The client accepts a pc_live_… or pc_test_… key and refuses anything else before sending a request. A sandbox uses the same base URL; the key decides which workspace you reach.
| Option | Default | What it does |
|---|---|---|
api |
— | Your API key (required). |
base |
https:/ |
The server; paths add /v1/. Change it only to reach a proxy or a test double. |
timeout |
30000 |
The timeout of one attempt, in milliseconds. |
retry |
{ max |
See Retries. |
fetch |
global |
Any WHATWG-compatible fetch. |
user |
processcart-sdk/ |
The User-Agent header. |
Every method also takes a last options argument for that one call: idempotencyKey, expectedRevision, requestId (sent as X-Request-Id), signal (an AbortSignal), timeoutMs and retry.
Quickstart
The six steps of the quickstart, in TypeScript. The key needs the runs:read, runs:write and workflows:read scopes (or full access).
// 1. Identify your key.
const me = await client.getMe();
console.log(`Connected to ${me.org.name} with the key "${me.key.name}"`);
// 2. Find a published workflow.
const workflows = await client.listWorkflows();
const workflow = workflows.data.find((w) => w.publishedVersion !== null);
if (!workflow) throw new Error("Publish a workflow first");
// 3. Start a run, with field values in the same transaction.
const run = await client.startRun({
workflowId: workflow.id,
name: "Order 1042",
fields: { customer_email: "ada@example.com", amount: 1700.5 },
});
// 4. Set field values; refused with 409 if someone changed the run since you read it.
const updated = await client.setRunFields(
run.id,
{ ship_date: "2026-10-09" },
{ expectedRevision: run.revision },
);
// 5. Complete the first open task.
const task = updated.tasks.find((t) => t.visible && !t.locked && t.state === "todo");
if (task) await client.completeTask(run.id, task.key, { expectedRevision: updated.revision });Then the sixth step, hear about changes: every run updated since your last sync, across every page (Incremental sync).
for await (const changed of client.listRuns({ updatedSince: "2026-10-08T00:00:00Z" })) {
console.log(changed.name, changed.revision);
}A method resolves to the response body, so run.tasks and run.fields are typed. Path parameters come first, in path order, then the request body (or the query), then options.
Pagination
A list method returns a request you can use three ways (Pagination):
// The first page: { data, nextCursor, hasMore }.
const first = await client.listRuns({ limit: 50 });
// Every item across every page, following nextCursor with the same query.
for await (const run of client.listRuns({ status: ["active"] })) {
console.log(run.name);
}
// Every page, each with its own $meta.
for await (const page of client.listRuns({ limit: 100 }).pages()) {
console.log(page.data.length, page.$meta.requestId);
}Nothing is sent until you await or iterate, and the iterator owns cursor. For incremental sync, walk client.listRuns({ updatedSince }) from the instant of your last sync.
Writes, idempotency and concurrency
Every POST, PATCH and DELETE carries an Idempotency-Key: a random UUID generated once per call and reused on each of that call's retries, so a retried call replays the stored answer instead of applying the change again (Idempotency). Pass options.idempotencyKey to make a call safe to repeat across processes — derived from your own order number, say.
Every Run the API returns carries its revision. Send the one you last saw as expectedRevision (the client sends it as the Expected-Revision header) and a write that raced another one is refused with 409 revision_mismatch instead of overwriting it (Concurrency). Each write resolves to the updated run, so chain the next write from its revision:
const run = await client.getRun(runId);
const updated = await client.setRunFields(runId, { amount: 125 }, { expectedRevision: run.revision });
await client.completeTask(runId, "review", { expectedRevision: updated.revision });expectedRevision takes the revision number (a non-negative integer; anything else throws a TypeError before a request is sent). On a 409 revision_mismatch the error's currentRevision is the run's revision now: read the run again, merge, and retry with it. deleteRunFile answers 204, so read the run before the next conditional write. Every method accepts the option, but only the writes that change an existing run read it: on schedules, automation rules, views, webhooks and workflows the server ignores it, and the last write wins. The API sends no ETag and refuses If-Match — CDN layers evaluate If-Match against the response and turn a successful write into a 412.
Every result also carries a non-enumerable $meta (JSON.stringify and deep-equality checks skip it):
$meta field |
What it holds |
|---|---|
status |
The HTTP status. |
request |
X-Request-Id — quote it to support. |
revision |
The run's revision, from the body (set on every Run read, create and write). |
replayed |
true when the API replayed the stored answer for the same Idempotency-Key. |
rate |
{ limit, remaining, reset } from the Rate headers. |
headers |
The raw response headers. |
An operation that answers 204 No Content (deleteView, deleteWebhook, deleteRunFile) resolves to an object holding only $meta.
Retries
The client makes up to maxAttempts attempts in all, the first included (3 by default; retry: { maxAttempts: 1 } turns retries off). It retries:
- 429
rate_limited— afterRetry-Afterseconds, else afterRateLimit-Reset, else with backoff (Rate limits). - 409
idempotency_in_progress— afterRetry-After, or 2 seconds without one. - 502, 503 and 504 on a
GET— afterRetry-Afterwhen the response has one, else with backoff. A write that meets one is not retried (see below); the error carriesretryAfter. - A network failure (
fetchitself threw) and a timed-out attempt.
A wait the server asked for is capped at retryAfterCapMs (60 seconds). Backoff waits 500 ms before the first retry and doubles each time, plus up to 250 ms of jitter, capped at maxDelayMs (8 seconds). It never retries a 500, a 502 / 503 / 504 on a write (a crash or a database outage after the commit can release the API's idempotency claim, so a retry with the same key could apply the change twice — re-running it is your call), any other 4xx, or a call you aborted with signal. When the attempts run out it throws the last failure. getHealth is the one call that resolves a 503: a degraded API answers { status: "degraded" }, read $meta.status to tell it from a 200.
Errors
Every API or transport failure rejects with a ProcessCartError; check for one with isProcessCartError:
import { isProcessCartError } from "@processcart/sdk";
try {
await client.completeTask(runId, "review");
} catch (error) {
if (!isProcessCartError(error)) throw error;
console.error(error.status, error.code, error.requestId);
for (const e of error.errors ?? []) console.error(e.field, e.code, e.message);
}The error carries status (0 when no response arrived), code (an error code, or network_error, timeout or non_problem_response from the client itself), title, detail, requestId, errors (the per-field problems of a 422), retryAfter, headers, the problem document as received, and raw (the response text when the body was not a problem document, such as a proxy's HTML page). On a 409 revision_mismatch, currentRevision is the run's revision now (the problem body's currentRevision). A call you abort rejects with your abort reason, not a ProcessCartError. The code list is open: handle codes this version does not know.
Files
uploadRunFile runs the three-step upload (Files) in one call: it requests an upload ticket, PUTs the bytes to the signed URL with exactly the ticket's headers and no API key, then finalizes.
import { readFile } from "node:fs/promises";
const { attachment, run } = await client.uploadRunFile(runId, {
target: { kind: "field", fieldKey: "packing_slip" },
filename: "packing-slip.pdf",
contentType: "application/pdf",
bytes: await readFile("./packing-slip.pdf"),
});bytes is a Blob, an ArrayBuffer or a Uint8Array (a Node Buffer is one); size defaults to its length. options.expectedRevision applies to the finalize step, and each API step gets its own Idempotency-Key. A refused PUT rejects with a ProcessCartError (code: "non_problem_response", the storage answer in raw) and nothing is finalized.
Webhooks
@processcart/sdk/webhooks verifies Standard Webhooks signatures with WebCrypto, without the client. Pass the RAW body — the exact bytes received, never a re-serialized object:
import { createServer } from "node:http";
import { headersFromRequest, verifyWebhookSignature } from "@processcart/sdk/webhooks";
import type { WebhookEvent } from "@processcart/sdk/webhooks";
const secret = process.env.PROCESSCART_WEBHOOK_SECRET!; // whsec_…
createServer(async (req, res) => {
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(chunk as Buffer);
const body = Buffer.concat(chunks);
const result = await verifyWebhookSignature(secret, headersFromRequest(req.headers), body);
if (!result.ok) return void res.writeHead(400).end(result.reason);
const event = JSON.parse(body.toString("utf8")) as WebhookEvent;
res.writeHead(204).end(); // answer within 10 seconds, then do the work
console.log(event.type, result.id); // dedupe on result.id
}).listen(3000);With a Request (Workers, Deno, Bun, Next.js route handlers), read await request.text() and pass headersFromRequest(request.headers). The check accepts any v1 entry of webhook-signature (both secrets sign for 24 hours after a rotation) and refuses a webhook-timestamp more than 300 seconds from now (toleranceSeconds changes it). A failure resolves to { ok: false, reason }: missing_headers, bad_timestamp, timestamp_outside_tolerance, no_matching_signature or bad_secret.
Types
Every request and response type comes from the OpenAPI document, so the SDK types match what the API validates:
import type { Run, RunTask, OpBody, OpQuery } from "@processcart/sdk";
type NewRun = OpBody<"startRun">; // the startRun body
type RunFilter = OpQuery<"listRuns">; // the listRuns queryThe component schemas a method takes or returns are exported under their OpenAPI names (Run, RunTask, RunField, WorkflowSummary, View, Webhook, …); the raw paths, components and webhooks interfaces are exported too. WebhookEvent (from @processcart/sdk/webhooks) is the union of every event envelope, narrowed by type. Field values are typed per workflow at runtime — getWorkflowSchema(id) returns the JSON Schema of one workflow's run bodies (Field values & typing).
Methods
| Method | HTTP | What it does |
|---|---|---|
get |
GET /v1 |
Identify the API key and its workspace |
get |
GET /v1 |
Check that the API is up |
list |
GET /v1 |
List runs |
start |
POST /v1 |
Start a run |
get |
GET /v1 |
Get a run |
update |
PATCH /v1 |
Update a run's name, priority, labels, due date or status |
trash |
DELETE /v1 |
Move a run to Trash |
restore |
POST /v1 |
Restore a run from Trash |
list |
GET /v1 |
List a run's tasks |
complete |
POST /v1 |
Complete a task |
reopen |
POST /v1 |
Reopen a completed task |
approve |
POST /v1 |
Approve an approval task |
reject |
POST /v1 |
Reject an approval task |
set |
PATCH /v1 |
Set field values |
list |
GET /v1 |
List a run's comments |
create |
POST /v1 |
Comment on a task |
get |
GET /v1 |
Get a comment |
request |
POST /v1 |
Request an upload ticket |
finalize |
POST /v1 |
Finalize an upload |
list |
GET /v1 |
List a run's files |
get |
GET /v1 |
Get a file with a download link |
delete |
DELETE /v1 |
Remove a file |
list |
GET /v1 |
List a run's activity |
list |
GET /v1 |
List workflows |
create |
POST /v1 |
Create a workflow from a template |
get |
GET /v1 |
Get a workflow |
update |
PATCH /v1 |
Update a workflow's name, description, folder, Key Date field or status |
get |
GET /v1 |
Get the JSON Schema for a workflow's run bodies |
get |
GET /v1 |
Export a workflow as a portable template |
list |
GET /v1 |
List shared views |
create |
POST /v1 |
Create a shared view |
get |
GET /v1 |
Get a shared view |
update |
PATCH /v1 |
Update a shared view |
delete |
DELETE /v1 |
Delete a shared view |
list |
GET /v1 |
List the runs a view selects |
list |
GET /v1 |
List webhooks |
create |
POST /v1 |
Create a webhook |
get |
GET /v1 |
Get a webhook |
update |
PATCH /v1 |
Update a webhook |
delete |
DELETE /v1 |
Delete a webhook |
enable |
POST /v1 |
Enable a webhook |
disable |
POST /v1 |
Disable a webhook |
rotate |
POST /v1 |
Rotate a webhook's signing secret |
test |
POST /v1 |
Send a test event |
list |
GET /v1 |
List a webhook's deliveries |
get |
GET /v1 |
Get a delivery |
redeliver |
POST /v1 |
Send a delivery again |
list |
GET /v1 |
List schedules |
create |
POST /v1 |
Create a schedule |
get |
GET /v1 |
Get a schedule |
update |
PATCH /v1 |
Update a schedule |
delete |
DELETE /v1 |
Delete a schedule |
pause |
POST /v1 |
Pause a schedule |
resume |
POST /v1 |
Resume a schedule |
run |
POST /v1 |
Start a run from a schedule now |
preview |
GET /v1 |
Preview a schedule's next occurrences |
list |
GET /v1 |
List automation rules |
create |
POST /v1 |
Create an automation rule |
get |
GET /v1 |
Get an automation rule |
update |
PATCH /v1 |
Update an automation rule |
delete |
DELETE /v1 |
Delete an automation rule |
enable |
POST /v1 |
Turn an automation rule on |
disable |
POST /v1 |
Turn an automation rule off |
list |
GET /v1 |
List a rule's executions |
list |
GET /v1 |
List members |
list |
GET /v1 |
List groups |
export |
GET /v1 |
Export a workflow's runs as CSV |
uploadRunFile(id, { target, filename, contentType, bytes, size? }, options?) adds the one-call upload described under Files.
Runtime support
The SDK needs fetch and WebCrypto: Node.js 22.12 or later, Deno, Bun and Cloudflare Workers. Use it from server-side code only — an API key acts on your whole workspace, so it never belongs in code that runs in a browser (the API sends no CORS headers).
Versioning
The package follows semantic versioning, starting at 0.1.0; SDK_VERSION holds the installed version and rides the User-Agent header. It targets version 1 of the API (every path under /v1), which changes only additively (Versioning & deprecation).