# Create a batch job

Submit up to 1,000 mixed enrich and operation items as one asynchronous job.

**POST** `https://api.up2data.ai/v1/batch`

**Credits:** Billed at submission - profile, company, and post items: 1 credit each, never refunded. Every other operation reserves the worst case at submission and settles to what it actually billed once its queue finishes - see the operations table below - **Mode:** asynchronous

Every /v1 capability is batchable. Mix `profile`, `company`, and `post` enrich items (`{ "type": "profile" | "company" | "post", "url": "..." }`) freely with operation items (`{ "operation": "...", "body": {...} }`), where `body` is the same shape as that operation's own sync request body. `page`, `per_page`, and `pagination_token` are rejected everywhere in a batch item's `body` - batch paginates through everything up to `max_results` on its own, instead of one page per call. `search.people.sales_nav` and `search.companies.sales_nav` still take `filters` or `sales_nav_url`, exactly one, same as their sync endpoints. `search.jobs` requires at least one of `company_url`, `keywords`, or `filters` in a batch item - unlike the sync endpoint, an empty `search.jobs` item is rejected with 400 instead of returning a free empty result. Deliver results by `webhook_url` (the `batch.completed` event) and/or by polling.

Every item reserves its worst case at submission. A `profile`, `company`, `post`, or Sales Navigator search item is charged in full up front and never refunds for a failed item - the only refund on those is the unused part of a Sales Navigator search item's `max_results` reservation, once that search runs and comes back short. Every other operation also reserves the worst case at submission, but settles down to what it actually billed once its queue finishes: a `422 unprocessable_target` failure, or a company or job that isn't found, costs 0 and refunds in full - the same as an empty page or an empty direction/category. See the table below for the exact rate of each operation, matching its sync endpoint.

| Operation | Sync twin | `max_results` (default / max) | Billing | Result `type` |
|---|---|---|---|---|
| `profile` (enrich) | [Enrich a profile](https://up2data.ai/docs/v1/api/profiles-enrich/) | not accepted | 1 credit flat, charged in full at submission, never refunded | `profile` |
| `company` (enrich) | [Enrich a company](https://up2data.ai/docs/v1/api/companies-enrich/) | not accepted | 1 credit flat, never refunded | `company` |
| `post` (enrich) | [Enrich a post](https://up2data.ai/docs/v1/api/posts-enrich/) | not accepted | 1 credit flat, never refunded | `post` |
| `profiles.activity` | [Get profile activity](https://up2data.ai/docs/v1/api/profiles-activity/) | 100 / 500 | Reserved: 2 credits per 25-row page (`ceil(max_results / 25)` pages). Billed: the same rate on rows actually returned, minimum 1 page. | `activity_post`, `activity_comment`, or `activity_reaction` (matches `kind`) |
| `profiles.recommendations` | [Get profile recommendations](https://up2data.ai/docs/v1/api/profiles-recommendations/) | not accepted | Reserved: 1 credit per requested `direction` (`both` reserves 2). Billed: 1 credit per direction that returned at least one recommendation; an empty direction is free. | `recommendation` |
| `profiles.interests` | [Get profile interests](https://up2data.ai/docs/v1/api/profiles-interests/) | not accepted | Reserved: 1 credit per requested `category` (`all` reserves 5). Billed: 1 credit per non-empty category. | `interest` |
| `companies.jobs_count` | [Count company job openings](https://up2data.ai/docs/v1/api/companies-jobs-count/) | not accepted | 1 credit, reserved and billed only if the company is found; 0 otherwise. | `jobs_count` |
| `companies.posts` | [Get company posts](https://up2data.ai/docs/v1/api/companies-posts/) | 100 / 500 | Reserved: 1 credit per `max_results` (flat, not paginated). Billed: 1 credit per post actually returned. | `company_post` |
| `companies.headcount` | [Find custom headcount](https://up2data.ai/docs/v1/api/companies-headcount/) | not accepted | 1 credit, reserved and billed only if the company is found; 0 otherwise. | `headcount` |
| `jobs.enrich` | [Enrich a job](https://up2data.ai/docs/v1/api/jobs-enrich/) | not accepted | 1 credit, reserved and billed only if the job is found; 0 otherwise. | `job` |
| `posts.engagement.comments` | [Get post comments](https://up2data.ai/docs/v1/api/posts-engagement-comments/) | 100 / 1,000 | Reserved: 1 credit per `max_results` (flat). Billed: 1 credit per comment actually returned. | `comment` |
| `posts.engagement.reactions` | [Get post reactions](https://up2data.ai/docs/v1/api/posts-engagement-reactions/) | 100 / 1,000 | Reserved: 1 credit per `max_results` (flat). Billed: 1 credit per reaction actually returned. | `reaction` |
| `posts.engagement.reposts` | [Get post reposts](https://up2data.ai/docs/v1/api/posts-engagement-reposts/) | 100 / 1,000 | Reserved: 1 credit per `max_results` (flat). Billed: 1 credit per repost actually returned. | `repost` |
| `search.people.sales_nav` | [Search people (Sales Navigator)](https://up2data.ai/docs/v1/api/search-people-sales-nav/) | 100 / 2,500 | Reserved: 1 credit per `max_results` (flat). Billed: 0 - refunds the unused part of the reservation once the search runs and returns fewer than `max_results`. | `profile` |
| `search.companies.sales_nav` | [Search companies (Sales Navigator)](https://up2data.ai/docs/v1/api/search-companies-sales-nav/) | 100 / 1,000 | Same reserve-and-refund model as `search.people.sales_nav`. | `company` |
| `search.people.standard` | [Search people (standard)](https://up2data.ai/docs/v1/api/search-people-standard/) | 100 / 2,500 | Reserved: 1 credit per 25-row page (`ceil(max_results / 25)`). Billed: 1 credit per page of rows actually returned; 0 when empty. | `person` |
| `search.companies.standard` | [Search companies (standard)](https://up2data.ai/docs/v1/api/search-companies-standard/) | 100 / 1,000 | Same page-based reserve/bill as `search.people.standard`. | `company` |
| `search.jobs` | [Search jobs](https://up2data.ai/docs/v1/api/search-jobs/) | 100 / 500 | Reserved: 1 credit per `max_results` (flat, matching the sync per-job rate). Billed: 1 credit per job actually returned; 0 when empty. | `job` |
| `search.posts` | [Search posts](https://up2data.ai/docs/v1/api/search-posts/) | 100 / 500 | Reserved: 1 credit per 25-row page. Billed: 1 credit per page of rows actually returned; 0 when empty. | `post` |

> **Warning:** A `profile`, `company`, `post`, or Sales Navigator search item is billed in full at submission and never refunds for a failed item - including `unprocessable_target` and retryable errors like `429`, `500`, or `504`. The only refund on those is the unused part of a Sales Navigator search item's `max_results` reservation, credited back once that search runs. Every other operation item settles down to what it actually billed once its own queue finishes, so a failed or empty item on those costs 0 and refunds in full.

> **Info:** Batch submissions accept an optional `Idempotency-Key` header, so a retried submission with the same key returns the original job instead of creating a duplicate or charging twice. See [Batch jobs](https://up2data.ai/docs/v1/guides/batch-jobs/).

## Request

| Field | Type | Notes |
|---|---|---|
| `items` **required** | `object[]` | Up to 1,000 items, freely mixed. |
| `items[].type` | `string` | Enrich item. Enum: profile, company, post. Required with url. |
| `items[].url` | `string` | Required with type. |
| `items[].operation` | `string` | Operation item. Enum: 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, search.posts. Required with body. |
| `items[].body` | `object` | Request body for the operation: the same shape as that operation's own sync request body. page, per_page, and pagination_token are rejected. A scalar operation (companies.jobs_count, companies.headcount, jobs.enrich) rejects max_results entirely; every other operation accepts it with its own default and cap - see the table below. search.people.sales_nav and search.companies.sales_nav take filters or sales_nav_url, exactly one, same as their sync endpoints. search.jobs requires at least one of company_url, keywords, or filters. Required with operation. |
| `webhook_url` | `string` | HTTPS endpoint for the batch.completed event. Omit for polling-only. |
| `Idempotency-Key` | `string (header)` | Optional. A retried submission with the same key returns the original job; no duplicate is created. |

## Call it

**cURL**

```bash
curl -X POST "https://api.up2data.ai/v1/batch" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -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"
    },
    {
      "operation": "posts.engagement.comments",
      "body": {
        "urn": "7267273010393358336",
        "max_results": 500
      }
    }
  ],
  "webhook_url": "https://app.example.com/hooks/up2data"
}'
```

**Python**

```python
import requests

url = "https://api.up2data.ai/v1/batch"
headers = {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
}
payload = {
    "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"
        },
        {
            "operation": "posts.engagement.comments",
            "body": {
                "urn": "7267273010393358336",
                "max_results": 500
            }
        }
    ],
    "webhook_url": "https://app.example.com/hooks/up2data"
}

response = requests.post(url, headers=headers, json=payload)
print(response.json())
```

**Node**

```javascript
const url = "https://api.up2data.ai/v1/batch";
const headers = {
  "X-API-Key": "YOUR_API_KEY",
  "Content-Type": "application/json",
};
const body = {
  "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"
    },
    {
      "operation": "posts.engagement.comments",
      "body": {
        "urn": "7267273010393358336",
        "max_results": 500
      }
    }
  ],
  "webhook_url": "https://app.example.com/hooks/up2data"
};

const response = await fetch(url, {
  method: "POST",
  headers,
  body: JSON.stringify(body),
});
const data = await response.json();
console.log(data);
```

**Go**

```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
)

func main() {
	url := "https://api.up2data.ai/v1/batch"
	body := bytes.NewBufferString(`{
  "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"
    },
    {
      "operation": "posts.engagement.comments",
      "body": {
        "urn": "7267273010393358336",
        "max_results": 500
      }
    }
  ],
  "webhook_url": "https://app.example.com/hooks/up2data"
}`)
	req, err := http.NewRequest("POST", url, body)
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-API-Key", "YOUR_API_KEY")
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	fmt.Println(string(respBody))
}
```

## Response

**202**

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

## Webhook payload

**batch.completed**

```json
{
  "event": "batch.completed",
  "job": { "job_id": "job_7hd92kfa", "status": "completed", "total": 1000, "estimated_completion": "2026-07-07T14:03:00Z" }
}
```

## Errors

| Status | Type | When | Billed |
|---|---|---|---|
| 400 | `invalid_request` | Malformed body, an item missing type/url or operation/body, an unknown or not-yet-enabled operation, page/per_page/pagination_token in an operation body, max_results on a scalar operation, a Sales Navigator item missing (or sending both of) filters/sales_nav_url, a search.jobs item with no criterion, or more than 1,000 items. | No |
| 401 | `invalid_api_key` | Missing, malformed, or revoked key. | No |
| 402 | `insufficient_credits` | Balance can't cover the credits this submission would charge. | No |
| 429 | `rate_limited` | Over the 300 requests/min limit. Honor Retry-After. | No |
| 500 | `internal_error` | Something broke on the server side. | No |

See the [Errors guide](https://up2data.ai/docs/v1/guides/errors/) for every error type this version can return.
