Argentive Card te POSTea eventos firmados a tus endpoints registrados. Mismo contrato y formato que Stripe / AgentCard, pensado para que sea trivial de integrar.
payload = t + "." + raw_body (el body como llegó, sin parsear)
expected = HMAC_SHA256(secret, payload) (hex lowercase)
verify = compare_constant_time(v1, expected)
fresh = (now_epoch - t) <= 300 (5 min de tolerancia)
ok = verify && fresh
Atención
Usá el body raw, no el parseado. Cualquier reformateo del JSON (espacios, orden de keys) invalida la firma. En Express usá express.raw({ type: 'application/json' }).
Verificación en Node.js
verify.ts
import crypto from 'crypto'
const SECRET = process.env.ARGENTIVE_WEBHOOK_SECRET! // whsec_...
const TOLERANCE_SECONDS = 300
export function verifyWebhook(rawBody: Buffer, signatureHeader: string): boolean {
const match = signatureHeader.match(/^t=(\d+),v1=([0-9a-f]+)$/)
if (!match) return false
const [, t, v1] = match
// 1. Anti-replay
const age = Math.floor(Date.now() / 1000) - Number(t)
if (Math.abs(age) > TOLERANCE_SECONDS) return false
// 2. Reconstruir y comparar
const payload = `${t}.${rawBody.toString('utf8')}`
const expected = crypto
.createHmac('sha256', SECRET)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(v1, 'hex'),
Buffer.from(expected, 'hex'),
)
}
Verificación en Python
verify.py
import os, hmac, hashlib, time, re
SECRET = os.environ['ARGENTIVE_WEBHOOK_SECRET']
TOLERANCE_SECONDS = 300
def verify_webhook(raw_body: bytes, signature_header: str) -> bool:
m = re.match(r'^t=(\d+),v1=([0-9a-f]+)$', signature_header)
if not m:
return False
t, v1 = m.groups()
# 1. Anti-replay
if abs(int(time.time()) - int(t)) > TOLERANCE_SECONDS:
return False
# 2. HMAC
payload = f'{t}.{raw_body.decode("utf-8")}'
expected = hmac.new(SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(v1, expected)
Best practices del handler
1. Respondé 2xx rápido
Argentive considera success cualquier respuesta 2xx. Si tardás más de 10 segundos, marcamos el intento como fallido y reintentamos. Mejor: respondé 200 inmediatamente y procesá async en background.
handler.ts
// Next.js route handler
export async function POST(req: Request) {
const rawBody = Buffer.from(await req.arrayBuffer())
const signature = req.headers.get('Argentive-Signature') ?? ''
if (!verifyWebhook(rawBody, signature)) {
return new Response('invalid signature', { status: 401 })
}
const event = JSON.parse(rawBody.toString('utf8'))
// Procesar async — NO bloquear la respuesta
void processEvent(event).catch(console.error)
return new Response('ok', { status: 200 })
}
2. Idempotencia por evt_id
Por diseño Argentive puede entregar el mismo evento más de una vez (en reintentos tras un timeout, por ejemplo). Dedupe usando el campo id:
processEvent.ts
async function processEvent(event: ArgentiveEvent) {
const already = await db.processedWebhooks.findUnique({ where: { evtId: event.id } })
if (already) return // ya lo procesamos
await db.processedWebhooks.create({ data: { evtId: event.id, type: event.type } })
switch (event.type) {
case 'transaction.cleared': return handleClear(event.data.object)
case 'card.closed': return handleCardClosed(event.data.object)
// ...
}
}
3. No actúes sobre transaction.authorized
Atención
transaction.authorized es un estado intermedio — puede ser seguido porcleared O voided. Si actualizás tu UI o emitís comprobante en authorized, vas a tener inconsistencias. Confiá en cleared para todo lo que afecte usuarios o contabilidad.
4. Manejá failure count
Si tu endpoint falla 50 veces consecutivas, Argentive lo marca como status: degraded y reduce la frecuencia de reintentos. Después de 100 fallos consecutivos pasa a disabled y dejamos de enviarte eventos. Tenés que reactivarlo desde el dashboard o vía API.
Testing
Para desarrollo local, usá el CLI con argentive-admin listen — tunelea webhooks productivos o test a tu localhost. Ver CLI admin.