TypeScript SDK

The official TypeScript client — typed methods for every operation, cursor pagination, idempotency keys, retries and webhook verification.

View this page as Markdown

@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/sdk

The 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
apiKey — Your API key (required).
baseUrl https://app.processcart.com/api The server; paths add /v1/…. Change it only to reach a proxy or a test double.
timeoutMs 30000 The timeout of one attempt, in milliseconds.
retry { maxAttempts: 3, maxDelayMs: 8000, retryAfterCapMs: 60000 } See Retries.
fetch globalThis.fetch Any WHATWG-compatible fetch.
userAgent processcart-sdk/<version> 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.
requestId 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.
rateLimit { limit, remaining, reset } from the RateLimit-* 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 — after Retry-After seconds, else after RateLimit-Reset, else with backoff (Rate limits).
  • 409 idempotency_in_progress — after Retry-After, or 2 seconds without one.
  • 502, 503 and 504 on a GET — after Retry-After when the response has one, else with backoff. A write that meets one is not retried (see below); the error carries retryAfter.
  • A network failure (fetch itself 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 query

The 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
getMe(…) GET /v1/me Identify the API key and its workspace
getHealth(…) GET /v1/health Check that the API is up
listRuns(…) GET /v1/runs List runs
startRun(…) POST /v1/runs Start a run
getRun(…) GET /v1/runs/{id} Get a run
updateRun(…) PATCH /v1/runs/{id} Update a run's name, priority, labels, due date or status
trashRun(…) DELETE /v1/runs/{id} Move a run to Trash
restoreRun(…) POST /v1/runs/{id}/restore Restore a run from Trash
listRunTasks(…) GET /v1/runs/{id}/tasks List a run's tasks
completeTask(…) POST /v1/runs/{id}/tasks/{key}/complete Complete a task
reopenTask(…) POST /v1/runs/{id}/tasks/{key}/reopen Reopen a completed task
approveTask(…) POST /v1/runs/{id}/tasks/{key}/approve Approve an approval task
rejectTask(…) POST /v1/runs/{id}/tasks/{key}/reject Reject an approval task
setRunFields(…) PATCH /v1/runs/{id}/fields Set field values
listRunComments(…) GET /v1/runs/{id}/comments List a run's comments
createRunComment(…) POST /v1/runs/{id}/comments Comment on a task
getRunComment(…) GET /v1/runs/{id}/comments/{commentId} Get a comment
requestRunFile(…) POST /v1/runs/{id}/files Request an upload ticket
finalizeRunFile(…) POST /v1/runs/{id}/files/{attachmentId}/finalize Finalize an upload
listRunFiles(…) GET /v1/runs/{id}/files List a run's files
getRunFile(…) GET /v1/runs/{id}/files/{attachmentId} Get a file with a download link
deleteRunFile(…) DELETE /v1/runs/{id}/files/{attachmentId} Remove a file
listRunActivity(…) GET /v1/runs/{id}/activity List a run's activity
listWorkflows(…) GET /v1/workflows List workflows
createWorkflow(…) POST /v1/workflows Create a workflow from a template
getWorkflow(…) GET /v1/workflows/{id} Get a workflow
updateWorkflow(…) PATCH /v1/workflows/{id} Update a workflow's name, description, folder, Key Date field or status
getWorkflowSchema(…) GET /v1/workflows/{id}/schema Get the JSON Schema for a workflow's run bodies
getWorkflowTemplate(…) GET /v1/workflows/{id}/template Export a workflow as a portable template
listViews(…) GET /v1/views List shared views
createView(…) POST /v1/views Create a shared view
getView(…) GET /v1/views/{id} Get a shared view
updateView(…) PATCH /v1/views/{id} Update a shared view
deleteView(…) DELETE /v1/views/{id} Delete a shared view
listViewRuns(…) GET /v1/views/{id}/runs List the runs a view selects
listWebhooks(…) GET /v1/webhooks List webhooks
createWebhook(…) POST /v1/webhooks Create a webhook
getWebhook(…) GET /v1/webhooks/{id} Get a webhook
updateWebhook(…) PATCH /v1/webhooks/{id} Update a webhook
deleteWebhook(…) DELETE /v1/webhooks/{id} Delete a webhook
enableWebhook(…) POST /v1/webhooks/{id}/enable Enable a webhook
disableWebhook(…) POST /v1/webhooks/{id}/disable Disable a webhook
rotateWebhookSecret(…) POST /v1/webhooks/{id}/rotate-secret Rotate a webhook's signing secret
testWebhook(…) POST /v1/webhooks/{id}/test Send a test event
listWebhookDeliveries(…) GET /v1/webhooks/{id}/deliveries List a webhook's deliveries
getWebhookDelivery(…) GET /v1/webhooks/{id}/deliveries/{deliveryId} Get a delivery
redeliverWebhookDelivery(…) POST /v1/webhooks/{id}/deliveries/{deliveryId}/redeliver Send a delivery again
listSchedules(…) GET /v1/schedules List schedules
createSchedule(…) POST /v1/schedules Create a schedule
getSchedule(…) GET /v1/schedules/{id} Get a schedule
updateSchedule(…) PATCH /v1/schedules/{id} Update a schedule
deleteSchedule(…) DELETE /v1/schedules/{id} Delete a schedule
pauseSchedule(…) POST /v1/schedules/{id}/pause Pause a schedule
resumeSchedule(…) POST /v1/schedules/{id}/resume Resume a schedule
runScheduleNow(…) POST /v1/schedules/{id}/run Start a run from a schedule now
previewSchedule(…) GET /v1/schedules/{id}/preview Preview a schedule's next occurrences
listAutomationRules(…) GET /v1/automation-rules List automation rules
createAutomationRule(…) POST /v1/automation-rules Create an automation rule
getAutomationRule(…) GET /v1/automation-rules/{id} Get an automation rule
updateAutomationRule(…) PATCH /v1/automation-rules/{id} Update an automation rule
deleteAutomationRule(…) DELETE /v1/automation-rules/{id} Delete an automation rule
enableAutomationRule(…) POST /v1/automation-rules/{id}/enable Turn an automation rule on
disableAutomationRule(…) POST /v1/automation-rules/{id}/disable Turn an automation rule off
listAutomationRuleExecutions(…) GET /v1/automation-rules/{id}/executions List a rule's executions
listUsers(…) GET /v1/users List members
listGroups(…) GET /v1/groups List groups
exportRunsCsv(…) GET /v1/exports/runs.csv 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).