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"],
"country": "FRA",
"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"],
"country": "FRA",
"zones": ["Paris", "75015", "Lyon 2e"],
"include_reviews": false,
"scrap_mode": "fast",
"stop_at": 1000,
"max_per_phone": 0,
"extra_columns": ["gps", "departement", "region"]
}
| Field | Type | Notes |
|---|---|---|
queries |
string[] (1..20) |
Each item ≤ 200 chars, trimmed, deduplicated |
country |
string (ISO3) |
Target country, ISO-3166 alpha-3 (e.g. FRA, USA, DEU). Defaults to "FRA". Drives the GPS grid and zone resolution. |
zones |
string[] (1..50) |
Geographic zones expressed within country: a state/region, a county/department, a city with optional radius ("Austin 30km"), or the country name for a full-country scrape. For France, city names, postal codes, INSEE and arrondissements also work. Resolved server-side. |
include_reviews |
boolean |
If true, fetches the latest reviews per POI (raises EF cost) |
scrap_mode |
enum |
Subdivision depth of saturated tiles: fast (default), normal, ultra. Drives recall, runtime and EF cost — see Scrape modes. |
stop_at |
int (1..2000000) |
Stop the scrape once about this many places are collected. It is a stop threshold, not a hard cap: each worker finishes the map tile it is on, so the final count is usually higher than the value you pass. Omit or null to scrape the whole zone. The EF estimate is reduced proportionally. |
max_per_phone |
int (0..10) |
Maximum listings kept per phone number — for shared switchboards and networks of local listings pointing at one call centre. 0 (default) = no limit; 1 = a single contact per number. Listings with no usable number are never dropped. Deduplication of a listing against itself is always on and not configurable. No effect on EF cost. See Duplicates. |
extra_columns |
string[] |
Optional output columns, off by default. Allowed: gps (adds exact lat/lon), departement, region (both adm2/adm1 names — derived from the postal code in France, from the business GPS via reverse-geo elsewhere). 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": 100,
"total": 412,
"offset": 0,
"items": [
{ "nom": "Cabinet Dupont", "site_web": "https://...", "telephone": "+33 1 ...", "...": "..." }
]
}
| Field | Meaning |
|---|---|
count |
Rows returned in this response (never more than limit) |
total |
Rows the job holds in total, independent of offset and limit |
offset |
Offset actually applied |
items |
The rows themselves |
Omitting limit returns every row in a single response — on a large scrape
that is hundreds of thousands of rows. To page through them, compare
offset + count with total: another page is left as long as it is smaller.
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 (including the optional country ISO3 field, default "FRA", which selects the country the zones are resolved in), 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.GET /api/stats/dashboard— aggregate KPI counters (total scraped leads, scrapes in the last 7 days, ready-to-download exports) for the home dashboard header band. UI-internal, tenant-scoped, may change without notice./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.