Extract Data with the Jobs API
One create/poll/result contract over every extraction endpoint: pick the work with type, track it by job_id, fetch the result when it is ready.
Core endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/jobs | Create job |
| GET | /api/v1/jobs/{job_id} | Get status (polling) |
| GET | /api/v1/jobs/{job_id}/result | Fetch final result |
Base URL: https://api.scrupp.com. A job is the same task as a direct call to the underlying endpoint — same credits, same settlement.
Create job
POST /api/v1/jobs
Headers
Authorization: Bearer YOUR_API_KEY
Idempotency-Key: <uuid>
Content-Type: application/json
Any of the three auth forms works: Authorization: Bearer, X-API-Key, or ?api_key=. Idempotency-Key makes a retried create return the original job instead of paying twice — reuse the same key for 24h.
Body
{
"type": "contact.decision_maker.linkedin",
"input": { "items": ["https://linkedin.com/company/openai"] },
"callback_url": "https://hooks.zapier.com/hooks/catch/123/abc",
"metadata": { "source": "zapier", "external_id": "row_9481" }
}
input takes the same body as the equivalent per-path endpoint. metadata is stored and echoed back on status and result.
Response
{
"job_id": 918273,
"status": "queued",
"created_at": "2026-09-07T11:30:00+00:00",
"poll_url": "/api/v1/jobs/918273",
"result_url": "/api/v1/jobs/918273/result",
"metadata": { "source": "zapier", "external_id": "row_9481" }
}
Replaying an Idempotency-Key returns the original job with 200 instead of 202. Reusing one with a different body is a 409 idempotency_key_reused.
Example
curl -X POST "https://api.scrupp.com/api/v1/jobs" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: 7c4a8d09-ca37-4f0b-9a1d-2b1f5c3e9a10" \
-H "Content-Type: application/json" \
--data '{
"type": "sales_navigator.search",
"input": { "url": "https://www.linkedin.com/sales/search/people?query=...", "with_emails": true, "max": 100 }
}'
Poll status
GET /api/v1/jobs/{job_id}
Response
{
"job_id": 918273,
"status": "running",
"progress": 35,
"records": 35,
"cost": "Calculating...",
"retry_after": 5,
"error": null,
"metadata": null,
"result_url": "/api/v1/jobs/918273/result"
}
Wait retry_after seconds between polls to keep your task-operation count down. progress is null until the job reports a target size.
Statuses
- queued → accepted, not started
- running → processing
- succeeded → result ready
- failed → see error
A failed job is refunded in full: the upfront credit hold is released and cost settles at 0.
Prefer a callback over polling
Pass callback_url when creating the job and Scrupp posts to it on completion, so Zapier/Make/n8n runs do not burn operations on polling. See Webhooks.
Get result
GET /api/v1/jobs/{job_id}/result
Response
{
"job_id": 918273,
"status": "succeeded",
"data": {
"items": [
{ "name": "John Doe", "title": "CMO", "email": "john@openai.com" }
]
},
"metadata": { "source": "zapier", "external_id": "row_9481" }
}
If the job has not finished, the response is 409 with error code NOT_READY and a retry_after — treat it as "keep waiting", not as a failure.
Not ready yet
HTTP/1.1 409 Conflict
{
"job_id": 918273,
"status": "running",
"error": { "code": "NOT_READY", "message": "Job has not finished yet." },
"retry_after": 5
}
Job types
Every type maps to an endpoint documented in the API groups; input is that endpoint's body.
| type | Description | Equivalent endpoint |
|---|---|---|
| sales_navigator.search | Export a Sales Navigator search | POST /sn/search |
| linkedin.search | Export a LinkedIn search | POST /linkedin/search |
| apollo.search | Run an Apollo search | POST /apollo/search |
| linkedin.profile | Enrich LinkedIn profiles | POST /linkedin/profile |
| company.linkedin | Company lookup by LinkedIn URL | POST /company/linkedin |
| company.domain | Company lookup by domain | POST /company/domain |
| company.name | Company lookup by name | POST /company/name |
| contact.decision_maker.linkedin | Decision makers by LinkedIn URL | POST /contact/decision-maker/linkedin |
| contact.decision_maker.name | Decision makers by company name | POST /contact/decision-maker/name |
| contact.decision_maker.domain | Decision makers by domain | POST /contact/decision-maker/domain |
| email.initials | Find emails in bulk from name + domain | POST /email/initials |
| email.linkedin | Enrich LinkedIn profiles and find emails | POST /email/linkedin |
Unknown type
An unrecognised type returns 400 unknown_type and lists every supported value in the message, so you never have to guess.