Inbound · Called from your POS

REST API Reference

Your POS calls BipBip. The API has two surfaces: Orders, to manage the lifecycle of what BipBip delivers to you via webhook, and Menu, to keep your catalog in sync — availability, prices and the brand's full menu. It is the counterpart of theWebhook Spec.

SurfacePrefixWhat it does
Orders/api/v1/OrdersAccept, reject, advance status, get and list.
Menu/api/v1/menuAvailability, prices, per-entity edits and full catalog publishing.

Base URLs per environment

Production: https://merchant-api.bipbip.hn. Staging: https://merchant-api.bipbip.dev. Routes are prefixed with /api/v1/, versioned in the path.

Path casing

The contract publishes /api/v1/Orderswith a capital O and /api/v1/menuin lowercase. Use the exact casing documented on each endpoint below.

No public IP allowlist

BipBip does not publish a fixed IP allowlist in either direction — both outbound calls to BipBip and inbound webhooks travel over public TCP/443 with TLS. If your security policy requires a static allowlist or a private network path (e.g. AWS PrivateLink), contact [email protected] to arrange a custom setup for your tenant.

Authentication

Send theX-Bipbip-Api-Keyheader on every request, with the value the BipBip team gives you during onboarding. Each integrated merchant gets its own API Key, which identifies the client and scopes everything the API returns — orders, stores and brand.Mutations (POST/PUT/PATCH) additionally requireIdempotency-Key andX-Bipbip-Schema-Version(currently 1.0). To get your API Keys, email [email protected].

# Read — X-Bipbip-Api-Key only
curl -H "X-Bipbip-Api-Key: <api-key>" \
     https://merchant-api.bipbip.hn/api/v1/Orders/<orderKey>

# Mutation — add Idempotency-Key and X-Bipbip-Schema-Version
curl -X POST \
     -H "X-Bipbip-Api-Key: <api-key>" \
     -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
     -H "X-Bipbip-Schema-Version: 1.0" \
     -H "Content-Type: application/json" \
     -d '{}' \
     https://merchant-api.bipbip.hn/api/v1/Orders/<orderKey>/accept

Protecting the API Key

Never expose the API Key in the frontend, in public repositories or in client bundles. Keep it in server-side environment variables and never commit it.

Idempotency-Key

Every mutation — Orders and Menu alike — requires theIdempotency-Keyheader (UUID v4, unique per logical attempt) along withX-Bipbip-Schema-Version: 1.0. Responses are cached server-side for 24 hours.

  • Same key + same body within 24h → BipBip returns the original response without re-executing.
  • Same key + different body → HTTP 422 with a type ending in /idempotency-conflict.
  • New key → request processed normally.
  • Missing Idempotency-Key → HTTP 422.
  • Missing or unsupported X-Bipbip-Schema-Version → HTTP 400.

When to reuse the key and when to mint a new one

Reuse the same key when retrying the same logical attempt: timeout, 5xx, dropped connection. The server guarantees a single side effect. Mint a new key when a different logical attempt starts — for example, after a manual operator action.
{
  "type": "https://bipbip.app/probs/idempotency-conflict",
  "title": "Idempotency conflict",
  "detail": "Idempotency-Key reused with a different request body.",
  "status": 422,
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
  "meta": null
}

Rate limits

Limits apply per API Key and are active on every endpoint. When you exceed them, BipBip returns HTTP 429 with aRetry-Afterheader — the fix is exponential backoff with jitter on the client.

WindowLimitStrategy
Fixed window100 requests / minResets every 60s
Sliding window1,000 requests / hourEvaluated continuously

Errors

Every error response follows RFC 7807 Problem Details, with the fieldstype,title,detail,status,traceId andmeta. What meta carries depends on the error: transition 409s bringcurrentStatus and allowedTransitions[]; validation 422s bring errors, a map from field path to the list of rules it broke.

Discriminate on the type suffix, not on detail

detail is human-facing text and may change between versions. Thetype suffix is stable. Match withtype.endsWith('/invalid-state-transition') so you don't couple to the host, which varies across environments.
StatusCauseWhat to do
400Missing X-Bipbip-Schema-Version, or the value is not supported.Add the header with value 1.0 to every mutation.
401API Key missing, invalid or revoked.Check X-Bipbip-Api-Key. If it was revoked, contact support.
404The resource does not exist or does not belong to the authenticated client (order, store, menu code).Confirm the exact identifier received via webhook or read.
409Transition forbidden by the state machine, acceptance window expired, or bulk deactivation blocked on PUT /menu.Read meta.allowedTransitions[] and pivot. Empty list = terminal state, do not retry.
422Invalid body, missing Idempotency-Key, idempotency conflict, or a menu combo that is not shipped.Inspect meta.errors. Do not retry without fixing the body.
429Quota exceeded for the API Key.Respect Retry-After and retry with backoff and jitter.
502 / 503BackOffice did not respond or is unavailable. Only on the menu endpoints that delegate to it.Retry with the same Idempotency-Key and backoff.

Generic error

{
  "type": "https://bipbip.app/probs/not-found",
  "title": "Order not found",
  "detail": "Order ord_xxx does not exist or does not belong to the authenticated client.",
  "status": 404,
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
  "meta": null
}

Invalid transition (409)

{
  "type": "https://bipbip.app/probs/invalid-state-transition",
  "title": "Invalid state transition",
  "detail": "Cannot transition from 'Preparing' to 'HandedOver'.",
  "status": 409,
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
  "meta": {
    "currentStatus": "Preparing",
    "allowedTransitions": [
      "Ready"
    ]
  }
}

Validation error (422)

{
  "type": "https://bipbip.app/probs/validation",
  "title": "Validation failed",
  "detail": "One or more request fields are invalid.",
  "status": 422,
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
  "meta": {
    "errors": {
      "items[0].targetCode": [
        "The targetCode field is required."
      ]
    }
  }
}

Orders

Lifecycle of the orders BipBip delivers to you via webhook. Mutations requireX-Bipbip-Api-Key,X-Bipbip-Schema-Version andIdempotency-Key.

Statuses are read in PascalCase and written in snake_case

Reads (GET and the status field of every response) return Pending, Accepted,Preparing, Ready,HandedOver,Rejected, Cancelled. The PUT /status body, in contrast, acceptspreparing, ready andhanded_over. They are not the same vocabulary: do not compare thestatus you read against the one you write without normalizing.
POST/api/v1/Orders/{orderKey}/accept

Accept an order

Runs the atomic cascade Pending → Accepted → Preparing in a single operation. The order is never observable in Accepted from your POS: the response status is Preparing.

The acceptance window expires even while the order still reads Pending

BipBip validates the call against the expiresAt it published in the creation webhook. Past that instant, /accept returns409 with a type ending in/acceptance-timeout — even if GETstill shows the order as Pending. Readorder.expiresAt and don't accept out of window.

Key behavior

  • The response carries status: "Preparing" directly — Accepted is a non-observable intermediate step.
  • After /accept, do not call PUT /status with preparing: it returns 409.
  • Your POS's next action is ready when the order is done.
  • For auto-accept stores, BipBip runs the same cascade when it receives your webhook 200; expiresAt arrives as null.
  • Publishes the order.accepted_by_external_merchant.v1 event. The internal cascade to Preparing emits no extra event.

Path parameters

orderKeystringrequired
Opaque order identifier delivered by BipBip in the order.created webhook (format ord_ + 16 base62 characters).

Headers

X-Bipbip-Api-Keystringrequired
API Key issued by BipBip during onboarding.
X-Bipbip-Schema-Versionstringrequired
Schema version. Currently 1.0. Without it → 400.
Idempotency-KeyUUID v4required
Unique per logical attempt. Same key + same body within 24h → cached response. Same key + different body → 422.

Body

(empty)object
v1.0 — empty body: send a literal {}. The remoteOrderId was already bound when you replied to the order.created webhook, so do not include it here.

Returns — 202 Accepted

messagestring
Informational message. Its text may change without notice: for business logic use the HTTP status and data.
data.orderKeystring
Opaque order identifier (same as the path).
data.statusstring
Resulting status: "Preparing".
data.acceptedAtISO 8601
When the acceptance was recorded, per BipBip's clock.
data.remoteOrderIdstring | null
Your POS reference, already bound. It travels in every later webhook for this order.
POST/api/v1/Orders/{orderKey}/reject

Reject an order

Moves the order from Pending to Rejected (terminal state), persists the structured reason and cancels the acceptance timeout job.

An unknown reasonCode does not fail — it is normalized

If you send a value outside the catalog, the API does not return 422 — it normalizes it toSYSTEM_ISSUE and returns that indata.reason. A typo in the code does not surface as an error, it surfaces as a misclassified rejection metric. Compare it against what you sent.

Path parameters

orderKeystringrequired
Opaque order identifier.

Headers

X-Bipbip-Api-Keystringrequired
Merchant API Key.
X-Bipbip-Schema-Versionstringrequired
1.0
Idempotency-KeyUUID v4required
Unique per logical attempt.

Body

reasonCodeenumrequired
Rejection reason. Closed enum: STORE_CLOSED, OUT_OF_OPERATING_HOURS, ITEM_NOT_OFFERED, ITEM_OUT_OF_STOCK, PRICE_MISMATCH, SYSTEM_ISSUE.
messagestringoptional
Free-text detail for support. Not shown to the end customer.

Returns — 202 Accepted

data.orderKeystring
Opaque order identifier.
data.statusstring
Always "Rejected", which is terminal: the order accepts no further transitions.
data.rejectedAtISO 8601
When the rejection was recorded, per BipBip's clock.
data.reasonstring | null
The reasonCode that was applied. It may differ from what you sent if it fell outside the catalog.
PUT/api/v1/Orders/{orderKey}/status

Advance status

Advances an already-accepted order to the next valid state: preparing → ready → handed_over. Forward transitions only.

Expected transition flow

After /accept the order is already in Preparing. From there:

  • preparing → ready
  • ready → handed_over

preparing remains in the enum for compatibility, but calling it after/accept returns 409.

Post-acceptance cancellations are not exposed in v1.0

The enum does not accept cancelled. Cancellations after acceptance originate inside BipBip — customer, operator, timeout or failed webhook — and reach your POS through thecancelled event of the PUT /events webhook. To cancel an already-accepted order, email[email protected].

Path parameters

orderKeystringrequired
Opaque order identifier.

Headers

X-Bipbip-Api-Keystringrequired
Merchant API Key.
X-Bipbip-Schema-Versionstringrequired
1.0
Idempotency-KeyUUID v4required
Unique per logical attempt.

Body

statusenumrequired
Target status in snake_case: preparing, ready or handed_over. A transition that is not allowed returns 409 with the valid targets in meta.allowedTransitions[].

ready does not depend on you alone: the driver can also mark it ready from their app, and that record advances the order. A 409 with meta.currentStatus: "Ready" is not an error — it is a state already reached. Continue with handed_over.
occurredAtISO 8601optional
When the change actually happened in your system — useful if you queue events and send them late. If omitted, BipBip uses its own clock. When supplied by the merchant, it is clamped to [now−24h, now+5min].

Returns — 202 Accepted

data.orderKeystring
Opaque order identifier.
data.statusstring
Resulting status after applying the transition.
data.changedAtISO 8601
When BipBip recorded the change. It may differ from the occurredAt you sent.
GET/api/v1/Orders/{orderKey}

Get an order

Returns the current status, the timestamp of every transition, the change history and the full commercial content. Read-only: no Idempotency-Key required.

Use it to recover a webhook you could not process

The data.order object has the same shape as theorder.created webhook payload: items,payment, summary,charges, discounts andinvoice. The same parser serves both channels — if a webhook was dropped, you rebuild it from here with no new code.

Path parameters

orderKeystringrequired
Opaque order identifier.

Headers

X-Bipbip-Api-Keystringrequired
Merchant API Key.

Returns — 200 OK

data.orderKeystring
Public, opaque order identifier. It is the only id that travels in URLs and payloads.
data.storeIdinteger
BipBip's internal store id. To address a store, use your own storeRemoteId.
data.brandIdinteger
BipBip's internal id for the brand that owns the store.
data.statusenum
Current status in PascalCase: Pending, Accepted, Preparing, Ready, HandedOver, Rejected or Cancelled.

The driver is not a status. Whether one is assigned is told by driverAssignedAt, not by status.
data.merchantStatusReasonstring | null
Reason for the last status change, when whoever triggered it declared one.
data.remoteOrderIdstring | null
The order reference in your POS.
data.receivedAtISO 8601
When BipBip received the order. It anchors the acceptance window.
data.acceptedAt · rejectedAt · preparingAt · readyAtISO 8601 | null
Timestamps of each merchant transition. preparingAt usually matches acceptedAt because of the accept cascade.
data.driverAssignedAtISO 8601 | null
When a driver was assigned. It is cleared if the driver is released and the order goes back to pending reassignment.
data.handedOverAtISO 8601 | null
When you released the food: to the driver on delivery, to the customer on pickup. That is where your progress ends.

It can be null with the order already in HandedOver: that happens when you never reported the handover and BipBip closed the order once delivery was confirmed. In that case nobody knows when you released it. Do not use this field to detect that the order left — use status for that.
data.deliveredAtISO 8601 | null
Since when the customer has the order. On orders created from August 12, 2026 onwards, null = no confirmed delivery yet. Earlier ones return null permanently, even if they were delivered: the field shipped without backfilling the earlier rows. If you reconcile a period spanning that date, do not read that null as “not delivered”.

This is the field that tells “the driver picked it up, it is on its way” apart from “it arrived”: both look like HandedOver, because your progress as a merchant ends when you release the food. On delivery the driver confirms it and it is later than handedOverAt; on pickup it matches it, because you handed the order to the customer yourself.
data.cancelledAtISO 8601 | null
When it was cancelled. It can be populated after a delivery, because of a refund or an incident.
data.history[]array
Chronological trace of status changes, with the actor behind each one.
fromStatusstring | null
Previous status. null on the first item, when the order was created.
toStatusstring
Status it moved to.
changedAtISO 8601
When the change happened.
actorTypeenum
Who triggered it: merchant (you, through the API), customer, operator (BipBip operator), driver or system (acceptance timeout, delivery failure).
actorIdstring | null
Actor identifier where applicable. null for system.
reasonstring | null
Declared reason. Free text: do not parse it.
driverobject | null
Driver behind the transition. Present only on driver changes. This — not reason — is where you read it.
eventenum
driver_assigned (there is a driver), driver_reassigned (replaced, there is a new one) or driver_released (there is no driver anymore, wait for a reassignment).
codestring | null
Driver identifier, the same one BipBip uses when talking about them with people (e.g. BIP-01424). Use it to refer to a specific driver when contacting support. Falls back to the internal id when the driver has no code on file, and on rows predating the code travelling in this payload. Treat it as opaque: it is a label, not a number.
fullNamestring | null
Driver name.
previousCodestring | null
Who they replace, by the same criteria. Only on driver_reassigned.
data.orderobject | null
Commercial content of the order. Same shape as the order.created webhook. null only if the original snapshot could not be read.
displayCodestring
Human-readable order code, for support.
storeRemoteIdstring
The store code in your POS, the one you configured in BipBip.
currencystring
ISO 4217 currency for every amount.
createdAtISO 8601
When the order was created in BipBip.
expiresAtISO 8601 | null
Acceptance deadline. null for auto-accept stores.
fulfillmentobject
type (delivery | pickup), isExpress, prepareBy and, depending on the type, driverPickupAt or customerPickupAt.
customerobject | null
Customer name. Only on pickup, to call them at the counter; null on delivery for PII minimization.
paymentobject
methods[] (supports combining cash, card and bips), amountToCollect and changeFor.
summaryobject
Aggregate totals for reconciliation: subtotal, taxes, discounts, additionalCharges, grandTotal.
charges[] · discounts[]array
Itemized charges and discounts, each declaring who bills it (billedBy) or who absorbs it (fundedBy).
items[]array
Ordered products with code, remoteCode, quantity, unitPrice, tax, lineTotal, note and their modifierGroups[].
customerNotestring | null
Customer note for the whole order.
invoiceobject | null
taxId and businessName if the customer requested a tax invoice; null for final consumers.

Driver release — how it looks in history

The driver does not move the merchant's status, so assign, reassign and release all produce rows wherefromStatus == toStatus — the order stays where it was. Without looking at driver.event, a release looks like an assignment and you would believe the order still has a driver.

[
  {
    "fromStatus": "Ready",
    "toStatus": "Ready",
    "changedAt": "2026-08-07T17:20:10Z",
    "actorType": "operator",
    "actorId": "Daniel",
    "reason": "Released: driver BIP-01424 (manual_release)",
    "driver": {
      "event": "driver_released",
      "code": "BIP-01424",
      "fullName": "Juan Pérez"
    }
  },
  {
    "fromStatus": "Ready",
    "toStatus": "Ready",
    "changedAt": "2026-08-07T17:18:02Z",
    "actorType": "operator",
    "actorId": "Daniel",
    "reason": "Driver BIP-01424 asignado manualmente por operador Daniel",
    "driver": {
      "event": "driver_assigned",
      "code": "BIP-01424",
      "fullName": "Juan Pérez"
    }
  }
]
GET/api/v1/Orders

List orders

Lists the authenticated client's orders, sorted by order number descending, with opaque cursor pagination.

Headers

X-Bipbip-Api-Keystringrequired
Merchant API Key.

Query parameters

Cursorstringoptional
Opaque cursor returned in nextCursor by the previous response. Do not interpret or hand-build it.
PageSizeintegeroptional
Page size. Maximum 100, default 20.
Statusstringoptional
Filter by status, in the same vocabulary reads return: Pending, Accepted, Preparing, Ready, HandedOver, Rejected, Cancelled. There is no driver status to filter on.
FromDateISO 8601optional
Start of the date range.
ToDateISO 8601optional
End of the date range.

Returns — 200 OK

data.items[]array
Page of results, newest first.
orderKeystring
Opaque identifier. Use it in GET /api/v1/Orders/{orderKey} for the detail.
storeIdinteger
BipBip's internal store id.
statusenum
Current status, in PascalCase.
receivedAtISO 8601
When BipBip received the order. It is not the listing's sort criterion: it can differ from the real creation order, so the listing sorts by order number and the cursor is that same number.
acceptedAtISO 8601 | null
When you accepted it. null if it is still pending or you rejected it.
remoteOrderIdstring | null
The order reference in your POS.
driverAssignedAtISO 8601 | null
When a driver was assigned. null if there is none yet or the driver was released.
deliveredAtISO 8601 | null
Since when the customer has the order. Together with status it tells an in-transit order apart from a delivered one without fetching the detail. See the order detail for the full semantics.
data.nextCursorstring | null
Opaque cursor for the next page. null on the last page.
data.hasMoreboolean
The stop condition for your pagination loop.

State machine — what the merchant transitions

From the REST API the merchant triggers these transitions:PendingPreparing (via /accept, which passes through Accepted automatically) orRejected (via /reject);PreparingReadyHandedOver (via PUT /status).

The Cancelled state is set by BipBip and reaches your POS via webhook — the merchant does NOT transition it from the REST API.

Driver assignment is not a status. BipBip notifies it with thedriver_assigned event of thePUT /events webhook and records it indriverAssignedAt, but the order'sstatus does not move: it stays where it was until you advance it. The driver cycle runs in parallel, so that event can arrive even while the order is inPreparing, before you reportready.