Developers
Shop API
for your website
Your website and your shop, finally in sync. Products, stock and orders flow both ways, and your site hears about every change within seconds.
Base URL
https://bluemint.app/api/v1Watch first
Get a key
In your dashboard: Settings → Website API. Start with a test key.
Send products and orders
Import your catalogue once, then send each order as it's placed.
Listen for webhooks
We tell your site within seconds when orders change or stock moves.
Before you start
Every request is JSON over HTTPS, with your secret key in the header.
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxsk_test_…- Test key. Works on a separate sandbox shop, so nothing real changes. Build with this first.
sk_live_…- Live key. Changes your real products, stock and orders.
- Keep it secret
- Keys are shown once. Keep them on your server, never in browser code.
- Read-only keys
- Can only read. A write returns
403 insufficient_scope. - Speed limit
- About 120 requests a minute per key. Over it you get
429with aRetry-Afterheader in seconds. - Money
- We send
"1250.00"(a string, in taka). You send plain numbers like1250or1250.50. - Time
- All timestamps are ISO 8601, in UTC.
Products
Send your catalogue once, then send only what changes.
We match every product by your own id, the external_id. Send the same product again and it's updated, never duplicated.
PUT/products
Up to 100 products per call. Variants (sizes, colours) are optional; each needs its own external_id and sku. Image URLs are downloaded in the background.
curl -X PUT https://bluemint.app/api/v1/products \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{
"products": [{
"external_id": "wp-1042",
"sku": "TEE-BLK",
"name": "Black tee",
"description": "Cotton, regular fit",
"price": 650,
"images": ["https://example.com/tee.jpg"],
"category": "T-shirts",
"weight_grams": 220,
"barcode": "8901234567890",
"active": true,
"variants": [
{ "external_id": "wp-1042-m", "sku": "TEE-BLK-M", "attributes": { "size": "M" } },
{ "external_id": "wp-1042-xl", "sku": "TEE-BLK-XL", "attributes": { "size": "XL" }, "price": 700 }
]
}]
}'{ "data": [
{ "external_id": "wp-1042", "id": "cmk1…", "status": "created" },
{ "external_id": "bad-one", "status": "error", "error": "sku TEE-RED belongs to a product managed by another sync." }
] }- Each product comes back
created,updatedorerror. One bad product doesn't stop the rest. - A product with the same
skuthat was added by hand in the dashboard is linked, not copied.
GET/products
Every product with live stock. Add updated_since to fetch only what changed. It also returns products that were switched off (active: false) so you can hide them.
curl "https://bluemint.app/api/v1/products?limit=50&updated_since=2026-10-01T00:00:00Z" \
-H "Authorization: Bearer $KEY"{
"data": [{
"id": "cmk1…", "external_id": "wp-1042", "sku": "TEE-BLK", "name": "Black tee",
"description": "Cotton, regular fit", "price": "650.00", "images": ["https://…"],
"category": "T-shirts", "weight_grams": 220, "barcode": "8901234567890", "active": true,
"updated_at": "2026-10-26T08:15:00.000Z",
"variants": [{ "id": "cmv1…", "external_id": "wp-1042-m", "sku": "TEE-BLK-M",
"attributes": { "size": "M" }, "price": null, "available": 12 }],
"available": 12
}],
"next_cursor": "MjAyNi0x…"
}availableis what can be sold right now: on hand minus reserved.nullmeans stock isn't tracked.- A variant
priceofnullsells at the product price. limitis 1 to 100 (default 50). Keep passingcursorfromnext_cursoruntil it'snull.
GET/products/{id}
One product, by our id or by yours with ext: in front: /products/ext:wp-1042.
Stock
We keep the stock count, so your website never oversells.
Every sale lowers it: from your website, the shop counter or a chat. Then we tell your site. Show stock from available, or keep it fresh with the stock.changed webhook.
PUT/stock
Up to 500 items. Name each by sku, variant_external_id or product_external_id, with the real count you have (quantity, a whole number). A product with sizes or colours must be named by variant. Add warehouse_id to pick a warehouse; otherwise your default one is used.
curl -X PUT https://bluemint.app/api/v1/stock \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "items": [
{ "sku": "TEE-BLK-M", "quantity": 40 },
{ "variant_external_id": "wp-1042-xl", "quantity": 15 }
] }'{ "data": [
{ "ref": "TEE-BLK-M", "status": "set", "on_hand": 40 },
{ "ref": "wp-1042-xl", "status": "unchanged", "on_hand": 15 },
{ "ref": "NOPE", "status": "error", "error": "not_found" }
] }set- The count was corrected.
unchanged- It was already that number.
errornot_found,variant_required,invalid_warehouse,no_warehouseorinternal_error.
Orders
Send each order the moment a customer checks out.
We price it from your catalogue, reserve the stock, and it goes to delivery exactly like any other order: courier, accounts and reports included.
There is no customers endpoint. The customer travels inside POST /orders, and we match them by phone: a repeat number is the same customer. There is no Idempotency-Key header either; the external_id is what makes a retry safe.
POST/orders
curl -X POST https://bluemint.app/api/v1/orders \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{
"external_id": "wp-order-8841",
"customer": { "name": "Rahim Uddin", "phone": "01712345678", "email": "rahim@example.com" },
"address": { "text": "House 12, Road 4, Dhanmondi", "area": "Dhanmondi", "district": "Dhaka" },
"items": [
{ "sku": "TEE-BLK-M", "quantity": 2 },
{ "product_external_id": "wp-2001", "quantity": 1 }
],
"delivery_charge": 80,
"payment": { "method": "COD" },
"note": "Call before delivery"
}'{
"id": "cmo1…", "number": "ORD-10231", "external_id": "wp-order-8841",
"status": "received", "fulfillment_status": "none", "payment_status": "pending",
"total": "1830.00", "delivery_charge": "80.00",
"customer": { "name": "Rahim Uddin", "phone": "01712345678" },
"address": "House 12, Road 4, Dhanmondi, Dhanmondi, Dhaka",
"courier": null,
"items": [
{ "sku": "TEE-BLK-M", "name": "Black tee", "quantity": 2, "unit_price": "650.00" },
{ "sku": "CAP-NVY", "name": "Navy cap", "quantity": 1, "unit_price": "450.00" }
],
"created_at": "2026-10-26T08:20:00.000Z", "updated_at": "2026-10-26T08:20:00.000Z"
}- Items
- Name each by
sku,variant_external_idorproduct_external_id. - Price
- Comes from your catalogue. Your own
unit_priceis used only if the shop allowed it on your key. - Phone
- Must be a Bangladesh mobile number.
- Payment
COD, orPAID. WithPAID, addamount_paidfor a part payment; the rest is collected on delivery.- Courier
courierfills in once staff hand it over:{ "name": "pathao", "tracking_code": "…" }.201- Order created.
200- An order with this external_id already exists. Returned unchanged.
422invalid_phone,unknown_item(not found, or a variant is needed) oramount_too_large.
GET/orders/{id}
One order, by our id or yours: /orders/ext:wp-order-8841. Works for any order in the shop, whichever channel it came from.
GET/orders?external_id=…
Look up by your id. Returns { "data": [order] }, or an empty list.
PATCH/orders/{id}
Change an order your site created. Every field is optional:
status: "cancelled"- Cancel it. Any other change in the same request is ignored.
addresstext,area,district.phone- A new phone number. A bad one is
422 invalid_phone. note- Added to the order notes.
paid: true- Marks it fully paid in advance.
- Orders from other channels can't be changed:
409 not_api_order. - Once an order is with a courier, edits and "paid" are refused:
409 order_locked. - A cancelled, refunded or uncollected order can't be marked paid:
409 invalid_transition.
curl -X PATCH https://bluemint.app/api/v1/orders/ext:wp-order-8841 \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "paid": true, "note": "Paid by card on site" }'Webhooks
Add your URL once, and we tell your site the moment anything changes.
Set it in Settings → Website API. We POST to it within seconds of a change.
order.created- A new order arrives, from any channel.
order.updated- An order's status, payment, delivery or courier changes.
stock.changed- A product's available stock changes (a sale, a return, a correction).
product.updated- A product is edited.
{
"id": "evt_…",
"type": "order.updated",
"created_at": "2026-10-26T08:30:00.000Z",
"business_id": "cmb1…",
"mode": "live",
"data": { /* the order or product, same shape as the GET calls */ }
}data is the latest state when we send it, so several quick changes may arrive as one. stock.changed carries the product with fresh available. mode is live or test.
Check it came from us
Every webhook is signed. Check the signature on the raw body, before parsing the JSON, and reject anything older than 5 minutes.
X-ISR-Event- The event type.
X-ISR-Delivery- The id of this delivery.
X-ISR-Timestamp- Unix seconds when it was signed.
X-ISR-Signaturesha256=followed by the hex HMAC-SHA256 of{timestamp}.{raw body}, using your signing secret.
const crypto = require("crypto");
function verify(secret, timestamp, rawBody, header) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(String(header ?? ""));
return a.length === b.length && crypto.timingSafeEqual(a, b) && Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
}function verify($secret, $timestamp, $rawBody, $header) {
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $header) && abs(time() - (int)$timestamp) < 300;
}The signing secret is shown once, when you add the URL or rotate it. Lost it? Rotate.
Deliveries and retries
- Reply with any
2xxwithin 10 seconds. Anything else, a timeout or a redirect counts as failed. - We retry after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 3 hours, 6 hours and 12 hours, then stop (9 tries in all).
- Order isn't guaranteed. Compare
data.updated_at. - Changes your own key made aren't sent back to the URL linked to that key, so your updates don't loop.
- After 50 deliveries in a row fail all their tries, the URL is switched off. Turn it back on in Settings once fixed.
- Each URL is live or test: a test URL only gets sandbox events.
- While the Shop API is switched off for the shop, webhooks stop too. Changes in that time aren't sent later.
Sandbox
Test keys work on a copy of your shop. Break things freely.
- It starts with copies of up to 500 of your products, 100 in stock each, without images. The copies have no
external_id, so usesku. - No real courier is ever booked. In Settings → Website API you can move a test order to shipped, delivered or returned, and reset the sandbox.
- Products you send with a test key get no images (the
imagesfield is accepted, just not downloaded). - A reset replaces products and stock. Test customers and accounting entries stay and build up over time.
- Webhooks from the sandbox say
mode: "test".
Errors
Errors always come back in the same shape, with a code you can act on.
{ "error": { "code": "invalid_request", "message": "items.0.quantity: Expected number" } }400 invalid_json- The body isn't valid JSON.
400 invalid_request- A field is missing or wrong. The message names it.
400 invalid_limit · invalid_cursor · invalid_updated_since- A bad query value.
401 invalid_key- The key is missing, wrong or revoked.
403 insufficient_scope- A read-only key tried to write.
403 not_enabled- The Shop API isn't switched on for this shop yet.
404 not_found- No such product or order.
409 not_api_order · order_locked · invalid_transition- The order can't be changed that way. See Orders.
422 invalid_phone · unknown_item · amount_too_large- We understood the order but can't place it.
429 rate_limited- Too fast. Wait for
Retry-After. 500 internal_error- Our side. Try again; resending an order with the same external_id is safe.
The batch calls (PUT /products, PUT /stock) answer 200 and report each item on its own.