Saltar al contenido principal

Webhooks

Los salientes avisan a tu sistema cuando ocurre algo en el programa. Los entrantes son los avisos que IncentIA recibe de tiendas, pasarelas y proveedores de evidencias.

Salientes​

Los creas en Integraciones › Webhooks (permiso Suscripciones webhook, Pages.Loyalty.Webhooks). Cómo se configuran, en Webhooks. Desde la pantalla, una suscripción escucha un evento; por API puede escuchar varios.

Eventos​

EventoCuándo se envía
MemberCreatedSe registra un socio
PointsEarnedUn socio gana puntos
PointsRedeemedUn socio gasta puntos
TierChangedUn socio cambia de nivel
RewardRedeemedUn socio canjea una recompensa
CouponUsedSe usa un cupón
PointsExpiringA un socio le van a caducar puntos
GamePrizeWonUn participante gana un premio en un juego

Eventos del ciclo de vida​

Además de los ocho anteriores, hay eventos de participación, juegos, sorteos, cupones, socios y pagos. Comparten el formato, las cabeceras y la firma de cualquier otro aviso.

X-Event-TypeValor (eventTypes)data.eventCuándo se envía
EvidenceReceived9evidence.receivedSe recibe un ticket (reclamación)
EvidenceValidated10evidence.validatedSe aprueba
EvidenceRejected11evidence.rejectedSe rechaza o es duplicado
EvidenceInReview12evidence.in_reviewEspera revisión de una persona
EvidencePaid13evidence.paidSe liquida su pago
ParticipationCreated14participation.createdSe registra una participación nueva
ParticipationValidated15participation.validatedUna participación pasa a validada
ParticipationRejected16participation.rejectedUna participación pasa a rechazada
GamePlayed17game.playedTermina una jugada, con premio o sin él
GamePrizeWon8prize.wonUna jugada otorga un premio (también participantes sin cuenta de socio)
RaffleDrawn18raffle.drawnSe realiza un sorteo (uno por sorteo; en uno por tienda, uno por cada tienda)
CouponIssued19coupon.issuedSe crea un cupón desde el panel o se completa una tarjeta de sellos. No se envía al emitir lotes por API
MemberUpdated20member.updatedCambia un dato del perfil de un socio
MemberAnonymized21member.anonymizedTermina un borrado RGPD
PayoutCompleted22payout.completedSe completa o confirma un pago de cashback
PayoutFailed23payout.failedUn pago falla de forma definitiva (no se envía si hay un reintento pendiente)
JobCompleted24job.completedTermina un trabajo masivo de /api/v1/jobs (importación o exportación), bien o con error: mira status

X-Event-Type y el campo event del aviso llevan el nombre de la primera columna; data.event lleva el nombre con puntos. data empieza siempre por event, version (versión del formato, hoy 1) y occurredAt, seguidos solo de identificadores, códigos, estados e importes: nunca nombres, datos de contacto ni datos bancarios. Añadir campos no sube version; quitarlos o renombrarlos sí.

{
"event": "ParticipationValidated",
"timestamp": "2026-11-01T10:15:02.0000000Z",
"data": {
"event": "participation.validated",
"version": 1,
"occurredAt": "2026-11-01T10:15:02Z",
"attemptId": 991, "campaignId": 17, "channel": "Landing", "trigger": "Evidence",
"status": "Validated", "outcome": null, "reason": null,
"participantId": 55, "memberId": null, "claimId": 1042
}
}
EventoCampos principales de data
participation.*attemptId, campaignId, channel, trigger, status, outcome, participantId, memberId, claimId; reason solo en rejected
game.playedplayId, gameId, memberId, participantId, result, prizeId, pointsSpent, pointsWon
prize.wonplayId, gameId, memberId, participantId, prizeId, prizeName, prizeType, pointsAwarded, couponCode
raffle.drawndrawId, campaignId, drawnAt, scope, storeId, slotKey, recuentos, outputHashSha256 y winners[] (winnerId, role, participantId, memberId, claimId)
coupon.issuedcouponId, code, source, loyaltyProgramId, campaignId, memberId, type, discountValue, maxUses, validFrom, validUntil
member.updatedmemberId, loyaltyProgramId, status, changedFields (solo los nombres de los campos), source
member.anonymizedmemberId, loyaltyProgramId, certificateId
payout.*payoutId, memberId, campaignId, claimId, status, type, amount, currency, processedAt
evidence.*claimId, campaignId, externalParticipantRef, status, rejectionCode, rejectionReason, amount, currency

Todos se emiten cuando la operación que los origina ya se ha confirmado: si se deshace, no se envía nada. En prize.won y coupon.issued va el código del cupón, porque es lo que tu sistema debe entregar.

Suscribirse a estos eventos

El selector de Tipo de Evento de Integraciones › Webhooks ofrece hoy solo los ocho primeros. Para el resto, crea o edita la suscripción con la API de administración (/api/services/app/WebhookSubscription/Create o Update) y envía los valores de la tabla en eventTypes, por ejemplo "eventTypes": [14, 15, 16, 18]. Una suscripción puede escuchar varios eventos.

También puedes recibir los eventos en tu cola SQS o tema SNS en lugar de en una URL.

Formato del aviso​

IncentIA hace un POST con Content-Type: application/json:

{
"event": "PointsEarned",
"timestamp": "2026-10-22T09:30:00.0000000Z",
"data": { }
}

data depende del evento. El aviso se emite cuando la operación que lo origina ya se ha confirmado.

CabeceraContenido
X-Event-TypeNombre del evento
X-TimestampFecha del aviso (ISO 8601)
X-Correlation-IdIdentificador de seguimiento
X-IncentIA-Idempotency-KeyClave única del aviso. No cambia en los reintentos
X-SignatureFirma HMAC (solo si la suscripción tiene Secreto)

Verificar la firma​

La firma es v1= seguido del HMAC-SHA256 en hexadecimal (minúsculas) de este texto, con tu Secreto como clave:

{X-Timestamp}.{X-IncentIA-Idempotency-Key}.{cuerpo sin modificar}

Usa el valor de X-Timestamp tal cual llega y el cuerpo en bruto, sin volver a serializarlo.

Antes de calcular nada, comprueba el prefijo v1= (ver la política de versiones) y compara en tiempo constante.

TypeScript

import crypto from 'node:crypto';

export function isValid(headers: Record<string, string | undefined>, rawBody: string, secret: string): boolean {
const signed = `${headers['x-timestamp']}.${headers['x-incentia-idempotency-key']}.${rawBody}`;
const expected = 'v1=' + crypto.createHmac('sha256', secret).update(signed).digest('hex');
const received = headers['x-signature'] ?? '';
return received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}

C#

using System.Security.Cryptography;
using System.Text;

static bool IsValid(string timestamp, string idempotencyKey, string rawBody, string received, string secret)
{
var signed = $"{timestamp}.{idempotencyKey}.{rawBody}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = "v1=" + Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(signed))).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(received ?? ""));
}

PHP

<?php
function isValid(array $headers, string $rawBody, string $secret): bool {
$signed = ($headers['X-Timestamp'] ?? '') . '.' . ($headers['X-IncentIA-Idempotency-Key'] ?? '') . '.' . $rawBody;
$expected = 'v1=' . hash_hmac('sha256', $signed, $secret);
return hash_equals($expected, $headers['X-Signature'] ?? '');
}

// $rawBody = file_get_contents('php://input'); // sin json_decode

Como la firma incluye la fecha y la clave de idempotencia, comprobarla valida también esas cabeceras. Guarda las claves ya procesadas para descartar duplicados.

Entrega y reintentos​

  • Responde con 2xx en menos de 30 segundos.
  • Ante un error se reintenta hasta tres veces más. Las respuestas 408, 425 y 429 también se reintentan.
  • IncentIA no sigue redirecciones: un 3xx cuenta como fallo.
  • La URL de destino debe ser pública; las direcciones internas se rechazan.
  • La suscripción se desactiva sola tras 10 fallos consecutivos, o 50 respuestas de saturación (408/425/429) seguidas. Verás el contador en Fallos Consecutivos.
tip

Responde 200 enseguida y procesa el aviso en segundo plano: así no agotas el tiempo de espera.

Reenviar una entrega​

Si tu receptor estuvo caído, puedes pedir que se reenvíe una entrega ya registrada: en Integraciones › Webhooks, pestaña Entregas, botón Reenviar, o por API con una clave con el ámbito webhooks:write:

curl -s -X POST "$BASE/api/v1/webhook-deliveries/4821/redeliver" \
-H "Authorization: Bearer $KEY"
# 202 {"deliveryId":4821,"subscriptionId":12,"eventType":"PointsEarned","idempotencyKey":"..."}

Se reenvía el mismo contenido y la misma X-IncentIA-Idempotency-Key (tu sistema debe deduplicar), con una X-Timestamp y una firma nuevas, y solo a la URL original. Respuestas de rechazo:

CódigocodeCuándo
404not_foundLa entrega no existe en tu cuenta
409subscription_inactiveLa suscripción está desactivada
409event_not_subscribedLa suscripción ya no incluye ese evento
409target_changedCambiaste la URL de destino después de la entrega
409delivery_in_progressYa hay un reenvío o reintento en curso
410subscription_goneLa suscripción se eliminó
410payload_expiredEl contenido ya no se conserva (14 días por defecto)
403feature_disabledWebhooks no está incluido en el plan

Entrantes​

Son rutas públicas que protege una firma. Normalmente las configura IncentIA al conectar la integración; esta tabla es para depurar. El registro de entregas está en Administración › Webhooks Entrantes.

OrigenRutaFirma
ShopifyPOST /api/webhooks/shopify/{integrationId}X-Shopify-Hmac-Sha256 (HMAC-SHA256 en Base64 del cuerpo)
WooCommercePOST /api/webhooks/woocommerce/{integrationId}X-WC-Webhook-Signature (HMAC-SHA256 en Base64 del cuerpo)
StripePOST /api/member/portal/payments/stripe-webhookStripe-Signature
Evidencias (validación externa)POST /api/evidence/callbackX-Evidence-Signature

En Shopify y WooCommerce la clave de la firma es el secreto de la integración. Los temas no admitidos se ignoran con 200 y {"status":"ignored"}.

Callback de evidencias​

Un proveedor externo de validación devuelve el veredicto de un ticket. Firma el cuerpo en bruto con HMAC-SHA256, en hexadecimal o Base64, usando el secreto de callback del servidor, y envíalo en X-Evidence-Signature.

{ "evidenceId": 123, "approved": true, "amount": 24.5, "reason": null, "timestamp": 1793750400 }
CampoNotas
evidenceIdObligatorio
approvedtrue aprueba, false rechaza
amountOpcional. Importe validado, hasta 1 000 000
reasonOpcional. Hasta 500 caracteres
timestampObligatorio. Segundos Unix del momento de la firma; se rechaza si se aleja más de 5 minutos de la hora del servidor

Respuestas: 200 con {"status":"processed"}, 401 si falta o no coincide la firma o si timestamp falta o está fuera de la ventana, 422 si la evidencia ya la rechazó o marcó como duplicada una persona (un approved: true no puede revertir ese veredicto), 404 si no existe la evidencia y 503 si el servidor no tiene el secreto configurado. El cuerpo admite como máximo 2 MB.

Relacionado​