API - Webhooks
Recebe eventos em tempo real sempre que algo acontece na tua conta — sem teres de fazer polling a API. Cada evento e entregue como um POST HTTP assinado ao URL que registas.
https://exemplo.pt/troko-hook) e escolhes os event types a subscrever.POST com o payload JSON e um header de assinatura HMAC.2xxem < 5 segundos.Envia um POST para criar o teu webhook endpoint:
curl -X POST https://troko.ddns.net/api/v1/webhooks \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"url": "https://exemplo.pt/troko-hook",
"events": ["ad.published", "ad.sold", "review.created"]
}'
# Resposta:
# {
# "id": "wh_001",
# "url": "https://exemplo.pt/troko-hook",
# "secret": "whsec_abc123...",
# "events": ["ad.published", "ad.sold", "review.created"]
# }Guarda o secret devolvido — vais precisar dele para verificar assinaturas.
| Evento | Descrição |
|---|---|
ad.published | Anúncio publicado e visível no marketplace. |
ad.sold | Anúncio marcado como vendido pelo owner. |
user.registered | Novo utilizador registado na plataforma. |
booking.confirmed | Reserva confirmada por ambas as partes. |
review.created | Nova avaliação publicada. |
ad.updated | Anúncio actualizado pelo owner. |
ad.deleted | Anúncio removido pelo owner ou por moderação. |
ad.expired | Anúncio expirou (fim do período ativo). |
message.sent | Mensagem enviada numa conversa. |
quote.requested | Pedido de orçamento criado. |
Todos os eventos partilham uma envelope comum. O campo data varia conforme o type.
{
"id": "evt_01HQ9X3K7M2P5RZTYV8N4FJD6B",
"type": "ad.published",
"created_at": "2026-05-15T14:32:08.214Z",
"api_version": "v1",
"data": {
"ad": {
"id": "ad_abc123",
"title": "VW Golf 1.6 TDI",
"price": 12500.00,
"owner_id": "usr_xyz789",
"url": "https://troko.ddns.net/anuncios/ad_abc123"
}
}
}{
"id": "evt_02JR8Y4L8N3Q6SAUZW9O5GKE7C",
"type": "ad.sold",
"created_at": "2026-05-15T16:45:12.001Z",
"api_version": "v1",
"data": {
"ad": {
"id": "ad_abc123",
"title": "VW Golf 1.6 TDI",
"price": 12500.00,
"owner_id": "usr_xyz789",
"buyer_id": "usr_def456"
}
}
}{
"id": "evt_03KS9Z5M9O4R7TBVAX0P6HLF8D",
"type": "user.registered",
"created_at": "2026-05-15T09:12:33.550Z",
"api_version": "v1",
"data": {
"user": {
"id": "usr_new001",
"name": "Maria Silva",
"email": "maria@exemplo.pt"
}
}
}{
"id": "evt_04LT0A6N0P5S8UCWBY1Q7IMG9E",
"type": "booking.confirmed",
"created_at": "2026-05-15T11:20:45.800Z",
"api_version": "v1",
"data": {
"booking": {
"id": "bk_res001",
"ad_id": "ad_abc123",
"user_id": "usr_def456",
"date": "2026-05-20T10:00:00.000Z",
"status": "confirmed"
}
}
}{
"id": "evt_05MU1B7O1Q6T9VDXCZ2R8JNH0F",
"type": "review.created",
"created_at": "2026-05-15T18:05:22.300Z",
"api_version": "v1",
"data": {
"review": {
"id": "rev_001",
"reviewer_id": "usr_def456",
"reviewed_id": "usr_xyz789",
"rating": 5,
"comment": "Excelente vendedor, muito rápido."
}
}
}Cada pedido inclui dois headers:
Troko-Signature — assinatura HMAC-SHA256 em hexadecimal.Troko-Timestamp — timestamp Unix (segundos) em que o evento foi enviado.A assinatura e calculada sobre a string {timestamp}.{raw_body}, usando o teu signing secret.
import crypto from "node:crypto";
function verifyWebhook(rawBody, timestamp, signature, secret) {
// Rejeitar timestamps com mais de 5 minutos
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(timestamp)) > 300) {
return false;
}
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex"),
);
}
// Uso com Express:
app.post("/troko-hook", (req, res) => {
const sig = req.headers["troko-signature"];
const ts = req.headers["troko-timestamp"];
if (!verifyWebhook(req.rawBody, ts, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = req.body;
console.log("Evento recebido:", event.type);
res.status(200).json({ received: true });
});import hmac
import hashlib
import time
def verify_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
# Rejeitar timestamps com mais de 5 minutos
now = int(time.time())
if abs(now - int(timestamp)) > 300:
return False
expected = hmac.new(
secret.encode("utf-8"),
f"{timestamp}.{raw_body.decode('utf-8')}".encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
# Uso com Flask:
@app.route("/troko-hook", methods=["POST"])
def webhook():
sig = request.headers.get("Troko-Signature")
ts = request.headers.get("Troko-Timestamp")
if not verify_webhook(request.data, ts, sig, os.environ["WEBHOOK_SECRET"]):
return {"error": "Invalid signature"}, 401
event = request.json
print(f"Evento recebido: {event['type']}")
return {"received": True}, 200Se a tua resposta não for 2xx em 5 segundos, fazemos retry com backoff exponencial:
| Tentativa | Delay |
|---|---|
| 1a | 1 minuto |
| 2a | 5 minutos |
| 3a (final) | 30 minutos |
Apos 3 tentativas falhadas o evento e marcado como failed. O campo id do evento e unico e estavel entre retries — usa-o como chave de idempotencia.
A consola de gestão de webhooks (criar endpoint, rodar secret, ver entregas) esta em desenvolvimento. Entretanto, pede acesso atraves de /contacto.