TypeScript SDK

AdminUpdated Sep 24, 2026

TypeScript SDK

shippified-sdk is a small, dependency-free TypeScript client for the REST API. It provides typed methods for each resource and a signature verifier for outbound webhooks. It works anywhere a fetch implementation exists: Node 18+, browsers, Cloudflare Workers, Deno, and Bun. The current version is 0.2.0.

Important: The SDK is not published to npm. npm install shippified-sdk will fail or install an unrelated package. Install it from a checkout of the Shippified repository or from a tarball you pack yourself, as shown below.

Install

The SDK is in the shippified-sdk/ directory of the Shippified repository. Its package entry point is the compiled dist/ output.

From a checkout

Build it, then add it to your project as a local dependency:

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

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

As a tarball

A tarball is a single self-contained file you can copy to another machine, vendor into your repository, or install in CI. npm pack builds the SDK first:

cd shippified/shippified-sdk
npm install
npm pack                                   # writes shippified-sdk-0.2.0.tgz

cd /path/to/your-project
npm install /path/to/shippified-sdk-0.2.0.tgz

npm can't install a package from a subdirectory of a git repository, so a github: dependency URL does not work for the SDK. Use one of the two options above.

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

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",     // default
});

Option

Default

Description

token

none

Bearer token: an API key or a session token. Required for every method except the public ones.

baseUrl

https://shippified.net

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

fetch

globalThis.fetch

A custom fetch, for environments that don't have one built in.

If token is missing, authenticated methods throw a plain Error before any request is sent.

Resource clients

Property

Methods

Endpoints

orders

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

/api/orders…

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)

There are also convenience methods on the client itself:

  • getState(): GET /api/state.

  • listOrders(): pages through every order and returns them as one array.

  • listBots(), createBot(), createShare(), getLandingStats(), getPublicProfile().

A few endpoints have no SDK method. Call them with fetch directly:

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

  • GET /api/carrier-status (which carriers have live tracking on the instance)

  • GET /api/account/export and DELETE /api/account. These need a signed-in session and refuse API keys. See Authentication → Account export and deletion.

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}`);

Boolean and number filters are serialized as query parameters. Filters that are undefined, null, or "" are left out.

OrderStatus is "ordered" | "canceled" | "shipped" | "delivered" | "issue", also exported as the ORDER_STATUSES array. See Orders → Status. The server ignores a status value it doesn't recognize and returns unfiltered results, so stick to the typed values rather than casting strings. Status is derived by the server from parsing and carrier tracking and can't be set through the API.

Walk every order

const all = await shippified.listOrders();   // pages with limit=200 until hasMore is false

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 existing order" : "created");

Record a sale price

const updated = await shippified.orders.patch("ord_mpvu9uim_aeh5t", {
  salePriceCents: 62000,
});

OrderPatch lists only what the server honors. salePriceCents is always writable, and null clears it. Every other field (costCents, estimatedDelivery, trackingNumber, carrier, orderNumber, itemSummary, quantity) is fill-missing-only: it is written only when the order has no value yet. Adding a tracking number to an ordered order moves it to shipped. Values you write are recorded in the order's userEdited list, and re-parsing never overwrites them. A patch that changes nothing returns the order unchanged. See Orders → Update an order.

await shippified.orders.patch("ord_mpvu9uim_aeh5t", { salePriceCents: null });   // clear the sale price

Tracking

const cached = await shippified.orders.getTracking("ord_mpvu9uim_aeh5t");
const live = await shippified.orders.refreshTracking("ord_mpvu9uim_aeh5t");

Both return Promise<unknown>. The response shape is described in Orders → Tracking. Carriers without live tracking on the instance come back with liveUnavailable: true instead of carrier events.

const summary = await shippified.orders.syncTracking();
// { total, refreshed, failed, delivered }: up to 50 in-flight orders on carriers with live tracking

You don't need to call syncTracking on a schedule. A background tracker already refreshes in-flight orders on live carriers.

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);

import goes through the same pipeline as live mail: the source's allowed senders are checked against the real sender, and a repeated Message-ID comes back with duplicate: true. 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. from is only a fallback sender for pasted bodies that have no headers.

Poll and rebuild an email source

await shippified.emailSources.poll("email_m1x2y3z5");                     // fetch new 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 re-reads the mailbox and replaces the orders this source produced in the window. It checks that the mailbox can be read before it deletes anything: if sign-in fails, nothing is changed and lastRebuild.error says why. Orders you edited (including ones with a sale price) and orders that also came in through Discord or manual entry are kept. It throws 400 for forwarding sources and 409 when a rebuild is already running. To re-run parsing on messages Shippified already stored, without reading the mailbox, use orders.reparse().

Templates

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

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

// Partial update: omitted fields keep their current 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 for the template model.

Create a webhook subscription

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

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

// Rotating returns a new secret once, resets the failure counter and reactivates the subscription.
const rotated = await shippified.webhookSubscriptions.rotateSecret(sub.id);
await saveSecret(rotated.id, rotated.secret);

Mint and revoke API keys

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

Verifying webhook signatures

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

import { verifyShippifiedSignature } from "shippified-sdk";

export default {
  async fetch(request: Request, env: { SHIPPIFIED_WEBHOOK_SECRET: string }) {
    const rawBody = await request.text();                     // exact bytes, before JSON.parse
    const valid = await verifyShippifiedSignature({
      rawBody,
      header: request.headers.get("x-shippified-signature") ?? "",
      secret: env.SHIPPIFIED_WEBHOOK_SECRET,
    });
    if (!valid) return new Response("invalid signature", { status: 401 });

    const event = JSON.parse(rawBody);
    // handle event.type / event.data
    return new Response(null, { status: 204 });
  },
};

Option

Description

rawBody

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

header

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

secret

The subscription's whsec_… secret.

crypto

Optional Crypto implementation for runtimes without globalThis.crypto.

For the signature scheme and code in other languages, see Webhooks → Verifying signatures.

Errors

A response with a status other than 2xx 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, etc.
  }
}

Status

Suggested handling

401

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

404

The resource doesn't exist, or it isn't in this workspace.

429

A rate limit or cap. Check err.body.reason === "plan_limit_reached" for the monthly plan limit.

5xx

Retry with backoff. For POST /api/orders, include an orderNumber so a retry merges instead of creating a duplicate.

Type coverage

The exported types (Order, OrderPatch, Bot, EmailSource, CustomTemplate, WebhookSubscription, WebhookEventType, PublicProfile, Paginated<T>, and others) follow the API's wire format as of 0.2.0. They are hand-maintained. Order has an index signature, so a field the server adds later is still available as unknown. PublicProfile.level is null when the profile owner has public stats turned off.

One known gap: WebhookSubscription doesn't declare needsSecretRotation, which the server returns on every subscription. Read it as (sub as { needsSecretRotation?: boolean }).needsSecretRotation, and rotate the secret when it is true (see Webhooks).

For the authoritative field list, see Orders and the OpenAPI spec at /api/openapi.json.

Was this page helpful?
TypeScript SDK