# BipBip Webhook Outbound API Direction: BipBip calls the merchant's POS server. The merchant exposes these endpoints. BipBip calls them. ## Transport Contract All parameters below are fixed and are not negotiable per merchant. | Parameter | Value | |-----------|-------| | Order creation path | `POST {merchantBaseUrl}/v1/order/{remoteId}` | | Order updates path | `PUT {merchantBaseUrl}/v1/order/{remoteId}/{remoteOrderId}/events` | | Menu change path | `POST {merchantBaseUrl}/v1/menu/change/{changeKey}` | | Body format | JSON | | Timeout per attempt | 15 seconds | | ACK for creation | HTTP 200 with `{ "remoteOrderId": "" }` — required | | ACK for order update | HTTP 200 — response body is ignored | | ACK for menu change | HTTP 200 — response body is ignored | | Versioning | In path (`/v1/`) — future versions can coexist in parallel | | Forward compatibility | Tolerate unknown `event` values and unknown top-level fields; respond 200 | Path parameters: - `remoteId`: Store identifier configured during onboarding. BipBip includes it in the URL so the POS can route to the correct store without parsing the body. - `remoteOrderId`: The POS's own order ID, returned in the `200 OK` ACK of the original creation webhook. BipBip stores it and uses it to build the updates URL. - `changeKey`: Change request identifier. Format: `chg_` + 16 base62 characters. ## Signed Headers BipBip sends these headers on webhook requests. The creation and menu-change webhooks carry all five; the order updates endpoint declares only four — `X-Bipbip-Event-Type` is not part of its contract: | Header | Value | |--------|-------| | `X-Bipbip-Signature-256` | HMAC-SHA256 in format `sha256=` | | `X-Bipbip-Timestamp` | Unix epoch seconds (integer) | | `X-Bipbip-Event-Type` | `order.created` for creation; `menu.change.completed.v1` for menu. The spec does **not** declare this header on the order updates endpoint — route on `body.event` there and do not require the header to be present | | `X-Bipbip-Delivery-Id` | UUID, unique per dispatch attempt. Same value across retries. Use for deduplication. | | `X-Bipbip-Schema-Version` | `1.0` | ## HMAC Signature Verification HMAC algorithm: SHA-256. Message to sign: `{timestamp}.{raw-request-body}`, where `{timestamp}` is the value of `X-Bipbip-Timestamp` and `{raw-request-body}` is the exact raw bytes of the HTTP body (do not re-serialize). Verification procedure: 1. Extract `timestamp` from `X-Bipbip-Timestamp`. 2. Reject the request if `abs(now - timestamp) > 300` seconds (5 minutes). This mitigates replay attacks. 3. Construct the signed message: `.`. 4. Compute `HMAC-SHA256(message, sharedSecret)`. 5. Strip the `sha256=` prefix from `X-Bipbip-Signature-256`. 6. Compare the computed digest against the stripped header value using a constant-time comparison function. Do not use string equality. ```javascript // Node.js reference implementation import crypto from 'node:crypto'; app.post('/v1/order/:remoteId', async (req, res) => { const sig = req.headers['x-bipbip-signature-256']; const ts = req.headers['x-bipbip-timestamp']; const raw = req.rawBody; // must be the raw buffer, not re-serialized const expected = 'sha256=' + crypto .createHmac('sha256', process.env.BIPBIP_SECRET) .update(`${ts}.${raw}`) .digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return res.status(401).end(); } const order = JSON.parse(raw); const remoteOrderId = await pos.createOrder(order); res.status(200).json({ remoteOrderId }); }); ``` ## Authentication Strategies (BipBip to Merchant) Configurable per endpoint in the Merchant Portal. Controls whether BipBip adds an additional authentication header beyond the HMAC signature. **Critical rule: all three strategies always include the HMAC signature. The merchant must always validate HMAC regardless of strategy. The strategy only controls whether an additional auth header is added — it never replaces the signature.** | Strategy | What BipBip adds | |----------|-----------------| | `hmac_only` (default) | Only HMAC signature headers. No `Authorization` header. | | `static_headers` | Arbitrary additional HTTP headers (e.g., `X-Api-Key`, `Authorization: Bearer `). Sensitive values are encrypted with AWS KMS and decrypted at dispatch time. Supports 24-hour dual-window rotation: during the rotation window BipBip signs with the new secret but the merchant can accept both. | | `oauth_client_credentials` | BipBip obtains a Bearer token via OAuth 2.0 Client Credentials and injects `Authorization: Bearer `. Token is cached with a 60-second expiry buffer. If the merchant returns 401, BipBip invalidates the cache and retries once with a fresh token. Supports `client_secret_post` and `client_secret_basic`, scopes, and extra parameters. Supports 24-hour dual-window rotation for `client_secret`. | ## Retry Policy BipBip uses at-least-once delivery semantics. Retries are re-queued without exponential backoff — the next attempt arrives within seconds, not minutes. | Trigger | Behavior | |---------|----------| | 5xx or network error | BipBip retries (up to `maxRetries`, default 3 total attempts, configurable per endpoint) | | Timeout > 15s | Treated as a transient failure; counts as one attempt consumed | | 4xx | Terminal — BipBip does not retry | | HTTP 200 without `remoteOrderId` (creation only) | Treated as a failed attempt; BipBip retries | Deduplicate by `X-Bipbip-Delivery-Id`. All retries of the same delivery carry the same UUID. **Exhaustion consequences**: - `order.created` exhaustion: BipBip cancels the order internally (notifies the customer). No update webhooks are sent for that order. - Order update (`PUT /events`) exhaustion: delivery is silently dropped. BipBip does not expose a separate failure event to the POS. - Menu change webhook exhaustion: delivery is marked failed internally. No menu changes are reverted or cancelled. ## Order Update Events All updates arrive at `PUT {merchantBaseUrl}/v1/order/{remoteId}/{remoteOrderId}/events`. Discriminate by `body.event`. **`event` is not the order status.** It says what happened, not what state the resource ended up in. Only `cancelled` and `delivered` have a counterpart in the order status. `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`. Do not use `event` to overwrite the internal order status outside those two cases. Read the resource state with `GET /api/v1/Orders/{orderKey}`; it never takes a driver value. Updates are only sent if the creation webhook for that order was successfully delivered (merchant returned HTTP 200). ### `cancelled` Order has been cancelled. Process: cancel the order in the POS. Modifies the order status. This 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 it. The optional field `reason` is informative. Current values: - `customer_requested`: customer cancelled from the BipBip app. - `operator_action`: BipBip back-office operator cancelled. - `acceptance_timeout`: POS did not respond to the order within the acceptance window. - `delivery_failed`: BipBip exhausted retries delivering the creation webhook. New reason values may be added without a schema version bump. Treat `reason` as informational only. ### `driver_assigned` A driver has been assigned to pick up the order. The `driver` object contains `fullName` (required) and optionally `phone`. **Does not modify the order status.** This event can arrive **more than once** per order lifecycle. Reassignment occurs when a BipBip operator manually reassigns a driver (e.g., due to an incident). The POS must always display the most recently received driver. Do not deduplicate `driver_assigned` by `orderKey + event` — doing so leaves the POS showing a driver who is no longer assigned. Deduplicate by `X-Bipbip-Delivery-Id` instead. The origin of assignment (automatic system, operator manual assignment, or driver self-assignment in self-service cities) does not appear in the webhook payload. The shape is identical in all cases. ### `driver_released` A BipBip operator has manually released the previously assigned driver. This is an **informative event only** — the order remains logically active and its status is unchanged. Do not roll the order back to a prior state. What to do when this event arrives: 1. Clear the driver display from the POS UI. 2. Wait for the next `driver_assigned` event with the replacement driver. Additional fields present in this event: - `releasedDriver`: identity of the released driver (same shape as `driver`: `fullName` required, `phone` optional). - `reason`: currently always `manual_release`. - `expectedNext`: hint for the next expected event, currently always `driver_reassignment`. ### `delivered` Order has been delivered to the customer. Mark the order as delivered in the POS. Modifies the order status. Note that a `cancelled` event can still arrive afterwards. The webhook is not the only way to learn this. The REST order carries `deliveredAt` (both in `GET /api/v1/Orders/{orderKey}` and in the listing), so a POS that prefers polling can resolve delivery without depending on this event. `status` alone cannot: an order in transit and a delivered one both read `HandedOver`. See rest-api.md. ## Order Lifecycle (for reference) Two tracks run in parallel. The status the merchant reports advances linearly: ``` pending → accepted → preparing → ready → handed_over ``` Driver assignment is **not a step inside that cycle** — it runs alongside it and does not modify it. BipBip starts looking for a driver while the order is still in `preparing`: ``` driver_assigned ↓ driver_released (informative, no rollback) ↓ driver_assigned (new driver — reassignment) ↓ delivered ``` The `driver_assigned` → `driver_released` → `driver_assigned` sequence can repeat multiple times before final delivery. Practical consequence: `driver_assigned` can arrive at any point before `handed_over`, even before the merchant reports `ready`. The POS must not assume a fixed order between `ready` and `driver_assigned`. ## Menu Change Webhook BipBip calls `POST {merchantBaseUrl}/v1/menu/change/{changeKey}` after finishing application of a menu change. Event type: `X-Bipbip-Event-Type: menu.change.completed.v1`. This webhook fires in two cases: 1. The merchant requested the change via the REST API (`/api/v1/menu/...`). The `changeKey` matches the `changeId` returned in the original `202 Accepted` response. 2. The restaurant changed availability directly (not via the integration API). BipBip mints the `changeKey` automatically. There is no prior `202 Accepted` to correlate against. The payload and handling procedure are identical in both cases. This is a **result notification with no side effects**. Retry exhaustion marks the delivery as failed internally but does not revert, cancel, or modify any menu change or order. Applied changes remain applied. The merchant can query the change status via `GET /api/v1/menu/changes/{changeKey}`. Handling procedure: 1. Validate the HMAC signature. 2. Deduplicate by `X-Bipbip-Delivery-Id`. 3. Reconcile `results` per store with internal POS state (optional, informational only). 4. Respond `200 OK`. Note on `modifierOption` store-level overrides: when a change confirms `price` or `name` on a `modifierOption` at the store level with `applied: true`, BipBip has persisted the override — but the BipBip customer app does not yet reflect it to end customers (deferred integration in `BipBipAPICliente`). The REST API marks these feature combinations with `pendingCustomerRollout: true` in `GET /api/v1/menu/capabilities`. For schema details, see `schemas.md`.