Événements & webhooks
Flux d'événements du compte (GET /api/events) et webhooks sortants signés — les déclencheurs derrière Zapier, Make, n8n et toute automatisation.
Événements & webhooks
Chaque changement d'état significatif du compte produit un événement : un job se termine, une pipeline aboutit, un run de veille calcule son diff. Deux façons de les consommer :
- Polling —
GET /api/events, un flux à curseur. Simple, sans état, marche partout. - Webhooks — outsend POSTe chaque événement vers ton endpoint HTTPS, signé HMAC-SHA256, avec retries automatiques.
Les deux sont limités au compte authentifié et fonctionnent avec les clés API comme avec le cookie de session.
Types d'événements
| Type | Émis quand |
|---|---|
job.completed |
Un job atteint done |
job.failed |
Un job atteint failed |
job.cancelled |
Un job est annulé |
pipeline.completed |
Une pipeline atteint done |
pipeline.failed |
Une pipeline atteint failed |
pipeline.cancelled |
Une pipeline est annulée |
veille.run_completed |
Un run de veille finit de calculer son diff |
Les événements sont émis quelques secondes après le changement d'état, avec une fenêtre de détection de 7 jours.
Forme d'un événement
{
"id": 42,
"type": "job.completed",
"created_at": "2026-08-07 09:12:00",
"data": {
"id": "a1b2c3…",
"job_type": "scrap",
"status": "done",
"queries": ["plombier"],
"zones": ["Lyon"],
"results_count": 1250,
"error_count": 0,
"ef_cost": 0.18,
"created_at": "2026-08-07 08:55:12",
"completed_at": "2026-08-07 09:11:58",
"error_message": null,
"pipeline_id": null,
"source_job_id": null,
"download_url": "https://outsend.xyz/api/jobs/a1b2c3…/download"
}
}
Pour les événements pipeline.*, data porte {id, name, status, created_at, completed_at}. Pour veille.run_completed : {run_id, veille_id, veille_name, job_id, is_baseline, total_count, new_count, removed_count, modified_count, computed_at}.
GET /api/events
Polling à curseur. Auth : cookie de session ou clé API.
| Paramètre | Type | Notes |
|---|---|---|
since_id |
int ≥ 0 | Ne renvoyer que les événements d'id strictement supérieur. Défaut 0. |
types |
string | Filtre CSV, ex. job.completed,pipeline.completed. Type inconnu → 422. |
limit |
int 1–100 | Défaut 50. |
order |
asc | desc |
Défaut asc (chronologique, pour rattraper). desc renvoie les plus récents d'abord — pratique pour afficher des exemples représentatifs dans une interface. |
curl -s "https://outsend.xyz/api/events?since_id=0&types=job.completed" \
-H "Authorization: Bearer osk_..."
Réponse — 200 OK
{
"events": [ { "id": 42, "type": "job.completed", "created_at": "…", "data": { } } ],
"last_id": 42,
"head_id": 57
}
Deux curseurs, à ne pas confondre :
last_id— id du dernier événement de cette page. C'est lui qu'on repasse ensince_idpour rattraper. Chaque événement n'est alors vu qu'une fois.head_id— id de l'événement le plus récent du compte pour ce filtre, quelle que soit la page demandée. C'est lui qu'on mémorise pour démarrer « à partir de maintenant » sans rejouer l'historique.
Le piège : l'ordre étant croissant par défaut, ?limit=1 renvoie l'événement le plus ancien, donc son last_id n'est pas la tête. Un connecteur qui le mémorisait comme curseur de départ rejouait tout l'historique du compte au polling suivant. Utiliser head_id.
Les champs sont garantis par le contrat job.* courant : un événement émis avant l'existence d'un champ ressort avec ce champ à null, jamais absent.
POST /api/webhooks
Enregistre un endpoint. Auth : cookie de session ou clé API. Maximum 5 endpoints actifs par compte.
| Champ | Type | Notes |
|---|---|---|
url |
string | https:// uniquement, hôte public. Hôtes internes, IP privées et outsend lui-même sont refusés (400). |
events |
string[] | Au moins un type du tableau ci-dessus. Type inconnu → 422. |
secret |
string, optionnel | 16–128 caractères. Généré (whsec_…) si absent. |
curl -s -X POST https://outsend.xyz/api/webhooks \
-H "Authorization: Bearer osk_..." -H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/outsend", "events": ["job.completed"]}'
Réponse — 201 Created
{ "id": 7, "url": "https://example.com/hooks/outsend", "events": ["job.completed"], "secret": "whsec_…", "is_active": true, "consecutive_failures": 0, "created_at": "…", "disabled_at": null }
Le secret n'est renvoyé qu'ici. Stocke-le pour vérifier les signatures.
GET /api/webhooks
Liste les endpoints du compte — sans les secrets. Réponse : { "webhooks": [ … ] }.
GET /api/webhooks/{id}/deliveries
Les 20 dernières tentatives de livraison de l'endpoint (statut, tentatives, code HTTP, erreur) — le premier réflexe pour déboguer une intégration. 404 si l'endpoint n'est pas à toi.
DELETE /api/webhooks/{id}
Supprime l'endpoint (suppression réelle — c'est ce que fait n8n à la désactivation d'un workflow). Réponse : 204.
Contrat de livraison
Chaque événement est POSTé en JSON vers ton URL :
| Header | Contenu |
|---|---|
Content-Type |
application/json |
User-Agent |
outsend-webhooks/1.0 |
X-Outsend-Event |
Type d'événement, ex. job.completed |
X-Outsend-Delivery |
Id de livraison (unique par endpoint × événement) |
X-Outsend-Signature |
sha256=<hex> — HMAC-SHA256 du corps brut avec ton secret |
Réponds en 2xx sous 10 secondes. Tout le reste compte comme un échec.
Vérifier la signature
import hmac, hashlib
def is_valid(secret: str, raw_body: bytes, signature_header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature_header, expected)
Le HMAC se calcule sur le corps brut de la requête, avant tout parsing JSON.
Retries
Les livraisons en échec sont retentées avec backoff : 1 min → 5 min → 30 min → 2 h → 6 h, puis la livraison est abandonnée (gave_up). Après 20 échecs consécutifs, l'endpoint est désactivé (is_active: false, disabled_at rempli) — recrée-le une fois ton récepteur réparé.
Et ensuite
- Authentification — créer une clé API
- Jobs — les objets derrière les événements
job.* - Veille — les runs derrière
veille.run_completed