First steps

Quickstart

This guide takes you from "I have my credentials" to "I received and confirmed my first order" without any intervention from the BipBip team. Follow the four steps in order.

No test environment — direct integration against production

BipBip does not have a sandbox available yet. The pilot integrates directly against production with coordination from the team. Coordinate with [email protected] before sending test orders.

Prerequisites

Before getting started, the BipBip team provides the following configuration items. You need all of them before writing your 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 — your store identifier, defined by you (e.g. POS_TGU_001). One per registered store.
  • 4
    Registered base URL — the base URL of your POS where BipBip will send webhooks (must be publicly accessible)

The 4 steps

  1. 1

    Implement the webhook endpoint

    BipBip will send POST {yourBaseUrl}/v1/order/{remoteId} each time a new order arrives for your store. Your 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 remoteOrderId in the body is treated as a failed delivery and BipBip will retry. See BipBip keeps retrying.
  2. 2

    Verify the HMAC signature

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

  3. 3

    Respond with your remoteOrderId

    Once the signature is verified and the order is created in your POS, return HTTP 200 with:

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

    This value is your POS's internal identifier for this order. BipBip stores it and will include it in all subsequent cancellation webhooks so you can correlate them.

  4. 4

    Accept the order by calling the REST API

    After receiving the webhook and responding with 200, you can formally accept the order by calling POST /api/v1/Orders/{orderKey}/accept. This confirms that your POS is ready to prepare the order.

    This step is optional if your store has auto-accept enabled — in that case BipBip accepts the order automatically upon receiving your 200.

    // 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.
    
    const API_BASE_URL = 'https://api.bipbip.com'; // Replace with the URL provided by BipBip
    const API_KEY      = process.env.BIPBIP_API_KEY;  // Your X-Bipbip-Api-Key header value
    
    /**
     * Accepts a BipBip order and sets your internal order ID (remoteOrderId).
     *
     * @param {string} orderKey      - The opaque BipBip order key (format: "ord_" + 16 base62 chars)
     * @param {string} remoteOrderId - Your POS's own internal order identifier
     * @param {string} idempotencyKey - A unique UUID for this request (reuse to safely retry)
     * @returns {Promise<object>} - The accepted order details from BipBip
     */
    async function acceptOrder(orderKey, remoteOrderId, 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,
          'Idempotency-Key':  idempotencyKey,  // Required on all mutations — enables safe retry
        },
        body: JSON.stringify({
          remoteOrderId, // Your internal POS order ID — BipBip will include it in cancel webhooks
        }),
      });
    
      if (!response.ok) {
        const error = await response.json().catch(() => ({}));
        throw new Error(`Accept failed: ${response.status} — ${JSON.stringify(error)}`);
      }
    
      return response.json();
    }
    
    // ── Usage example ─────────────────────────────────────────────────────────────
    // In your 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 your POS system and get your internal ID
    //   const remoteOrderId = await yourPos.createOrder(order);
    //
    //   // Step 2: Accept the order on BipBip (use a stable UUID per attempt)
    //   const idempotencyKey = randomUUID();
    //   await acceptOrder(order.orderKey, remoteOrderId, idempotencyKey);
    //
    //   // Step 3: The webhook ACK body must include remoteOrderId
    //   res.status(200).json({ remoteOrderId });
    // }
    
    module.exports = { acceptOrder };

    REST API rate limits

    The API is limited to 100 requests per minute (fixed window) and 1,000 requests per hour (sliding window), partitioned by API Key. If you exceed the limit you receive HTTP 429 — implement exponential backoff with jitter in your 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 header X-Bipbip-Signature-256. Verifying this signature is mandatory — without it, any malicious actor can send fake orders to your 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 — event type. Possible values: order.created, order.cancelled, order.driver_assigned, order.delivered

Recommended clock skew: maximum 300 seconds

Validate that the difference between X-Bipbip-Timestamp and your server's clock does 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 ===, == or strcmp() 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. If you use Date.now(), time() or DateTime.UtcNow to build the message, the timestamp will differ from BipBip's and the signature will never verify.

Solution: always read the timestamp from the X-Bipbip-Timestamp header. Do not generate it yourself.

Code samples

Pick your 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 ──────────────────────────────────────────
// In your 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 = generateYourInternalOrderId(order);
//   res.status(200).json({ remoteOrderId });
// });

module.exports = { verifyBipBipSignature };

Support

Troubleshooting

The four most frequently asked questions during integration. If you cannot find the answer here, contact [email protected].

My signature doesn't verify

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

Most likely cause: you are signing the re-serialized JSON instead of the raw body as it arrived over the wire.

How to fix:

  1. Verify that you capture the raw bytes of the body 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 your local clock.
  3. Confirm that you are using constant-time comparison (see Common pitfalls).
  4. If the problem persists, log the exact message you are signing and compare it byte by byte.

I'm receiving the webhook twice

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

Cause: BipBip delivers webhooks with at-least-once guarantee. If your 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 — if you have already processed that ID, return 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 my 200

Symptom: your 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 your 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 an order.cancelled webhook is sent to your endpoint.

I'm not receiving any webhooks

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

Checklist:

  1. Publicly accessible URL: the registered base URL must be reachable from the internet. Test it with curl -X POST https://your-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: if your server has inbound firewall rules, make sure to allow HTTPS traffic from BipBip's IP ranges (ask the team for the exact range).
  3. Header inspection: if 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: ask the BipBip team to verify whether the delivery is Pending, Delivered or Failed.

Reference

Glossary

Six terms that appear throughout the integration contract. Use this as a reference when the Webhook Spec or the REST API mention a term you do not recognize.

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). Do not use it internally — use your remoteOrderId for internal correlation.
remoteOrderId
Your POS's own order identifier. You return it in the body of the ACK to the creation webhook (HTTP 200) and BipBip persists it. Required field.
Usage: BipBip includes it in the URL of all subsequent cancellation webhooks (/v1/order/{remoteId}/{remoteOrderId}) so you can correlate the cancellation 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. Lets you distinguish which store each order comes from when operating multiple stores with the same base endpoint.
Idempotency-Key
Header you send when calling mutation endpoints on the REST API (/accept, /reject, /status). Value: any string unique per logical attempt (recommended: UUID v4).
Usage: if you send the same Idempotency-Key with the same body within 24 hours, BipBip returns the cached response without re-executing the mutation — enables safe retries without duplicate side effects. If you use the same key with a different body, you receive HTTP 409 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. If you receive the same X-Bipbip-Delivery-Id twice (retry), you have already processed that event — return 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:
State Meaning Merchant action
Pending Order received, awaiting POS response Respond with remoteOrderId, then call /accept or /reject
Accepted Merchant accepted the order Call POST /status with preparing (REST API, camelCase lowercase)
Rejected Merchant rejected the order (terminal) No further actions
Preparing Order being prepared Call POST /status with ready (REST API, camelCase lowercase)
Ready Ready for driver pickup No action — BipBip advances to HandedOver when the driver picks up
DriverAssigned Driver assigned — courier en route to the merchant Receive order.driver_assigned webhook with driver details. Informational only — optionally update the operational view and respond 200
HandedOver Handed over to the driver (terminal) No further actions
Cancelled Cancelled (terminal) Receive order.cancelled webhook — update your POS
Delivered Delivered to the customer (terminal) Receive order.delivered webhook — mark it as delivered in your POS and respond 200
Note: state casing varies by channel. The webhook uses PascalCase (Pending, Accepted, HandedOver…) in the previousStatus field of the payload. The REST API uses camelCase lowercase (pending, accepted, handedOver…) in the status field of the response and in newStatus in the body of POST /api/v1/Orders/{orderKey}/status.