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.
Latest release: August 12, 2026·Contract v1.0
How to read this changelog
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.
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
PUT {baseUrl}/v1/order/{remoteId}/{remoteOrderId}/status
{ "orderKey": "ord_oOR7xSbWz0QksS2I", "status": "driver_assigned", ... }
After
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.