REST API and Node SDK
Push customers, products, orders, shipments and past tickets to purser from your server with a secret key, and how the Node SDK does it for you.
To answer "where is my order?" or "is this in stock?", the AI needs your order, product and shipment data. A custom-built store sends that data to purser through the REST API: push whenever something changes in your system, and use the bulk endpoints to bring in your history when you first connect.
Basics
| Base URL | https://askpurser.com/api/v1 |
| Authentication | Authorization: Bearer sk_… |
| Format | JSON, Content-Type: application/json |
| Reference | https://askpurser.com/api/v1 (an interactive reference page); the OpenAPI document is at https://askpurser.com/api/v1/spec.json |
The reference page and the OpenAPI document are generated from the API's own definitions; where they differ from this page, they are right.
Two kinds of key
Secret key (sk_…) | Publishable key (pk_…) | |
|---|---|---|
| Used by | Your server, calling the REST API | The embed code on your web pages, the in-app page |
| Can do | Call the data endpoints for this workspace | Only start chat sessions |
| Can it be public? | No, keep it on your server | Yes, it is in your web pages by design |
| Where to get it | Integration (接入) → "3. Push order data" (推送订单数据) → Create key (生成密钥) | In the embed code under Integration (接入) → "2. Install the chat widget on your site" (把客服窗口装到你的网站) |
- Only owners and admins can create secret keys. The key is shown once; purser only stores its hash, so copy it into your server's environment right away.
- Each key belongs to one workspace. You do not (and cannot) pass a workspace ID: the key alone decides which workspace the data goes to.
- Older keys in the
sk_live_…andpk_sand_…format keep working. - Setup (接入) → "3. Push order data" lists all of the workspace's keys (owners and admins only). When a key leaks or is no longer used, click Revoke (撤销): it stops working at once, every request with it gets
401, and it cannot be restored. The publishable key in your chat-window embed code is marked "in use by the window" (窗口在用) and cannot be revoked. - The identity secret used for signed-in customers is a different thing; see Identity tokens.
Check that a key works:
curl https://askpurser.com/api/v1/ping \
-H "Authorization: Bearer $PURSER_SECRET_KEY"
# {"orgId":"…"} the workspace this key belongs toData endpoints
| Method and path | What it does | Per call |
|---|---|---|
GET /ping | Check a key; returns the workspace it belongs to | — |
POST /customers | Upsert customers | 25 |
POST /products | Upsert products (with variants) | 25 |
POST /orders | Upsert orders (with line items) | 25 |
POST /shipments | Upsert shipments (with tracking events) | 25 |
POST /tickets | Upsert past tickets from a previous helpdesk (with their whole thread) | 50 tickets, 2,000 messages in total |
POST /customers/bulk etc. | Bulk upsert, processed asynchronously (one each for customers, products, orders, shipments) | 1,000 |
GET /batches/{id} | Progress of one bulk upsert | — |
POST /backfills | Start a history import | — |
GET /backfills/{id} | Progress of a history import | — |
POST /backfills/{id}/close | Say that everything for a history import has been sent | — |
POST /actions/{id}/status | Report how a refund ended, see Refund endpoint | — |
There is also POST /widget/sessions, called with the publishable key (header x-purser-key). The chat widget uses it to start a session; you normally never call it.
Upsert rules
- The request body is
{ "data": [ … ] }. - Objects are upserted by
id, which is your system's ID: created if new, updated otherwise. updatedAtis required: when your system last changed the object. A write that is not newer than the stored copy is ignored and reported asstale, so pushes arriving out of order never overwrite new data with old.- Timestamps are ISO-8601 strings with a time zone (such as
2026-09-26T08:00:00Z) or epoch milliseconds. - Amounts are integers in minor units:
1999means $19.99, and for yen1999means ¥1999. Currencies are ISO 4217 codes in capitals, such asUSDandJPY. - Every object can carry
custom: your own fields, stored as they are and not shown to the AI by default. - A customer's
tagsreplace only the tags the API set before; tags your agents added in the console are kept. Leavetagsout to change nothing;[]removes the API's tags. - A customer can carry
totalSpent+totalSpentCurrencyandordersCount(lifetime spend and order count as your system knows them). Customer segments use them first; without them, purser counts the orders it has received.
Order statuses: pending, processing, partially_shipped, shipped, delivered, cancelled, refunded, returned.
Shipment statuses: label_created, in_transit, out_for_delivery, delivered, delayed, exception, lost, returned_to_sender.
Every field is listed in the API reference.
Example: push one order
curl -X POST https://askpurser.com/api/v1/orders \
-H "Authorization: Bearer $PURSER_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"data":[{
"id": "ord_2001",
"updatedAt": "2026-09-26T08:00:00Z",
"number": "2001",
"customerId": "cus_mia",
"email": "mia@example.com",
"status": "processing",
"currency": "USD",
"total": 1599,
"placedAt": "2026-09-25T10:12:00Z",
"items": [{ "id": "li_1", "title": "Canvas tote", "quantity": 1, "unitPrice": 1599 }]
}]}'{ "results": [{ "id": "ord_2001", "status": "created" }] }Each object's result is one of created, updated or stale.
The order's customerId must match the sub in the identity token; that is how the AI finds a signed-in customer's orders when they ask in the chat.
Idempotency keys
The write endpoints accept an Idempotency-Key header, with the same rules as Stripe:
- same key, same body, again within 24 hours: you get the first response back and nothing is processed twice;
- same key, different body:
409(CONFLICT), which usually means a bug in your code.
Send the same key when you retry after a network timeout and you never write twice.
Bulk upserts and history imports
The synchronous endpoints take at most 25 objects per call. For more, use the /bulk endpoints: up to 1,000 objects per call, validated immediately and processed in the background. They answer 202 with { "batchId": "…" } right away; follow progress with GET /batches/{id} (pending, processing, done, failed). The upsert rules are the same as for the synchronous endpoints.
To import hundreds of thousands of past orders, group many batches into one history import (backfill) with a single progress number:
POST /backfills(optionally with{ "label": "…" }) returns anid;- send each
/bulkrequest with"backfillId": "<id>"; - when you have sent everything,
POST /backfills/{id}/close; - follow
GET /backfills/{id}; it becomesdoneonce every object received has been processed.
Errors
Every non-2xx response has this body:
{ "code": "CONFLICT", "status": 409, "message": "…", "data": { } }| Status | Meaning |
|---|---|
400 | The body does not match the schema; data.issues says where |
401 | No key, an unknown key or a revoked key |
404 | No such object in this key's workspace |
409 | The idempotency key was used with a different body; or the backfill does not exist or is already closed |
413 | Too many objects in one call; split it, or use /bulk |
429 | Too many requests. The response carries Retry-After (seconds); wait that long and retry |
Rate limits
Per minute; over the limit you get 429 with Retry-After: 60:
| What | Limit |
|---|---|
All calls with one secret key (sk_…) | 600 a minute |
Of those, /bulk routes | 30 a minute |
Chat sessions started (/widget/sessions), same shop and IP | 60 a minute |
For large amounts of history use /bulk and history imports (above) rather than looping one object at a time. The Node SDK backs off and retries on 429 by itself.
Node SDK: @purser-ai/node
Not on npm yet
@purser-ai/node has not been published, so npm install will not find it yet. Until it is, call the REST API directly (the curl above is a complete request). What follows is how it works once published.
Zero dependencies, Node 20 or later (it also runs on Bun, Deno and Cloudflare Workers: it only uses fetch and WebCrypto). Request and response types are generated from the API's OpenAPI document.
import { Purser, signIdentityToken } from "@purser-ai/node";
const purser = new Purser({ secretKey: process.env.PURSER_SECRET_KEY });
await purser.ping(); // { orgId }
await purser.orders.upsert({
id: "ord_2001", updatedAt: Date.now(), number: "2001", customerId: "cus_mia",
status: "shipped", currency: "USD", total: 1599, placedAt: Date.now(),
});
// History: an array or any (async) iterable, so a paginated export streams straight through
const result = await purser.backfill("orders", fetchAllOrders(), { onProgress: console.log });| Member | Notes |
|---|---|
new Purser({ secretKey, baseUrl? }) | secretKey must start with sk_; baseUrl defaults to https://askpurser.com |
ping() | Returns the key's workspace, { orgId } |
customers / products / orders / shipments / tickets | Each has upsert(objectOrArray, { idempotencyKey? }) |
backfill(resource, source, opts?) | resource is one of customers, products, orders, shipments; options label, concurrency (default 4), pollMs (default 2000), onProgress |
signIdentityToken(...) | Sign an identity token, see Identity tokens |
actionsHandler(...), verifyActionSignature(...) | The server side of the refund endpoint, see Refund endpoint |
What the SDK does for you:
- Coalescing: single-object
upsertcalls for the same resource within 20 ms share one request (each call still gets its own result), which suits a burst of webhooks; - Batching: arrays are split into requests of 25 objects (50 for tickets);
- Idempotency: every write carries an
Idempotency-Key; if you passidempotencyKey, each split batch uses<your key>:<offset>; - Retries: network errors,
429and5xxare retried with jittered backoff, at most 4 attempts in total; other4xxerrors are thrown at once; - History imports:
backfillsends batches of 1,000 through/bulk, closes the backfill when the source is exhausted, then polls until it isdone(or a batch has failed) and returns the final progress; - Errors: it throws
PurserErrorwithstatus,codeand the API'sbody.
Identity tokens: telling purser who the customer is
Sign a short-lived, single-use token on your server with the identity secret so the chat widget recognises a signed-in customer.
Refund endpoint (Actions API)
A signed HTTPS endpoint your custom store implements so purser can ask for a refund amount and start a refund: request format, signature check, idempotency, retries and reporting the outcome.