Documentação · Guias

Webhooks: receber e verificar

O Urdume envia um POST com JSON para o seu endereço sempre que acontece algo que subscreveu. Cada envio é assinado com o segredo do endereço.

1. Configurar

  1. Em Programadores › Webhooks, crie um endereço HTTPS e escolha os eventos (ou todos).
  2. Guarde o segredo (whsec_…) no servidor, por exemplo em URDUME_WEBHOOK_SECRET. Só é mostrado quando o pede.
  3. Use "Enviar evento de teste" para experimentar. Os endereços de teste recebem só os eventos de teste, e os de produção só os de produção.

2. O que chega

Pedido

POST /webhooks/urdume HTTP/1.1
Content-Type: application/json; charset=utf-8
Urdume-Signature: t=1790086400,v1=5f1c…
Urdume-Event-Id: 01JA4…
Urdume-Event-Type: passport_session.completed

{
  "id": "01JA4…",
  "object": "event",
  "type": "passport_session.completed",
  "api_version": "v1",
  "created": 1790086400,
  "livemode": false,
  "data": {
    "object": { "id": "01JA2…", "object": "passport_session", "status": "completed", ... }
  }
}

"data.object" é o objeto em causa, tal como a API o devolve. A lista completa de eventos está na referência da API.

3. Verificar a assinatura

Urdume-Signature tem um timestamp (t) e a assinatura (v1) = HMAC-SHA256 do texto "t.corpo" com o segredo. Calcule-a sobre o corpo em bruto, compare em tempo constante e recuse timestamps com mais de 5 minutos.

Verificação

PHP

<?php
$body = file_get_contents('php://input');           // o corpo exatamente como chegou
$header = $_SERVER['HTTP_URDUME_SIGNATURE'] ?? '';
$secret = getenv('URDUME_WEBHOOK_SECRET');            // whsec_… (o valor completo)

$parts = [];
foreach (explode(',', $header) as $piece) {
    [$key, $value] = array_pad(explode('=', trim($piece), 2), 2, '');
    $parts[$key][] = $value;
}
$timestamp = (int) ($parts['t'][0] ?? 0);
$expected = hash_hmac('sha256', $timestamp . '.' . $body, $secret);
$valid = abs(time() - $timestamp) <= 300
    && array_filter($parts['v1'] ?? [], static fn ($s) => hash_equals($expected, $s)) !== [];
if (!$valid) {
    http_response_code(400);
    exit;
}

$event = json_decode($body, true);
if ($event['type'] === 'passport_session.completed') {
    $session = $event['data']['object'];
    // ex.: marcar o produto $session['metadata']['produto_id'] como "com passaporte"
}
http_response_code(200);

Node

const crypto = require('crypto');
const express = require('express');

// O corpo tem de chegar em bruto (Buffer) para a assinatura bater certo.
app.post('/webhooks/urdume', express.raw({ type: 'application/json' }), (req, res) => {
  const parts = {};
  for (const piece of (req.get('Urdume-Signature') || '').split(',')) {
    const [key, value] = piece.trim().split('=');
    (parts[key] ||= []).push(value);
  }
  const timestamp = Number(parts.t?.[0] || 0);
  const expected = crypto
    .createHmac('sha256', process.env.URDUME_WEBHOOK_SECRET)
    .update(`${timestamp}.`)
    .update(req.body)
    .digest('hex');
  const valid = Math.abs(Date.now() / 1000 - timestamp) <= 300 &&
    (parts.v1 || []).some((s) => s.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
  if (!valid) return res.sendStatus(400);

  const event = JSON.parse(req.body);
  if (event.type === 'passport_session.completed') {
    // ...
  }
  res.sendStatus(200);
});

Python

import hashlib, hmac, json, os, time
from flask import abort, request

@app.post("/webhooks/urdume")
def urdume_webhook():
    body = request.get_data()  # bytes, exatamente como chegaram
    parts = {}
    for piece in request.headers.get("Urdume-Signature", "").split(","):
        key, _, value = piece.strip().partition("=")
        parts.setdefault(key, []).append(value)
    timestamp = int(parts.get("t", ["0"])[0] or 0)
    expected = hmac.new(
        os.environ["URDUME_WEBHOOK_SECRET"].encode(), f"{timestamp}.".encode() + body, hashlib.sha256
    ).hexdigest()
    valid = abs(time.time() - timestamp) <= 300 and any(
        hmac.compare_digest(expected, s) for s in parts.get("v1", [])
    )
    if not valid:
        abort(400)

    event = json.loads(body)
    if event["type"] == "passport_session.completed":
        session = event["data"]["object"]
        # ...
    return "", 200

Boas práticas

  • Responda 2xx depressa (em menos de 10 segundos) e faça o trabalho pesado depois. Outra resposta conta como falha.
  • O mesmo evento pode chegar mais do que uma vez: use Urdume-Event-Id para não o tratar duas vezes.
  • As falhas repetem-se ao fim de 1 min, 5 min, 30 min, 2 h, 5 h, 10 h e 24 h. Se 10 entregas seguidas esgotarem as tentativas, o endereço é desligado (volta a ligar-se no painel, onde também pode reenviar entregas).
  • Não siga redirecionamentos: o Urdume não os segue e só envia para endereços HTTPS públicos.
Guia seguinte Modo de teste →