The Balaji Customer API gives a wholesale account programmatic access to its own data. A credential belongs to one Balaji account. Reads cover that account and its sub-accounts. You never send a customer number.
/api/v1 is additive only: Balaji adds fields and routes but never removes or renames one. A breaking change ships as /api/v2 with six months' notice, and both versions run side by side.
bbw_test_. Keep it out of your source code.curl "https://shop.balajibrands.com/api/v1/items?sku=CL-CASE-IPH16-BK" \
-H "Authorization: Bearer YOUR_TEST_KEY"{ "success": true, "data": ... }. A failure is { "success": false, "error": { "code", "message" } }. Every response carries an X-Request-Id header. Quote it when you ask for help.?dry_run=true works on any key.curl -X POST "https://shop.balajibrands.com/api/v1/orders?dry_run=true" \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"po_number": "P26101",
"ship_to": {
"address_id": 4128
},
"lines": [
{
"sku": "ZZ-TFM-IP17-BK",
"quantity": 24,
"unit_price": 4.31,
"line_ref": "1"
},
{
"upc": "812345678901",
"quantity": 12
}
],
"notes": "Deliver to back door"
}'A live key places real orders once Balaji switches live ordering on for your account.
The body is the same for every event: an id, the type, created_at, test (true for a test event), customer_id and data with the record. The record has the same fields as the matching read route.
{
"id": "evt_example",
"type": "shipment.created",
"created_at": "2026-11-06T18:00:00.000Z",
"test": false,
"customer_id": 14442724,
"data": {
"shipment": {
"shipment_number": "IF1234567",
"order_number": "SO51696890123",
"tracking": [
{
"number": "1Z999AA10123456784"
}
]
}
}
}# Recompute the signature of a saved delivery and compare it to the header.
# t is the timestamp from the header; body.json is the raw request body.
printf '%s.%s' "$T" "$(cat body.json)" | openssl dgst -sha256 -hmac "$WHSEC" -heximport { createHmac, timingSafeEqual } from "node:crypto";
// rawBody must be the exact bytes received, before any JSON parsing.
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const given = header.split(",").filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
return given.some((sig) => sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
}import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = [p.split("=", 1) for p in header.split(",")]
t = next((v for k, v in parts if k == "t"), None)
if t is None or abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(v, expected) for k, v in parts if k == "v1")Base URL https://shop.balajibrands.com. Money is whole cents in a field ending _cents, with its currency. Times are UTC. Fields are only ever added, never removed.
GET/api/v1/items
Scope: catalog:read
Look up items by sku and/or upc (comma lists, 50 identifiers together at most), or search with q. You see only the items your account can order, at your own price. Name sku/upc, or q, not both. cursor pages a q search only.
Each key reads 2,000 item rows a day (Pacific time). Rows returned count; not_found entries do not.
| Parameter | In | Description |
|---|---|---|
| sku | query | Comma list of Balaji SKUs. |
| upc | query | Comma list of barcodes, 8 to 14 digits each. |
| q | query | Search text. |
| cursor | query | The next_cursor of a previous q page. |
curl -X GET "https://shop.balajibrands.com/api/v1/items?sku=CL-CASE-IPH16-BK,CL-CASE-IPH16-BL" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/items?sku=CL-CASE-IPH16-BK,CL-CASE-IPH16-BL", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/items?sku=CL-CASE-IPH16-BK,CL-CASE-IPH16-BL",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"items": [
{
"sku": "CL-CASE-IPH16-BK",
"upc": "012345678905",
"name": "Clear Case iPhone 16",
"color": "Black",
"brand": "CLICK",
"series": "Clear Series",
"device": "iPhone 16",
"image_url": "https://www.balajiwireless.com/images/cl-case-iph16-bk.jpg",
"case_pack": 5,
"unit_price": 4.31,
"unit_price_cents": 431,
"currency": "USD",
"in_stock": true,
"quantity_available": 40,
"orderable": true,
"next_arrival_date": "2026-11-20"
}
],
"not_found": [
"CL-OLD-SKU"
],
"next_cursor": null
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/catalog
Scope: catalog:read
Every item your website account can order, in the /items row shape plus updated_at, on_order and backorderable, oldest change first. Pages hold up to 200 rows. A page can hold fewer rows than limit, or none, and still have a next_cursor: keep paging until it is null. On the last page save next_updated_since and send it as updated_since next time.
Your account can read 50,000 catalog rows a day (Pacific time), shared by all your keys. That is separate from the /items allowance.
| Parameter | In | Description |
|---|---|---|
| updated_since | query | Only rows changed after this time. ISO 8601 with a zone. We reach back 30 minutes before it, so you may see a row again. |
| cursor | query | The next_cursor of the previous page. Send the same filters with it. |
| limit | query | Rows per page, 1 to 200. Default 100. |
curl -X GET "https://shop.balajibrands.com/api/v1/catalog?updated_since=2026-11-05T08:00:00Z" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/catalog?updated_since=2026-11-05T08:00:00Z", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/catalog?updated_since=2026-11-05T08:00:00Z",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"items": [
{
"sku": "CL-CASE-IPH16-BK",
"upc": "012345678905",
"name": "Clear Case iPhone 16",
"color": "Black",
"brand": "CLICK",
"series": "Clear Series",
"device": "iPhone 16",
"image_url": "https://www.balajiwireless.com/images/cl-case-iph16-bk.jpg",
"case_pack": 5,
"unit_price": 4.31,
"unit_price_cents": 431,
"currency": "USD",
"in_stock": true,
"quantity_available": 40,
"orderable": true,
"next_arrival_date": "2026-11-20",
"updated_at": "2026-11-05T00:00:00.000000Z",
"on_order": 600,
"backorderable": false
}
],
"next_cursor": null,
"next_updated_since": "2026-11-05T00:00:00.000000Z"
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/orders
Scope: orders:read
Sales orders from every channel (website, phone, your rep, EDI and this API) on your account and its sub-accounts, oldest change first. Changes reach this list up to 15 minutes after they happen.
| Parameter | In | Description |
|---|---|---|
| updated_since | query | Only rows changed after this time. ISO 8601 with a zone. We reach back 30 minutes before it, so you may see a row again. |
| cursor | query | The next_cursor of the previous page. Send the same filters with it. |
| limit | query | Rows per page, 1 to 200. Default 100. |
| status | query | Comma list of order statuses. |
| po_number | query | Only orders with this PO number. 1 to 45 printable characters. |
curl -X GET "https://shop.balajibrands.com/api/v1/orders?updated_since=2026-11-05T08:00:00Z&status=pending_fulfillment,partially_fulfilled" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/orders?updated_since=2026-11-05T08:00:00Z&status=pending_fulfillment,partially_fulfilled", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/orders?updated_since=2026-11-05T08:00:00Z&status=pending_fulfillment,partially_fulfilled",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"orders": [
{
"order_number": "SO51696890123",
"po_number": "P26101",
"customer_id": 14442724,
"status": "Pending Fulfillment",
"order_date": "2026-09-25",
"total_cents": 12184,
"currency": "USD",
"ship_method": "UPS Ground",
"updated_at": "2026-09-25T17:02:11.123456Z"
}
],
"next_cursor": null,
"next_updated_since": "2026-09-25T17:02:11.123456Z"
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
POST/api/v1/orders
Scope: orders:write
Send one purchase order. It becomes one Balaji sales order on your account terms, placed through the same checkout the website uses, so every check a website order passes applies.
"result": "validated"). Nothing is placed."result": "dry_run"). Dry-run a full day's batch before your first live push.202 with "result": "pending_release" and release_after. The order waits 30 minutes, then goes to the warehouse queue. Cancel it before then with DELETE /api/v1/orders/{po_number}. Live keys answer 503 LIVE_ORDERS_PAUSED until Balaji switches live ordering on for your account.The call does not wait for checkout. Read the outcome with GET /api/v1/orders/{po_number}: it shows pending_release, then the order once it is placed, or failed with an error_code if checkout refused it (for example CREDIT_LIMIT_EXCEEDED or OUT_OF_STOCK).
One PO number places one order. Send the same po_number again and nothing new is saved: you get the existing order back, with "result": "already_placed" once it is placed. That makes a retry after a timeout safe.
If any line has a problem, nothing is accepted and the error lists every bad line.
Holds. An order that looks wrong is accepted and held, not refused: it answers 202 with "result": "held" and a holds list, and nothing reaches checkout until a person releases it. A hold names the line and the number it was compared with. Holds: LINE_QUANTITY_ABOVE_HISTORY (a line over 10 times your largest line in the last 90 days), ORDER_TOTAL_ABOVE_HISTORY (a subtotal over 5 times your largest order in the last 90 days) and CASE_PACK_ROUNDED (a quantity that is not a whole number of case packs; checkout would round it up, so it is held instead). An item or customer with no 90-day history is not judged. A test key and a dry run show the holds too.
Daily ceilings. Each key has a ceiling of orders, units and dollars a day, counted in Pacific time. An order that would pass one answers 429 DAILY_LIMIT_REACHED and nothing is accepted. A dry run checks the ceiling and reserves nothing.
| Parameter | In | Description |
|---|---|---|
| dry_run | query | true checks the order without placing or saving it. |
curl -X POST "https://shop.balajibrands.com/api/v1/orders" \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"po_number": "P26101",
"ship_to": {
"address_id": 4128
},
"lines": [
{
"sku": "ZZ-TFM-IP17-BK",
"quantity": 24,
"unit_price": 4.31,
"line_ref": "1"
},
{
"upc": "812345678901",
"quantity": 12
}
],
"notes": "Deliver to back door"
}'const res = await fetch("https://shop.balajibrands.com/api/v1/orders", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"po_number": "P26101",
"ship_to": {
"address_id": 4128
},
"lines": [
{
"sku": "ZZ-TFM-IP17-BK",
"quantity": 24,
"unit_price": 4.31,
"line_ref": "1"
},
{
"upc": "812345678901",
"quantity": 12
}
],
"notes": "Deliver to back door"
}),
});
const { success, data, error } = await res.json();import requests
res = requests.post(
"https://shop.balajibrands.com/api/v1/orders",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
json={
"po_number": "P26101",
"ship_to": {
"address_id": 4128
},
"lines": [
{
"sku": "ZZ-TFM-IP17-BK",
"quantity": 24,
"unit_price": 4.31,
"line_ref": "1"
},
{
"upc": "812345678901",
"quantity": 12
}
],
"notes": "Deliver to back door"
},
)
body = res.json()Request body
{
"po_number": "P26101",
"ship_to": {
"address_id": 4128
},
"lines": [
{
"sku": "ZZ-TFM-IP17-BK",
"quantity": 24,
"unit_price": 4.31,
"line_ref": "1"
},
{
"upc": "812345678901",
"quantity": 12
}
],
"notes": "Deliver to back door"
}Response 200
{
"success": true,
"data": {
"mode": "test",
"result": "validated",
"placed": false,
"po_number": "P26101",
"ship_to": {
"address_id": 4128,
"label": "Store 17",
"addressee": "Zadikuzz Group",
"addr1": "100 Calle Principal",
"addr2": null,
"city": "San Juan",
"state": "PR",
"zip": "00901",
"country": "US",
"store_id": null,
"default_shipping": false
},
"store_id": null,
"shipping": {
"method": "UPS Ground",
"estimated_rate": 18.4
},
"lines": [
{
"line": 0,
"line_ref": "1",
"sku": "ZZ-TFM-IP17-BK",
"requested_quantity": 24,
"quantity": 24,
"case_pack": 4,
"unit_price": 4.31,
"po_unit_price": 4.31,
"price_flag": false
}
],
"subtotal": 103.44,
"subtotal_cents": 10344,
"currency": "USD",
"price_flags": 0,
"holds": []
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 422 The request is well formed but cannot be accepted. `INVALID_LINES` lists every bad line in `problems`. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/orders/{po_number}
Scope: orders:read
Status, total and shipments of one order on your account or a sub-account, found by your own PO number. An order this API accepted shows here while it is held or waiting out its release window, before it is a sales order. A new shipment can take up to 15 minutes to appear. Check no more than once every 15 minutes per order; use GET /api/v1/orders to follow many orders.
| Parameter | In | Description |
|---|---|---|
| po_number * | path |
curl -X GET "https://shop.balajibrands.com/api/v1/orders/P26101" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/orders/P26101", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/orders/P26101",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"po_number": "P26101",
"order_number": "SO51696890123",
"status": "Pending Fulfillment",
"total": 121.84,
"total_cents": 12184,
"currency": "USD",
"order_date": "9/25/2026",
"shipments": [
{
"shipment_number": "IF1234567",
"ship_date": "2026-09-26",
"tracking": [
"1Z999AA10123456784"
]
}
]
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 404 No such record on your account. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
DELETE/api/v1/orders/{po_number}
Scope: orders:write
Cancels an order this API accepted while it is still held or waiting out its 30-minute release window. After that it has gone to the warehouse queue and cancellation goes through customer service (CANCEL_WINDOW_CLOSED). Cancelling twice answers cancelled again. An order checkout refused was never placed (ORDER_NOT_PLACED). It gives back the order's daily ceiling slot.
| Parameter | In | Description |
|---|---|---|
| po_number * | path |
curl -X DELETE "https://shop.balajibrands.com/api/v1/orders/P26101" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/orders/P26101", {
method: "DELETE",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.delete(
"https://shop.balajibrands.com/api/v1/orders/P26101",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"po_number": "P26101",
"result": "cancelled",
"status": "cancelled"
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 404 No such record on your account. · 409 The order cannot be cancelled. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/orders/by-number/{order_number}
Scope: orders:read
One order on your account or a sub-account by its Balaji order number, with its shipments. Use it for orders placed without a PO number.
| Parameter | In | Description |
|---|---|---|
| order_number * | path |
curl -X GET "https://shop.balajibrands.com/api/v1/orders/by-number/SO51696890123" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/orders/by-number/SO51696890123", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/orders/by-number/SO51696890123",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"order_number": "SO51696890123",
"po_number": "P26101",
"customer_id": 14442724,
"status": "Pending Fulfillment",
"order_date": "2026-09-25",
"total_cents": 12184,
"currency": "USD",
"ship_method": "UPS Ground",
"updated_at": "2026-09-25T17:02:11.123456Z",
"shipments": [
{
"shipment_number": "IF1234567",
"order_number": "SO51696890123",
"po_number": "P26101",
"customer_id": 14442724,
"status": "Shipped",
"ship_date": "2026-09-26",
"ship_method": "UPS Ground",
"tracking": [
{
"number": "1Z999AA10123456784",
"carrier": "ups",
"url": "https://www.ups.com/track?tracknum=1Z999AA10123456784"
}
],
"carrier_status": "In Transit",
"updated_at": "2026-09-26T21:40:03.000000Z"
}
]
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 404 No such record on your account. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/shipments
Scope: shipments:read
Fulfillments on your account and its sub-accounts with order number, PO, ship method, tracking numbers and links, and the last UPS status we recorded. carrier and url are null when the carrier cannot be told from the number. Changes reach this list up to 15 minutes after they happen.
| Parameter | In | Description |
|---|---|---|
| updated_since | query | Only rows changed after this time. ISO 8601 with a zone. We reach back 30 minutes before it, so you may see a row again. |
| cursor | query | The next_cursor of the previous page. Send the same filters with it. |
| limit | query | Rows per page, 1 to 200. Default 100. |
curl -X GET "https://shop.balajibrands.com/api/v1/shipments?updated_since=2026-11-05T08:00:00Z" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/shipments?updated_since=2026-11-05T08:00:00Z", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/shipments?updated_since=2026-11-05T08:00:00Z",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"shipments": [
{
"shipment_number": "IF1234567",
"order_number": "SO51696890123",
"po_number": "P26101",
"customer_id": 14442724,
"status": "Shipped",
"ship_date": "2026-09-26",
"ship_method": "UPS Ground",
"tracking": [
{
"number": "1Z999AA10123456784",
"carrier": "ups",
"url": "https://www.ups.com/track?tracknum=1Z999AA10123456784"
}
],
"carrier_status": "In Transit",
"updated_at": "2026-09-26T21:40:03.000000Z"
}
],
"next_cursor": null,
"next_updated_since": "2026-09-26T21:40:03.000000Z"
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/invoices
Scope: finance:read
Invoices on your account and its sub-accounts, oldest change first. open=true keeps invoices with an amount due. Each row links its PDF at pdf_url.
| Parameter | In | Description |
|---|---|---|
| updated_since | query | Only rows changed after this time. ISO 8601 with a zone. We reach back 30 minutes before it, so you may see a row again. |
| cursor | query | The next_cursor of the previous page. Send the same filters with it. |
| limit | query | Rows per page, 1 to 200. Default 100. |
| open | query | true keeps invoices with an amount due. |
curl -X GET "https://shop.balajibrands.com/api/v1/invoices?updated_since=2026-11-05T08:00:00Z" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/invoices?updated_since=2026-11-05T08:00:00Z", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/invoices?updated_since=2026-11-05T08:00:00Z",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"invoices": [
{
"id": 8800123,
"invoice_number": "INV90412",
"customer_id": 14442724,
"status": "Open",
"issued_on": "2026-09-26",
"due_on": "2026-10-26",
"overdue": false,
"terms": "Net 30",
"po_number": "P26101",
"total_cents": 12184,
"amount_due_cents": 12184,
"currency": "USD",
"pdf_url": "/api/v1/invoices/8800123/pdf",
"updated_at": "2026-09-26T22:10:00.000000Z"
}
],
"next_cursor": null,
"next_updated_since": "2026-09-26T22:10:00.000000Z"
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/invoices/{invoice_id}
Scope: finance:read
One invoice with its lines. Another account's invoice and a missing one both answer 404.
| Parameter | In | Description |
|---|---|---|
| invoice_id * | path |
curl -X GET "https://shop.balajibrands.com/api/v1/invoices/8800123" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/invoices/8800123", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/invoices/8800123",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"id": 8800123,
"invoice_number": "INV90412",
"customer_id": 14442724,
"status": "Open",
"issued_on": "2026-09-26",
"due_on": "2026-10-26",
"overdue": false,
"terms": "Net 30",
"po_number": "P26101",
"total_cents": 12184,
"amount_due_cents": 12184,
"currency": "USD",
"pdf_url": "/api/v1/invoices/8800123/pdf",
"updated_at": "2026-09-26T22:10:00.000000Z",
"lines": [
{
"item_id": 51234,
"sku": "ZZ-TFM-IP17-BK",
"name": "Tempered Glass iPhone 17 Black",
"item_type": "InvtPart",
"quantity": 24,
"rate_cents": 431,
"amount_cents": 10344,
"currency": "USD"
}
]
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 404 No such record on your account. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/invoices/{invoice_id}/pdf
Scope: finance:read
The invoice as the branded Balaji PDF. Another account's invoice and a missing one both answer 404.
| Parameter | In | Description |
|---|---|---|
| invoice_id * | path |
curl -X GET "https://shop.balajibrands.com/api/v1/invoices/8800123/pdf" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/invoices/8800123/pdf", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/invoices/8800123/pdf",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 404 No such record on your account. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/credit-memos
Scope: finance:read
Credit memos on your account and its sub-accounts, with the invoices each was applied to and what is still unused.
| Parameter | In | Description |
|---|---|---|
| updated_since | query | Only rows changed after this time. ISO 8601 with a zone. We reach back 30 minutes before it, so you may see a row again. |
| cursor | query | The next_cursor of the previous page. Send the same filters with it. |
| limit | query | Rows per page, 1 to 200. Default 100. |
curl -X GET "https://shop.balajibrands.com/api/v1/credit-memos?updated_since=2026-11-05T08:00:00Z" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/credit-memos?updated_since=2026-11-05T08:00:00Z", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/credit-memos?updated_since=2026-11-05T08:00:00Z",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"credit_memos": [
{
"id": 8800456,
"number": "CM2201",
"customer_id": 14442724,
"status": "Open",
"issued_on": "2026-10-02",
"total_cents": 2500,
"amount_remaining_cents": 2500,
"currency": "USD",
"applied_to": [],
"updated_at": "2026-10-02T18:00:00.000000Z"
}
],
"next_cursor": null,
"next_updated_since": "2026-10-02T18:00:00.000000Z"
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/payments
Scope: finance:read
Customer payments on your account and its sub-accounts, with the invoices each was applied to and any amount not yet applied.
| Parameter | In | Description |
|---|---|---|
| updated_since | query | Only rows changed after this time. ISO 8601 with a zone. We reach back 30 minutes before it, so you may see a row again. |
| cursor | query | The next_cursor of the previous page. Send the same filters with it. |
| limit | query | Rows per page, 1 to 200. Default 100. |
curl -X GET "https://shop.balajibrands.com/api/v1/payments?updated_since=2026-11-05T08:00:00Z" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/payments?updated_since=2026-11-05T08:00:00Z", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/payments?updated_since=2026-11-05T08:00:00Z",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"payments": [
{
"id": 8800789,
"number": "PYMT3301",
"customer_id": 14442724,
"status": "Deposited",
"issued_on": "2026-10-20",
"total_cents": 12184,
"amount_remaining_cents": 0,
"currency": "USD",
"applied_to": [
{
"id": 8800123,
"invoice_number": "INV90412"
}
],
"updated_at": "2026-10-20T16:30:00.000000Z"
}
],
"next_cursor": null,
"next_updated_since": "2026-10-20T16:30:00.000000Z"
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/returns
Scope: finance:read
Return authorizations (RMAs) on your account and its sub-accounts, with the order each came from and the credit memo it produced, when there is one.
| Parameter | In | Description |
|---|---|---|
| updated_since | query | Only rows changed after this time. ISO 8601 with a zone. We reach back 30 minutes before it, so you may see a row again. |
| cursor | query | The next_cursor of the previous page. Send the same filters with it. |
| limit | query | Rows per page, 1 to 200. Default 100. |
curl -X GET "https://shop.balajibrands.com/api/v1/returns?updated_since=2026-11-05T08:00:00Z" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/returns?updated_since=2026-11-05T08:00:00Z", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/returns?updated_since=2026-11-05T08:00:00Z",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"returns": [
{
"id": 8800321,
"number": "RMA1105",
"customer_id": 14442724,
"status": "Refunded",
"requested_on": "2026-10-01",
"total_cents": 2500,
"currency": "USD",
"source_order": {
"id": 7700123,
"order_number": "SO51696890123"
},
"credit_memo": {
"id": 8800456,
"number": "CM2201",
"total_cents": 2500,
"currency": "USD"
},
"updated_at": "2026-10-02T18:00:00.000000Z"
}
],
"next_cursor": null,
"next_updated_since": "2026-10-02T18:00:00.000000Z"
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/statement
Scope: finance:read
Your open balance, aging buckets and open items: open invoices, then unused credit memos and payments as negative amounts, so the open items sum to the balance. It is the current state only. There is no as_of parameter; the as_of field reports how fresh the data is.
curl -X GET "https://shop.balajibrands.com/api/v1/statement" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/statement", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/statement",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"as_of": "2026-11-05T17:45:00.000Z",
"balance_cents": 9684,
"currency": "USD",
"aging": {
"current": {
"count": 1,
"amount_cents": 12184
},
"days_1_30": {
"count": 0,
"amount_cents": 0
},
"days_31_60": {
"count": 0,
"amount_cents": 0
},
"days_61_90": {
"count": 0,
"amount_cents": 0
},
"days_91_plus": {
"count": 0,
"amount_cents": 0
}
},
"open_items": [
{
"kind": "invoice",
"id": 8800123,
"number": "INV90412",
"issued_on": "2026-09-26",
"due_on": "2026-10-26",
"days_past_due": null,
"amount_cents": 12184,
"currency": "USD"
},
{
"kind": "credit_memo",
"id": 8800456,
"number": "CM2201",
"issued_on": "2026-10-02",
"due_on": null,
"days_past_due": null,
"amount_cents": -2500,
"currency": "USD"
}
]
}
}Errors: 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/ship-tos
Scope: orders:read
Your own ship-to addresses. Map your store codes to address_id once with PUT /api/v1/ship-to-map.
curl -X GET "https://shop.balajibrands.com/api/v1/ship-tos" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/ship-tos", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/ship-tos",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"ship_tos": [
{
"address_id": 4128,
"label": "Store 17",
"addressee": "Zadikuzz Group",
"addr1": "100 Calle Principal",
"addr2": null,
"city": "San Juan",
"state": "PR",
"zip": "00901",
"country": "US",
"store_id": null,
"default_shipping": false
}
]
}
}Errors: 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/ship-to-map
Scope: orders:read
Your store codes and the ship-to address each maps to.
curl -X GET "https://shop.balajibrands.com/api/v1/ship-to-map" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/ship-to-map", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/ship-to-map",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"stores": [
{
"store_id": "S17",
"address_id": 4128,
"updated_at": "2026-11-05T18:00:00.000Z"
}
]
}
}Errors: 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
PUT/api/v1/ship-to-map
Scope: orders:write
Replaces your whole store map. Store codes are not case sensitive. If any address_id is not one of your addresses, nothing is saved. Once you have a map, every POST /api/v1/orders must name ship_to.store_id and ships to the address that store maps to.
curl -X PUT "https://shop.balajibrands.com/api/v1/ship-to-map" \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"stores": [
{
"store_id": "S17",
"address_id": 4128
},
{
"store_id": "S18",
"address_id": 4129
}
]
}'const res = await fetch("https://shop.balajibrands.com/api/v1/ship-to-map", {
method: "PUT",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"stores": [
{
"store_id": "S17",
"address_id": 4128
},
{
"store_id": "S18",
"address_id": 4129
}
]
}),
});
const { success, data, error } = await res.json();import requests
res = requests.put(
"https://shop.balajibrands.com/api/v1/ship-to-map",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
json={
"stores": [
{
"store_id": "S17",
"address_id": 4128
},
{
"store_id": "S18",
"address_id": 4129
}
]
},
)
body = res.json()Request body
{
"stores": [
{
"store_id": "S17",
"address_id": 4128
},
{
"store_id": "S18",
"address_id": 4129
}
]
}Response 200
{
"success": true,
"data": {
"stores": [
{
"store_id": "S17",
"address_id": 4128,
"updated_at": "2026-11-05T18:00:00.000Z"
},
{
"store_id": "S18",
"address_id": 4129,
"updated_at": "2026-11-05T18:00:00.000Z"
}
]
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 422 The request is well formed but cannot be accepted. `INVALID_LINES` lists every bad line in `problems`. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/webhooks
Scope: webhooks:manage
Your account's webhook endpoints, newest first. Sending is switched off until Balaji turns it on for your account; registration works before that.
curl -X GET "https://shop.balajibrands.com/api/v1/webhooks" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/webhooks", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/webhooks",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"endpoints": [
{
"id": "cwe_8f2c1a9b0d3e4f56",
"url": "https://erp.example.com/balaji/hooks",
"description": "ERP order sync",
"events": [
"order.created",
"order.updated",
"shipment.created"
],
"mode": "test",
"secret_prefix": "whsec_3kq9",
"status": "active",
"disabled_reason": null,
"disabled_at": null,
"failing_since": null,
"last_success_at": "2026-11-06T09:12:44.000Z",
"created_at": "2026-11-05T18:00:00.000Z"
}
]
}
}Errors: 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
POST/api/v1/webhooks
Scope: webhooks:manage
Registers an https endpoint for the events you list. The answer carries the signing secret (whsec_...) once; store it, because it is never shown again. A test key makes a test endpoint and a live key a live one. Up to 10 endpoints per account: a further one answers 409 TOO_MANY_ENDPOINTS, and a URL that is not https or not public answers 422 INVALID_URL.
Each delivery is a POST with the header Balaji-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of <t>.<raw body> with your secret. Reject a signature older than 5 minutes.
curl -X POST "https://shop.balajibrands.com/api/v1/webhooks" \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.example.com/balaji/hooks",
"events": [
"order.created",
"order.updated",
"shipment.created"
],
"description": "ERP order sync"
}'const res = await fetch("https://shop.balajibrands.com/api/v1/webhooks", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://erp.example.com/balaji/hooks",
"events": [
"order.created",
"order.updated",
"shipment.created"
],
"description": "ERP order sync"
}),
});
const { success, data, error } = await res.json();import requests
res = requests.post(
"https://shop.balajibrands.com/api/v1/webhooks",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
json={
"url": "https://erp.example.com/balaji/hooks",
"events": [
"order.created",
"order.updated",
"shipment.created"
],
"description": "ERP order sync"
},
)
body = res.json()Request body
{
"url": "https://erp.example.com/balaji/hooks",
"events": [
"order.created",
"order.updated",
"shipment.created"
],
"description": "ERP order sync"
}Response 201
{
"success": true,
"data": {
"endpoint": {
"id": "cwe_8f2c1a9b0d3e4f56",
"url": "https://erp.example.com/balaji/hooks",
"description": "ERP order sync",
"events": [
"order.created",
"order.updated",
"shipment.created"
],
"mode": "test",
"secret_prefix": "whsec_3kq9",
"status": "active",
"disabled_reason": null,
"disabled_at": null,
"failing_since": null,
"last_success_at": null,
"created_at": "2026-11-05T18:00:00.000Z"
},
"secret": "whsec_3kq9EXAMPLEONLYnotarealsecret0000000000"
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 409 The order cannot be cancelled. · 422 The request is well formed but cannot be accepted. `INVALID_LINES` lists every bad line in `problems`. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/webhooks/{endpoint_id}
Scope: webhooks:manage
One endpoint. Another account's endpoint and a missing one both answer 404.
| Parameter | In | Description |
|---|---|---|
| endpoint_id * | path |
curl -X GET "https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"endpoint": {
"id": "cwe_8f2c1a9b0d3e4f56",
"url": "https://erp.example.com/balaji/hooks",
"description": "ERP order sync",
"events": [
"order.created"
],
"mode": "live",
"secret_prefix": "whsec_3kq9",
"status": "disabled",
"disabled_reason": "Deliveries failed for 24 hours.",
"disabled_at": "2026-11-06T09:00:00.000Z",
"failing_since": "2026-11-05T09:00:00.000Z",
"last_success_at": "2026-11-05T08:58:10.000Z",
"created_at": "2026-11-01T18:00:00.000Z"
}
}
}Errors: 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 404 No such record on your account. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
PATCH/api/v1/webhooks/{endpoint_id}
Scope: webhooks:manage
Send any of url, events, description, or enabled true to turn a disabled endpoint back on. A bad URL answers 422 INVALID_URL.
| Parameter | In | Description |
|---|---|---|
| endpoint_id * | path |
curl -X PATCH "https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56" \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"enabled": true
}'const res = await fetch("https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56", {
method: "PATCH",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"enabled": true
}),
});
const { success, data, error } = await res.json();import requests
res = requests.patch(
"https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
json={
"enabled": True
},
)
body = res.json()Request body
{
"enabled": true
}Response 200
{
"success": true,
"data": {
"endpoint": {
"id": "cwe_8f2c1a9b0d3e4f56",
"url": "https://erp.example.com/balaji/hooks",
"description": "ERP order sync",
"events": [
"order.created"
],
"mode": "live",
"secret_prefix": "whsec_3kq9",
"status": "active",
"disabled_reason": null,
"disabled_at": null,
"failing_since": null,
"last_success_at": "2026-11-05T08:58:10.000Z",
"created_at": "2026-11-01T18:00:00.000Z"
}
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 404 No such record on your account. · 422 The request is well formed but cannot be accepted. `INVALID_LINES` lists every bad line in `problems`. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
DELETE/api/v1/webhooks/{endpoint_id}
Scope: webhooks:manage
Removes the endpoint and drops its unsent events.
| Parameter | In | Description |
|---|---|---|
| endpoint_id * | path |
curl -X DELETE "https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56", {
method: "DELETE",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.delete(
"https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"deleted": true
}
}Errors: 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 404 No such record on your account. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
POST/api/v1/webhooks/{endpoint_id}/test
Scope: webhooks:manage
Sends one sample event of the type you name, marked test true, once, and answers with what your endpoint returned. A failed test never disables the endpoint. 503 WEBHOOKS_PAUSED while sending is switched off; 409 ENDPOINT_DISABLED for a disabled endpoint.
| Parameter | In | Description |
|---|---|---|
| endpoint_id * | path |
curl -X POST "https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56/test" \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "order.created"
}'const res = await fetch("https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56/test", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"type": "order.created"
}),
});
const { success, data, error } = await res.json();import requests
res = requests.post(
"https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56/test",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
json={
"type": "order.created"
},
)
body = res.json()Request body
{
"type": "order.created"
}Response 200
{
"success": true,
"data": {
"event_id": "evt_5b1d2c3e4f5a6b7c",
"delivered": true,
"status_code": 204,
"error": null
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 404 No such record on your account. · 409 The order cannot be cancelled. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
GET/api/v1/webhooks/{endpoint_id}/deliveries
Scope: webhooks:manage
Recent attempts for one endpoint, newest first.
| Parameter | In | Description |
|---|---|---|
| endpoint_id * | path | |
| limit | query | Attempts to return, 1 to 200. Default 50. |
curl -X GET "https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56/deliveries" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56/deliveries", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.get(
"https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56/deliveries",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"deliveries": [
{
"id": 9120,
"event_id": "evt_5b1d2c3e4f5a6b7c",
"event_type": "order.created",
"test": true,
"attempt": 1,
"status_code": 204,
"ok": true,
"error": null,
"duration_ms": 182,
"created_at": "2026-11-06T09:12:44.000Z"
}
]
}
}Errors: 400 The request is wrong. Read `message`. · 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 404 No such record on your account. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
POST/api/v1/webhooks/{endpoint_id}/rotate-secret
Scope: webhooks:manage
Mints a new signing secret and shows it once. The old secret stops working at once.
| Parameter | In | Description |
|---|---|---|
| endpoint_id * | path |
curl -X POST "https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56/rotate-secret" \
-H "Authorization: Bearer YOUR_TEST_KEY"const res = await fetch("https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56/rotate-secret", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_TEST_KEY",
},
});
const { success, data, error } = await res.json();import requests
res = requests.post(
"https://shop.balajibrands.com/api/v1/webhooks/cwe_8f2c1a9b0d3e4f56/rotate-secret",
headers={"Authorization": "Bearer YOUR_TEST_KEY"},
)
body = res.json()Response 200
{
"success": true,
"data": {
"endpoint": {
"id": "cwe_8f2c1a9b0d3e4f56",
"url": "https://erp.example.com/balaji/hooks",
"description": null,
"events": [
"order.created"
],
"mode": "test",
"secret_prefix": "whsec_7mz2",
"status": "active",
"disabled_reason": null,
"disabled_at": null,
"failing_since": null,
"last_success_at": null,
"created_at": "2026-11-05T18:00:00.000Z"
},
"secret": "whsec_7mz2EXAMPLEONLYnotarealsecret0000000000"
}
}Errors: 401 The key is missing, wrong, expired or revoked. · 403 The key lacks the scope, or the account cannot do this now. · 404 No such record on your account. · 429 Too many requests, the daily item allowance is used, or an order would pass a daily ceiling. `Retry-After` gives the seconds to wait on `RATE_LIMITED` and `DAILY_LIMIT_REACHED`. · 503 Temporarily unavailable. Retry the same request.
POST/api/v1/oauth/token
Partner client credentials, not a customer key.
For partner platforms only. Authenticate the client with its id and secret (HTTP Basic, or client_id and client_secret form fields). Grants: authorization_code and refresh_token. A code or refresh token presented twice revokes its connection. See the partner guide in the docs.
Response 200
{
"access_token": "bbw_pat_example",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "bbw_prt_example",
"scope": "order_api"
}Errors: 400 The request or grant is wrong. · 401 The client id or secret is wrong. · 403 The shop's account no longer has online access. · 503 Temporarily unavailable.
POST/api/v1/oauth/revoke
Partner client credentials, not a customer key.
For partner platforms only. RFC 7009 revocation. Either token of a connection revokes the whole connection. It answers 200 for an unknown token, so the call cannot probe for valid tokens.
Errors: 400 The request is wrong. · 401 The client id or secret is wrong. · 503 Temporarily unavailable.
Every error has the same shape and a stable code. Handle the code, not the message text.
{
"success": false,
"error": {
"code": "INVALID_LINES",
"message": "1 line(s) cannot be ordered.",
"problems": [
{
"line": 1,
"line_ref": "2",
"code": "UNKNOWN_ITEM",
"message": "UPC 812345678901 is not on an active item."
}
]
}
}| HTTP | Code | Fix |
|---|---|---|
| 400 | VALIDATION_ERROR | The request body or query is wrong. Read message; details lists the fields. |
| status | MAX_QTY_EXCEEDED | A line is over the maximum quantity set for your account. Lower it or ask your rep. |
| status | PRICING_INCOMPLETE | An item has no price on your account. Remove the line or ask your rep. |
| 401 | UNAUTHORIZED | The key is missing, wrong, expired or revoked. Send Authorization: Bearer <key>, or create a new key. |
| status | CREDIT_LIMIT_EXCEEDED | Checkout refused the order: it is over your available credit. It shows as error_code on the order status. Pay down the balance or ask your rep. |
| 403 | INSUFFICIENT_SCOPE | The key lacks the scope in required_scope. Create a key with that scope. |
| 403 | ACCOUNT_INACTIVE | The account no longer has online access, so nothing can be read. Contact your rep. |
| 403 | ACCOUNT_NOT_ORDERABLE | The account cannot place orders now. Contact your rep. |
| 403 | PENDING_APPROVAL | The account is under review. Contact your rep. |
| status | CUSTOMER_INACTIVE | The account is inactive and cannot order. Contact your rep. |
| status | CUSTOMER_ON_HOLD | The account is on hold and cannot order. Reads still work. Contact your rep. |
| 409 | ORDER_NOT_PLACED | Checkout refused this order, so it was never placed and there is nothing to cancel. error_code says why. |
| 409 | CANCEL_WINDOW_CLOSED | The order has gone to the warehouse queue. Contact Balaji customer service to cancel it. |
| 404 | NOT_FOUND | No such record on your account. Another account's record gives the same answer. |
| status | OUT_OF_STOCK | An item has no stock for this order. Remove the line or lower the quantity. |
| status | INSUFFICIENT_STOCK | An item has less stock than ordered. Lower the quantity. |
| status | ITEM_UNAVAILABLE | An item is no longer sold. Remove the line. |
| 422 | UNKNOWN_SHIP_TO | The address_id is not one of your addresses. GET /api/v1/ship-tos lists them. A store map with such an address is not saved. |
| 422 | INVALID_URL | The endpoint URL must be https on the default port, with a public host name and no credentials. IP addresses, localhost and internal names are refused. |
| 409 | TOO_MANY_ENDPOINTS | An account can have at most 10 webhook endpoints. Delete one first. |
| 409 | ENDPOINT_DISABLED | The endpoint was disabled after failed deliveries. Re-enable it with PATCH and { "enabled": true }. |
| 503 | WEBHOOKS_PAUSED | Webhook sending is switched off for now, so no test event was sent. Retry later. |
| 422 | UNKNOWN_STORE | The store_id is not in your store map. Add it with PUT /api/v1/ship-to-map. |
| 422 | SHIP_TO_STORE_MISMATCH | The store maps to a different address than the one you sent. Nothing was placed. Check your store map. |
| 422 | STORE_ID_REQUIRED | Your account has a store map, so send ship_to.store_id on every order. |
| 422 | DUPLICATE_STORE | A store_id appears twice in your store map request. Send each store once. |
| 422 | INVALID_LINES | One or more lines cannot be ordered. problems lists every bad line with its own code. |
| 429 | RATE_LIMITED | Over 120 requests a minute or 50,000 a day for this key. Wait the Retry-After seconds, then retry. |
| 429 | DAILY_LIMIT_REACHED | The order would pass today's order, unit or dollar ceiling for this key. Nothing was placed. The ceiling resets at midnight Pacific time; your account admin can change it. |
| 429 | DAILY_CATALOG_LIMIT | The daily item allowance is used. It opens again after midnight Pacific time. |
| 503 | NOT_CONFIGURED | Ordering is not available right now. Retry later. |
| 503 | LIVE_ORDERS_PAUSED | Live ordering is not switched on for your account. Use your test key, or dry_run=true. |
| 503 | SEARCH_UNAVAILABLE | The item check is down for a moment. Retry after the Retry-After seconds. |
| 503 | WEBHOOKS_UNAVAILABLE | Webhook signing is not set up yet, so no endpoint can be added or given a new secret. Retry later or contact your rep. |
| 503 | UPSTREAM_UNAVAILABLE | Our system did not answer. Retry the same request. A retried order cannot be placed twice. |
| status | ORDER_REJECTED | Checkout refused the order for a reason without its own code. It shows as error_code on the order status. |
An INVALID_LINES answer lists every bad line in problems. Each has the zero-based line, your line_ref, and one of these codes.
| UNKNOWN_ITEM | The SKU or UPC is not an active item you can order. |
| AMBIGUOUS_UPC | The barcode sits on more than one item. Send the SKU. |
| DUPLICATE_ITEM | The same item appears on two lines. Combine them. |
| NO_PRICE | The item has no price on your account. Ask your rep. |
Checkout refuses an order after you have sent it, not on the POST. The refusal shows as error_code on the order status (CREDIT_LIMIT_EXCEEDED, MAX_QTY_EXCEEDED, PRICING_INCOMPLETE, CUSTOMER_INACTIVE, CUSTOMER_ON_HOLD, OUT_OF_STOCK, INSUFFICIENT_STOCK, ITEM_UNAVAILABLE). A refusal without a code of its own shows as ORDER_REJECTED.
/api/v1 is additive only. Balaji adds fields and routes but never removes or renames one, so ignore fields you do not know.
A breaking change ships as /api/v2. You get six months' notice by email and in the changelog, and both versions run side by side.