---
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://docs.up2data.ai/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://docs.up2data.ai/v1/api/profiles-enrich/) | 1 credit - 2 with any flag, 3 with with_company_type | Sync |
| POST | [`/profiles/activity`](https://docs.up2data.ai/v1/api/profiles-activity/) | 2 credits per call | Sync |
| POST | [`/profiles/recommendations`](https://docs.up2data.ai/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://docs.up2data.ai/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://docs.up2data.ai/v1/api/companies-enrich/) | 1 credit - 2 with company type | Sync |
| POST | [`/companies/jobs-count`](https://docs.up2data.ai/v1/api/companies-jobs-count/) | 1 credit | Sync |
| POST | [`/companies/posts`](https://docs.up2data.ai/v1/api/companies-posts/) | 1 credit per returned post | Sync |
| POST | [`/companies/headcount`](https://docs.up2data.ai/v1/api/companies-headcount/) | 1 credit | Sync |
| POST | [`/jobs/enrich`](https://docs.up2data.ai/v1/api/jobs-enrich/) | 1 credit | Sync |
| POST | [`/search/jobs`](https://docs.up2data.ai/v1/api/search-jobs/) | 1 credit per job returned | Sync |
| POST | [`/posts/enrich`](https://docs.up2data.ai/v1/api/posts-enrich/) | 1 credit - +1 per flag | Sync |
| POST | [`/search/posts`](https://docs.up2data.ai/v1/api/search-posts/) | 1 credit per page (up to 25 posts) | Sync |
| POST | [`/posts/engagement/comments`](https://docs.up2data.ai/v1/api/posts-engagement-comments/) | 1 credit per page of up to 100 comments | Sync |
| POST | [`/posts/engagement/reactions`](https://docs.up2data.ai/v1/api/posts-engagement-reactions/) | 1 credit per page of up to 100 reactions | Sync |
| POST | [`/posts/engagement/reposts`](https://docs.up2data.ai/v1/api/posts-engagement-reposts/) | 1 credit per page of up to 100 reposts | Sync |
| POST | [`/search/people`](https://docs.up2data.ai/v1/api/search-people/) (BETA) | 1 credit per page (up to 25 results) | Sync |
| POST | [`/search/people/sales-nav`](https://docs.up2data.ai/v1/api/search-people-sales-nav/) (BETA) | 1 credit per page (up to 25 results) | Sync |
| POST | [`/search/people/standard`](https://docs.up2data.ai/v1/api/search-people-standard/) | 1 credit per page (up to 25 results) | Sync |
| POST | [`/search/companies`](https://docs.up2data.ai/v1/api/search-companies/) | 1 credit per page (up to 25 results) | Sync |
| POST | [`/search/companies/sales-nav`](https://docs.up2data.ai/v1/api/search-companies-sales-nav/) (BETA) | 1 credit per page (up to 25 results) | Sync |
| POST | [`/search/companies/standard`](https://docs.up2data.ai/v1/api/search-companies-standard/) | 1 credit per page (up to 25 results) | Sync |
| POST | [`/batch`](https://docs.up2data.ai/v1/api/batch-create/) | Billed at submission - profile, company, and post items: 1 credit each (more with the same flags as their sync endpoint), 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://docs.up2data.ai/v1/api/batch-get/) | Free | Sync |
| GET | [`/batch/{job_id}/results`](https://docs.up2data.ai/v1/api/batch-results/) | Free | Sync |
| GET | [`/signals`](https://docs.up2data.ai/v1/api/signals-list/) | Free | Sync |
| POST | [`/signals`](https://docs.up2data.ai/v1/api/signals-create/) | Free - each run bills as a batch job at run time | Sync |
| GET | [`/signals/{signal_id}`](https://docs.up2data.ai/v1/api/signals-get/) | Free | Sync |
| PATCH | [`/signals/{signal_id}`](https://docs.up2data.ai/v1/api/signals-update/) | Free | Sync |
| DELETE | [`/signals/{signal_id}`](https://docs.up2data.ai/v1/api/signals-delete/) | Free | Sync |
| POST | [`/signals/{signal_id}/pause`](https://docs.up2data.ai/v1/api/signals-pause/) | Free | Sync |
| POST | [`/signals/{signal_id}/resume`](https://docs.up2data.ai/v1/api/signals-resume/) | Free | Sync |
| GET | [`/signals/{signal_id}/runs`](https://docs.up2data.ai/v1/api/signals-runs/) | Free | Sync |
| GET | [`/account`](https://docs.up2data.ai/v1/api/account-get/) | Free | Sync |
| POST | [`/account/webhook-secret/rotate`](https://docs.up2data.ai/v1/api/account-webhook-secret-rotate/) | 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://docs.up2data.ai/v1/api/profiles-enrich.md), [`POST /profiles/activity`](https://docs.up2data.ai/v1/api/profiles-activity.md), [`POST /profiles/recommendations`](https://docs.up2data.ai/v1/api/profiles-recommendations.md), [`POST /profiles/interests`](https://docs.up2data.ai/v1/api/profiles-interests.md), [`POST /companies/enrich`](https://docs.up2data.ai/v1/api/companies-enrich.md), [`POST /companies/jobs-count`](https://docs.up2data.ai/v1/api/companies-jobs-count.md), [`POST /companies/posts`](https://docs.up2data.ai/v1/api/companies-posts.md), [`POST /companies/headcount`](https://docs.up2data.ai/v1/api/companies-headcount.md), [`POST /jobs/enrich`](https://docs.up2data.ai/v1/api/jobs-enrich.md), [`POST /search/jobs`](https://docs.up2data.ai/v1/api/search-jobs.md), [`POST /posts/enrich`](https://docs.up2data.ai/v1/api/posts-enrich.md), [`POST /search/posts`](https://docs.up2data.ai/v1/api/search-posts.md), [`POST /posts/engagement/comments`](https://docs.up2data.ai/v1/api/posts-engagement-comments.md), [`POST /posts/engagement/reactions`](https://docs.up2data.ai/v1/api/posts-engagement-reactions.md), [`POST /posts/engagement/reposts`](https://docs.up2data.ai/v1/api/posts-engagement-reposts.md), [`POST /search/people`](https://docs.up2data.ai/v1/api/search-people.md), [`POST /search/people/sales-nav`](https://docs.up2data.ai/v1/api/search-people-sales-nav.md), [`POST /search/people/standard`](https://docs.up2data.ai/v1/api/search-people-standard.md), [`POST /search/companies`](https://docs.up2data.ai/v1/api/search-companies.md), [`POST /search/companies/sales-nav`](https://docs.up2data.ai/v1/api/search-companies-sales-nav.md), [`POST /search/companies/standard`](https://docs.up2data.ai/v1/api/search-companies-standard.md), [`GET /signals`](https://docs.up2data.ai/v1/api/signals-list.md), [`POST /signals`](https://docs.up2data.ai/v1/api/signals-create.md), [`GET /signals/{signal_id}`](https://docs.up2data.ai/v1/api/signals-get.md), [`PATCH /signals/{signal_id}`](https://docs.up2data.ai/v1/api/signals-update.md), [`DELETE /signals/{signal_id}`](https://docs.up2data.ai/v1/api/signals-delete.md), [`POST /signals/{signal_id}/pause`](https://docs.up2data.ai/v1/api/signals-pause.md), [`POST /signals/{signal_id}/resume`](https://docs.up2data.ai/v1/api/signals-resume.md), [`GET /signals/{signal_id}/runs`](https://docs.up2data.ai/v1/api/signals-runs.md), [`GET /account`](https://docs.up2data.ai/v1/api/account-get.md), [`POST /account/webhook-secret/rotate`](https://docs.up2data.ai/v1/api/account-webhook-secret-rotate.md).

Past that, use batch: [`POST /batch`](https://docs.up2data.ai/v1/api/batch-create.md), [`GET /batch/{job_id}`](https://docs.up2data.ai/v1/api/batch-get.md), [`GET /batch/{job_id}/results`](https://docs.up2data.ai/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://docs.up2data.ai/v1/guides/errors/). For what a call actually costs, see [Credits](https://docs.up2data.ai/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://docs.up2data.ai/v1/api/profiles-enrich/), [companies/enrich](https://docs.up2data.ai/v1/api/companies-enrich/), [posts/enrich](https://docs.up2data.ai/v1/api/posts-enrich/) - are fine up to a few hundred records. Past that, use [`POST /v1/batch`](https://docs.up2data.ai/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": "...", "fields"?, "with_followers_and_connections"?, "with_full_skills_and_endorsements"?, "with_latest_company_details"?, "with_company_type"? }` |
| `company` | `{ "type": "company", "url": "...", "fields"?, "with_company_type"? }` |
| `post` | `{ "type": "post", "url": "...", "with_comments"?, "with_reactions"?, "with_reposts"? }` |

The optional keys are the same flags and `fields` the matching sync endpoint takes, with the same meaning and the same price: a flagged `profile` item costs 2 credits (3 with `with_company_type`, which needs `with_latest_company_details`), a `company` item with `with_company_type` costs 2, and each engagement flag on a `post` item adds 1 credit and bundles the first 25 of that kind into the result entry's `data`.

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://docs.up2data.ai/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://docs.up2data.ai/v1/guides/credits/) for the per-item rates and [Rate limits](https://docs.up2data.ai/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://docs.up2data.ai/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://docs.up2data.ai/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://docs.up2data.ai/v1/guides/webhooks/).

For large jobs, [`GET /v1/batch/{job_id}/results`](https://docs.up2data.ai/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://docs.up2data.ai/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://docs.up2data.ai/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://docs.up2data.ai/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://docs.up2data.ai/v1/api/batch-get/) or [`GET /v1/batch/{job_id}/results`](https://docs.up2data.ai/v1/api/batch-results/) once you get the event.

A [signal](https://docs.up2data.ai/v1/guides/signals/) sends the same event once per run, to the `webhook_url` in its `request`. Map the `job_id` back to the signal with [`GET /v1/signals/{signal_id}/runs`](https://docs.up2data.ai/v1/api/signals-runs/), or keep one webhook URL per signal.

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

## Your signing secret

Every organization has one signing secret, a `whsec_` string that is the same for every webhook you receive. Read it from `webhook.signing_secret` on [`GET /v1/account`](https://docs.up2data.ai/v1/api/account-get/), or copy it from Settings, Webhook Integrations in the portal; it is created the first time it is read or used. To replace it, call [`POST /v1/account/webhook-secret/rotate`](https://docs.up2data.ai/v1/api/account-webhook-secret-rotate/) or press Rotate in the portal. Rotation is immediate and there is no overlap window: deliveries after that point, including retries of earlier jobs, are signed with the new secret only, so switch your verifier first.

## Verifying the signature

`X-Up2Data-Signature` is an HMAC-SHA256 of `{timestamp}.{raw_body}`, computed with your signing 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://docs.up2data.ai/v1/api/batch-get/) instead - see [Batch jobs](https://docs.up2data.ai/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 | 5,000 | Per API key |
| Requests per minute, all keys | 10,000 | Per organization |
| Requests in flight | 6,000 | Per organization |
| Searches per hour | 5,000 | Per organization |

The per-key requests per minute and the searches per hour can be raised for your organization; [`GET /v1/account`](https://docs.up2data.ai/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://docs.up2data.ai/v1/api/search-people/), [companies](https://docs.up2data.ai/v1/api/search-companies/), [jobs](https://docs.up2data.ai/v1/api/search-jobs/), [posts](https://docs.up2data.ai/v1/api/search-posts/), their Sales Navigator ([people](https://docs.up2data.ai/v1/api/search-people-sales-nav/), [companies](https://docs.up2data.ai/v1/api/search-companies-sales-nav/)) and standard ([people](https://docs.up2data.ai/v1/api/search-people-standard/), [companies](https://docs.up2data.ai/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://docs.up2data.ai/v1/api/batch-create/) instead of issuing more sync searches.

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

## Response headers

Every authenticated business response carries:

```http
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4987
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: 5000
X-Search-RateLimit-Remaining: 4997
X-Search-RateLimit-Reset: 1767229200
```

## Getting rate limited

```json
{
  "error": {
    "type": "rate_limited",
    "message": "Search rate limit exceeded (5,000 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@up2data.ai.",
    "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://docs.up2data.ai/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://docs.up2data.ai/v1/api/profiles-enrich/) | 1 credit, or 2 credits with any of `with_followers_and_connections`, `with_full_skills_and_endorsements`, `with_latest_company_details`, or by listing `followers_count` or `connections_count` in `fields`; 3 credits with `with_latest_company_details` plus `with_company_type` |
| [`POST /v1/companies/enrich`](https://docs.up2data.ai/v1/api/companies-enrich/) | 1 credit, or 2 credits with `with_company_type` |
| [`POST /v1/companies/jobs-count`](https://docs.up2data.ai/v1/api/companies-jobs-count/) | 1 credit |
| [`POST /v1/companies/posts`](https://docs.up2data.ai/v1/api/companies-posts/) | 1 credit per returned post; empty pages are free |
| [`POST /v1/companies/headcount`](https://docs.up2data.ai/v1/api/companies-headcount/) | 1 credit |
| [`POST /v1/posts/enrich`](https://docs.up2data.ai/v1/api/posts-enrich/) | 1 credit, +1 credit per `with_comments`, `with_reactions`, `with_reposts` flag (first 25 of each) |
| [`POST /v1/jobs/enrich`](https://docs.up2data.ai/v1/api/jobs-enrich/) | 1 credit |
| [`POST /v1/posts/engagement/comments`](https://docs.up2data.ai/v1/api/posts-engagement-comments/) | 1 credit per page of up to 100 comments; empty pages are free |
| [`POST /v1/posts/engagement/reactions`](https://docs.up2data.ai/v1/api/posts-engagement-reactions/) | 1 credit per page of up to 100 reactions; empty pages are free |
| [`POST /v1/posts/engagement/reposts`](https://docs.up2data.ai/v1/api/posts-engagement-reposts/) | 1 credit per page of up to 100 reposts; empty pages are free |
| [`POST /v1/profiles/activity`](https://docs.up2data.ai/v1/api/profiles-activity/) | 2 credits per call (one `kind` per call) |
| [`POST /v1/profiles/recommendations`](https://docs.up2data.ai/v1/api/profiles-recommendations/) | 1 credit per non-empty direction (`both` is up to 2); empty results are free |
| [`POST /v1/profiles/interests`](https://docs.up2data.ai/v1/api/profiles-interests/) | 1 credit per non-empty category (`all` is up to 5); empty results are free |
| [`POST /v1/search/people`](https://docs.up2data.ai/v1/api/search-people/), [`POST /v1/search/people/sales-nav`](https://docs.up2data.ai/v1/api/search-people-sales-nav/), [`POST /v1/search/people/standard`](https://docs.up2data.ai/v1/api/search-people-standard/) | 1 credit per page of up to 25 results; empty pages are free |
| [`POST /v1/search/companies`](https://docs.up2data.ai/v1/api/search-companies/), [`POST /v1/search/companies/sales-nav`](https://docs.up2data.ai/v1/api/search-companies-sales-nav/), [`POST /v1/search/companies/standard`](https://docs.up2data.ai/v1/api/search-companies-standard/) | 1 credit per page of up to 25 results; empty pages are free |
| [`POST /v1/search/jobs`](https://docs.up2data.ai/v1/api/search-jobs/) | 1 credit per job returned; zero results are free |
| [`POST /v1/search/posts`](https://docs.up2data.ai/v1/api/search-posts/) | 1 credit per page of up to 25 posts; empty pages are free |
| [`POST /v1/batch`](https://docs.up2data.ai/v1/api/batch-create/) items | `profile`, `company`, `post`, and Sales Navigator search items: 1 credit each, or the flagged rate of the matching sync endpoint when the item carries its flags (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://docs.up2data.ai/v1/api/batch-create/) for exact numbers |
| [`GET /v1/batch/{job_id}`](https://docs.up2data.ai/v1/api/batch-get/), [`GET /v1/batch/{job_id}/results`](https://docs.up2data.ai/v1/api/batch-results/) | Free |
| [`GET /v1/account`](https://docs.up2data.ai/v1/api/account-get/) | Free |
| [`/v1/signals`](https://docs.up2data.ai/v1/api/signals-create/) routes | Free to schedule, read, pause, resume and delete. Each run submits a batch job billed at the batch rates above, at run time, from your balance at that moment |

## 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://docs.up2data.ai/v1/api/profiles-enrich/), [`companies/enrich`](https://docs.up2data.ai/v1/api/companies-enrich/), [`posts/enrich`](https://docs.up2data.ai/v1/api/posts-enrich/), [`jobs/enrich`](https://docs.up2data.ai/v1/api/jobs-enrich/)), on [`profiles/activity`](https://docs.up2data.ai/v1/api/profiles-activity/), on [`companies/jobs-count`](https://docs.up2data.ai/v1/api/companies-jobs-count/), [`companies/posts`](https://docs.up2data.ai/v1/api/companies-posts/) and [`companies/headcount`](https://docs.up2data.ai/v1/api/companies-headcount/) (1 credit), on the post engagement endpoints and on [`profiles/recommendations`](https://docs.up2data.ai/v1/api/profiles-recommendations/) and [`profiles/interests`](https://docs.up2data.ai/v1/api/profiles-interests/) (1 credit), on [`search/posts`](https://docs.up2data.ai/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://docs.up2data.ai/v1/guides/batch-jobs/).

See [Errors](https://docs.up2data.ai/v1/guides/errors/) for the complete status table and [Rate limits](https://docs.up2data.ai/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`, or the signal referenced by `signal_id`, does not exist | No |
| 409 | `conflict` | The signal is completed and cannot be paused or resumed; change its `schedule` instead | 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 or search 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; on `/signals` routes, the scheduling service is unreachable (503) | 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://docs.up2data.ai/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://docs.up2data.ai/v1/guides/credits/) for the full rate table and [Rate limits](https://docs.up2data.ai/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` adds 1 credit, raising profile enrich to 2 credits.
- [ ] 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 (5,000/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://docs.up2data.ai/v1/llms-full.txt. Read it before answering field-level questions.
