FR
Copied
API

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:

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.

Response200 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

json { "domain": "example.com", "subject_filter": "outsend" }

Response200 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

Response200 OK, JobPublic[].


Get a job

GET /api/jobs/{id}

Response200 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).

Response200 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.

Response200 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.

Response200 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.

Response204 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).

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