EN
Copied
API

É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 :

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 :

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