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 |
|
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)
- O pedido veio da MetCare (quem tem o secret).
- 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. |
- Valide sempre a assinatura antes de processar.
- Implemente idempotência (ex.: por
event_id). - Responda rápido — processe em assíncrono se necessário.
- Registe logs para debugging.
- Use HTTPS em produção.
- 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-Idao reportar falhas de entrega.