# Batch jobs

Submit up to 1,000 mixed items in one call and read results by webhook or by polling.

## When to reach for batch

The sync endpoints - [profiles/enrich](https://up2data.ai/docs/v1/api/profiles-enrich/), [companies/enrich](https://up2data.ai/docs/v1/api/companies-enrich/), [posts/enrich](https://up2data.ai/docs/v1/api/posts-enrich/) - are fine up to a few hundred records. Past that, use [`POST /v1/batch`](https://up2data.ai/docs/v1/api/batch-create/): submit up to 1,000 items in one job, processed in parallel, with results delivered by webhook or by polling.

Every /v1 capability is batchable, as one of two item shapes. Three enrich shapes:

| `type` | Shape |
|---|---|
| `profile` | `{ "type": "profile", "url": "..." }` |
| `company` | `{ "type": "company", "url": "..." }` |
| `post` | `{ "type": "post", "url": "..." }` |

or an operation item, running any other /v1 capability as a batch item instead of a loop of sync calls:

```json
{ "operation": "posts.engagement.comments", "body": { "urn": "7267273010393358336", "max_results": 500 } }
```

`operation` is one of `profiles.activity`, `profiles.recommendations`, `profiles.interests`, `companies.jobs_count`, `companies.posts`, `companies.headcount`, `jobs.enrich`, `posts.engagement.comments`, `posts.engagement.reactions`, `posts.engagement.reposts`, `search.people.sales_nav`, `search.companies.sales_nav`, `search.people.standard`, `search.companies.standard`, `search.jobs`, or `search.posts`. `body` is the same shape as that operation's own sync request body, except `page`, `per_page`, and `pagination_token` are never accepted - batch paginates through everything up to `max_results` on its own. `max_results` defaults to 100 for every non-scalar operation, with its own cap per operation (2,500 for people search, 1,000 for company search, 500 or 1,000 for everything else); a scalar operation (`companies.jobs_count`, `companies.headcount`, `jobs.enrich`) rejects `max_results` entirely. `search.people.sales_nav` and `search.companies.sales_nav` take `filters` or `sales_nav_url`, exactly one, same as their sync endpoints. See [`POST /v1/batch`](https://up2data.ai/docs/v1/api/batch-create/) for the full table of every operation's `max_results` default and cap, billing, and result row type.

## Submit a job

```bash
curl -X POST https://api.up2data.ai/v1/batch \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: import-chunk-014" \
  -d '{
    "items": [
      { "type": "profile", "url": "https://linkedin.com/in/a" },
      { "type": "profile", "url": "https://linkedin.com/in/b" },
      { "type": "company", "url": "https://linkedin.com/company/acme" }
    ],
    "webhook_url": "https://your-server.com/hooks/up2data"
  }'
```

```json
{
  "data": { "job_id": "job_7hd92kfa", "status": "processing", "total": 3, "estimated_completion": "2026-07-07T14:03:00Z" },
  "meta": { "creditsUsed": 3, "creditsRemaining": 98416, "billed": true, "reason": "job_accepted", "requestId": "req_0pl2m8xz" }
}
```

Submission is a single request no matter how many items you send, and it's billed for the full total right then - see [Credits](https://up2data.ai/docs/v1/guides/credits/) for the per-item rates and [Rate limits](https://up2data.ai/docs/v1/guides/rate-limits/) for the request limits.

> **Info:** Send an `Idempotency-Key` header with your submission. Retrying it with the same key returns the original job instead of creating a duplicate or charging twice - useful when a submission times out and you're not sure it landed.

## Get results

Poll the job directly:

```bash
curl https://api.up2data.ai/v1/batch/job_7hd92kfa -H "X-API-Key: YOUR_API_KEY"
```

```json
{
  "data": {
    "job_id": "job_7hd92kfa", "status": "completed", "total": 1000, "succeeded": 968, "failed": 32,
    "results": [
      { "type": "profile", "operation": "enrich.profile", "url": "https://linkedin.com/in/a", "status": "ok", "input": "https://linkedin.com/in/a", "data": { "full_name": "...", "headline": "..." } },
      { "type": "company", "operation": "enrich.company", "url": "https://linkedin.com/company/acme", "status": "ok", "input": "https://linkedin.com/company/acme", "data": { "name": "Acme", "headcount": 512 } }
    ],
    "failures": [
      { "type": "profile", "url": "https://linkedin.com/in/gone", "status": "unprocessable_target" },
      { "type": "company", "url": "https://linkedin.com/company/timed-out", "status": "upstream_timeout" }
    ]
  },
  "meta": { "creditsUsed": 1000, "creditsRemaining": 421012, "billed": true, "reason": "job_completed", "requestId": "req_v82n1kd0" }
}
```

Every `results` and `failures` entry carries the `operation` that produced it, and every `results` entry also carries `input`, echoing what you submitted for it.

All 1,000 items here are `profile` and `company` enrich items, charged when the job was submitted, and none of the 32 failures refund anything: a `results` entry, an `unprocessable_target` failure, and a `500` or `504` failure all bill the same - 1 credit for a profile, company, or post, or a `max_results`-sized reservation for a Sales Navigator search item. The job's `creditsUsed` still shows the full 1,000 - see [Credits](https://up2data.ai/docs/v1/guides/credits/). Every other operation item refunds differently: it reserves the same worst case at submission, but settles down to what it actually billed once its queue finishes, so a failed or empty item on those operations costs 0.

[`GET /v1/batch/{job_id}`](https://up2data.ai/docs/v1/api/batch-get/) is free to poll as often as you like, whether the job is still running or done. Or set `webhook_url` and listen for `batch.completed` instead - see [Webhooks](https://up2data.ai/docs/v1/guides/webhooks/).

For large jobs, [`GET /v1/batch/{job_id}/results`](https://up2data.ai/docs/v1/api/batch-results/) returns a cursor page of result entries instead of the whole job at once, so you can stream results in while the job is still processing instead of re-fetching a growing `results` array on every poll:

```bash
curl "https://api.up2data.ai/v1/batch/job_7hd92kfa/results?limit=100" -H "X-API-Key: YOUR_API_KEY"
```

Pass the `next_cursor` from each response back in as `cursor` until `has_more` is `false`.

## Credits

Submitting a job reserves the worst case for every item at once: 1 credit per profile, company, or post item, and each operation item at its own rate - see the [full operations table](https://up2data.ai/docs/v1/api/batch-create/) for exact numbers. A profile, company, post, or Sales Navigator search item never refunds for a failed item - a `422 unprocessable_target` item and a retryable `500` or `504` failure both bill the same as a success; only a Sales Navigator search item's unused `max_results` reservation refunds, once that search runs and comes back short. Every other operation settles down to what it actually billed once its queue finishes: a `422 unprocessable_target` failure, a company or job that isn't found, or an empty page all cost 0 and refund in full. See [Credits](https://up2data.ai/docs/v1/guides/credits/) for the full table.

