First steps

Quickstart

This guide covers the path from "credentials in hand" to "first order received and confirmed" without any intervention from the BipBip team. The four steps follow the order shown.

Sandbox in progress — direct integration against production

The test environment (sandbox) is under construction. In the meantime, the pilot integrates directly against production with coordination from the BipBip team. Coordination prior to sending test orders is handled through[email protected].

Prerequisites

Before getting started, the BipBip team provides the following configuration items. All are required before writing the first line of code:

  • 1
    HMAC Secret — shared key for verifying webhook authenticity (one per account)
  • 2
    API Key (X-Bipbip-Api-Key) — authentication header for calling the REST API
  • 3
    remoteId — store identifier defined by the merchant (e.g. POS_TGU_001). One per registered store.
  • 4
    Registered base URL — the base URL of the POS server where BipBip will send webhooks (must be publicly accessible)

The 4 steps

  1. 1

    Implementing the webhook endpoint

    BipBip sends POST {baseUrl}/v1/order/{remoteId}each time a new order arrives for the store. The POS endpoint must:

    • Capture the raw body before parsing the JSON
    • Verify the HMAC signature (see HMAC Verification)
    • Return HTTP 200 with a JSON body that includes remoteOrderId
    • Respond within 30 seconds (BipBip treats any slow response as a failure)

    remoteOrderId is required

    Returning HTTP 200 without a valid remoteOrderIdin the body is treated as a failed delivery and BipBip will retry. SeeBipBip keeps retrying.
  2. 2

    Verifying the HMAC signature

    Every request from BipBip includes the headerX-Bipbip-Signature-256with an HMAC-SHA256 signature. Verifying the signature guarantees that the request originates from BipBip and was not modified in transit. The HMAC Verification section contains code samples.

  3. 3

    Responding with the POS remoteOrderId

    Once the signature is verified and the order is created in the POS, the HTTP 200 response includes:

    {
      "remoteOrderId": "POS-2026-04-11-00142"
    }

    This value is the POS internal identifier for this order. BipBip stores it and includes it in all subsequent cancellation webhooks to enable correlation.

  4. 4

    Accepting the order via the REST API

    After receiving the webhook and responding with 200, formal order acceptance happens viaPOST /api/v1/Orders/{orderKey}/accept. That call executes an atomic cascade that transitions the order from pending → preparing(passing internally through accepted) in a single operation. The response returns status: "preparing" directly — do NOT call PUT /status with preparing afterwards; the next merchant action is to mark the order ready when it is.

    This step is optional if the store has auto-accept enabled — in that case BipBip runs the same cascade automatically upon receiving the 200, and the order arrives directly at preparing without the merchant invoking /accept.

    // Accept an order via the BipBip REST API — Node.js (Quickstart Step 4)
    // Call POST /api/v1/Orders/{orderKey}/accept after receiving and verifying the webhook.
    // Requires Node.js 18+ (native fetch). No npm packages required.
    //
    // In v1.0 this call executes an atomic cascade: pending → accepted → preparing.
    // The response returns status: "preparing" directly. Do NOT call PUT /status with
    // "preparing" afterwards — the next merchant action is to mark the order ready.
    
    const API_BASE_URL = 'https://merchant-api.bipbip.hn'; // Staging: https://merchant-api.bipbip.dev
    const API_KEY      = process.env.BIPBIP_API_KEY;       // X-Bipbip-Api-Key header value
    
    /**
     * Accepts a BipBip order. The body is empty in v1.0 — the remoteOrderId was already
     * linked when the POS responded 200 to the order.created webhook.
     *
     * @param {string} orderKey       - The opaque BipBip order key (format: "ord_" + 16 base62 chars)
     * @param {string} idempotencyKey - A unique UUID v4 for this request (reuse to safely retry)
     * @returns {Promise<object>}     - { message, data: { orderKey, status: "preparing", acceptedAt, preparingAt, remoteOrderId } }
     */
    async function acceptOrder(orderKey, idempotencyKey) {
      const url = `${API_BASE_URL}/api/v1/Orders/${encodeURIComponent(orderKey)}/accept`;
    
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type':            'application/json',
          'X-Bipbip-Api-Key':        API_KEY,
          'X-Bipbip-Schema-Version': '1.0',
          'Idempotency-Key':         idempotencyKey, // Required on all mutations — enables safe retry
        },
        body: '{}', // Empty body in v1.0
      });
    
      if (!response.ok) {
        const error = await response.json().catch(() => ({}));
        throw new Error(`Accept failed: ${response.status} — ${JSON.stringify(error)}`);
      }
    
      return response.json();
    }
    
    // ── Usage example ─────────────────────────────────────────────────────────────
    // Inside the webhook handler (after HMAC verification):
    //
    // const { randomUUID } = require('crypto');
    //
    // async function handleOrderWebhook(req, res) {
    //   const order = JSON.parse(req.body.toString('utf8'));
    //
    //   // Step 1: Create the order in the POS system and obtain the internal ID
    //   const remoteOrderId = await pos.createOrder(order);
    //
    //   // Step 2: Respond 200 with remoteOrderId — this links the POS ID to the order
    //   res.status(200).json({ remoteOrderId });
    //
    //   // Step 3: Accept the order on BipBip (use a stable UUID per attempt).
    //   // Skip this step if the store has auto-accept enabled — BipBip runs the cascade itself.
    //   const idempotencyKey = randomUUID();
    //   await acceptOrder(order.orderKey, idempotencyKey);
    // }
    
    module.exports = { acceptOrder };

    REST API rate limits

    The API is limited to 100 requests per minute (fixed window) and1,000 requests per hour (sliding window), partitioned by API Key. When the limit is exceeded BipBip returns HTTP 429 — the solution is exponential backoff with jitter in the client.

Sandbox — coming soon

Test environment: coming soon

BipBip does not have a sandbox environment at this time. The pilot integrates directly against production in coordination with the team. There are no isolated test URLs, API keys, or test orders available yet. When the sandbox becomes available, this section will be updated with the corresponding instructions.

Security

HMAC Verification

Every webhook BipBip sends includes an HMAC-SHA256 signature in the headerX-Bipbip-Signature-256. Verifying this signature is mandatory — without it, any malicious actor can send fake orders to the endpoint.

The algorithm

BipBip signs each request using the following formula:

message   = "{timestamp}.{rawBody}"
keyBytes  = UTF-8 bytes of the HMAC secret
signature = "sha256=" + LOWERCASE(HEX(HMAC-SHA256(keyBytes, UTF8(message))))

The relevant headers in each request are:

  • X-Bipbip-Timestamp — Unix timestamp in seconds (integer as string)
  • X-Bipbip-Signature-256 — the signature in sha256=<hex> format
  • X-Bipbip-Delivery-Id — unique UUID per delivery batch (use for deduplication)
  • X-Bipbip-Event-Type — only order.created on the creation webhook. Subsequent updates (cancelled, driver_assigned, driver_released, delivered) go to PUT /v1/order/{remoteId}/{remoteOrderId}/events and are discriminated by body.event (that endpoint does not declare this header). See the endpoint specdriver_assigned can arrive more than once per order.

Recommended clock skew: maximum 300 seconds

The difference between X-Bipbip-Timestampand the local server clock should not exceed 300 seconds (5 minutes). This protects against replay attacks where a valid request is captured and resent hours later.

Common pitfalls (footguns)

These three mistakes account for 90% of cases where the signature does not verify. Review them before looking for another problem.

Footgun 1: signing the re-serialized JSON instead of the raw body

The most frequent mistake: parsing the body with JSON.parse()first and then signing the re-serialized object. Any difference in whitespace, key ordering, or numeric precision produces a different signature than BipBip's.

Solution: capture the raw bytes of the body before calling any JSON parsing function. Verify the signature. Only then parse.

Footgun 2: string comparison without timing-safe equality

Comparing the computed signature with the received one using ===,== orstrcmp()introduces a timing oracle vulnerability: an attacker can measure response time to deduce characters of the valid signature one by one.

Solution: always use a constant-time comparison function:crypto.timingSafeEqual() in Node.js,hmac.compare_digest() in Python,CryptographicOperations.FixedTimeEquals() in C#,hash_equals() in PHP.

Footgun 3: generating the timestamp locally instead of reading the header

The signature includes the timestamp that BipBip wrote in X-Bipbip-Timestamp. Using Date.now(),time() orDateTime.UtcNowto build the message produces a timestamp that differs from BipBip's, and the signature never verifies.

Solution: always read the timestamp from the X-Bipbip-Timestamp header. Generating it locally produces a mismatch.

Code samples

Selection by language. All samples use only the standard library — no external dependencies required.

// HMAC-SHA256 webhook signature verification — Node.js
// Verify that the webhook payload from BipBip is authentic before processing it.
// Requires Node.js 18+ (native fetch not needed here; only built-in crypto module).

const crypto = require('crypto');

/**
 * Verifies the HMAC-SHA256 signature of an incoming BipBip webhook.
 *
 * @param {string} secret         - HMAC secret provided by BipBip during onboarding
 * @param {string} timestamp      - Value of the X-Bipbip-Timestamp header (Unix seconds as string)
 * @param {Buffer|string} rawBody - Raw request body bytes BEFORE any JSON.parse() call
 * @param {string} signature      - Value of the X-Bipbip-Signature-256 header (e.g. "sha256=abc123...")
 * @returns {boolean}             - true if the signature is valid and the timestamp is within skew limit
 */
function verifyBipBipSignature(secret, timestamp, rawBody, signature) {
  // Step 1: Validate timestamp to prevent replay attacks.
  // Reject requests where the clock skew exceeds 300 seconds (5 minutes).
  const now = Math.floor(Date.now() / 1000);
  const ts = parseInt(timestamp, 10);
  if (Math.abs(now - ts) > 300) {
    return false;
  }

  // Step 2: Build the signed message exactly as BipBip does:
  //   message = "{timestamp}.{rawBody}"
  // IMPORTANT: rawBody must be the original bytes received over the wire.
  // Do NOT re-serialize a parsed JSON object — any whitespace/key-order
  // difference will produce a different signature.
  const message = `${timestamp}.${rawBody}`;

  // Step 3: Compute HMAC-SHA256 with the shared secret.
  // Both key and message are treated as UTF-8.
  // The digest is lowercased hex (BipBip never uses base64).
  const computed = crypto
    .createHmac('sha256', secret)
    .update(message, 'utf8')
    .digest('hex');

  // Step 4: Prepend the "sha256=" prefix to match the header value format.
  const expected = `sha256=${computed}`;

  // Step 5: Use a timing-safe comparison to prevent timing-oracle attacks.
  // crypto.timingSafeEqual requires two Buffers of equal length.
  const expectedBuf = Buffer.from(expected, 'utf8');
  const receivedBuf = Buffer.from(signature, 'utf8');
  if (expectedBuf.length !== receivedBuf.length) {
    return false;
  }
  return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}

// ── Express.js integration example ──────────────────────────────────────────
// Inside an Express app, use express.raw() (not express.json()) so that the
// raw body bytes are available for signature verification.
//
// app.use('/v1/order/:remoteId', express.raw({ type: '*/*' }), (req, res) => {
//   const secret    = process.env.BIPBIP_HMAC_SECRET;
//   const timestamp = req.headers['x-bipbip-timestamp'];
//   const signature = req.headers['x-bipbip-signature-256'];
//   const rawBody   = req.body; // Buffer when using express.raw()
//
//   if (!verifyBipBipSignature(secret, timestamp, rawBody, signature)) {
//     return res.status(401).json({ error: 'Invalid signature' });
//   }
//
//   const order = JSON.parse(rawBody.toString('utf8'));
//   const remoteOrderId = generateInternalOrderId(order);
//   res.status(200).json({ remoteOrderId });
// });

module.exports = { verifyBipBipSignature };

Support

Troubleshooting

The four most frequently asked questions during integration. If the answer is not here, the contact is [email protected].

Signature does not verify

Symptom: the POS code computes the HMAC but the calculated signature never matches X-Bipbip-Signature-256.

Most likely cause: the signature is being applied to the re-serialized JSON instead of the raw body as it arrived over the wire.

How to fix:

  1. Verify that the raw bytes of the body are captured before any call to JSON.parse(), json_decode() or equivalent.
  2. Confirm that the signed message is exactly "{timestamp}.{rawBody}" — the timestamp comes from the header, not the local clock.
  3. Confirm the use of constant-time comparison (see Common pitfalls).
  4. If the problem persists, log the exact message being signed and compare it byte by byte.

Webhook arrives twice

Symptom: the POS creates the same order twice, or receives two webhooks for the same event.

Cause: BipBip delivers webhooks with at-least-once guarantee. If the POS endpoint takes too long to respond or a network error occurs, BipBip retries the delivery. This is expected behavior, not a bug.

How to fix: implement deduplication using the X-Bipbip-Delivery-Id header. This header is a unique UUID per delivery batch — when the ID has already been processed, the response is HTTP 200 immediately without reprocessing.

// Example: deduplicación con X-Bipbip-Delivery-Id
const processed = new Set();

app.post('/v1/order/:remoteId', async (req, res) => {
  const deliveryId = req.headers['x-bipbip-delivery-id'];

  if (processed.has(deliveryId)) {
    return res.status(200).json({ remoteOrderId: yourStore.getByDeliveryId(deliveryId) });
  }

  // ... verificar HMAC, procesar orden ...
  processed.add(deliveryId);
  res.status(200).json({ remoteOrderId });
});

BipBip keeps retrying after the 200

Symptom: the endpoint returns HTTP 200 but BipBip keeps sending the same webhook.

Cause: returning HTTP 200 without a valid remoteOrderId is treated as a failed delivery. BipBip needs that value to compose the URL for future cancellation webhooks.

How to fix: ensure the response body is valid JSON with a non-null, non-empty remoteOrderId:

// Correcto — BipBip marca el delivery como exitoso
{ "remoteOrderId": "POS-INTERNAL-12345" }

// Incorrecto — BipBip trata esto como delivery fallido y reintenta
{}
{ "remoteOrderId": null }
{ "remoteOrderId": "" }

Retries exhausted

If BipBip exhausts all retries without success, the order is automatically cancelled and a cancelled event is sent to the endpoint.

No webhooks arriving

Symptom: BipBip confirms it dispatched the webhook but the POS server received nothing.

Checklist:

  1. Publicly accessible URL: the registered base URL must be reachable from the internet. The test runs with curl -X POST https://server.com/v1/order/test from an external network. localhost or private VPN URLs do not work without tunneling (e.g. ngrok).
  2. Firewall and allowlist: when the POS server has inbound firewall rules, HTTPS traffic from BipBip's IP ranges must be allowed (the exact range is provided by the team).
  3. Header inspection: when the request arrives but is not processed, log all incoming headers. Verify that X-Bipbip-Signature-256 and X-Bipbip-Timestamp are present.
  4. BackOffice status: the BipBip team verifies whether the delivery is Pending, Delivered or Failed.

Reference

Glossary

Six terms that appear throughout the integration contract. They serve as reference when theWebhook Spec or theREST API mention an unfamiliar term.

orderKey
BipBip's public opaque identifier for an order. Format: ord_ prefix followed by 16 base62 characters (e.g. ord_4xK9mZqPwRtN2aLb).
Usage: the only order identifier BipBip exposes externally. Used as a URL segment in REST endpoints (/api/v1/Orders/{orderKey}/accept). It must not be used internally — internal correlation uses theremoteOrderId from the POS.
remoteOrderId
The POS internal order identifier. Returned in the body of the ACK to the creation webhook (HTTP 200) and persisted by BipBip. Required field.
Usage: BipBip includes it in the URL of all subsequent update webhooks (/v1/order/{remoteId}/{remoteOrderId}/events) to enable correlation without looking up by orderKey.
remoteId
Store identifier defined by the merchant (e.g. POS_TGU_001,plaza-pedregal-42). Configured once during onboarding, one per registered store.
Usage: appears as {remoteId} in the path of incoming webhooks. Allows distinguishing which store each order comes from when operating multiple stores with the same base endpoint.
Idempotency-Key
Header sent by the POS when calling mutation endpoints on the REST API (/accept, /reject, /status). Value: any string unique per logical attempt (recommended: UUID v4).
Usage: when the same Idempotency-Key is sent with the same body within 24 hours, BipBip returns the cached response without re-executing the mutation — enables safe retries without duplicate side effects. The same key with a different body returns HTTP 422 with a type ending in /idempotency-conflict.
X-Bipbip-Delivery-Id
Header BipBip sends in each webhook dispatch. Value: unique UUID per delivery batch for a given event.
Usage: deduplication key on the merchant side. When the sameX-Bipbip-Delivery-Id arrives twice (retry), the event has already been processed — the response is 200 without re-executing business logic. It differs from orderKey: multiple delivery IDs can exist for the same order if retries occurred.
Order states (from the POS)
The seven states an order can have in the BipBip system, with the merchant's decision or action at each transition:
StateMeaningMerchant action
pendingOrder received, awaiting POS responseRespond with remoteOrderId, then call /accept or /reject
acceptedInternal state of the /accept cascade. Not observable between calls — the order moves atomically from pending to preparingNo additional action. The acceptedAt timestamp is recorded and available via GET /Orders/{key}
rejectedMerchant rejected the order (terminal)No further actions
preparingOrder being preparedCall PUT /status with ready
readyReady for driver pickupCall PUT /status with handed_over when the driver picks up
driver_assignedDriver assigned — courier en route to the merchantIncoming PUT /status with status: driver_assigned and a driver object (fullName, phone?). Informational — respond 200
handed_overHanded over to the driver (terminal from the merchant's perspective)No further actions — next state: delivered
cancelledCancelled (terminal). The merchant does NOT trigger this — post-acceptance cancellations are handled via supportIncoming PUT /status with status: cancelled + optional reason — the POS updates the state
deliveredDelivered to the customer (terminal)Incoming PUT /status with status: delivered — the POS marks the order as delivered and responds 200
Unified casing in v1.0: both the webhook and the REST API usesnake_case lowercase (pending,accepted,handed_over,driver_assigned…). The webhook carries it in body.event of the OrderUpdateEvent envelope — where event is what happened, not the state of the resource. The REST API receives it in the body of PUT /api/v1/Orders/{orderKey}/status(accepts preparing | ready | handed_over; preparing stays in the enum for forward-compat but returns 409 because the /accept cascade already left the order there).