FR
Copied
API

Events & webhooks

Account event feed (GET /api/events) and outgoing signed webhooks — the triggers behind Zapier, Make, n8n and any automation.

Events & webhooks

Every meaningful state change in your account produces an event: a job finishes, a pipeline completes, a monitoring run computes its diff. Two ways to consume them:

Both are scoped to the authenticated account and work with API keys as well as session cookies.

Event types

Type Emitted when
job.completed A job reaches done
job.failed A job reaches failed
job.cancelled A job is cancelled
pipeline.completed A pipeline reaches done
pipeline.failed A pipeline reaches failed
pipeline.cancelled A pipeline is cancelled
veille.run_completed A monitoring run finishes computing its diff

Events are emitted within a few seconds of the state change and retained with a 7-day detection window.

Event shape

{
  "id": 42,
  "type": "job.completed",
  "created_at": "2026-08-07 09:12:00",
  "data": {
    "id": "a1b2c3…",
    "job_type": "scrap",
    "status": "done",
    "queries": ["plumber"],
    "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"
  }
}

data for pipeline.* events carries {id, name, status, created_at, completed_at}. For veille.run_completed it carries {run_id, veille_id, veille_name, job_id, is_baseline, total_count, new_count, removed_count, modified_count, computed_at}.


GET /api/events

Cursor-based polling. Auth: session cookie or API key.

Query param Type Notes
since_id int ≥ 0 Only return events with id strictly greater. Default 0.
types string CSV filter, e.g. job.completed,pipeline.completed. Unknown types → 422.
limit int 1–100 Default 50.
order asc | desc Default asc (chronological, for catching up). desc returns the most recent events first — use it to show representative samples in a UI.
curl -s "https://outsend.xyz/api/events?since_id=0&types=job.completed" \
  -H "Authorization: Bearer osk_..."

Response — 200 OK

{
  "events": [ { "id": 42, "type": "job.completed", "created_at": "…", "data": { } } ],
  "last_id": 42,
  "head_id": 57
}

Two cursors, do not confuse them:

The trap: since the default order is ascending, ?limit=1 returns the oldest event, so its last_id is not the head. A connector that stored it as its starting cursor would replay the whole account history on its next poll. Use head_id.

Fields are guaranteed by the current job.* contract: an event emitted before a field existed is returned with that field set to null, never absent.


POST /api/webhooks

Registers an endpoint. Auth: session cookie or API key. Maximum 5 active endpoints per account.

Field Type Notes
url string https:// only, public host. Internal hosts, private IPs and outsend itself are refused (400).
events string[] At least one type from the table above. Unknown types → 422.
secret string, optional 16–128 chars. Generated (whsec_…) if omitted.
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"]}'

Response — 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 }

The secret is returned only here. Store it to verify signatures.

GET /api/webhooks

Lists the account's endpoints — without secrets. Response: { "webhooks": [ … ] }.

GET /api/webhooks/{id}/deliveries

Last 20 delivery attempts for the endpoint (status, attempts, HTTP code, error) — the first place to look when debugging an integration. 404 if the endpoint is not yours.

DELETE /api/webhooks/{id}

Deletes the endpoint (hard delete — this is what n8n does when a workflow is deactivated). Response: 204.


Delivery contract

Each event is POSTed as JSON to your URL:

Header Content
Content-Type application/json
User-Agent outsend-webhooks/1.0
X-Outsend-Event Event type, e.g. job.completed
X-Outsend-Delivery Delivery id (unique per endpoint × event)
X-Outsend-Signature sha256=<hex> — HMAC-SHA256 of the raw body with your secret

Respond with any 2xx within 10 seconds. Anything else counts as a failure.

Verifying the 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)

Compute the HMAC over the raw request body, before any JSON parsing.

Retries

Failed deliveries are retried with backoff: 1 min → 5 min → 30 min → 2 h → 6 h, then the delivery is abandoned (gave_up). After 20 consecutive failures the endpoint is disabled (is_active: false, disabled_at set) — re-create it once your receiver is fixed.

What's next