---
name: up2data-api
description: Use when integrating the Up2Data API (/v1) - authenticating with X-API-Key, calling the synchronous profile, company, post and Sales Navigator endpoints, submitting and reading POST /v1/batch jobs, verifying and handling the batch.completed webhook, and reasoning about credits, billing and rate limits.
---

# Up2Data API (/v1) - Integration Guide

Up2Data's /v1 API is a usage-based B2B data API: real-time LinkedIn profile, company, post, and job enrichment, live people, company, job, and post search, Sales Navigator search, and asynchronous batch jobs, all through one `X-API-Key` header. Every /v1 capability is also batchable as an operation item, not just profile, company, and post enrich. Every call answers a `{data, meta}` envelope on success or an `{error}` envelope on failure. Sync enrich, activity, and a `profile`, `company`, `post`, or Sales Navigator batch item bill both when the target is found and when it comes back not found, private, deleted, or unreachable (`422 unprocessable_target`); every other batch operation refunds a `422` down to 0 once its queue settles. A call is only free when it fails with `400`, `401`, `402`, `429`, `500`, or `504`. Job search bills per job returned; people, company, and post search (Sales Navigator and standard alike) bill per page of up to 25 results, and empty pages are free. The base URL is `https://api.up2data.ai/v1`. Sync endpoints answer directly, typically in 2 to 8 seconds; submit `POST /batch` for volume and read results by webhook (`batch.completed`) or by polling `GET /batch/{job_id}`.

## How to use this skill

- Answer only from this file and the full reference at https://up2data.ai/docs/v1/llms-full.txt.
- If something is not covered in one of those two places, say so. Do not invent an endpoint, a request field, a query parameter, or a response field.
- Prefer a synchronous call for one-off lookups; use `POST /batch` once volume goes past a few hundred records.
- Every example is runnable once the placeholders are filled in. Keep them that way when you adapt them.

## Base URL

```
https://api.up2data.ai/v1
```

## Endpoints

| Method | Path | Credits | Mode |
|---|---|---|---|
| POST | [`/profiles/enrich`](https://up2data.ai/docs/v1/api/profiles-enrich/) | 1 credit - 4 with followers/connections or full skills (not stacking) | Sync |
| POST | [`/profiles/activity`](https://up2data.ai/docs/v1/api/profiles-activity/) | 2 credits per call | Sync |
| POST | [`/profiles/recommendations`](https://up2data.ai/docs/v1/api/profiles-recommendations/) | 1 credit per non-empty direction - direction both is up to 2; empty results are free | Sync |
| POST | [`/profiles/interests`](https://up2data.ai/docs/v1/api/profiles-interests/) | 1 credit per non-empty category - category all is up to 5; empty results are free | Sync |
| POST | [`/companies/enrich`](https://up2data.ai/docs/v1/api/companies-enrich/) | 1 credit | Sync |
| POST | [`/companies/jobs-count`](https://up2data.ai/docs/v1/api/companies-jobs-count/) | 1 credit | Sync |
| POST | [`/companies/posts`](https://up2data.ai/docs/v1/api/companies-posts/) | 1 credit per returned post | Sync |
| POST | [`/companies/headcount`](https://up2data.ai/docs/v1/api/companies-headcount/) | 1 credit | Sync |
| POST | [`/jobs/enrich`](https://up2data.ai/docs/v1/api/jobs-enrich/) | 1 credit | Sync |
| POST | [`/search/jobs`](https://up2data.ai/docs/v1/api/search-jobs/) | 1 credit per job returned | Sync |
| POST | [`/posts/enrich`](https://up2data.ai/docs/v1/api/posts-enrich/) | 1 credit | Sync |
| POST | [`/search/posts`](https://up2data.ai/docs/v1/api/search-posts/) | 1 credit per page (up to 25 posts) | Sync |
| POST | [`/posts/engagement/comments`](https://up2data.ai/docs/v1/api/posts-engagement-comments/) | 1 credit per returned comment | Sync |
| POST | [`/posts/engagement/reactions`](https://up2data.ai/docs/v1/api/posts-engagement-reactions/) | 1 credit per returned reaction | Sync |
| POST | [`/posts/engagement/reposts`](https://up2data.ai/docs/v1/api/posts-engagement-reposts/) | 1 credit per returned repost | Sync |
| POST | [`/search/people`](https://up2data.ai/docs/v1/api/search-people/) | 1 credit per page (up to 25 results) | Sync |
| POST | [`/search/people/sales-nav`](https://up2data.ai/docs/v1/api/search-people-sales-nav/) | 1 credit per page (up to 25 results) | Sync |
| POST | [`/search/people/standard`](https://up2data.ai/docs/v1/api/search-people-standard/) | 1 credit per page (up to 25 results) | Sync |
| POST | [`/search/companies`](https://up2data.ai/docs/v1/api/search-companies/) | 1 credit per page (up to 25 results) | Sync |
| POST | [`/search/companies/sales-nav`](https://up2data.ai/docs/v1/api/search-companies-sales-nav/) | 1 credit per page (up to 25 results) | Sync |
| POST | [`/search/companies/standard`](https://up2data.ai/docs/v1/api/search-companies-standard/) | 1 credit per page (up to 25 results) | Sync |
| POST | [`/batch`](https://up2data.ai/docs/v1/api/batch-create/) | 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 | Async |
| GET | [`/batch/{job_id}`](https://up2data.ai/docs/v1/api/batch-get/) | Free | Sync |
| GET | [`/batch/{job_id}/results`](https://up2data.ai/docs/v1/api/batch-results/) | Free | Sync |
| GET | [`/account`](https://up2data.ai/docs/v1/api/account-get/) | Free | Sync |

## Step 1 - Sync call or batch job

Sync, answered directly in the response - fine up to a few hundred records: [`POST /profiles/enrich`](https://up2data.ai/docs/v1/api/profiles-enrich.md), [`POST /profiles/activity`](https://up2data.ai/docs/v1/api/profiles-activity.md), [`POST /profiles/recommendations`](https://up2data.ai/docs/v1/api/profiles-recommendations.md), [`POST /profiles/interests`](https://up2data.ai/docs/v1/api/profiles-interests.md), [`POST /companies/enrich`](https://up2data.ai/docs/v1/api/companies-enrich.md), [`POST /companies/jobs-count`](https://up2data.ai/docs/v1/api/companies-jobs-count.md), [`POST /companies/posts`](https://up2data.ai/docs/v1/api/companies-posts.md), [`POST /companies/headcount`](https://up2data.ai/docs/v1/api/companies-headcount.md), [`POST /jobs/enrich`](https://up2data.ai/docs/v1/api/jobs-enrich.md), [`POST /search/jobs`](https://up2data.ai/docs/v1/api/search-jobs.md), [`POST /posts/enrich`](https://up2data.ai/docs/v1/api/posts-enrich.md), [`POST /search/posts`](https://up2data.ai/docs/v1/api/search-posts.md), [`POST /posts/engagement/comments`](https://up2data.ai/docs/v1/api/posts-engagement-comments.md), [`POST /posts/engagement/reactions`](https://up2data.ai/docs/v1/api/posts-engagement-reactions.md), [`POST /posts/engagement/reposts`](https://up2data.ai/docs/v1/api/posts-engagement-reposts.md), [`POST /search/people`](https://up2data.ai/docs/v1/api/search-people.md), [`POST /search/people/sales-nav`](https://up2data.ai/docs/v1/api/search-people-sales-nav.md), [`POST /search/people/standard`](https://up2data.ai/docs/v1/api/search-people-standard.md), [`POST /search/companies`](https://up2data.ai/docs/v1/api/search-companies.md), [`POST /search/companies/sales-nav`](https://up2data.ai/docs/v1/api/search-companies-sales-nav.md), [`POST /search/companies/standard`](https://up2data.ai/docs/v1/api/search-companies-standard.md), [`GET /account`](https://up2data.ai/docs/v1/api/account-get.md).

Past that, use batch: [`POST /batch`](https://up2data.ai/docs/v1/api/batch-create.md), [`GET /batch/{job_id}`](https://up2data.ai/docs/v1/api/batch-get.md), [`GET /batch/{job_id}/results`](https://up2data.ai/docs/v1/api/batch-results.md). Submit up to 1,000 items in one job: profile, company, and post enrich items freely mixed with operation items running any other /v1 capability (profile activity, recommendations, interests, company posts/jobs-count/headcount, job enrich, post engagement, or any search) as a batch item instead of a loop of sync calls.

## Step 2 - Authenticate

The X-API-Key header, where keys come from, and the 401/402 rows it can return.

## The X-API-Key header

Every request carries one header:

```http
X-API-Key: YOUR_API_KEY
```

No OAuth flow, no token exchange, no expiry to track - the key you generate is the credential you send, on every call, until you revoke it. Keys are created and rotated at [https://app.up2data.ai](https://app.up2data.ai).

> **Warning:** Never expose a key in client-side code, a public repository, or a URL. Anyone holding it can spend your credits.

Multiple keys can be active on the same account at once, so rotation never means downtime: create a new key, deploy it, then revoke the old one. Revocation takes effect within 60 seconds - keys are cached briefly, so the old key can keep working for a few more seconds after you revoke it.

## Example call

```bash
curl -X POST https://api.up2data.ai/v1/profiles/enrich \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://linkedin.com/in/satyanadella"}'
```

## When a key fails

| Status | Type | Meaning |
|---|---|---|
| 401 | `invalid_api_key` | Missing, malformed, or revoked key |
| 402 | `insufficient_credits` | The key is valid, but your balance cannot cover this call |

```json
{
  "error": {
    "type": "invalid_api_key",
    "message": "The API key provided is invalid or has been revoked.",
    "requestId": "req_1n4kd02p"
  }
}
```

Neither of these is billed - see the full status table in [Errors](https://up2data.ai/docs/v1/guides/errors/). For what a call actually costs, see [Credits](https://up2data.ai/docs/v1/guides/credits/).


## Step 3 - 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.


## Step 4 - Webhooks and signature verification

batch.completed delivery, signature verification, and the retry schedule.

## batch.completed

The only webhook event `/v1` sends today. It fires once a [`POST /v1/batch`](https://up2data.ai/docs/v1/api/batch-create/) job finishes, whether every item resolved or some failed.

```http
POST https://your-server.com/hooks/up2data
X-Up2Data-Event: batch.completed
X-Up2Data-Signature: t=1767225600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Content-Type: application/json
```

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

The payload is a summary, not the results - fetch those with [`GET /v1/batch/{job_id}`](https://up2data.ai/docs/v1/api/batch-get/) or [`GET /v1/batch/{job_id}/results`](https://up2data.ai/docs/v1/api/batch-results/) once you get the event.

> **Info:** Return any `2xx` status within 10 seconds to acknowledge delivery. Anything else counts as a failed attempt.

## Verifying the signature

`X-Up2Data-Signature` is an HMAC-SHA256 of `{timestamp}.{raw_body}`, computed with your webhook secret over the raw request body, before you parse it as JSON.

```python
import hashlib, hmac, time

def verify(payload: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    pairs = dict(p.split("=", 1) for p in header.split(","))
    timestamp, signature = pairs["t"], pairs["v1"]
    if abs(time.time() - int(timestamp)) > tolerance:
        return False  # replay protection
    expected = hmac.new(
        secret.encode(), f"{timestamp}.".encode() + payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
```

```javascript
import crypto from "node:crypto";

function verify(payload, header, secret, tolerance = 300) {
  const pairs = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const { t, v1 } = pairs;
  if (Math.abs(Date.now() / 1000 - Number(t)) > tolerance) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${payload}`)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
```

## Retries

If your endpoint doesn't return a `2xx` within 10 seconds, delivery is retried 5 times with exponential backoff: 1m, 5m, 30m, 2h, 6h. Retries carry the same `job_id`, so dedupe on it rather than assuming one delivery per job.

## Skipping webhooks entirely

`webhook_url` is optional on `POST /v1/batch`. Without it, poll [`GET /v1/batch/{job_id}`](https://up2data.ai/docs/v1/api/batch-get/) instead - see [Batch jobs](https://up2data.ai/docs/v1/guides/batch-jobs/).


## Rate limits

Per-key request limits, the per-organization search quota, and the headers that report them.

## Limits

| Limit | Default | Scope |
|---|---|---|
| Requests per minute | 300 | Per API key |
| Requests per minute, all keys | 10,000 | Per organization |
| Requests in flight | 6,000 | Per organization |
| Searches per hour | 20 | Per organization |

The per-key requests per minute and the searches per hour can be raised for your organization; [`GET /v1/account`](https://up2data.ai/docs/v1/api/account-get/) reports the values that apply to you, plus the searches left this hour. There is no cap on concurrent batch jobs.

Request limits apply per API key across every enrich, search, and batch call. Submitting a batch job counts as one request no matter how many items it holds.

The search quota counts calls, not results: every `POST /v1/search/*` call is 1 - [people](https://up2data.ai/docs/v1/api/search-people/), [companies](https://up2data.ai/docs/v1/api/search-companies/), [jobs](https://up2data.ai/docs/v1/api/search-jobs/), [posts](https://up2data.ai/docs/v1/api/search-posts/), their Sales Navigator ([people](https://up2data.ai/docs/v1/api/search-people-sales-nav/), [companies](https://up2data.ai/docs/v1/api/search-companies-sales-nav/)) and standard ([people](https://up2data.ai/docs/v1/api/search-people-standard/), [companies](https://up2data.ai/docs/v1/api/search-companies-standard/)) variants - and so is every `search.*` operation item (Sales Navigator, standard, jobs, or posts) you send inside a `POST /v1/batch` job. Enrich items and every non-search operation item inside a batch don't count against it. The quota is shared per organization, across every key. A sync search call returns at most 100 results (`pagination.has_more` shows when more exist); for a bigger pull - up to 2,500 for people, 1,000 for companies, 500 for jobs or posts - send it as an operation item inside [`POST /v1/batch`](https://up2data.ai/docs/v1/api/batch-create/) instead of issuing more sync searches.

Need a higher limit? Email support@uptodata.io, and for search limits specifically, put "Higher search limits" in the subject line.

## Response headers

Every authenticated business response carries:

```http
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1767225600
X-Global-RateLimit-Limit: 10000
X-Global-RateLimit-Remaining: 9987
X-Active-Requests-Limit: 6000
X-Active-Requests-Remaining: 5998
```

The global headers count requests across every key in the organization. The active-requests headers count calls still in flight, including this one.

Search responses also carry:

```http
X-Search-RateLimit-Limit: 20
X-Search-RateLimit-Remaining: 17
X-Search-RateLimit-Reset: 1767229200
```

## Getting rate limited

```json
{
  "error": {
    "type": "rate_limited",
    "message": "Search rate limit exceeded (20 searches per hour per organization). A sync search returns at most 100 results per call - for a bigger pull, send it as a POST /v1/batch search item instead of sending more searches. Need a higher limit? Email support@uptodata.io.",
    "requestId": "req_55azx1qm"
  }
}
```

The response includes a `Retry-After` header, in seconds. Rate-limited requests are never billed. Back off exponentially with jitter, and treat `Retry-After` as a floor rather than the exact wait time. See [Errors](https://up2data.ai/docs/v1/guides/errors/) for the full status table.


## Credits and the billing rule

The rate for every endpoint, and why a not-found target (422) is billed everywhere.

## How billing works

Up2Data charges by credit, one currency for every operation. Every successful response carries a `meta` block reporting exactly what that call cost; an error response skips `meta` and returns just `{"error": {"type", "message", "requestId"}}` instead, but a `requestId` is there either way:

```json
"meta": {
  "creditsUsed": 1,
  "creditsRemaining": 98420,
  "billed": true,
  "reason": "profile_found",
  "requestId": "req_8f2ka91x"
}
```

## Rates

| Endpoint | Rate |
|---|---|
| [`POST /v1/profiles/enrich`](https://up2data.ai/docs/v1/api/profiles-enrich/) | 1 credit, or 4 credits with `with_followers_and_connections`, `with_full_skills_and_endorsements`, or by listing `followers_count`, `connections_count`, or `skill_endorsements` in `fields` (none of these stack) |
| [`POST /v1/companies/enrich`](https://up2data.ai/docs/v1/api/companies-enrich/) | 1 credit |
| [`POST /v1/companies/jobs-count`](https://up2data.ai/docs/v1/api/companies-jobs-count/) | 1 credit |
| [`POST /v1/companies/posts`](https://up2data.ai/docs/v1/api/companies-posts/) | 1 credit per returned post; empty pages are free |
| [`POST /v1/companies/headcount`](https://up2data.ai/docs/v1/api/companies-headcount/) | 1 credit |
| [`POST /v1/posts/enrich`](https://up2data.ai/docs/v1/api/posts-enrich/) | 1 credit |
| [`POST /v1/jobs/enrich`](https://up2data.ai/docs/v1/api/jobs-enrich/) | 1 credit |
| [`POST /v1/posts/engagement/comments`](https://up2data.ai/docs/v1/api/posts-engagement-comments/) | 1 credit per returned comment; empty pages are free |
| [`POST /v1/posts/engagement/reactions`](https://up2data.ai/docs/v1/api/posts-engagement-reactions/) | 1 credit per returned reaction; empty pages are free |
| [`POST /v1/posts/engagement/reposts`](https://up2data.ai/docs/v1/api/posts-engagement-reposts/) | 1 credit per returned repost; empty pages are free |
| [`POST /v1/profiles/activity`](https://up2data.ai/docs/v1/api/profiles-activity/) | 2 credits per call (one `kind` per call) |
| [`POST /v1/profiles/recommendations`](https://up2data.ai/docs/v1/api/profiles-recommendations/) | 1 credit per non-empty direction (`both` is up to 2); empty results are free |
| [`POST /v1/profiles/interests`](https://up2data.ai/docs/v1/api/profiles-interests/) | 1 credit per non-empty category (`all` is up to 5); empty results are free |
| [`POST /v1/search/people`](https://up2data.ai/docs/v1/api/search-people/), [`POST /v1/search/people/sales-nav`](https://up2data.ai/docs/v1/api/search-people-sales-nav/), [`POST /v1/search/people/standard`](https://up2data.ai/docs/v1/api/search-people-standard/) | 1 credit per page of up to 25 results; empty pages are free |
| [`POST /v1/search/companies`](https://up2data.ai/docs/v1/api/search-companies/), [`POST /v1/search/companies/sales-nav`](https://up2data.ai/docs/v1/api/search-companies-sales-nav/), [`POST /v1/search/companies/standard`](https://up2data.ai/docs/v1/api/search-companies-standard/) | 1 credit per page of up to 25 results; empty pages are free |
| [`POST /v1/search/jobs`](https://up2data.ai/docs/v1/api/search-jobs/) | 1 credit per job returned; zero results are free |
| [`POST /v1/search/posts`](https://up2data.ai/docs/v1/api/search-posts/) | 1 credit per page of up to 25 posts; empty pages are free |
| [`POST /v1/batch`](https://up2data.ai/docs/v1/api/batch-create/) items | `profile`, `company`, `post`, and Sales Navigator search items: 1 credit each (search items reserve `max_results` credits, refunded for any shortfall) - never refunded for a failed item. Every other operation item reserves the worst case at submission, matching its own sync rate above, and refunds down to what it actually billed once its queue settles - see the [full table](https://up2data.ai/docs/v1/api/batch-create/) for exact numbers |
| [`GET /v1/batch/{job_id}`](https://up2data.ai/docs/v1/api/batch-get/), [`GET /v1/batch/{job_id}/results`](https://up2data.ai/docs/v1/api/batch-results/) | Free |
| [`GET /v1/account`](https://up2data.ai/docs/v1/api/account-get/) | Free |

## The billing rule

**A not-found target is billed everywhere.** Whenever Up2Data scrapes a target and finds it missing, private, deleted, or unreachable (422 `unprocessable_target`), that counts as billable work - on synchronous enrich ([`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/), [`jobs/enrich`](https://up2data.ai/docs/v1/api/jobs-enrich/)), on [`profiles/activity`](https://up2data.ai/docs/v1/api/profiles-activity/), on [`companies/jobs-count`](https://up2data.ai/docs/v1/api/companies-jobs-count/), [`companies/posts`](https://up2data.ai/docs/v1/api/companies-posts/) and [`companies/headcount`](https://up2data.ai/docs/v1/api/companies-headcount/) (1 credit), on the post engagement endpoints and on [`profiles/recommendations`](https://up2data.ai/docs/v1/api/profiles-recommendations/) and [`profiles/interests`](https://up2data.ai/docs/v1/api/profiles-interests/) (1 credit), on [`search/posts`](https://up2data.ai/docs/v1/api/search-posts/) when the `author_url` or `author_urn` profile is unreachable (1 credit), and on every batch item. Outside a batch job, only 400, 401, 402, 429, 500, and 504 are never billed.

This is deliberately stricter than a pure pay-on-success model: the scrape ran and returned an answer, so it's billed like a hit. Only requests that return no data because of our failure (500, 504) or were rejected before any work started (400, 401, 402, 429) are free.

> **Info:** A `POST /v1/batch` job reserves the worst case for every item at submission. A `profile`, `company`, `post`, or Sales Navigator search item never refunds for a failed item: a `422 unprocessable_target` item bills at its type's rate just like a successful one, and so does an item that fails with a retryable error like `500` or `504`. A Sales Navigator search item's unused `max_results` reservation is the only thing on those items that refunds, once that search runs. Every other operation item settles differently: it reserves the same worst case at submission, but refunds down to what it actually billed once its queue finishes, so a failed or empty item on those operations costs 0. See [Batch jobs](https://up2data.ai/docs/v1/guides/batch-jobs/).

See [Errors](https://up2data.ai/docs/v1/guides/errors/) for the complete status table and [Rate limits](https://up2data.ai/docs/v1/guides/rate-limits/) for what happens when you're throttled.


## Errors

Every error type, its status code, and whether it is billed.

## Error shape

Every error, on every endpoint, has the same envelope:

```json
{
  "error": {
    "type": "unprocessable_target",
    "message": "This profile is private or has been deleted.",
    "requestId": "req_9k2mm4xd"
  }
}
```

`requestId` rides along on every response, success or failure - include it when you contact support.

## Error types

| Status | Type | When | Billed |
|---|---|---|---|
| 400 | `invalid_request` | Malformed body, unknown field, or a bad URL format | No |
| 401 | `invalid_api_key` | Missing, malformed, or revoked key | No |
| 402 | `insufficient_credits` | The key is valid, but your balance cannot cover the call | No |
| 404 | `not_found` | The batch job referenced by `job_id` does not exist | No |
| 422 | `unprocessable_target` | The target profile, company, or post is not found, private, deleted, or unreachable | Yes - everywhere, including `profiles/activity` and each batch item |
| 429 | `rate_limited` | Over the request, search, or concurrent-batch limit; honor `Retry-After` | No |
| 500 | `internal_error` | Something broke on our side | No |
| 504 | `upstream_timeout` | The live scrape did not finish in time | No |
| n/a | `upstream_error` | Another upstream scrape failure, distinct from a timeout | No |

> **Info:** `upstream_error` covers an upstream scrape failure other than a timeout. It's never billed, and safe to retry the same as `upstream_timeout`.

## Retries

429, 500, and 504 (and `upstream_error`) are safe to retry - outside a batch job, none of them are billed. Use exponential backoff with jitter, and treat `Retry-After` as a floor, not a fixed delay.

`POST /v1/batch` accepts an `Idempotency-Key` header so a retried submission returns the original job instead of creating a duplicate - see [Batch jobs](https://up2data.ai/docs/v1/guides/batch-jobs/).

## Batch items fail individually

A batch job never fails as a whole because one item did. Each entry in `results` or `failures` carries its own status and its own `operation`. A `profile`, `company`, `post`, or Sales Navigator search item was already billed in full at submission: a success, an `unprocessable_target` failure, and a `500` or `504` failure all bill the same - 1 credit per enrich item, or a `max_results`-sized reservation per Sales Navigator search item - and the only refund is the unused part of that reservation, once the search runs. Every other operation item reserves the same worst case at submission but settles once its queue finishes: a failed item (`unprocessable_target`, or a company or job that isn't found) costs 0 and refunds in full, the same as an empty result. Items still processing affect neither count. See [Credits](https://up2data.ai/docs/v1/guides/credits/) for the full rate table and [Rate limits](https://up2data.ai/docs/v1/guides/rate-limits/) for the quota headers.


## Gotchas checklist

Check every one of these before calling an integration done.

- [ ] Identify a target with exactly one of its allowed identifiers (`url` or `urn` for profiles/posts; `url`, `domain`, or `linkedin_id` for companies) - never send more than one or none.
- [ ] `422 unprocessable_target` is billed on every sync endpoint, and on a `profile`, `company`, `post`, or Sales Navigator batch item. Every other batch operation refunds a `422` down to 0 once its queue settles. Only `400`, `401`, `402`, `429`, `500` and `504` are free outside a batch job.
- [ ] Restricting a response with `fields` does not reduce the cost; `with_followers_and_connections` and `with_full_skills_and_endorsements` raise it to 4 credits and do not stack.
- [ ] Send an `Idempotency-Key` header on `POST /batch` so a retried submission returns the original job instead of creating a duplicate.
- [ ] `POST /batch` reserves the worst case for every item at submission. A `profile`, `company`, `post`, or Sales Navigator search item never refunds for a failed item, only the unused `max_results` of a search item; every other operation item settles down to what it actually billed once its queue finishes, so a failed or empty item on those costs 0.
- [ ] Verify `X-Up2Data-Signature` (HMAC-SHA256 of `{timestamp}.{raw_body}`) before trusting a `batch.completed` payload, and respond `2xx` within 10 seconds or the delivery is retried.
- [ ] `GET /batch/{job_id}` is free to poll as often as you like; prefer `GET /batch/{job_id}/results` with a `cursor` for large jobs instead of re-fetching the whole `results` array.
- [ ] Provide `filters` or `sales_nav_url` on a search endpoint, never both, never neither.
- [ ] The search quota (20/hour per organization) counts calls, not results - prefer a high `max_results` over issuing more searches.
- [ ] Honor `Retry-After` on a `429`, and treat it as a floor, not a fixed delay.

## Full reference

Every request field, response shape, webhook payload and error is in https://up2data.ai/docs/v1/llms-full.txt. Read it before answering field-level questions.
