Jobs API
Unified surface for every workload Outsend runs — source acquisition, enrichment, verification, reporting.
Jobs API
The Jobs API is the unified surface for every workload Outsend runs on a tenant's behalf: source acquisition (scrap) and the enrichment, verification and reporting modules that operate on the resulting items. A job is the only billable unit.
See also:
- Jobs lifecycle — pending → running → done | failed | cancelled | expired
- States and events — SSE event payload reference
- Limits — EF quota, per-job caps, retention
All endpoints require an authenticated session cookie. Endpoints that create or mutate jobs additionally require an active user; POST /api/jobs and POST /api/jobs/resume also require a verified email address. Admin-only routes (/api/admin/*, /api/jobs/queue) are not documented here.
Conventions
| Item | Value |
|---|---|
| Base URL | https://outsend.xyz |
| Auth | Session cookie (outsend_session) |
| Content-Type | application/json for POST bodies |
| Job identifier | Opaque string (job.id), stable for the lifetime of the job |
| Timestamps | ISO 8601 UTC |
The JobPublic object
Every endpoint that returns a job returns the same shape:
{
"id": "j_01HXYZ...",
"job_type": "scrap",
"queries": ["dentiste"],
"zones": ["Paris", "75015"],
"include_reviews": false,
"status": "running",
"grid_points_count": 412,
"processed_points": 87,
"results_count": 64,
"error_count": 0,
"ef_cost": 0.041,
"created_at": "2026-05-27T09:12:03Z",
"started_at": "2026-05-27T09:12:05Z",
"completed_at": null,
"expires_at": "2026-06-26T09:12:03Z",
"error_message": null,
"output_filename": null,
"download_available": false,
"source_job_id": null,
"pipeline_id": null,
"email_mode": null,
"breakdown": { "by_query": {"dentiste": 64}, "by_zone": {"Paris": 64} },
"dead_queries": [],
"flagged_tiles_count": 0,
"total_attempts_count": 87,
"query_stats": { "dentiste": { "tiles": 87, "with_results": 71 } }
}
status is one of pending | running | done | failed | cancelled | expired.
Errors
All endpoints return {"detail": "..."} (or {"detail": {"message": ..., "errors": [...]}} for validation errors). Generic codes: 401 not authenticated, 403 not authorised (other tenant or unverified email), 404 not found, 422 Pydantic validation. Endpoint-specific causes are listed inline.
Create a job (generic)
POST /api/jobs
Creates a scrap job — the canonical source acquisition workload that runs queries across a geographic grid. For every other workload, use the typed shortcut described below; passing a type field to POST /api/jobs is not supported.
Request body
{
"queries": ["dentiste", "orthodontiste"],
"zones": ["Paris", "75015", "Lyon 2e"],
"include_reviews": false,
"extra_columns": ["gps", "departement", "region"]
}
| Field | Type | Notes |
|---|---|---|
queries |
string[] (1..20) |
Each item ≤ 200 chars, trimmed, deduplicated |
zones |
string[] (1..50) |
City names, postal codes, or arrondissements; resolved server-side |
include_reviews |
boolean |
If true, fetches the latest reviews per POI (raises EF cost) |
extra_columns |
string[] |
Optional output columns, off by default. Allowed: gps (adds exact lat/lon), departement, region. Unknown values are ignored. See the scrap module. |
Response — 200 OK, a JobPublic in pending status.
Specific causes: 400 zone parsing failed / EF quota exceeded / empty grid; 403 email not verified.
Create a job (typed shortcut)
Every enrichment, verification and report module has a dedicated endpoint that accepts the items it operates on. Each shortcut returns a JobPublic whose job_type is fixed to the module slug.
POST /api/jobs/{type}
type |
Purpose | Module doc |
|---|---|---|
reviews |
Pull the latest reviews for each POI | reviews |
emails |
Discover contact emails from each POI's website | emails |
verify-emails |
Anti-bounce verification (no VPN) | verify-emails |
socials |
Detect linked social network profiles | socials |
phones-extra |
Find additional phone numbers beyond the Maps listing | phones-extra |
legal-ids |
Extract SIRET / SIREN from the website | legal-ids |
legal-mentions |
Parse the legal-notice page (capital, RCS, …) | legal-mentions |
legal-data |
Enrich via SIRENE / INPI (api.gouv.fr) |
legal-data |
pricing |
Extract SaaS / B2B pricing | pricing |
techstack |
Detect CMS, frameworks, analytics, payment, CRM | techstack |
pagespeed |
Score via Google PSI API v5 | pagespeed |
ads-intelligence |
Marketing/ads profiling (pixels, CMP, retargeting) | ads-intelligence |
brand-assets |
Logo, favicon, palette, optional screenshot | brand-assets |
dead-check |
Flag dead sites (DNS, parking, default-server, SSL) | dead-check |
delivery-check |
Gmail Inbox / Promotions / Spam placement test | delivery-check |
Request body (shape shared by every item-driven module)
{
"items": [
{ "nom": "Cabinet Dupont", "site_web": "https://dupont-dentiste.fr", "ville": "Paris" }
],
"source_job_id": "j_01HXYZ..."
}
| Field | Type | Notes |
|---|---|---|
items |
dict[] (1..10 000) |
Module-specific keys; usually a subset of a previous job's CSV |
source_job_id |
string? |
Chains the new job to a previous job, used for traceability and billing display |
Module-specific overrides
POST /api/jobs/emails— acceptsmode: "normal" | "deep"(defaultnormal).POST /api/jobs/brand-assets— acceptscapture_screenshot: boolean(defaultfalse, ~5× slower per item when on).POST /api/jobs/delivery-check— does not takeitems. Body:
json
{ "domain": "example.com", "subject_filter": "outsend" }
Response — 200 OK, a JobPublic in pending status. Additional cause: 422 if items is empty, too large, or missing keys required by the module.
List jobs
GET /api/jobs?limit={n}&offset={n}
Returns the authenticated user's jobs, most recent first.
| Param | Type | Default | Range |
|---|---|---|---|
limit |
int |
100 |
clamped to [1, 500] |
offset |
int |
0 |
≥ 0 |
Response — 200 OK, JobPublic[].
Get a job
GET /api/jobs/{id}
Response — 200 OK, a single JobPublic. Includes live counters (processed_points, results_count, query_stats, breakdown) that the dashboard polls between SSE events.
Stream live progress (SSE)
GET /api/jobs/{id}/stream?since={log_id}
Server-Sent Events stream that emits status transitions, log lines and counter updates as the worker progresses. Reconnects honour the Last-Event-ID header automatically; the since query param is a fallback for clients that don't speak SSE natively. Event taxonomy (status, log, progress, done, error) and payload shapes are documented in States and events.
Headers returned
Content-Type: text/event-stream
Cache-Control: no-cache
X-Accel-Buffering: no
List a job's items
GET /api/jobs/{id}/items?offset={n}&limit={n}
Returns the rows of the job's output CSV as JSON, for chaining into an enrichment job. Only available for jobs whose status == "done" and whose job_type produces a reusable CSV (i.e. not delivery_check and not viewport_test).
Response — 200 OK
{
"count": 412,
"items": [
{ "nom": "Cabinet Dupont", "site_web": "https://...", "telephone": "+33 1 ...", "...": "..." }
]
}
Specific causes: 400 job not done or job_type has no reusable output; 410 CSV expired or deleted.
Download a job's result
GET /api/jobs/{id}/download?format=csv|json|xlsx
Downloads the job's output. CSV is the canonical artefact written by the worker (UTF-8 BOM, ; separator); JSON and XLSX are derived on the fly. All exports are run through a spreadsheet-formula-injection sanitiser.
format |
Media type | Filename |
|---|---|---|
csv (default) |
text/csv; charset=utf-8 |
{job.output_filename} |
json |
application/json; charset=utf-8 |
{base}.json |
xlsx |
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
{base}.xlsx |
Specific causes: 400 job still pending/running or unsupported format; 410 output expired, missing, or job failed before writing a row.
Cancel a job
POST /api/jobs/{id}/cancel
Requests cancellation of a pending or running job. Returns 400 if the job is already terminal. If the job belongs to a pipeline, downstream stages are short-circuited.
Response — 200 OK, {"ok": true}.
Resume a job
POST /api/jobs/{id}/resume
Creates a new job that picks up a cancelled or failed scrap where it left off. The new job inherits the source's queries, zones and partial CSV; the worker skips coordinates already processed. EF is debited only for the remaining grid points.
Response — 200 OK, a new JobPublic (the resume job) in pending status. Its source_job_id references the original.
Specific causes: 400 source job not resumable (wrong type, not interrupted, or already fully processed); 403 email not verified.
Delete a job
DELETE /api/jobs/{id}
Permanently removes the job and its output CSV. Refuses to delete a job that is still running — cancel it first.
Response — 204 No Content. Specific cause: 400 job still running.
Estimate EF cost
POST /api/estimate
Computes the EF cost of a hypothetical scrap job without creating one. Drives the live cost meter in the launch form. Estimation is free and unmetered.
Request body — same shape as POST /api/jobs, but queries and zones may be empty (returns valid: false).
Response — 200 OK, a JobEstimateResponse:
{
"valid": true,
"grid_points": 412,
"total_requests": 824,
"queries_count": 2,
"ef_cost": 0.041,
"estimated_duration_seconds": 1380,
"errors": [],
"warnings": []
}
| Field | Meaning |
|---|---|
valid |
true iff errors is empty |
grid_points |
Distinct GPS tiles across the union of zones |
total_requests |
grid_points × len(queries) — what the worker will actually call |
queries_count |
Echoes len(queries) for UI display |
ef_cost |
France-equivalent units; see Limits |
estimated_duration_seconds |
Best-effort wall-clock estimate |
errors |
Hard blockers (over-quota, unparseable zones, empty grid) |
warnings |
Soft signals (not currently used) |
Notes on omitted endpoints
The following routes exist but are intentionally not part of the public surface:
GET /api/jobs/queue— anonymised global queue for the public dashboard widget. Tenant-agnostic, scoped separately./api/admin/*— operator-only.GET /api/jobs/{id}/breakdown,GET /api/jobs/{id}/map,GET /api/jobs/{id}/output-columns,GET /api/jobs/{id}/delivery-result,POST /api/jobs/parse-list,GET /api/brand-lookup,GET /api/brand-assets/{owner}/{filename},GET /api/delivery-check/seeds— UI-internal helpers that may change without notice.