Read and write the orders, products and customers in a Bosla workspace, and get a signed webhook the moment one changes. Connect a store Bosla doesn't integrate with, a warehouse system, or your own reporting.
In Bosla, open Settings → Connections → Developer API and create a key. Give it only the access it needs. The key starts with bsk_ and is shown once, so copy it somewhere safe.
A workspace can make 120 requests a minute, shared by all of its keys. Your workspace's exact limit is on its Developer API page. Every response says where you stand:
Header
Meaning
X-RateLimit-Limit
Requests allowed per minute.
X-RateLimit-Remaining
Requests left in the current minute.
X-RateLimit-Reset
Seconds until the minute resets.
Retry-After
On a 429 only: seconds to wait before trying again.
Plans include a number of API writes a month. Reads are free, and creating or updating orders is never counted and never refused: an order always gets in. Every other write, such as a status change, a cancel, a tag or a product change, counts once when it succeeds; endpoints that count are marked below. Once the month's writes are used up, those endpoints answer 403 until the month turns or the plan changes. Managing webhook endpoints doesn't count.
Anything other than a 2xx carries one shape. Branch on code, which is stable; show message to people, since its wording can change. When more than one field is wrong, details lists every problem.
Response · 400
{
"error": {
"code": "invalid_request",
"message": "externalId should not be empty",
"details": [
"externalId should not be empty",
"lineItems.0.quantity must be a number"
]
}
}
Status
Code
Meaning
400
invalid_request
The request is malformed or a field failed validation. message says which.
401
unauthorized
No API key was sent, or the key is invalid or revoked.
403
forbidden
The key lacks the scope this endpoint needs, or the workspace's plan doesn't include the developer API, or the monthly write allowance is used up.
404
not_found
No record with that id in this workspace.
409
conflict
The request clashes with the record's current state.
429
rate_limited
Too many requests this minute. Wait for Retry-After seconds.
List endpoints return a page of records and a cursor: { data, nextCursor }. Pass nextCursor back as cursor to get the next page, and stop when it's null. A cursor is a position, not a page number, so records arriving while you page never make you skip or repeat one.
limit is 1 to 100, 50 by default.
sortBy is createdAt or updatedAt; order is desc or asc. Keep both the same across pages: a cursor made with one sort is refused with another.
createdSince, createdBefore, updatedSince and updatedBefore take ISO 8601 times. Since includes the time you give; before excludes it.
Ask for everything changed since your last run, oldest change first, and remember the updatedAt of the last record you saved. Because updatedSince includes its own time, the last record comes back once more on the next run, so save by id. Run it every few minutes on its own, or alongside webhooks to catch anything a webhook missed.
Node.js
// Pull everything that changed since the last run, oldest change first.
let cursor = null;
const since = await loadLastSync(); // e.g. "2026-09-10T08:00:00.000Z"
do {
const params = new URLSearchParams({ sortBy: "updatedAt", order: "asc", updatedSince: since, limit: "100" });
if (cursor) params.set("cursor", cursor);
const res = await fetch(`https://api.bosla.io/api/v1/external/orders?${params}`, {
headers: { "x-bosla-api-key": process.env.BOSLA_API_KEY },
});
const { data, nextCursor } = await res.json();
for (const order of data) {
await saveOrder(order);
await saveLastSync(order.updatedAt);
}
cursor = nextCursor;
} while (cursor);
Someone who has ordered. Bosla builds customers from orders, matching on phone number first and email second, so one person who orders from several channels is one customer.
Fields
idstring
Bosla's id for the customer (UUID).
namestring | null
The name from their latest order.
emailstring | null
Their email, when an order carried one.
phonestring | null
Their phone number, when an order carried one.
totalOrdersnumber
How many orders they've placed.
totalSpentnumber
What those orders add up to.
currencystring | null
The currency of totalSpent.
lastOrderAtstring | null
When they last ordered.
createdAtstring
When Bosla first stored the record. ISO 8601, UTC.
updatedAtstring
When anything on the record last changed. ISO 8601, UTC.
Creates the order, or updates the one you sent earlier with the same externalId. Safe to retry: sending the same order twice never makes two. Order creates are never counted against your monthly writes and never refused for quota.
Body
externalIdstringrequired
Your own order id. It's the key the update matches on.
orderNumberstring
The number a person reads, if it differs from externalId.
statusstring
Defaults to new. See the note below.
customerobject
name, phone, email. The phone number is how Bosla recognises a returning customer and whom confirmation messages go to.
shippingAddressobject
address1, city, phone.
lineItemsobject[]
name (required), quantity (required), price, sku.
totalPricenumber
What the customer pays.
currencystring
Defaults to EGP.
tagsstring[]
Replaces the order's tags.
placedAtstring
When the customer placed the order, ISO 8601. Send it when importing past orders; defaults to now.
An update replaces every field you send and clears the optional ones you leave out, so always send the whole order.
The status never moves backwards on an update: re-sending a confirmed order as new keeps it confirmed. A returned order stays returned. To move a status back on purpose, use Set an order's status.
Sets the status directly, in either direction. For confirming and cancelling, prefer the endpoints below: they check the order is in a state where that makes sense, and a cancel reaches the store platform too.
Path parameters
idstringrequired
The order's Bosla id.
Body
statusstringrequired
new, confirmed, shipped, delivered, returned or on_hold.
Cancels an order that hasn't shipped. Bosla has no cancelled status: the order becomes returned and gets the tag canceled by Bosla. An order from a store platform is cancelled on that platform too; if the platform refuses, nothing changes in Bosla and you get a 400. Cancelling an order that is already returned returns it unchanged. A shipped or delivered order is refused: handle it as a return.
Path parameters
idstringrequired
The order's Bosla id.
Body
reasonstring
Why, for example customer. Carried on the order.cancelled webhook.
Creates the product, or updates the one you sent earlier with the same externalId. The response also carries ok: true, kept for integrations built on the earlier version of this API.
Body
externalIdstringrequired
Your own product id. It's the key the update matches on.
titlestringrequired
The product name.
descriptionstring
Plain text or HTML.
statusstring
active (default), draft or archived.
imageUrlstring
The main photo. Bosla's AI agent uses it to recognise the product in customers' photos.
productUrlstring
The product's page on your storefront.
tagsstring
Comma-separated, like denim, jackets.
variantsobject[]
title (required), id (a number; defaults to its position), price (a string, like "900"), sku, inventoryQuantity. Leave out inventoryQuantity when you don't track stock.
Changes only the fields you send. Works on products from custom_api and bosla; a product synced from a store platform is edited on that platform and syncs back, so it's refused here.
Path parameters
idstringrequired
The product's Bosla id.
Body
titlestring
The product name.
descriptionstring
Plain text or HTML.
productTypestring
The product type.
vendorstring
The brand or vendor.
statusstring
active, draft or archived.
imageUrlstring
A full https:// URL.
productUrlstring
A full https:// URL.
tagsstring[]
Up to 50. Replaces the product's tags.
variantsobject[]
Replaces every variant: title (required), id (digits, as a string), price (a number), sku, inventoryQuantity (null means not tracked).
Sets the product's status to archived. It isn't deleted, because past orders and the AI agent's product matches still point at it. Send status: "active" to bring it back.
Manage where Bosla sends webhooks from your own code. The same endpoints are in the dashboard under Settings, Connections, Developer API. A workspace can have up to 10.
The response carries the endpoint's signing secret. Listing and reading endpoints never return it again; it stays visible in the dashboard, and you can rotate it.
Body
urlstringrequired
An https:// URL on a public server, without a username or password in it.
When something you subscribed to happens, Bosla sends a POST with a JSON body to your endpoint. Add endpoints in the dashboard or through the API; each one has its own signing secret, starting with whsec_.
The URL has to be https:// on a public server, with no username or password in it. Redirects are not followed.
A delivery can arrive more than once, so deduplicate on the event id.
Deliveries can arrive out of order, especially after a retry. Keep the version with the latest data.object.updatedAt.
Answer fast and do the work afterwards. Bosla waits 10 seconds for a 2xx.
A new order reaches Bosla from any source: a store platform, the AI agent, the dashboard or this API.
order.updated
Anything on an order you can see changes. Fires once per change, whatever else fires with it.
order.status_changed
The order's status moves. Sent alongside order.updated.
order.cancelled
The order is cancelled. Sent alongside order.updated and order.status_changed.
product.created
A product is added: synced from a store, typed in the dashboard, or created through this API.
product.updated
A product's visible fields change, including stock and archiving.
product.deleted
A hand-added product is deleted in the dashboard. Archiving through the API sends product.updated instead.
customer.created
Someone orders for the first time under a new phone number or email.
To mirror orders, subscribe to order.created and order.updated: you see every change exactly once. The narrower order events are for reacting to one thing, such as printing a waybill when an order is confirmed. Re-syncing an order that didn't actually change sends nothing.
Check every delivery before trusting it. The X-Bosla-Signature header looks like t=1789123245,v1=5f2b...:
Take t (a Unix time in seconds) and v1 from the header.
Compute an HMAC-SHA256 of <t>.<raw request body> with the endpoint's secret, as lowercase hex. Use the body exactly as received, before any JSON parsing.
Compare it to v1 in constant time, and reject a t more than 5 minutes from now. The time is signed with the body, so an old delivery can't be replayed.
import crypto from "node:crypto";
import express from "express";
const app = express();
// Verify against the raw body. Parsing it first and re-serialising changes the bytes.
app.post("/bosla/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Bosla-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const timestamp = Number(parts.t);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) {
return res.status(400).send("stale or missing timestamp");
}
const expected = crypto
.createHmac("sha256", process.env.BOSLA_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1 ?? "", "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(400).send("bad signature");
}
const event = JSON.parse(req.body);
res.sendStatus(200); // answer first, then do the work
queueForProcessing(event);
});
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["BOSLA_WEBHOOK_SECRET"].encode()
@app.post("/bosla/webhooks")
def bosla_webhook():
body = request.get_data() # the raw bytes
parts = dict(p.split("=", 1) for p in request.headers.get("X-Bosla-Signature", "").split(",") if "=" in p)
timestamp = int(parts.get("t", 0))
if not timestamp or abs(time.time() - timestamp) > 300:
abort(400)
expected = hmac.new(SECRET, f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")):
abort(400)
event = request.get_json()
# process event["type"] and event["data"]["object"]
return "", 200
A delivery fails when your server answers anything other than 2xx, takes longer than 10 seconds, or can't be reached. Bosla then tries again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours: eight attempts over about 45 hours, so a server that's down overnight still gets everything.
If every delivery to an endpoint fails for 3 days straight, Bosla turns the endpoint off and tells the workspace owner. Fix the receiver, turn it back on, and use the delivery log to retry what failed, or catch up with updatedSince.