Skip to content

Response shape and conventions ​

Five things behave differently here than in most APIs. Reading this page before you generate a client will save you the four bugs it describes.

Everything is wrapped in an envelope ​

The thing you asked for is in data. The fields beside it describe the outcome.

json
{
  "code": 0,
  "message": "Success",
  "data": { "id": "prod_1", "name": "Kofe", "price": "9900" },
  "messages": { "uz": "Muvaffaqiyatli", "ru": "Успешно", "en": "Success" },
  "status_code": 200,
  "time": "2026-08-25T08:32:00.000Z"
}

Dispatch on code, not on the HTTP status. 0 is success; anything else is an error and the error codes page lists them. The code is stable across releases. The message text is prose and may be reworded.

messages carries the same explanation in every locale Bione ships, so you can show a user an error in their language without a second request.

Every success is HTTP 200, even a create ​

A POST that creates something answers 200, not 201.

This one breaks generated clients. A client built on the assumption that "201 means created" will treat every successful create as an unhandled status and either throw or silently discard the body. If your generator lets you say which status carries the success payload, say 200 for every operation.

Errors keep their real status: 400, 401, 403, 404, 409, 429.

Money is a string ​

json
{ "price": "9900", "total": "125000.5" }

Never a number. Reading a price as a float loses precision on large sums, and sums here are in so'm.

The scale is not padded. A price stored as 9900.00 arrives as "9900", and one stored as 9900.50 arrives as "9900.5". Parse these with a decimal library and compare the parsed values — never compare the strings, and never assume two decimal places.

Lists are paginated, always in one shape ​

json
{
  "items": [ ... ],
  "total": 42,
  "page": 1,
  "pageSize": 20,
  "totalPages": 3
}

Ask for a page with ?page= and ?pageSize=. That object is what sits in data for every list route on this surface.

Retries are safe if you ask for them ​

Send an Idempotency-Key header — any unique string you generate — on any non-GET request:

POST /integration/v1/products
Idempotency-Key: 6f1c9e2a-...

Then:

What happenedWhat you get back
First callThe real response, stored for 24 hours
Retry, same key, same bodyThe original response, replayed. The work does not happen twice
Retry, same key, different body409 — the key is already spoken for
Retry while the first is still running425 — try again shortly

A call that failed releases its key, so you can retry a failure with the same one.

A key is scoped to your API key and to the route and resource you sent it to — the method, the path and its ids. The same key on another route, on another resource (DELETE /integration/v1/products/A then …/products/B) or under another API key is a new request: it runs, and it never replays a response stored for a different one. Mint a fresh key per operation anyway; reuse across routes is safe, not useful.

Creating an order has a second, stronger guarantee. Its requestId field is stored with the order rather than in a 24-hour cache, so replaying it returns the original order however much later. Use requestId as the one you rely on, and the header as a convenience. A requestId already used for a different order — another customer's — is refused with 409 (code 10009) rather than replayed.

Some fields belong to the merchant's own system ​

A merchant may run an ERP alongside Bione. When they do, that system owns some of their product fields — commonly price, stockQty, name — and Bione holds those in sync with it.

Writing one of those fields is refused, with 409, before anything is stored. Nothing is partially applied: the whole update is rejected, and the fields that caused it are listed in data.fields.

json
{
  "code": 12010,
  "message": "These fields are managed by a connected external system and cannot be edited here: price",
  "data": { "fields": ["price"] },
  "status_code": 409
}

Do not hardcode which fields those are. The set is per merchant, and it changes when they connect, disconnect or reconfigure their system. Every product carries a lockedFields array naming the fields locked for that merchant right now:

json
{ "id": "prod_1", "name": "Kofe", "lockedFields": ["price", "stockQty"] }

Read it before you attempt an update. A merchant with no external system connected — which is most of them — has an empty array and nothing is refused.

Refusing rather than accepting-then-overwriting is deliberate. An update that appeared to work and quietly reverted an hour later would leave you shipping an integration that looks correct and is not.

A route that returns nothing says "OK" ​

Deleting a product answers 200 with the string "OK" in data, rather than 204 with an empty body. That keeps one response shape across every route.