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
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
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 validremoteOrderIdin the body is treated as a failed delivery and BipBip will retry. See BipBip keeps retrying. - 2
Verify the HMAC signature
Every request from BipBip includes the header
X-Bipbip-Signature-256with 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
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
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 };""" Accept an order via the BipBip REST API — Python 3.8+ (Quickstart Step 4) Call POST /api/v1/orders/{orderKey}/accept after receiving and verifying the webhook. Uses only Python standard library (urllib.request, json, uuid). No pip packages required. """ import json import os import urllib.request import urllib.error from uuid import uuid4 API_BASE_URL = "https://api.bipbip.com" # Replace with the URL provided by BipBip API_KEY = os.environ.get("BIPBIP_API_KEY", "") # Your X-Bipbip-Api-Key header value def accept_order(order_key: str, remote_order_id: str, idempotency_key: str) -> dict: """ Accepts a BipBip order and sets your internal order ID (remoteOrderId). Args: order_key: The opaque BipBip order key (format: "ord_" + 16 base62 chars). remote_order_id: Your POS's own internal order identifier. idempotency_key: A unique UUID for this request (reuse to safely retry). Returns: dict: The accepted order details from BipBip. Raises: urllib.error.HTTPError: If the API returns a non-2xx status code. """ url = f"{API_BASE_URL}/api/v1/orders/{order_key}/accept" payload = json.dumps({"remoteOrderId": remote_order_id}).encode("utf-8") # Build the request with required headers req = urllib.request.Request( url, data=payload, method="POST", headers={ "Content-Type": "application/json", "X-Bipbip-Api-Key": API_KEY, "Idempotency-Key": idempotency_key, # Required on all mutations — enables safe retry }, ) with urllib.request.urlopen(req) as response: return json.loads(response.read().decode("utf-8")) # ── Usage example ────────────────────────────────────────────────────────────── # In your webhook handler (after HMAC verification): # # def handle_order_webhook(raw_body: bytes) -> dict: # order = json.loads(raw_body) # # # Step 1: Create the order in your POS system and get your internal ID # remote_order_id = your_pos.create_order(order) # # # Step 2: Accept the order on BipBip (use a stable UUID per attempt) # idempotency_key = str(uuid4()) # accept_order(order["orderKey"], remote_order_id, idempotency_key) # # # Step 3: The webhook ACK body must include remoteOrderId # return {"remoteOrderId": remote_order_id}// Accept an order via the BipBip REST API — C# / .NET 6+ (Quickstart Step 4) // Call POST /api/v1/orders/{orderKey}/accept after receiving and verifying the webhook. // Uses only System.Net.Http.HttpClient (built into .NET). No NuGet packages required. using System; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; /// <summary> /// Client for the BipBip Merchant REST API. /// </summary> public class BipBipOrderClient { private readonly HttpClient _http; private readonly string _apiKey; /// <param name="apiKey">Your X-Bipbip-Api-Key header value (provided by BipBip).</param> /// <param name="baseAddress">BipBip API base URL (provided by BipBip). Default: https://api.bipbip.com</param> public BipBipOrderClient(string apiKey, string baseAddress = "https://api.bipbip.com") { _apiKey = apiKey; _http = new HttpClient { BaseAddress = new Uri(baseAddress) }; } /// <summary> /// Accepts a BipBip order and sets your internal order ID (remoteOrderId). /// </summary> /// <param name="orderKey">The opaque BipBip order key (format: "ord_" + 16 base62 chars).</param> /// <param name="remoteOrderId">Your POS's own internal order identifier.</param> /// <param name="idempotencyKey">A unique GUID for this request (reuse to safely retry).</param> public async Task<JsonElement> AcceptOrderAsync( string orderKey, string remoteOrderId, string idempotencyKey) { // Build the request body — only remoteOrderId is required on /accept var body = JsonSerializer.Serialize(new { remoteOrderId }); using var content = new StringContent(body, Encoding.UTF8, "application/json"); // Set the required authentication and idempotency headers using var request = new HttpRequestMessage( HttpMethod.Post, $"/api/v1/orders/{Uri.EscapeDataString(orderKey)}/accept") { Content = content }; request.Headers.Add("X-Bipbip-Api-Key", _apiKey); request.Headers.Add("Idempotency-Key", idempotencyKey); // Required on all mutations var response = await _http.SendAsync(request); var json = await response.Content.ReadAsStringAsync(); response.EnsureSuccessStatusCode(); // Throws on 4xx/5xx return JsonDocument.Parse(json).RootElement; } } // ── Usage example ───────────────────────────────────────────────────────────── // In your webhook handler (after HMAC verification): // // var client = new BipBipOrderClient( // apiKey: Environment.GetEnvironmentVariable("BIPBIP_API_KEY")!); // // // Step 1: Create the order in your POS system and get your internal ID // string remoteOrderId = await yourPos.CreateOrderAsync(order); // // // Step 2: Accept the order on BipBip (use a stable GUID per attempt) // string idempotencyKey = Guid.NewGuid().ToString(); // await client.AcceptOrderAsync(order.OrderKey, remoteOrderId, idempotencyKey); // // // Step 3: The webhook ACK body must include remoteOrderId // return Ok(new { remoteOrderId });<?php /** * Accept an order via the BipBip REST API — PHP 8.0+ (Quickstart Step 4) * Call POST /api/v1/orders/{orderKey}/accept after receiving and verifying the webhook. * Uses only PHP built-in cURL functions. No Composer packages required. */ define('BIPBIP_API_BASE', 'https://api.bipbip.com'); // Replace with the URL provided by BipBip $apiKey = getenv('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 $apiKey Your X-Bipbip-Api-Key header value. * @param string $idempotencyKey A unique UUID for this request (reuse to safely retry). * @return array Decoded JSON response from BipBip. * @throws RuntimeException If the API returns a non-2xx status code. */ function acceptBipBipOrder( string $orderKey, string $remoteOrderId, string $apiKey, string $idempotencyKey ): array { $url = BIPBIP_API_BASE . '/api/v1/orders/' . rawurlencode($orderKey) . '/accept'; $payload = json_encode(['remoteOrderId' => $remoteOrderId]); $curl = curl_init($url); curl_setopt_array($curl, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', "X-Bipbip-Api-Key: {$apiKey}", "Idempotency-Key: {$idempotencyKey}", // Required on all mutations — enables safe retry ], ]); $body = curl_exec($curl); $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); if ($status < 200 || $status >= 300) { throw new RuntimeException("Accept failed: HTTP {$status} — {$body}"); } return json_decode($body, true); } // ── Usage example ────────────────────────────────────────────────────────────── // In your webhook handler (after HMAC verification): // // $apiKey = getenv('BIPBIP_API_KEY'); // $rawBody = file_get_contents('php://input'); // $order = json_decode($rawBody, true); // // // Step 1: Create the order in your POS system and get your internal ID // $remoteOrderId = yourPos()->createOrder($order); // // // Step 2: Accept the order on BipBip (use a stable UUID per attempt) // $idempotencyKey = bin2hex(random_bytes(16)); // UUID-style unique key // acceptBipBipOrder($order['orderKey'], $remoteOrderId, $apiKey, $idempotencyKey); // // // Step 3: The webhook ACK body must include remoteOrderId // header('Content-Type: application/json'); // echo json_encode(['remoteOrderId' => $remoteOrderId]);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
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 insha256=<hex>formatX-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
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 }; """
HMAC-SHA256 webhook signature verification — Python 3.8+
Verify that the webhook payload from BipBip is authentic before processing it.
Uses only Python standard library (hmac, hashlib, time). No pip packages required.
"""
import hashlib
import hmac
import time
def verify_bipbip_signature(
secret: str,
timestamp: str,
raw_body: bytes,
signature: str,
max_skew_seconds: int = 300,
) -> bool:
"""
Verifies the HMAC-SHA256 signature of an incoming BipBip webhook.
Args:
secret: HMAC secret provided by BipBip during onboarding.
timestamp: Value of the X-Bipbip-Timestamp header (Unix seconds as string).
raw_body: Raw request body bytes BEFORE any json.loads() call.
signature: Value of the X-Bipbip-Signature-256 header (e.g. "sha256=abc123...").
max_skew_seconds: Maximum allowed clock skew in seconds (default: 300).
Returns:
True if the signature is valid and the timestamp is within the skew limit.
"""
# Step 1: Validate timestamp to prevent replay attacks.
# Reject requests where the clock skew exceeds max_skew_seconds.
now = int(time.time())
try:
ts = int(timestamp)
except (ValueError, TypeError):
return False
if abs(now - ts) > max_skew_seconds:
return False
# Step 2: Build the signed message exactly as BipBip does:
# message = "{timestamp}.{rawBody}"
# IMPORTANT: raw_body 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.
message = f"{timestamp}.{raw_body.decode('utf-8')}".encode('utf-8')
# Step 3: Compute HMAC-SHA256 with the shared secret.
# Both key and message are treated as UTF-8 encoded bytes.
computed_hex = hmac.new(
secret.encode('utf-8'),
message,
hashlib.sha256,
).hexdigest() # hexdigest() returns lowercase hex — matches BipBip's format
# Step 4: Prepend the "sha256=" prefix to match the header value format.
expected = f"sha256={computed_hex}"
# Step 5: Use a timing-safe comparison to prevent timing-oracle attacks.
# hmac.compare_digest() is constant-time and accepts str or bytes.
return hmac.compare_digest(expected, signature)
# ── Flask integration example ────────────────────────────────────────────────
# In your Flask app, access the raw body via request.get_data() (not request.json)
# so that the bytes are available for signature verification before deserialization.
#
# @app.route('/v1/order/<remote_id>', methods=['POST'])
# def receive_order(remote_id):
# secret = os.environ['BIPBIP_HMAC_SECRET']
# timestamp = request.headers.get('X-Bipbip-Timestamp', '')
# signature = request.headers.get('X-Bipbip-Signature-256', '')
# raw_body = request.get_data() # bytes — no JSON parsing yet
#
# if not verify_bipbip_signature(secret, timestamp, raw_body, signature):
# return jsonify({'error': 'Invalid signature'}), 401
#
# order = request.json # safe to parse after verification
# remote_order_id = your_internal_id_generator(order)
# return jsonify({'remoteOrderId': remote_order_id}), 200 // HMAC-SHA256 webhook signature verification — C# / .NET 6+
// Verify that the webhook payload from BipBip is authentic before processing it.
// Uses only System.Security.Cryptography (built into .NET). No NuGet packages required.
using System;
using System.Security.Cryptography;
using System.Text;
/// <summary>
/// Utilities for verifying BipBip webhook signatures.
/// </summary>
public static class BipBipWebhookVerifier
{
/// <summary>
/// Verifies the HMAC-SHA256 signature of an incoming BipBip webhook.
/// </summary>
/// <param name="secret">HMAC secret provided by BipBip during onboarding.</param>
/// <param name="timestamp">Value of the X-Bipbip-Timestamp header (Unix seconds as string).</param>
/// <param name="rawBody">Raw request body string BEFORE any deserialization.</param>
/// <param name="signature">Value of the X-Bipbip-Signature-256 header (e.g. "sha256=abc123...").</param>
/// <param name="maxSkewSeconds">Maximum allowed clock skew in seconds (default: 300).</param>
/// <returns>True if the signature is valid and the timestamp is within the skew limit.</returns>
public static bool VerifySignature(
string secret,
string timestamp,
string rawBody,
string signature,
int maxSkewSeconds = 300)
{
// Step 1: Validate timestamp to prevent replay attacks.
// Reject requests where the clock skew exceeds maxSkewSeconds.
if (!long.TryParse(timestamp, out long ts))
return false;
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (Math.Abs(now - ts) > maxSkewSeconds)
return false;
// Step 2: Build the signed message exactly as BipBip does:
// message = "{timestamp}.{rawBody}"
// IMPORTANT: rawBody must be the original string received over the wire.
// Do NOT re-serialize a deserialized object — any whitespace/key-order
// difference will produce a different signature.
string message = $"{timestamp}.{rawBody}";
// Step 3: Compute HMAC-SHA256 with the shared secret.
// Both key and message are UTF-8 encoded bytes.
byte[] keyBytes = Encoding.UTF8.GetBytes(secret);
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
using var hmac = new HMACSHA256(keyBytes);
byte[] hashBytes = hmac.ComputeHash(messageBytes);
// Step 4: Convert hash to lowercase hex and prepend the "sha256=" prefix.
string computedHex = BitConverter.ToString(hashBytes)
.Replace("-", string.Empty)
.ToLowerInvariant();
string expected = $"sha256={computedHex}";
// Step 5: Use a timing-safe comparison to prevent timing-oracle attacks.
// CryptographicOperations.FixedTimeEquals() compares byte arrays in constant time.
byte[] expectedBytes = Encoding.UTF8.GetBytes(expected);
byte[] receivedBytes = Encoding.UTF8.GetBytes(signature);
return CryptographicOperations.FixedTimeEquals(expectedBytes, receivedBytes);
}
}
// ── ASP.NET Core integration example ────────────────────────────────────────
// Read the raw body string from the request stream BEFORE binding to a model.
// Use [FromBody] with a string or read Request.Body manually.
//
// [HttpPost("/v1/order/{remoteId}")]
// public async Task<IActionResult> ReceiveOrder(
// string remoteId,
// [FromHeader(Name = "X-Bipbip-Timestamp")] string timestamp,
// [FromHeader(Name = "X-Bipbip-Signature-256")] string signature)
// {
// string secret = _config["BipBip:HmacSecret"]!;
// string rawBody = await new StreamReader(Request.Body).ReadToEndAsync();
//
// if (!BipBipWebhookVerifier.VerifySignature(secret, timestamp, rawBody, signature))
// return Unauthorized();
//
// var order = JsonSerializer.Deserialize<OrderPayload>(rawBody);
// string remoteOrderId = _orderService.CreateOrder(order!);
// return Ok(new { remoteOrderId });
// }
//
// NOTE: To enable raw body reading in ASP.NET Core you may need to call
// Request.EnableBuffering() in a middleware before the controller executes. <?php
/**
* HMAC-SHA256 webhook signature verification — PHP 8.0+
* Verify that the webhook payload from BipBip is authentic before processing it.
* Uses only PHP built-in functions (hash_hmac, hash_equals). No Composer packages required.
*/
/**
* 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 string $rawBody Raw request body string BEFORE any json_decode() call.
* @param string $signature Value of the X-Bipbip-Signature-256 header (e.g. "sha256=abc123...").
* @param int $maxSkewSeconds Maximum allowed clock skew in seconds (default: 300).
* @return bool True if the signature is valid and the timestamp is within the skew limit.
*/
function verifyBipBipSignature(
string $secret,
string $timestamp,
string $rawBody,
string $signature,
int $maxSkewSeconds = 300
): bool {
// Step 1: Validate timestamp to prevent replay attacks.
// Reject requests where the clock skew exceeds $maxSkewSeconds.
$now = time();
$ts = (int) $timestamp;
if (abs($now - $ts) > $maxSkewSeconds) {
return false;
}
// Step 2: Build the signed message exactly as BipBip does:
// message = "{timestamp}.{rawBody}"
// IMPORTANT: $rawBody must be the original string received over the wire.
// Do NOT re-serialize a json_decode result — any whitespace/key-order
// difference will produce a different signature.
$message = "{$timestamp}.{$rawBody}";
// Step 3: Compute HMAC-SHA256 with the shared secret.
// hash_hmac() returns a lowercase hex string by default — matches BipBip's format.
$computedHex = hash_hmac('sha256', $message, $secret);
// Step 4: Prepend the "sha256=" prefix to match the header value format.
$expected = "sha256={$computedHex}";
// Step 5: Use a timing-safe comparison to prevent timing-oracle attacks.
// hash_equals() is constant-time and is the recommended PHP function for this purpose.
return hash_equals($expected, $signature);
}
// ── PHP / Laravel / Slim integration example ─────────────────────────────────
// Read the raw input BEFORE calling json_decode so that the original bytes
// are available for signature verification.
//
// // Plain PHP (no framework):
// $secret = getenv('BIPBIP_HMAC_SECRET');
// $timestamp = $_SERVER['HTTP_X_BIPBIP_TIMESTAMP'] ?? '';
// $signature = $_SERVER['HTTP_X_BIPBIP_SIGNATURE_256'] ?? '';
// $rawBody = file_get_contents('php://input'); // raw bytes — no JSON parsing yet
//
// if (!verifyBipBipSignature($secret, $timestamp, $rawBody, $signature)) {
// http_response_code(401);
// echo json_encode(['error' => 'Invalid signature']);
// exit;
// }
//
// $order = json_decode($rawBody, true); // safe to parse after verification
// $remoteOrderId = generateYourInternalOrderId($order);
// echo json_encode(['remoteOrderId' => $remoteOrderId]); 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:
- Verify that you capture the raw bytes of the body before any call to
JSON.parse(),json_decode()or equivalent. - Confirm that the signed message is exactly
"{timestamp}.{rawBody}"— the timestamp comes from the header, not your local clock. - Confirm that you are using constant-time comparison (see Common pitfalls).
- 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
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:
- 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/testfrom an external network. localhost or private VPN URLs do not work without tunneling (e.g. ngrok). - 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).
- Header inspection: if the request arrives but is not processed, log all incoming headers. Verify that
X-Bipbip-Signature-256andX-Bipbip-Timestampare present. - BackOffice status: ask the BipBip team to verify whether the delivery is
Pending,DeliveredorFailed.
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 yourremoteOrderIdfor 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 byorderKey. - 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-Idtwice (retry), you have already processed that event — return 200 without re-executing business logic. It differs fromorderKey: 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/acceptor/rejectAccepted Merchant accepted the order Call POST /statuswithpreparing(REST API, camelCase lowercase)Rejected Merchant rejected the order (terminal) No further actions Preparing Order being prepared Call POST /statuswithready(REST API, camelCase lowercase)Ready Ready for driver pickup No action — BipBip advances to HandedOverwhen the driver picks upDriverAssigned Driver assigned — courier en route to the merchant Receive order.driver_assignedwebhook with driver details. Informational only — optionally update the operational view and respond 200HandedOver Handed over to the driver (terminal) No further actions Cancelled Cancelled (terminal) Receive order.cancelledwebhook — update your POSDelivered Delivered to the customer (terminal) Receive order.deliveredwebhook — mark it as delivered in your POS and respond 200 - Note: state casing varies by channel.
The webhook uses PascalCase (
Pending,Accepted,HandedOver…) in thepreviousStatusfield of the payload. The REST API uses camelCase lowercase (pending,accepted,handedOver…) in thestatusfield of the response and innewStatusin the body ofPOST /api/v1/Orders/{orderKey}/status.