Outbound · Implemented by the POS
Webhook Spec
This document describes the outbound contract that BipBip uses to deliver new orders to the POS. It is the endpoint that the POS system must implement and expose so that BipBip can call it.
This is the counterpart of the REST API
Transport contract
Closed transport contract — these parameters are fixed and not negotiated per merchant.
| Parameter | Value |
|---|---|
| Creation path | POST {baseUrl}/v1/order/{remoteId} |
| Updates path | PUT {baseUrl}/v1/order/{remoteId}/{remoteOrderId}/events |
| Events on that path | cancelled, driver_assigned, driver_released, delivered — discriminated by body.event |
| Versioning | In the path (/v1/) — future versions can run in parallel |
| Body | JSON — schemas Order and OrderUpdateEvent |
| Timeout | 15 seconds per attempt |
| ACK (creation) | HTTP 200 with { remoteOrderId } — required |
| ACK (status update) | HTTP 200 — body ignored |
| Forward compat | Unknown statuses and extra top-level fields must be tolerated → 200 OK and ignore |
Signed headers
BipBip signs every request with HMAC-SHA256 over {timestamp}.{rawBody}using the shared secret from onboarding. The HMAC verification guide documents the full algorithm and code samples.
| Header | Description |
|---|---|
| X-Bipbip-Signature-256 | HMAC-SHA256 in the format sha256=<hex> (lowercase) |
| X-Bipbip-Timestamp | Unix epoch in seconds. Rejected if it exceeds 300s (5 min) of skew. |
| X-Bipbip-Event-Type | order.created on the creation webhook and menu.change.completed.v1 on the menu one. The spec does not declare it on the updates endpoint: there, discrimination is by body.event. |
| X-Bipbip-Schema-Version | Payload schema version. Currently 1.0. BipBip only bumps it on incompatible changes; adding new status values or other fields is additive and does not change the version. |
| X-Bipbip-Delivery-Id | Unique UUID per dispatch. Use it for deduplication. |
Authentication strategies
Beyond the HMAC signature —which always travels—, each webhook endpoint can configure one of three strategies in the Merchant Portal for an additional auth header. The strategy only controls whether that extra header is added; it never replaces the signature.
| Strategy | What it adds |
|---|---|
| hmac_only default | HMAC-SHA256 signature only (X-Bipbip-Signature-256 + X-Bipbip-Timestamp). No Authorization header is sent. |
| static_headers | Adds arbitrary HTTP headers (e.g. X-Api-Key, Authorization: Bearer <static-token>). Sensitive values are encrypted with AWS KMS and decrypted at dispatch time. Supports a 24h dual-window rotation. |
| oauth_client_credentials | BipBip obtains a Bearer token via OAuth 2.0 Client Credentials and injects Authorization: Bearer <token>. The token is cached (60s buffer before expiry); on a 401 from the merchant, BipBip invalidates the cache and retries once with a fresh token. Supports client_secret_post and client_secret_basic, scopes and extra params. 24h dual-window rotation for the client_secret. |
The HMAC signature is ALWAYS present — regardless of strategy
All three strategies include the HMAC signature in X-Bipbip-Signature-256. The strategy only decides whether an additional auth header is added — it does not replace the signature.
Your POS must always validate the HMAC signature, whatever strategy is configured. There is no mTLS or custom basic-auth.
Retry policy
BipBip implements at-least-once delivery. Retries are re-enqueued without exponential backoff — the next attempt arrives within seconds, not minutes.
- 5xx or network error → BipBip retries (up to
maxRetries, default 3 total attempts, configurable per endpoint) - 4xx → terminal, BipBip does not retry
- Timeout > 15s → treated as a failure and retried
- HTTP 200 without remoteOrderId (creation only) → treated as a failure and retried
- All retries carry the same
X-Bipbip-Delivery-Id— deduplicate using that UUID
Creation retries exhausted
order.created webhook fails, BipBip cancels the order internally (notifying the customer) and the order never reaches the POS.PUT /events calls that fail after every attempt are dropped — BipBip does not expose a separate failure event.Endpoints
The POS must expose these two endpoints. The second is a single updates endpoint — the event type is discriminated by body.event(cancelled, driver_assigned, driver_released, delivered).
event is not the order status
event says what happened, not what state the order ended up in. Only cancelled and delivered have a counterpart in the resource state.
driver_assigned and driver_released belong to the driver cycle, which runs in parallel with the merchant's progress and does not modify it. An order can receive a driver_assigned while in preparing and stay in preparing.
What NOT to do: use event to overwrite the internal order status outside those two cases. The resource state is read with GET /api/v1/Orders/{orderKey} and never takes a driver value.
/v1/order/{remoteId}Receive a new order
BipBip sends this webhook when an order is created. The POS verifies the HMAC signature, deduplicates with X-Bipbip-Delivery-Id, persists the order, and responds HTTP 200 with { remoteOrderId } in the body. The remoteOrderId value is required — BipBip uses it to compose the URL of subsequent updates.
Path parameters
remoteIdstringrequiredHeaders (signed by BipBip)
X-Bipbip-Signature-256stringrequired{timestamp}.{rawBody}, formatted as sha256=<hex>.X-Bipbip-TimestampintegerrequiredX-Bipbip-Event-Typestringrequiredorder.created on this endpoint.X-Bipbip-Delivery-IdUUIDrequiredBody
Returns — 200 OK (required)
remoteOrderIdstringrequiredPUT /events calls./v1/order/{remoteId}/{remoteOrderId}/eventsOrder updates
Single endpoint for every update after creation. body.event discriminates between cancelled, driver_assigned, driver_released, and delivered. event says what happened, not the resource state: only cancelled and delivered modify it. driver_assigned can arrive more than once per order (driver reassignment). The POS MUST tolerate unknown values and respond 200 OK ignoring them. Only invoked if the creation webhook responded successfully.
Path parameters
remoteIdstringrequiredremoteOrderIdstringrequiredHeaders (signed by BipBip)
X-Bipbip-Signature-256stringrequired{timestamp}.{rawBody}.X-Bipbip-TimestampintegerrequiredX-Bipbip-Schema-Versionstringrequired1.0. Future versions bump it and may bring new top-level fields and new event values.X-Bipbip-Delivery-IdUUIDrequiredThis endpoint does not declare X-Bipbip-Event-Type
body.event and do not make the handler depend on X-Bipbip-Event-Type being present here.Body — discriminated by event
event. Four variants: cancelled (carries reason; modifies the status), driver_assigned (carries driver; can repeat; does not modify the status), driver_released (carries releasedDriver, reason, expectedNext; informational, does not modify the status), delivered (modifies the status).Returns — 200 OK
(body)anyLifecycle
Two parallel tracks
The status the merchant reports advances linearly. Driver assignment is not a step inside that cycle: it runs in parallel and does not modify it. BipBip starts looking for a driver while the order is still in preparing.
Order status (reported by the merchant)
Driver cycle (events, not statuses)
Practical consequence: driver_assigned can arrive at any point beforehanded_over, even before you reportready. Do not assume a fixed order betweenready and driver_assigned.
driver_assigned can arrive more than once
A BipBip operator may reassign the driver before delivery — and the internal system can also rotate drivers automatically. Each reassignment fires a new driver_assigned with the updated driver data, same schema as the first one.
Do: always show the most recently received driver. Do NOT: idempotize by orderKey + event and discard the second event — that leaves you displaying a driver that is no longer correct. Deduplicate by X-Bipbip-Delivery-Id.
The internal origin of the assignment (Auto / Operator / Driver hub) does not travel in the webhook — the payload is identical for all three cases.
driver_released is informational — do NOT roll back state
When driver_released arrives, the order logically remains in driver_assigned — it does not roll back to ready or any earlier state.
Do: clear the displayed driver in the POS UI and wait for the next driver_assigned with the new driver. The expectedNext field hints the next expected event (driver_reassignment).
delivered does not shield you from a later cancelled
The cancelled event can arrive even after delivered: a refund or an incident reverses an order that was already delivered.
Do NOT: treat delivery as a final state that blocks a later cancellation. Charge reconciliation depends on that cancellation.
Updates are tied to creation success
order.created) never reached the POS with HTTP 200, BipBip does not send any subsequent PUT /events for that order.Forward compatibility — REQUIRED
BipBip may add new values to event and extra top-level fieldswithout bumping the schema version. The POS MUST:
- Gracefully ignore unrecognized
eventvalues → respond200 OK. - Do not treat unknown top-level fields as errors.
Menu-change webhook
A third endpoint, independent of the order lifecycle. BipBip calls it when it finishes applying a menu change, and it carries its own event type: X-Bipbip-Event-Type: menu.change.completed.v1.
Result notification, no side effect
Unlike order.created —whose total failure cancels the order—, if this webhook's retries are exhausted, BipBip marks the delivery as failed internally but does not revert or cancel any change or order.
Already-applied changes stay applied; you simply won't have received the confirmation. You can query the status with GET /api/v1/menu/changes/{changeKey}.
Per-store modifierOption overrides: persisted, but currently a customer-facing no-op
When the confirmed change includes a store-level price or name for a modifierOption, an applied: true result means BipBip persisted the override — but the BipBip app does not yet reflect it to the end customer.
The REST API flags these combos with pendingCustomerRollout: true in GET /api/v1/menu/capabilities.
Schema v1.0
Schemas
Definition of the JSON bodies BipBip sends and the expected ACK back. The headerX-Bipbip-Schema-Version: 1.0identifies this version. Additive changes (new status values or optional fields) do not bump it — existing integrators keep working as long as they tolerate unknown values.
Each schema declares required (required) and optional (optional) fields. Optional fields may be absent or take the value null as indicated in the description.
Order
The v1.0 payload BipBip sends to the POS when a new order is created (POST /v1/order/{remoteId}, X-Bipbip-Event-Type: order.created). Designed under the data-minimization principle: only the fields the POS needs to cook, invoice, and charge the order are included.
Identification
orderKeystringrequiredord_ followed by 16 base62 characters. Use it when invoking the BipBip REST API to accept, reject, or advance the order's status.displayCodestringrequiredstoreRemoteId if global uniqueness is required.storeRemoteIdstringrequiredcurrencystringrequiredHNL).createdAtISO-8601requiredexpiresAtISO-8601requiredFulfillment & customer
Payment & totals
Order lines
Notes & invoicing
customerNotestring | nulloptionalnull if no note exists.Fulfillment
type field discriminates the variant: delivery (BipBip driver picks up and delivers) or pickup (customer comes to the store). Each variant populates only one of the two pickup timestamps.Fields
typeenumrequireddelivery: BipBip driver picks up and delivers. pickup: customer comes to the store.isExpressbooleanrequiredtrue if the customer paid for express delivery → prioritize in the kitchen. Always false on pickup.prepareByISO-8601requireddriverPickupAtISO-8601 | nulloptionalnull on pickup.customerPickupAtISO-8601 | nulloptionalnull on delivery.Customer
firstName is included so the cashier can call the customer to the counter ("Order for Ana"). Last name, phone, and email are not included. Only present on pickup orders; on delivery, customer = null.Fields
firstNamestringrequiredPayment
cash is collected at handover; card and bips are pre-paid in the app.Fields
amountToCollectdecimalrequiredmethods[].amount where type=cash. 0.00 if completely pre-paid.changeFordecimal | nulloptionalnull if completely pre-paid.Summary
grandTotal = subtotal + taxes − discounts + additionalCharges.Fields
subtotaldecimalrequiredunitPrice × quantity of items before taxes. The merchant's revenue line.taxesdecimalrequiredtax × quantity of items. Taxes to remit.discountsdecimalrequireddiscounts[].amount (positive value).additionalChargesdecimalrequiredcharges[].amount.grandTotaldecimalrequiredpayment.methods[].amount.Charge
code is a closed enum; new codes require a major schema bump with prior announcement.Fields
codeenumrequireddelivery_fee, express_fee, service_fee, driver_tip, small_order_fee.amountdecimalrequiredbilledByenumrequiredbipbip (BipBip invoices it; not merchant revenue) | merchant (merchant revenue).Discount
code: "GENERIC" when there is a discount; future iterations will break out coupons, loyalty, etc.Fields
codestringrequiredcode=GENERIC. Future iterations will break out coupons, loyalty levels, etc.namestringrequiredamountdecimalrequiredfundedByenumrequiredbipbip (BipBip funds it; merchant receives full price) | merchant (merchant funds it; impacts revenue).Invoice
null in the predominant case (end consumer).Fields
taxIdstringrequiredbusinessNamestringrequiredItem
code) and the merchant POS code (remoteCode) — the latter falls back to code if not configured.Fields
codestringrequiredremoteCodestringrequiredremoteCode was configured, this field falls back to code (BipBip catalog).namestringrequiredquantityinteger ≥ 1requiredunitPricedecimalrequiredtaxdecimalrequiredlineTotaldecimalrequired(unitPrice + tax) × quantity + modifiers. Provided for fast reconciliation; the POS can recompute it locally.notestring | nulloptionalnull if no note exists.ItemModifierGroup & ModifierOption
ItemModifierGroup
codestringrequirednamestringrequiredOrderAck
remoteOrderId is required: BipBip persists it and uses it to compose the URL of future updates. Responses without this field are considered failed and are retried.Fields
remoteOrderIdstringrequiredintegration_order.remote_order_id and uses it to compose the updates webhook URL (PUT /v1/order/{remoteId}/{remoteOrderId}/events). Responses without this field are retried.OrderUpdateEvent
event discriminates the event: cancelled, driver_assigned, driver_released, delivered. Event-specific fields (reason, driver, releasedDriver, expectedNext) appear only in the corresponding event.event describes what happened, not the resource state
driver_assigned, driver_released) do not modify the order status. Only cancelled and delivered have a counterpart in it.Forward compatibility — REQUIRED
200 OK and ignore unknown values of event, and tolerate unknown top-level fields.Fields
orderKeystringrequiredord_ + 16 base62 chars). Can be used when invoking the REST API.remoteOrderIdstringrequiredOrderAck. Included for convenience; the path parameter is authoritative.eventenumrequiredcancelled (carries reason; modifies the status), driver_assigned (carries driver; can repeat on reassignment; does not modify the status), driver_released (carries releasedDriver, reason, expectedNext; informational, no rollback, does not modify the status), delivered (modifies the status). New values may be added without a bump; on an unknown value, respond 200 OK and ignore.occurredAtISO-8601requiredreasonstring | nulloptionalevent=cancelled (values: customer_requested, operator_action, acceptance_timeout, delivery_failed) or when event=driver_released (value: manual_release). New values may be added without a bump — treat informatively.expectedNextstring | nulloptionalevent=driver_released. Current value: driver_reassignment (BipBip is rotating the driver; the next driver_assigned will carry the new one). New values may be added without a bump — treat informatively.OrderEventDriver
OrderUpdateEvent. It is the type of driver (when event = driver_assigned) and also of releasedDriver (when event = driver_released). Absent on other events. Can be received more than once per order — each time with the data of the currently assigned driver.Fields
fullNamestringrequiredphonestring | nulloptionalnull if the driver has not registered one or marked it private.MerchantErrorResponse
Fields (all optional)
errorstringoptionalinvalid_payload, duplicate_delivery).messagestringoptionaltimestampISO-8601optional