Reservei Docs
Referência da API

Webhooks

Configurar e gerenciar webhooks para notificações em tempo real

Webhooks

Receba notificações em tempo real sobre eventos importantes como alterações em reservas, pagamentos e atualizações de status.

Por que usar Webhooks?

Webhooks permitem que você seja notificado automaticamente quando eventos ocorrem, eliminando a necessidade de fazer polling na API. Isso resulta em melhor experiência do usuário e menor consumo de recursos.

Como Funciona

┌─────────────────────┐     ┌─────────────────────┐     ┌─────────────────────┐
│    Evento Ocorre    │────▶│   API Reservei      │────▶│   Seu Servidor      │
│  (ex: bilhete       │     │   envia POST        │     │   processa e        │
│   emitido)          │     │   para sua URL      │     │   responde 200 OK   │
└─────────────────────┘     └─────────────────────┘     └─────────────────────┘
  1. Um evento ocorre na plataforma (reserva criada, bilhete emitido, etc.)
  2. A API Reservei envia um POST para a URL configurada
  3. Seu servidor processa o evento e responde com 200 OK
  4. Se falhar, tentamos novamente com backoff exponencial

GET /api/v1/webhooks

Lista todos os webhooks configurados para seu time.

Response

{
  "success": true,
  "data": [
    {
      "id": "wh_0000AbCdEf123456",
      "url": "https://seu-app.com/api/webhooks/reservei",
      "events": [
        "booking.created",
        "booking.confirmed",
        "booking.ticketed",
        "booking.cancelled"
      ],
      "active": true,
      "created_at": "2025-01-10T10:00:00Z",
      "last_triggered_at": "2025-01-15T14:30:00Z",
      "success_rate": 98.5,
      "total_deliveries": 127,
      "failed_deliveries": 2
    }
  ]
}

Campos

CampoTipoDescrição
idstringID único do webhook
urlstringURL que recebe as notificações
eventsarrayLista de eventos escutados
activebooleanSe o webhook está ativo
secretstringChave para verificar assinaturas (só no momento da criação)
success_ratenumberTaxa de sucesso das entregas (%)
total_deliveriesnumberTotal de notificações enviadas
failed_deliveriesnumberTotal de falhas

POST /api/v1/webhooks

Cria um novo webhook.

Request

{
  "url": "https://seu-app.com/api/webhooks/reservei",
  "events": [
    "booking.created",
    "booking.confirmed",
    "booking.ticketed",
    "booking.cancelled",
    "wallet.credit",
    "wallet.debit"
  ]
}

Parâmetros

CampoTipoObrigatórioDescrição
urlstringSimURL que receberá as notificações (deve ser HTTPS)
eventsarraySimLista de eventos para escutar

Requisitos da URL

  • Deve usar HTTPS (HTTP não é aceito por segurança)
  • Deve ser acessível publicamente
  • Deve responder em menos de 5 segundos
  • Deve retornar status 2xx para confirmar recebimento

Response

{
  "success": true,
  "data": {
    "id": "wh_0000AbCdEf123456",
    "url": "https://seu-app.com/api/webhooks/reservei",
    "events": [
      "booking.created",
      "booking.confirmed",
      "booking.ticketed",
      "booking.cancelled",
      "wallet.credit",
      "wallet.debit"
    ],
    "active": true,
    "secret": "whsec_AbCdEfGhIjKlMnOpQrStUvWxYz123456789"
  }
}

Guarde o Secret!

O secret é exibido apenas uma vez no momento da criação. Use-o para verificar a autenticidade das notificações recebidas.


PUT /api/v1/webhooks/:id

Atualiza um webhook existente.

Request

{
  "url": "https://novo-endpoint.com/webhooks",
  "events": ["booking.created", "booking.ticketed"],
  "active": true
}

Response

{
  "success": true,
  "data": {
    "id": "wh_0000AbCdEf123456",
    "url": "https://novo-endpoint.com/webhooks",
    "events": ["booking.created", "booking.ticketed"],
    "active": true,
    "updated_at": "2025-01-15T16:00:00Z"
  }
}

DELETE /api/v1/webhooks/:id

Remove um webhook.

Response

{
  "success": true,
  "data": {
    "id": "wh_0000AbCdEf123456",
    "deleted": true
  }
}

Eventos Disponíveis

Eventos de Reserva

EventoDescriçãoQuando é Disparado
booking.createdNova reserva criadaApós POST /api/v1/bookings com sucesso
booking.confirmedPagamento confirmadoApós confirmação de pagamento
booking.ticketedBilhetes emitidosQuando companhia emite bilhetes
booking.cancelledReserva canceladaApós cancelamento
booking.expiredReserva expirouPrazo de pagamento excedido
booking.failedFalha na reservaErro na emissão
booking.updatedReserva atualizadaAlteração de dados/status

Eventos de Wallet

EventoDescriçãoQuando é Disparado
wallet.creditCrédito adicionadoRecarga ou estorno confirmado
wallet.debitDébito realizadoReserva confirmada
wallet.recharge.pendingRecarga pendenteRecarga solicitada
wallet.recharge.completedRecarga completaPagamento confirmado
wallet.recharge.expiredRecarga expiradaPrazo de pagamento excedido
wallet.low_balanceSaldo baixoSaldo < R$ 500,00

Eventos de Integração

EventoDescriçãoQuando é Disparado
integration.connectedIntegração conectadaNova integração ativada
integration.disconnectedIntegração desconectadaIntegração removida
api_key.createdNova API keyChave criada
api_key.revokedAPI key revogadaChave desativada

Formato do Payload

Todas as notificações seguem este formato:

{
  "id": "evt_0000AbCdEf123456",
  "type": "booking.ticketed",
  "api_version": "2025-01-01",
  "created_at": "2025-01-15T14:30:00Z",
  "data": {
    "object": {
      "id": "ord_0000GhIjKl789012",
      "booking_reference": "XJ59L2",
      "status": "ticketed",
      "total_amount": "7825.60",
      "currency": "BRL",
      "documents": [
        {
          "type": "electronic_ticket",
          "unique_identifier": "001-1234567890",
          "passenger_id": "pas_0000..."
        }
      ],
      "passengers": [...],
      "slices": [...],
      "created_at": "2025-01-15T10:30:00Z"
    },
    "previous_attributes": {
      "status": "confirmed"
    }
  },
  "team_id": "team_0000...",
  "webhook_id": "wh_0000AbCdEf123456"
}

Campos do Evento

CampoTipoDescrição
idstringID único do evento
typestringTipo do evento
api_versionstringVersão da API
created_atstringTimestamp do evento
data.objectobjectObjeto atual (reserva, transação, etc.)
data.previous_attributesobjectAtributos que mudaram (para eventos de update)
team_idstringID do seu time
webhook_idstringID do webhook que recebeu

Verificando a Assinatura

Para garantir que a notificação veio da Reservei, verifique a assinatura no header X-Reservei-Signature.

Headers Enviados

HeaderDescrição
X-Reservei-SignatureAssinatura HMAC-SHA256 do payload
X-Reservei-TimestampTimestamp Unix do envio
X-Reservei-Event-IdID do evento
Content-Typeapplication/json

Algoritmo de Verificação

  1. Concatene o timestamp + . + payload raw
  2. Calcule HMAC-SHA256 usando seu secret
  3. Compare com a assinatura recebida
import crypto from 'crypto';
import express from 'express';

const app = express();
const WEBHOOK_SECRET = process.env.RESERVEI_WEBHOOK_SECRET;

// Importante: usar raw body para verificação
app.use('/webhook', express.raw({ type: 'application/json' }));

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-reservei-signature'];
  const timestamp = req.headers['x-reservei-timestamp'];
  const payload = req.body.toString();
  
  // Verificar timestamp (previne replay attacks)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return res.status(401).json({ error: 'Timestamp too old' });
  }
  
  // Calcular assinatura esperada
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(signedPayload)
    .digest('hex');
  
  // Comparação segura
  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Processar evento
  const event = JSON.parse(payload);
  console.log(`✅ Evento recebido: ${event.type}`);
  
  switch (event.type) {
    case 'booking.created':
      // Processar nova reserva
      handleBookingCreated(event.data.object);
      break;
    case 'booking.ticketed':
      // Enviar bilhete para cliente
      handleBookingTicketed(event.data.object);
      break;
    case 'wallet.low_balance':
      // Alertar admin
      handleLowBalance(event.data.object);
      break;
  }
  
  // Responder rapidamente
  res.status(200).json({ received: true });
});

function handleBookingCreated(booking) {
  console.log(`Nova reserva: ${booking.booking_reference}`);
}

function handleBookingTicketed(booking) {
  console.log(`Bilhete emitido: ${booking.documents[0].unique_identifier}`);
}

function handleLowBalance(wallet) {
  console.log(`⚠️ Saldo baixo: R$ ${wallet.balance}`);
}
import hmac
import hashlib
import time
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ['RESERVEI_WEBHOOK_SECRET']

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Reservei-Signature')
    timestamp = request.headers.get('X-Reservei-Timestamp')
    payload = request.get_data(as_text=True)
    
    # Verificar timestamp
    now = int(time.time())
    if abs(now - int(timestamp)) > 300:
        return jsonify({'error': 'Timestamp too old'}), 401
    
    # Calcular assinatura
    signed_payload = f"{timestamp}.{payload}"
    expected_signature = hmac.new(
        WEBHOOK_SECRET.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()
    
    # Comparação segura
    if not hmac.compare_digest(signature, expected_signature):
        return jsonify({'error': 'Invalid signature'}), 401
    
    # Processar evento
    event = request.json
    print(f"✅ Evento recebido: {event['type']}")
    
    if event['type'] == 'booking.created':
        handle_booking_created(event['data']['object'])
    elif event['type'] == 'booking.ticketed':
        handle_booking_ticketed(event['data']['object'])
    
    return jsonify({'received': True}), 200

def handle_booking_created(booking):
    print(f"Nova reserva: {booking['booking_reference']}")

def handle_booking_ticketed(booking):
    print(f"Bilhete: {booking['documents'][0]['unique_identifier']}")
<?php

$webhookSecret = getenv('RESERVEI_WEBHOOK_SECRET');
$signature = $_SERVER['HTTP_X_RESERVEI_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_RESERVEI_TIMESTAMP'] ?? '';
$payload = file_get_contents('php://input');

// Verificar timestamp
$now = time();
if (abs($now - intval($timestamp)) > 300) {
    http_response_code(401);
    echo json_encode(['error' => 'Timestamp too old']);
    exit;
}

// Calcular assinatura
$signedPayload = $timestamp . '.' . $payload;
$expectedSignature = hash_hmac('sha256', $signedPayload, $webhookSecret);

// Comparação segura
if (!hash_equals($signature, $expectedSignature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Processar evento
$event = json_decode($payload, true);
error_log("✅ Evento recebido: " . $event['type']);

switch ($event['type']) {
    case 'booking.created':
        handleBookingCreated($event['data']['object']);
        break;
    case 'booking.ticketed':
        handleBookingTicketed($event['data']['object']);
        break;
}

http_response_code(200);
echo json_encode(['received' => true]);

function handleBookingCreated($booking) {
    error_log("Nova reserva: " . $booking['booking_reference']);
}

function handleBookingTicketed($booking) {
    error_log("Bilhete: " . $booking['documents'][0]['unique_identifier']);
}
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io"
    "math"
    "net/http"
    "os"
    "strconv"
    "time"
)

var webhookSecret = os.Getenv("RESERVEI_WEBHOOK_SECRET")

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-Reservei-Signature")
    timestamp := r.Header.Get("X-Reservei-Timestamp")
    
    payload, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Failed to read body", http.StatusBadRequest)
        return
    }
    
    // Verificar timestamp
    ts, _ := strconv.ParseInt(timestamp, 10, 64)
    now := time.Now().Unix()
    if math.Abs(float64(now-ts)) > 300 {
        http.Error(w, "Timestamp too old", http.StatusUnauthorized)
        return
    }
    
    // Calcular assinatura
    signedPayload := timestamp + "." + string(payload)
    mac := hmac.New(sha256.New, []byte(webhookSecret))
    mac.Write([]byte(signedPayload))
    expectedSignature := hex.EncodeToString(mac.Sum(nil))
    
    // Comparação segura
    if !hmac.Equal([]byte(signature), []byte(expectedSignature)) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }
    
    // Processar evento
    var event map[string]interface{}
    json.Unmarshal(payload, &event)
    
    eventType := event["type"].(string)
    println("✅ Evento recebido:", eventType)
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]bool{"received": true})
}

Política de Retentativas

Se sua URL não responder com 2xx, tentaremos novamente com backoff exponencial:

TentativaIntervaloHorário Exemplo
Imediato10:00:00
5 minutos10:05:00
30 minutos10:35:00
2 horas12:35:00
12 horas00:35:00
24 horas00:35:00 (+1 dia)

Após 6 tentativas falhas consecutivas, o webhook é desativado automaticamente. Você receberá um email de alerta e precisará reativar manualmente no painel.


Boas Práticas

✅ Faça

  1. Responda rapidamente - Retorne 200 OK em até 5 segundos
  2. Processe assincronamente - Use filas (Redis, SQS, RabbitMQ) para tarefas demoradas
  3. Implemente idempotência - O mesmo evento pode ser enviado mais de uma vez
  4. Verifique a assinatura - Sempre valide que a requisição veio da Reservei
  5. Use HTTPS - Obrigatório para receber webhooks
  6. Monitore entregas - Acompanhe a taxa de sucesso no painel
  7. Trate erros graciosamente - Log erros mas retorne 200 se possível

❌ Não Faça

  1. Processar no handler - Não execute lógica pesada síncrona
  2. Ignorar duplicados - Mesmo evento pode vir mais de uma vez
  3. Confiar cegamente - Sempre verifique a assinatura
  4. Usar HTTP - Apenas HTTPS é aceito
  5. Timeout longo - Não demore mais que 5 segundos para responder
  6. Ignorar alertas - Falhas consecutivas desativam o webhook

Exemplo de Processamento Assíncrono

import { Queue } from 'bullmq';

const webhookQueue = new Queue('webhook-events');

app.post('/webhook', async (req, res) => {
  // Verificar assinatura...
  
  // Enfileirar para processamento assíncrono
  await webhookQueue.add('process-event', {
    event: req.body
  });
  
  // Responder imediatamente
  res.status(200).json({ received: true });
});

// Worker processa em background
const worker = new Worker('webhook-events', async (job) => {
  const { event } = job.data;
  
  switch (event.type) {
    case 'booking.ticketed':
      await sendTicketToCustomer(event.data.object);
      await updateCRM(event.data.object);
      await notifySlack(event.data.object);
      break;
  }
});

Testando Webhooks

Ambiente de Desenvolvimento

Use ferramentas como ngrok ou localtunnel para expor localhost:

# Instalar ngrok
npm install -g ngrok

# Expor porta 3000
ngrok http 3000

# Use a URL gerada (ex: https://abc123.ngrok.io/webhook)

Teste Manual via cURL

Simule um evento localmente:

curl -X POST http://localhost:3000/webhook \
  -H "Content-Type: application/json" \
  -H "X-Reservei-Signature: test" \
  -H "X-Reservei-Timestamp: $(date +%s)" \
  -d '{
    "id": "evt_test123",
    "type": "booking.created",
    "data": {
      "object": {
        "id": "ord_test",
        "booking_reference": "TEST01"
      }
    }
  }'

Erros Comuns

CódigoErroCausaSolução
400Invalid URLURL malformada ou HTTPUse HTTPS válido
400Invalid eventsEventos não reconhecidosVerifique lista de eventos
404Webhook not foundID inválidoVerifique o ID
409URL already registeredURL já existeUse URL diferente
422URL not reachableNão conseguimos acessarVerifique firewall/DNS