🧰TypeScript SDK

Install and use shippified-sdk, the typed client for every Shippified resource, plus its webhook signature helper.

Written for
TypeScript and JavaScript developers
Applies to
All plans
AdminUpdated Sep 26, 2026

shippified-sdk is a small TypeScript client for the REST API with no dependencies. It gives you typed methods for each resource and a verifier for webhook signatures. It runs anywhere fetch exists: Node 18+, browsers, Cloudflare Workers, Deno and Bun. The current version is 0.2.0.

The SDK is not published to npm. npm install shippified-sdk fails. You need a copy of the package source (the shippified-sdk folder) or a tarball packed from it. Without one, call the REST API directly: every SDK method is one plain HTTPS request, and every page in this section shows the curl version.

Install

The package is ESM-only ("type": "module"), ships its own type declarations, and needs Node 18 or newer.

cd shippified-sdk
npm install
npm run build          # compiles src/ to dist/

cd /path/to/your-project
npm install /path/to/shippified-sdk

A tarball is one file you can copy between machines, commit to your repo or install in CI.

Create a client

import { ShippifiedClient } from "shippified-sdk";

const shippified = new ShippifiedClient({
  token: process.env.SHIPPIFIED_API_KEY,   // "sk_…" API key (or a session token)
  // baseUrl: "https://shippified.net",     // the default
});

Option

Default

Description

token

none

An API key or session token. Needed for everything except the public methods.

baseUrl

https://shippified.net

Instance origin, without /api. A trailing slash is removed.

fetch

globalThis.fetch

Your own fetch, for runtimes without one. Without either, the constructor throws.

If token is missing, authenticated methods throw a plain Error (ShippifiedClient: token required for GET /api/orders) before sending anything.

Don't create a client with an API key in browser code: the key would be visible to anyone. Run the SDK on your server.

Resource clients

Property

Methods

Endpoints

orders

list(filters), get(id), create(input), patch(id, patch), delete(id), getTracking(id), refreshTracking(id), carrierStatus(), syncTracking(), reparse()

/api/orders…, /api/carrier-status

bots

list(opts), get(id), create(body), update(id, patch), delete(id)

/api/bots…

emailSources

list(opts), get(id), create(body), update(id, patch), delete(id), poll(id), rebuild(id, { sinceDays }), import(body)

/api/email-sources…, /api/email/import

templates

list() (built-ins), listCustom(), createCustom(body), updateCustom(id, body), deleteCustom(id), preview(body), matchPreview(body), detect(body)

/api/templates…

subscriptions

list(opts), get(id), create(body), update(id, patch), delete(id)

/api/subscriptions… (recurring costs)

shares

list(), create(body), delete(slug), imageUrl(slug), pageUrl(slug)

/api/shares…

apiKeys

list(), create(name), delete(id)

/api/account/api-keys…

account

setUsername(u), checkAvailability(u), setDisplayName(name), setVisibility(bool), setPublicStats(bool), setTimezone(tz)

/api/account/…

webhookLogs

list({ limit }), clear()

/api/webhook-logs

webhookSubscriptions

list(opts), get(id), create(body), update(id, patch), delete(id), test(id, eventType), rotateSecret(id)

/api/webhook-subscriptions…

billing

getUsage(), createCheckout(plan, opts), createPortal(returnUrl)

/api/billing/…

public

getLandingStats(), getProfile(u), getHealth(), getOpenApi()

Public endpoints, no token

Shortcuts on the client itself: getState() (GET /api/state), listOrders() (every order, paging with limit=200 until hasMore is false), listBots() (every bot as { bots }, paging with limit=200 until hasMore is false), createBot(), createShare(), getLandingStats(), getPublicProfile().

No SDK method exists for these; call them with fetch:

  • GET /api/bots/:id/logs (one bot's log), GET /api/inbox and GET /api/inbox/:id

  • Insights: GET /api/profitability, GET /api/calendar, GET /api/leaderboard

  • POST /api/settings, the embed-copier and Discord-bot endpoints

  • GET /api/account/export and DELETE /api/account, which need a signed-in session and refuse API keys anyway. See Authentication.

Examples

List orders with filters

const page = await shippified.orders.list({
  status: "shipped",
  hasTracking: true,
  from: "2026-09-01",
  limit: 100,
});

for (const order of page.items) {
  console.log(order.id, order.itemSummary, order.trackingNumber);
}
console.log(`${page.items.length} of ${page.total}, hasMore=${page.hasMore}`);

Filters that are undefined, null or "" are left out of the query string. OrderStatus is "ordered" | "canceled" | "shipped" | "delivered" | "issue", also exported as ORDER_STATUSES. The server ignores a status it doesn't recognise and returns unfiltered results, so use the typed values instead of casting strings.

Walk every order

const all = await shippified.listOrders();

Create an order

const { order, merged } = await shippified.orders.create({
  itemSummary: "Example Console Bundle",
  store: "bestbuy",
  orderNumber: "BBY01-000000000001",
  trackingNumber: "1Z999AA10123456784",
  costCents: 49999,
});
console.log(order.id, merged ? "merged into an existing order" : "created");

OrderCreateInput matches POST /api/orders, including eventType (exported values: ORDER_EVENT_TYPES).

Record a sale price

await shippified.orders.patch("ord_mpvu9uim_aeh5t", { salePriceCents: 62000 });
await shippified.orders.patch("ord_mpvu9uim_aeh5t", { salePriceCents: null });   // clear it

OrderPatch lists only what the server honours. salePriceCents is always writable; every other field (costCents, estimatedDelivery, trackingNumber, carrier, orderNumber, itemSummary, quantity) is written only when the order has no value yet. See Orders → Update an order.

Tracking

const { carriers } = await shippified.orders.carrierStatus();
const cached = await shippified.orders.getTracking("ord_mpvu9uim_aeh5t");
const live = await shippified.orders.refreshTracking("ord_mpvu9uim_aeh5t");
const summary = await shippified.orders.syncTracking();   // { total, refreshed, failed, delivered }

getTracking and refreshTracking return Promise<unknown>; the shape is in Orders → Tracking. Carriers without live tracking come back with liveUnavailable: true. A background tracker already refreshes in-flight orders, so you don't need to call syncTracking on a schedule.

Create a bot and build its URL

const bot = await shippified.bots.create({ name: "Acme Monitor", slug: "acme-monitor", type: "monitor" });
const { user } = await shippified.getState();
const url = `https://shippified.net/api/webhooks/discord/${user.webhookHandle}/${bot.slug}`;

Import an email

import { readFile } from "node:fs/promises";

const raw = await readFile("order-confirmation.eml", "utf8");
const result = await shippified.emailSources.import({ emailSourceId: "email_m1x2y3z5", raw });
console.log(result.order.id, result.merged, result.duplicate ?? false);

It goes through the same pipeline as live mail. It throws ShippifiedApiError with status 403 for a blocked sender, 422 when the email can't be read as an order, and 429 at the plan limit. See Import a raw email.

Poll and rebuild an email source

await shippified.emailSources.poll("email_m1x2y3z5");                       // check mail now
await shippified.emailSources.rebuild("email_m1x2y3z5", { sinceDays: 30 });  // 202, runs in the background

const source = await shippified.emailSources.get("email_m1x2y3z5");
console.log(source.rebuilding, source.lastRebuild, source.lastError);

rebuild throws 400 for forwarding sources and for sources that need reconnecting (needs_auth, auth_failed) or are paused, before anything is deleted, and 409 while one is already running. To re-run parsing on stored messages without reading the mailbox, use orders.reparse().

Templates

// Which template would win for this email, and what order would it make? Read-only.
const detected = await shippified.templates.detect({ source: "email", sample: rawEmail });

// Custom templates, newest first (the order they're tried in).
const templates = await shippified.templates.listCustom();

// Partial update: omitted fields keep their values. `source` can't change.
await shippified.templates.updateCustom(templates[0].id, { name: "Renamed" });

// Apply template changes to existing orders.
const summary = await shippified.orders.reparse();   // { reparsed, changed, merged, removed, skipped }

See the Templates API.

Webhook subscriptions

const sub = await shippified.webhookSubscriptions.create({
  url: "https://hooks.example.com/shippified",
  eventTypes: ["order.created", "order.shipped", "order.delivered"],
});
await saveSecret(sub.id, sub.secret!);   // only in this response

const result = await shippified.webhookSubscriptions.test(sub.id, "order.created");
console.log(result.ok, result.status, result.error);

// Subscriptions from before secrets were stored need a rotation to resume.
for (const s of (await shippified.webhookSubscriptions.list()).items) {
  if (s.needsSecretRotation) {
    const rotated = await shippified.webhookSubscriptions.rotateSecret(s.id);
    await saveSecret(rotated.id, rotated.secret);
  }
}

API keys

const { key, record } = await shippified.apiKeys.create("CI pipeline");
// `key` is the raw sk_… secret, returned only now.
await shippified.apiKeys.delete(record.id);

Verify webhook signatures

verifyShippifiedSignature checks the X-Shippified-Signature header with WebCrypto HMAC-SHA256 and a constant-time comparison. It works in Node 18+, browsers and edge runtimes.

import { verifyShippifiedSignature } from "shippified-sdk";

const valid = await verifyShippifiedSignature({
  rawBody,                                             // the exact body string
  header: request.headers.get("x-shippified-signature") ?? "",
  secret: process.env.SHIPPIFIED_WEBHOOK_SECRET!,
});

Option

Description

rawBody

The raw request body as a string. Don't re-serialise parsed JSON.

header

The X-Shippified-Signature value. The sha256= prefix is optional.

secret

The subscription's whsec_… secret.

crypto

Optional Crypto implementation where globalThis.crypto is missing; otherwise it throws verifyShippifiedSignature: WebCrypto unavailable….

For the scheme and examples in other languages, see Webhooks → Verify the signature.

Errors

Any non-2xx response throws ShippifiedApiError:

import { ShippifiedApiError } from "shippified-sdk";

try {
  await shippified.orders.get("ord_does_not_exist");
} catch (err) {
  if (err instanceof ShippifiedApiError) {
    console.error(err.status);   // 404
    console.error(err.message);  // the API's `error` string, or "HTTP <status>"
    console.error(err.body);     // parsed JSON body (or raw text)
  } else {
    throw err;                   // network error, missing token, …
  }
}

Status

What to do

401

The token is invalid, revoked or expired. Don't retry with it.

404

It doesn't exist, or isn't in this workspace.

429

A rate limit or cap. err.body.reason === "plan_limit_reached" means the free plan's monthly limit.

5xx

Retry with backoff. For orders.create, include an orderNumber so a retry merges instead of duplicating. getHealth() throws a 503 whose body is the health report.

Types

The exported types (Order, OrderPatch, OrderCreateInput, Bot, EmailSource, CustomTemplate, CustomTemplateInput, MatchRule, WebhookSubscription, WebhookEventType, BillingUsage, PublicProfile, Paginated<T> and more) follow the API's wire format as of 0.2.0. They're written by hand. Order has an index signature, so a field the server adds later is still reachable as unknown. PublicProfile.level is null when the owner has public stats turned off. For the authoritative field list, see Orders and the API reference.

Was this page helpful?