# Changelog Every entry states whether the change requires an action on your side. The contract stays on version `1.0`; breaking changes are announced in advance and are never applied silently. Every entry is classified by change level: - **Breaking** — Requires a code change. - **Behavior** — The shape of the contract did not change, but its meaning did; review your POS assumptions. - **Additive** — Something was added; your current integration keeps working unchanged. ## August 12, 2026 ### [Breaking] The update webhook’s discriminator is renamed to `event` The unified envelope of the update webhooks called its discriminator `status`, but only two of its four values are order statuses: `cancelled` and `delivered`. `driver_assigned` and `driver_released` belong to the driver axis and never move the order, so a merchant reading `status: "driver_assigned"` was invited to overwrite its own status with a driver event. The field is now called `event` and the route moves from `/status` to `/events`, so the path stops promising a status the body does not carry. The accepted values do not change: `cancelled`, `driver_assigned`, `delivered` and `driver_released`. The set stays extensible — an unrecognized `event` value must be ignored, and adding a new one is not a breaking change. Before: ```http PUT {baseUrl}/v1/order/{remoteId}/{remoteOrderId}/status { "orderKey": "ord_oOR7xSbWz0QksS2I", "status": "driver_assigned", ... } ``` After: ```http PUT {baseUrl}/v1/order/{remoteId}/{remoteOrderId}/events { "orderKey": "ord_oOR7xSbWz0QksS2I", "event": "driver_assigned", ... } ``` **Action required.** Change the receiver’s route to `/events` and read the `event` field instead of `status`. There is no transition window and no duplicated field: the previous route stops being emitted immediately. ### [Behavior] `handedOverAt` stays `null` when the merchant never reported the handover If the merchant does not declare the handover and BipBip closes the order once delivery is confirmed, `handedOverAt` used to store the instant of the **delivery to the customer**. The column then asserted a moment nobody observed: read back, an order where the merchant stayed silent looked as if it had been handed to the driver exactly when the customer received it. That fact now lives in `deliveredAt`, so `handedOverAt` can say the honest thing and stay `null`. An order closed that way reads as `handedOverAt: null` with `deliveredAt` populated. It is not a hole in the data: it is the difference between “we do not know” and “it happened at this time”. **Review.** If your POS assumes an order in `handed_over` always carries `handedOverAt`, account for the `null`. An order can be in a terminal state without the handover moment being known. ### [Additive] `deliveredAt` states when the customer received the order `handed_over` answers “is the merchant done?” and cannot answer “does the customer have it?”. On a `delivery` order those two moments are separated by the driver’s trip: the merchant releases the food to the driver, and the driver reaches the customer later. `deliveredAt` answers the second one, both in the detail and in the listing, next to `driverAssignedAt`. On `pickup` the customer collects at the store, so the merchant’s handover **is** the delivery and the value is derived from the fulfillment type. Both channels get a value, so on an order created from August 12, 2026 onwards `null` means “there is no confirmed delivery yet” and never “this channel cannot have one”. Orders predating that date return `null` permanently, even if they were delivered. The field shipped without backfilling the earlier rows, because that moment had never been recorded anywhere: there was no value to copy. **Review.** New field in `GET /orders` and in `GET /orders/{orderKey}`. If you reconcile a period spanning August 12, 2026, do not read `null` as “not delivered” on orders predating that date: for those, `handed_over` remains the only sign of closure. ## August 11, 2026 ### [Breaking] The driver is no longer an order status The `driver_assigned` status was removed from the state machine. An order never reports that value in `status`, and `GET /orders` no longer accepts it as a filter. Driver assignment runs in parallel to the merchant’s progress and is now exposed in the `driverAssignedAt` field. **Action required.** If your POS filtered by `status=driver_assigned` or compared against that value, read `driverAssignedAt` instead. In exchange, it is now possible to mark `ready` with a driver already assigned, which was previously rejected with `409`. ### [Behavior] A handed-over order can still be cancelled An order in `handed_over` can move to `cancelled`: a refund or an incident after handover closes it in that state and the merchant receives the corresponding webhook. Previously those cancellations were not notified. **Review.** If your POS assumed `handed_over` was an absolute final state, account for receiving an `event: "cancelled"` webhook afterwards. ### [Behavior] The customer’s cancellation reason travels as text On cancellations originated by the customer, `history[].reason` carries the reason as text. It previously carried only its numeric identifier, which meant nothing outside BipBip: the merchant received the cancellation with no way to say why it happened. If the customer writes a comment, that comment is what travels; if they do not, the name of the reason they picked travels instead. Orders cancelled before this change keep the numeric identifier. **No action.** `reason` is descriptive text meant for humans and its content is not stable. Do not parse it or derive logic from it. ### [Behavior] `GET /orders` sorts by order number The previous criterion was the moment BipBip registered the order, which can differ from the real creation order and place an older order above a more recent one. The pagination cursor is now that same number. **No action.** Cursor pagination keeps working identically. ### [Behavior] An order can move to `ready` without the merchant asking The `ready` status stops depending exclusively on `PUT /orders/{orderKey}/status`. The driver marks the order as ready from their own app, and that record advances it too. As a result, a `PUT` with `status: "ready"` can answer `409` on an order the merchant had not advanced yet. That 409 does not describe a conflict: it describes a state already reached, and `meta` says so outright. 409 response: ```http PUT {baseUrl}/api/v1/Orders/{orderKey}/status → 409 { "status": 409, "meta": { "currentStatus": "Ready", "allowedTransitions": ["HandedOver"] } } ``` **Review.** If your POS assumes only it moves the order to `ready`, treat a 409 with `meta.currentStatus: "Ready"` as a state already reached and continue with `handed_over`, instead of considering it an error. ### [Additive] The listing states whether the order has a driver Every item in `GET /orders` includes `driverAssignedAt`, the moment a driver was assigned to the order, or `null` if it does not have one yet. With `status` alone it was not possible to tell a ready order waiting for a driver from a ready order with a driver on the way. **No action.** New field in the response. ### [Additive] The driver is identified by their code The `history[]` items concerning the driver now carry the `driver` object with a `code` field (for example `BIP-01424`), the identifier to use with BipBip support. The `driver_assigned`, `driver_reassigned` and `driver_released` events are distinguished through `driver.event`. When the driver has no code on file, `code` reports their numeric identifier instead. The field is always a string and its format is not stable, so it must not be validated against the `BIP-#####` pattern. **No action.** New field inside `history[]`. Treat it as opaque: it is a label, not a number. ## August 10, 2026 ### [Breaking] The menu is read with the same names it is written with On menu reads, `imageUrl` is now called `image` and `modifiers` is now called `modifierGroups` — the names the menu upload already used. With two names for the same concept, reusing the write model to deserialize the read left the fields as `null` without raising any error. **Action required.** Rename both fields in the `GET /menu/stores/{storeRemoteId}` deserializer. `image` returns the image re-hosted by BipBip in WebP, not the original URL that was sent. ### [Behavior] Local products receive a canonical code When a local product is created, BipBip derives its own internal code and preserves the merchant’s code in `remoteCode`. Previously the submitted code was used as the internal code and the merchant’s code was lost. Later calls accept either of the two codes, so the merchant can keep addressing the product with its own. **No action.** Local products created earlier keep their original code and keep resolving unchanged. ### [Additive] Local product creation accepts tax, limit and image `POST /menu/stores/{storeRemoteId}/products` accepts `tax`, `maxPerOrder`, `image` and `channels`. The accepted values for each field are published in `GET /menu/capabilities`. **No action.** Optional fields; omitting them preserves the previous defaults. ### [Additive] The order detail returns its full contents `GET /orders/{orderKey}` now carries the `order` object with the order’s commercial content: products, modifiers, payment methods, charges, discounts and billing data. Its structure is the same as the `order.created` webhook, so it can be read with the model already implemented for that webhook. **Optional, recommended.** Allows recovering an order whose webhook could not be processed, without depending on a redelivery. ## August 8, 2026 ### [Additive] The menu exposes per-store restrictions Every product and every modifier option now carries `orderable`, `resumesAt` and the `overrides[]` array, ordered by precedence: the first element determines the item’s current state. Overrides that change a value include `base`, the master-catalog value the item returns to if the restriction is lifted. `resumesAt` states when the restriction expires on its own; its absence means it stays in force until it is explicitly lifted. **No action.** Restricted items stay in the response instead of being omitted, which makes it possible to inspect them and lift the restrictions applied.