Developers

Bosla developer API

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.

Base URLhttps://api.bosla.io/api/v1/external
On this page

Getting started

Overview

The API is JSON over HTTPS. Every request is made with an API key that belongs to one workspace, and it can only see and change that workspace's data.

  • Orders: every order in the workspace, from any store or channel. Send your own, confirm, cancel and tag them.
  • Products: the whole catalog. Create and update the products your system owns, and keep their stock right.
  • Customers: everyone who has ordered, matched across channels by phone number.
  • Webhooks: a signed POST to your server whenever an order, product or customer changes, so you don't have to poll.

Conventions

  • Ids are UUIDs. Times are ISO 8601 in UTC, like 2026-09-01T14:30:00.000Z.
  • Money is a plain number in the record's currency: 950 with EGP is 950 pounds, not piastres.
  • null means unknown or not sent. It never stands in for zero; in particular, a null stock level means stock isn't tracked.
  • New fields can appear in responses and webhooks at any time, so ignore the ones you don't know. Existing fields are not renamed or removed.

Quick start

  1. 1

    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.

  2. 2

    Make your first call. Replace the key with yours:

    Request
    export BOSLA_API_KEY="bsk_..."
    
    curl "https://api.bosla.io/api/v1/external/orders?limit=1" \
      -H "x-bosla-api-key: $BOSLA_API_KEY"
    Response · 200
    {
      "data": [
        {
          "id": "5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10",
          "externalId": "ORD-10432",
          "source": "custom_api",
          "orderNumber": "10432",
          "status": "new",
          "customer": {
            "id": "c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f",
            "name": "Sara Ahmed",
            "phone": "+201012345678",
            "email": "[email protected]"
          },
          "totalPrice": 950,
          "currency": "EGP",
          "itemsCount": 1,
          "channel": "custom_api",
          "tags": [
            "vip"
          ],
          "lineItems": [
            {
              "title": "Oversized denim jacket - Blue / L",
              "variantTitle": null,
              "sku": "JKT-BLU-L",
              "quantity": 1,
              "price": 900,
              "productId": null,
              "variantId": null
            }
          ],
          "shippingAddress": {
            "name": "Sara Ahmed",
            "phone": "+201012345678",
            "address1": "12 Tahrir St, Apt 4",
            "address2": null,
            "city": "Cairo",
            "province": null,
            "country": null,
            "zip": null
          },
          "totals": {
            "subtotal": null,
            "shipping": null,
            "discount": null,
            "tax": null,
            "total": 950
          },
          "paymentMethod": null,
          "financialStatus": null,
          "note": null,
          "shipment": null,
          "placedAt": "2026-09-01T14:30:00.000Z",
          "createdAt": "2026-09-01T14:30:02.118Z",
          "updatedAt": "2026-09-01T14:30:02.118Z"
        }
      ],
      "nextCursor": null
    }
  3. 3

    To hear about changes as they happen, add a webhook endpoint and verify its signatures.

The workspace's plan has to include the developer API. If it doesn't, every call answers 403 and says so.

Authentication

Send the key in the x-bosla-api-key header. If your HTTP client only does bearer tokens, Authorization: Bearer bsk_... works too.

Header
x-bosla-api-key: bsk_4f1c...9a2e
  • A key belongs to the workspace, not to a person, and keeps working when whoever created it leaves the team. A workspace can have 10 active keys.
  • Revoking a key in the dashboard stops it on the very next request.
  • Keep keys on your server. Never put one in a website, a mobile app or a repository: anyone holding it can read the workspace's customers.

Scopes

Each key has scopes, chosen when it's created. A request outside them answers 403 naming the missing scope. A :write scope includes its :read.

ScopeAllows
orders:readList and read orders.
orders:writeCreate and update orders, change status, confirm, cancel and tag. Includes orders:read.
products:readList and read products.
products:writeCreate, edit and archive products and set stock. Includes products:read.
customers:readList and read customers. There is no write scope: customers come from orders.
webhooks:manageAdd, change and remove webhook endpoints, send test events and read deliveries.

Rate limits and usage

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:

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute.
X-RateLimit-RemainingRequests left in the current minute.
X-RateLimit-ResetSeconds until the minute resets.
Retry-AfterOn a 429 only: seconds to wait before trying again.

Monthly writes

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.

Errors

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"
    ]
  }
}
StatusCodeMeaning
400invalid_requestThe request is malformed or a field failed validation. message says which.
401unauthorizedNo API key was sent, or the key is invalid or revoked.
403forbiddenThe 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.
404not_foundNo record with that id in this workspace.
409conflictThe request clashes with the record's current state.
429rate_limitedToo many requests this minute. Wait for Retry-After seconds.
5xxserver_errorSomething failed on our side. Retry with backoff.

Pagination and syncing

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.

Keeping a copy in sync

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);

Objects

The order object

Every order in the workspace, whatever it came from. Money is a number in currency, not in cents.

Fields

idstring
Bosla's id for the order (UUID). Use it in the URL of every order endpoint.
externalIdstring | null
The order's id in the system it came from. For orders you send, your own id.
sourcestring
Where it came from: shopify, salla, easyorders, custom_api (this API), bosla (typed in the dashboard or taken by the AI agent).
orderNumberstring | null
The number a person reads, like 10432.
statusstring
new, confirmed, shipped, delivered, returned or on_hold. A cancelled order is returned.
customerobject
id (the customer, or null), name, phone, email. Each can be null.
totalPricenumber | null
What the customer pays.
currencystring | null
ISO 4217 code, like EGP.
itemsCountnumber
Number of line items.
channelstring | null
The channel the order was taken on, when known.
tagsstring[]
Labels on the order.
lineItemsobject[]
title, variantTitle, sku, quantity, price, and the productId / variantId the source system sent, when it sent them.
shippingAddressobject | null
name, phone, address1, address2, city, province, country, zip. Each can be null.
totalsobject
subtotal, shipping, discount, tax, total. Null where the source didn't send one.
paymentMethodstring | null
For example cod, or the store's payment gateway name.
financialStatusstring | null
The store platform's payment status, when it has one.
notestring | null
The customer's note at checkout.
shipmentobject | null
Once a courier is tracking it: provider, trackingNumber, state, lastEventAt.
placedAtstring
When the customer placed it: the source's own time, or when Bosla received it.
createdAtstring
When Bosla first stored the record. ISO 8601, UTC.
updatedAtstring
When anything on the record last changed. ISO 8601, UTC.

The product object

A product in the workspace's catalog, synced from a store, typed in the dashboard, or sent through this API.

Fields

idstring
Bosla's id for the product (UUID).
externalIdstring
The product's id in the system it came from. For products you send, your own id.
sourcestring
shopify, salla, easyorders, custom_api or bosla. Only custom_api and bosla products can be edited through this API.
titlestring
The product name.
descriptionstring | null
Plain text or the store's HTML.
productTypestring | null
The store's product type.
vendorstring | null
The brand or vendor.
statusstring
active, draft or archived.
imageUrlstring | null
The main product photo.
productUrlstring | null
The product's page on the storefront.
tagsstring[]
Labels on the product.
variantsobject[]
id, title, price, sku, inventoryQuantity. An inventoryQuantity of null means stock isn't tracked. It never means zero.
createdAtstring
When Bosla first stored the record. ISO 8601, UTC.
updatedAtstring
When anything on the record last changed. ISO 8601, UTC.

The customer object

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.

Endpoints

Account

Check that a key works and see what it may do.

Get the current key

GET/me
scope any

Returns the workspace and the key making the request, with its scopes. Use it to test a key someone pastes into your settings page.

Request
curl "https://api.bosla.io/api/v1/external/me" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 200
{
  "workspaceId": "3f6c2a8e-5d1b-4f7a-9e0c-2b4d6f8a1c3e",
  "apiKey": {
    "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "name": "Warehouse sync",
    "scopes": [
      "orders:write",
      "products:read"
    ]
  }
}

Orders

Read every order in the workspace, send your own, and move them through confirmation and cancellation.

List orders

GET/orders
scope orders:read

Newest first unless you ask otherwise. See pagination for syncing.

Query parameters

limitinteger
1 to 100. Defaults to 50.
cursorstring
The nextCursor from the previous page.
sortBystring
createdAt (default) or updatedAt.
orderstring
desc (default) or asc.
createdSincestring
Only records created at or after this ISO 8601 time.
createdBeforestring
Only records created before this time.
updatedSincestring
Only records changed at or after this time.
updatedBeforestring
Only records changed before this time.
statusstring
new, confirmed, shipped, delivered, returned or on_hold.
sourcestring
Only orders from this source, like shopify or custom_api.
externalIdstring
The order's id in its own system.
Request
curl "https://api.bosla.io/api/v1/external/orders?status=new&limit=20" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10",
      "externalId": "ORD-10432",
      "source": "custom_api",
      "orderNumber": "10432",
      "status": "new",
      "customer": {
        "id": "c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f",
        "name": "Sara Ahmed",
        "phone": "+201012345678",
        "email": "[email protected]"
      },
      "totalPrice": 950,
      "currency": "EGP",
      "itemsCount": 1,
      "channel": "custom_api",
      "tags": [
        "vip"
      ],
      "lineItems": [
        {
          "title": "Oversized denim jacket - Blue / L",
          "variantTitle": null,
          "sku": "JKT-BLU-L",
          "quantity": 1,
          "price": 900,
          "productId": null,
          "variantId": null
        }
      ],
      "shippingAddress": {
        "name": "Sara Ahmed",
        "phone": "+201012345678",
        "address1": "12 Tahrir St, Apt 4",
        "address2": null,
        "city": "Cairo",
        "province": null,
        "country": null,
        "zip": null
      },
      "totals": {
        "subtotal": null,
        "shipping": null,
        "discount": null,
        "tax": null,
        "total": 950
      },
      "paymentMethod": null,
      "financialStatus": null,
      "note": null,
      "shipment": null,
      "placedAt": "2026-09-01T14:30:00.000Z",
      "createdAt": "2026-09-01T14:30:02.118Z",
      "updatedAt": "2026-09-01T14:30:02.118Z"
    }
  ],
  "nextCursor": "eyJzIjoiY3JlYXRlZEF0IiwibyI6ImRlc2MiLC4uLn0"
}

Get an order

GET/orders/{id}
scope orders:read

One order by its Bosla id.

Path parameters

idstringrequired
The order's Bosla id.
Request
curl "https://api.bosla.io/api/v1/external/orders/5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 200
{
  "id": "5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10",
  "externalId": "ORD-10432",
  "source": "custom_api",
  "orderNumber": "10432",
  "status": "new",
  "customer": {
    "id": "c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f",
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "email": "[email protected]"
  },
  "totalPrice": 950,
  "currency": "EGP",
  "itemsCount": 1,
  "channel": "custom_api",
  "tags": [
    "vip"
  ],
  "lineItems": [
    {
      "title": "Oversized denim jacket - Blue / L",
      "variantTitle": null,
      "sku": "JKT-BLU-L",
      "quantity": 1,
      "price": 900,
      "productId": null,
      "variantId": null
    }
  ],
  "shippingAddress": {
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "address1": "12 Tahrir St, Apt 4",
    "address2": null,
    "city": "Cairo",
    "province": null,
    "country": null,
    "zip": null
  },
  "totals": {
    "subtotal": null,
    "shipping": null,
    "discount": null,
    "tax": null,
    "total": 950
  },
  "paymentMethod": null,
  "financialStatus": null,
  "note": null,
  "shipment": null,
  "placedAt": "2026-09-01T14:30:00.000Z",
  "createdAt": "2026-09-01T14:30:02.118Z",
  "updatedAt": "2026-09-01T14:30:02.118Z"
}

Create or update an order

POST/orders
scope orders:write

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.
Request
curl -X POST "https://api.bosla.io/api/v1/external/orders" \
  -H "x-bosla-api-key: $BOSLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"externalId":"ORD-10432","orderNumber":"10432","customer":{"name":"Sara Ahmed","phone":"+201012345678","email":"[email protected]"},"shippingAddress":{"address1":"12 Tahrir St, Apt 4","city":"Cairo"},"lineItems":[{"name":"Oversized denim jacket - Blue / L","quantity":1,"price":900,"sku":"JKT-BLU-L"}],"totalPrice":950,"currency":"EGP","tags":["vip"],"placedAt":"2026-09-01T14:30:00Z"}'
Response · 201
{
  "id": "5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10",
  "externalId": "ORD-10432",
  "source": "custom_api",
  "orderNumber": "10432",
  "status": "new",
  "customer": {
    "id": "c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f",
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "email": "[email protected]"
  },
  "totalPrice": 950,
  "currency": "EGP",
  "itemsCount": 1,
  "channel": "custom_api",
  "tags": [
    "vip"
  ],
  "lineItems": [
    {
      "title": "Oversized denim jacket - Blue / L",
      "variantTitle": null,
      "sku": "JKT-BLU-L",
      "quantity": 1,
      "price": 900,
      "productId": null,
      "variantId": null
    }
  ],
  "shippingAddress": {
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "address1": "12 Tahrir St, Apt 4",
    "address2": null,
    "city": "Cairo",
    "province": null,
    "country": null,
    "zip": null
  },
  "totals": {
    "subtotal": null,
    "shipping": null,
    "discount": null,
    "tax": null,
    "total": 950
  },
  "paymentMethod": null,
  "financialStatus": null,
  "note": null,
  "shipment": null,
  "placedAt": "2026-09-01T14:30:00.000Z",
  "createdAt": "2026-09-01T14:30:02.118Z",
  "updatedAt": "2026-09-01T14:30:02.118Z"
}

Set an order's status

PATCH/orders/{id}

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.
Request
curl -X PATCH "https://api.bosla.io/api/v1/external/orders/5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10" \
  -H "x-bosla-api-key: $BOSLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"shipped"}'
Response · 200
{
  "id": "5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10",
  "externalId": "ORD-10432",
  "source": "custom_api",
  "orderNumber": "10432",
  "status": "shipped",
  "customer": {
    "id": "c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f",
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "email": "[email protected]"
  },
  "totalPrice": 950,
  "currency": "EGP",
  "itemsCount": 1,
  "channel": "custom_api",
  "tags": [
    "vip"
  ],
  "lineItems": [
    {
      "title": "Oversized denim jacket - Blue / L",
      "variantTitle": null,
      "sku": "JKT-BLU-L",
      "quantity": 1,
      "price": 900,
      "productId": null,
      "variantId": null
    }
  ],
  "shippingAddress": {
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "address1": "12 Tahrir St, Apt 4",
    "address2": null,
    "city": "Cairo",
    "province": null,
    "country": null,
    "zip": null
  },
  "totals": {
    "subtotal": null,
    "shipping": null,
    "discount": null,
    "tax": null,
    "total": 950
  },
  "paymentMethod": null,
  "financialStatus": null,
  "note": null,
  "shipment": null,
  "placedAt": "2026-09-01T14:30:00.000Z",
  "createdAt": "2026-09-01T14:30:02.118Z",
  "updatedAt": "2026-09-01T14:30:02.118Z"
}

Confirm an order

POST/orders/{id}/confirm

Confirms a new or on_hold order. Confirming an order that is already confirmed does nothing and returns it. Any other status is refused with a 400.

Path parameters

idstringrequired
The order's Bosla id.
Request
curl -X POST "https://api.bosla.io/api/v1/external/orders/5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10/confirm" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 201
{
  "id": "5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10",
  "externalId": "ORD-10432",
  "source": "custom_api",
  "orderNumber": "10432",
  "status": "confirmed",
  "customer": {
    "id": "c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f",
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "email": "[email protected]"
  },
  "totalPrice": 950,
  "currency": "EGP",
  "itemsCount": 1,
  "channel": "custom_api",
  "tags": [
    "vip"
  ],
  "lineItems": [
    {
      "title": "Oversized denim jacket - Blue / L",
      "variantTitle": null,
      "sku": "JKT-BLU-L",
      "quantity": 1,
      "price": 900,
      "productId": null,
      "variantId": null
    }
  ],
  "shippingAddress": {
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "address1": "12 Tahrir St, Apt 4",
    "address2": null,
    "city": "Cairo",
    "province": null,
    "country": null,
    "zip": null
  },
  "totals": {
    "subtotal": null,
    "shipping": null,
    "discount": null,
    "tax": null,
    "total": 950
  },
  "paymentMethod": null,
  "financialStatus": null,
  "note": null,
  "shipment": null,
  "placedAt": "2026-09-01T14:30:00.000Z",
  "createdAt": "2026-09-01T14:30:02.118Z",
  "updatedAt": "2026-09-01T14:30:02.118Z"
}

Unconfirm an order

POST/orders/{id}/unconfirm

Puts a confirmed order back to new. An order that is already new is returned unchanged; anything further along is refused.

Path parameters

idstringrequired
The order's Bosla id.
Request
curl -X POST "https://api.bosla.io/api/v1/external/orders/5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10/unconfirm" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 201
{
  "id": "5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10",
  "externalId": "ORD-10432",
  "source": "custom_api",
  "orderNumber": "10432",
  "status": "new",
  "customer": {
    "id": "c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f",
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "email": "[email protected]"
  },
  "totalPrice": 950,
  "currency": "EGP",
  "itemsCount": 1,
  "channel": "custom_api",
  "tags": [
    "vip"
  ],
  "lineItems": [
    {
      "title": "Oversized denim jacket - Blue / L",
      "variantTitle": null,
      "sku": "JKT-BLU-L",
      "quantity": 1,
      "price": 900,
      "productId": null,
      "variantId": null
    }
  ],
  "shippingAddress": {
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "address1": "12 Tahrir St, Apt 4",
    "address2": null,
    "city": "Cairo",
    "province": null,
    "country": null,
    "zip": null
  },
  "totals": {
    "subtotal": null,
    "shipping": null,
    "discount": null,
    "tax": null,
    "total": 950
  },
  "paymentMethod": null,
  "financialStatus": null,
  "note": null,
  "shipment": null,
  "placedAt": "2026-09-01T14:30:00.000Z",
  "createdAt": "2026-09-01T14:30:02.118Z",
  "updatedAt": "2026-09-01T14:30:02.118Z"
}

Cancel an order

POST/orders/{id}/cancel

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.
Request
curl -X POST "https://api.bosla.io/api/v1/external/orders/5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10/cancel" \
  -H "x-bosla-api-key: $BOSLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason":"customer"}'
Response · 201
{
  "id": "5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10",
  "externalId": "ORD-10432",
  "source": "custom_api",
  "orderNumber": "10432",
  "status": "returned",
  "customer": {
    "id": "c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f",
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "email": "[email protected]"
  },
  "totalPrice": 950,
  "currency": "EGP",
  "itemsCount": 1,
  "channel": "custom_api",
  "tags": [
    "vip",
    "canceled by Bosla"
  ],
  "lineItems": [
    {
      "title": "Oversized denim jacket - Blue / L",
      "variantTitle": null,
      "sku": "JKT-BLU-L",
      "quantity": 1,
      "price": 900,
      "productId": null,
      "variantId": null
    }
  ],
  "shippingAddress": {
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "address1": "12 Tahrir St, Apt 4",
    "address2": null,
    "city": "Cairo",
    "province": null,
    "country": null,
    "zip": null
  },
  "totals": {
    "subtotal": null,
    "shipping": null,
    "discount": null,
    "tax": null,
    "total": 950
  },
  "paymentMethod": null,
  "financialStatus": null,
  "note": null,
  "shipment": null,
  "placedAt": "2026-09-01T14:30:00.000Z",
  "createdAt": "2026-09-01T14:30:02.118Z",
  "updatedAt": "2026-09-01T14:30:02.118Z"
}

Add tags

POST/orders/{id}/tags

Adds tags to the order. Tags it already has are skipped.

Path parameters

idstringrequired
The order's Bosla id.

Body

tagsstring[]required
1 to 20 tags, each up to 100 characters.
Request
curl -X POST "https://api.bosla.io/api/v1/external/orders/5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10/tags" \
  -H "x-bosla-api-key: $BOSLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tags":["gift-wrap"]}'
Response · 201
{
  "id": "5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10",
  "externalId": "ORD-10432",
  "source": "custom_api",
  "orderNumber": "10432",
  "status": "new",
  "customer": {
    "id": "c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f",
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "email": "[email protected]"
  },
  "totalPrice": 950,
  "currency": "EGP",
  "itemsCount": 1,
  "channel": "custom_api",
  "tags": [
    "vip",
    "gift-wrap"
  ],
  "lineItems": [
    {
      "title": "Oversized denim jacket - Blue / L",
      "variantTitle": null,
      "sku": "JKT-BLU-L",
      "quantity": 1,
      "price": 900,
      "productId": null,
      "variantId": null
    }
  ],
  "shippingAddress": {
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "address1": "12 Tahrir St, Apt 4",
    "address2": null,
    "city": "Cairo",
    "province": null,
    "country": null,
    "zip": null
  },
  "totals": {
    "subtotal": null,
    "shipping": null,
    "discount": null,
    "tax": null,
    "total": 950
  },
  "paymentMethod": null,
  "financialStatus": null,
  "note": null,
  "shipment": null,
  "placedAt": "2026-09-01T14:30:00.000Z",
  "createdAt": "2026-09-01T14:30:02.118Z",
  "updatedAt": "2026-09-01T14:30:02.118Z"
}

Remove a tag

DELETE/orders/{id}/tags/{tag}

Removes one tag. URL-encode the tag if it has spaces or symbols.

Path parameters

idstringrequired
The order's Bosla id.
tagstringrequired
The tag to remove.
Request
curl -X DELETE "https://api.bosla.io/api/v1/external/orders/5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10/tags/gift-wrap" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 200
{
  "id": "5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10",
  "externalId": "ORD-10432",
  "source": "custom_api",
  "orderNumber": "10432",
  "status": "new",
  "customer": {
    "id": "c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f",
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "email": "[email protected]"
  },
  "totalPrice": 950,
  "currency": "EGP",
  "itemsCount": 1,
  "channel": "custom_api",
  "tags": [
    "vip"
  ],
  "lineItems": [
    {
      "title": "Oversized denim jacket - Blue / L",
      "variantTitle": null,
      "sku": "JKT-BLU-L",
      "quantity": 1,
      "price": 900,
      "productId": null,
      "variantId": null
    }
  ],
  "shippingAddress": {
    "name": "Sara Ahmed",
    "phone": "+201012345678",
    "address1": "12 Tahrir St, Apt 4",
    "address2": null,
    "city": "Cairo",
    "province": null,
    "country": null,
    "zip": null
  },
  "totals": {
    "subtotal": null,
    "shipping": null,
    "discount": null,
    "tax": null,
    "total": 950
  },
  "paymentMethod": null,
  "financialStatus": null,
  "note": null,
  "shipment": null,
  "placedAt": "2026-09-01T14:30:00.000Z",
  "createdAt": "2026-09-01T14:30:02.118Z",
  "updatedAt": "2026-09-01T14:30:02.118Z"
}

Products

Read the whole catalog, and keep the products your own system owns in step, stock included.

List products

GET/products
scope products:read

Every product in the workspace, from every source.

Query parameters

limitinteger
1 to 100. Defaults to 50.
cursorstring
The nextCursor from the previous page.
sortBystring
createdAt (default) or updatedAt.
orderstring
desc (default) or asc.
createdSincestring
Only records created at or after this ISO 8601 time.
createdBeforestring
Only records created before this time.
updatedSincestring
Only records changed at or after this time.
updatedBeforestring
Only records changed before this time.
statusstring
active, draft or archived.
sourcestring
Only products from this source.
externalIdstring
The product's id in its own system.
skustring
Products with a variant carrying this SKU.
Request
curl "https://api.bosla.io/api/v1/external/products?sku=JKT-BLU-L" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "8d2f4b6e-1c3a-4e5f-8a7b-9c0d1e2f3a4b",
      "externalId": "PROD-1042",
      "source": "custom_api",
      "title": "Oversized denim jacket",
      "description": "Heavyweight denim, relaxed fit.",
      "productType": null,
      "vendor": null,
      "status": "active",
      "imageUrl": "https://cdn.example.com/jacket.jpg",
      "productUrl": "https://shop.example.com/products/denim-jacket",
      "tags": [
        "denim",
        "jackets"
      ],
      "variants": [
        {
          "id": "1",
          "title": "Blue / L",
          "price": 900,
          "sku": "JKT-BLU-L",
          "inventoryQuantity": 12
        },
        {
          "id": "2",
          "title": "Blue / XL",
          "price": 900,
          "sku": "JKT-BLU-XL",
          "inventoryQuantity": null
        }
      ],
      "createdAt": "2026-08-20T09:12:44.501Z",
      "updatedAt": "2026-09-01T10:03:17.920Z"
    }
  ],
  "nextCursor": "eyJzIjoiY3JlYXRlZEF0IiwibyI6ImRlc2MiLC4uLn0"
}

Get a product

GET/products/{id}
scope products:read

One product by its Bosla id.

Path parameters

idstringrequired
The product's Bosla id.
Request
curl "https://api.bosla.io/api/v1/external/products/8d2f4b6e-1c3a-4e5f-8a7b-9c0d1e2f3a4b" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 200
{
  "id": "8d2f4b6e-1c3a-4e5f-8a7b-9c0d1e2f3a4b",
  "externalId": "PROD-1042",
  "source": "custom_api",
  "title": "Oversized denim jacket",
  "description": "Heavyweight denim, relaxed fit.",
  "productType": null,
  "vendor": null,
  "status": "active",
  "imageUrl": "https://cdn.example.com/jacket.jpg",
  "productUrl": "https://shop.example.com/products/denim-jacket",
  "tags": [
    "denim",
    "jackets"
  ],
  "variants": [
    {
      "id": "1",
      "title": "Blue / L",
      "price": 900,
      "sku": "JKT-BLU-L",
      "inventoryQuantity": 12
    },
    {
      "id": "2",
      "title": "Blue / XL",
      "price": 900,
      "sku": "JKT-BLU-XL",
      "inventoryQuantity": null
    }
  ],
  "createdAt": "2026-08-20T09:12:44.501Z",
  "updatedAt": "2026-09-01T10:03:17.920Z"
}

Create or update a product

POST/products
scope products:writeCounts toward monthly writes

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.
Request
curl -X POST "https://api.bosla.io/api/v1/external/products" \
  -H "x-bosla-api-key: $BOSLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"externalId":"PROD-1042","title":"Oversized denim jacket","description":"Heavyweight denim, relaxed fit.","imageUrl":"https://cdn.example.com/jacket.jpg","productUrl":"https://shop.example.com/products/denim-jacket","tags":"denim, jackets","variants":[{"id":1,"title":"Blue / L","price":"900","sku":"JKT-BLU-L","inventoryQuantity":12},{"id":2,"title":"Blue / XL","price":"900","sku":"JKT-BLU-XL"}]}'
Response · 201
{
  "ok": true,
  "id": "8d2f4b6e-1c3a-4e5f-8a7b-9c0d1e2f3a4b",
  "externalId": "PROD-1042",
  "source": "custom_api",
  "title": "Oversized denim jacket",
  "description": "Heavyweight denim, relaxed fit.",
  "productType": null,
  "vendor": null,
  "status": "active",
  "imageUrl": "https://cdn.example.com/jacket.jpg",
  "productUrl": "https://shop.example.com/products/denim-jacket",
  "tags": [
    "denim",
    "jackets"
  ],
  "variants": [
    {
      "id": "1",
      "title": "Blue / L",
      "price": 900,
      "sku": "JKT-BLU-L",
      "inventoryQuantity": 12
    },
    {
      "id": "2",
      "title": "Blue / XL",
      "price": 900,
      "sku": "JKT-BLU-XL",
      "inventoryQuantity": null
    }
  ],
  "createdAt": "2026-08-20T09:12:44.501Z",
  "updatedAt": "2026-09-01T10:03:17.920Z"
}

Edit a product

PATCH/products/{id}
scope products:writeCounts toward monthly writes

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).
Request
curl -X PATCH "https://api.bosla.io/api/v1/external/products/8d2f4b6e-1c3a-4e5f-8a7b-9c0d1e2f3a4b" \
  -H "x-bosla-api-key: $BOSLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Oversized denim jacket (2026)","tags":["denim","jackets","new"]}'
Response · 200
{
  "id": "8d2f4b6e-1c3a-4e5f-8a7b-9c0d1e2f3a4b",
  "externalId": "PROD-1042",
  "source": "custom_api",
  "title": "Oversized denim jacket (2026)",
  "description": "Heavyweight denim, relaxed fit.",
  "productType": null,
  "vendor": null,
  "status": "active",
  "imageUrl": "https://cdn.example.com/jacket.jpg",
  "productUrl": "https://shop.example.com/products/denim-jacket",
  "tags": [
    "denim",
    "jackets",
    "new"
  ],
  "variants": [
    {
      "id": "1",
      "title": "Blue / L",
      "price": 900,
      "sku": "JKT-BLU-L",
      "inventoryQuantity": 12
    },
    {
      "id": "2",
      "title": "Blue / XL",
      "price": 900,
      "sku": "JKT-BLU-XL",
      "inventoryQuantity": null
    }
  ],
  "createdAt": "2026-08-20T09:12:44.501Z",
  "updatedAt": "2026-09-01T10:03:17.920Z"
}

Set stock

PUT/products/{id}/inventory
scope products:writeCounts toward monthly writes

Sets stock for one or more variants, matched by variant id or by sku. Variants you don't list keep their stock. Same source rule as editing.

Path parameters

idstringrequired
The product's Bosla id.

Body

variantsobject[]required
1 to 250 entries. Each has an id or a sku, and inventoryQuantity: a whole number, or null to stop tracking stock for that variant.
Request
curl -X PUT "https://api.bosla.io/api/v1/external/products/8d2f4b6e-1c3a-4e5f-8a7b-9c0d1e2f3a4b/inventory" \
  -H "x-bosla-api-key: $BOSLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"variants":[{"sku":"JKT-BLU-L","inventoryQuantity":7}]}'
Response · 200
{
  "id": "8d2f4b6e-1c3a-4e5f-8a7b-9c0d1e2f3a4b",
  "externalId": "PROD-1042",
  "source": "custom_api",
  "title": "Oversized denim jacket",
  "description": "Heavyweight denim, relaxed fit.",
  "productType": null,
  "vendor": null,
  "status": "active",
  "imageUrl": "https://cdn.example.com/jacket.jpg",
  "productUrl": "https://shop.example.com/products/denim-jacket",
  "tags": [
    "denim",
    "jackets"
  ],
  "variants": [
    {
      "id": "1",
      "title": "Blue / L",
      "price": 900,
      "sku": "JKT-BLU-L",
      "inventoryQuantity": 7
    },
    {
      "id": "2",
      "title": "Blue / XL",
      "price": 900,
      "sku": "JKT-BLU-XL",
      "inventoryQuantity": null
    }
  ],
  "createdAt": "2026-08-20T09:12:44.501Z",
  "updatedAt": "2026-09-01T10:03:17.920Z"
}

Archive a product

DELETE/products/{id}
scope products:writeCounts toward monthly writes

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.

Path parameters

idstringrequired
The product's Bosla id.
Request
curl -X DELETE "https://api.bosla.io/api/v1/external/products/8d2f4b6e-1c3a-4e5f-8a7b-9c0d1e2f3a4b" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 200
{
  "id": "8d2f4b6e-1c3a-4e5f-8a7b-9c0d1e2f3a4b",
  "externalId": "PROD-1042",
  "source": "custom_api",
  "title": "Oversized denim jacket",
  "description": "Heavyweight denim, relaxed fit.",
  "productType": null,
  "vendor": null,
  "status": "archived",
  "imageUrl": "https://cdn.example.com/jacket.jpg",
  "productUrl": "https://shop.example.com/products/denim-jacket",
  "tags": [
    "denim",
    "jackets"
  ],
  "variants": [
    {
      "id": "1",
      "title": "Blue / L",
      "price": 900,
      "sku": "JKT-BLU-L",
      "inventoryQuantity": 12
    },
    {
      "id": "2",
      "title": "Blue / XL",
      "price": 900,
      "sku": "JKT-BLU-XL",
      "inventoryQuantity": null
    }
  ],
  "createdAt": "2026-08-20T09:12:44.501Z",
  "updatedAt": "2026-09-01T10:03:17.920Z"
}

Customers

Read-only. To create or update a customer, send an order under their phone number or email.

List customers

GET/customers
scope customers:read

Everyone who has ordered.

Query parameters

limitinteger
1 to 100. Defaults to 50.
cursorstring
The nextCursor from the previous page.
sortBystring
createdAt (default) or updatedAt.
orderstring
desc (default) or asc.
createdSincestring
Only records created at or after this ISO 8601 time.
createdBeforestring
Only records created before this time.
updatedSincestring
Only records changed at or after this time.
updatedBeforestring
Only records changed before this time.
phonestring
Matches on the last 10 digits, so 01012345678 and +201012345678 find the same customer.
emailstring
Case-insensitive exact match.
Request
curl "https://api.bosla.io/api/v1/external/customers?phone=01012345678" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f",
      "name": "Sara Ahmed",
      "email": "[email protected]",
      "phone": "+201012345678",
      "totalOrders": 3,
      "totalSpent": 2450,
      "currency": "EGP",
      "lastOrderAt": "2026-09-01T14:30:00.000Z",
      "createdAt": "2026-06-11T18:40:09.330Z",
      "updatedAt": "2026-09-01T14:30:02.300Z"
    }
  ],
  "nextCursor": "eyJzIjoiY3JlYXRlZEF0IiwibyI6ImRlc2MiLC4uLn0"
}

Get a customer

GET/customers/{id}
scope customers:read

One customer by their Bosla id.

Path parameters

idstringrequired
The customer's Bosla id.
Request
curl "https://api.bosla.io/api/v1/external/customers/c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 200
{
  "id": "c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f",
  "name": "Sara Ahmed",
  "email": "[email protected]",
  "phone": "+201012345678",
  "totalOrders": 3,
  "totalSpent": 2450,
  "currency": "EGP",
  "lastOrderAt": "2026-09-01T14:30:00.000Z",
  "createdAt": "2026-06-11T18:40:09.330Z",
  "updatedAt": "2026-09-01T14:30:02.300Z"
}

Webhook endpoints

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.

List event types

GET/webhooks/event-types
scope webhooks:manage

Every event an endpoint can subscribe to.

Request
curl "https://api.bosla.io/api/v1/external/webhooks/event-types" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 200
{
  "data": [
    "order.created",
    "order.updated",
    "order.status_changed",
    "order.cancelled",
    "product.created",
    "product.updated",
    "product.deleted",
    "customer.created"
  ]
}

List endpoints

GET/webhooks
scope webhooks:manage

Every endpoint in the workspace. Signing secrets are not included.

Request
curl "https://api.bosla.io/api/v1/external/webhooks" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "e7a1c3d5-9b2f-4e6a-8c0d-1f3b5d7e9a2c",
      "url": "https://example.com/bosla/webhooks",
      "description": "Sync to our warehouse system",
      "events": [
        "order.created",
        "order.updated"
      ],
      "status": "active",
      "disabledReason": null,
      "disabledCause": null,
      "failingSince": null,
      "lastSuccessAt": "2026-09-10T08:15:31.004Z",
      "createdAt": "2026-09-01T12:00:00.000Z",
      "updatedAt": "2026-09-01T12:00:00.000Z"
    }
  ]
}

Add an endpoint

POST/webhooks
scope webhooks:manage

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.
eventsstring[]required
At least one event type.
descriptionstring
A note for your team, up to 255 characters.
Request
curl -X POST "https://api.bosla.io/api/v1/external/webhooks" \
  -H "x-bosla-api-key: $BOSLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/bosla/webhooks","events":["order.created","order.updated"],"description":"Sync to our warehouse system"}'
Response · 201
{
  "id": "e7a1c3d5-9b2f-4e6a-8c0d-1f3b5d7e9a2c",
  "url": "https://example.com/bosla/webhooks",
  "description": "Sync to our warehouse system",
  "events": [
    "order.created",
    "order.updated"
  ],
  "status": "active",
  "disabledReason": null,
  "disabledCause": null,
  "failingSince": null,
  "lastSuccessAt": "2026-09-10T08:15:31.004Z",
  "createdAt": "2026-09-01T12:00:00.000Z",
  "updatedAt": "2026-09-01T12:00:00.000Z",
  "secret": "whsec_9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}

Get an endpoint

GET/webhooks/{id}
scope webhooks:manage

One endpoint. When status is disabled, disabledCause says why: failures (every delivery failed for 3 days) or workspace.

Path parameters

idstringrequired
The endpoint's Bosla id.
Request
curl "https://api.bosla.io/api/v1/external/webhooks/e7a1c3d5-9b2f-4e6a-8c0d-1f3b5d7e9a2c" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 200
{
  "id": "e7a1c3d5-9b2f-4e6a-8c0d-1f3b5d7e9a2c",
  "url": "https://example.com/bosla/webhooks",
  "description": "Sync to our warehouse system",
  "events": [
    "order.created",
    "order.updated"
  ],
  "status": "active",
  "disabledReason": null,
  "disabledCause": null,
  "failingSince": null,
  "lastSuccessAt": "2026-09-10T08:15:31.004Z",
  "createdAt": "2026-09-01T12:00:00.000Z",
  "updatedAt": "2026-09-01T12:00:00.000Z"
}

Update an endpoint

PATCH/webhooks/{id}
scope webhooks:manage

Change the URL, the events or the description, or turn the endpoint off and on. Only the fields you send change.

Path parameters

idstringrequired
The endpoint's Bosla id.

Body

urlstring
A new https:// URL.
eventsstring[]
Replaces the subscribed events. At least one.
descriptionstring
A note for your team.
statusstring
active or disabled. Turn an endpoint back on after fixing the receiver.
Request
curl -X PATCH "https://api.bosla.io/api/v1/external/webhooks/e7a1c3d5-9b2f-4e6a-8c0d-1f3b5d7e9a2c" \
  -H "x-bosla-api-key: $BOSLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events":["order.created","order.updated","product.updated"]}'
Response · 200
{
  "id": "e7a1c3d5-9b2f-4e6a-8c0d-1f3b5d7e9a2c",
  "url": "https://example.com/bosla/webhooks",
  "description": "Sync to our warehouse system",
  "events": [
    "order.created",
    "order.updated",
    "product.updated"
  ],
  "status": "active",
  "disabledReason": null,
  "disabledCause": null,
  "failingSince": null,
  "lastSuccessAt": "2026-09-10T08:15:31.004Z",
  "createdAt": "2026-09-01T12:00:00.000Z",
  "updatedAt": "2026-09-01T12:00:00.000Z"
}

Delete an endpoint

DELETE/webhooks/{id}
scope webhooks:manage

Stops all deliveries to the endpoint, including retries still waiting.

Path parameters

idstringrequired
The endpoint's Bosla id.
Request
curl -X DELETE "https://api.bosla.io/api/v1/external/webhooks/e7a1c3d5-9b2f-4e6a-8c0d-1f3b5d7e9a2c" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"

Response · 204, no body

Rotate the signing secret

POST/webhooks/{id}/secret/rotate
scope webhooks:manage

Replaces the secret. The old one stops verifying immediately, so update your receiver right after.

Path parameters

idstringrequired
The endpoint's Bosla id.
Request
curl -X POST "https://api.bosla.io/api/v1/external/webhooks/e7a1c3d5-9b2f-4e6a-8c0d-1f3b5d7e9a2c/secret/rotate" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 201
{
  "secret": "whsec_2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
}

Send a test event

POST/webhooks/{id}/test
scope webhooks:manage

Sends a signed webhook.test event to the endpoint now and returns how the delivery went. The endpoint has to be active.

Path parameters

idstringrequired
The endpoint's Bosla id.
Request
curl -X POST "https://api.bosla.io/api/v1/external/webhooks/e7a1c3d5-9b2f-4e6a-8c0d-1f3b5d7e9a2c/test" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 201
{
  "id": "d2c4e6f8-0a1b-4c3d-9e5f-7a9b1c3d5e7f",
  "eventId": "0f9e8d7c-6b5a-4938-8271-605f4e3d2c1b",
  "eventType": "webhook.test",
  "status": "succeeded",
  "attempts": 1,
  "nextAttemptAt": null,
  "responseStatus": 200,
  "responseExcerpt": "ok",
  "error": null,
  "durationMs": 184,
  "lastAttemptAt": "2026-09-10T08:15:31.004Z",
  "createdAt": "2026-09-10T08:15:30.812Z"
}

List deliveries

GET/webhooks/{id}/deliveries
scope webhooks:manage

Recent deliveries to the endpoint, newest first, with the response your server gave. To page, pass the nextCursor back as before.

Path parameters

idstringrequired
The endpoint's Bosla id.

Query parameters

statusstring
pending, sending, succeeded or failed.
limitinteger
1 to 100. Defaults to 25.
beforestring
A delivery id: return deliveries older than it.
Request
curl "https://api.bosla.io/api/v1/external/webhooks/e7a1c3d5-9b2f-4e6a-8c0d-1f3b5d7e9a2c/deliveries?status=failed" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "d2c4e6f8-0a1b-4c3d-9e5f-7a9b1c3d5e7f",
      "eventId": "0f9e8d7c-6b5a-4938-8271-605f4e3d2c1b",
      "eventType": "order.updated",
      "status": "failed",
      "attempts": 8,
      "nextAttemptAt": null,
      "responseStatus": 500,
      "responseExcerpt": "Internal Server Error",
      "error": "Receiver answered HTTP 500",
      "durationMs": 184,
      "lastAttemptAt": "2026-09-10T08:15:31.004Z",
      "createdAt": "2026-09-10T08:15:30.812Z"
    }
  ],
  "nextCursor": null
}

Retry a delivery

POST/webhooks/{id}/deliveries/{deliveryId}/retry
scope webhooks:manage

Sends a delivery again now, even one that has used all its retries, and returns the result.

Path parameters

idstringrequired
The endpoint's Bosla id.
deliveryIdstringrequired
The delivery to send again.
Request
curl -X POST "https://api.bosla.io/api/v1/external/webhooks/e7a1c3d5-9b2f-4e6a-8c0d-1f3b5d7e9a2c/deliveries/d2c4e6f8-0a1b-4c3d-9e5f-7a9b1c3d5e7f/retry" \
  -H "x-bosla-api-key: $BOSLA_API_KEY"
Response · 201
{
  "id": "d2c4e6f8-0a1b-4c3d-9e5f-7a9b1c3d5e7f",
  "eventId": "0f9e8d7c-6b5a-4938-8271-605f4e3d2c1b",
  "eventType": "order.updated",
  "status": "succeeded",
  "attempts": 9,
  "nextAttemptAt": null,
  "responseStatus": 200,
  "responseExcerpt": "ok",
  "error": null,
  "durationMs": 184,
  "lastAttemptAt": "2026-09-10T08:15:31.004Z",
  "createdAt": "2026-09-10T08:15:30.812Z"
}

Webhooks

How webhooks work

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.

Events

EventSent when
order.createdA new order reaches Bosla from any source: a store platform, the AI agent, the dashboard or this API.
order.updatedAnything on an order you can see changes. Fires once per change, whatever else fires with it.
order.status_changedThe order's status moves. Sent alongside order.updated.
order.cancelledThe order is cancelled. Sent alongside order.updated and order.status_changed.
product.createdA product is added: synced from a store, typed in the dashboard, or created through this API.
product.updatedA product's visible fields change, including stock and archiving.
product.deletedA hand-added product is deleted in the dashboard. Archiving through the API sends product.updated instead.
customer.createdSomeone 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.

Payload and headers

Every event has the same envelope. data.object is the whole order, product or customer as it is after the change, in the same shape the API returns.

order.status_changed
{
  "id": "0f9e8d7c-6b5a-4938-8271-605f4e3d2c1b",
  "type": "order.status_changed",
  "createdAt": "2026-09-02T11:20:45.012Z",
  "origin": {
    "type": "bosla"
  },
  "data": {
    "object": {
      "id": "5b0e7c1a-2f7d-4c1e-9a55-3f1f0f6d2a10",
      "externalId": "ORD-10432",
      "source": "custom_api",
      "orderNumber": "10432",
      "status": "confirmed",
      "customer": {
        "id": "c41e9f02-6b7a-4d3c-8e1f-2a3b4c5d6e7f",
        "name": "Sara Ahmed",
        "phone": "+201012345678",
        "email": "[email protected]"
      },
      "totalPrice": 950,
      "currency": "EGP",
      "itemsCount": 1,
      "channel": "custom_api",
      "tags": [
        "vip"
      ],
      "lineItems": [
        {
          "title": "Oversized denim jacket - Blue / L",
          "variantTitle": null,
          "sku": "JKT-BLU-L",
          "quantity": 1,
          "price": 900,
          "productId": null,
          "variantId": null
        }
      ],
      "shippingAddress": {
        "name": "Sara Ahmed",
        "phone": "+201012345678",
        "address1": "12 Tahrir St, Apt 4",
        "address2": null,
        "city": "Cairo",
        "province": null,
        "country": null,
        "zip": null
      },
      "totals": {
        "subtotal": null,
        "shipping": null,
        "discount": null,
        "tax": null,
        "total": 950
      },
      "paymentMethod": null,
      "financialStatus": null,
      "note": null,
      "shipment": null,
      "placedAt": "2026-09-01T14:30:00.000Z",
      "createdAt": "2026-09-01T14:30:02.118Z",
      "updatedAt": "2026-09-02T11:20:44.870Z"
    },
    "previousStatus": "new",
    "status": "confirmed"
  }
}
FieldMeaning
idThe event's id. Deduplicate on it.
typeOne of the events above, or webhook.test from a test send.
createdAtWhen the change happened.
originWho made the change. See avoiding loops.
data.objectThe record after the change.
data.previousOn order.updated and product.updated: the earlier value of each field that changed.
data.changedFieldsOn order.updated and product.updated: the names of the fields that changed.
data.previousStatusOn order.status_changed, with data.status.
data.reasonOn order.cancelled: the reason given, or null.
order.updated · data
{
  "object": {
    "…": "the order as it is now"
  },
  "previous": {
    "status": "new",
    "tags": [
      "vip"
    ]
  },
  "changedFields": [
    "status",
    "tags"
  ]
}
HeaderValue
X-Bosla-Signaturet=<unix seconds>,v1=<signature>. See below.
X-Bosla-Event-IdThe event's id, the same as id in the body. Deduplicate on it.
X-Bosla-Event-TypeThe event type, the same as type in the body.
X-Bosla-Delivery-IdThe delivery's id. It stays the same across retries of that delivery.
User-AgentBosla-Webhooks/1.0

Avoiding loops

If your system writes to Bosla and also listens to its webhooks, it hears its own writes echoed back. origin tells you who made each change:

origin.typeThe change came from
apiAn API key. origin.apiKeyId says which one: skip events carrying your own key's id.
shopifyA sync from a store platform. Also salla, easyorders.
boslaSomething inside Bosla: the dashboard, an automation, the AI agent or a courier update.

Your key's id is apiKey.id in the response to GET /me.

Verifying signatures

Check every delivery before trusting it. The X-Bosla-Signature header looks like t=1789123245,v1=5f2b...:

  1. Take t (a Unix time in seconds) and v1 from the header.
  2. 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.
  3. 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);
});

Retries and failures

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.

Help

Support

Stuck, or need something the API doesn't do yet? Email [email protected] with the request you made and the X-Bosla-Delivery-Id or error you got back.