Webhooks

Receção de eventos em tempo real (subscrições, pagamentos, notificações) via HTTP POST no seu endpoint. Sem necessidade de polling.


Resumo

Item Descrição
Funcionamento O MetCare envia POST para o seu URL quando ocorre um evento.
Segurança Header X-Signature: HMAC-SHA256(secret, raw body). Validar sempre.
Configuração Contactar developers@met-care.com para registar webhook e obter secret.
URLs API Sandbox — https://integration.met-care.com/sandbox/v1 · Produção — https://integration.met-care.com/v1

Visão geral

  • Tempo real — Eventos enviados logo após ocorrência
  • Eficiente — Sem polling
  • Confiável — Retry automático em falhas
  • Seguro — Assinatura digital (secret)

Configuração

A configuração é feita através da equipa MetCare. Contacte developers@met-care.com para webhooks e tokens.

O que recebe ao registar

  • Um secret único (devolvido uma vez). Guarde em variável de ambiente ou secret manager.
  • Não exponha o secret em frontend, repositórios ou logs.
  • Com o secret, o seu servidor verifica que cada POST é da MetCare e que o corpo não foi alterado.

Informações para registo

  • URL do endpoint — Pública, HTTPS em produção
  • Eventos desejados — Lista (ex.: ["subscription.created", "subscription.paid"]) ou ["*"] para todos
  • Consumer name — Identificador único por URL

Lista completa de eventos

Use estes valores ao registar ou ["*"] para todos. Fonte: internal/platform/events.

Subscrições

Evento Descrição
subscription.created Nova subscrição criada
subscription.activated Subscrição ativada
subscription.paid Pagamento da subscrição recebido
subscription.cancelled Subscrição cancelada
subscription.renewed Subscrição renovada
subscription.expired Subscrição expirada
subscription.updated Subscrição atualizada
subscription.suspended Subscrição suspensa

Pagamentos

Evento Descrição
payment.received Pagamento recebido
payment.failed Pagamento falhado
payment.refunded Pagamento reembolsado
payment.pending Pagamento pendente

Notificações

Evento Descrição
notification.created Notificação criada
notification.completed Notificação concluída
notification.failed Notificação falhada
notification.channel.webhook.sent / .failed Webhook de notificação
notification.channel.email.created / .sent / .failed Email
notification.channel.sms.created / .sent / .failed SMS
notification.channel.push.created / .sent / .failed Push

Formato do payload

Todas as requisições seguem o mesmo formato:

{
  "event": "subscription.updated",
  "timestamp": "2026-01-15T10:30:00Z",
  "data": { }
}
Campo Tipo Descrição
event string Tipo do evento
timestamp datetime (RFC3339) Data/hora do evento
data object Dados do evento (estrutura varia)

Exemplo: subscription.created

{
  "event": "subscription.created",
  "timestamp": "2026-01-15T10:30:00Z",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "userId": 12345,
    "tutorId": 67890,
    "aggregateId": "AGG-2026-001",
    "status": "active",
    "statusDate": "2026-01-15T10:30:00Z",
    "healthPlanId": "plan-premium-001",
    "startDate": "2026-01-15T00:00:00Z",
    "endDate": "2026-12-31T23:59:59Z",
    "pricePaid": 29990,
    "createdAt": "2026-01-15T10:30:00Z",
    "updatedAt": "2026-01-15T10:30:00Z",
    "type": "monthly"
  }
}

Valores em Kz (Kwanza). Telefones angolanos: +244 9XX XXX XXX.

Exemplo: subscription.suspended

{
  "event": "subscription.suspended",
  "timestamp": "2026-01-15T10:30:00Z",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "userId": 12345,
    "status": "suspended",
    "statusDate": "2026-01-15T10:30:00Z",
    "statusReason": "payment_failed",
    "statusNotes": "Pagamento em atraso após 3 tentativas"
  }
}

Exemplo: evento de notificação

O data dos eventos de notificação segue a estrutura do payload de notificação (sem dados sensíveis). Ver Notificações para enviar notificações via API.

{
  "event": "notification.channel.email.sent",
  "timestamp": "2026-01-15T10:30:00Z",
  "data": {
    "id": "uuid-da-notificacao",
    "title": "Lembrete de renovação",
    "message": "A sua subscrição renova em 5 dias.",
    "type": "reminder",
    "priority": "normal",
    "channels": ["email", "sms"],
    "plan_id": null,
    "metadata": null,
    "sent_at": "2026-01-15T10:30:00Z"
  }
}

Segurança: headers e validação

Headers enviados

Header Descrição
Content-Type application/json
X-Signature HMAC-SHA256(secret, corpo bruto em bytes) em hexadecimal (64 caracteres). Usar o corpo bruto (raw body), não o JSON já parseado.

Validação (obrigatória)

  1. O pedido veio da MetCare (quem tem o secret).
  2. O corpo não foi alterado em trânsito.

Algoritmo: Calcular expected = hex(HMAC-SHA256(secret, raw_body)) com o corpo bruto recebido (antes de JSON.parse). Comparar com X-Signature usando comparação timing-safe. Rejeitar com 401 se diferente.

Importante: A assinatura é sobre o raw body. Não use JSON.stringify(req.body) depois de parsear.

Exemplo: Node.js (Express)

const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

function verifyWebhookSignature(rawBody, signature, secret) {
  if (!signature || signature.length !== 64 || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature, 'utf8'),
      Buffer.from(expected, 'utf8')
    );
  } catch { return false; }
}

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-signature'];
  if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  const payload = JSON.parse(req.body.toString());
  const { event, data, event_id, timestamp } = payload;
  // Processar evento...
  res.status(200).json({ received: true });
});

Exemplo: Python (Flask)

import hmac, hashlib, os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', '').encode('utf-8')

def verify_webhook_signature(raw_body: bytes, signature: str) -> bool:
    if not signature or len(signature) != 64:
        return False
    expected = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Signature', '')
    raw_body = request.get_data()
    if not verify_webhook_signature(raw_body, signature):
        abort(401)
    payload = request.get_json()
    event, data = payload.get('event'), payload.get('data')
    # Processar evento...
    return 'OK', 200

Exemplo: PHP

<?php
$WEBHOOK_SECRET = getenv('WEBHOOK_SECRET');

function verifyWebhookSignature(string $rawBody, string $signature, string $secret): bool {
    if (strlen($signature) !== 64 || $secret === '') return false;
    $expected = hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $signature);
}

$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
if (!verifyWebhookSignature($rawBody, $signature, $WEBHOOK_SECRET)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}
$payload = json_decode($rawBody, true);
// Processar $payload['event'], $payload['data']...
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['received' => true]);

Resposta, retry e idempotência

Resposta do seu endpoint

  • 200–299 — Sucesso (não será retentado)
  • 400–499 — Erro do cliente (não será retentado)
  • 500–599 — Erro do servidor (será retentado)

Resposta recomendada: {"received": true, "event_id": "..."} ou OK.

Retry

Em caso de erro (HTTP ≥ 400): até 3 tentativas, exponential backoff (1s, 2s, 4s), timeout 8s por tentativa. Após falha total, o evento fica marcado como falhado (consultar via equipa MetCare).

Idempotência

Use event_id (ou event + timestamp + data.id) para identificar eventos únicos e evitar processar o mesmo evento mais de uma vez. Exemplo: guardar event_id processados e responder 200 sem reprocessar se já existir.


Resumo e boas práticas

O quê Descrição
Secret Guardar em env/secret manager. Nunca expor.
URL HTTPS em produção; responder 200–299 para sucesso.
Verificação HMAC-SHA256(secret, raw_body) vs X-Signature (timing-safe). Rejeitar 401 se diferente.
Corpo Usar sempre o corpo bruto para a assinatura; não JSON.stringify(req.body) após parse.
  1. Valide sempre a assinatura antes de processar.
  2. Implemente idempotência (ex.: por event_id).
  3. Responda rápido — processe em assíncrono se necessário.
  4. Registe logs para debugging.
  5. Use HTTPS em produção.
  6. Monitore eventos que falharam após todas as tentativas.

Exemplo completo (Node.js/Express)

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

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
const processedEvents = new Set();

function verifySignature(rawBody, signature, secret) {
  if (!signature || signature.length !== 64 || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signature, 'utf8'), Buffer.from(expected, 'utf8'));
}

app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  try {
    const signature = req.headers['x-signature'];
    if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    const payload = JSON.parse(req.body.toString());
    const { event, data, event_id } = payload;

    if (event_id && processedEvents.has(event_id)) {
      return res.status(200).json({ received: true, duplicate: true });
    }

    await processWebhookEvent(event, data, payload.timestamp);
    if (event_id) processedEvents.add(event_id);

    res.status(200).json({ received: true, event_id });
  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

async function processWebhookEvent(event, data, timestamp) {
  switch (event) {
    case 'subscription.created':
      await handleSubscriptionCreated(data);
      break;
    case 'subscription.updated':
      await handleSubscriptionUpdated(data);
      break;
    case 'subscription.activated':
    case 'subscription.suspended':
    case 'subscription.cancelled':
    case 'subscription.expired':
      await handleSubscriptionStatus(data, event);
      break;
    default:
      console.warn(`Unknown event type: ${event}`);
  }
}

async function handleSubscriptionCreated(data) {
  console.log('New subscription created:', data.id);
}
async function handleSubscriptionUpdated(data) {
  console.log('Subscription updated:', data.id);
}
async function handleSubscriptionStatus(data, event) {
  console.log(`Subscription ${data.id}: ${event}`);
}

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});

Suporte

  • Email: developers@met-care.com
  • Configuração de webhooks, tokens ou problemas de entrega. Inclua X-Webhook-Delivery-Id ao reportar falhas de entrega.