npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

shipbob-node-sdk

v0.0.28

Published

ShipBob API node SDK

Readme

ShipBob Node SDK

The ShipBob API is in a state of flux as their legacy 1.0 and 2.0 API versions are officially deprecated

end of support for 1.0 and 2.0 was July 31, 2026, and requests are progressively blocked through August 2026 until all 1.0 requests return 410 Gone (August 29, 2026).

First of all there are no official SDKs for ShipBob. I'm just dropping this here, in case it will speed up somebody else getting started using their API.

This SDK supports multiple API versions, but being generated it is lower-level than a hand-written wrapper would be. You will need to provide for "POST" a body and "GET" will use query. path will be needed when a parameter is part of the url. So, you'll need to refer to their API reference to be able to see how to pass parameters (the typings/intellisense shows them, too). Also, headers may be required as when you place orders (even with sendChannel set to true) to satisfy the typings.

Refering to the /test folder should provide enough examples to get started - there are inventory, create/update products and place order examples.

install

npm i shipbob-node-sdk
# or
pnpm add shipbob-node-sdk
# or
yarn add shipbob-node-sdk
import { createAPI } from 'shipbob-node-sdk';
// generated functions live under their version - 2026-07 is newest, and what createAPI defaults to
import { get202607Product } from 'shipbob-node-sdk/dist/client/2026-07';

// create your client like this (return object has extra functions/objects)
const client = await createAPI('<your-token-here>', 'https://sandbox-api.shipbob.com');
// the generated functions are now authenticated - nothing to pass around
const productSearch = await get202607Product();

API implementation

Everything is implemented except for the /simulate/* endpoints, which are not in OpenAPI (see "Follow URIs and simulate" below).

Generated clients ship for 2025-07, 2026-01 and 2026-07 (import from shipbob-node-sdk/dist/client/<version>):

  • createAPI(...) applies your token/base URL/headers and the interceptors to every version's HTTP client, so generated functions from any shipped version just work (ie: get202601Product(), get202607Product()). Their API version is baked into their URL.
  • options.apiVersion selects which version's GET /channel retrieves the channels (unknown versions like experimental fall back to the newest). It's also the default path prefix for the get/post/put/patch/delete helpers (see "Follow URIs and simulate").
  • options.channelLoadRetry retries a failed GET /channel - { retries?: number; delaySeconds?: number; maxRateLimitWaitSeconds?: number }. Their gateway returns the occasional ephemeral 401 (empty body) on a token that succeeds seconds either side, which without a retry fails createAPI outright. A 403 is never retried - that one is a real missing channels_read scope. Skip the call entirely by injecting options.channel. Each option is documented in full on CreateOptions.
    • retries (default 1, max 10) - retries, not attempts, so the default is up to 2 calls. 0 disables.
    • delaySeconds (default 0.5, max 60) - the wait before retrying a connection failure, 401 or 5xx. A retry-after on those is logged but not obeyed - it is not about your request rate.
    • maxRateLimitWaitSeconds (default 5, max 60) - the longest 429 wait to sit through. A 429 is slept for its full x-retry-after or not retried at all, since retrying into a window that has not cleared cannot succeed. Raise it to wait longer ones out, at the cost of createAPI blocking that long.
  • One active createAPI at a time. Each version's generated client is a module-level singleton, so a second createAPI re-points baseUrl/auth and replaces the interceptors - the newest call wins outright, and the earlier API object's requests go out with the newer token. Sandbox and production side by side in one process are not supported.

Known OpenAPI spec deficiencies

The vendored openapi-*.json files are kept exactly as ShipBob publishes them, so their defects flow into the generated types. Cataloged here instead of patching the spec (a re-download would silently revert any patch). Sometimes their specs even have issues that prevent them being used to generate code:

  • Untyped schemas generate unknown. 22 of /product's 26 query param schemas (ie: Products.Get.Api.V5.Product.PageSize.Int) are literally {"additionalProperties":false} - no type - in 2025-07, 2026-01 and 2026-07, so PageSize, SKU, Barcode, HasVariants etc. compile with any value (250, '250', an object). Only Search, SortBy, SortOrder (and IsInventorySyncEnabled, below) have a type. The inventory endpoints type theirs properly ({"type":"integer"}), which is why the compiler checks those but not these. Worse for 2025-07 as of the 2026-09-25 download: its older export typed these params string, so they at least rejected numbers and objects.

  • IsInventorySyncEnabled cannot be sent. The /product filter is declared {"type":"object"} with no properties, which generates { [key: string]: never } - only an empty object type-checks, and a boolean does not. Cast it, or use the get helper. In 2026-01 and 2026-07, and in 2025-07 as of the 2026-09-25 download (was string).

  • List query params must be comma-separated. The spec implies the OpenAPI default serialization (explode: true, repeated keys ie: InventoryIds=1&InventoryIds=2), but the API silently honours only one key and returns partial results instead of failing. createAPI forces explode: false (InventoryIds=1,2) on every client.

  • Reserved characters must be sent unescaped. Query values only match with literal , and : - the request interceptor un-escapes %2C/%3A on every URL.

  • Paging hrefs are missing the version prefix (and use different casing, ie: /Product?cursor=... for /product - their routing is case-insensitive). See "Follow URIs and simulate".

  • Announced endpoints can be missing from the spec. Their changelog lists the sandbox simulations with the 2026-01 release (POST /2026-01/simulate/shipment, GET /2026-01/simulate/status/{simulationId}), but none of our spec downloads has ever contained them - "simulat" appears in no openapi-*.json in this repo's history, including the 2026-09-25 downloads. Call them with the get/post helpers (see "Follow URIs and simulate"). POST /inventory/history:query from the same changelog entry did make it into 2026-01 and 2026-07.

  • Webhook payloads have no published schema at all. The spec only covers webhook management (subscribe/list/delete) - the delivered payloads are typed nowhere. They approximately mirror the resource view models (ie: OrdersShipmentViewModel); the differences below were verified against live 2026-01 captures and are corrected in WebhookPayloadsByTopicV2:

    • shipment payloads carry order_type and zone (absent from OrdersShipmentViewModel); zone.id is null on a cancelled shipment, and tracking is null until a label exists.
    • retailer_program_data named its dates camelCase in the spec (doNotShipBeforeDate, shipByDate) but the wire sends snake_case (do_not_ship_before_date, ship_by_date). The spec now uses snake_case - 2026-01 as of the 2026-08-19 download (the 2026-06-22 one was still camelCase), 2026-07 from its first download (2026-08-19), and 2025-07 as of the 2026-09-25 download. 2026-09-25 payloads still send snake_case, so spec and wire now agree and WebhookPayloadsByTopicV2 no longer corrects them.
    • several fields arrive null where the spec says optional-only: status_details[].extra_information, shipping_terms.carrier_type / payment_term, and shipment measurements (on the order.shipment.on_hold and order.shipment.exception captures, both 2026-09; the cancelled and delivered captures carried measurements). purchase_order_number and retailer_program_type arrive null too, where the spec now says required (below).
    • The same spec fix tightened those two types. The old specs' required listed purchaseOrderNumber / retailerProgramType - camelCase names matching no property, so it was silently ignored and both generated optional. The fixed required names the real properties, so purchase_order_number and retailer_program_type now generate as required, non-null string (same downloads as the date fix). 2026-09-25 payloads still send null for both, so the generated types are now wrong where they used to be merely loose - WebhookPayloadsByTopicV2 overrides them as string | null.
    • no model exists for the order.shipment.tracking.updated payload (the Tracking* schemas are the public tracking API - a different shape). The topic itself was missing from the spec's generated WebhooksTopics enum for 2026-01 until the 2026-08-19 download - the enum WebhookPayloadsByTopicV2 (hand-written) checks its topics against.

    Verified 2026-09-25 by type-checking a live capture of every subscribed topic (order.shipped, order.shipment.delivered / cancelled / on_hold / exception / tracking.updated) against WebhookPayloadsByTopicV2: all pass, and the shipment and order captures fail against the raw generated types. The scrubbed captures live in test/webhook-payloads-v2.samples.ts and are type-checked on every yarn build.

  • The webhook create response is under-declared. 200 is the spec's only success code; the 201 that means the subscription was actually created appears in no yyyy-mm spec, and no spec has ever declared both (1.0/2.0 declare only 201). See "Undocumented behaviour" below.

  • Assorted wrong response types - see the hand-written corrections in src/types.ts (ie: SetExternalSyncResponse).

Undocumented behaviour (and where the docs are wrong)

The spec deficiencies above are at least machine-readable. These are things their published documentation states incorrectly, or does not state at all - each one below cost a support ticket or a live capture to establish, so they are recorded here rather than rediscovered.

  • Webhook subscriptions survive an API version's end-of-life. Their team confirmed it on request; nothing documents it. With a 6-month release cadence, this is what decides whether you need a migration job - you don't. A subscription stops only when deleted. See "Webhooks".

  • POST /webhook reports success for a subscription it did not create. 201 means created; 200 means the topic+url is already registered and nothing happened, so events never flow. Only 200 is documented and it is the spec's sole success code, so the status that means "it worked" is undeclared and a generated client treats the no-op as the happy path. Their old v2.0 page had 201 right; the current page does not. Treat anything but 201 as a failure.

  • The V2 webhook generation is named everywhere except the documentation. 2025-07 is where their webhook schemas first carry a version marker at all - Webhooks.V2.*, against a plainly unversioned Webhooks.Create.WebhookViewModel in 1.0/2.0 (there is no V1). It is the same boundary where topics went underscore to dot, the delivery header went shipbob-topic to x-webhook-topic, and the declared create status changed. Four wire changes, one version bump, one name, and no page connecting them. It spans every yyyy-mm version, which is why WebhookPayloadsByTopicV2 has no per-version twin.

  • ShipBob silently migrated existing subscriptions when V2 landed. Unconfirmed by them - this is inferred from our own API testing, but the behaviour is consistent. A subscription registered before 2025-07 should speak one generation; a migrated one is a single "frankenstein" record living in both, and that explains every oddity below:

    • it publishes both shipbob-topic: order_shipped and x-webhook-topic: order.shipped, so one event arrives twice - handle both, or deduplicate.
    • the management endpoints list it in dot notation with nothing marking its generation, so they cannot be used to tell migrated from native.
    • a create for a topic+url it already holds returns 200 (see above) even though nothing in the current view looks like a conflict.
    • deleting works normally and from either side: one DELETE against the legacy or the current endpoint unsubscribes both, confirming it really is one record.

    Delivery is run through a third party (SVIX). Might explain why Shipbob's own support seem unable to answer questions on how webhooks work.

  • The 150/min rate limit is a default - limits are raised per account on request, so read x-remaining-calls rather than the documented number. Beware x-remaining-calls-ip, an unmentioned per-IP counter that reads like the real one but isn't. They cannot or will not create a second account, so you can separate writes vs. reads. ie: placing orders and syncing inventory must share the same rate limit, but they will bump your rate limit! I suggest throttling your calls and using processing queues with managed concurrency.

  • Ephemeral 401s on a valid token are not acknowledged anywhere. Their gateway intermittently returns a 401 with an empty body on a token that succeeds seconds either side, with rate-limit headers showing plenty of budget remaining. options.channelLoadRetry exists because of this.

  • Some constants exist only in a PDF. packaging_material_type_id is a bare number in the spec and the id-to-name mapping is published only as a document (with a gap where value 4 should be), so PackagingMaterial in src/types.ts is transcribed by hand. GET /packaging-requirement is the only runtime source of truth.

Webhooks

As a personal note. I regret implementing webhooks at all, since I needed to implement polling anyway. There are various gaps in the webhooks. Using their table is perhaps the best way to explain:

| Shipment Status | Status Detail Name | Comment | | --------------- | ------------------ | ----------------------------------------------------------------------------------------------- | | Processing | Labeled | I think this maps to "order.shipped" webhook, doesn't always have tracking (or invoice amounts) | | Completed | Delivered | Does not always fire when delivered (seems to be carrier dependent) | | Completed | Delivery Exception | No Webhook (need to poll for RTS, etc.) | | Exception | * | These do fire, but are not useful for delivery |

ie: No webhooks for delivery exceptions ie: Completed -> DeliveryException when carrier needs more details. The shipment_exception webhook only applies to before the shipment leaves their FC (ie: inventory related issue).

Partial rows of table from here: (https://developer.shipbob.com/#shipment-statuses)

Webhook payload typings are exported as WebhookPayloadsByTopicV2 for narrowing an incoming payload by its topic. It's basically an order and the status is known. WRO and other webhooks were added sometime in 2026.

The topics are not versioned the way the REST endpoints are - there is no ...For202607 twin, and you subscribe through whichever versioned /webhook endpoint you like. ShipBob have confirmed that a subscription outlives the API version that created them: one made via /2026-01/webhook keeps delivering after 2026-01 reaches end of support, so you do not re-create subscriptions on their 6-month release cadence. A subscription stops only when you delete it.

That cuts both ways: subscriptions predating the 2025-07 rename are still delivering the older underscore topics, typed as WebhookPayloadsByTopicLegacy. Narrow on the header rather than the topic - legacy posts shipbob-topic: order_shipped, newer ones post x-webhook-topic: order.shipped - because get202601Webhook and the dashboard list every subscription in dot notation regardless of its generation. Subscriptions migrated when 2025-07 landed are one record in both systems and fire both headers, so handle both or deduplicate.

Follow URIs and simulate

The returned get/post/put/patch/delete helpers cover endpoints outside the OpenAPI spec (ie: /simulate/* - see /test/shipbob-api.simulate.spec.ts) and paging URIs (next/prev/first/last in the JSON responses - see the paging tests). The paging HATEOAS is not a full href - the API version prefix is missing (ie: /Product?cursor=...) - so the helpers prepend options.apiVersion (defaults to 2026-07, the newest - longest support before the version sunsets), with a per-call override (ie: api.get('/product', 'experimental')):

import { createAPI } from 'shipbob-node-sdk';
import { get202601Product } from 'shipbob-node-sdk/dist/client/2026-01';
import {
  type Get202601ProductResponses,
  type Get202601ProductErrors,
} from 'shipbob-node-sdk/dist/client/2026-01/types.gen';

const client = await createAPI('<your-token-here>', 'https://sandbox-api.shipbob.com', {
  logTraffic: true,
  // version prefix for the get/post helpers (and the channel load)
  apiVersion: '2026-01',
});
const productSearch = await get202601Product();
assert.ok(productSearch.data, 'should have data');
const { next } = productSearch.data;
assert.ok(next !== null && next !== undefined, 'should have more pages');
// you can get these types by clicking ie: `get202601Product` function and look at typings parameters.
const pagedResult = await client.get<Get202601ProductResponses, Get202601ProductErrors>(next);
assert.ok(pagedResult.data, 'should have paged data');

Building locally

For making changes to this library locally - use yarn link to test out the changes easily. This is useful if you would like to contribute. If you don't want an NPM dependency, it's easy to generate this yourself from OpenAPI spec and then just copy the index.ts file to your project.

NOTE: I did not notice until I had written a custom implementation that ShipBob had published an Open API spec :facepunch:.

# this is how the clients are generated (see scripts in package.json)
$ yarn generate:2026-01
$ yarn generate:2026-07

Testing

Running the tests in this repo

yarn test (mocha + tsx). There are two kinds of spec in /test:

  • Offline - channel-load-retry and send-channel-id replace fetch with a stub, need no credentials and always run.
  • Live - everything else calls the real ShipBob API and asserts on data belonging to one account (particular orders, channels and webhook subscriptions), so they cannot pass against anyone else's. They skip when their credentials are absent, printing which key was missing, so a clean checkout gets a green run rather than a confusing failure.

To actually run the live ones, uncomment the keys you need in test/.env (test/.env.sample is the template). Doing so points them at whatever SHIPBOB_API_URL holds and they will issue real requests - several create and delete orders and webhook subscriptions.

# for the PAT (Personal Access Token) login (recommended)
SHIPBOB_API_TOKEN=<redacted>
SHIPBOB_API_URL=https://sandbox-api.shipbob.com
# for the oAuth login
SHIPBOB_CLIENT_ID=<redacted>
SHIPBOB_CLIENT_SECRET=<redacted>
# for the Web UI scraper (web.shipbob.com) login
[email protected]
SHIPBOB_WEB_UI_PASSWORD=<redacted>

To run a subset, use --grep - .mocharc.json sets a spec glob that both positional arguments and --spec merge with rather than replace, so naming a file does not narrow anything:

npx mocha --grep "channelLoadRetry"   # just the offline retry suite

You can also run and debug any of these in VS Code.

Mocking this library in your own tests

You can fake out this library itself, or otherwise mocking the ShipBob API http calls are quite easy to simulate with nock. Here's a way to test creating an order verifying idempotent operation.

// NOTE: nock > 14 with undici is needed to mock underlying "fetch" calls
const CHANNELS_RESPONSE = {
  items: [{
    id: 1,
    application_name: 'SMA',
    name: 'test',
    scopes: []
  }]
};

const nockScope = nock('https://sandbox-api.shipbob.com')
  .defaultReplyHeaders({ 'content-type': 'application/json' })
  // createAPI loads channels following `options.apiVersion` (defaults to the newest API version)
  .get('/2026-07/channel')
  .once()
  .reply(200, JSON.stringify(CHANNELS_RESPONSE))
  .post('/2026-01/order')
  .once()
  .reply(422, JSON.stringify({
    "": [
      "Cannot insert order with existing ReferenceId"
    ]
  }))
  .get('/2026-01/order?ReferenceIds=123')
  .once()
  .reply(200, JSON.stringify([{
    id: 1,
    order_number: '18743683',
  }]))
  ;
...
assert.ok(nockScope.isDone(), 'should have completed nock requests');

Adding more events

To replace what could be considered "missing" webhooks, such as WRO completed (Receiving originally had no webhooks!).

You can follow the section How to sync WROs for a more robust solution. Read on if you are interested in how this works, but also why it won't work!

If you want something more event driven, you can use the emails they send out with an inbound email processor: ie:

// this is done using Mandrill/Mailchimp Inbound mail processor:
for (const event of events) {
  const { subject } = event.msg;
  switch (subject) {
    case 'Your WRO is now complete':
      // ie: Your WRO 756713 is now complete and all associated inventory is ready to ship! ...
      //     https://web.shipbob.com/app/Merchant/#/inventory/receiving/756713/summary ...
      const match = /Your WRO (?<wro>\d+) is now complete/i.exec(event.msg.text);
      if (match === null || match.groups === undefined || !('wro' in match.groups)) {
        throw new Error(`cannot find wro in email '${taskStorageId}'`);
      }
      const wro = match.groups.wro;
      console.log(` Got it!  Received WRO# '${wro}'`);
      break;
    case 'Your box/pallet is now complete!':
      console.log(`Ignoring subject: '${subject}'`);
      break;
    default:
      console.log(`Unsupported subject: '${subject}'.`);
      break;
  }
}

You can publish that as an event or push to a queue and it will act as a "webhook".

NOTE: I discovered after writing the above inbound mail handler that a WRO you create may be split. ie: we created 1 WRO and it was split into 6 more WROs by the ShipBob team, so it's not really possible to link back to your system when that occurs. Also, they have indicated to me there's no link on these WROs or UROs (Unidentified Receiving Orders) that they create. There's no hierarchical relationship with the split WROs they are creating. In other words, you will need to implement polling anyway, so adding this is probably not worthwhile.

UPDATE: 2026-07 (in the spec since our first download, 2026-08-19) links UROs to WROs, one way only:

  • URO -> WRO: per the spec, every URO carries linked_wro_id (null/0 when unlinked) and linked_wro_date - not yet checked against a live response. GET /2026-07/unidentified-receiving-order?isCompleted=true lists linked UROs (false, the default, lists unlinked), and POST /2026-07/unidentified-receiving-order/{uroId}:associateWRO links one yourself. Not in 2026-01 or 2025-07.
  • WRO -> URO: nothing. The WRO has no URO field and no receiving endpoint filters by URO, so to find a WRO's UROs, page the linked UROs and match linked_wro_id yourself.
  • Split WROs are still unrelated to each other.

Broken in practice (2026-09-25, production): GET /2026-07/unidentified-receiving-order returns 200 with an empty array for both isCompleted=false and isCompleted=true - page 1 of both unlinked and linked is [], so the list endpoint returns no UROs either way. The link fields above are therefore unverified against any real URO. See the URO test in test/shipbob-api-2026-07.spec.ts. Their website also seems to be broken "on hold receiving" has none either.

OAuth

There is no S2S (server-to-server) oAuth. User intervention is required. There are only helper methods to help with that. You could bypass that with a webscraper (see next section).

  • oAuthGetConnectUrl() direct an end user to follow the generated URL. Use offline_access as part of scope to get a refresh token.
  • oAuthGetAccessToken() call this with the code query string fragment https://yourname.ngrok.io/#code=...&id_token=... the redirect_uri (and your client Id and secret from ShipBob App)
  • oAuthRefreshAccessToken() call this with the last refresh_token you received, otherwise the same as oAuthGetAccessToken() without code.

The method to get/refresh access token both return access_token, so you provide that to createAPI(...) with your application name (via options.channelPredicateOrApplicationName).

If you create products with your API, you will not be able to see them with an oAuth app. I went down a big rabbit hole here - something to do with different channels. Try not to waste as much time as I did here and avoid using oAuth unless you are building actually an app IMO.

Access APIs available to web.shipbob.com

The Web UI has a much broader set of unsupported APIs (ordersapi.shipbob.com, shippingservice.shipbob.com, merchantpricingapi.shipbob.com, etc.). For example, you cannot "patch" a WRO on the public receiving API, but you can somehow with a web login, so it unlocks extra functionality and has a separate rate limit from the API. You would need to create a ShipBob account and then use those credentials to login with a scraper (see /src/WebScraper.ts). The account can only be logged with one session at a time. Look at AuthScopesWeb type and you can probably work backwards the scopes needed for each API. If you go this route then the peer dependency on puppeteer is not optional.

There is no documentation for these extra APIs - you can look through network tracing in the browser. See unit tests for full example:

// scrape web.shipbob.com website:
// NOTE: not exported from the package root (that would make puppeteer a hard dependency)
import { getAccessTokenUI } from 'shipbob-node-sdk/dist/WebScraper';
import { createAPI } from 'shipbob-node-sdk';
import { get202601InventoryLevelLocations } from 'shipbob-node-sdk/dist/client/2026-01';

const authResponse = await getAccessTokenUI({ email, password }, 60 /* timeout seconds */);

const missingChannelScope = authResponse.scopes.indexOf('channels_read') === -1;

const extraHeaders = {
  origin: 'https://web.shipbob.com',
  referer: 'https://web.shipbob.com/',
};

// options are a discriminated union: load + select ('SMA'), inject a persisted `channel`, or skip entirely
await createAPI(
  authResponse.accessToken,
  'https://api.shipbob.com',
  missingChannelScope
    ? { skipChannelLoad: true, extraHeaders }
    : { channelPredicateOrApplicationName: 'SMA', extraHeaders }
);

const inventoryListResponse = await get202601InventoryLevelLocations({
  query: { InventoryIds: [20104984] },
});

Polling Orders for tracking

This is a suggested way to track orders from ShipBob. This may be better than using the webhook, sometimes the order_shipped webhook fires with no tracking information.

  1. Poll GET /2026-01/order?HasTracking=true&IsTrackingUploaded=false&startDate=03-25-2025
// right now for me I get error "An unexpected database error occurred. Please try again later", but it worked on 1.0 API
const result = await get202601Order({
  query: {
    HasTracking: true,
    IsTrackingUploaded: false,
    StartDate: '2025-03-25T14:15:22Z',
  },
});
  1. Iterate through each order (and each shipment)
  2. Sync the tracking back to your platform
  3. Mark the order as shipped using this endpoint (https://developer.shipbob.com/api-docs/#tag/Orders/paths/~11.0~1shipment~1%7BshipmentId%7D/put). Or, you can mark it as shipped using the bulk mark as shipped endpoint (https://developer.shipbob.com/api-docs/#tag/Orders/paths/~11.0~1shipment~1:bulkUpdateTrackingUpload/post).

How to sync WROs

Syncing WROs (Warehouse Receiving Orders) back to your system has 2 options.

Option #1 - Simple (wait until the WRO status is Completed)

  • Poll our GET WRO endpoint for WROs in Completed status: GET https://sandbox-api.shipbob.com/2026-01/receiving?Statuses=Completed&ExternalSync=false
/**
 * this was /2.0/receiving-extended
 */
const response = await get202601Receiving({
  query: {
    Statuses: ['Completed'],
    ExternalSync: false,
  },
});
  • Iterate through each inventory id in the inventory_quantities array and sync the stowed_quantity back to your system
  • Mark the WRO as synced (see below for the endpoint to use and sample request)
  • Poll the GET WRO endpoint again and it will no longer show up as you already synced this and ExternalSync is now set to true

Mark the WRO as synced (this is optional and a alternative option to continuously polling completed WROs that you may or may not know you have already synced)

// use Ids from "getReceivingExtended" with ExternalSync: false.
const response = await post202601ReceivingSetExternalSync({
  body: {
    ids: [918363],
    is_external_sync: true,
  },
});

Option #2 - Advanced (partial receiving)

  • Poll GET WRO endpoint for WROs with statuses "processing" and "completed": https://sandbox-api.shipbob.com/2026-01/receiving?Statuses=Processing,Completed&ExternalSync=false.
const response = await get202601Receiving({
  query: {
    Statuses: ['Processing'],
    ExternalSync: false,
  },
});

Note: When initially testing you can remove the statuses from the query params, and you will see the default status of "AwaitingArrival" whenever a WRO is created. There is no sandbox simulation to move these forward.

  • For each WRO, make a request to the Get Warehouse Receiving Order Boxes endpoint: https://sandbox-api.shipbob.com/2026-01/receiving/442997/boxes
  • Iterate through each box
  • Iterate through each item in the box_items array
  • Sync the stowed_quantity for each item back to your system

Synchronizing inventory levels

There are no webhooks or easy way to sync. Plus once you place an order there is a delay (~1 minute) before the inventory levels are impacted.

So, if you have one FC, you can use total_sellable_quantity for each inventory item.

For multiple FCs - their recommendation is to use for each FC the fulfillable_quantity and subtract the total_exception_quantity (since it could be assigned to either FC).

You can't do this anymore in 2026-01 - it requires 2 separate API calls (inventory-level and inventory-level/locations). See the get inventory for a SKU test in test/shipbob-api-2026-01.spec.ts for a working example of combining them.

{
  "id": 1234,
  "name": "...",
  "total_fulfillable_quantity": 1688,
  "total_onhand_quantity": 1688,
  "total_committed_quantity": 0,
  "total_sellable_quantity": 1688,
  "total_exception_quantity": 0,
  "fulfillable_quantity_by_fulfillment_center": [
    {
      "id": 211,
      "name": "Fairburn (GA)",
      "fulfillable_quantity": 1688,
      "onhand_quantity": 1688,
      "committed_quantity": 0,
      "awaiting_quantity": 0,
      "internal_transfer_quantity": 0
    }
  ]
}

Background

Originally I wrote a library that had endpoints not available in OpenAPI spec ie:

  • /2.0/receiving-extended
  • /experimental/product :skull:
  • /experimental/receiving :skull:

It was originally cumbersome to keep up-to-date and their dated API versions expire on a schedule (currently supported for 24 months, new release every 6 months), so it only made sense to use only their OpenAPI specs to generate version specific clients.