TypeScript SDK
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
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-sdkwill 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-sdkAs 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.tgznpm 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 |
|---|---|---|
| none | Bearer token: an API key or a session token. Required for every method except the public ones. |
|
| Instance origin, without |
|
| A custom |
If token is missing, authenticated methods throw a plain Error before any request is sent.
Resource clients
Property | Methods | Endpoints |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) andGET /api/inboxGET /api/carrier-status(which carriers have live tracking on the instance)GET /api/account/exportandDELETE /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 falseCreate 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 priceTracking
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 trackingYou 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 |
|---|---|
| The raw request body as a string. Don't re-serialize parsed JSON. |
| The |
| The subscription's |
| Optional |
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 |
|---|---|
| The token is invalid, revoked, or expired. Don't retry with the same token. |
| The resource doesn't exist, or it isn't in this workspace. |
| A rate limit or cap. Check |
| Retry with backoff. For |
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.