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"],
  "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.

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": 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.

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