Primeros pasos

Quickstart

Esta guía cubre el camino desde "credenciales obtenidas" hasta "primera orden recibida y confirmada" sin intervención del equipo BipBip. Los cuatro pasos siguen el orden indicado.

Sandbox en proceso — integración directa en producción

El ambiente de pruebas (sandbox) está en construcción. Mientras tanto, el piloto integra directamente contra producción con coordinación del equipo BipBip. La coordinación previa al envío de órdenes de prueba se gestiona a través de[email protected].

Precondiciones

Antes de empezar, asegúrate de tener estos elementos de configuración. El equipo BipBip te los entrega durante el onboarding — todos son requeridos antes de escribir la 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 la tienda, definido por el comercio (ej: POS_TGU_001). Uno por tienda registrada.
  • 4
    Base URL registrada — la URL base del servidor del POS donde BipBip enviará los webhooks (debe ser públicamente accesible)

Los 4 pasos

  1. 1

    Implementar el endpoint del webhook

    BipBip envía POST {baseUrl}/v1/order/{remoteId}cada vez que entra una orden nueva a 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 15 segundos (BipBip toma cualquier respuesta lenta como fallo y reintenta)

    Importante: 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. VerBipBip sigue reintentando.
  2. 2

    Verificar la firma HMAC

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

  3. 3

    Responder con el remoteOrderId del POS

    Una vez verificada la firma y creada la orden en el POS, la respuesta HTTP 200 incluye:

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

    Este valor es el identificador interno del POS para esta orden. BipBip lo guarda y lo incluye en todos los webhooks de cancelación subsiguientes para permitir la correlación.

  4. 4

    Aceptar la orden llamando a la REST API

    Después de recibir el webhook y responder 200, la aceptación formal de la orden se hace llamando aPOST /api/v1/Orders/{orderKey}/accept. Esa llamada ejecuta de forma automática la secuencia de estados de la orden (pending → accepted → preparing) en una sola operación. El response trae status: "preparing" directo — no es necesario (ni válido) llamar después PUT /status con preparing; la siguiente acción es marcar ready cuando el pedido esté listo.

    Este paso es opcional si tu tienda tiene auto-accept habilitado — en ese caso BipBip ejecuta esa misma secuencia automáticamente al recibir el 200, y la orden llega directo a preparing sin que tengas que invocar /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 };

    Rate limits de la REST API

    El límite es 100 requests por minuto (ventana fija) y1,000 por hora (ventana deslizante), por API Key. Al alcanzar el límite, BipBip devuelve HTTP 429 — la solución es exponential backoff con jitter.

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 headerX-Bipbip-Signature-256. Verificar esta firma es obligatorio — sin ella, cualquier actor malicioso puede enviar órdenes falsas al 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 — solo order.created en el creation webhook. Las novedades posteriores (cancelled, driver_assigned, driver_released, delivered) van por PUT /v1/order/{remoteId}/{remoteOrderId}/events y se discriminan por body.event (ese endpoint no declara este header). Ver el spec del endpointdriver_assigned puede llegar más de una vez por orden.
  • X-Bipbip-Schema-Version — versión del schema del payload. Actualmente 1.0.

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

La diferencia entre X-Bipbip-Timestampy el reloj local no debe superar 300 segundos (5 minutos). Así se bloquean los ataques de replay — un request válido capturado y reenviado horas después se rechaza sin que llegue a ejecutarse.

Errores comunes (footguns)

Estos tres errores son la causa del 90% de los casos donde la firma no verifica. Conviene revisarlos 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: captura los bytes crudos del body antes de llamar a cualquier función de JSON parsing. Verifica la firma. El parsing va después.

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

Comparar la firma calculada con la recibida usando ===,== ostrcmp()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: usa 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. El uso de Date.now(),time() oDateTime.UtcNowpara construir el mensaje produce un timestamp diferente al de BipBip y la firma nunca verifica.

Solución: lee siempre el timestamp del header X-Bipbip-Timestamp. Generarlo localmente produce un desajuste.

Code samples

Selección por lenguaje. Todos los samples usan solo la librería estándar — sin 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 ──────────────────────────────────────────
// 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 };

Soporte

Troubleshooting

Las cuatro preguntas más frecuentes durante la integración. Si la respuesta no aparece acá, el contacto es [email protected].

Mi firma no verifica

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

Causa más probable: la firma se aplica sobre el JSON re-serializado en vez del raw body tal como llegó por el wire.

Cómo arreglarlo:

  1. Verifica que capturas los bytes crudos del body antes de cualquier llamada a JSON.parse(), json_decode() o equivalente.
  2. Confirma que el mensaje firmado sea exactamente "{timestamp}.{rawBody}" — el timestamp viene del header, no de tu reloj local.
  3. Confirma que usas comparación en tiempo constante (ver Errores comunes).
  4. Si persiste, loguea el mensaje exacto que firmas y compáralo byte a byte.

Recibo el webhook dos veces

Síntoma: el 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 el endpoint del POS tarda demasiado en responder o hay un error de red, BipBip reintenta el envío. Es comportamiento esperado, no un bug.

Cómo arreglarlo: implementa deduplicación con el header X-Bipbip-Delivery-Id. Este header es un UUID único por lote de intentos — si ya procesaste ese ID, responde HTTP 200 inmediato 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 del 200

Síntoma: el 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: asegúrate 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 del creation

Si BipBip agota todos los intentos del webhook order.created sin éxito, la orden se cancela internamente (notifica al cliente) y nunca llega al POS. No se envía un PUT /status de fallo separado — BipBip no expone ese evento al POS.

No recibo ningún webhook

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

Checklist:

  1. URL públicamente accesible: la base URL registrada debe ser accesible desde internet. La prueba se hace con curl -X POST https://servidor.com/v1/order/test desde una red externa. Las URLs localhost o VPN privada no funcionan sin tunelado (ej: ngrok).
  2. Firewall: permitir tráfico HTTPS entrante (TCP/443) sin restricción por IP. BipBip no publica un IP allowlist fijo. Si la política de seguridad exige allowlist estático o ruta privada (ej: AWS PrivateLink), el contacto es [email protected] para coordinar config custom.
  3. Inspección de headers: si el request llega pero no se procesa, loguear todos los headers de entrada. Verificar que X-Bipbip-Signature-256 y X-Bipbip-Timestamp estén presentes.
  4. Estado en BackOffice: solicitar al equipo BipBip la verificación del estado del delivery (Pending, Delivered o Failed).

Referencia

Glosario

Seis términos que aparecen en todo el contrato de integración. Sirven como referencia cuando elWebhook Spec o laREST API mencionen un término poco familiar.

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. Úsalo como segmento de URL en los endpoints REST (/api/v1/Orders/{orderKey}/accept). No expongas este identificador en tu UI interna — para la correlación interna usa elremoteOrderId del POS.
remoteOrderId
El identificador de orden del propio POS. Se devuelve 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 novedades subsiguientes (/v1/order/{remoteId}/{remoteOrderId}/events) para permitir la correlación sin buscar por orderKey.
remoteId
Identificador de la tienda definido por el comercio (ej: POS_TGU_001,plaza-pedregal-42). Se configura una vez durante el onboarding, uno por tienda registrada.
Uso: aparece como {remoteId} en el path de los webhooks entrantes. Permite distinguir desde qué tienda proviene cada orden cuando se operan múltiples tiendas con el mismo endpoint base.
Idempotency-Key
Header que el POS envía 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: cuando se envía 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. La misma key con un body diferente devuelve HTTP 422 con un type terminado en /idempotency-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. Cuando llega el mismoX-Bipbip-Delivery-Id dos veces (reintento), el evento ya fue procesado — la respuesta es 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:
EstadoSignificadoAcción del comercio
pendingOrden recibida, esperando respuesta del POSResponder con remoteOrderId, luego /accept o /reject
acceptedEstado intermedio que se ejecuta automáticamente dentro de /accept. No observable entre llamadas — la orden pasa de pending a preparing en una sola operaciónSin acción adicional. El timestamp acceptedAt queda registrado y disponible vía GET /Orders/{key}
rejectedComercio rechazó la orden (terminal)Sin acciones adicionales
preparingOrden en preparaciónLlamar PUT /status con ready
readyLista para entrega al driverLlamar PUT /status con handed_over cuando el driver retira
driver_assignedDriver asignado — repartidor en camino al comercioLlega PUT /status con status: driver_assigned y objeto driver (fullName, phone?). Informativo — respuesta 200
handed_overEntregada al driver (terminal del lado del comercio)Sin acciones adicionales — siguiente estado: delivered
cancelledCancelada (terminal). El comercio NO la dispara — la cancelación posterior a la aceptación se gestiona vía soporteLlega PUT /status con status: cancelled + reason opcional — el POS actualiza el estado
deliveredEntregada al cliente (terminal)Llega PUT /status con status: delivered — el POS marca la orden como entregada y responde 200
Casing unificado en v1.0: tanto el webhook como la REST API usansnake_case lowercase (pending,accepted,handed_over,driver_assigned…). El webhook lo lleva en body.event del envelope OrderUpdateEvent — donde event es lo que ocurrió, no el estado del recurso. La REST API lo recibe en el body de PUT /api/v1/Orders/{orderKey}/status(acepta preparing | ready | handed_over; preparing queda en el enum por forward-compat pero produce 409 porque /accept ya dejó la orden ahí).