Developers2026-11Preview

NinjaBuy Business API

Import products from Taobao, Weidian and 1688, keep their price and stock in sync, and follow every order from the seller in China, through our warehouse check, to your customer's door. JSON over HTTPS, one account, one balance.

Base URL

Every route on this site lives under one base URL. The Shopify app and everything you build call the same routes, so whatever the app does, your code can do too.

https://www.ninjabuy.com/api/v1

Prefer to ask an AI assistant? The same account works over MCP at https://www.ninjabuy.com/mcp: AI assistants (MCP).

Catalog
Orders
Account
Your first call
curl "https://www.ninjabuy.com/api/v1/balance" \
  -H "Authorization: Bearer $NINJABUY_API_KEY" \
  -H "NinjaBuy-Version: 2026-11"

Authentication

Send an API key as a bearer token on every request. Keys start nb_. During the preview our team issues them: once your Business account is set up we send each key once, over a channel you choose, with the scopes you asked for. Ask us to revoke one and it stops working on the next request.

Keep keys on your server. A key in a browser, an app or a public repository can be read by anyone, and it can spend your balance.

An app that acts for other NinjaBuy sellers uses OAuth 2.1 instead: dynamic client registration, the authorization code flow with PKCE (S256), and the seller's own approval screen. Its access token goes in the same header and works on the same routes. Access tokens last an hour; refresh tokens renew them for seven days. AI assistants connect the same way (MCP).

products:readscope
Read imports, products, variants and sync rules.
products:writescope
Import, edit, push and delete products; change sync rules.
inventory:readscope
Read inventory levels.
orders:readscope
Read orders, fulfillment orders and shipments.
orders:writescope
Create, hold, release and cancel orders. Spends your balance.
balance:readscope
Read the balance and its transactions.
webhooks:writescope
Manage webhook endpoints.

A call outside a key's scopes answers 403 insufficient-scope and names the scope it needs.

Header
Authorization: Bearer nb_7Qk2...vX9e
Response 401
HTTP/2 401

{
  "type": "https://www.ninjabuy.com/api/v1/problems/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "The API key is missing, wrong, expired or revoked.",
  "request_id": "req_7Hq2Lw9c"
}

Versioning

The path carries the major version, /v1. Inside it, dated versions change fields and behaviour. Pin the one you built against with the NinjaBuy-Version header; every answer names the version it used in the same header.

2026-11 is the current version, and it is a preview: names and fields can still change before it is marked stable. Every change is in the changelog, and we tell every key holder by email before a change that could break them.

Within a version we only add: new routes, new optional parameters, new fields in answers and new event types. Build your client to ignore fields it does not know.

Pin a version
GET /api/v1/orders
NinjaBuy-Version: 2026-11

HTTP/2 200
NinjaBuy-Version: 2026-11

Requests and answers

  • Send JSON with Content-Type: application/json; answers are JSON in UTF-8. Errors are application/problem+json.
  • Money is an object with a decimal string and a currency: { "amount": "264.20", "currency": "CNY" }. What NinjaBuy charges is in CN¥; your selling prices are in your store's currency.
  • Ids are strings with a prefix that names their kind (prd_, ord_, shp_). Treat them as opaque.
  • Times are ISO 8601 in UTC. Countries are two-letter ISO codes.
  • Every answer carries an X-Request-Id header, and every error carries the same id as request_id. Quote it when you write to us.
Money, ids and dates
{
  "id": "ord_3n7q",
  "created_at": "2026-11-01T09:14:02Z",
  "total": { "amount": "264.20", "currency": "CNY" },
  "suggested_price": { "amount": "49.99", "currency": "USD" }
}

Pagination

Lists answer newest first, as data, has_more and cursor. Send that cursor back for the next page while has_more is true. A cursor only works with the filters it came from.

limitinteger
1 to 100. 25 when left out.
cursorstring
The previous page's cursor. Leave it out for the first page.
curl "https://www.ninjabuy.com/api/v1/orders?limit=25&cursor=c_2v9kq" \
  -H "Authorization: Bearer $NINJABUY_API_KEY" \
  -H "NinjaBuy-Version: 2026-11"
Response 200
HTTP/2 200 OK

{
  "data": [
    { "id": "ord_3n7q", "status": "checked" },
    { "id": "ord_3n6z", "status": "shipped" }
  ],
  "has_more": true,
  "cursor": "c_1q4ea"
}

Rate limits

Limits are per account, shared by all its keys, OAuth tokens and its MCP connection. Every answer says where you stand in the IETF RateLimit-Policy and RateLimit headers; a refusal is 429 rate-limited with Retry-After in seconds.

WindowWhat counts
"create-links";q=30;w=60Calls that read a listing from the marketplace: creating or refreshing an import, pushing a product. 30 a minute, shared with the links API and MCP.
"default";q=120;w=60Every other call. 120 a minute.
Response 429
HTTP/2 429
Content-Type: application/problem+json
Retry-After: 12
RateLimit-Policy: "create-links";q=30;w=60
RateLimit: "create-links";r=0;t=12

{
  "type": "https://www.ninjabuy.com/api/v1/problems/rate-limited",
  "title": "Too many requests",
  "status": 429,
  "detail": "Try again in 12 seconds.",
  "request_id": "req_7Hq2Lw9c"
}

Idempotency

Send an Idempotency-Key (1 to 255 printable ASCII characters; a UUID is ideal) on any POST, and a retry after a timeout cannot place a second order. For 24 hours the same key, on the same route with the same body, answers the first answer again byte for byte, with Idempotent-Replayed: true, without running again or counting against a limit.

  • The same key with a different body or route: 422 idempotency-key-reused.
  • The first request still running: 409 idempotency-key-in-progress, with Retry-After.
  • A rate-limit refusal is not an answer: the key stays free, so retrying after Retry-After runs the request.

Orders also carry your own external_id: a second order with the same one is refused with 409 duplicate-order, whatever key it was sent with.

A retried request
POST /api/v1/orders
Idempotency-Key: 4f0c2b4e-9d1a-4e0f-b0a8-5d1d3c7e2f61

HTTP/2 201
Idempotent-Replayed: true
X-Request-Id: req_7Hq2Lw9c

Errors

Errors are problem JSON (RFC 9457): type is a stable URL to branch on, title names it, detail explains this case in words you can show, and request_id finds it in our logs. A body that fails validation adds errors, each with a JSON pointer to the field.

StatusTypeWhen
400bad-jsonThe body is not JSON.
400bad-bodyA field is missing or wrong; errors[] points at it.
400bad-idempotency-keyThe Idempotency-Key is empty, too long or not printable ASCII.
401unauthorizedThe key or token is missing, wrong, expired or revoked.
403insufficient-scopeThe key lacks the scope the route needs.
404not-foundNo such object on this account. Another account's ids answer the same.
405method-not-allowedThe route exists, not with this method; Allow lists the ones it takes.
409duplicate-orderAn order with this external_id exists; order names it.
409idempotency-key-in-progressThe first request with this key is still running.
409invalid-stateThe object cannot do that now, for example cancelling a shipped order.
415unsupported-media-typeThe body is not application/json.
422idempotency-key-reusedThe key was used for a different request.
422unprocessableValid JSON we cannot act on: an unread link, a sold-out variant. detail says which.
429rate-limitedToo many calls in the window. Wait Retry-After seconds.
500internal-errorOur fault. Retry with the same Idempotency-Key.
503service-unavailableWe could not check the key or reach a store right now. Retry after Retry-After.
Response 400
HTTP/2 400

{
  "type": "https://www.ninjabuy.com/api/v1/problems/bad-body",
  "title": "Invalid request body",
  "status": 400,
  "detail": "1 field is invalid.",
  "request_id": "req_7Hq2Lw9c",
  "errors": [
    {
      "pointer": "/ship_to/country",
      "detail": "Must be a two-letter country code."
    }
  ]
}