Ir al contenido
StampyWalletDocs
API y webhooks / Webhooks / Webhooks

Webhooks

Beta

Recibe una llamada HTTP firmada en tu servidor, Zapier, Make o n8n en cada visita, recompensa o tarjeta nueva, y verifica su firma HMAC.

Un webhook es una dirección de tu servidor a la que StampyWallet llama en cada evento del mostrador: una visita, una recompensa, una tarjeta nueva, una tarjeta regalo vendida. Es lo que conecta StampyWallet con un TPV, un CRM, una hoja de cálculo o una herramienta sin código, sin integración oficial.

¿En el otro sentido? : para que tu TPV añada un sello en cada venta, usa POST /v1/passages, o recibe las ventas de tu TPV directamente.

Configurar una dirección

En Ajustes › Integraciones › Webhooks (el propietario y los encargados):

  1. Añade una dirección https:// pública y marca, si quieres, los eventos. Ninguna casilla marcada: todos los eventos, incluidos los que se añadan más adelante.
  2. Copia el secreto de firma, que empieza por whsec_. Solo se muestra una vez; después la pantalla solo enseña los cuatro últimos caracteres.
  3. Haz clic en «Enviar una prueba»: sale enseguida hacia tu dirección un passage.cree de ejemplo, marcado con "test": true, y la pantalla muestra la respuesta obtenida.

La pantalla muestra los 20 últimos envíos (estado, código, duración, intento) y un botón Reenviar en los que han fallado. Diez direcciones como máximo por comercio; cada una se puede desactivar, reactivar o eliminar.

Atención : se rechazan: una dirección que no sea https://, una dirección que contenga credenciales (https://yo:secreto@…) y cualquier dirección privada o local (localhost, 10.x, 192.168.x…), aunque se obtenga por DNS. Se aceptan todos los puertos. Las redirecciones nunca se siguen: una respuesta 3xx cuenta como fallo.

Lo que recibe tu servidor

Una petición POST en JSON, con estas cabeceras:

Cabeceras
POST /votre/adresse HTTP/1.1
Content-Type: application/json
User-Agent: StampyWallet-Webhooks/1.0
StampyWallet-Evenement: passage.cree
StampyWallet-Id: evt_WdGHduXPRbExh0vh5eYtCjXo
StampyWallet-Signature: t=1792648365,v1=8f3c…(64 caracteres hexadecimales)

Y este cuerpo. El sobre es el mismo para todos los eventos; solo cambia donnees (la lista de eventos):

passage.cree
{
  "id": "evt_WdGHduXPRbExh0vh5eYtCjXo",
  "type": "passage.cree",
  "cree_le": "2026-10-22T05:52:45.694Z",
  "commerce": { "id": "9f1cbdfc-df71-4fab-a554-3bdbb7c71ebf", "nom": "Le Café des Crochets" },
  "donnees": {
    "client": { "id": "40de4b86-1d7a-4cc6-add3-b7a83fa43c04", "prenom": "Inès",
                "telephone": "+33611223344", "email": "ines@example.com" },
    "passage": {
      "id": "226fdea1-ba59-49fb-93e6-60a14a765134",
      "quantite": 1,
      "montant": null,
      "devise": "EUR",
      "reference": "caisse-1",
      "source": "scan",
      "piste": "normal",
      "tampons": 1,
      "objectif": 2
    }
  }
}
  • id: evt_ seguido de 24 caracteres. El mismo en todos los envíos de un evento (intentos, reenvíos, y de una dirección a otra): es tu clave para eliminar duplicados.
  • cree_le: el momento del evento (no el del envío), en ISO 8601 UTC.
  • test: presente, a true, solo en el envío del botón «Enviar una prueba».
  • donnees.client: { id, prenom }, más telephone y email solo si el cliente los ha dado; null para una tarjeta regalo o un bono.
  • Los importes (montant, prix) van en céntimos, enteros, con su devise (ISO 4217).
  • Pueden aparecer claves nuevas: ignora lo que no conozcas.

Verificar la firma

Verifica cada petición antes de fiarte de su contenido: sin eso, cualquiera podría llamar a tu dirección.

El cálculo
StampyWallet-Signature: t=<marca de tiempo unix en segundos>,v1=<firma>
firma = hex( HMAC-SHA256( clave = secreto, mensaje = "<t>.<cuerpo sin procesar>" ) )
  • La clave es el secreto completo, prefijo whsec_ incluido.
  • El mensaje es la marca de tiempo t, un punto y luego el cuerpo tal como se recibe, en bytes. No vuelvas a leer el JSON antes de verificar: un JSON reescrito ya no tiene los mismos bytes.
  • Rechaza un t a más de 5 minutos de tu reloj: así se impide reenviar una petición antigua.
  • La cabecera puede llevar varios v1= (el día de un cambio de secreto): basta con que coincida uno. Compara en tiempo constante.
const crypto = require('node:crypto');

/** corpsBrut: el cuerpo recibido, en Buffer (o en texto), ANTES de cualquier JSON.parse. */
function verifierStampyWallet(secret, corpsBrut, entete, tolerance = 300) {
  let t = null;
  const v1 = [];
  for (const morceau of String(entete || '').split(',')) {
    const i = morceau.indexOf('=');
    const cle = morceau.slice(0, i).trim();
    const valeur = morceau.slice(i + 1).trim();
    if (cle === 't' && /^\d+$/.test(valeur)) t = Number(valeur);
    if (cle === 'v1') v1.push(valeur);
  }
  if (t === null || v1.length === 0) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > tolerance) return false;
  const attendue = crypto.createHmac('sha256', secret)
    .update(`${t}.`).update(corpsBrut).digest('hex');
  return v1.some((s) => s.length === attendue.length
    && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(attendue)));
}

// Con Express: conservar el cuerpo sin procesar en esta ruta.
app.post('/stampywallet', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifierStampyWallet(process.env.STAMPYWALLET_WEBHOOK_SECRET,
    req.body, req.get('StampyWallet-Signature'));
  if (!ok) return res.status(400).end();
  const evenement = JSON.parse(req.body.toString('utf8'));
  // eliminar duplicados con evenement.id y procesar más tarde
  res.status(200).end();
});

Vector de prueba

Para comprobar tu código: con estos valores y una tolerancia infinita (Infinity, float("inf"), PHP_INT_MAX), la función debe devolver verdadero. El cuerpo ocupa una sola línea, sin salto de línea final, y contiene una «é» a propósito: la firma se calcula sobre los bytes UTF-8.

Vector
secret : whsec_5WZbPq8rLk2N7xYdTf3Hc9JmVg4RsQe1
t      : 1761134400
corps  : {"id":"evt_4fK9mQ2xT7bL1nR8vZ3cW6yH","type":"passage.cree","cree_le":"2026-10-22T12:00:00.000Z","commerce":{"id":"9b2f6c1e-4d3a-4f7b-8e21-5a6c7d8e9f01","nom":"Le Café du Coin"},"donnees":{"client":{"id":"3c1d2e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f","prenom":"Camille"},"passage":{"id":"7e8f9a0b-1c2d-4e3f-9a4b-5c6d7e8f9a0b","quantite":1,"montant":null,"devise":"EUR","reference":null,"source":"scan","piste":"normal","tampons":4,"objectif":10}}}
v1     : 4346dff69a3e9e25937b22ec031a8eaf8cfce3baca4dfb932865682b8f891f1d
en-tête: t=1761134400,v1=4346dff69a3e9e25937b22ec031a8eaf8cfce3baca4dfb932865682b8f891f1d

Responder, y los reintentos

  • Responde con un código 2xx en menos de 10 segundos. El cuerpo de tu respuesta se ignora. Responde rápido y haz el trabajo largo después.
  • Si no, ocho intentos en total: el primero enseguida, luego a 1 min, 5 min, 30 min, 2 h, 5 h, 8 h y 8 h. El último sale unas 23 h 30 después del primero; al octavo fallo, el envío se abandona.
  • Tras 50 fallos seguidos en una dirección, se desactiva y el propietario del comercio recibe un correo. Reactívala cuando hayas arreglado tu servidor: su contador vuelve a cero.
  • El envío de prueba solo se intenta una vez y no cuenta en los fallos.
  • Reenviar vuelve a enviar el mismo evento (mismo id, mismos bytes), con una firma nueva.
  • Al menos una vez, sin orden garantizado: un evento puede llegar dos veces, elimina duplicados con StampyWallet-Id; dos eventos cercanos pueden llegar desordenados, guíate por cree_le.
  • Los envíos se conservan 30 días en el registro.
Estado de un envíoSignificado
en_attenteAún no se ha intentado
echecEl último intento ha fallado; hay otro previsto
livreSe ha recibido un 2xx
abandonneOcho fallos, o dirección desactivada, o sustituido por un reenvío

Sin código : Zapier, Make y n8n reciben estos webhooks tal cual: pega la dirección de su disparador «Webhook» como dirección. Los pasos a seguir están en Conectar tu TPV.

Actualizado el

¿Te ha sido útil esta página?