Primeros pasos

Quickstart

Esta guía te lleva desde "tengo mis credenciales" hasta "recibí y confirmé mi primera orden" sin intervención del equipo BipBip. Seguí los cuatro pasos en orden.

Sin ambiente de pruebas — integración directa en producción

BipBip no tiene un sandbox disponible aún. El piloto integra directamente contra producción con coordinación del equipo. Coordiná con [email protected] antes de enviar órdenes de prueba.

Precondiciones

Antes de comenzar, el equipo BipBip te provee los siguientes elementos de configuración. Necesitás todos antes de escribir tu primera línea de código:

  • 1
    HMAC Secret — clave compartida para verificar la autenticidad del webhook (una por cuenta)
  • 2
    API Key (X-Bipbip-Api-Key) — header de autenticación para llamar la REST API
  • 3
    remoteId — identificador de tu tienda, definido por vos (ej: POS_TGU_001). Uno por tienda registrada.
  • 4
    Base URL registrada — la URL base de tu servidor donde BipBip enviará los webhooks (debe ser públicamente accesible)

Los 4 pasos

  1. 1

    Implementá el endpoint del webhook

    BipBip enviará POST {tuBaseUrl}/v1/order/{remoteId} cada vez que llegue una nueva orden para tu tienda. Tu endpoint debe:

    • Capturar el cuerpo crudo (raw body) antes de parsear el JSON
    • Verificar la firma HMAC (ver Verificación HMAC)
    • Devolver HTTP 200 con un JSON body que incluya remoteOrderId
    • Responder en menos de 30 segundos (BipBip toma cualquier respuesta lenta como fallo)

    Ojo: remoteOrderId es obligatorio en la respuesta

    Sin un remoteOrderId válido en el body, BipBip trata el delivery como fallido y reintenta. El 200 solo no alcanza. Ver BipBip sigue reintentando.
  2. 2

    Verificá la firma HMAC

    Toda solicitud de BipBip incluye el header X-Bipbip-Signature-256 con una firma HMAC-SHA256. Verificar la firma garantiza que el request proviene de BipBip y no fue modificado en tránsito. Consultá la sección Verificación HMAC para los code samples.

  3. 3

    Respondé con tu remoteOrderId

    Una vez verificada la firma y creada la orden en tu POS, devolvé HTTP 200 con:

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

    Este valor es tu identificador interno del POS para esta orden. BipBip lo guarda y lo incluirá en todos los webhooks de cancelación subsiguientes para que puedas correlacionar.

  4. 4

    Aceptá la orden llamando la REST API

    Después de recibir el webhook y responder 200, podés aceptar formalmente la orden llamando a POST /api/v1/Orders/{orderKey}/accept. Esto confirma que tu POS está listo para preparar el pedido.

    Este paso es opcional si tu tienda tiene auto-accept habilitado — en ese caso BipBip acepta la orden automáticamente al recibir tu 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 };

    Rate limits de la REST API

    Tenés 100 requests por minuto (ventana fija) y 1.000 por hora (ventana deslizante), por API Key. Si llegás al límite, BipBip te devuelve HTTP 429 — implementá exponential backoff con jitter y lo manejás sin problemas.

Sandbox — próximo

Ambiente de pruebas: próximo

BipBip no cuenta con un ambiente sandbox en este momento. El piloto integra directamente contra producción en coordinación con el equipo. No existen URLs, API keys ni órdenes de prueba aisladas disponibles aún. Cuando el sandbox esté disponible, esta sección se actualizará con las instrucciones correspondientes.

Seguridad

Verificación HMAC

Cada webhook que BipBip envía incluye una firma HMAC-SHA256 en el header X-Bipbip-Signature-256. Verificar esta firma es obligatorio — sin ella, cualquier actor malicioso puede enviar órdenes falsas a tu endpoint.

El algoritmo

BipBip firma cada request usando la siguiente fórmula:

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

Los headers relevantes en cada request son:

  • X-Bipbip-Timestamp — Unix timestamp en segundos (número entero como string)
  • X-Bipbip-Signature-256 — la firma en formato sha256=<hex>
  • X-Bipbip-Delivery-Id — UUID único por envío (usar para deduplicación)
  • X-Bipbip-Event-Type — tipo de evento. Posibles valores: order.created, order.cancelled, order.driver_assigned, order.delivered

Clock skew: rechazá requests más viejos de 5 minutos

Fijate que la diferencia entre X-Bipbip-Timestamp y tu reloj no supere 300 segundos. Así bloqueás ataques de replay — si alguien captura un request válido y lo reenvía horas después, lo rechazás sin que llegue a ejecutarse.

Errores comunes (footguns)

Estos tres errores son la causa del 90% de los casos donde la firma no verifica. Revisálos antes de buscar otro problema.

Footgun 1: firmar el JSON re-serializado en vez del raw body

El error más frecuente: parsear el body con JSON.parse() primero y después firmar el objeto re-serializado. Cualquier diferencia de whitespace, orden de keys o precisión numérica produce una firma diferente a la de BipBip.

Solución: capturá los bytes crudos del body antes de llamar a cualquier función de JSON parsing. Verificá la firma. Solo después parseás.

Footgun 2: comparación de strings sin timing-safe equality

Comparar la firma calculada con la recibida usando ===, == o strcmp() introduce una vulnerabilidad de timing oracle: un atacante puede medir el tiempo de respuesta para deducir caracteres de la firma válida de a uno.

Solución: usá siempre una función de comparación en tiempo constante: crypto.timingSafeEqual() en Node.js, hmac.compare_digest() en Python, CryptographicOperations.FixedTimeEquals() en C#, hash_equals() en PHP.

Footgun 3: generar el timestamp localmente en vez de leer el header

La firma incluye el timestamp que BipBip escribió en X-Bipbip-Timestamp. Si usás Date.now(), time() o DateTime.UtcNow para construir el mensaje, el timestamp será diferente al de BipBip y la firma nunca verificará.

Solución: leé siempre el timestamp del header X-Bipbip-Timestamp. No lo generes vos.

Code samples

Seleccioná tu lenguaje. Todos los samples usan solo la librería estándar — no se necesitan dependencias externas.

// 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 };

Soporte

Troubleshooting

Las cuatro preguntas más frecuentes durante la integración. Si no encontrás la respuesta acá, contactá a [email protected].

Mi firma no verifica

Síntoma: tu código computa el HMAC pero la firma calculada nunca coincide con X-Bipbip-Signature-256.

Causa más probable: estás firmando el JSON re-serializado en vez del raw body tal como llegó por el wire.

Cómo arreglarlo:

  1. Verificá que captures los bytes crudos del body antes de cualquier llamada a JSON.parse(), json_decode() o equivalente.
  2. Confirmá que el mensaje firmado sea exactamente "{timestamp}.{rawBody}" — el timestamp proviene del header, no de tu reloj local.
  3. Confirmá que usás comparación en tiempo constante (ver Errores comunes).
  4. Si persiste, logueá el mensaje exacto que estás firmando y comparalo byte a byte.

Recibo el webhook dos veces

Síntoma: tu POS crea la misma orden dos veces o recibe dos webhooks para el mismo evento.

Causa: BipBip entrega webhooks con garantía at-least-once. Si tu endpoint tarda demasiado en responder o hay un error de red, BipBip reintenta el envío. Eso es comportamiento esperado, no un bug.

Cómo arreglarlo: implementá deduplicación usando el header X-Bipbip-Delivery-Id. Este header es un UUID único por lote de intentos — si ya procesaste ese ID, devolvé HTTP 200 inmediatamente sin reprocesar.

// 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 sigue reintentando después de mi 200

Síntoma: tu endpoint devuelve HTTP 200 pero BipBip sigue enviando el mismo webhook.

Causa: devolver HTTP 200 sin un remoteOrderId válido se trata como delivery fallido. BipBip necesita ese valor para componer la URL de los webhooks de cancelación futuros.

Cómo arreglarlo: asegurate de que tu response body sea un JSON válido con remoteOrderId no nulo y no vacío:

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

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

Agotados los reintentos

Si BipBip agota todos los reintentos sin éxito, la orden se cancela automáticamente y se envía un webhook order.cancelled a tu endpoint.

No recibo ningún webhook

Síntoma: BipBip confirma que despachó el webhook pero tu servidor no recibió nada.

Checklist:

  1. URL públicamente accesible: la base URL registrada debe ser accesible desde internet. Probala con curl -X POST https://tu-servidor.com/v1/order/test desde una red externa. Las URLs localhost o VPN privada no funcionan sin tunelado (ej: ngrok).
  2. Firewall y allowlist: si tu servidor tiene reglas de firewall de entrada, asegurate de permitir tráfico HTTPS desde los rangos de IP de BipBip (consultá el rango exacto al equipo).
  3. Inspección de headers: si el request llega pero no lo procesás, logueá todos los headers de entrada. Verificá que X-Bipbip-Signature-256 y X-Bipbip-Timestamp estén presentes.
  4. Estado en BackOffice: pedile al equipo BipBip que verifique si el delivery está Pending, Delivered o Failed.

Referencia

Glosario

Seis términos que aparecen en todo el contrato de integración. Usalos de referencia cuando el Webhook Spec o la REST API mencionen un término que no reconocés.

orderKey
Identificador opaco público de BipBip para una orden. Formato: prefijo ord_ seguido de 16 caracteres base62 (ej: ord_4xK9mZqPwRtN2aLb).
Uso: único identificador de orden que BipBip expone externamente. Se usa como segmento de URL en los endpoints REST (/api/v1/Orders/{orderKey}/accept). Nunca lo expongas internamente — usá tu remoteOrderId para correlación interna.
remoteOrderId
El identificador de orden de tu propio POS. Lo devolvés en el body del ACK al webhook de creación (HTTP 200) y BipBip lo persiste. Campo obligatorio.
Uso: BipBip lo incluye en la URL de todos los webhooks de cancelación subsiguientes (/v1/order/{remoteId}/{remoteOrderId}) para que puedas correlacionar la cancelación sin buscar por orderKey.
remoteId
Identificador de la tienda definido por el comercio (ej: POS_TGU_001, plaza-pedregal-42). Lo configurás una vez durante el onboarding, uno por tienda registrada.
Uso: aparece como {remoteId} en el path de los webhooks entrantes. Te permite distinguir desde qué tienda proviene cada orden si operás múltiples tiendas con el mismo endpoint base.
Idempotency-Key
Header que vos enviás al llamar los endpoints de mutación de la REST API (/accept, /reject, /status). Valor: cualquier string único por intento lógico (recomendado: UUID v4).
Uso: si enviás la misma Idempotency-Key con el mismo body dentro de las 24 horas, BipBip devuelve la respuesta en caché sin reejecutar la mutación — permite reintentos seguros sin efectos dobles. Si usás la misma key con un body diferente, recibís HTTP 409 Conflict.
X-Bipbip-Delivery-Id
Header que BipBip envía en cada webhook dispatch. Valor: UUID único por lote de intentos de entrega de un mismo evento.
Uso: clave de deduplicación del lado del comercio. Si recibís el mismo X-Bipbip-Delivery-Id dos veces (reintento), ya procesaste ese evento — devolvé 200 sin reejecutar la lógica de negocio. Es distinto del orderKey: pueden existir múltiples delivery IDs para la misma orden si hubo reintentos.
Estados de orden (desde el POS)
Los siete estados que puede tener una orden en el sistema BipBip, con la decisión o acción del comercio en cada transición:
Estado Significado Acción del comercio
Pending Orden recibida, esperando respuesta del POS Responder con remoteOrderId, luego /accept o /reject
Accepted Comercio aceptó la orden Llamar POST /status con preparing (REST API, camelCase lowercase)
Rejected Comercio rechazó la orden (terminal) Sin acciones adicionales
Preparing Orden en preparación Llamar POST /status con ready (REST API, camelCase lowercase)
Ready Lista para entrega al driver Sin acción — BipBip avanza a HandedOver cuando el driver retira
DriverAssigned Driver asignado — repartidor en camino al comercio Recibís webhook order.driver_assigned con datos del driver. Solo informativo — actualizá la pantalla operativa si lo querés y respondé 200
HandedOver Entregada al driver (terminal) Sin acciones adicionales
Cancelled Cancelada (terminal) Recibís webhook order.cancelled — actualizá tu POS
Delivered Entregada al cliente (terminal) Recibís webhook order.delivered — marcala como entregada en tu POS y respondé 200
Nota: casing de estados según el canal. El webhook usa PascalCase (Pending, Accepted, HandedOver…) en el campo previousStatus del payload. La REST API usa camelCase lowercase (pending, accepted, handedOver…) en el campo status de la respuesta y en newStatus del body de POST /api/v1/Orders/{orderKey}/status.