# Reverse Contact API - Complete Documentation > B2B data enrichment API. Enrich social profiles and company pages with structured professional data. REST API with JSON responses, Bearer token authentication, and credit-based billing. - Base URL: `https://api.reversecontact.com` (every path starts with `/v2`) - Authentication: `Authorization: Bearer rc_your_api_key` - Async jobs: an async endpoint answers with `data.webhookId`. Poll `GET /v2/webhooks/{webhookId}?wait=20` until `status` is `succeeded` or `errored`, with an HTTP timeout above 20 seconds: each poll answers as soon as the job ends, or after at most 20 seconds with the job still running. While it runs, start two polls at least `Retry-After` seconds apart (2 if the header is missing): a held poll already lasts longer, so the next one starts right away; after an early answer, pause for the rest. Without `wait`, that is a pause of `Retry-After` seconds between polls. A `5xx` is retried like any other call (its `Retry-After` if present, otherwise exponential backoff). Polls are free and not rate limited. Guide: [Polling](https://docs.reversecontact.com/guides/polling.md). A List import answers with an operation instead, read at its `pollUrl`: [Lists](https://docs.reversecontact.com/guides/lists.md). Bulk enrichment (`POST /v2/enrich/persons/bulk`) answers with `data.job`, read at its `links.self` with the same `wait`, then its results CSV at `links.results`: [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons.md) - MCP server for the API: `https://api.reversecontact.com/mcp` (Streamable HTTP, OAuth sign-in or Bearer token auth) --- # Getting started > Set up your first Reverse Contact request in less than 5 minutes: get an API key, call the API with a Social URL, read the response, then try live data. - Page: https://docs.reversecontact.com/getting-started - Last verified: 2026-11-04 ## Before you begin 1. [Create an account](https://app.reversecontact.com/auth/signup) if you don't have one yet. 2. Generate an API key: open Workspace > [API Keys](https://app.reversecontact.com/api-keys), create a new key, then copy it and keep it secure. 3. Have a Social URL ready, such as `https://www.social.com/in/janedoe`, or a person's full name and company domain. [Authentication](https://docs.reversecontact.com/authentication) covers API keys and permissions. ## 1. Make your first request All endpoints are served from `https://api.reversecontact.com/v2/`. This request fetches a person profile from a Social URL, with your key in the `RC_API_KEY` environment variable. The `TypeScript SDK` and `Python SDK` tabs use the official SDKs: install them with `npm install @reverse-contact/sdk` or `pip install reversecontact`, see [TypeScript and JavaScript SDK](https://docs.reversecontact.com/sdk/javascript) and [Python SDK](https://docs.reversecontact.com/sdk/python). **POST /v2/fetch/persons** ```javascript [JavaScript] const response = await fetch("https://api.reversecontact.com/v2/fetch/persons", { method: "POST", headers: { "Authorization": `Bearer ${process.env.RC_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://www.social.com/in/janedoe" }) }); const result = await response.json(); console.log(result.data.firstName, result.data.lastName); ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.fetch.person({ url: "https://www.social.com/in/janedoe" }); if (result === null) console.log("Profile not found: nothing was charged"); else console.log(result.data.firstName, result.data.lastName); ``` ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/fetch/persons \ -H "Authorization: Bearer ${RC_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.social.com/in/janedoe"}' ``` ```python [Python] import os import requests response = requests.post( "https://api.reversecontact.com/v2/fetch/persons", headers={ "Authorization": f"Bearer {os.environ['RC_API_KEY']}", "Content-Type": "application/json" }, json={"url": "https://www.social.com/in/janedoe"} ) result = response.json() print(result["data"]["firstName"], result["data"]["lastName"]) ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.fetch.person(url="https://www.social.com/in/janedoe") if result is None: print("Profile not found: nothing was charged") elif result.data is not None: print(result.data.first_name, result.data.last_name) ``` > [!TIP] > To test without writing code, use the Try it button on any endpoint page: it runs the request from your browser with your API key. ## 2. Read the response Every API response has the same top-level fields: | Field | Description | | ----- | ----------- | | `success` | `true` if the request succeeded | | `data` | The returned object for this endpoint | | `error` | Error details if something went wrong, `null` on success | | `metadata` | Request ID and execution time, useful for debugging | | `quotas` | Your remaining credits and rate limit status | See [API reference](https://docs.reversecontact.com/api-reference) for the response format and the shared types. ## 3. Get real-time data from the live endpoints The `live`, Find Email and Find Phone endpoints fetch fresh data asynchronously. They reply immediately with a `webhookId`, then deliver the result once it's ready, usually within seconds. > [!IMPORTANT] > We aim to deliver each result in under 15 seconds, but it can take tens of minutes depending on conditions. Use live endpoints to update databases and feed async pipelines, not to answer a real-time request a user is waiting on. For the live profile endpoints, Find Email and Find Phone, the simplest way to retrieve the result is polling: trigger the job, then call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. The [Find Email](https://docs.reversecontact.com/endpoints/contact-email) and [Find Phone](https://docs.reversecontact.com/endpoints/contact-phone) pages describe their results. **Trigger + poll** ```javascript [JavaScript] const KEY = "YOUR_API_KEY"; const BASE = "https://api.reversecontact.com"; const trigger = await fetch(`${BASE}/v2/fetch/persons/live`, { method: "POST", headers: { "Authorization": `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://social.com/in/janedoe" }) }); const { data: { webhookId } } = await trigger.json(); while (true) { const res = await fetch(`${BASE}/v2/webhooks/${webhookId}`, { headers: { "Authorization": `Bearer ${KEY}` } }); const { data } = await res.json(); if (data.status === "succeeded") { console.log(data.result); break; } if (data.status === "errored") throw new Error(data.errorCode ?? "Job failed"); await new Promise(r => setTimeout(r, 2000)); } ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.fetch.personLive({ url: "https://social.com/in/janedoe" }); console.log("Save this job id:", job.id); // Polls until the job ends, and throws JobFailedError if it fails. const done = await job.wait(); console.log(done.data.result); ``` ```bash [cURL] KEY="YOUR_API_KEY" ID=$(curl -sX POST https://api.reversecontact.com/v2/fetch/persons/live \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"url":"https://social.com/in/janedoe"}' | jq -r .data.webhookId) while :; do S=$(curl -s https://api.reversecontact.com/v2/webhooks/$ID \ -H "Authorization: Bearer $KEY" | jq -r .data.status) [ "$S" = "succeeded" ] && break [ "$S" = "errored" ] && echo "job failed" && exit 1 sleep 2 done curl -s https://api.reversecontact.com/v2/webhooks/$ID \ -H "Authorization: Bearer $KEY" | jq .data.result ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.fetch.person_live(url="https://social.com/in/janedoe") print("Save this job id:", job.id) # Polls until the job ends, and raises JobFailedError if it fails. done = job.wait() print(done.data.result if done.data else None) ``` > [!TIP] > Polling needs no webhook receiver. Polls are free and do not count against your per-minute rate limit. To get each result as soon as it's ready with fewer requests, add `?wait=20` to the poll: each call then waits up to 20 seconds for the job to end. The SDKs' `job.wait()` already polls this way. The [Polling guide](https://docs.reversecontact.com/guides/polling) walks through the integration and compares polling with webhooks. ## 4. Many people or companies? Let a List orchestrate the work The endpoints above look up one person at a time. For a file, a CRM segment or a set of prospects, a [List](https://docs.reversecontact.com/guides/lists) orchestrates the whole pipeline instead: send it up to 25,000 Social profile URLs, emails or names in one import, or the URL of a public CSV file, and it identifies each person, skips those already on the List, buys the data you ask for (profile, work email, phone number) under a credit ceiling you set, and keeps the result for you to read or export. Your team sees the same List in the app. The [Lists guide](https://docs.reversecontact.com/guides/lists) explains what a List orchestrates, and [Build your first List](https://docs.reversecontact.com/guides/build-a-list) walks through a first List step by step. ## FAQ ### What is the base URL of the Reverse Contact API? All Reverse Contact endpoints are served from `https://api.reversecontact.com/v2/`, for example `POST https://api.reversecontact.com/v2/fetch/persons`. The examples send the API key in an `Authorization: Bearer` header. ### How do I get a Reverse Contact API key? Create a Reverse Contact account, then open Workspace > API Keys, create a new key, copy it and keep it secure. The [Authentication](https://docs.reversecontact.com/authentication) page covers API keys and permissions. ### How long does a live endpoint take to return data? The `live` endpoints, Find Email and Find Phone reply immediately with a `webhookId`, then deliver the result once it's ready, usually within seconds. Reverse Contact aims to deliver each result in under 15 seconds, but it can take tens of minutes depending on conditions, so live endpoints are built to feed async pipelines, not to block a request a user is waiting on. ### How do I get a live profile result without setting up a webhook? Use polling: trigger the live job, then call `GET /v2/webhooks/:webhookId` until the status is `succeeded`, or stop if it is `errored`. Polling needs no webhook receiver, and polls are free and do not count against your per-minute rate limit. See the [Polling guide](https://docs.reversecontact.com/guides/polling). ### What replaced `/v2/search/persons`? On October 13, 2026, `/v2/search/persons` was replaced by `/v2/search/people`. The old endpoint now returns `410 Gone`. See the [endpoint transition guide](https://docs.reversecontact.com/guides/endpoint-transition-october-2026). --- # Authentication > Authenticate requests to the Reverse Contact API with a Bearer API key in the Authorization header, and fix 401 errors from a missing or expired key. - Page: https://docs.reversecontact.com/authentication - Last verified: 2026-09-01 Every API request requires your key in the `Authorization` header. Setting it up takes two steps. ## 1. Copy your API key Open Workspace > [API Keys](https://app.reversecontact.com/api-keys) and copy your key. It starts with `rc_*****`. > [!TIP] > If you don't have an account yet, start with [Getting started](https://docs.reversecontact.com/getting-started). ## 2. Add it to your request Test with the [Usage endpoint](https://docs.reversecontact.com/endpoints/usage). It returns your credit balance and costs zero credits: ```bash [cURL] curl https://api.reversecontact.com/v2/usage \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript [JavaScript] const response = await fetch("https://api.reversecontact.com/v2/usage", { headers: { "Authorization": "Bearer YOUR_API_KEY" } }); const { quotas } = await response.json(); console.log("Credits left:", quotas.workspace.credits.left); ``` ```python [Python] import requests response = requests.get( "https://api.reversecontact.com/v2/usage", headers={ "Authorization": "Bearer YOUR_API_KEY" } ) data = response.json() print("Credits left:", data["quotas"]["workspace"]["credits"]["left"]) ``` ## Verify it works A `200` response with your quotas means authentication is working: ```json [200 OK] { "success": true, "quotas": { "creditsConsumed": 0, "workspace": { "credits": { "total": 4000, "used": 31, "left": 3969 }, "minuteRateLimit": { "limit": 500, "left": 500 } } } } ``` See the [full response structure](https://docs.reversecontact.com/endpoints/usage) for all fields. | Check | Expected | | ----- | -------- | | HTTP status | `200` | | `success` | `true` | | `quotas.workspace.credits.left` | Your remaining credits | > [!NOTE] > If all three checks pass, your API key is working. Next, enrich your first profile with [Your first API call](https://docs.reversecontact.com/guides/first-api-call). ## Fix authentication errors A `401` response means the API key was missing, malformed, not recognized, or expired (`API_KEY_EXPIRED`): **401 Unauthorized** ```json [Key Missing] { "success": false, "data": null, "error": { "code": "API_KEY_MISSING", "message": "API Key is required" }, "metadata": { "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "executionTimeMs": 1 } } ``` ```json [Key Invalid] { "success": false, "data": null, "error": { "code": "API_KEY_INVALID", "message": "API Key is invalid" }, "metadata": { "requestId": "f6e5d4c3-b2a1-0987-fedc-ba9876543210", "executionTimeMs": 1 } } ``` ```json [Malformed Header] { "success": false, "data": null, "error": { "code": "API_KEY_MALFORMED", "message": "Malformed Authorization header. Expected 'Authorization: Bearer ' or 'x-api-key: '." } } ``` | Issue | Fix | | ----- | --- | | Missing `Bearer` prefix | Use `Authorization: Bearer rc_...`, not just `rc_...` | | Malformed credential header | Use `Authorization: Bearer ` or `x-api-key: ` | | Typo or truncated key | Copy the full key again from [API Keys](https://app.reversecontact.com/api-keys) | | Revoked or deleted key | Create a new key in the dashboard | | Expired key (`API_KEY_EXPIRED`) | Create a new key in the dashboard | | Wrong workspace | Check you're using a key from the right workspace | > [!TIP] > Include the `metadata.requestId` from error responses when contacting support. It speeds up troubleshooting. A valid key can still get a `403`, for one of two reasons: the workspace is suspended (`WORKSPACE_DISABLED`), or the key lacks the permission the endpoint needs (`INSUFFICIENT_PERMISSIONS`, see [API key management](https://docs.reversecontact.com/guides/api-key-management)). For all error codes and retry strategies, see [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries). ## Keep your key safe Store your key in an environment variable. Never hardcode it: ```bash [macOS / Linux] export RC_API_KEY="rc_0123456789abcdef0123456789abcdef" ``` ```powershell [Windows] $env:RC_API_KEY = "rc_0123456789abcdef0123456789abcdef" ``` ```bash [.env] RC_API_KEY=rc_0123456789abcdef0123456789abcdef ``` - Use one key per environment (dev, staging, production). - Rotate your keys: revoke compromised keys and generate new ones from [API Keys](https://app.reversecontact.com/api-keys). - Never expose keys in client-side code, public repos, or browser JavaScript. - Monitor credits and rate limits with `GET /v2/usage`. ## FAQ ### How do I authenticate requests to the Reverse Contact API? Copy your API key, which starts with `rc_`, from Workspace > API Keys and send it with every request as `Authorization: Bearer `. The Reverse Contact API also accepts the key in an `x-api-key: ` header. ### How can I test my API key without spending credits? Call the Reverse Contact Usage endpoint, `GET /v2/usage`, which returns your credit balance and costs zero credits. A `200` response with `success: true` and your `quotas` means authentication is working. The full response is documented on the [Usage endpoint](https://docs.reversecontact.com/endpoints/usage) page. ### Why does the Reverse Contact API return 401 Unauthorized? A `401` means the API key was missing (`API_KEY_MISSING`), not recognized (`API_KEY_INVALID`), sent in a malformed header (`API_KEY_MALFORMED`) or past its expiration date (`API_KEY_EXPIRED`). Check that the header reads `Authorization: Bearer rc_...`, that the key was copied in full and has not been revoked or deleted, and that the key belongs to the right workspace. ### Why does a valid API key return 403? A `403` means the request is authenticated but not allowed, for one of two reasons: the workspace is suspended (`WORKSPACE_DISABLED`), or the API key lacks the permission the endpoint needs (`INSUFFICIENT_PERMISSIONS`). See [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries). ### Can I put my Reverse Contact API key in front-end code? No. Never expose a Reverse Contact API key in client-side code, public repos or browser JavaScript: store the key in an environment variable such as `RC_API_KEY` and never hardcode it. Use one key per environment (dev, staging, production), and revoke compromised keys and generate new ones from Workspace > API Keys. --- # Your first API call > Send your first enrichment request to the Reverse Contact API, parse the JSON response, and fix the errors a first call most often returns. - Page: https://docs.reversecontact.com/guides/first-api-call - Last verified: 2026-10-13 With your API key ready, make your first profile enrichment call and inspect the result. > [!TIP] > Don't have an API key yet? Start with [Authentication](https://docs.reversecontact.com/authentication). ## 1. Send the request The Person Profile endpoint retrieves a person's professional profile from a Social URL. It costs 1 credit (cached data). **POST /v2/fetch/persons** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/fetch/persons \ -H "Authorization: Bearer ${RC_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.social.com/in/janedoe"}' ``` ```javascript [JavaScript] const response = await fetch("https://api.reversecontact.com/v2/fetch/persons", { method: "POST", headers: { "Authorization": `Bearer ${process.env.RC_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://www.social.com/in/janedoe" }) }); const result = await response.json(); console.log(result.data.firstName, result.data.lastName); ``` ```python [Python] import os import requests response = requests.post( "https://api.reversecontact.com/v2/fetch/persons", headers={ "Authorization": f"Bearer {os.environ['RC_API_KEY']}", "Content-Type": "application/json" }, json={"url": "https://www.social.com/in/janedoe"} ) result = response.json() print(result["data"]["firstName"], result["data"]["lastName"]) ``` > [!TIP] > To test without writing code, use the **Try it** button on any endpoint page: it runs the request from your browser with your API key. ## 2. Read the response A successful response returns the profile inside `data`. This example keeps the fields most integrations read first: the full profile has 27 fields, including `summary`, `education`, `languages` and `certifications`, all shown in the [Person profile example](https://docs.reversecontact.com/endpoints/fetch-profile#example-response). **200 OK** ```json [Response] { "success": true, "data": { "id": "prs_01jbd5vewyenebxgaz869ffe5t", "publicId": "janedoe", "firstName": "Jane", "lastName": "Doe", "headline": "VP of Marketing at Acme Corp", "photoUrl": "https://media.licdn.com/dms/image/...", "isOpenToWork": false, "location": { "city": "San Francisco", "state": "California", "country": "United States", "countryCode": "US" }, "currentPosition": { "title": "VP of Marketing", "companyName": "Acme Corp" }, "experience": [ { "title": "VP of Marketing", "companyName": "Acme Corp", "startEndDate": { "start": "2021-06-01T00:00:00.000Z", "end": null } } ], "skills": ["Marketing Strategy", "B2B SaaS", "Demand Generation"] }, "error": null, "quotas": { "creditsConsumed": 1, "workspace": { "credits": { "total": 10000, "used": 1, "left": 9999 } } }, "metadata": { "requestId": "a1b2c3d4-e5f6-4a90-abcd-ef1234567890", "executionTimeMs": 230, "updatedAt": "2025-01-15T09:30:00.000Z" } } ``` Start with these key fields: | Field | What it gives you | | ----- | ----------------- | | `data.firstName` / `data.lastName` | Full name | | `data.headline` | Current role summary | | `data.currentPosition` | Job title + company ([how it is chosen](https://docs.reversecontact.com/guides/current-position-resolution)) | | `data.experience` | Full work history with dates | | `data.skills` | List of professional skills | | `data.location` | City, state, country | See the [Person Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-profile) for all available fields, and [API reference](https://docs.reversecontact.com/api-reference) for the full response structure. ## 3. Handle errors If the URL is invalid, you get a `422` with zero credits charged: **422 Validation Error** ```json [Invalid URL] { "success": false, "data": null, "error": { "code": "INVALID_LINKEDIN_URL", "message": "Invalid LinkedIn profile URL. Expected format: https://linkedin.com/in/username" } } ``` | Issue | Fix | | ----- | --- | | Invalid Social URL | Send the full profile URL, with `https://` and the `/in/username` path | | `401` Unauthorized | Check your API key. See [Authentication](https://docs.reversecontact.com/authentication) | | `402` Insufficient credits | Purchase more credits from the dashboard | | `404` Not found | The profile doesn't exist or isn't indexed yet. Try the [Live endpoint](https://docs.reversecontact.com/endpoints/fetch-profile-live) | For all error codes and retry strategies, see [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries). ## FAQ ### How many credits does a Person Profile request cost? A `POST /v2/fetch/persons` request to the Person Profile endpoint costs 1 credit (cached data). The response reports the credits used in `quotas.creditsConsumed`. ### Am I charged for an invalid Social URL? No. If the URL is invalid, the Person Profile endpoint returns a `422` with the error code `INVALID_LINKEDIN_URL` and zero credits charged. Send the full profile URL, with `https://` and the `/in/username` path. ### Why does the Person Profile endpoint return a `404`? A `404` from the Person Profile endpoint means the profile doesn't exist or isn't indexed yet. In that case, try the [Live endpoint](https://docs.reversecontact.com/endpoints/fetch-profile-live). ### Where do I find a person's current job title and company in the profile response? In a Person Profile response, `data.currentPosition` holds the job title and company, `data.headline` the current role summary, and `data.experience` the full work history with dates. See [how the current position is chosen](https://docs.reversecontact.com/guides/current-position-resolution). ### Can I test the Reverse Contact API without writing code? Yes. Every endpoint page has a "Try it" button that runs the request from your browser with your API key. --- # Which endpoint should I use? > Choose a Reverse Contact API endpoint for profile lookup, enrichment, search, contacts or bulk processing, with costs and sync or async delivery. - Page: https://docs.reversecontact.com/guides/which-endpoint-should-i-use - Last verified: 2026-11-04 The right Reverse Contact endpoint depends on the data you start from: a Social URL, an email or a name, search filters, or a sentence that describes the people or companies you want. Find your starting point below. > [!TIP] > If you don't have an account or an API key yet, start with [Getting started](https://docs.reversecontact.com/getting-started). ## Pick your starting point - You have a Social URL: [get the full profile](#profiles) - You have an email, a name or a domain: [find the matching profile](#enrichment) - You have filters such as a job title or a location: [search our database](#search) - You can describe the people you want in a sentence: [Agentic Search](https://docs.reversecontact.com/endpoints/search-people-prompt) - You are building a people search box in your own product: [People auto-suggestion](https://docs.reversecontact.com/endpoints/search-people-auto-suggestion) - You can describe the companies you want in a sentence: [Agentic Company Search](https://docs.reversecontact.com/endpoints/search-companies-prompt) - You need a professional or personal email address: [find an email](#contact) - You have a CSV file of contacts: [Enrich a CSV file](https://docs.reversecontact.com/guides/enrich-a-csv-file), or send its rows in one request with [Bulk enrichment](#bulk-enrichment) ## Profiles Turn a Social URL into a person or company profile. | Endpoint | Credits | Delivery | When to use | | -------- | :-----: | :------: | ----------- | | [`POST /v2/fetch/persons/check`](https://docs.reversecontact.com/endpoints/check-person) | 0 | Sync | Check if a person profile exists before spending credits | | [`POST /v2/fetch/companies/check`](https://docs.reversecontact.com/endpoints/check-company) | 0 | Sync | Check if a company profile exists before spending credits | | [`POST /v2/fetch/persons`](https://docs.reversecontact.com/endpoints/fetch-profile) | 1 | Sync | Retrieve a cached person profile. Fast and cheap | | [`POST /v2/fetch/companies`](https://docs.reversecontact.com/endpoints/fetch-company) | 1 | Sync | Retrieve a cached company profile. Fast and cheap | | [`POST /v2/fetch/persons/live`](https://docs.reversecontact.com/endpoints/fetch-profile-live) | 2 | Async | Force a fresh scrape when you need the latest data | | [`POST /v2/fetch/companies/live`](https://docs.reversecontact.com/endpoints/fetch-company-live) | 2 | Async | Force a fresh scrape when you need the latest data | > [!TIP] > The recommended flow: call Check first (free), then Fetch if the record is recent, or Live if it's missing or stale. See [Data freshness](https://docs.reversecontact.com/guides/data-freshness) to decide when a record is recent enough. ## Enrichment Find a profile when you don't have a Social URL. Send the details you have and get the full profile back in the same response. | Endpoint | Credits | Delivery | Input | | -------- | :-----: | :------: | ----- | | [`POST /v2/enrich/persons`](https://docs.reversecontact.com/endpoints/enrich-person) | 2 | Sync | Any combination of email, name, company name or domain | | [`POST /v2/enrich/companies`](https://docs.reversecontact.com/endpoints/enrich-company) | 2 | Sync | A company domain (e.g. `acme.com`) | > [!NOTE] > Credits are charged only when a match is found. A not-found response (`404`) is free. ## Search Query our database of indexed profiles with filters. No Social URL needed. | Endpoint | Credits | Delivery | When to use | | -------- | :-----: | :------: | ----------- | | [`POST /v2/search/people`](https://docs.reversecontact.com/endpoints/search-people) | 1 per profile | Sync | Search people by function, seniority, title, location, company... | | [`POST /v2/search/people/prompt`](https://docs.reversecontact.com/endpoints/search-people-prompt) | 1 per page + 1 per profile | Sync | Describe the people you want in plain language instead of building filters | | [`POST /v2/search/people/count`](https://docs.reversecontact.com/endpoints/search-people-count) | 0 | Sync | Count matching people before spending credits | | [`POST /v2/search/people/auto-suggestion`](https://docs.reversecontact.com/endpoints/search-people-auto-suggestion) | 0 | Sync | Complete a skill as someone types, in a search box of your own product | | [`POST /v2/search/organizations`](https://docs.reversecontact.com/endpoints/search-companies) | 1 per company | Sync | Search companies by industry, specialities, size, founding year, location... | | [`POST /v2/search/organizations/prompt`](https://docs.reversecontact.com/endpoints/search-companies-prompt) | 1 per page + 1 per company | Sync | Describe the companies you want in plain language instead of building filters | | [`POST /v2/search/organizations/count`](https://docs.reversecontact.com/endpoints/search-companies-count) | 0 | Sync | Count matching companies before spending credits | > [!NOTE] > People search and Company search return full profiles and cost 1 credit per profile or company delivered, so use their free count endpoints to size a query first. `POST /v2/search/companies` goes into maintenance on November 9, 2026, for a few weeks: use `POST /v2/search/organizations` instead, see [Migrating company search to the new endpoint](https://docs.reversecontact.com/guides/migrating-search-companies-to-organizations). ## Contact Find a professional or personal email address from a Social profile URL, or from a person's full name and company domain. Check an address you already have before you send to it. | Endpoint | Credits | Delivery | When to use | | -------- | :-----: | :------: | ----------- | | [`POST /v2/contact/email`](https://docs.reversecontact.com/endpoints/contact-email) | 3 professional, 5 personal, when found | Async | Find a professional (default) or personal email address with `emailType` | | [`POST /v2/contact/email/verify`](https://docs.reversecontact.com/endpoints/contact-email-verify) | 1 per verdict | Async | Check whether an address you already have can receive mail: `valid`, `risky`, `invalid` or `unknown` | > [!NOTE] > Find Email is billed only when an email is found, on the kind of email found: 3 credits for a professional email, 5 for a personal one, so a lookup with `emailType` set to `any` costs 3, 5 or 8 (8 only when it finds both). See [Credits by email type](https://docs.reversecontact.com/endpoints/contact-email#credits-by-email-type). ## Lists Keep people or companies in a List you fill over time, and buy their emails, phone numbers and profiles in one import. See [Lists](https://docs.reversecontact.com/guides/lists), then [Build your first List](https://docs.reversecontact.com/guides/build-a-list). | Endpoint | Credits | Delivery | When to use | | -------- | :-----: | :------: | ----------- | | [`POST /v2/lists/:listId/members`](https://docs.reversecontact.com/endpoints/list-members-add) | 1 or 2 per new member | Sync | Add up to 1,000 people or companies and get one result per Signal | | [`POST /v2/lists/:listId/intake/signals`](https://docs.reversecontact.com/endpoints/list-intake-signals) | Per new member and per family bought, up to `maxCredits` | Async | Import up to 25,000 Signals, or a public CSV file or Google Sheet, and buy emails, phones, profiles or company details | | [`GET /v2/lists/:listId/export`](https://docs.reversecontact.com/endpoints/list-export) | 0 | Sync | Download the members with the data the List holds, and the Pending Intake lines with what was sent | ## Bulk enrichment Send up to 25,000 people in one request and get one CSV row per person back, without keeping them in a List. See [Enrich people in bulk](https://docs.reversecontact.com/guides/bulk-enrichment). | Endpoint | Credits | Delivery | When to use | | -------- | :-----: | :------: | ----------- | | [`POST /v2/enrich/persons/bulk`](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) | Per item, as its unit call, only when found | Async | Enrich a file of people once: profiles, emails or phone numbers | | [`GET /v2/jobs/:jobId/results`](https://docs.reversecontact.com/endpoints/job-results) | 0 | Sync | Download the results CSV once the job is completed or cancelled | ## Sync vs async Most endpoints return data instantly in the API response. Live endpoints, Find Email, a List import and Bulk enrichment deliver their results asynchronously. | Delivery | How it works | | -------- | ------------ | | Async | Results pulled on demand via [polling](https://docs.reversecontact.com/guides/polling), or pushed to your [webhook](https://docs.reversecontact.com/guides/webhooks) URL. A List import is followed at its `pollUrl` instead, and a Bulk enrichment job at [`GET /v2/jobs/:jobId`](https://docs.reversecontact.com/endpoints/job), each with its own `webhookUrl` | | Sync | Instant JSON response | > [!TIP] > For async results, start with [Polling](https://docs.reversecontact.com/guides/polling): it needs no setup and no public endpoint, and it is free to call. If you already run a public receiver and need push delivery, see [Webhooks](https://docs.reversecontact.com/guides/webhooks). ## FAQ ### How do I find a person's profile without a Social URL? Use `POST /v2/enrich/persons` with any combination of email, name, company name or domain: it returns the full profile in the same response. `POST /v2/enrich/persons` costs 2 credits, charged only when a match is found, and a `404` not-found response is free. See [Enrich person](https://docs.reversecontact.com/endpoints/enrich-person). ### Should I use the cached Fetch endpoint or the Live endpoint for profiles? `POST /v2/fetch/persons` and `POST /v2/fetch/companies` return a cached profile synchronously for 1 credit, while `POST /v2/fetch/persons/live` and `POST /v2/fetch/companies/live` force a fresh scrape for 2 credits and deliver the result asynchronously. The recommended flow is to call the free Check endpoint first (`POST /v2/fetch/persons/check` or `POST /v2/fetch/companies/check`), then Fetch if the record is recent, or Live if it's missing or stale. See [Data freshness](https://docs.reversecontact.com/guides/data-freshness). ### How much does people or company search cost? `POST /v2/search/people` returns full profiles and costs 1 credit per profile delivered, so size a query first with `POST /v2/search/people/count`, which costs 0 credits. `POST /v2/search/organizations` works the same way for companies: 1 credit per company delivered, sized for free with `POST /v2/search/organizations/count`. ### How do I find a professional email address, and what does it cost? Use `POST /v2/contact/email` (Find Email) with a Social profile URL, or with a person's full name and company domain. Find Email delivers its result asynchronously, by polling or webhook, and finds professional emails by default; set `emailType` to `personal` for a personal address, or to `any` to accept either. Find Email is billed only when an email is found, on the kind of email found: 3 credits for a professional email and 5 credits for a personal email, so an `any` lookup costs 3, 5 or 8 credits (8 only when it finds both). ### Which Reverse Contact endpoints are asynchronous? Live endpoints and Find Email deliver their results asynchronously, while most other endpoints return data instantly in the API response. Live endpoint results can be pulled on demand with polling (no setup, no public endpoint, free to call) or pushed to a webhook URL. See [Polling](https://docs.reversecontact.com/guides/polling). A List import and a Bulk enrichment job are asynchronous too: a List import is read at its `pollUrl`, and a Bulk enrichment job at `GET /v2/jobs/:jobId`, both for free. --- # Polling for async results > The recommended way to retrieve async enrichment results: trigger a job, then poll for the result. Polling is free and needs no public HTTPS endpoint. - Page: https://docs.reversecontact.com/guides/polling - Last verified: 2026-10-26 ## The recommended way to retrieve async results All `live` endpoints run in the background. They reply immediately with a `webhookId`, then deliver the result once it's ready, usually within seconds. [Find Email](https://docs.reversecontact.com/endpoints/contact-email), [Verify Email](https://docs.reversecontact.com/endpoints/contact-email-verify) and [Find Phone](https://docs.reversecontact.com/endpoints/contact-phone) work the same way. > [!IMPORTANT] > Live endpoints have no synchronous SLA. We aim to deliver each result in under 15 seconds, but processing can stretch to tens of minutes depending on conditions (source availability, queue load, retries). Even with fast polling, live endpoints are not built to sit inline in a synchronous, real-time request a user is waiting on. Use them to update databases, enrich records, and feed async pipelines. Polling and webhooks both keep the work off your critical path. Polling is the recommended way to retrieve that result: trigger a job, then call `GET /v2/webhooks/:webhookId` until the result is ready. It needs no infrastructure on your side: no public URL to expose, no receiver to deploy, no tunnel to maintain. It fits backend services, batch pipelines, mobile apps, CLI tools, notebooks, and local development. > [!TIP] > Polling is the recommended method for most teams: > > - It needs no public HTTPS endpoint, so there is nothing to deploy and no tunnel to maintain. > - With `?wait=20`, each status check waits for the job and answers the moment it ends, so you get each result as soon as it's ready. > - Status checks cost 0 credits, whatever the status returned. > - Polling does not count toward your per-minute RPM. > - It works in a backend, a mobile app, a CLI or a notebook, and behind a firewall. ## Trigger and poll Trigger any async endpoint, save the `webhookId` it returns, then call `GET /v2/webhooks/:webhookId?wait=20` until the status is `succeeded`. Each call answers as soon as the result is ready, or after at most 20 seconds with the job still running. The loop starts two polls at least `Retry-After` seconds apart, so a call that comes back early is followed by a short pause (see [Polling frequency](#polling-frequency)). **Trigger and poll** ```javascript [JavaScript] const KEY = "YOUR_API_KEY"; const BASE = "https://api.reversecontact.com"; const trigger = await fetch(`${BASE}/v2/fetch/persons/live`, { method: "POST", headers: { "Authorization": `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://social.com/in/janedoe" }) }); const { data: { webhookId } } = await trigger.json(); while (true) { const started = Date.now(); // Answers as soon as the job ends, or after at most 20 seconds with the job still running. const res = await fetch(`${BASE}/v2/webhooks/${webhookId}?wait=20`, { headers: { "Authorization": `Bearer ${KEY}` }, signal: AbortSignal.timeout(30000) // longer than the wait }); const { data } = await res.json(); if (data.status === "succeeded") { console.log(data.result); break; } if (data.status === "errored") throw new Error(data.errorCode ?? "Job failed"); // Still running: start the next poll at least Retry-After seconds after this one. const retryAfter = Number(res.headers.get("Retry-After")) || 2; await new Promise(r => setTimeout(r, retryAfter * 1000 - (Date.now() - started))); } ``` ```bash [cURL] KEY="YOUR_API_KEY" ID=$(curl -sX POST https://api.reversecontact.com/v2/fetch/persons/live \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"url":"https://social.com/in/janedoe"}' | jq -r .data.webhookId) # Each call answers as soon as the job ends, or after at most 20 seconds. while :; do START=$SECONDS S=$(curl -s --max-time 30 "https://api.reversecontact.com/v2/webhooks/$ID?wait=20" \ -H "Authorization: Bearer $KEY" | jq -r .data.status) case "$S" in succeeded) break ;; created|in_progress) # still running: an early answer pauses first [ $((SECONDS - START)) -lt 2 ] && sleep 2 ;; errored) echo "job failed"; exit 1 ;; *) echo "poll failed"; exit 1 ;; esac done curl -s https://api.reversecontact.com/v2/webhooks/$ID \ -H "Authorization: Bearer $KEY" | jq .data.result ``` ```python [Python] import time, requests KEY = "YOUR_API_KEY" BASE = "https://api.reversecontact.com" H = {"Authorization": f"Bearer {KEY}"} trigger = requests.post( f"{BASE}/v2/fetch/persons/live", headers={**H, "Content-Type": "application/json"}, json={"url": "https://social.com/in/janedoe"} ) webhook_id = trigger.json()["data"]["webhookId"] while True: started = time.monotonic() # Answers as soon as the job ends, or after at most 20 seconds with the job still running. res = requests.get(f"{BASE}/v2/webhooks/{webhook_id}", params={"wait": 20}, headers=H, timeout=30) data = res.json()["data"] if data["status"] == "succeeded": print(data["result"]) break if data["status"] == "errored": raise RuntimeError(data.get("errorCode") or "Job failed") # Still running: start the next poll at least Retry-After seconds after this one. retry_after = int(res.headers.get("Retry-After", 2)) time.sleep(max(0, retry_after - (time.monotonic() - started))) ``` > [!TIP] > To try polling before you write code, trigger any async endpoint, copy the `webhookId` and paste it in [Logs > Job Lookup](https://app.reversecontact.com/logs/jobs): the status updates in real time. Job Lookup calls the same API as your code. ## Which delivery method should you use? ``` Do you already run a public HTTPS receiver? | +-- NO -> Polling (recommended) | +-- YES -> Polling (recommended), or Webhooks if you prefer push delivery ``` Polling works at any volume and takes the least setup. Webhooks are an alternative for teams that already operate a public HTTPS receiver and prefer a push model. ## Polling vs webhooks Polling and webhooks use the same endpoints, handle the same volumes, and return the same data at the same enrichment cost. They differ in the delivery model: with polling you pull the result, with webhooks we push it to your server. | Criterion | Polling (recommended) | Webhooks (advanced) | | --------- | --------------------- | ------------------- | | Setup time | under 1 min | 30 to 60 min | | Public HTTPS endpoint | not required | required | | Local dev, mobile, CLI | works out of the box | needs ngrok or a tunnel | | Cost per status check | 0 credits | 0 credits | | Volume capacity | unlimited | unlimited | ## When to use polling Polling fits most integrations: - Backend services and APIs: server-to-server calls where you want the simplest delivery model, with one less moving part to monitor. - Batch jobs and pipelines: a pipeline step that blocks on the enrichment result as if the call were synchronous, without a callback receiver. - Internal tools behind a firewall or VPN: admin consoles or back-office systems with no public surface. - Mobile, desktop, CLI and notebooks: anything without a backend to receive callbacks. - Compliance-constrained environments: a security policy that forbids exposing inbound webhook receivers, or restricts which services can receive third-party callbacks. - Local development and prototyping: no ngrok or Cloudflare Tunnel to run while you iterate. Webhooks are an alternative when you already operate a public HTTPS receiver and prefer a push model. For everything else, polling is faster to integrate and equally cost-effective at any volume. You can also combine the two, for example webhooks as the primary delivery channel and polling as a fallback for status checks. See [Combining polling with a webhookUrl](#combining-polling-with-a-webhookurl). ## How it works 1. Call any async endpoint (for example `POST /v2/fetch/persons/live`) to create a job. You can omit `webhookUrl` from the request body when you intend to retrieve the result by polling. 2. The API responds immediately with `{ status: "created", webhookId, pollUrl }`. 3. Call `GET /v2/webhooks/:webhookId?wait=20` until `status` is `"succeeded"` or `"errored"`. Each call waits up to 20 seconds for the job to end; start two polls at least `Retry-After` seconds apart. 4. When `status` is `"succeeded"`, the `result` field contains the enrichment payload. ## Step 1: Trigger a job ```javascript [JavaScript] const res = await fetch("https://api.reversecontact.com/v2/fetch/persons/live", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://social.com/in/janedoe" }) }) const { data } = await res.json() console.log(data.webhookId, data.pollUrl) ``` ```bash [cURL] curl -X POST "https://api.reversecontact.com/v2/fetch/persons/live" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://social.com/in/janedoe" }' ``` ```python [Python] import requests res = requests.post( "https://api.reversecontact.com/v2/fetch/persons/live", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={"url": "https://social.com/in/janedoe"} ) data = res.json()["data"] print(data["webhookId"], data["pollUrl"]) ``` The response includes a `pollUrl` hint pointing to the polling endpoint: **Initial Response** ```json [JSON] { "success": true, "data": { "status": "created", "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "pollUrl": "/v2/webhooks/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" }, "error": null } ``` It also carries a `Retry-After` header: the number of seconds to wait before the first poll. A poll with `?wait=20` can start right away, since it waits for the job itself. ## Step 2: Poll for the result Call `GET /v2/webhooks/:webhookId?wait=20` until you receive a terminal status (`succeeded` or `errored`). The call answers as soon as the job ends, or after at most 20 seconds with the job still running: check the status, and start the next poll at least `Retry-After` seconds after the previous one started. ```javascript [JavaScript] async function pollUntilDone(webhookId) { while (true) { const started = Date.now() const res = await fetch(`https://api.reversecontact.com/v2/webhooks/${webhookId}?wait=20`, { headers: { "Authorization": "Bearer YOUR_API_KEY" }, signal: AbortSignal.timeout(30000) // longer than the wait }) const { data } = await res.json() if (data.status === "succeeded") return data.result if (data.status === "errored") throw new Error(data.errorCode ?? "Job failed") // Still running: a held poll lasted longer than Retry-After, an early answer pauses for the rest. const retryAfter = Number(res.headers.get("Retry-After")) || 2 await new Promise(r => setTimeout(r, retryAfter * 1000 - (Date.now() - started))) } } ``` ```bash [cURL] curl --max-time 30 "https://api.reversecontact.com/v2/webhooks/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d?wait=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python [Python] import time import requests def poll_until_done(webhook_id): while True: started = time.monotonic() res = requests.get( f"https://api.reversecontact.com/v2/webhooks/{webhook_id}", params={"wait": 20}, headers={"Authorization": "Bearer YOUR_API_KEY"}, timeout=30 # longer than the wait ) data = res.json()["data"] if data["status"] == "succeeded": return data["result"] if data["status"] == "errored": raise RuntimeError(data.get("errorCode") or "Job failed") # Still running: a held poll lasted longer than Retry-After, an early answer pauses for the rest. retry_after = int(res.headers.get("Retry-After", 2)) time.sleep(max(0, retry_after - (time.monotonic() - started))) ``` ### Response shapes While the job is still running (this response also carries a `Retry-After` header, see [Polling frequency](#polling-frequency)): **in_progress** ```json [JSON] { "success": true, "data": { "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "status": "in_progress", "eventType": "FETCH_PERSON_DATA", "errorCode": null, "createdAt": "2026-04-08T12:43:02.017Z", "result": null, "creditsConsumed": 0 } } ``` When the job completes successfully: **succeeded** ```json [JSON] { "success": true, "data": { "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "status": "succeeded", "eventType": "FETCH_PERSON_DATA", "errorCode": null, "createdAt": "2026-04-08T12:43:02.017Z", "result": { "firstName": "Bill", "lastName": "Gates", "headline": "Chair, Gates Foundation and Founder, Breakthrough Energy", "...": "endpoint-specific result payload" }, "creditsConsumed": 2 } } ``` When the job fails: **errored** ```json [JSON] { "success": true, "data": { "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "status": "errored", "eventType": "FETCH_PERSON_DATA", "errorCode": "person-not-found", "createdAt": "2026-04-08T12:43:02.017Z", "result": null, "creditsConsumed": 0 } } ``` `creditsConsumed`: credits billed for this job so far. 0 while the job is in progress; the final amount once it has completed. > [!NOTE] > If the enrichment was fetched but the push to your `webhookUrl` failed, the job still resolves as `status: "succeeded"` with the `result` populated, plus a `deliveryStatus` field naming the delivery problem (`webhook-url-invalid`, `webhook-url-errored`, `webhook-url-timeout`, or `webhook-url-unreachable`). Poll as usual to retrieve the data. These delivery failures are billed (the data was fetched) and not refunded; see [Credits](https://docs.reversecontact.com/guides/credits). `errorCode` is reserved for genuine enrichment failures, so it stays `null` in this case. ## Polling frequency The polling endpoint is free (zero credits) and does not count against your per-minute rate limit, with or without `wait`. Most jobs complete within a few seconds. One pacing rule covers every loop: start two polls at least `Retry-After` seconds apart, and 2 seconds apart when the header is missing. Every answer with the job still running carries a `Retry-After` header (`Retry-After: 2`), early answers to a long poll included. The response that creates the job carries it too, for the first poll. The header is absent once the job has succeeded or errored. The rule paces answers with the job still running: a `5xx` is retried as [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries) describes. | Approach | How the rule applies | When the result reaches you | | -------- | -------------------- | --------------------------- | | Long polling (recommended) | `GET /v2/webhooks/:webhookId?wait=20`. A held poll already lasts longer than `Retry-After`, so the next one starts right away; after an early answer, pause for the rest of `Retry-After` | As soon as it's ready | | Polling without `wait` | `GET /v2/webhooks/:webhookId` answers at once, so pause `Retry-After` seconds between polls | Within `Retry-After` seconds | Polling more slowly than that delays each result by as much: a loop that polls every 20 seconds can receive a result up to 20 seconds after it was ready. Polling faster than `Retry-After` offers no benefit. ### Long polling with wait Add `wait` to the query string, and the API holds the poll until the result is ready: | Query parameter | Type | Required | Description | | --------------- | ---- | -------- | ----------- | | `wait` | integer | Optional | Seconds the API may hold the request while the job runs, from `0` to `20`. Absent or `0`: the API answers at once, as a normal poll. Any other value returns `422` with `VALIDATION_ERROR`. | - Job already finished: the answer is immediate. - Job still running: the API holds the request until the result is ready or `wait` seconds have passed, whichever comes first, then answers exactly what a normal poll returns at that moment. - Early answers: the API can also answer before `wait` seconds with the job still running, for example when many long polls are open at once or during maintenance. Always check `status`. While it is `created` or `in_progress`, poll again with `wait` once `Retry-After` seconds have passed since the previous poll started. - Cost and limits: a long poll costs 0 credits and does not count toward your per-minute rate limit, like any poll. - Timeout: set your HTTP client timeout above `wait`, for example 30 seconds for `wait=20`. A shorter timeout cuts the request before the API answers. The [Trigger and poll](#trigger-and-poll) loop uses `wait=20` and this pacing rule: each result arrives as soon as it's ready, with far fewer requests than polling every few seconds. ### Without wait: follow Retry-After Without `wait`, each poll answers at once, so the pacing rule becomes a pause of `Retry-After` seconds between polls (2 when the header is missing), starting with the `Retry-After` of the response that created the job. **Poll at the Retry-After pace** ```javascript [JavaScript] // retryAfter: the Retry-After header of the response that created the job async function pollUntilDone(webhookId, retryAfter = 2) { while (true) { await new Promise(r => setTimeout(r, retryAfter * 1000)) const res = await fetch(`https://api.reversecontact.com/v2/webhooks/${webhookId}`, { headers: { "Authorization": "Bearer YOUR_API_KEY" } }) const { data } = await res.json() if (data.status === "succeeded") return data.result if (data.status === "errored") throw new Error(data.errorCode ?? "Job failed") retryAfter = Number(res.headers.get("Retry-After")) || 2 // 2 when the header is missing } } ``` ```bash [cURL] while :; do sleep "${WAIT:-2}" S=$(curl -s -D headers.txt "https://api.reversecontact.com/v2/webhooks/$ID" \ -H "Authorization: Bearer $KEY" | jq -r .data.status) case "$S" in succeeded) break ;; created|in_progress) ;; # still running: poll again errored) echo "job failed"; exit 1 ;; *) echo "poll failed"; exit 1 ;; esac WAIT=$(grep -i '^retry-after:' headers.txt | tr -dc '0-9') done ``` ```python [Python] import time import requests # retry_after: the Retry-After header of the response that created the job def poll_until_done(webhook_id, retry_after=2): while True: time.sleep(retry_after) res = requests.get( f"https://api.reversecontact.com/v2/webhooks/{webhook_id}", headers={"Authorization": "Bearer YOUR_API_KEY"}, timeout=30 ) data = res.json()["data"] if data["status"] == "succeeded": return data["result"] if data["status"] == "errored": raise RuntimeError(data.get("errorCode") or "Job failed") retry_after = int(res.headers.get("Retry-After", 2)) # 2 when the header is missing ``` ## Inspect and retrieve results from the dashboard To check a job without writing code, use Job Lookup in the dashboard: 1. Open [Logs > Job Lookup](https://app.reversecontact.com/logs/jobs) from the sidebar. 2. Paste any `webhookId` returned by an async endpoint. 3. Watch the job status update in real time, inspect the result payload, and download it as JSON. You can also open Job Lookup from the [Webhook Events](https://app.reversecontact.com/logs/events) page: the action button on the right of each row opens the lookup pre-filled for that event. Job Lookup lets you test polling end to end before you write any integration code, and it helps with support, ops and manual debugging. It calls the same API, so the dashboard shows exactly what your code receives from `GET /v2/webhooks/:webhookId`. ## Result retention Polling results are retained for 72 hours by default, counted from the moment the job is created (not from when it completes). After that window, the record and its stored payload are purged and the polling endpoint returns a `WEBHOOK_EVENT_EXPIRED` error. > [!IMPORTANT] > The 72-hour countdown starts at job creation, so for unusually long-running enrichments, fetch the result shortly after completion rather than days later. Workspaces with longer retention requirements can reach out to support to extend this window. ## Error responses | Status | Code | When | | ------ | ---- | ---- | | `404` | `WEBHOOK_EVENT_NOT_FOUND` | The `webhookId` doesn't exist or doesn't belong to your workspace | | `410` | `WEBHOOK_EVENT_EXPIRED` | The result has expired and is no longer retrievable | | `422` | `VALIDATION_ERROR` | `wait` is not an integer from `0` to `20` | ## Combining polling with a webhookUrl If you provide a `webhookUrl` in the request body, the API delivers the result to that URL, and the same result also remains retrievable via polling. This is useful when: - You want webhooks as the primary path but need a fallback in case a delivery is missed. - You want to inspect or re-fetch a result after the original webhook delivery. - You're migrating from webhooks to polling and want to validate parity before switching. ## FAQ ### How long does a live enrichment take? All Reverse Contact `live` endpoints run in the background: they reply immediately with a `webhookId` and usually deliver the result within seconds. The aim is under 15 seconds per result, but processing can stretch to tens of minutes depending on source availability, queue load and retries, so live endpoints are not built for synchronous, real-time requests a user is waiting on. ### Does polling for a result cost credits or count toward the rate limit? No. Status checks on `GET /v2/webhooks/:webhookId`, with or without the `wait` parameter, cost 0 credits whatever the status returned, and polling does not count toward your per-minute rate limit (RPM). The credits billed for the job appear in `creditsConsumed`, which is 0 while the job is in progress and shows the final amount once it has completed. ### How often should I poll for an async result? Call `GET /v2/webhooks/:webhookId?wait=20` in a loop until `status` is `succeeded` or `errored`, with an HTTP client timeout above the `wait` value, for example 30 seconds: each call answers as soon as the job ends, so the result arrives without delay and with few requests. Start two polls at least `Retry-After` seconds apart, or 2 seconds when the header is missing: a held poll already lasts longer, so the next one starts right away, and an answer that comes back early is followed by a pause for the rest of `Retry-After`. Without `wait`, the same rule means a pause of `Retry-After` seconds between polls; polling every 20 seconds instead delays each result by up to 20 seconds. See [Polling frequency](#polling-frequency). ### How long are polling results kept? Results from `GET /v2/webhooks/:webhookId` are retained for 72 hours by default, counted from the moment the job is created, not from when it completes. After that window, the record and its stored payload are purged and the polling endpoint returns `410` with `WEBHOOK_EVENT_EXPIRED`. Workspaces with longer retention requirements can reach out to support to extend this window. ### Do I need a webhook receiver to get async enrichment results? No. With polling, you can omit `webhookUrl` from the request body, save the `webhookId` the async endpoint returns, and call `GET /v2/webhooks/:webhookId` until `status` is `succeeded` or `errored`, with no public URL to expose, no receiver to deploy and no tunnel to maintain. If you do pass a `webhookUrl`, the result is delivered to that URL and remains retrievable via polling. --- # Webhooks > Webhooks POST async enrichment results to your HTTPS URL. Use them when you already run a public HTTPS receiver and prefer push delivery to polling. - Page: https://docs.reversecontact.com/guides/webhooks - Last verified: 2026-10-15 > [!TIP] > Most teams integrate faster with [Polling](https://docs.reversecontact.com/guides/polling): it needs no public HTTPS endpoint, each poll costs zero credits and does not count toward your per-minute rate limit, and you get to a first result in around 5 minutes. The webhook setup below is for teams that already run a public HTTPS receiver and need real-time push delivery. ## Introduction Webhooks deliver async enrichment results to a public HTTPS URL you operate. Use them when you already operate a public HTTPS receiver and prefer a push model. For most use cases, polling is faster to integrate and equally cost-effective: it uses the same endpoints and returns the same data at the same enrichment cost. The [Polling guide](https://docs.reversecontact.com/guides/polling) compares both side by side. ## How it works Async endpoints work in the background so your application doesn't have to wait. The API accepts your request immediately, processes it, and delivers the result to your server as soon as it's ready: 1. You call an async endpoint, for example `POST /v2/fetch/persons/live`. 2. The API answers immediately with a `webhookId` that uniquely identifies the request. Use it to match incoming callbacks to your original request. 3. The API fetches the data in the background. 4. As soon as the data is ready, the API sends a POST request with the JSON payload to your webhook URL. The delivery POST to your webhook URL does not count toward the per-minute rate limit. The request that creates the async job does count. See [Rate limits](https://docs.reversecontact.com/guides/rate-limits). > [!IMPORTANT] > Live endpoints have no synchronous SLA. We aim to POST each result in under 15 seconds, but processing can stretch to tens of minutes depending on conditions (source availability, queue load, retries). Live endpoints are built to update databases and feed async pipelines, not to back a synchronous, real-time request a user is waiting on. ## Async endpoints These endpoints deliver their results by webhook: | Family | Endpoint | Route | | ------ | -------- | ----- | | Profiles | Person Profile Live | [`POST /v2/fetch/persons/live`](https://docs.reversecontact.com/endpoints/fetch-profile-live) | | Profiles | Company Profile Live | [`POST /v2/fetch/companies/live`](https://docs.reversecontact.com/endpoints/fetch-company-live) | | Contact | Find Email | [`POST /v2/contact/email`](https://docs.reversecontact.com/endpoints/contact-email) | | Contact | Verify Email | [`POST /v2/contact/email/verify`](https://docs.reversecontact.com/endpoints/contact-email-verify) | | Contact | Find Phone | [`POST /v2/contact/phone`](https://docs.reversecontact.com/endpoints/contact-phone) | ## 1. Configure your webhook URL ### Workspace default (recommended) Set a single HTTPS URL at workspace level, and the results of your async requests are delivered to it without a `webhookUrl` in each request. > [!IMPORTANT] > The workspace default URL receives successful results only. When a job ends in error, no callback is sent to it: read its `errorCode` by [polling](https://docs.reversecontact.com/guides/polling) `GET /v2/webhooks/:webhookId`. A `webhookUrl` sent in the request receives both results and errors. 1. Go to [Settings > Webhooks](https://app.reversecontact.com/settings/webhooks) 2. Enter your HTTPS endpoint URL 3. Click Save 4. Click Test to send a test payload and verify connectivity > [!TIP] > With the workspace default URL, the [Webhook Events](https://app.reversecontact.com/logs/events) dashboard tracks every delivery. Delivery failures are not refunded: the data was fetched, so the credit is kept and the result stays retrievable via [polling](https://docs.reversecontact.com/guides/polling). ### Per-request webhook URL override (optional) Include a `webhookUrl` field in your request body to override the delivery target for a single request. This is the fastest way to test locally with a tunnel like ngrok: **Local tunnel** ```bash [Bash] ngrok http 3000 ``` Then use the forwarding URL in your request: **Request body** ```json [JSON] { "url": "https://social.com/in/janedoe", "webhookUrl": "https://a1b2c3d4.ngrok-free.app/webhooks/reversecontact" } ``` If no webhook URL is available (neither in the request body nor in workspace settings), the job still runs: retrieve its result by [polling](https://docs.reversecontact.com/guides/polling) `GET /v2/webhooks/:webhookId`. To receive it as a callback instead, configure a default URL in [Settings > Webhooks](https://app.reversecontact.com/settings/webhooks) or include `webhookUrl` in your request body. ## 2. Set up a receiver > [!TIP] > If you don't already operate a public HTTPS receiver, use [Polling](https://docs.reversecontact.com/guides/polling) instead: you trigger jobs the same way and pull the results on demand, with no server to run. A webhook receiver is a standard POST endpoint that accepts JSON and returns `200`. ```javascript [Node.js / Express] const express = require("express"); const app = express(); app.use(express.json()); app.post("/webhooks/reversecontact", (req, res) => { const event = req.body; console.log("Webhook received:", event.webhookId); if (event.data) { // Process the result, e.g. from POST /v2/fetch/persons/live console.log("Profile:", event.data.firstName, event.data.lastName); } // Always respond with 200 to acknowledge receipt res.status(200).json({ received: true }); }); app.listen(3000, () => console.log("Listening on port 3000")); ``` ```python [Python / Flask] from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/webhooks/reversecontact", methods=["POST"]) def webhook(): event = request.get_json() print("Webhook received:", event["webhookId"]) if "data" in event: # Process the result, e.g. from POST /v2/fetch/persons/live print("Profile:", event["data"]["firstName"], event["data"]["lastName"]) # Always respond with 200 to acknowledge receipt return jsonify({"received": True}), 200 if __name__ == "__main__": app.run(port=3000) ``` ## Webhook payload ### Error callback When a job ends in error and the request carried a `webhookUrl`, you receive a callback with an `errorCode` inside `data`. The original input is echoed back alongside the error code: `linkedinUrl` for a person, `slug` (the last part of the company URL) for a company, and `linkedinUrl`, `fullName` or `companyDomain` for Find Email and Find Phone: **Error callback** ```json [JSON] { "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "data": { "linkedinUrl": "https://social.com/in/janedoe", "errorCode": "person-not-found" } } ``` ### Result callback When the job completes successfully, the callback carries the requested data in `data`: **Result callback** ```json [JSON] { "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "data": { "...": "endpoint-specific result payload" } } ``` > [!TIP] > Check for `errorCode` inside `data` to distinguish errors from successful results. If `data.errorCode` is present, the request failed. Otherwise, treat the callback as the terminal success for that `webhookId`. ### Enrichment error codes These codes describe the enrichment outcome and arrive as `data.errorCode` in the callback: | Code | Description | | ---- | ----------- | | `invalid-request` | The request input is malformed or doesn't match the expected format | | `data-not-found` | No matching result was found | | `person-not-found` | The person profile doesn't exist | | `company-not-found` | The company profile doesn't exist | | `email-not-found` | No email could be resolved for this person | | `fetch-data-error` | The API failed while fetching data. Safe to retry | | `result-unavailable` | The enrichment completed but the result was lost on our side. Retry the job. If the request was charged, contact support to get the credit back. | | `data-subject-blocked` | This person or company asked not to be processed under GDPR or CCPA. The job has no result and its credits are refunded. Do not retry. | ### Delivery error codes These codes mean the enrichment was fetched successfully but the result could not be pushed to your webhook URL. The job is reported as `succeeded`, the `result` is still available, and the failed push is named by a `deliveryStatus` field (set to one of the codes below) on the poll and `check_task` responses. | Code | Billed | Description | | ---- | ------ | ----------- | | `webhook-url-invalid` | Yes | Your webhook returned a `4xx` response | | `webhook-url-errored` | Yes | Your webhook returned a `5xx` or a network error | | `webhook-url-timeout` | Yes | Your webhook did not respond in time | | `webhook-url-unreachable` | Yes | The webhook URL could not be reached from production | > [!NOTE] > Delivery failures are billed, not refunded: the data was successfully fetched, so the credit is kept. The job stays `succeeded` with the `result` populated: poll `GET /v2/webhooks/:webhookId` to retrieve it. The `deliveryStatus` field tells you the push failed (and why) without flipping the job to `errored`. ## Security best practices - Use HTTPS: the API rejects plain HTTP URLs for webhook delivery. - Validate the payload shape. Every callback contains `webhookId` and `data`: check for `errorCode` inside `data` to distinguish errors from results, and ignore anything that doesn't match. - Verify the `webhookId`. Store the `webhookId` from the initial response and check incoming callbacks against it, both to match each callback to its request and to reject unknown ones. - Respond quickly with a `200` status code. If you need to do heavy processing, acknowledge receipt first and process the data asynchronously. ## Dashboard monitoring The [Webhook Events](https://app.reversecontact.com/logs/events) page in your dashboard shows every webhook delivery: - Status distribution: a summary of succeeded, errored, and in-progress events. - Event log: each row displays the timestamp, `webhookId`, status, credits consumed, credits refunded, and error code (if any). - Filters: narrow down events by status, error code, or `webhookId` to find specific deliveries. ## Go to production Before going live: - Deduplicate on `webhookId`: in rare cases, a callback may be delivered more than once. - Log the `webhookId` in your application logs to trace requests end to end. - Monitor your credits. Credits are deducted when the request is accepted, before the result is delivered, and refundable errors are refunded automatically. See [Credits](https://docs.reversecontact.com/guides/credits). The [Production checklist](https://docs.reversecontact.com/guides/production-checklist) covers the rest of the hardening. ## Troubleshooting If you are not receiving webhooks: - Ensure your endpoint is publicly accessible from the internet (not only on `localhost`) - If you omitted `webhookUrl` from the request, verify that a workspace default URL is configured in [Settings > Webhooks](https://app.reversecontact.com/settings/webhooks) - A job that ends in error sends no callback to the workspace default URL: poll `GET /v2/webhooks/:webhookId` to read its `errorCode` - Verify that your endpoint accepts `POST` requests with a `Content-Type: application/json` body - Check your server/firewall logs for incoming requests If you get timeout errors: - Your webhook endpoint must respond within 10 seconds - If processing takes longer, return `200` immediately and handle the data asynchronously If you receive an error or not-found callback instead of data: - The API sends a callback with an `errorCode` in `data` when the result is not available, as expected (see [Enrichment error codes](#enrichment-error-codes)) - Verify the URL does not require authentication or return redirects If you receive duplicate deliveries: - Use the `webhookId` to deduplicate on your side For transient errors: - `fetch-data-error` is usually transient and safe to retry after a short delay - `result-unavailable` means the enrichment completed but the result was lost on our side. Retry the job. If the request was charged, contact support to get the credit back ## FAQ ### Do I need webhooks to receive async results from Reverse Contact? No. Polling uses the same endpoints and returns the same data at the same enrichment cost, needs no public HTTPS endpoint, costs zero credits per poll and does not count toward your per-minute rate limit. Webhooks are meant for teams that already run a public HTTPS receiver and prefer a push model, and most teams integrate faster with [Polling](https://docs.reversecontact.com/guides/polling). ### How long does it take to receive a webhook result? Reverse Contact aims to POST each async result to your webhook URL in under 15 seconds, but processing can stretch to tens of minutes depending on source availability, queue load and retries. There is no synchronous SLA: the live endpoints are built to update databases and feed async pipelines, not to back a real-time request a user is waiting on. ### Do I need to send a webhook URL with every async request? No. A default webhook URL set once in Settings > Webhooks receives the successful results of every async request sent without a `webhookUrl`, and a `webhookUrl` field in the request body overrides that default for a single request. Errors are sent only to a `webhookUrl` given in the request: with the default URL, read them by polling. If neither is set, the job still runs and you retrieve the result by polling `GET /v2/webhooks/:webhookId`. ### Are credits refunded when webhook delivery fails? No. A failed webhook delivery (`webhook-url-invalid`, `webhook-url-errored`, `webhook-url-timeout` or `webhook-url-unreachable`) is billed because the data was fetched successfully, so the credit is kept. The job stays `succeeded`, a `deliveryStatus` field names the delivery problem, and the result can still be retrieved by polling `GET /v2/webhooks/:webhookId`. ### How quickly must my webhook endpoint respond? An endpoint receiving Reverse Contact webhooks must respond within 10 seconds. If processing takes longer, return `200` immediately and handle the data asynchronously, because a webhook that does not respond in time is reported as `webhook-url-timeout` and that delivery failure is still billed. --- # Lists: orchestrate enrichment > Orchestrate enrichment for up to 25,000 people or companies with Lists, including deduplication, credit budgets and stored results. - Page: https://docs.reversecontact.com/guides/lists - Last verified: 2026-11-04 Lists are the centre of the Reverse Contact API. Every other endpoint is a single lookup: one person or one company in, one answer out, and your code chains the lookups, follows the async ones, retries, removes duplicates, counts credits and stores the results. A List orchestrates that whole pipeline for you, for up to 25,000 people or companies at a time. You hand it what you know about them: it identifies each one, buys the data you ask for under a budget you set, keeps the result, and tells you when it is done. ## What a List orchestrates For each line you send, a List runs the chain you would otherwise write yourself: 1. **Identity.** It picks the lookup each Signal needs. A Social profile URL is read from saved profiles, or live with `live: true`. An email, or a name with its company, is reversed to the person behind it. A domain, a company page URL or a company id leads to the company. A Signal no one matches is kept as [Pending Intake](#pending-intake), for free. 2. **Deduplication.** A person or a company already on the List is recognised and costs nothing, and lines naming the same identity with the same `externalId` are merged into one. 3. **Data.** It buys the data families you ask for (profile, company details, work email, phone number) only for the members that lack them. The lookup that adds a member already stores its profile, or its company details on a companies List, and the email of an email Signal becomes the member's work email, for free. Data a member already holds is never bought twice, and an email or a phone number already looked for is not looked for again. 4. **Budget.** It never spends more than the `maxCredits` you set. Before a step that would cross it, or when your balance runs out, it pauses, keeps the work done, and waits for you to resume. 5. **Execution.** The import runs in the background as one List Operation: free progress reads with long polling, a webhook each time it stops moving, a result for each line with your own `externalId`, and a cancel that never charges the lines not started. 6. **Storage.** Members and their data stay in the List, ready to read, to find by your own id, to export as CSV, or to grow with the next import. ## One set of Lists for the app, your code and your agents A List belongs to your workspace, not to the API. A List created with the API shows up in the app for your team. A List built in the app, from a Search or a file, can be read, filled and exported through the API. Ask, in the app, and your own agent, through the [MCP server](https://docs.reversecontact.com/guides/mcp), and your code, through the API or the [SDKs](https://docs.reversecontact.com/sdk/javascript), work on the same Lists with the same rules. However a member entered, it is one member, charged once. ## Single endpoints or a List | What you have to handle | With single endpoints | With a List | | -------------------------- | --------------------------------------------- | ---------------------------------------------------------------- | | Many people at once | One request per person | Up to 25,000 lines in one import | | Signals of different kinds | Your code picks the endpoint for each row | The List picks the lookup for each Signal | | Emails and phone numbers | One async job to follow per lookup | One operation for the whole import | | Duplicates | Your code detects them | A member already on the List is free | | Spending | Your code counts credits | `maxCredits`, with pause and resume | | Failures and retries | Your code retries, and must not pay twice | Send the import again with its `operationId`: nothing is charged twice | | The result | Your code stores it | Kept in the List, shared with the app and your agents | A single endpoint stays the right tool to look up one person or one company while a user waits: [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person), [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) or [Find Email](https://docs.reversecontact.com/endpoints/contact-email). Work on a set of people or companies you keep (a CRM segment, a prospect list that grows every week, target accounts to hand to your sales team) belongs in a List. ## A List or Bulk enrichment [Bulk enrichment](https://docs.reversecontact.com/guides/bulk-enrichment) also takes up to 25,000 people in one request, without a List: it looks up each one, gives you a CSV with one row per item, and keeps nothing else. Choose by what you do with the result: | You want to | Use | | -------------------------------------------------------- | ------- | | Enrich a file of people once and get it back as CSV | Bulk enrichment | | Keep the people, add to them and buy data over time | A List | | Never pay twice for someone you already have | A List | | Work on companies, or buy company details | A List | | Cap the spend with `maxCredits`, pause and resume | A List | The [job history](https://docs.reversecontact.com/endpoints/jobs) lists both: your Bulk enrichment jobs and the jobs of your Lists, such as imports, with their progress and credits. ## The building blocks of a List | Term | What it means | | ------------------ | --------------------------------------------------------------------------------------------------------------- | | **List** | A named set of people (`persons`) or of companies (`companies`) in your workspace, never both | | **Signal** | What you know about someone: a Social profile URL, an email, a first and last name with a company, a company domain | | **List Member** | A person or a company a Signal matched, with the data bought for it and your `externalId` | | **Pending Intake** | The Signals no one matched, kept on the List for free so you can see what was not found | | **Data family** | Data you buy for each member: `profile`, `company`, `contact_email`, `contact_phone` | | **List Operation** | One run of work on a List, such as an import, with an id, a status and counters, followed at its `pollUrl` or told to your `webhookUrl` | ## The API flow, endpoint by endpoint 1. **Create the List** with [Create a List](https://docs.reversecontact.com/endpoints/list-create): a name, and `persons` or `companies`. 2. **Fill it with Signals.** [Add members](https://docs.reversecontact.com/endpoints/list-members-add) takes up to 1,000 Signals and answers when they are done. [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals) queues up to 25,000 Signals or a public CSV file, and can buy data families for each member. 3. **Follow the import** with [Read a List operation](https://docs.reversecontact.com/endpoints/list-operation), the `pollUrl` of the import, or be told by its `webhookUrl`. 4. **Read the result** with [List members](https://docs.reversecontact.com/endpoints/list-members), or download it with [Export a List](https://docs.reversecontact.com/endpoints/list-export). Every call of this flow is free. You pay only for the members it adds and the data it buys (see [What a List costs](#what-a-list-costs)). [Build your first List](https://docs.reversecontact.com/guides/build-a-list) runs this flow step by step, with cURL, JavaScript, Python and the SDKs. The API key needs the `lists` permission. A call that spends credits also needs the permission of what it buys, as the matching endpoint would: `persons:enrich`, `companies:enrich` or `contact` (see [API key management](https://docs.reversecontact.com/guides/api-key-management)). ## Add members or run an import | You want to | Use | | -------------------------------------------------------- | ----------------------------------------------------------- | | Add a few hundred people and get the result in the reply | [Add members](https://docs.reversecontact.com/endpoints/list-members-add) | | Add more than 1,000 Signals, or a public CSV or Google Sheet | [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals) | | Buy emails, phone numbers, profiles or company details for each member | [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals) with `families` | | Cap what a run may spend | [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals) with `maxCredits` | | Be told when the work stops moving | [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals) with `webhookUrl` | An import of a CSV file sends its public URL in `sourceUrl` instead of `signals`: up to 25,000 rows and 25 MiB, with the columns matched from their headers (Social URL, email, first name, last name, company, domain). ## Grow a List and buy data over time A List is built for work that lasts: add a file today, buy emails for its members tomorrow, add the new leads of next week. - **Add new people at any time.** Send them with [Add members](https://docs.reversecontact.com/endpoints/list-members-add) or an import: a person already on the List reads `already_present` and costs nothing. - **Buy data later.** To buy data for members already on a List, import them again with `families`: their identity costs nothing, and only the data they lack is bought. An email or a phone number already looked for, found or not, is not looked for again. - **Take people out** with [Remove members](https://docs.reversecontact.com/endpoints/list-members-remove), which also removes the data stored for them, and delete a List you no longer need with [Delete a List](https://docs.reversecontact.com/endpoints/list-delete). - **Imports run in turn.** An import queued behind a paused one on the same List waits until you resume or cancel it (`stopReason: "blocked_by_paused_operation"`). Deleting a List returns `409 LIST_BUSY` while an operation is queued, running or paused on it, while the lines of a cancelled one still wait for their result, or while a Search of the app is still adding people to it. ## Link each member to your CRM with externalId Send `externalId` on each Signal, the id of the line in your CRM or file. Each line result gives it back, the member keeps it, and [List members](https://docs.reversecontact.com/endpoints/list-members) finds a member by it (`?externalId=crm-001`). You never have to store our ids to find the member of a line. A member keeps the first `externalId` it received, and `externalId` is never used to find anyone. ## Pending Intake A Signal no one matches does not become a member: the List keeps it as a **Pending Intake** line, free, so you can see what was not found and fix it. Rows of [Get a List](https://docs.reversecontact.com/endpoints/list) and [List members](https://docs.reversecontact.com/endpoints/list-members) carry `kind: "pending"` for those lines and `kind: "member"` for members. Pending Intake is never enriched, and [Export a List](https://docs.reversecontact.com/endpoints/list-export) writes its lines with what was sent. Send a Signal again once it is fixed: when it finds someone, the new member replaces its Pending Intake line. Dismiss lines with the `pendingIds` of [Remove members](https://docs.reversecontact.com/endpoints/list-members-remove). ## Control spend: maxCredits, pauses and resume An import with `families` whose worst case is above 25 credits needs `maxCredits`, the most it may spend. The import pauses before a step that would cross it, keeps the work done, and reports `stopReason: "max_credits"` with the smallest ceiling that lets it continue in `nextStep.minimumMaxCredits`. A balance that runs out pauses it the same way, with `stopReason: "balance"`. An import sent without `maxCredits` may spend up to its worst case. To resume, send the same import again, same `operationId`, with a higher `maxCredits` or after adding credits: nothing already done is charged twice. To stop an import for good, cancel it with [Cancel a List operation](https://docs.reversecontact.com/endpoints/list-operation-cancel): the lines that had not started are never charged. See [Pauses and how to resume](https://docs.reversecontact.com/endpoints/list-intake-signals#pauses-and-how-to-resume). ## What a List costs | What | Credits | | ------------------------------------- | --------------------------------------------------------------------- | | Create, read, export, remove, delete, follow and cancel | Free | | A new person | 1 from a Social profile URL (2 with `live: true`), 2 from an email or a name | | A new company | 1 from a company page URL or a company id (2 when the page is read live), 2 from a domain | | A member already on the List, a Signal no one matches | Free | | `profile` family on a people List, `company` family on a companies List | Per member that lacks it, 1 when read from its Social profile or company page URL, 2 when found by name or domain (2 in the worst case). A member added from a Signal already holds it, for free | | `company` family on a people List | Per member, 1 from the company page URL of the person's current company, 2 from its domain (2 in the worst case) | | `contact_email` family | 3 per email found, free when none is found. The email of an email Signal is kept for free | | `contact_phone` family | 20 per phone number found, free when none is found | ## Limits per workspace and per List | Limit | Paid plan | Free trial | | -------------------------------------- | --------- | ---------- | | Lists per workspace | 200 | 1 | | Members per List | 25,000 | 50 | | Pending Intake lines per List | 25,000 | 50 | | Signals per [Add members](https://docs.reversecontact.com/endpoints/list-members-add) call | 1,000 | 1,000 | | Signals or CSV rows per import | 25,000 | 25,000 | The Lists of a free trial that ends without a purchase are deleted 30 days after the trial ended. The List calls count toward your per-minute and daily rate limits like any other call, with two exceptions: reading an operation is never counted, and the request that starts an import counts once on the daily limit and not on the per-minute one, whatever its number of lines. The lines of the import then take their share of your per-minute limit as they run, at most 80% of it, and wait when that share is taken. See [Rate limits](https://docs.reversecontact.com/guides/rate-limits#how-requests-are-counted). ## Error codes of the List endpoints The List endpoints answer with the error envelope of every V2 endpoint (see [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries)). Their own codes: | Status | Code | Meaning | | ------ | ----------------------------------- | ----------------------------------------------------------------------- | | `400` | `LIST_INVALID_INPUT` | A field is missing or does not fit the List: the message says which | | `400` | `LIST_ENTITY_MISMATCH` | A company sent to a people List, or a person to a companies List, with [Add members](https://docs.reversecontact.com/endpoints/list-members-add). An import marks the line `refused` instead. | | `400` | `WEBHOOK_URL_UNREACHABLE` | The import `webhookUrl` is not a public HTTPS URL | | `402` | `INSUFFICIENT_CREDITS` | The balance cannot pay for what [Add members](https://docs.reversecontact.com/endpoints/list-members-add) could add | | `403` | `INSUFFICIENT_PERMISSIONS` | The key lacks `lists`, or the permission of what the call buys | | `404` | `LIST_NOT_FOUND` | No List with this id in your workspace | | `404` | `LISTS_NOT_ACTIVATED` | The workspace has never had a List: create one first | | `404` | `LIST_OPERATION_NOT_FOUND` | No operation with this id on this List | | `409` | `LIST_LIMIT_REACHED` | The workspace already has its maximum of Lists | | `409` | `LIST_MEMBER_LIMIT_REACHED` | The List already holds its maximum of members | | `409` | `LIST_PENDING_INTAKE_LIMIT_REACHED` | The List already holds its maximum of Pending Intake lines | | `409` | `LIST_BUSY` | The List you delete still has work on it: an operation queued, running or paused, the last lines of a cancelled one, or a Search of the app adding people. Wait for it or cancel it | | `409` | `LIST_OPERATION_NOT_CANCELLABLE` | The operation already ended | | `409` | `IDEMPOTENCY_KEY_REUSED` | The `Idempotency-Key` already created a List of the other kind | | `422` | `VALIDATION_ERROR` | A field has the wrong type or format, such as an id that is not a UUID | | `422` | `BULK_JOB_CSV_PARSE_ERROR` | The `sourceUrl` file of an import could not be read | | `429` | `LIST_EXPORT_IN_PROGRESS` | The workspace is already exporting a List: retry after `Retry-After` | | `429` | `LIST_EXPORT_BUSY` | The server is serving another export: retry after `Retry-After` | | `502` | `LIST_JOB_ENGINE_ERROR` | The import could not be queued: send it again with the same `operationId` | | `503` | `LISTS_PREPARING` | The first List of the workspace is being set up: retry in a few seconds | | `503` | `BULK_QUEUE_UNAVAILABLE` | The import queue is busy: retry shortly | | `504` | `GATEWAY_TIMEOUT` | The request ran out of time: see [Add members](https://docs.reversecontact.com/endpoints/list-members-add) and [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals) | ## FAQ ### What is a List in the Reverse Contact API? A List is a named set of people, or of companies, kept in your Reverse Contact workspace, and the part of the API that orchestrates the work on them. From Signals (Social URLs, emails, names with a company, company domains) it identifies each person or company, skips those already on the List, buys the data families you ask for under a `maxCredits` budget and keeps the result, which you read with `GET /v2/lists/:listId/members` or download with `GET /v2/lists/:listId/export`. It is the same List your team sees in the app. ### When should I use a List instead of Enrich profile? Use `POST /v2/enrich/persons` to look up one person while a user waits. Use a List to enrich a file or a CRM segment of hundreds or thousands of contacts, to buy emails or phone numbers for a set of people under a `maxCredits` cap, or to keep a set of prospects that grows over time: one List import takes up to 25,000 lines, runs in the background, and never charges twice for a person already on the List. ### How much does it cost to add someone to a List? A new person costs 1 credit from a Social profile URL read from saved data, 2 with `live: true`, and 2 from an email or a name. A person already on the List and a Signal no one matches are free, and so are creating, reading, exporting and deleting Lists. ### How do I find emails for everyone on a List? Send the members again to the import endpoint, `POST /v2/lists/:listId/intake/signals`, with `families: ["contact_email"]`. People already on the List cost nothing for their identity, each email found costs 3 credits, and a person without an email costs nothing. A person whose email was already looked for, found or not, is not looked up again, and the email of an email Signal becomes the person's work email for free. Above 25 credits of worst case, the import needs `maxCredits`. ### What happens to a Signal that matches no one in a List? A Signal no one matches is kept on the List as a Pending Intake line, for free, and never enriched. Get a List and List members return those lines with `kind: "pending"`, and Export a List writes them with what was sent. Send the Signal again once it is fixed: when it finds someone, the new member replaces its Pending Intake line. Dismiss the lines you do not need with the `pendingIds` of `DELETE /v2/lists/:listId/members`. --- # Build your first List > Turn CRM contacts into a List, buy their work emails under a credit cap, follow the import and read the result, with cURL, JavaScript, Python and the SDKs. - Page: https://docs.reversecontact.com/guides/build-a-list - Last verified: 2026-11-04 This guide turns three contacts of a CRM into a [List](https://docs.reversecontact.com/guides/lists), finds their work emails, and reads them back. It takes five calls, all free: you pay only for the members the List adds and the emails it finds, 11 credits at most here. ## Before you start - An API key in `RC_API_KEY`, from [API Keys](https://app.reversecontact.com/api-keys). A key with full access works. A scoped key needs `lists`, plus Person Enrichment for the people the List adds and Contact for the emails it buys (see [API key management](https://docs.reversecontact.com/guides/api-key-management)). - A few credits. A free trial holds one List of up to 50 members, enough for this guide. ## The steps ### 1. Create the List **POST /v2/lists** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/lists \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: q4-sales-leaders" \ -d '{"name": "Q4 sales leaders", "entity": "persons"}' ``` The response gives the List in `data`: keep `data.id`, the List id the next calls name in their path (`$LIST_ID` in the commands below). The `Idempotency-Key` makes the call safe to retry: sent again, it returns the same List instead of a second one. ### 2. Import the people and buy their emails **POST /v2/lists/:listId/intake/signals** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/lists/$LIST_ID/intake/signals \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f", "signals": [ { "linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001" }, { "email": "john.smith@acme.com", "externalId": "crm-002" }, { "firstName": "Alex", "lastName": "Martin", "companyDomain": "acme.com", "externalId": "crm-003" } ], "families": ["contact_email"], "maxCredits": 50 }' ``` Each Signal says what you know about one person, and `externalId` carries the id of the line in your CRM. `families` asks for a work email for each member that has none yet. The import answers at once with `data.operationId` and `data.pollUrl`, and runs in the background. The worst case of this import is 14 credits: 1 for Jane Doe found from her Social profile URL, 2 each for John Smith found from his email and Alex Martin found from his name, and 3 per email found. John Smith's email is the one you sent: it becomes his work email for free and is not looked up, so this import charges at most 11 credits. A line no one matches and an email not found cost nothing. Up to 25 credits of worst case, `maxCredits` is optional: without it, the import may spend up to its worst case. Send it anyway to set your own cap. `operationId` is a UUID you choose. Keep it: sending the same import again with it never charges twice, and it is how you resume an import that paused. ### 3. Follow the import **GET /v2/lists/:listId/operations/:operationId/progress** ```bash [cURL] curl "https://api.reversecontact.com/v2/lists/$LIST_ID/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress?wait=20" \ -H "Authorization: Bearer $RC_API_KEY" ``` This route is the `pollUrl` the import answered with, built from the List id and your `operationId`. With `wait=20`, each read waits up to 20 seconds for the import to complete, pause or stop, and otherwise answers with the import still running. Read it again while `clientAction` is `poll`. Then `done` with `status: "completed"` means the members and their emails are in the List, and `resume` means it paused: `nextStep` says what to do. These reads are free and never counted against your rate limits. Instead of reading, you can pass a public HTTPS `webhookUrl` with the import: it is told each time the operation stops moving. ### 4. Read the members and their emails **GET /v2/lists/:listId/members** ```bash [cURL] curl "https://api.reversecontact.com/v2/lists/$LIST_ID/members?kind=members" \ -H "Authorization: Bearer $RC_API_KEY" curl "https://api.reversecontact.com/v2/lists/$LIST_ID/members?externalId=crm-002" \ -H "Authorization: Bearer $RC_API_KEY" ``` Each member carries its `fields` (name, headline, company, location), your `externalId`, and `families.contactEmail` with the email found, or for John Smith the email you sent (`source: "signal:paste"`). The second call finds the member of a CRM line by its `externalId`, without storing any id of ours. ### 5. Or download the List as CSV **GET /v2/lists/:listId/export** ```bash [cURL] curl -o q4-sales-leaders.csv "https://api.reversecontact.com/v2/lists/$LIST_ID/export" \ -H "Authorization: Bearer $RC_API_KEY" ``` The file has one row per line of the List, members and Pending Intake, in the order they were added, with the data the List holds. Exporting is free and never looks anyone up again. A workspace exports one List at a time: run your exports one after the other, a second one at the same time returns `429 LIST_EXPORT_IN_PROGRESS`. ## The complete pipeline The same five steps in one script, with plain HTTP calls or with the [TypeScript](https://docs.reversecontact.com/sdk/javascript) and [Python](https://docs.reversecontact.com/sdk/python) SDKs. **Create, import, follow, read** ```javascript [JavaScript] const BASE = "https://api.reversecontact.com"; const headers = { "Authorization": `Bearer ${process.env.RC_API_KEY}`, "Content-Type": "application/json" }; // 1. Create the List: the Idempotency-Key makes a retry safe const list = await fetch(`${BASE}/v2/lists`, { method: "POST", headers: { ...headers, "Idempotency-Key": "q4-sales-leaders" }, body: JSON.stringify({ name: "Q4 sales leaders", entity: "persons" }) }).then((res) => res.json()); const listId = list.data.id; // 2. Import the people and buy their emails, 50 credits at most const queued = await fetch(`${BASE}/v2/lists/${listId}/intake/signals`, { method: "POST", headers, body: JSON.stringify({ operationId: crypto.randomUUID(), // keep it to resume the import signals: [ { linkedinUrl: "https://www.social.com/in/janedoe", externalId: "crm-001" }, { email: "john.smith@acme.com", externalId: "crm-002" }, { firstName: "Alex", lastName: "Martin", companyDomain: "acme.com", externalId: "crm-003" } ], families: ["contact_email"], maxCredits: 50 }) }).then((res) => res.json()); // 3. Follow the import until it stops moving let operation; do { const res = await fetch(`${BASE}${queued.data.pollUrl}?wait=20`, { headers }); operation = (await res.json()).data; } while (operation.clientAction === "poll"); if (operation.status !== "completed") throw new Error(operation.nextStep?.message ?? `Import ${operation.status}`); // 4. Read the members and their emails const members = await fetch(`${BASE}/v2/lists/${listId}/members?kind=members`, { headers }).then((res) => res.json()); for (const member of members.data.items) { console.log(member.externalId, member.fields.name, member.families.contactEmail?.value ?? "no email"); } ``` ```python [Python] import os import uuid import requests BASE = "https://api.reversecontact.com" headers = { "Authorization": f"Bearer {os.environ['RC_API_KEY']}", "Content-Type": "application/json", } # 1. Create the List: the Idempotency-Key makes a retry safe list_ = requests.post( f"{BASE}/v2/lists", headers={**headers, "Idempotency-Key": "q4-sales-leaders"}, json={"name": "Q4 sales leaders", "entity": "persons"}, ).json() list_id = list_["data"]["id"] # 2. Import the people and buy their emails, 50 credits at most queued = requests.post( f"{BASE}/v2/lists/{list_id}/intake/signals", headers=headers, json={ "operationId": str(uuid.uuid4()), # keep it to resume the import "signals": [ {"linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001"}, {"email": "john.smith@acme.com", "externalId": "crm-002"}, {"firstName": "Alex", "lastName": "Martin", "companyDomain": "acme.com", "externalId": "crm-003"}, ], "families": ["contact_email"], "maxCredits": 50, }, ).json() # 3. Follow the import until it stops moving while True: operation = requests.get( f"{BASE}{queued['data']['pollUrl']}", headers=headers, params={"wait": 20}, timeout=30 ).json()["data"] if operation["clientAction"] != "poll": break if operation["status"] != "completed": raise RuntimeError((operation.get("nextStep") or {}).get("message") or f"Import {operation['status']}") # 4. Read the members and their emails members = requests.get(f"{BASE}/v2/lists/{list_id}/members", headers=headers, params={"kind": "members"}).json() for member in members["data"]["items"]: email = (member["families"].get("contactEmail") or {}).get("value", "no email") print(member.get("externalId"), member["fields"].get("name"), email) ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); // 1. Create the List: the SDK sends an Idempotency-Key and reuses it on its retries const list = await client.lists.create({ name: "Q4 sales leaders", entity: "persons" }); const listId = list.data.id; // 2. Import the people and buy their emails, 50 credits at most const queued = await client.lists.importSignals(listId, { signals: [ { linkedinUrl: "https://www.social.com/in/janedoe", externalId: "crm-001" }, { email: "john.smith@acme.com", externalId: "crm-002" }, { firstName: "Alex", lastName: "Martin", companyDomain: "acme.com", externalId: "crm-003" } ], families: ["contact_email"], maxCredits: 50 }); // 3. Follow the import until it stops moving const operation = await client.lists.operations.wait(listId, queued.data.operationId); console.log(operation.data.status, operation.data.clientAction); // 4. Read the members and their emails for await (const member of client.lists.members.listAll(listId, { kind: "members" })) { console.log(member); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: # 1. Create the List: the SDK sends an Idempotency-Key and reuses it on its retries list_id = client.lists.create(name="Q4 sales leaders", entity="persons").data.id # 2. Import the people and buy their emails, 50 credits at most queued = client.lists.import_signals( list_id, signals=[ {"linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001"}, {"email": "john.smith@acme.com", "externalId": "crm-002"}, {"firstName": "Alex", "lastName": "Martin", "companyDomain": "acme.com", "externalId": "crm-003"}, ], families=["contact_email"], max_credits=50, ) # 3. Follow the import until it stops moving operation = client.lists.operations.wait(list_id, queued.data.operation_id) print(operation.data.status, operation.data.client_action) # 4. Read the members and their emails for member in client.lists.members.list_all(list_id, kind="members"): print(member) ``` A List holds more than one page of members: send `nextCursor` back as `cursor` to read the next one (see [List members](https://docs.reversecontact.com/endpoints/list-members)). The SDK iterators read every page for you. ## Import a CSV file instead Send the public URL of a CSV file or a Google Sheet in `sourceUrl`, in place of `signals`: up to 25,000 rows and 25 MiB. Columns are matched from their headers (Social URL, email, first name, last name, company, domain), and each row becomes a Signal. **Import a CSV file** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/lists/$LIST_ID/intake/signals \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "operationId": "7e8f9a0b-1c2d-4e3f-8a4b-5c6d7e8f9a0b", "sourceUrl": "https://example.com/exports/q4-leads.csv", "families": ["contact_email"], "maxCredits": 2000 }' ``` The answer adds `sourceRows`, the data rows read from the file, and `mapping`, the header each field was read from: check it before you buy anything more. A file that is not public can be sent as `signals` instead, one object per row, with the id of the row in `externalId`. ## What to do next - Add the new leads of next week to the same List: a person already on it costs nothing (see [Grow a List and buy data over time](https://docs.reversecontact.com/guides/lists#grow-a-list-and-buy-data-over-time)). - Buy phone numbers or full profiles later, for the members that lack them, with the same import and other `families`. - See what was not found in [Pending Intake](https://docs.reversecontact.com/guides/lists#pending-intake), and how an import pauses and resumes in [Control spend](https://docs.reversecontact.com/guides/lists#control-spend-maxcredits-pauses-and-resume). - Hand the work to an AI agent: the [MCP server](https://docs.reversecontact.com/guides/mcp) runs the same steps on the same Lists. ## FAQ ### How much does it cost to build a List of CRM contacts with their work emails? Creating, following, reading and exporting a List are free. Each new person costs 1 credit from a Social profile URL and 2 from an email or a name, and each work email found costs 3 credits. The email of a contact sent by email becomes its work email for free. For the three contacts of this guide, the import costs at most 11 credits, and a contact no one matches or an email not found costs nothing. ### Can I send the same List import twice without paying twice? Yes. Send the import to `POST /v2/lists/:listId/intake/signals` with the same `operationId`: the API returns the operation it already created and never charges anything twice. The same `operationId` also resumes an import that paused at its `maxCredits` or at a low balance. ### How do I find the member of a CRM contact in a List? Send the id of the CRM contact as `externalId` on its Signal. The member keeps it, and `GET /v2/lists/:listId/members?externalId=crm-001` returns that member, so you never have to store a Reverse Contact id. ### How do I know when a List import is finished? Read `GET /v2/lists/:listId/operations/:operationId/progress`, the `pollUrl` of the import, with `wait=20` until `clientAction` is no longer `poll`: `done` means it ended, `resume` means it paused and `nextStep` says what to do. These reads are free and not counted against your rate limits. You can also pass a public HTTPS `webhookUrl` with the import, which is told each time the operation stops moving. ### Can I build a List with the Reverse Contact SDK? Yes. From version 0.2.0, the TypeScript SDK `@reverse-contact/sdk` and the Python SDK `reversecontact` manage Lists with `client.lists`: `create`, `importSignals` (`import_signals` in Python), `operations.wait` to follow an import, and `members.listAll` (`members.list_all`) to read every member. --- # Enrich people in bulk > Enrich up to 25,000 people in one request, follow the job, then read the results as they come or download one row per person as CSV or NDJSON. - Page: https://docs.reversecontact.com/guides/bulk-enrichment - Last verified: 2026-11-04 Bulk enrichment looks up a whole file of people in one request. You send up to 25,000 people as JSON, each with a Social profile URL, an email, or a name with its company, and choose what to find for them: the profile, a work email, a phone number. The job runs in the background, and you get one row per person, in the order you sent them, with your own id on each row: read them as they come, or download them as a CSV or NDJSON file. It is the API version of [Bulk Import](https://app.reversecontact.com/bulk) in the app: the same lookups, the same prices, the same CSV. ## When to use Bulk enrichment | You want to | Use | | ----------------------------------------------------------------- | ------------------------------------------------------------------- | | Look up one person while a user waits | [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person) or [Find Email](https://docs.reversecontact.com/endpoints/contact-email) | | Enrich a file or a CRM export once and get it back as CSV | Bulk enrichment | | Keep the people, add to them over time, never pay twice for someone | A [List](https://docs.reversecontact.com/guides/lists) | | Work on companies, or cap the spend with `maxCredits` | A [List](https://docs.reversecontact.com/guides/lists) | | Enrich a CSV without writing code | [Bulk Import](https://app.reversecontact.com/bulk) in the app | Bulk enrichment keeps nothing once you have the file. If you will come back to the same people, to buy their phone numbers next month or to add next week's leads, start with a List instead. ## How a job runs 1. **Start the job** with [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons), `POST /v2/enrich/persons/bulk`. It answers `202` at once with the job. 2. **Follow it** with [Get a job](https://docs.reversecontact.com/endpoints/job), `GET /v2/jobs/:jobId?wait=20`, while its `status` is `queued` or `running`. Or pass a `webhookUrl`: it receives a `job.updated` event when the job settles. 3. **Get the results**: read them a page at a time while the job runs with [Read job results by page](https://docs.reversecontact.com/endpoints/job-rows), `GET /v2/jobs/:jobId/rows`, or download them once it settles with [Download job results](https://docs.reversecontact.com/endpoints/job-results), `GET /v2/jobs/:jobId/results`: one row per item, as CSV or NDJSON. 4. **Stop it** if you need to with [Cancel a job](https://docs.reversecontact.com/endpoints/job-cancel), and find any past job in the [Job history](https://docs.reversecontact.com/endpoints/jobs). Starting, following, reading the results and cancelling are free. You pay only for what the items find. ## What it costs Each item costs what its unit call costs, and only for what is found: | What the item looks up | Credits | | ---------------------------------- | ----------------------------------------------------------------------------- | | Profile, from a Social profile URL | 1, or 2 with `live: true` | | Profile, from an email or a name | 2 | | `contact_email` | 3 for a professional email, 5 for a personal one, 8 for both with `emailType: "any"` | | `contact_phone` | 20 per phone number found | When the job starts, it reserves the most each item can cost, in the order you sent them, as long as your balance covers it. An item the balance no longer covers is not run: its row reads `insufficient_credits`, it costs nothing, and `notRunForCredits` counts it, while a later item that costs less can still run. When the job settles, what an item did not find is given back, so you pay only for what was found. - An item that names the same person as an earlier one is looked up once: its row reads `duplicate` and costs nothing. - The email and phone finders start from a Social profile URL, or from a first name, a last name and a company domain. Any other item first has its profile matched, charged like a profile lookup when it is found. - On a Go, Plus or Max plan, a profile your plan covers costs no credit, as in its unit call. Once the workspace has delivered its [daily profile cap](https://docs.reversecontact.com/guides/rate-limits#daily-profile-cap-of-the-go-plus-and-max-plans), the items of the job wait until 00:00 UTC instead of being refused. - A key with [credit limits](https://docs.reversecontact.com/guides/api-key-management#_4-set-credit-limits-optional) must hold the whole reservation, and counts only what the job spent once it settles. ## Step by step ### 1. Prepare the items Each item takes a `linkedinUrl`, an `email`, or a `firstName` and `lastName` with a `companyDomain` (or a `companyName` when you have no domain). Add `externalId`, the id of the line in your CRM or file: it comes back on the row of the item. An item with no field at all is not run and reads `skipped`, and an item that holds only a company is looked up but rarely matches anyone. Then choose `families`: `profile` (the default), `contact_email`, `contact_phone`, or several. With `contact_email`, `emailType` picks `professional` (the default), `personal` or `any`. ### 2. Start the job **POST /v2/enrich/persons/bulk** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/enrich/persons/bulk \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 9e2f4a1c-6b3d-4e7f-8a5c-2d1b0e9f8a7c" \ -d '{ "name": "CRM export, November", "items": [ { "linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001" }, { "firstName": "John", "lastName": "Smith", "companyDomain": "acme.com", "externalId": "crm-002" } ], "families": ["profile", "contact_email"] }' ``` The answer gives the job in `data.job`: keep its `id` (`$JOB_ID` in the commands below). `creditsEstimated` is what the job reserved, `duplicateCount` the items looked up once, and `notRunForCredits` the items your balance did not cover. Send an `Idempotency-Key` you choose for the job. If the answer gets lost, send the same request with the same key: for 24 hours, it returns the job it created instead of reserving credits for a second one. ### 3. Follow the job **GET /v2/jobs/:jobId** ```bash [cURL] curl "https://api.reversecontact.com/v2/jobs/$JOB_ID?wait=20" \ -H "Authorization: Bearer $RC_API_KEY" ``` With `wait=20`, each read waits up to 20 seconds for the job to settle. Read it again while `status` is `queued` or `running`, with an HTTP timeout above 20 seconds. These reads are free, also at a zero balance, and never counted against your rate limits. `progress` counts the items done, and `credits.used` is set once the job settles. Instead of reading, you can pass a public HTTPS `webhookUrl` when you start the job: it receives one `job.updated` event when the job completes, is cancelled or fails, with the path of the results. A failed delivery is tried again for about 3 hours, so the same event can arrive twice: key it on `jobId` and `status`. Read the job if the event has not arrived. ### 4. Download the results **GET /v2/jobs/:jobId/results** ```bash [cURL] curl -o results.csv "https://api.reversecontact.com/v2/jobs/$JOB_ID/results" \ -H "Authorization: Bearer $RC_API_KEY" ``` The file is ready once the job is `completed` or `cancelled`: `links.results` of the job is set then. It holds one row per item, in the order you sent them: your fields and `externalId` first, then `rc_status`, `rc_message`, and the columns of what you asked for, such as `rc_email` or the `rc_` profile columns. See [Download job results](https://docs.reversecontact.com/endpoints/job-results) for every column. The file is UTF-8 with a byte order mark, so a spreadsheet opens accented names correctly. For a program, add `format=ndjson`: the file then holds one JSON object per item, with the person in the shape of [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person). **GET /v2/jobs/:jobId/results?format=ndjson** ```bash [cURL] curl -o results.ndjson "https://api.reversecontact.com/v2/jobs/$JOB_ID/results?format=ndjson" \ -H "Authorization: Bearer $RC_API_KEY" ``` ### Or read the results as they come You do not have to wait for the job to settle: [Read job results by page](https://docs.reversecontact.com/endpoints/job-rows) returns the same JSON objects, up to 100 per page, in the order sent, from the start of the run. Pass `nextCursor` back as `cursor` until it is `null`. While the next item still runs, the page is empty: read the same cursor again after the `Retry-After` seconds. Each page counts toward your rate limits, and the items of a running job already take up to 80% of your per-minute limit: read with `limit=100`, at the `Retry-After` pace. **GET /v2/jobs/:jobId/rows** ```bash [cURL] curl "https://api.reversecontact.com/v2/jobs/$JOB_ID/rows?limit=100" \ -H "Authorization: Bearer $RC_API_KEY" # The next page: pass back data.nextCursor of the previous one. curl "https://api.reversecontact.com/v2/jobs/$JOB_ID/rows?limit=100&cursor=$NEXT_CURSOR" \ -H "Authorization: Bearer $RC_API_KEY" ``` ## The complete pipeline **Start, follow, download** ```javascript [JavaScript] import { writeFile } from "node:fs/promises"; const BASE = "https://api.reversecontact.com"; const headers = { "Authorization": `Bearer ${process.env.RC_API_KEY}` }; // 1. Start the job: keep the Idempotency-Key to retry safely const created = await fetch(`${BASE}/v2/enrich/persons/bulk`, { method: "POST", headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() }, body: JSON.stringify({ name: "CRM export, November", items: [ { linkedinUrl: "https://www.social.com/in/janedoe", externalId: "crm-001" }, { firstName: "John", lastName: "Smith", companyDomain: "acme.com", externalId: "crm-002" } ], families: ["profile", "contact_email"] }) }); let { data: { job } } = await created.json(); // 2. Follow it while it is queued or running while (job.status === "queued" || job.status === "running") { const response = await fetch(`${BASE}${job.links.self}?wait=20`, { headers }); job = (await response.json()).data; } // 3. Download the results: write the bytes, byte order mark included if (job.links.results) { const response = await fetch(`${BASE}${job.links.results}`, { headers }); await writeFile("results.csv", Buffer.from(await response.arrayBuffer())); } console.log(job.status, `${job.credits.used} credits`); ``` ```python [Python] import os import uuid import requests BASE = "https://api.reversecontact.com" headers = {"Authorization": f"Bearer {os.environ['RC_API_KEY']}"} # 1. Start the job: keep the Idempotency-Key to retry safely job = requests.post( f"{BASE}/v2/enrich/persons/bulk", headers={**headers, "Idempotency-Key": str(uuid.uuid4())}, json={ "name": "CRM export, November", "items": [ {"linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001"}, {"firstName": "John", "lastName": "Smith", "companyDomain": "acme.com", "externalId": "crm-002"}, ], "families": ["profile", "contact_email"], }, ).json()["data"]["job"] # 2. Follow it while it is queued or running while job["status"] in ("queued", "running"): job = requests.get( f"{BASE}{job['links']['self']}", headers=headers, params={"wait": 20}, timeout=30 ).json()["data"] # 3. Download the results if job["links"]["results"]: with open("results.csv", "wb") as file: file.write(requests.get(f"{BASE}{job['links']['results']}", headers=headers).content) print(job["status"], job["credits"]["used"], "credits") ``` ```typescript [TypeScript SDK] import { writeFile } from "node:fs/promises"; import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); // 1. Start the job: the SDK sends an Idempotency-Key and reuses it on its retries const created = await client.bulk.enrichPersons({ name: "CRM export, November", items: [ { linkedinUrl: "https://www.social.com/in/janedoe", externalId: "crm-001" }, { firstName: "John", lastName: "Smith", companyDomain: "acme.com", externalId: "crm-002" } ], families: ["profile", "contact_email"] }); const jobId = created.data.job.id; // 2. Follow it until it no longer moves const job = await client.bulk.wait(jobId); // 3. Download the results: bulk.results returns the text without its byte order mark, // so write "\uFEFF" before it for a spreadsheet to open accented names correctly if (job.data.links.results) { await writeFile("results.csv", "\uFEFF" + (await client.bulk.results(jobId))); } console.log(job.data.status, "credits:", job.data.credits.used); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: # 1. Start the job: the SDK sends an Idempotency-Key and reuses it on its retries created = client.bulk.enrich_persons( name="CRM export, November", items=[ {"linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001"}, {"firstName": "John", "lastName": "Smith", "companyDomain": "acme.com", "externalId": "crm-002"}, ], families=["profile", "contact_email"], ) job_id = created.data.job.id # 2. Follow it until it no longer moves job = client.bulk.wait(job_id) # 3. Download the results if job.data.links.results: with open("results.csv", "w", encoding="utf-8") as file: file.write(client.bulk.results(job_id)) print(job.data.status, "credits:", job.data.credits.used) ``` ## Handle the results `rc_status` says what happened to each row, and what to do with it: | `rc_status` | What to do | | ----------------------- | -------------------------------------------------------------------------- | | `enriched`, `partial` | Use the row. `partial` found part of what you asked, such as the profile but no email | | `not_found` | Nothing to retry: only a profile matched on the way to a finder was charged | | `duplicate` | Read the row of the earlier item that names the same person | | `insufficient_credits` | Top up, then send these items again in a new job | | `rate_limited`, `error` | Send these items again later: they cost nothing | | `skipped` | Not run: the item had nothing to look up, or the job was cancelled before it. Complete the item, or send it again | | `invalid_input` | Fix the item, such as an invalid Social profile URL, then send it again | | `data_subject_blocked` | Drop the person: they asked not to be processed under GDPR or CCPA | | `unknown` | The lookup may have run but its result was not saved. It is not run again on its own | | `running` | Only in the file of a cancelled job: the item was still running. It ends within the hour: download the file again for its outcome | To retry, filter the rows by `rc_status` and send their items in a new job, with a new `Idempotency-Key`. `externalId` ties each new row back to your file. ## Cancel a job and read the history [Cancel a job](https://docs.reversecontact.com/endpoints/job-cancel) stops a job an API key or an AI assistant started: items already in flight may finish and are charged as usual, and items not started are never charged and read `skipped`. The results of the items done stay downloadable. An item in flight ends within the hour: until then, its row of the file reads `running`, and a page of [Read job results by page](https://docs.reversecontact.com/endpoints/job-rows) waits for it. A job started by a person in the app or with Ask is stopped there. The [Job history](https://docs.reversecontact.com/endpoints/jobs) lists every job of your workspace, newest first, whoever started it: your Bulk enrichment jobs, the files enriched with Bulk Import in the app, the imports and enrichments of your Lists, and the Search exports. Filter it by `type`, `status` or List. ## Limits | Limit | Value | | -------------------------------------- | -------------------------------------------------------- | | Items per request | 25,000 | | Request body | 25 MB of JSON, above it `413 REQUEST_BODY_TOO_LARGE` | | Rate limits | Starting a job counts as one request, whatever its number of items. Its items then take at most 80% of your per-minute limit as they run, and wait when that share is taken | | Following a job | Free and never counted against your rate limits | | Downloading the results | Free, counted like any other call | | Reading the results by page | Up to 100 items and 2 MB of JSON per page. Free, each page counted like any other call | A larger file goes in several jobs. The API key needs the permission of what the job asks for: Person Enrichment when `families` includes `profile` (the default), Contact for `contact_email` and `contact_phone`. See [Rate limits](https://docs.reversecontact.com/guides/rate-limits#how-requests-are-counted) for how a job shares the per-minute limit with your other calls. ## FAQ ### How many people can I enrich in one Bulk enrichment request? `POST /v2/enrich/persons/bulk` takes up to 25,000 people per request, in a JSON body of at most 25 MB. Each person is found best from a Social profile URL, an email, or a first and last name with a company. Split a larger file into several jobs. ### How much does Reverse Contact Bulk enrichment cost? Each person costs what its unit call costs, only for what is found: 1 credit for a profile from a Social profile URL (2 with `live: true`), 2 from an email or a name, 3 for a professional email, 5 for a personal one and 20 for a phone number. The job reserves the most each person can cost when it starts and gives back what was not found. Starting, following, downloading and cancelling a job are free. ### What happens when my balance does not cover every person of a Bulk enrichment job? The job reserves credits person by person, in the order you sent them, as long as your balance covers them, and does not run the people it no longer covers: their rows read `insufficient_credits` in the results and cost nothing, and `notRunForCredits` counts them. Top up, then send those people again in a new job. ### How do I know when a Bulk enrichment job is finished? Read `GET /v2/jobs/:jobId?wait=20` while `status` is `queued` or `running`: each read waits up to 20 seconds and is free. The job is finished when `status` is `completed`, `cancelled` or `failed`. You can also pass a public HTTPS `webhookUrl` when you start the job: it receives a `job.updated` event once the job settles. The results file, CSV or NDJSON, is then at `GET /v2/jobs/:jobId/results`. To use the results before the job finishes, read them a page at a time with `GET /v2/jobs/:jobId/rows`. ### Should I use Bulk enrichment or a List? Use Bulk enrichment to enrich a file of people once and get it back as CSV: nothing is kept. Use a List to keep people or companies in your workspace, add to them over time, buy their data later and never pay twice for someone already on the List, with a `maxCredits` cap on each import. --- # Enrich a CSV file > Upload a CSV of contacts in the app and download it enriched, send its rows to the API in bulk or as a List import, or loop over them row by row. - Page: https://docs.reversecontact.com/guides/enrich-a-csv-file - Last verified: 2026-11-04 You have a file of contacts and want it back with profiles, work emails or phone numbers. There are four ways to do it: - **In the app, no code:** [Bulk Import](https://app.reversecontact.com/bulk) takes your CSV and gives you an enriched CSV back. - **With [Bulk enrichment](https://docs.reversecontact.com/guides/bulk-enrichment), through the API:** your code sends the rows of the file in one request, up to 25,000 people, and downloads the same enriched CSV as Bulk Import. - **With a List, through the API:** one [List](https://docs.reversecontact.com/guides/lists) import reads the file from its public URL, or takes its rows as JSON, finds each person, buys the data you ask for and keeps the result. - **With the API, row by row:** your code reads the file and calls an endpoint for each row. ## Bulk Import in the app 1. Open [Bulk Import](https://app.reversecontact.com/bulk) and drop a CSV of up to 25,000 rows and 25 MB. You can also paste a list of emails, Social profile URLs or names instead of uploading a file. 2. Map your columns: tell the import which columns hold the Social profile URL, the email, the first and last name, the company name and the company domain. Common headers such as `linkedin_url`, `email` or `profile_url` are detected for you, and you can change any mapping. 3. Choose what to look up. Each row can get its full profile, a work email and a phone number: select one or several. 4. Review the import and start it. The review step counts the rows that will be enriched, the duplicates and the rows with no usable input, and shows the credits the import reserves before it starts. 5. Follow the import row by row on its job page. When it completes, you receive an in-app notification and an email, and the enriched CSV is ready to download. A row needs at least one usable input: a Social profile URL, an email, or a name with its company. A row that repeats an earlier one is not enriched or charged twice. ### What the enriched file contains The result keeps every column of your file and adds these columns to each row: | Column | Content | | ------ | ------- | | `rc_status` | `enriched`, `partial`, `not_found`, `invalid_input`, `skipped`, `insufficient_credits`, `data_subject_blocked`, `rate_limited`, `error` or `unknown` | | `rc_message` | Why a row was not enriched, in plain English | | `rc_*` profile columns | The fields of the [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person) response, one column per field (`rc_first_name`, `rc_current_position_title`, `rc_location_city`...). List fields such as `rc_experience` hold JSON. | | `rc_email`, `rc_email_status` | The work email, when you asked for one | | `rc_phone`, `rc_phone_type`, `rc_phone_status` | The phone number, when you asked for one | `partial` means some of the requested lookups found a result and others did not. When you ask for contact details without the profile, a single `rc_linkedin_url` column replaces the profile columns. The file is UTF-8 and opens directly in Excel. ### What it costs You pay only for what is found. A row that is not found, invalid, a duplicate or skipped costs nothing, with one exception: when you ask for a work email or a phone number and the row has neither a Social profile URL nor a first name, a last name and a company domain, its profile is matched first and charged as a profile lookup when it is found, even if no email or phone number is. | Lookup | Credits per row | | ------ | :-------------: | | Profile from a Social profile URL | 1 when found | | Profile from a Social profile URL, with Live enrichment | 2 when found | | Profile from an email or a name | 2 when found | | Work email | 3 when found | | Phone number | 20 when found | If your balance does not cover every row, the import takes the rows it can afford in file order and marks the others `insufficient_credits`. Top up and upload those rows again to enrich them. Live enrichment applies to rows with a Social profile URL: it scrapes each profile fresh instead of reading the cached profile, and a large file can take several minutes. Bulk Import works during the free trial with your trial credits, Live enrichment included. ## Enrich a CSV with Bulk enrichment Bulk enrichment is Bulk Import through the API. Read the file in your code and send its rows as `items` to [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons), up to 25,000 per request: each item takes a Social profile URL, an email, or a first and last name with the company, plus the id of the row in `externalId`. Add `families` to ask for the profile, an email or a phone number, as in the app. The job runs in the background and costs what Bulk Import costs: the credits are reserved when it starts, and what an item does not find is given back. Follow it with [Get a job](https://docs.reversecontact.com/endpoints/job) or a `webhookUrl`, then download the enriched CSV with [Download job results](https://docs.reversecontact.com/endpoints/job-results): one row per item, in the order sent, with the `rc_` columns described above. An item that repeats an earlier one keeps its own row, which reads `duplicate`, where the app leaves the repeated row out of the file. Bulk enrichment keeps nothing once you have the file: to keep the people, grow the set and never pay twice for the same person, use a List. [Enrich people in bulk](https://docs.reversecontact.com/guides/bulk-enrichment) walks through the whole flow, with what to do with each `rc_status`. ## Enrich a CSV with a List A [List](https://docs.reversecontact.com/guides/lists) import does the loop for you. Create a people List, then send the public URL of the file, or of a Google Sheet, in `sourceUrl` to [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals): up to 25,000 rows and 25 MiB, with the columns matched from their headers (Social URL, email, first name, last name, company, domain). On a free trial, a List holds up to 50 members. Add `families` to buy a profile, a work email or a phone number for each person, and `maxCredits` to cap what the import may spend. The import runs in the background and never charges twice for a person already on the List. A row no one matches stays on the List as Pending Intake, for free, so you can see what was not found. Follow it at its `pollUrl`, then read the members or download them with [Export a List](https://docs.reversecontact.com/endpoints/list-export). A file that is not public can be sent as `signals` instead, one object per row, with the id of the row in `externalId`. [Build your first List](https://docs.reversecontact.com/guides/build-a-list) walks through the whole flow, and [Lists](https://docs.reversecontact.com/guides/lists) explains how a List works. ## Enrich a CSV with the API, row by row Read the file in your code and call, for each row, the endpoint that matches what the row holds: | The row holds | Endpoint | Credits | | ------------- | -------- | :-----: | | A Social profile URL | [`POST /v2/fetch/persons`](https://docs.reversecontact.com/endpoints/fetch-profile) | 1 when found | | An email, or a name with its company | [`POST /v2/enrich/persons`](https://docs.reversecontact.com/endpoints/enrich-person) | 2 when found | | You need a work email | [`POST /v2/contact/email`](https://docs.reversecontact.com/endpoints/contact-email) | 3 when found | | You need a phone number | [`POST /v2/contact/phone`](https://docs.reversecontact.com/endpoints/contact-phone) | 20 when found | The example below reads `contacts.csv`, enriches each row from its `email` column with `POST /v2/enrich/persons`, and writes `contacts_enriched.csv` with five profile columns added. A `404` means no match and costs nothing. A `429` means the per-minute rate limit is reached: the example waits the number of seconds in `error.details.retryAfter`, then retries the same row. **Enrich a CSV** ```javascript [JavaScript] // npm install csv-parse csv-stringify import { readFileSync, writeFileSync } from "node:fs"; import { parse } from "csv-parse/sync"; import { stringify } from "csv-stringify/sync"; const KEY = "YOUR_API_KEY"; const FIELDS = ["firstName", "lastName", "currentPositionTitle", "currentCompanyName", "linkedinUrl"]; async function enrich(email) { while (true) { const response = await fetch("https://api.reversecontact.com/v2/enrich/persons", { method: "POST", headers: { "Authorization": `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ email }) }); if (response.status === 429) { const { error } = await response.json(); await new Promise((resolve) => setTimeout(resolve, error.details.retryAfter * 1000)); continue; } if (response.status === 404) return null; if (!response.ok) throw new Error(`HTTP ${response.status}`); return (await response.json()).data.person; } } const rows = parse(readFileSync("contacts.csv"), { columns: true, bom: true }); for (const row of rows) { const person = row.email ? await enrich(row.email) : null; for (const field of FIELDS) row[field] = person?.[field] ?? ""; } writeFileSync("contacts_enriched.csv", stringify(rows, { header: true })); ``` ```python [Python] # pip install requests import csv import time import requests KEY = "YOUR_API_KEY" FIELDS = ["firstName", "lastName", "currentPositionTitle", "currentCompanyName", "linkedinUrl"] def enrich(email): while True: response = requests.post( "https://api.reversecontact.com/v2/enrich/persons", headers={"Authorization": f"Bearer {KEY}"}, json={"email": email}, ) if response.status_code == 429: time.sleep(response.json()["error"]["details"]["retryAfter"]) continue if response.status_code == 404: return None response.raise_for_status() return response.json()["data"]["person"] with open("contacts.csv", newline="", encoding="utf-8-sig") as source, \ open("contacts_enriched.csv", "w", newline="", encoding="utf-8") as target: reader = csv.DictReader(source) writer = csv.DictWriter(target, fieldnames=reader.fieldnames + FIELDS) writer.writeheader() for row in reader: person = enrich(row["email"]) if row["email"] else None for field in FIELDS: row[field] = (person or {}).get(field) or "" writer.writerow(row) ``` For a row with a Social profile URL, call `POST /v2/fetch/persons` with `{"url": ...}` instead: it costs 1 credit instead of 2. Work emails and phone numbers are asynchronous: send every row first, keep the `webhookId` of each request, then collect the results by [polling](https://docs.reversecontact.com/guides/polling) or through [webhooks](https://docs.reversecontact.com/guides/webhooks). See [Rate limits](https://docs.reversecontact.com/guides/rate-limits) to size a large file against your per-minute limit. ## FAQ ### Can I upload a CSV file to the Reverse Contact API? The API takes no file upload, but it takes the rows of a file in one request: send them as `items` to `POST /v2/enrich/persons/bulk`, up to 25,000 people, and download the enriched CSV once the job is completed. A List import also reads a CSV file or a Google Sheet from its public URL: send it in `sourceUrl` to `POST /v2/lists/:listId/intake/signals`, up to 25,000 rows and 25 MiB, and read the result in the List. To enrich a CSV without code, use Bulk Import in the app: upload the file, map its columns and download the enriched CSV. You can also read the file in your code and call `POST /v2/enrich/persons` or `POST /v2/fetch/persons` for each row. ### How many rows can Bulk Import enrich at once? Bulk Import accepts a CSV of up to 25,000 rows and 25 MB per import. Split a larger file into several imports. Each row needs a Social profile URL, an email, or a name with its company. ### How much does it cost to enrich a CSV file? Bulk Import charges only rows that return a result: 1 credit for a profile found from a Social profile URL, 2 credits for a profile found from an email or a name, 3 credits for a work email found and 20 credits for a phone number found. Rows that are not found, invalid or duplicated cost nothing, except a profile matched on the way to a work email or a phone number, which is charged as a profile lookup when it is found. ### What does the enriched CSV contain? The enriched CSV from Bulk Import keeps every column of the original file and adds `rc_status`, `rc_message` and one `rc_*` column per profile field, plus `rc_email` and `rc_phone` columns when a work email or a phone number was requested. `rc_status` tells whether each row was enriched, not found, invalid or skipped. ### What happens when my credits run out during a Bulk Import? Bulk Import reserves credits before the job starts. If the balance does not cover every row, Bulk Import enriches the rows it can afford in file order and marks the other rows `insufficient_credits` in the enriched CSV, without charging them. Top up and upload those rows again to enrich them. --- # Credits > Understand Reverse Contact API credit charges, free endpoints, async refunds and ways to reduce costs for profile lookups and searches. - Page: https://docs.reversecontact.com/guides/credits - Last verified: 2026-11-04 Most requests cost between 0 and 3 credits, depending on the endpoint. Find Email costs 3 credits when a professional email is found and 5 when a personal one is found (8 when a lookup finds both), and Find Phone costs 20 credits when a number is found. The people search endpoints cost 1 credit per profile delivered and Company search 1 credit per company delivered, plus 1 credit per page for Agentic Search and Agentic Company Search. Most errors and status checks are always free. The [API pricing](https://docs.reversecontact.com/pricing) page lists every endpoint, and [Rate limits](https://docs.reversecontact.com/guides/rate-limits) covers how many requests you can send. ## What costs 0 credits - Errors: all 4xx and 5xx responses cost 0 credits, except `404`, whose cost depends on the endpoint (see [API pricing](https://docs.reversecontact.com/pricing)). - Check and Usage endpoints: always 0 credits, so you can test the API and decide before you spend. > [!IMPORTANT] > **When your workspace has fewer than 2 credits left, the free Check endpoints are paused too.** A call to `POST /v2/fetch/persons/check` or `/v2/fetch/companies/check` then returns `402 NO_CREDITS` instead of a result, because the fetch that follows a check can cost 2 credits. Status and result endpoints stay open: `GET /v2/usage`, `GET /v2/flags`, the Search `available-fields` endpoints, webhook polling (`GET /v2/webhooks/:webhookId`) and the [job](https://docs.reversecontact.com/endpoints/job) routes under `/v2/jobs` keep working so you can still read your balance and retrieve already-paid results. [Top up →](https://app.reversecontact.com/settings/billing?tab=credits) ## Per-endpoint pricing The [API pricing](https://docs.reversecontact.com/pricing) page lists every endpoint with what it costs when it finds a result and when it does not, whether an async lookup that finds nothing is refunded, and where its data comes from. Its figures are the ones the API bills, and the same table is in your dashboard under [Billing > API pricing](https://app.reversecontact.com/settings/billing/api-pricing). Async endpoints bill differently: credits are charged when the request creates the job, then refunded automatically based on the outcome. A job that ends in error without data (`fetch-data-error`, `invalid-data`, `result-unavailable`, `invalid-request`, `data-subject-blocked`) is refunded, and so is a Find Email or Find Phone lookup that finds nothing. A live fetch that finds no profile is billed. Refunds are auditable in the [Webhook Events](https://app.reversecontact.com/logs/events) dashboard. > [!NOTE] > Webhook delivery failures are not refunded. When your webhook URL is invalid, returns an error, times out, or is unreachable (`webhook-url-invalid`, `webhook-url-errored`, `webhook-url-timeout`, `webhook-url-unreachable`), the credit is kept because the data was successfully fetched. The job is reported as `succeeded` with a `deliveryStatus` field naming the delivery problem, and the `result` is populated: poll `GET /v2/webhooks/:webhookId` to retrieve it. ## Plans and credits Credits work on every endpoint. On a Go, Plus or Max plan, the calls your plan covers cost 0 credits, up to the daily profile cap described in [Rate limits](https://docs.reversecontact.com/guides/rate-limits), and every other call costs credits. What each plan covers is on the [pricing page](https://www.reversecontact.com/pricing). ## Ask, MCP, and dashboard usage The same endpoint prices apply wherever a call comes from: - Ask, the assistant in the dashboard, charges nothing for its model turns. Its tool calls use the same endpoints as the REST API and consume the same endpoint credits. - MCP calls use the same credits as the REST API. - Polling and status-only endpoints such as `GET /v2/usage` and `GET /v2/webhooks/:webhookId` stay free. In the dashboard, the [Usage](https://app.reversecontact.com/settings/usage) page filters its charts and tables by channel: MCP, API & Integrations, or Bulk CSV. How each of these counts toward your per-minute limit is in [Rate limits](https://docs.reversecontact.com/guides/rate-limits#how-requests-are-counted). ## Save credits Call the free Check endpoint before you fetch, to see if a profile exists and when it was last updated. Then decide between a cached fetch (1 credit) and a live scrape (2 credits). [Data freshness](https://docs.reversecontact.com/guides/data-freshness) shows how to choose. For bulk discovery, use Search. [People search](https://docs.reversecontact.com/endpoints/search-people) returns full profiles and costs 1 credit per profile delivered, so size your query first with the free [count endpoint](https://docs.reversecontact.com/endpoints/search-people-count). [Company search](https://docs.reversecontact.com/endpoints/search-companies) works the same way for companies: full company profiles at 1 credit per company delivered, sized for free with its own [count endpoint](https://docs.reversecontact.com/endpoints/search-companies-count). On both, lower `perPage` to cap what a single request can spend. ## FAQ ### Am I charged for errors or 404 responses? Reverse Contact API responses with a 4xx or 5xx status cost 0 credits, except `404`, whose cost depends on the endpoint. The Reverse Contact API pricing page gives the cost of a `404` for every endpoint, in its Not found column. ### Where can I see what each Reverse Contact endpoint costs? The Reverse Contact API pricing page, at docs.reversecontact.com/pricing, lists every endpoint with its cost when it finds a result, its cost when it does not, whether an async lookup that finds nothing is refunded, and where its data comes from. The same table is in the dashboard under Billing > API pricing. ### Can I still call the API when my credits run out? When a Reverse Contact workspace has fewer than 2 credits left, even the free Check endpoints (`POST /v2/fetch/persons/check` and `/v2/fetch/companies/check`) return `402 NO_CREDITS`. `GET /v2/usage`, `GET /v2/flags`, the Search `available-fields` endpoints, webhook polling (`GET /v2/webhooks/:webhookId`) and the job routes under `/v2/jobs` keep working, so you can still read your balance, follow a Bulk enrichment job and retrieve already-paid results. ### Are credits refunded when an async request finds nothing? It depends on the endpoint. Reverse Contact async requests are charged at creation. Find Email and Find Phone refund the credits automatically when no email or phone number is found, and a job that ends in error without data (`fetch-data-error`, `invalid-data`, `result-unavailable`, `invalid-request`, `data-subject-blocked`) is refunded. A live fetch that finds no profile is billed. Webhook delivery failures are not refunded, because the data was successfully fetched, and refunds can be audited in the Webhook Events dashboard. --- # Rate limits > The requests per minute of each plan, the daily profile cap of the Go, Plus and Max plans, and how to handle a 429. - Page: https://docs.reversecontact.com/guides/rate-limits - Last verified: 2026-11-04 Every workspace has a rate limit: the number of requests per minute the API accepts from it, all API keys together. The limit depends on your plan. The Go, Plus and Max plans also cap the profiles they deliver each day. Past a limit, the API returns `429 Too Many Requests`, charges 0 credits, and tells you when to retry. ## Rate limit by plan | Plan | Requests per minute | To go higher | | ---- | ------------------- | ------------ | | Free trial | 50 | Buy credits or a plan: the limit moves to 100. | | Pay as you go | 100 | Take a Go, Plus or Max plan from [Billing](https://app.reversecontact.com/settings/billing), then add 200 or 500 as a paid monthly add-on. Or [contact sales](https://www.reversecontact.com/contact). | | Go, Plus and Max | 100 included | Add 200 or 500 requests per minute as a paid monthly add-on, from [Billing](https://app.reversecontact.com/settings/billing). | | Enterprise | Set in your contract, from 1,000 | [Contact sales](https://www.reversecontact.com/contact). | A workspace on an older credit subscription keeps the limit set with its subscription. Buying credits never lowers a limit you already have. Your current limit is shown on the [Billing](https://app.reversecontact.com/settings/billing) page and in `quotas.minuteRateLimit.limit`, returned with every successful response. ## How requests are counted - The count is per workspace: every API key, the [MCP server](https://docs.reversecontact.com/guides/mcp), the [CLI](https://docs.reversecontact.com/guides/cli) and the dashboard draw from the same limit. - The count runs per calendar minute and starts again at the top of each minute, given in `nextReset`. - A request counts once, whatever it costs. A free Check or count request counts. A search page counts once, however many profiles it returns, and so does the request that creates an async job. - A background job takes requests from the same limit as its lines run: a [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) job, a file enriched with Bulk Import in the app, a [List](https://docs.reversecontact.com/guides/lists) import or a Search export, whoever started it (an API key, an AI assistant through the MCP server, or the app). Its lines take at most 80% of the limit and wait when that share is taken, so your other calls always keep at least 20% of it. These requests do not count: - `GET /v2/usage`, `GET /v2/flags` and the Search `available-fields` endpoints - [People auto-suggestion](https://docs.reversecontact.com/endpoints/search-people-auto-suggestion). It has its own limit per workspace, 1,000 calls per minute or 500 on the free trial, and an API key's own limits do not apply to it - Polling an async job with `GET /v2/webhooks/:webhookId` - Reading a List operation with `GET /v2/lists/:listId/operations/:operationId/progress` - The request that starts a List import (`POST /v2/lists/:listId/intake/signals`), whatever its number of lines: it counts once on the daily limit only. Its lines then take their share of the per-minute limit as they run, like the lines of any job - Reading a job with `GET /v2/jobs/:jobId` - The callbacks we send to your `webhookUrl` - The model turns of Ask, the assistant in the dashboard (its tool calls do count, like any API call) A [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) request (`POST /v2/enrich/persons/bulk`) counts as one request, whatever its number of items. Its items then take their share of the per-minute limit as they run, at most 80% of it, as above. Send a large file in one request rather than one request per row: the job paces itself and leaves your other calls at least 20% of the limit. ## Daily profile cap of the Go, Plus and Max plans A Go, Plus or Max plan delivers at most 100,000 profiles per workspace per calendar day (UTC), as set out in the [Terms of Service](https://www.reversecontact.com/legal/terms-of-services). - The cap counts each profile your plan delivers at no credit cost, through the API, the MCP server, the CLI or the dashboard. A search page of 100 profiles counts 100, and the same profile delivered twice counts twice. A `404`, an empty page, a Check or a count request counts 0. - Calls paid in credits do not count: a live fetch, Find Email, Find Phone or any call your plan does not cover. They keep working once the cap is reached. - Past the cap, the calls your plan covers return `429` with the code `DAILY_EXTRACTION_LIMIT_EXCEEDED` until 00:00 UTC. `error.details.resetAt` gives that moment. A [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) job that runs under your plan is not refused: its items wait until 00:00 UTC. The rate limit add-on does not raise the cap. Free trial, Pay as you go and Enterprise workspaces have no profile cap. An Enterprise contract can set a daily request limit instead. `quotas.dailyLimit` shows it (`null` when there is none), and past it the API returns `429` with the code `DAILY_LIMIT_EXCEEDED` until the time in `nextReset`. ## Limits per API key You can cap one API key below the workspace limit, per minute or per day, for example to keep a test integration from using the whole budget. A request refused by its key's own limit does not count toward the workspace limit. See [API key management](https://docs.reversecontact.com/guides/api-key-management#_3-set-request-limits-optional). To cap what a key spends rather than how often it calls, give it credit limits, per day, per month or in total. A call over one of them returns `KEY_CREDIT_LIMIT_REACHED` and does not count toward any rate limit. See [API key management](https://docs.reversecontact.com/guides/api-key-management#_4-set-credit-limits-optional). ## Read your usage The `quotas` object of every successful response shows where you stand in the current minute: **Rate limit usage** ```json [JSON] "minuteRateLimit": { "limit": 100, "used": 98, "left": 2, "nextReset": "2026-10-26T09:31:00.000Z" } ``` `left` is what you can still send this minute, and `nextReset` is when the count starts again. [`GET /v2/usage`](https://docs.reversecontact.com/endpoints/usage) returns the same object without counting toward the limit. ## When you hit the limit The API returns `429 Too Many Requests` with the code `RATE_LIMIT_EXCEEDED`. The request costs 0 credits. **429 response** ```json [JSON] { "success": false, "data": null, "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Rate limit exceeded: 100 requests per 60s. Retry after 12s", "details": { "limit": 100, "retryAfter": 12 } } } ``` The response also carries two headers: `Retry-After`, the seconds to wait, and `X-RateLimit-Reset`, the moment the limit resets as a Unix timestamp. Wait the number of seconds in `error.details.retryAfter`, then retry. See [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries#when-to-retry). A List export has its own limit, separate from this one: [one at a time per workspace](https://docs.reversecontact.com/endpoints/list-export#response-structure). For a long job, such as a large file or a nightly sync, pace your requests below your limit rather than waiting for a `429`: at 100 requests per minute, leave at least 600 ms between two requests. Read `left` in `quotas.minuteRateLimit` to slow down before you reach 0. ## FAQ ### What is the Reverse Contact API rate limit? The Reverse Contact API accepts 50 requests per minute on the free trial and 100 on Pay as you go and on the Go, Plus and Max plans. A Go, Plus or Max plan can raise it to 200 or 500 with a paid monthly add-on, and an Enterprise contract sets its own limit, from 1,000 requests per minute. The limit applies per workspace, all API keys together. ### How do I get a higher rate limit? On a Go, Plus or Max plan, add 200 or 500 requests per minute as a paid monthly add-on from the Billing page of the Reverse Contact dashboard. The add-on comes with a plan: a Pay as you go workspace takes a Go, Plus or Max plan first. Above 500 requests per minute, or for a higher limit without a plan, contact Reverse Contact sales. ### What happens when I exceed the Reverse Contact rate limit? The Reverse Contact API returns `429 Too Many Requests` with the code `RATE_LIMIT_EXCEEDED` and charges 0 credits. `error.details.retryAfter` and the `Retry-After` header give the number of seconds to wait before retrying. ### Do webhook callbacks and polling count toward the rate limit? No. The callbacks Reverse Contact sends to your `webhookUrl`, polling `GET /v2/webhooks/:webhookId`, `GET /v2/usage`, `GET /v2/flags` and the Search `available-fields` endpoints do not count toward the per-minute rate limit. The request that creates an async job counts once. ### Is there a daily limit on the Reverse Contact API? The Go, Plus and Max plans deliver at most 100,000 profiles per workspace per calendar day (UTC), counted on the calls the plan covers. Past that cap, those calls return `429` with `DAILY_EXTRACTION_LIMIT_EXCEEDED` until 00:00 UTC, while calls paid in credits keep working. Free trial and Pay as you go workspaces have no daily cap. --- # API key management > Create and manage Reverse Contact API keys with permission scopes, request and credit limits, expiration dates, and key revocation. - Page: https://docs.reversecontact.com/guides/api-key-management - Last verified: 2026-11-04 Your API keys work as soon as you create them, with full access to every endpoint. Permissions, rate limits and expiration dates are optional settings you can add as your integration grows. > [!TIP] > Don't have an API key yet? Start with [Authentication](https://docs.reversecontact.com/authentication). ## 1. Create a key 1. Open [API Keys](https://app.reversecontact.com/api-keys) in the dashboard. 2. Click **Create API Key** and give it a name (e.g. `prod-backend`, `staging-sync`). 3. Copy and store it securely. ## 2. Fine-tune permissions (optional) By default, every key has full access. As your team grows, you can scope each key to specific capabilities: | Scope | What it covers | | ----------------------- | ------------------------------------------------------------------- | | Person Enrichment | Enrich person profiles from Social URLs, emails or names | | Company Enrichment | Enrich company profiles from Social URLs or domains | | Search | Search people and companies, including lookalikes and Agentic Search for both | | Contact (Email & Phone) | Find professional email addresses and phone numbers | | Lists | Create [Lists](https://docs.reversecontact.com/guides/lists) and manage their members. A List call that spends credits also needs the scope of what it buys: Person Enrichment for new people and their profiles, Company Enrichment for new companies and company details, Contact for emails and phone numbers | Select only the scopes your integration needs, or leave them all checked for full access. [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) has no scope of its own: it needs Person Enrichment when it looks up profiles (`families` with `profile`, the default) and Contact when it looks up emails or phone numbers. The [job history](https://docs.reversecontact.com/endpoints/jobs) shows a key the jobs its scopes allow. > [!NOTE] > Free endpoints like `GET /v2/usage` are always accessible, regardless of permissions. The List endpoints are free too, but need the Lists scope. If a key tries to call an endpoint outside its scopes, the API returns a `403` status code. ## 3. Set request limits (optional) Per-key request limits cap how many requests an integration sends: | Setting | Description | | ------------------- | ----------------------------------------- | | RPM limit | Maximum requests per minute for this key | | Daily request limit | Maximum requests per day for this key | If you don't set any, your workspace defaults apply. A per-key limit can only cap usage below the workspace maximum, never above it. When a per-key request limit is reached, the API returns `429 Too Many Requests`. ## 4. Set credit limits (optional) A request can cost from 0 to several credits depending on the endpoint, so a request limit does not bound what a key spends: credit limits do. Each key accepts three, all optional and cumulative: | Setting | What it caps | Resets | | --------- | --------------------------------------------- | ------------------------------------------------ | | Per day | Credits the key spends per calendar day (UTC) | Every day at 00:00 UTC | | Per month | Credits the key spends per calendar month (UTC) | On the 1st of each month at 00:00 UTC | | Total | Credits the key spends from the moment you set the limit | Never on its own: raise it, remove it, or click **Reset** next to it | Leave a field empty for no limit. The limits count the credits the key spends on every channel that uses it: your API calls, the [MCP server](https://docs.reversecontact.com/guides/mcp), the [CLI](https://docs.reversecontact.com/guides/cli) and the tools you connect with the key. Credits given back, for example when a live lookup finds nothing, are given back to the limits too. Calls that cost 0 credits, such as `GET /v2/usage` or a Check, never count and are never refused. A call that would take the key over any of its limits is refused before it runs, so nothing is charged. A Bulk enrichment job is held as a whole: the credits it reserves must fit the limits, and once it settles, the key keeps only what it spent. The API returns the code `KEY_CREDIT_LIMIT_REACHED`: - Per day or per month: `429 Too Many Requests`, with `error.details.retryAfter`, the `Retry-After` header and `error.details.resetAt` giving when the limit reopens. - Total: `403 Forbidden`, without a reset time, since the limit only reopens when someone raises or resets it. **Credit limit reached** ```json [JSON] { "success": false, "data": null, "error": { "code": "KEY_CREDIT_LIMIT_REACHED", "message": "This API key reached its daily credit limit: 1,990 of 2,000 credits used, 25 required. It resets at 00:00 UTC.", "details": { "period": "day", "limit": 2000, "used": 1990, "required": 25, "retryAfter": 21600, "resetAt": "2026-10-29T00:00:00.000Z" } } } ``` `error.details.period` is `day`, `month` or `total`, and `required` is what the refused call would have cost. Every response made with the key shows where it stands in `quotas.key.creditLimits`, the call itself included: **Credit limits in a response** ```json [JSON] "creditLimits": { "daily": { "limit": 2000, "used": 1940, "left": 60, "nextReset": "2026-10-29T00:00:00.000Z" }, "monthly": null, "total": { "limit": 100000, "used": 12400, "left": 87600, "nextReset": null } } ``` A limit the key does not have is `null`. The **Restrictions** column of the [API Keys](https://app.reversecontact.com/api-keys) table shows the limit each key is closest to reaching, and the edit window shows what the key spent today, this month and against its total. ## 5. Add an expiration date (optional) An expiration date suits temporary access, such as a key for a contractor, a demo integration or a test environment. After the date you choose, the key stops working automatically. You can change or remove the expiration anytime by editing the key. ## Manage existing keys To edit a key, click the edit icon on any active key and update its name, permissions, request limits, credit limits or expiration. Changes take effect immediately. Raising or lowering a total credit limit keeps what the key already spent, and Reset starts it over at 0. A revoked key stops working immediately. It stays visible in the table for reference but cannot be reactivated. When in doubt, create a new key first, update your integration, then revoke the old one. ## Quick tips - Use one key per environment (dev, staging, production), so you can limit or revoke one without touching the others. - Give each key a name that says where it runs, such as `prod-crm-sync` or `staging-enrichment`, so you can tell keys apart when you audit them. - Start with full access, and add scopes later as your team and integrations grow. ## Something not working? | Symptom | Fix | | ------- | --- | | `403 Forbidden` | With `INSUFFICIENT_PERMISSIONS`, the key is missing a required permission: edit it in [API Keys](https://app.reversecontact.com/api-keys). `WORKSPACE_DISABLED` (suspended workspace) does not come from the key | | `429 Too Many Requests` | A per-key request limit was reached, or a daily or monthly credit limit (`KEY_CREDIT_LIMIT_REACHED`). Raise it or wait for the reset in `error.details.retryAfter` | | `403 Forbidden` (`KEY_CREDIT_LIMIT_REACHED`) | The key reached its total credit limit. Raise or reset it in [API Keys](https://app.reversecontact.com/api-keys) | | `401 Unauthorized` (`API_KEY_EXPIRED`) | The key passed its expiration date. Create a new one | For all error codes and retry strategies, see [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries). ## FAQ ### Do Reverse Contact API keys have full access by default? Yes. By default, every Reverse Contact API key has full access to all endpoints, with no setup needed. You can optionally scope a key to Person Enrichment, Company Enrichment, Search, Contact (Email & Phone) or Lists, and free endpoints like `GET /v2/usage` are always accessible regardless of permissions. ### Why does my API key return 403 Forbidden? A `403` has two possible causes. With `INSUFFICIENT_PERMISSIONS`, the Reverse Contact API key tried to call an endpoint outside its scopes: edit the key in [API Keys](https://app.reversecontact.com/api-keys) to add the missing scope, and the change takes effect immediately. `WORKSPACE_DISABLED` (a suspended workspace) does not come from the key: see [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries). ### Can I set a spending limit on a single API key? Yes. Each Reverse Contact API key accepts three optional credit limits: per day (UTC), per month (UTC) and in total. A call that would take the key over one of them is refused before it runs and charges nothing, with the code `KEY_CREDIT_LIMIT_REACHED`: `429` for the daily and monthly limits, `403` for the total limit. Each key can also have request limits, per minute and per day, below the workspace maximum. ### What happens when an API key expires? After its expiration date, a Reverse Contact API key stops working automatically and calls return `401 Unauthorized` with the code `API_KEY_EXPIRED`, and the fix is to create a new key. You can change or remove a key's expiration anytime by editing the key. ### Can I reactivate a revoked API key? No. A revoked Reverse Contact API key stops working immediately and stays visible in the API Keys table for reference, but cannot be reactivated. When in doubt, create a new key first, update your integration, then revoke the old one. --- # Data quality > How we decide what to return, what each response tells you about its own reliability, and how to trade match rate against certainty. - Page: https://docs.reversecontact.com/guides/data-quality - Last verified: 2026-11-03 The Reverse Contact API returns no record rather than a doubtful one, because a wrong profile written to your CRM gets copied, reported on, and emailed before anyone notices, while a missing profile is a gap you can fill later. Every response also carries signals about its own reliability, and you can tune the balance between match rate and certainty for your use case. ## A not-found is a real answer When we cannot identify a person or a company with confidence, we say so. You get a `404`, or a documented error code on the async endpoints, instead of a plausible-looking record assembled to fill the response. The rest of the API follows the same rule: filters that we cannot honor are rejected instead of silently dropped, a job that failed reports why, and a profile removed by its owner stays removed. In each case you get a gap you can act on, instead of a record that looks right and gives you no reason to check it. ## Three questions behind every response Every record you receive answers three separate questions, and each one has its own signal in the payload. 1. Identity resolution: is this the right person? 2. Freshness: is this still true? 3. Deliverability: can I act on it? A response can score well on one and poorly on another: a profile can be the right person and eighteen months out of date, and a perfectly formed email address can belong to someone who left the company last year. Read the three signals separately. ## 1. Is this the right person? ### Two ways to identify someone The API has two families of endpoints, and they take different inputs. | Family | Endpoints | What you send | | ------ | --------- | ------------- | | Fetch | [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile), [Person profile status](https://docs.reversecontact.com/endpoints/check-person), [Person profile live](https://docs.reversecontact.com/endpoints/fetch-profile-live) | A Social profile URL, or a Reverse Contact `id` (`prs_...`) | | Enrich | [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person), [Enrich company](https://docs.reversecontact.com/endpoints/enrich-company) | Attributes: `email`, `firstName`, `lastName`, `companyDomain`, `companyName`, or a company `domain` | Fetch assumes you already know which profile you want and gives you its content. Enrich does the identity resolution: it takes the attributes you have and decides who they point to. Everything below about ambiguity applies to Enrich. [Find Email](https://docs.reversecontact.com/endpoints/contact-email) sits in between: it accepts a Social profile URL, or a full name with a company domain. ### Match rate and certainty pull in opposite directions Every field you add to an Enrich request narrows the search: certainty goes up and the match rate goes down. | What you send | Match rate | Certainty it is your person | | ------------- | ---------- | ----------------------------- | | `firstName` + `lastName` | Highest. Almost always returns someone | Lowest. There are hundreds of people with a common name | | `firstName` + `lastName` + `companyName` | High. Company labels are written many ways | Moderate. Company names are not unique | | `firstName` + `lastName` + `companyDomain` | Lower. Only matches if that person is known at that domain | High | | `email` | Lower. Only matches if we know that address | Highest. One address points to one person at one company | The right input depends on what a wrong match costs you: - For automatic CRM writes, outbound sequences and anything a customer sees, prefer certainty: send `companyDomain` or the email, and accept more `404`s. - For coverage work reviewed by a human, or a first pass you intend to refine, a looser query is fine: get the candidates, then decide. The same logic applies to [Find Email](https://docs.reversecontact.com/endpoints/contact-email): a name-and-domain lookup can resolve to the wrong person, and the address it then discovers is a correct address for the wrong person. ### A Social URL is not a permanent identifier Do not use a profile URL as a primary key. The person who owns the profile chooses its public slug and can change it whenever they like. When they do, the URL you stored six months ago points at nothing, and your call comes back as `404`, or as `422 INVALID_LINKEDIN_URL` if the stored value is malformed. Store our identifiers instead: - The `id` (`prs_...` for a person, `com_...` for a company) is stable across slug changes. Every response that returns a person or a company carries it. - Both [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) and [Person profile status](https://docs.reversecontact.com/endpoints/check-person) accept `id` in place of `url`, and `id` wins when you send both. - [Person profile live](https://docs.reversecontact.com/endpoints/fetch-profile-live) resolves the `id` to the current public slug before it starts, so a live refresh keeps working after a rename. Keep the URL for display and for humans. Key your records on `id`. ### Use `alternativePersons` as an ambiguity flag When several people match your input, [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person) returns the best match in `data.person` and the other candidates in `data.alternativePersons`, ordered best first, up to nine. Use that array as a confidence signal: - Empty: one person fits what you sent, and the record is safe to write automatically. - Not empty: several people fit. Add an identifying field and retry, or route the record to human review before it reaches a CRM or a sequence. **Ambiguity gate** ```javascript [JavaScript] const { data } = await enrichPerson({ firstName, lastName, companyDomain }); if (data.alternativePersons.length > 0) { // Several people match this input. Do not write automatically. await queueForReview(data.person, data.alternativePersons); } else { await writeToCrm(data.person); } ``` Each alternative is a full profile, the same shape as `data.person`, so you can review candidates side by side without an extra Profile call. Their `currentCompanyId` is always `null`: to fetch the company of a candidate, read the `companyId` of its `currentPosition`. ### How the current position is chosen A profile can list several jobs with no end date: a main role, a board seat, an advisory gig. `currentPosition` is the one we single out, and we pick it with fixed, published tie-breakers rather than a heuristic that drifts between calls. The `experience` and `education` arrays are ordered by the same fixed rules, whether the record came from our database or from a live fetch. When a title looks wrong, the cause is almost always a missing or outdated date in the source profile. [Current position and work history ordering](https://docs.reversecontact.com/guides/current-position-resolution) walks through exactly how the choice is made. ## 2. Is this still true? Professional data goes stale: people change jobs, companies rename, teams get restructured. Every record tells you when it was last refreshed. | Where you look | Field | What it means | | -------------- | ----- | ------------- | | [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person), [People search](https://docs.reversecontact.com/endpoints/search-people) | `updateDate` | When we last refreshed that record | | [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile), [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) | `metadata.updatedAt` | Last upstream update, present only when the response was served from cache | | [Person profile status](https://docs.reversecontact.com/endpoints/check-person), [Company profile status](https://docs.reversecontact.com/endpoints/check-company) | `lastUpdate` | When we last fetched the profile from the source, `null` when it does not exist | You can act on freshness in three ways: - Read it before you spend: [Person profile status](https://docs.reversecontact.com/endpoints/check-person) costs 0 credits and returns `exists` plus `lastUpdate`, along with `experienceCount`, `skillCount` and `schoolCount` so you can spot thin records too. - Filter it at the source: [People search](https://docs.reversecontact.com/endpoints/search-people) and [Company search](https://docs.reversecontact.com/endpoints/search-companies) accept `maxDataAgeDate`, an ISO 8601 datetime that returns only profiles refreshed after that date. Prefer it over filtering stale rows in your own code after you paid for them. - Refresh on demand: the live endpoints pull a fresh copy when the stored record is too old for your use case. Pick an explicit threshold per use case instead of treating every record the same: | Use case | Suggested threshold | If older | | -------- | ------------------- | -------- | | Outbound sequence, meeting prep | 30 days | Refresh live before use | | CRM enrichment at scale | 90 days | Refresh in your next batch | | Analytics, market sizing | 180 days | Usually fine as is | [Data freshness](https://docs.reversecontact.com/guides/data-freshness) covers the Check → Fetch → Live pattern and what it saves. ## 3. Can I act on it? [Find Email](https://docs.reversecontact.com/endpoints/contact-email) returns one entry per address discovered, each with a `value` and a `type`, typically `professional`. It is asynchronous: you get a `webhookId`, then retrieve the result by [polling](https://docs.reversecontact.com/guides/polling) or through a [webhook callback](https://docs.reversecontact.com/guides/webhooks). When nothing is found you receive `email-not-found` rather than a constructed address. Two habits protect your sender reputation whatever the source of an address: - Keep your own suppression list authoritative: unsubscribes, complaints and hard bounces you have already recorded always win over a freshly discovered address. - Track bounce rate per input type. If one segment bounces above your baseline, the input that produced it is usually the problem rather than the addresses themselves. Keep separate counters for name-based and URL-based lookups, since a name-and-domain lookup can resolve to the wrong person. > [!NOTE] > Finding an address does not mean you may contact it. Consent and applicable regulations remain yours to handle: the API tells you what exists, not whom you may email. ## Filters that fail loudly A bad search filter can return bad data with no visible sign. If we ignored a field we did not recognize, you would receive a full page of results drawn from a query you never asked for, and nothing in the response would look wrong. So the search endpoints validate every filter before the query runs. A rejected request returns `422`, lists every problem in `error.details.errors`, and costs 0 credits. Unknown field names, blank values, values outside a closed list such as `currentCompanyIndustry`, and inverted ranges are all refused rather than dropped. See [Search filter validation](https://docs.reversecontact.com/guides/errors-retries#search-filter-validation) for the complete list. Two behaviors affect how you read search results: - Title matching is token-based, not exact: `"CTO"` also matches `"Deputy CTO"`. Multiple titles are OR-matched, and `excludeCurrentPositionTitle` always wins over a match. - Headcount is stored as fixed brackets. A range only matches a bracket it fully contains, so bounds that fall inside a bracket return an empty array. That result costs 0 credits, but it looks like a real "no results": send bracket boundaries. ## When a profile is blocked A person or company blocked under a GDPR or CCPA data subject request returns `451`. Profile and company lookups answer `451` whichever input names them: profile URL, email, internal id, domain or name. Find Email and Find Phone answer `451` when given the profile URL or internal id. That state is permanent: unlike a stale or missing record, it will not come back. Retrying wastes calls, so treat `451` as a terminal outcome in your pipeline and drop the record from your enrichment queue rather than leaving it to be picked up on the next pass. ## Putting it together For a pipeline that writes to a system of record, this is the shape we recommend: **Decision tree** ```text [Text] For each lead: 1. Enrich with the most identifying fields you have │ ├─ 404 (no confident match) │ └─ Retry with fewer fields → route the result to review │ └─ Match │ ├─ alternativePersons not empty? │ └─ Route to human review, do not write │ └─ Unique match │ ├─ Store the id (prs_...) and updateDate │ └─ updateDate outside your threshold? └─ Refresh live by id, then write ``` The ambiguity gate and the freshness gate are the two checks that matter: skip either one and wrong data reaches your CRM unnoticed. ## Quality checklist - Use Fetch when you already know the profile, and Enrich when you are still resolving who it is. - Key your records on our `id`, not on a Social URL that its owner can rename. - Never write a record with a non-empty `alternativePersons` array without review. - Choose your input mix deliberately: certainty for automated writes, coverage for reviewed work. - Store the freshness timestamp next to every record so you can audit staleness later. - Set a staleness threshold per use case, filter with `maxDataAgeDate` on search, and refresh live above it. - Treat `404` as a normal outcome to log and `451` as terminal, not as errors to retry blindly. - Keep your suppression list ahead of any discovered address. - Track match rate and bounce rate per input type. They tell you which inputs to improve. ## FAQ ### Why does the Reverse Contact API return a 404 instead of a best guess? When Reverse Contact cannot identify a person or a company with confidence, the API returns a `404`, or a documented error code on the async endpoints, instead of a plausible-looking record assembled to fill the response. A wrong profile written to a CRM gets copied, reported on and emailed before anyone notices, while a missing profile is a gap you can fill later. ### Should I send a name, a company domain or an email to Enrich profile? Match rate and certainty pull in opposite directions in Enrich profile: `firstName` plus `lastName` almost always returns someone but gives the lowest certainty, adding `companyDomain` gives high certainty, and an `email` gives the highest certainty with a lower match rate. For automatic CRM writes, outbound sequences or anything a customer sees, send `companyDomain` or the email and accept more `404`s. ### What should I do when Enrich profile returns several matching people? When several people match the input, Enrich profile returns the best match in `data.person` and the other candidates in `data.alternativePersons`, ordered best first, up to nine. An empty `alternativePersons` array means one person fits and the record is safe to write automatically. A non-empty array means you should add an identifying field and retry, or route the record to human review before it reaches a CRM or a sequence. ### Should I store Social URLs or Reverse Contact ids? Key your records on the Reverse Contact `id` (`prs_...` for a person, `com_...` for a company), which is stable across slug changes, and keep the Social URL for display. A profile owner can change their public slug whenever they like, and a stored URL then returns `404`, or `422 INVALID_LINKEDIN_URL` if the stored value is malformed. ### What does a 451 response mean? A `451` from the Reverse Contact API means the person or company is blocked under a GDPR or CCPA data subject request. That state is permanent, so treat `451` as a terminal outcome and drop the record from your enrichment queue instead of retrying. --- # Data freshness > Choose the right endpoint mode, Check, Fetch or Live, for each profile lookup, based on the data freshness you need and the credits you want to spend. - Page: https://docs.reversecontact.com/guides/data-freshness - Last verified: 2026-10-15 A profile lookup runs in one of three modes, Check, Fetch or Live, and the mode you pick sets both the data freshness you get and the credits you pay. ## Three modes | Mode | Endpoint pattern | What you get | Credits | Speed | | ---- | ---------------- | ------------ | :-----: | :---: | | Check | [`*/check`](https://docs.reversecontact.com/endpoints/check-person) | Whether the profile exists and when it was last updated | 0 | Instant | | Fetch | [`/v2/fetch/*`](https://docs.reversecontact.com/endpoints/fetch-profile) | Cached profile data | 1 | Instant | | Live | [`*/live`](https://docs.reversecontact.com/endpoints/fetch-profile-live) | Fresh data delivered via webhook | 2 | Async | ## Optimize your requests Call Check first, then Fetch or Live depending on its answer: **Decision tree** ```text [Text] 1. Call CHECK (free) │ ├─ Profile not found? │ └─ Call LIVE (2 credits) → fresh data via webhook │ └─ Profile found │ ├─ Updated recently (< your threshold)? │ └─ Call FETCH (1 credit) → instant cached data │ └─ Data is stale? └─ Call LIVE (2 credits) → fresh data via webhook ``` This Check → Fetch → Live pattern can cut your costs by up to 50% if many of the profiles you query are already fresh in our database. ## When to use each mode ### Check (0 credits) - Before every fetch/live call in a batch pipeline - To monitor data freshness across your lead list - To build a queue that prioritizes stale profiles for live refresh ### Fetch (1 credit) - When the profile exists and was updated within your acceptable freshness window - For high-volume pipelines where real-time accuracy isn't critical - As the default mode. Fall back to Live only when Check shows stale data ### Live (2 credits) - When the profile doesn't exist in our database yet - When cached data is too old for your use case - For high-stakes enrichment (e.g. before a sales call or outreach campaign) ## Example: batch pipeline A pipeline that enriches 1,000 profiles at minimum cost looks like this: **Batch pipeline** ```text [Text] For each profile URL: 1. POST /v2/fetch/persons/check (free) 2. If exists + updated < 30 days: POST /v2/fetch/persons (1 credit) 3. Else: POST /v2/fetch/persons/live (2 credits, webhook) ``` If 60% of profiles are fresh in our database, the cost for 1,000 profiles is: - 600 × 1 credit (Fetch) = 600 - 400 × 2 credits (Live) = 800 - Total: 1,400 credits (vs 2,000 if using Live for all) ## FAQ ### What is the difference between Check, Fetch and Live? In the Reverse Contact API, Check costs 0 credits and instantly tells you whether a profile exists and when it was last updated. Fetch costs 1 credit and returns cached profile data instantly, while Live costs 2 credits and delivers fresh data asynchronously. ### When should I use Live instead of Fetch? Use Live (2 credits) when the profile does not exist in the Reverse Contact database yet, when the cached data is too old for your use case, or for high-stakes enrichment such as before a sales call or outreach campaign. Fetch (1 credit) is the default mode, with a fallback to Live only when Check shows stale data. ### How can I reduce the credits I spend on profile enrichment? Call the free Check endpoint first, then use Fetch (1 credit) if the profile exists and was updated within your threshold, or Live (2 credits) if the profile is missing or stale. This Check → Fetch → Live pattern can cut costs by up to 50% if many of the profiles you query are already fresh in the Reverse Contact database. ### How many credits does it take to enrich 1,000 profiles? With the Check → Fetch → Live pattern and 60% of profiles fresh in the Reverse Contact database, 1,000 profiles cost 1,400 credits: 600 × 1 credit for Fetch plus 400 × 2 credits for Live, with free Check calls. Using Live for all 1,000 profiles would cost 2,000 credits. --- # Missing and empty profile fields > Interpret missing profile fields, null values, empty lists and zero in Reverse Contact responses, and decide when a full profile or refresh helps. - Page: https://docs.reversecontact.com/guides/missing-and-empty-fields - Last verified: 2026-09-28 A blank field does not automatically mean zero. It can mean the result did not include that field, the source supplied no usable value, the returned collection is empty, or Reverse Contact does not collect that kind of data at all. Keep these cases separate in Ask answers, Lists, exports and your own systems. Reverse Contact never fills an unknown value with a guess. ## Check the result shape first Different lookups return different sets of fields. A light person result contains identity and current-role information. Photos, connection counts and work history belong to a full person profile, so those keys can be absent from a light result. [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person) returns the full profile by default. Check `data.isFullProfile` before reading those fields: when it is `false`, only the light shape could be returned, and the [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) endpoint returns the full profile from the returned `id` or `linkedinUrl`. A missing key in the light shape says nothing about the person's source profile. ## Read each empty-looking value correctly | What you receive | What you know | What you must not assume | | ---------------- | ------------- | ------------------------ | | A field is outside the result shape | This lookup did not return that field | The value is empty or zero | | `null` | No usable value was returned for this collected field | The exact reason, or that the value is `0` | | `[]` | The returned collection contains no entries | That entries exist elsewhere, or why none were returned | | `0` | The returned numeric value is zero | That another `null` or blank field also means zero | | No field exists for that kind of data | Reverse Contact does not collect it in profile enrichment | That a later refresh will add it | A `null` value carries no per-field reason code. The source may not provide the value, but the response does not prove whether it was omitted, unavailable at collection time or unusable for another reason. Preserve `null` as unknown unless another field or error explicitly explains it. Use a field's documented contract when it gives `null` a specific meaning. For example, `end: null` in an employment date range means that the position is current. Do not extend that meaning to unrelated fields. ## Common fields ### Connection count `connectionsCount` is a number or `null` on a full person profile. Preserve a returned `0` as zero and preserve `null` as unknown. Never store, display or average `null` as `0`. The field is not part of a light person result. If Ask or a List has only the light shape, request the full profile before deciding that the count is unknown. ### Profile photo `photoUrl` is a URL or `null` on a full person profile. A blank avatar can therefore mean that the full profile was not delivered or that the full result returned `null`. Neither case explains why a photo URL was unavailable. ### Work history `experience` is a list on a full person profile. An empty list means that the returned work-history collection has no entries. It does not identify a collection failure or prove a specific reason. Fields inside an employment entry can be independently `null`, including the description, contract type, company identifiers, location and dates. Keep the available parts of the entry instead of discarding the whole position. See [Current position and work history ordering](https://docs.reversecontact.com/guides/current-position-resolution) for ordering rules. ### Optional company fields Company fields such as `websiteUrl`, `industry`, `description`, `tagline`, `logoUrl`, `headquarter`, `foundedOn`, `phone`, `followersCount` and `employeesCount` can be `null`. Treat each one as unknown without guessing a cause. An exact `employeesCount` can be `null` while `employeeCountRange` is present. Keep the range as a range; do not invent an exact employee count from it. Lists such as company locations can be empty even when other company fields are present. ## Data that profile enrichment does not collect Profile enrichment does not collect posts or publications, comments, reactions or other interactions. Those fields are not temporarily blank and a refresh will not add them. We retired activities without a replacement, to focus the product on professional identity and company data. Email addresses and phone numbers also use dedicated lookups rather than profile fields. A "not found" result from one of those lookups is different from a profile field returning `null`. ## Decide whether to refresh An empty-looking value belongs to the version of the profile that was returned. A live refresh collects the profile again but does not guarantee that an unknown field will become available, so check the record's age before spending credits on one. Use [Data freshness](https://docs.reversecontact.com/guides/data-freshness) to choose between Check, cached Fetch and Live. Review [Credits](https://docs.reversecontact.com/guides/credits) before a paid lookup. ## Handle the result in Ask and Lists - In Ask, request a full profile when you need photos, connection counts or work history. Ask can explain a returned value, but a `null` carries no reason code, so the response cannot tell Ask why a field is empty. - In Lists, keep blank cells as unknown. Enrich the relevant data family when the field has not been collected for that row, and review the quote before confirming. - In exports and integrations, preserve `null`, empty lists and zero as different values. Do not replace all three with an empty string or zero. - For failures, tell a successful profile with missing fields apart from a request or lookup error. See [Error handling and retries](https://docs.reversecontact.com/guides/errors-retries). For shorter answers about empty fields, privacy and common errors, see the [Product FAQ](https://app.reversecontact.com/docs/product/faq). ## FAQ ### Does null mean zero in Reverse Contact profile data? No. In Reverse Contact profile data, `null` means no usable value was returned for that field and carries no per-field reason code, so preserve it as unknown, while a returned `0` is a real zero. The exception is a field whose documented contract gives `null` a specific meaning, such as `end: null` in an employment date range, which means that the position is current. ### Why are photoUrl and connectionsCount missing from a person result? Photos, connection counts and work history belong to the full person profile, so `photoUrl`, `connectionsCount` and `experience` can be absent from a light person result, and a missing key says nothing about the person's source profile. Enrich profile returns the full profile by default: check `data.isFullProfile`, and when it is `false`, the Person profile endpoint returns the full profile from the returned `id` or `linkedinUrl`. See [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile). ### Why is employeesCount null when employeeCountRange is present? An exact `employeesCount` can be `null` while `employeeCountRange` is present on a Reverse Contact company result. Keep the range as a range and do not invent an exact employee count from it. ### Can I get a person's posts, comments or reactions through profile enrichment? No. Reverse Contact profile enrichment does not collect posts or publications, comments, reactions or other interactions, so those fields are not temporarily blank and a refresh will not add them. Reverse Contact retired activities without a replacement, to focus on professional identity and company data. ### Will a live refresh fill in a null field? Not necessarily. A live refresh collects the profile again, but it does not guarantee that an unknown field will become available, so check the record's age before spending credits on a live refresh. See [Data freshness](https://docs.reversecontact.com/guides/data-freshness). --- # Current position and work history ordering > Understand the fixed rules we use to pick a person's current job and to order their work experience and education. - Page: https://docs.reversecontact.com/guides/current-position-resolution - Last verified: 2026-09-14 A person's profile can list many jobs and several schools, but the API always returns them in a predictable shape: one job is singled out as the `currentPosition`, and the `experience` and `education` arrays follow a fixed order. Two rules decide both, the same for every profile we return. ## Why it is not obvious Career histories are messy: - Someone may list several jobs with no end date (a main role, a board seat, an advisory gig), so "the one that is still open" is not always enough on its own. - Dates can be missing or vague. - Job titles come from different data sources that do not always agree. So we apply a fixed set of tie-breakers, always in the same order. ## How the current position is chosen We ask up to four questions, in order, and stop at the first one that gives an answer: 1. Do we already know which company they currently work at? If yes, look only at their jobs at that company and pick the one they started most recently. 2. Otherwise, do we at least know the company name? If yes, match jobs by company name instead, and again pick the most recently started one. Some data sources give us a name but not a precise company identifier. 3. Otherwise, which jobs have no end date? Among the jobs that are still open, pick the one they started most recently. 4. As a last resort, when nothing above applies, pick whichever job they started most recently. At every step, the most recently started job wins. A job with no start date at all always loses to a job that has one. If the person has no jobs listed, there is no current position, and the field is `null`. ## How experience and education are ordered Every profile is returned with its `experience` and `education` arrays sorted the same way: - Most recent first, based on the start date. - Entries without a start date go at the end of the list. - If two entries start on the same date, the one that is still ongoing (no end date) comes first. The same rule applies to jobs and to schools, wherever the profile came from: a result served from our database and a fresh live lookup are ordered identically. ## Example with four ongoing jobs Say Maria's profile lists: | Job | Company | Started | Ended | | ------------- | ------ | ------- | ------- | | Advisor | Acme | 2018 | Ongoing | | Head of Sales | Globex | 2021 | Ongoing | | Sales Lead | Globex | 2023 | Ongoing | | Mentor | Volunteerly | Unknown | Ongoing | For the current position: - If our data tells us her current company is Globex, we look only at the two Globex jobs and pick the newest: Sales Lead (2023). - If we had no company hint at all, step 3 applies: all four jobs are ongoing, so we pick the most recently started one, again Sales Lead (2023). The Mentor role has no start date, so it can never win. Whichever step resolves the current position, her `experience` array comes back in the same order: Sales Lead (2023), Head of Sales (2021), Advisor (2018), then Mentor, last because it has no start date. ## What this means in practice - The first item in `experience` is the person's most recent role, and `currentPosition` is the job we determined they hold today. - The order is always computed on our side: it never depends on how the underlying data arrived. - If a profile shows a surprising current job, the cause is usually a missing or outdated date in the source data, not the rules above. ## Why it is built this way Before, different data sources inferred the current job (and listed experiences) slightly differently, so the same person could look different depending on where the data came from. Now one set of rules applies everywhere the API returns a profile, so the same person gets the same current position and the same order whatever the source. ## FAQ ### How does Reverse Contact choose a person's current position? The `currentPosition` field is chosen by asking up to four questions in order and stopping at the first one that gives an answer: jobs at the known current company, then jobs matched by company name, then jobs with no end date, then any job. At every step the most recently started job wins, and a job with no start date always loses to a job that has one. See [How the current position is chosen](#how-the-current-position-is-chosen). ### Why is `currentPosition` null? `currentPosition` is `null` when the person has no jobs listed, because there is then no current position to pick. ### In what order are `experience` and `education` returned? The `experience` and `education` arrays are sorted most recent first by start date, with entries without a start date at the end. If two entries start on the same date, the ongoing one (no end date) comes first, and the order is identical for a result served from the database and a fresh live lookup. ### Should I read `currentPosition` or the first item of `experience` for someone's current job? `currentPosition` is the job Reverse Contact determined the person holds today, while the first item in `experience` is the person's most recent role by start date. The `experience` order is the same regardless of how `currentPosition` was resolved. ### Why does a profile show an unexpected current position? When `currentPosition` shows a surprising job, the cause is usually a missing or outdated date in the source data, not the selection rules. The rules are fixed and always applied in the same order, so the result is predictable. --- # Error handling & retries > Handle API errors predictably: the error response format, what each HTTP status means, search filter validation, and safe retry strategies. - Page: https://docs.reversecontact.com/guides/errors-retries - Last verified: 2026-11-04 When a request fails, the API returns an HTTP status and an error response with the same structure on every endpoint, including an error code you can match on programmatically. ## Response format Every error follows the same JSON structure. Match on the `code` field to handle specific cases in your code: **Error Response** ```json [JSON] { "success": false, "data": null, "error": { "code": "INSUFFICIENT_CREDITS", "message": "Insufficient credits: 0 available, 1 required" }, "metadata": { "requestId": "00bc3fa0-348f-4955-a1c1-3c7c91234568", "executionTimeMs": 3 } } ``` > [!NOTE] > Always include `metadata.requestId` when contacting support. It lets us trace your request. ## Error reference | Status | What to do | | ------ | ---------- | | `400` | The request is malformed: a JSON body that cannot be read, or a value the endpoint refuses, such as `INVALID_IDEMPOTENCY_KEY`, `WEBHOOK_URL_UNREACHABLE` (a `webhookUrl` that is not public HTTPS) or `JOB_INVALID_INPUT` (a filter or a cursor of the [job history](https://docs.reversecontact.com/endpoints/jobs)). Fix the request before you send it again. | | `422` | Check your request body: a required field is missing or malformed (`INVALID_LINKEDIN_URL` is also returned for an invalid URL). On the search endpoints, see [Search filter validation](#search-filter-validation) for the exact reason. On [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons), `UNKNOWN_FIELD` names a field the body or an item does not define, with `details.suggestion` and `details.acceptedFields`. With `IDEMPOTENCY_KEY_REUSED`, the `Idempotency-Key` was already used with a different body: use a new key. | | `401` | Check your API key: it may be missing, malformed, invalid, or expired (`API_KEY_EXPIRED`). See [Authentication](https://docs.reversecontact.com/authentication). | | `402` | Not enough credits. A paid lookup that costs more than your balance returns `INSUFFICIENT_CREDITS`, and the free `/check` endpoints return `NO_CREDITS` once fewer than 2 credits are left. Bulk enrichment returns `BULK_JOB_INSUFFICIENT_CREDITS` when your balance does not cover a single item. Purchase more or [upgrade your plan](https://app.reversecontact.com/settings/billing). | | `403` | Your workspace may be disabled (`WORKSPACE_DISABLED`), your key may lack a required permission (`INSUFFICIENT_PERMISSIONS`), or your key reached its total credit limit (`KEY_CREDIT_LIMIT_REACHED`): edit the key in [API Keys](https://app.reversecontact.com/api-keys) to add the permission, or to raise or reset the limit. Bulk enrichment returns `BULK_JOB_PHONE_FINDER_DISABLED` when it asks for phone numbers on a workspace where phone finding is not enabled, and [Cancel a job](https://docs.reversecontact.com/endpoints/job-cancel) returns `JOB_STARTED_BY_A_PERSON` for a job a person started in the app: stop it there. | | `404` | No result found for your query. Some endpoints still charge credits; see [API pricing](https://docs.reversecontact.com/pricing). | | `409` | With `IDEMPOTENCY_KEY_IN_USE`, a request with the same `Idempotency-Key` is still being processed: wait the `Retry-After` seconds, then retry with the same key to receive the first request's response. See [Find Email](https://docs.reversecontact.com/endpoints/contact-email#idempotency-key-header). With `JOB_RESULTS_NOT_READY`, the job has not finished: follow it with [Get a job](https://docs.reversecontact.com/endpoints/job) and download the results once `links.results` is set. With `JOB_NOT_CANCELLABLE`, the job already ended: there is nothing to retry. With `JOB_RESULTS_FORMAT_UNAVAILABLE`, the results of the job were stored before NDJSON and reading by page existed: download them as CSV. | | `410` | The endpoint was retired. Call the endpoint named in `error.details.alternative` instead. | | `413` | The JSON body is over 25 MB (`REQUEST_BODY_TOO_LARGE`). Send a smaller body: retrying the same one will not help. | | `429` | Rate limit reached. Wait the number of seconds in `error.details.retryAfter`, then retry (see below). The limits of each plan are in [Rate limits](https://docs.reversecontact.com/guides/rate-limits). With `KEY_CREDIT_LIMIT_REACHED`, the key reached its daily or monthly credit limit: see [API key management](https://docs.reversecontact.com/guides/api-key-management#_4-set-credit-limits-optional). | | `451` | This person or company asked not to be processed under GDPR or CCPA. The lookup is permanently blocked. | | `500` | Unexpected error on our side. Retry with backoff (see below). | | `502` | A provider we rely on failed for this request. Retry with backoff (see below). | | `503` | Our service is temporarily unavailable. Retry with backoff (see below). With `BULK_QUEUE_UNAVAILABLE`, nothing was created: send the Bulk enrichment request again shortly, with the same `Idempotency-Key`. | | `504` | The lookup took too long and timed out. Retry with backoff (see below). | ## Search filter validation The search endpoints validate every filter before running the query. A rejected request returns `422`, costs 0 credits, and never reaches the database. Without this check, a filter we cannot honor would return an empty page that looks like a legitimate "no results". `error.details.errors` always lists every problem found, so one round trip is enough to fix a whole payload. | `error.code` | Meaning | | ------------ | ------- | | `UNKNOWN_FIELD` | The field does not exist. Unknown fields are rejected rather than ignored, because an ignored filter returns unfiltered results. `details.suggestion` gives the closest accepted field, `details.acceptedFields` lists them all. A `page` sent to People Search or Company Search, which paginate with a cursor, suggests `cursor`. | | `INVALID_FILTER_VALUE` | The value is outside a closed list. This covers industry and country values plus the People Search `currentFunction`, `currentSubFunction` and `currentSeniority` taxonomies, its contract types and the `changedJobsWithinDays` windows, as well as `keywords` or `schoolName` without a single word. `details.suggestions` proposes up to three valid values, or the eight contract types when none is close. | | `EMPTY_FILTER_VALUE` | A blank string, empty array or empty object was sent. Remove the field instead. | | `FILTER_VALUES_LIMIT_EXCEEDED` | A filter carries more than 20 values. Split an include list into searches of at most 20 values and merge the results. Splitting does not work for an `exclude...` list, because each search only applies its own list: shorten it, or use a taxonomy filter such as `excludeCurrentFunction`. For countries, use [`location.area`](https://docs.reversecontact.com/reference/areas). | | `INVALID_RANGE` | A `min`/`max` pair is negative, fractional, inverted (`min` greater than `max`), or above the maximum of its filter, given in `details.max`, such as `2100` for the Company Search `foundedYear`. | | `INCOMPLETE_RANGE_FILTER` | `followersCount` used as the only filter requires both bounds. | | `INVALID_PAGINATION` | `perPage` is not a whole number. | | `PAGINATION_LIMIT_EXCEEDED` | `perPage` is above 100. `details` carries `field` and `max`. | | `INVALID_ID_FORMAT` | `currentCompanyId` is not a `com_` identifier returned by our API, or `schoolId` is not a `sch_` identifier. | | `INVALID_DATE_FORMAT` | `maxDataAgeDate` is not an ISO 8601 date-time. | | `CONTRADICTORY_FILTERS` | The same value appears in an included and excluded filter, which can never match. | | `NO_FILTER_PROVIDED` | The body carries pagination only (`cursor` / `perPage`), or `maxDataAgeDate` is the only filter of a Company Search, which it can narrow but not select on its own. | | `INVALID_SEARCH_FILTERS` | Several of the above at once. Read `details.errors`. | ```json [Response] { "success": false, "data": null, "error": { "code": "INVALID_FILTER_VALUE", "message": "\"Technology\" is not an accepted value for \"industry\". Did you mean \"Technology, Information and Internet\", \"Technology, Information and Media\"? This filter is matched exactly, so an unlisted value can never return a result.", "details": { "field": "industry", "value": "Technology", "reason": "not_in_accepted_values", "suggestions": ["Technology, Information and Internet", "Technology, Information and Media"], "documentation": "https://docs.reversecontact.com/guides/industries" } } } ``` Values are compared tolerantly before being rejected: case, surrounding spaces, accents and `&` versus `and` are normalized for you. Only an unknown value is refused. ## When to retry These statuses are safe to retry, and they cost 0 credits: - `429`: you've hit a rate limit. Wait the number of seconds in `error.details.retryAfter`, then retry. With `KEY_CREDIT_LIMIT_REACHED`, that wait runs until 00:00 UTC or the 1st of the month: pause the work rather than retrying in a loop. - `500`, `502`, `503` and `504`: a temporary issue on our side or at a provider we rely on. Retry with exponential backoff (below), and stop after a few attempts, for example 5, then alert. Incidents are posted on [status.reversecontact.com](https://status.reversecontact.com), where you can subscribe to updates by email, Slack, webhook or RSS. - `409` with `IDEMPOTENCY_KEY_IN_USE`: a request with the same `Idempotency-Key` is still being processed. Wait the `Retry-After` seconds, then retry with the same key. A `409 JOB_RESULTS_NOT_READY` asks you to wait for the job instead, and a `409 JOB_NOT_CANCELLABLE` or `409 JOB_RESULTS_FORMAT_UNAVAILABLE` is final. All other errors (`400`, `401`, `402`, `403`, `404`, `410`, `413`, `422`, `451`) require a change to your request, your key or your credit balance before retrying. Find Email and Find Phone debit credits when their job is created, and [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) reserves them when it creates its job. Before you retry one of them after a timeout or a dropped connection, make sure the request carries an `Idempotency-Key`: a retry with the same key returns the first response instead of starting a second lookup or job. See [Find Email](https://docs.reversecontact.com/endpoints/contact-email#idempotency-key-header). Wait a little longer between each retry to give the server time to recover: **Retry delay** ```javascript [JavaScript] const delay = Math.min(1000 * 2 ** attempt, 15000) const jitter = Math.floor(Math.random() * 250) await new Promise((r) => setTimeout(r, delay + jitter)) ``` ```python [Python] delay = min(1 * 2 ** attempt, 15) jitter = random.uniform(0, 0.25) time.sleep(delay + jitter) ``` ## Timeouts and aborted requests Set your client timeout to at least 45 seconds on the search and count endpoints, and 60 seconds on prompt search. When a search runs out of time, we answer with a JSON `504` before those limits, and it costs 0 credits. A shorter client timeout can cut the connection just before that answer arrives. Closing the connection does not cancel the request. It keeps running on our side and can still be billed, even though you never receive the page. A retry after an abort can then bill the same page twice, so raise a timeout that fires too early rather than aborting and retrying. ## Credits and errors All error responses (`401`, `402`, `403`, `409`, `410`, `413`, `422`, `429`, `451`, `500`, `502`, `503`, `504`) cost 0 credits. The `404` status is the only exception: some endpoints charge credits when the lookup was performed but no result was found. See [API pricing](https://docs.reversecontact.com/pricing) for the full breakdown. ## FAQ ### Which Reverse Contact API errors are safe to retry? `429` (rate limit reached) is safe to retry after the number of seconds in `error.details.retryAfter`. `500`, `502`, `503` and `504` (a temporary issue on our side or at a provider) are safe to retry with exponential backoff, for example a delay that doubles from 1 second up to 15 seconds, plus up to 250 ms of random jitter, for a few attempts. All other errors require a change to your request, your key or your credit balance before retrying. See [When to retry](#when-to-retry). ### Do failed API requests cost credits? Error responses (`401`, `402`, `403`, `409`, `410`, `413`, `422`, `429`, `451`, `500`, `502`, `503`, `504`) cost 0 credits. `404` is the only exception: some endpoints charge credits when the lookup was performed but no result was found. See [API pricing](https://docs.reversecontact.com/pricing). ### Can an aborted request be billed? Yes. Closing the connection does not cancel the request, so it can still be billed. Set your client timeout to at least 45 seconds on the search and count endpoints, and 60 seconds on prompt search. A `504` from us costs 0 credits. ### What is the difference between `INSUFFICIENT_CREDITS` and `NO_CREDITS`? Both codes come with a `402` status. `INSUFFICIENT_CREDITS` means a paid lookup costs more than your remaining balance, and `error.details` gives the credits `required` and `available`. `NO_CREDITS` blocks the free `/check` endpoints once fewer than 2 credits are left. Both clear when you top up by purchasing more credits or upgrading your plan. ### Why does a search request return `422` instead of an empty result? The search endpoints validate every filter before running the query, because a filter that cannot be honored would otherwise return an empty page that looks like a legitimate "no results". A rejected search returns `422`, costs 0 credits, and lists every problem found in `error.details.errors`. See [Search filter validation](#search-filter-validation). ### What does a `451` error mean? A `451` means this person or company asked not to be processed under GDPR or CCPA, and the lookup is permanently blocked. Profile and company lookups answer `451` whichever input names them: profile URL, email, internal id, domain or name. Find Email and Find Phone answer `451` when given the profile URL or internal id. Like the other error responses, a `451` costs 0 credits. --- # Production checklist > Hardening checklist for auth, retries, quotas, webhook handling, and support readiness, to go through before your integration goes to production. - Page: https://docs.reversecontact.com/guides/production-checklist - Last verified: 2026-10-15 Go through this checklist before your integration goes to production. ## Before go-live - Store API keys server-side or in a secure secrets manager. See [Authentication](https://docs.reversecontact.com/authentication#keep-your-key-safe). - Log `metadata.requestId` for every request so support can trace issues quickly. - Monitor `quotas.workspace.credits.left` and `quotas.workspace.minuteRateLimit.left` from the [Usage endpoint](https://docs.reversecontact.com/endpoints/usage). - Route async workflows through a durable [webhook receiver](https://docs.reversecontact.com/guides/webhooks#_2-set-up-a-receiver), not a manual test endpoint. - Make webhook processing idempotent by deduplicating on `webhookId`. See [Webhooks](https://docs.reversecontact.com/guides/webhooks#security-best-practices). ## Request safety - Use timeouts on every outbound request. On a long poll (`GET /v2/webhooks/:webhookId?wait=20`), keep the timeout above the `wait` value, for example 30 seconds. See [Polling](https://docs.reversecontact.com/guides/polling#long-polling-with-wait). - Retry only `429` and `5xx` with exponential backoff. See [Error handling](https://docs.reversecontact.com/guides/errors-retries#when-to-retry). - Send an `Idempotency-Key` header on [Find Email](https://docs.reversecontact.com/endpoints/contact-email#idempotency-key-header) and [Find Phone](https://docs.reversecontact.com/endpoints/contact-phone#idempotency-key-header), and send the same key when you retry, so a lost response never starts and bills a second lookup. - On a `429`, wait the number of seconds in `error.details.retryAfter` before retrying. - Treat `4xx` validation or auth failures as permanent until you change the input or credentials. ## Webhook safety - Use HTTPS for every production webhook URL. - Return `200` quickly, then process heavy work asynchronously. - Validate that each callback contains `webhookId` + `data` or `webhookId` + `errorCode`. See [Webhook payload](https://docs.reversecontact.com/guides/webhooks#webhook-payload). - Deduplicate on `webhookId` to handle rare duplicate deliveries. ## Monitoring & alerts - Alert on repeated `401`, `402`, `429`, and `5xx` responses. - Surface credit exhaustion before it blocks critical workflows. See [Credits](https://docs.reversecontact.com/guides/credits). - Keep one smoke test for a sync endpoint and one for an async endpoint (see below). - Document which team owns [API key rotation](https://docs.reversecontact.com/guides/api-key-management), webhook incidents, and quota alerts. ## Recommended smoke tests 1. `GET /v2/usage` returns `200` and a valid `quotas` object. 2. `POST /v2/fetch/persons/check` returns `200` with `creditsConsumed: 0`. 3. One async endpoint returns an immediate response and the result is delivered to your webhook URL. 4. Your application logs both the outbound request and the webhook completion using the same `requestId` or `webhookId`. ## You're ready to go live When every item above is in place, the integration is ready for production traffic. ## FAQ ### Which Reverse Contact API errors should I retry? Retry only `429` and `5xx` responses from the Reverse Contact API, with exponential backoff, and on a `429` wait the number of seconds in `error.details.retryAfter` before retrying. Treat `4xx` validation or auth failures as permanent until you change the input or credentials. See [Error handling](https://docs.reversecontact.com/guides/errors-retries#when-to-retry). ### How do I monitor credits and rate limits in production? Monitor `quotas.workspace.credits.left` and `quotas.workspace.minuteRateLimit.left` from the Reverse Contact Usage endpoint, and alert on repeated `401`, `402`, `429` and `5xx` responses. Surface credit exhaustion before it blocks critical workflows. See [Usage endpoint](https://docs.reversecontact.com/endpoints/usage). ### Can Reverse Contact send the same webhook twice? Rare duplicate deliveries are possible, so make Reverse Contact webhook processing idempotent by deduplicating on `webhookId`. Return `200` quickly, then process heavy work asynchronously. See [Webhooks](https://docs.reversecontact.com/guides/webhooks#security-best-practices). ### What smoke tests should I run before going live with the Reverse Contact API? The recommended smoke tests are: `GET /v2/usage` returns `200` and a valid `quotas` object, `POST /v2/fetch/persons/check` returns `200` with `creditsConsumed: 0`, and one async endpoint returns an immediate response and delivers the result to your webhook URL. Your application should also log both the outbound request and the webhook completion using the same `requestId` or `webhookId`. ### What should I log so support can trace an API issue? Log `metadata.requestId` for every Reverse Contact API request so support can trace issues quickly. For async workflows, also log the webhook completion with the same `requestId` or `webhookId` as the outbound request. --- # Reverse Contact API use cases > Find people, enrich companies, get work emails and build prospect lists with the Reverse Contact API. Follow code examples and check credit costs. - Page: https://docs.reversecontact.com/use-cases - Last verified: 2026-11-04 Choose a use case based on the data you have and the result you need. Each recipe gives you the endpoints, expected results and credit costs, with code from the first request to the result. Examples use cURL, the TypeScript SDK or the Python SDK, plus the CLI when a command is available. ## Use cases at scale For larger datasets, [enrich up to 25,000 people in one request](https://docs.reversecontact.com/guides/bulk-enrichment), [keep contacts in a List and buy their emails](https://docs.reversecontact.com/guides/lists), or [enrich a CSV file of contacts](https://docs.reversecontact.com/guides/enrich-a-csv-file). You can find these guides alongside the recipes in the use case gallery. ## All use cases ### Find & enrich - [Find a person from an email, a name or a profile URL](https://docs.reversecontact.com/use-cases/find-a-person) - [Find a company from a domain or a page URL](https://docs.reversecontact.com/use-cases/find-a-company) - [Keep profiles fresh](https://docs.reversecontact.com/use-cases/keep-profiles-fresh) ### Contact - [Get a work email or a phone number](https://docs.reversecontact.com/use-cases/get-a-work-email-or-phone) ### Build lists - [Build a list of people](https://docs.reversecontact.com/use-cases/build-a-list-of-people) - [Build a list of companies](https://docs.reversecontact.com/use-cases/build-a-list-of-companies) ### At scale - [Enrich up to 25,000 people in one request](https://docs.reversecontact.com/guides/bulk-enrichment) - [Keep contacts in a List and buy their emails](https://docs.reversecontact.com/guides/lists) - [Enrich a CSV file of contacts](https://docs.reversecontact.com/guides/enrich-a-csv-file) ## FAQ ### Which Reverse Contact API endpoint should I use? Choose the use case that matches your starting data and the result you need. Each recipe lists the endpoints and credit costs, with code to retrieve the result. ### Can I follow the API use cases in TypeScript or Python? The use case recipes include cURL, TypeScript SDK and Python SDK examples. You can also use CLI examples when a command is available. ### How do I enrich contacts in bulk or from a CSV file? Follow the [Bulk enrichment guide](https://docs.reversecontact.com/guides/bulk-enrichment) for requests of up to 25,000 people, or the [CSV enrichment guide](https://docs.reversecontact.com/guides/enrich-a-csv-file) for contact files. Use the [Lists guide](https://docs.reversecontact.com/guides/lists) to store contacts and buy their emails. --- # Find a person from an email, a name or a profile URL > Find a person from an email, a name and company, or a Social profile URL with the API. Compare cached and live profiles, code examples and credits. - Page: https://docs.reversecontact.com/use-cases/find-a-person - Last verified: 2026-11-04 Find a professional profile from an email address, a full name with a company, or a Social profile URL. Enrich and cached Fetch return the result in the response. Use Live to fetch fresh data in the background. ## Which endpoint | Input you have | Endpoint | Cost | Mode | | --- | --- | --- | --- | | Email, or name with a company | [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person) | 2 credits when found. A 404 is free | Synchronous | | Social profile URL or person id | [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) | 1 credit when found in the cache. A 404 is free | Synchronous | | Social profile URL or person id, with a stale or missing cache | [Person profile live](https://docs.reversecontact.com/endpoints/fetch-profile-live) | 2 credits when the job is created | Asynchronous | ## How it works Set `RC_API_KEY` to your [API key](https://docs.reversecontact.com/authentication). Install the [TypeScript SDK](https://docs.reversecontact.com/sdk/javascript), [Python SDK](https://docs.reversecontact.com/sdk/python) or [CLI](https://docs.reversecontact.com/guides/cli) for your chosen example. The cURL live example also uses `jq`. Choose either the email lookup or the URL fetch, depending on your input. ### Find the person behind an email Send the email to Enrich profile and read `data.person`. If no person matches, you get HTTP `404`, or `null` / `None` in the SDKs. To search by name, replace `email` with `firstName: "Jane"`, `lastName: "Doe"` and `companyDomain: "acme.com"`. In Python, use `first_name`, `last_name`, `company_domain`. In the CLI, use `--first-name Jane --last-name Doe --company-domain acme.com`. **Code Examples** ```bash [cURL] curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/enrich/persons \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "jane.doe@acme.com"}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.enrich.person({ email: "jane.doe@acme.com" }); if (result === null) { console.log("No confident match: nothing was charged"); } else { const { person } = result.data; console.log(person?.firstName, person?.lastName, person?.currentCompanyName); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.enrich.person(email="jane.doe@acme.com") if result is None: print("No confident match: nothing was charged") elif result.data is not None and result.data.person is not None: person = result.data.person print(person.first_name, person.last_name, person.current_company_name) ``` ```bash [CLI] rc enrich person --email jane.doe@acme.com ``` ### Use the profile URL when you have one If you already have the Social profile URL, call Person profile and read the profile directly from `data`, including `firstName`, `lastName` and `currentPosition`. You can also send the `id` from a previous response instead of `url`. **Code Examples** ```bash [cURL] curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/fetch/persons \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.social.com/in/janedoe"}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.fetch.person({ url: "https://www.social.com/in/janedoe" }); if (result === null) { console.log("Profile not found: nothing was charged"); } else { console.log(result.data.firstName, result.data.lastName); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.fetch.person(url="https://www.social.com/in/janedoe") if result is None: print("Profile not found: nothing was charged") elif result.data is not None: print(result.data.first_name, result.data.last_name) ``` ```bash [CLI] rc fetch person --url https://www.social.com/in/janedoe ``` ### Refresh a stale or missing profile You can use Live when the cached profile is too old or a cached fetch returns `404`. To decide based on the profile's age, [check profile freshness](https://docs.reversecontact.com/use-cases/keep-profiles-fresh) first. Save the job id, then poll until the job succeeds or fails. A successful job returns the profile in `data.result`. **Code Examples** ```bash [cURL] curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/fetch/persons/live \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.social.com/in/janedoe"}' > live-job.json WEBHOOK_ID=$(jq -er .data.webhookId live-job.json) printf 'Save this job id: %s\n' "$WEBHOOK_ID" while :; do result=$(curl --fail-with-body -sS --max-time 30 \ "https://api.reversecontact.com/v2/webhooks/$WEBHOOK_ID?wait=20" \ -H "Authorization: Bearer $RC_API_KEY") || break case "$(printf '%s' "$result" | jq -r '.data.status')" in succeeded) printf '%s' "$result" | jq '.data.result'; break ;; errored) printf '%s' "$result" | jq '.data | {errorCode, creditsConsumed}'; break ;; created|in_progress) sleep 2 ;; *) printf '%s\n' "$result"; break ;; esac done ``` ```typescript [TypeScript SDK] import ReverseContact, { JobFailedError } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.fetch.personLive({ url: "https://www.social.com/in/janedoe" }); console.log("Save this job id:", job.id); // resume after a restart with client.jobs.wait(id) try { // Long polls until the job ends, 30 minutes at most by default. const done = await job.wait(); console.log(done.data.result?.firstName, done.data.result?.lastName); } catch (error) { if (error instanceof JobFailedError) console.log("Job failed:", error.errorCode, "credits:", error.creditsConsumed); else throw error; } ``` ```python [Python SDK] import os from reversecontact import JobFailedError, ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.fetch.person_live(url="https://www.social.com/in/janedoe") print("Save this job id:", job.id) # resume after a restart with client.jobs.wait(id) try: # Long polls until the job ends, 30 minutes at most by default. done = job.wait() person = done.data.result if done.data else None if person is not None: print(person.first_name, person.last_name) except JobFailedError as error: print("Job failed:", error.error_code) ``` ```bash [CLI] rc fetch person --url https://www.social.com/in/janedoe --live --wait --timeout 300 ``` ## What it costs Enrich profile costs **2 credits when a person is found**, with the full profile included. Person profile costs **1 credit when the profile is found in the cache**. A `404` from either synchronous lookup costs nothing. Person profile live consumes **2 credits per request when the job is created**. A `person-not-found` failure remains billed. Failures with `invalid-data` or `fetch-data-error` are refunded. Check the [live failure rules](https://docs.reversecontact.com/endpoints/fetch-profile-live#webhook-callback-errors-advanced) before retrying. Polling is free. > [!NOTE] > For 1,000 email lookups that match 600 people, Enrich costs 600 × 2 = **1,200 credits**. The 400 unmatched requests cost nothing. Fetching 600 known URLs from the cache instead costs 600 × 1 = **600 credits**. Starting 100 live jobs consumes **200 credits** at creation. ## Good to know Add a company domain or company name to a name-based lookup to reduce ambiguous matches. Review `data.alternativePersons` before choosing a candidate. Enrich usually returns the full profile. If you get `data.isFullProfile: false`, Enrich returned a light profile instead. A separate cached fetch can complete that result for 1 additional credit when found. Live results can take tens of minutes. [Polling](https://docs.reversecontact.com/guides/polling) is recommended. Send `webhookUrl` only if you want [webhook delivery](https://docs.reversecontact.com/guides/webhooks). If an SDK wait times out, resume with `client.jobs.wait(jobId)` rather than starting another job. In the CLI, use `rc task` with the saved id. Handle [missing and empty profile fields](https://docs.reversecontact.com/guides/missing-and-empty-fields), and follow [current position ordering](https://docs.reversecontact.com/guides/current-position-resolution) when selecting a job. Profile enrichment does not replace [Find Email or Find Phone](https://docs.reversecontact.com/use-cases/get-a-work-email-or-phone). ## FAQ ### How do I find a person from an email address with the API? Send `email` to `POST /v2/enrich/persons`. Enrich profile returns the matched profile in `data.person` and costs 2 credits when a person is found. An unmatched lookup costs nothing. ### How do I find a person by name and company? Send `firstName`, `lastName` and `companyDomain` or `companyName` to Enrich profile. Company information narrows the match. Review `data.alternativePersons` when several people match. ### Which endpoint should I use when I already have a Social profile URL? Use `POST /v2/fetch/persons` for the cached profile at 1 credit when found. Use Person profile live when the cached profile is stale or missing, at 2 credits consumed when the job is created. ### Does a live profile lookup cost credits when no person is found? Person profile live consumes 2 credits at job creation, and a `person-not-found` failure remains billed. The live endpoint documents which failures receive a refund. ### Why does Enrich profile return fewer fields than expected? Enrich profile sets `data.isFullProfile` to `false` when only the light profile could be returned in time. A separate Person profile request with the returned person id can retrieve the cached full profile for 1 additional credit when found. --- # Find a company from a domain or a page URL > Find a company profile from a domain or Social company page URL with the API. Compare cached and live data, credit costs and SDK examples. - Page: https://docs.reversecontact.com/use-cases/find-a-company - Last verified: 2026-11-04 Find a company profile from a domain or Social company page URL, including industry and employee information. Enrich and cached Fetch return the profile immediately. Use Live to retrieve a fresh profile in the background. ## Which endpoint | Input you have | Endpoint | Cost | Mode | | --- | --- | --- | --- | | Company domain | [Enrich company](https://docs.reversecontact.com/endpoints/enrich-company) | 2 credits when found. A 404 is free | Synchronous | | Company page URL or company id | [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) | 1 credit when found in the cache. A 404 is free | Synchronous | | Company page URL or company id, with a stale or missing cache | [Company profile live](https://docs.reversecontact.com/endpoints/fetch-company-live) | 2 credits when the job is created | Asynchronous | ## How it works Set `RC_API_KEY` to your [API key](https://docs.reversecontact.com/authentication). Install the [TypeScript SDK](https://docs.reversecontact.com/sdk/javascript), [Python SDK](https://docs.reversecontact.com/sdk/python) or [CLI](https://docs.reversecontact.com/guides/cli) for your chosen example. The cURL live example also uses `jq`. Choose either the domain lookup or the page URL fetch, depending on your input. ### Find the company behind a domain Send a bare domain or a full website URL as `domain` to Enrich company. Read the profile from `data.company` before saving the result. If no company matches, handle the HTTP `404` response, or `null` / `None` in the SDKs. **Code Examples** ```bash [cURL] curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/enrich/companies \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "acme.com"}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.enrich.company({ domain: "acme.com" }); if (result === null) { console.log("No company found: nothing was charged"); } else { console.log(result.data.company?.name, result.data.company?.industry); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.enrich.company(domain="acme.com") if result is None: print("No company found: nothing was charged") elif result.data is not None and result.data.company is not None: print(result.data.company.name, result.data.company.industry) ``` ```bash [CLI] rc enrich company --domain acme.com ``` ### Fetch a company page you already know When you have the company page URL, fetch the cached profile and read `data.companyName`, `data.industry` and `data.employeesCount`. You can send the company `id` from an earlier response instead of `url`. **Code Examples** ```bash [cURL] curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/fetch/companies \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.social.com/company/acme-corp"}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.fetch.company({ url: "https://www.social.com/company/acme-corp" }); if (result === null) { console.log("Company not found: nothing was charged"); } else { console.log(result.data.companyName, result.data.industry); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.fetch.company(url="https://www.social.com/company/acme-corp") if result is None: print("Company not found: nothing was charged") elif result.data is not None: print(result.data.company_name, result.data.industry) ``` ```bash [CLI] rc fetch company --url https://www.social.com/company/acme-corp ``` ### Refresh the company when the cache is too old You can use Live for a stale or missing company profile. To decide based on the profile's age, [check freshness](https://docs.reversecontact.com/use-cases/keep-profiles-fresh) first. Save the job id, then poll for the company profile in `data.result`. **Code Examples** ```bash [cURL] curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/fetch/companies/live \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.social.com/company/acme-corp"}' > live-job.json WEBHOOK_ID=$(jq -er .data.webhookId live-job.json) printf 'Save this job id: %s\n' "$WEBHOOK_ID" while :; do result=$(curl --fail-with-body -sS --max-time 30 \ "https://api.reversecontact.com/v2/webhooks/$WEBHOOK_ID?wait=20" \ -H "Authorization: Bearer $RC_API_KEY") || break case "$(printf '%s' "$result" | jq -r '.data.status')" in succeeded) printf '%s' "$result" | jq '.data.result'; break ;; errored) printf '%s' "$result" | jq '.data | {errorCode, creditsConsumed}'; break ;; created|in_progress) sleep 2 ;; *) printf '%s\n' "$result"; break ;; esac done ``` ```typescript [TypeScript SDK] import ReverseContact, { JobFailedError } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.fetch.companyLive({ url: "https://www.social.com/company/acme-corp" }); console.log("Save this job id:", job.id); // resume after a restart with client.jobs.wait(id) try { // Long polls until the job ends, 30 minutes at most by default. const done = await job.wait(); console.log(done.data.result?.companyName, done.data.result?.industry); } catch (error) { if (error instanceof JobFailedError) console.log("Job failed:", error.errorCode, "credits:", error.creditsConsumed); else throw error; } ``` ```python [Python SDK] import os from reversecontact import JobFailedError, ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.fetch.company_live(url="https://www.social.com/company/acme-corp") print("Save this job id:", job.id) # resume after a restart with client.jobs.wait(id) try: # Long polls until the job ends, 30 minutes at most by default. done = job.wait() company = done.data.result if done.data else None if company is not None: print(company.company_name, company.industry) except JobFailedError as error: print("Job failed:", error.error_code) ``` ```bash [CLI] rc fetch company --url https://www.social.com/company/acme-corp --live --wait --timeout 300 ``` ## What it costs Enrich company costs **2 credits when a company is found**, with the full profile included. Company profile costs **1 credit when the company is found in the cache**. A `404` from either synchronous lookup is free. Company profile live consumes **2 credits per request when the job is created**. A `company-not-found` failure remains billed. Failures with `invalid-data` or `fetch-data-error` are refunded. The [live failure rules](https://docs.reversecontact.com/endpoints/fetch-company-live#webhook-callback-errors-advanced) describe the other outcomes. Polling costs nothing. > [!NOTE] > For 1,000 domain lookups that match 800 companies, Enrich costs 800 × 2 = **1,600 credits**. The 200 unmatched lookups are free. Retrieving 800 known company URLs from the cache instead costs **800 credits**. Starting 100 live refreshes consumes **200 credits** at creation. ## Good to know Enrich returns `data.company` and `data.isFullProfile`. Cached Fetch returns the company directly in `data`. If Enrich returns a light profile, `data.isFullProfile` is `false` and the request still costs 2 credits. A separate cached fetch can complete the profile for 1 additional credit when found. Use `employeesCount` for the exact headcount when available. The `employeeCountRange` field gives a size bracket. Treat missing values as unknown, as described in [Missing and empty profile fields](https://docs.reversecontact.com/guides/missing-and-empty-fields). Live jobs can take tens of minutes. Use [polling](https://docs.reversecontact.com/guides/polling), or pass `webhookUrl` for optional [webhook delivery](https://docs.reversecontact.com/guides/webhooks). Resume a saved job with `client.jobs.wait(jobId)` or `rc task` after a timeout instead of starting a new paid refresh. ## FAQ ### How do I find a company profile from a domain with the API? Send `domain` to `POST /v2/enrich/companies`. Enrich company returns the profile in `data.company` and costs 2 credits when a company is found. An unmatched lookup is free. ### Can I fetch a company from a Social company page URL? Send `url` to `POST /v2/fetch/companies` for the cached profile. Company profile costs 1 credit when found and returns the company directly in `data`. ### When should I use Company profile live? Use Company profile live when the cached profile is stale or missing. A live refresh consumes 2 credits when the job is created and returns the fresh company asynchronously through polling or a webhook. ### Is a company live lookup free when the company is not found? No. A `company-not-found` failure from Company profile live remains billed at 2 credits. A `404` from cached Company profile or Enrich company is free. --- # Keep profiles fresh > Check person and company profile freshness for free, compare the last update date, and refresh stale data with live API jobs and polling. - Page: https://docs.reversecontact.com/use-cases/keep-profiles-fresh - Last verified: 2026-11-04 Use your stored person or company profile URLs to check when Reverse Contact last fetched each profile. The status request is free and returns immediately. Refresh only stale or missing profiles with a live job, then retrieve the fresh data when the job ends. ## Which endpoint | Input you have | Endpoint | Cost | Mode | | --- | --- | --- | --- | | Person URL or id to check | [Person profile status](https://docs.reversecontact.com/endpoints/check-person) | Free. Your workspace needs at least 2 credits | Synchronous | | Company URL or id to check | [Company profile status](https://docs.reversecontact.com/endpoints/check-company) | Free. Your workspace needs at least 2 credits | Synchronous | | Stale or missing person profile | [Person profile live](https://docs.reversecontact.com/endpoints/fetch-profile-live) | 2 credits when the job is created | Asynchronous | | Stale or missing company profile | [Company profile live](https://docs.reversecontact.com/endpoints/fetch-company-live) | 2 credits when the job is created | Asynchronous | ## How it works Set `RC_API_KEY` to your [API key](https://docs.reversecontact.com/authentication) and install the [TypeScript SDK](https://docs.reversecontact.com/sdk/javascript), [Python SDK](https://docs.reversecontact.com/sdk/python) or [CLI](https://docs.reversecontact.com/guides/cli). The shell examples use `jq`. You can adjust the examples' 30-day freshness threshold to match your own policy. ### Read the last update date for free Read `data.exists` and `data.lastUpdate` from each status response. The `lastUpdate` field is an ISO 8601 timestamp, or `null` when the profile is absent. Keep these responses to skip live calls for fresh profiles in the next steps. **Code Examples** ```bash [cURL] curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/fetch/persons/check \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.social.com/in/janedoe"}' > person-status.json curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/fetch/companies/check \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.social.com/company/acme-corp"}' > company-status.json jq .data person-status.json company-status.json ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const personStatus = await client.fetch.checkPerson({ url: "https://www.social.com/in/janedoe" }); const companyStatus = await client.fetch.checkCompany({ url: "https://www.social.com/company/acme-corp" }); console.log(personStatus.data, companyStatus.data); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: person_status = client.fetch.check_person(url="https://www.social.com/in/janedoe") company_status = client.fetch.check_company(url="https://www.social.com/company/acme-corp") print(person_status.data, company_status.data) ``` ```bash [CLI] rc check person --url https://www.social.com/in/janedoe --json > person-status.json rc check company --url https://www.social.com/company/acme-corp --json > company-status.json jq .data person-status.json company-status.json ``` ### Refresh the person only when needed If the person is absent or `lastUpdate` is older than your threshold, start a live refresh and save the job id before waiting. Read the fresh person from `data.result` after the job succeeds. **Code Examples** ```bash [cURL] if jq -e '.data | .exists == true and (.lastUpdate != null) and ((.lastUpdate | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) >= (now - 30 * 86400))' person-status.json > /dev/null; then echo "Cached profile is fresh; no live request" else curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/fetch/persons/live \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.social.com/in/janedoe"}' > person-job.json WEBHOOK_ID=$(jq -er .data.webhookId person-job.json) printf 'Save this job id: %s\n' "$WEBHOOK_ID" while :; do result=$(curl --fail-with-body -sS --max-time 30 \ "https://api.reversecontact.com/v2/webhooks/$WEBHOOK_ID?wait=20" \ -H "Authorization: Bearer $RC_API_KEY") || break case "$(printf '%s' "$result" | jq -r '.data.status')" in succeeded) printf '%s' "$result" | jq '.data.result'; break ;; errored) printf '%s' "$result" | jq '.data | {errorCode, creditsConsumed}'; break ;; created|in_progress) sleep 2 ;; *) printf '%s\n' "$result"; break ;; esac done fi ``` ```typescript [TypeScript SDK] const personCutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; const personData = personStatus.data; if (!personData.exists || !personData.lastUpdate || Date.parse(personData.lastUpdate) < personCutoff) { const job = await client.fetch.personLive({ url: "https://www.social.com/in/janedoe" }); console.log("Save this job id:", job.id); const done = await job.wait(); console.log(done.data.result); } else { console.log("Cached profile is fresh; no live request"); } ``` ```python [Python SDK] from datetime import datetime, timedelta, timezone person_cutoff = datetime.now(timezone.utc) - timedelta(days=30) person_data = person_status.data if person_data is None: raise RuntimeError("Status response is missing data") person_updated = datetime.fromisoformat(str(person_data.last_update).replace("Z", "+00:00")) if person_data.last_update else None if not person_data.exists or person_updated is None or person_updated < person_cutoff: with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.fetch.person_live(url="https://www.social.com/in/janedoe") print("Save this job id:", job.id) done = job.wait() print(done.data.result if done.data else None) else: print("Cached profile is fresh; no live request") ``` ```bash [CLI] if jq -e '.data | .exists == true and (.lastUpdate != null) and ((.lastUpdate | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) >= (now - 30 * 86400))' person-status.json > /dev/null; then echo "Cached profile is fresh; no live request" else rc fetch person --url https://www.social.com/in/janedoe --live --wait --timeout 300 fi ``` ### Refresh the company only when needed If the company is absent or `lastUpdate` is older than your threshold, start a live refresh and save the job id before waiting. Read the fresh company from `data.result` after the job succeeds. **Code Examples** ```bash [cURL] if jq -e '.data | .exists == true and (.lastUpdate != null) and ((.lastUpdate | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) >= (now - 30 * 86400))' company-status.json > /dev/null; then echo "Cached profile is fresh; no live request" else curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/fetch/companies/live \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.social.com/company/acme-corp"}' > company-job.json WEBHOOK_ID=$(jq -er .data.webhookId company-job.json) printf 'Save this job id: %s\n' "$WEBHOOK_ID" while :; do result=$(curl --fail-with-body -sS --max-time 30 \ "https://api.reversecontact.com/v2/webhooks/$WEBHOOK_ID?wait=20" \ -H "Authorization: Bearer $RC_API_KEY") || break case "$(printf '%s' "$result" | jq -r '.data.status')" in succeeded) printf '%s' "$result" | jq '.data.result'; break ;; errored) printf '%s' "$result" | jq '.data | {errorCode, creditsConsumed}'; break ;; created|in_progress) sleep 2 ;; *) printf '%s\n' "$result"; break ;; esac done fi ``` ```typescript [TypeScript SDK] const companyCutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; const companyData = companyStatus.data; if (!companyData.exists || !companyData.lastUpdate || Date.parse(companyData.lastUpdate) < companyCutoff) { const job = await client.fetch.companyLive({ url: "https://www.social.com/company/acme-corp" }); console.log("Save this job id:", job.id); const done = await job.wait(); console.log(done.data.result); } else { console.log("Cached profile is fresh; no live request"); } ``` ```python [Python SDK] from datetime import datetime, timedelta, timezone company_cutoff = datetime.now(timezone.utc) - timedelta(days=30) company_data = company_status.data if company_data is None: raise RuntimeError("Status response is missing data") company_updated = datetime.fromisoformat(str(company_data.last_update).replace("Z", "+00:00")) if company_data.last_update else None if not company_data.exists or company_updated is None or company_updated < company_cutoff: with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.fetch.company_live(url="https://www.social.com/company/acme-corp") print("Save this job id:", job.id) done = job.wait() print(done.data.result if done.data else None) else: print("Cached profile is fresh; no live request") ``` ```bash [CLI] if jq -e '.data | .exists == true and (.lastUpdate != null) and ((.lastUpdate | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) >= (now - 30 * 86400))' company-status.json > /dev/null; then echo "Cached profile is fresh; no live request" else rc fetch company --url https://www.social.com/company/acme-corp --live --wait --timeout 300 fi ``` ## What it costs Person profile status and Company profile status cost **0 credits**, but each requires at least 2 credits in the workspace. Below that balance, the API returns `402 NO_CREDITS`. Status calls count toward your request rate limits. Each live refresh consumes **2 credits when the job is created**. A `person-not-found` or `company-not-found` failure remains billed. Other failures can be refunded under the rules on the live endpoint pages. Polling costs 0 credits and does not count toward the per-minute rate limit. > [!NOTE] > Check 1,000 profiles for free. If 800 are fresh and you refresh the other 200, the live jobs consume 200 × 2 = **400 credits** at creation, before any failure refunds. This example reuses the profiles you already stored. Fetching fresh cached copies would add 1 credit per profile found. ## Good to know Status reports the age of Reverse Contact's cache. If your stored copy needs updating but the cache is recent, use [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) or [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) for 1 credit when found instead of running Live. See [Data freshness](https://docs.reversecontact.com/guides/data-freshness) for when to check, fetch or refresh. Keep live jobs in the background, as results can take tens of minutes. The SDK raises `JobFailedError` on failure and `JobTimeoutError` when the wait deadline passes. After a timeout, resume with the saved id using `client.jobs.wait(jobId)` or `rc task`. A timeout alone does not require a new paid refresh. [Polling](https://docs.reversecontact.com/guides/polling) is recommended, and [webhooks](https://docs.reversecontact.com/guides/webhooks) are optional. ## FAQ ### How do I check when a person or company profile was last updated? Call Person profile status or Company profile status with the profile URL or id. The free response includes `data.exists` and `data.lastUpdate`, the timestamp of the last fetch from the source. ### Why does a free profile status check return 402 NO_CREDITS? Person profile status and Company profile status require at least 2 credits in the workspace even though the checks consume 0 credits. Below that balance, the API returns `402 NO_CREDITS`. ### How do I refresh only stale profiles with the API? Compare `data.lastUpdate` against your own freshness threshold. Start Person profile live or Company profile live only when the cache is stale or missing, then retrieve `data.result` by polling. ### How much does refreshing 200 profiles cost? Starting 200 live profile jobs consumes 400 credits at creation, before any failure refunds. Profile status checks and polling are free. ### Does a fresh status response update the profile stored in my CRM? No. Profile status reports the age of Reverse Contact's cache. Retrieve that cached profile with Person profile or Company profile if your CRM copy needs updating. A cached profile costs 1 credit when found. --- # Get a work email or a phone number > Find a work email or phone number from a Social profile URL with the API. Poll async results, check credit costs and verify existing email addresses. - Page: https://docs.reversecontact.com/use-cases/get-a-work-email-or-phone - Last verified: 2026-11-04 Find a work email or phone number from a Social profile URL, a person id, or a full name with a company domain. The lookup runs in the background, so save the job id and poll for the result. You can also verify an email address from another source. ## Which endpoint | Input you have | Endpoint | Cost | Mode | | --- | --- | --- | --- | | Profile URL, person id, or name and company domain | [Find Email](https://docs.reversecontact.com/endpoints/contact-email) | 3 credits for a professional email, 5 for a personal one, 8 when an `any` lookup finds both. A lookup with no match is free | Asynchronous | | Profile URL, person id, or name and company domain | [Find Phone](https://docs.reversecontact.com/endpoints/contact-phone) | 20 credits when at least one number is found. A lookup with no match is free | Asynchronous | | An email address to check | [Verify Email](https://docs.reversecontact.com/endpoints/contact-email-verify) | 1 credit per verdict, including `unknown`. A failed check is refunded | Asynchronous | ## How it works Set `RC_API_KEY` to your [API key](https://docs.reversecontact.com/authentication). Install the [TypeScript SDK](https://docs.reversecontact.com/sdk/javascript), [Python SDK](https://docs.reversecontact.com/sdk/python) or [CLI](https://docs.reversecontact.com/guides/cli) for your chosen example. The cURL examples also use `jq`. ### Start the lookup Send a profile `url` to Find Email, or replace `url` with `fullName: "Jane Doe"` and `companyDomain: "acme.com"`. Set `emailType: "professional"` to find a work email. For a phone lookup, use `/v2/contact/phone`, `client.contact.findPhone()` or `client.contact.find_phone()` with the same identity fields. Omit `emailType`. **Code Examples** ```bash [cURL] curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/contact/email \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.social.com/in/janedoe", "emailType": "professional"}' > contact-job.json export WEBHOOK_ID=$(jq -er .data.webhookId contact-job.json) printf 'Save this job id: %s\n' "$WEBHOOK_ID" # For a phone instead, post to /v2/contact/phone without emailType. ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.contact.findEmail({ url: "https://www.social.com/in/janedoe", emailType: "professional" }); // For a phone instead: client.contact.findPhone({ url: "https://www.social.com/in/janedoe" }) console.log("Save this job id:", job.id); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.contact.find_email( url="https://www.social.com/in/janedoe", email_type="professional" ) # For a phone instead: client.contact.find_phone(url="https://www.social.com/in/janedoe") print("Save this job id:", job.id) ``` ```bash [CLI] rc contact email --url https://www.social.com/in/janedoe --json > contact-job.json export WEBHOOK_ID=$(jq -er .data.webhookId contact-job.json) printf 'Save this job id: %s\n' "$WEBHOOK_ID" # For a phone instead: rc contact phone --url https://www.social.com/in/janedoe ``` ### Wait for the result Set the `WEBHOOK_ID` environment variable to the id from the first step. When the job completes, retrieve the email or phone array from `data.result`. Each entry contains `value` and `type`. Polling is free and recommended. For optional push delivery, pass `webhookUrl` when starting the lookup and follow the [Webhooks guide](https://docs.reversecontact.com/guides/webhooks). **Code Examples** ```bash [cURL] while :; do result=$(curl --fail-with-body -sS --max-time 30 \ "https://api.reversecontact.com/v2/webhooks/$WEBHOOK_ID?wait=20" \ -H "Authorization: Bearer $RC_API_KEY") || break case "$(printf '%s' "$result" | jq -r '.data.status')" in succeeded) printf '%s' "$result" | jq '.data.result'; break ;; errored) printf '%s' "$result" | jq '.data | {errorCode, creditsConsumed}'; break ;; created|in_progress) sleep 2 ;; *) printf '%s\n' "$result"; break ;; esac done ``` ```typescript [TypeScript SDK] import ReverseContact, { JobFailedError } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const jobId = process.env.WEBHOOK_ID; if (!jobId) throw new Error("Set WEBHOOK_ID to the saved job id"); try { const done = await client.jobs.wait(jobId); console.log(done.data.result, "credits:", done.data.creditsConsumed); } catch (error) { if (error instanceof JobFailedError) { console.log("Lookup failed:", error.errorCode, "credits:", error.creditsConsumed); } else { throw error; } } ``` ```python [Python SDK] import os from reversecontact import JobFailedError, ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: try: done = client.jobs.wait(os.environ["WEBHOOK_ID"]) if done.data is not None: print(done.data.result, "credits:", done.data.credits_consumed) except JobFailedError as error: print("Lookup failed:", error.error_code, "credits:", error.credits_consumed) ``` ```bash [CLI] rc task "$WEBHOOK_ID" --wait --timeout 300 ``` ### Verify before you send Use Verify Email if you want to check an address from a form, CRM or another source. Find Email already returns verified addresses, so you do not need to check those again. Poll the new job for `data.result.status`, one of `valid`, `risky`, `invalid` or `unknown`. **Code Examples** ```bash [cURL] curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/contact/email/verify \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "jane.doe@acme.com"}' > verify-job.json WEBHOOK_ID=$(jq -er .data.webhookId verify-job.json) printf 'Save this job id: %s\n' "$WEBHOOK_ID" while :; do result=$(curl --fail-with-body -sS --max-time 30 \ "https://api.reversecontact.com/v2/webhooks/$WEBHOOK_ID?wait=20" \ -H "Authorization: Bearer $RC_API_KEY") || break case "$(printf '%s' "$result" | jq -r '.data.status')" in succeeded) printf '%s' "$result" | jq '.data.result'; break ;; errored) printf '%s' "$result" | jq '.data | {errorCode, creditsConsumed}'; break ;; created|in_progress) sleep 2 ;; *) printf '%s\n' "$result"; break ;; esac done ``` ```typescript [TypeScript SDK] import ReverseContact, { JobFailedError } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.contact.verifyEmail({ email: "jane.doe@acme.com" }); console.log("Save this job id:", job.id); // resume after a restart with client.jobs.wait(id) try { // Long polls until the job ends, 30 minutes at most by default. const done = await job.wait(); console.log(done.data.result?.email, done.data.result?.status); } catch (error) { if (error instanceof JobFailedError) console.log("Ended without a verdict:", error.errorCode, "credits:", error.creditsConsumed); else throw error; } ``` ```python [Python SDK] import os from reversecontact import JobFailedError, ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.contact.verify_email(email="jane.doe@acme.com") print("Save this job id:", job.id) # resume after a restart with client.jobs.wait(id) try: # Long polls until the job ends, 30 minutes at most by default. done = job.wait() if done.data and done.data.result: print(done.data.result.email, done.data.result.status) except JobFailedError as error: print("Ended without a verdict:", error.error_code) ``` ## What it costs Find Email charges for the kind of email found: **3 credits for a professional email**, **5 for a personal one**, and **8 when an `any` lookup finds both**. At job creation, `professional` debits 3 credits. For `personal` and `any`, the API debits 5, then adjusts the balance to the result. An `any` request needs at least 8 credits available before starting. A lookup that finds no email is refunded. Find Phone costs **20 credits when at least one phone number is found**. A lookup with no match costs 0 credits. Verify Email costs **1 credit per verdict**, including `unknown`. A failed check without a verdict is refunded. Polling costs nothing. A webhook delivery failure remains billed when the lookup found contact data or verification produced a verdict. > [!NOTE] > 1,000 lookups that find 600 professional emails cost 600 × 3 = **1,800 credits**. The 400 that find nothing are refunded. Separately, 100 successful phone lookups cost **2,000 credits**, and checking 100 addresses from your CRM that each receive a verdict costs **100 credits**. ## Good to know Live jobs can take tens of minutes. Save the job id before waiting, and resume polling the same id after a timeout instead of starting a new paid lookup. The SDKs send an `Idempotency-Key` for contact calls. To retry across process restarts, save and reuse your own key with the same request. See [Find Email retries](https://docs.reversecontact.com/endpoints/contact-email#idempotency-key-header). For a whole contact file, use [Bulk enrichment](https://docs.reversecontact.com/guides/bulk-enrichment) or [keep contacts in a List](https://docs.reversecontact.com/guides/lists). Follow the [rate limit guide](https://docs.reversecontact.com/guides/rate-limits) when sending many lookups. ## FAQ ### How do I find a work email from a Social profile URL with the API? Send the profile `url` to `POST /v2/contact/email` with `emailType: "professional"`, then poll the returned job id. Find Email returns the address in `data.result`, with `value` and `type` for each email. ### How many credits does Find Email cost? Find Email costs 3 credits when a professional email is found and 5 for a personal one. An `any` lookup costs 3, 5 or 8 credits, with 8 charged when both kinds are found. A lookup that finds no email is refunded. ### Do I need a webhook to get a work email or phone number? No. Polling is the recommended way to retrieve Find Email and Find Phone results. Each poll costs 0 credits and needs no public HTTPS endpoint. For push delivery, pass `webhookUrl` in the request. ### Do I need to verify an email returned by Find Email? No. Find Email already returns verified addresses. Use Verify Email to check an address from a form, CRM or another source. Each verdict costs 1 credit, including `unknown`. ### How much does finding a phone number cost? Find Phone costs 20 credits when at least one phone number is found. A lookup with no match costs 0 credits, and reading the job result by polling is free. --- # Build a list of people > Build a prospect list with the people search API. Count matches for free, filter by title and location, use prompts or lookalikes, and cap credits. - Page: https://docs.reversecontact.com/use-cases/build-a-list-of-people - Last verified: 2026-11-04 Find people by job title, company and location, describe your audience in a prompt, or find people similar to a known profile. You can count a filtered search for free before retrieving profiles. Search returns full profiles synchronously from the index, within a budget you set. ## Which endpoint | Input you have | Endpoint | Cost | Mode | | --- | --- | --- | --- | | Filters to count matches before buying profiles | [People search count](https://docs.reversecontact.com/endpoints/search-people-count) | Free | Synchronous | | Explicit filters | [People search](https://docs.reversecontact.com/endpoints/search-people) | 1 credit per profile delivered. An empty page is free | Synchronous | | A description of the people to find | [Agentic Search](https://docs.reversecontact.com/endpoints/search-people-prompt) | 1 credit per page plus 1 per profile delivered | Synchronous | | One seed profile URL or person id | [People lookalike](https://docs.reversecontact.com/endpoints/search-people-lookalike) | 1 credit per profile delivered. An empty page is free | Synchronous | ## How it works Set `RC_API_KEY` to your [API key](https://docs.reversecontact.com/authentication). Install the [TypeScript SDK](https://docs.reversecontact.com/sdk/javascript), [Python SDK](https://docs.reversecontact.com/sdk/python) or [CLI](https://docs.reversecontact.com/guides/cli) for your chosen example. Shell examples use `jq`. Prompt and lookalike searches are alternatives to filtered search, each with its own charges. ### Count your target audience for free Count people whose current title matches `CTO` and whose location is France. The response contains `data.count` without any profiles. Leave `cursor` and `perPage` out of the count request, then use the same filters for the paid search. **Code Examples** ```bash [cURL] curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/search/people/count \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"currentPositionTitle": "CTO", "location": {"country": "France"}}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const count = await client.search.countPeople({ currentPositionTitle: "CTO", location: { country: "France" } }); console.log("Matching people:", count.data.count); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: count = client.search.count_people( current_position_title="CTO", location={"country": "France"} ) print("Matching people:", count.data.count if count.data else None) ``` ### Read profiles within a credit budget Retrieve at most 100 profiles for at most 100 credits. Request 25 at a time and reduce the final request to the remaining budget. The raw API returns profiles in `data.data` and the next cursor in `data.metadata.nextCursor`. SDK iterators handle pagination, and the CLI counts for free before searching. **Code Examples** ```bash [cURL] remaining=100 cursor="" filters='{"currentPositionTitle": "CTO", "location": {"country": "France"}}' while [ "$remaining" -gt 0 ]; do size=$((remaining < 25 ? remaining : 25)) body=$(printf '%s' "$filters" | jq --arg cursor "$cursor" --argjson size "$size" \ '. + {perPage: $size} + (if $cursor == "" then {} else {cursor: $cursor} end)') result=$(curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/search/people \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" -d "$body") || break printf '%s' "$result" | jq -c '.data.data[]' delivered=$(printf '%s' "$result" | jq -er '.data.data | length') remaining=$((remaining - delivered)) next_cursor=$(printf '%s' "$result" | jq -r '.data.metadata.nextCursor // empty') [ -z "$next_cursor" ] && break [ "$next_cursor" = "$cursor" ] && { echo "Repeated cursor" >&2; break; } cursor=$next_cursor done ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); for await (const person of client.search.iterPeople( { currentPositionTitle: "CTO", location: { country: "France" }, perPage: 25 }, { maxResults: 100 } )) { console.log(person.firstName, person.lastName); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: for person in client.search.iter_people( current_position_title="CTO", location={"country": "France"}, per_page=25, max_results=100, ): if person is not None: print(person.first_name, person.last_name) ``` ```bash [CLI] remaining=100 cursor="" while [ "$remaining" -gt 0 ]; do size=$((remaining < 25 ? remaining : 25)) if [ -n "$cursor" ]; then result=$(rc search people --title CTO --country France --limit "$size" --cursor "$cursor" --json) || break else result=$(rc search people --title CTO --country France --limit "$size" --json) || break fi printf '%s' "$result" | jq -c '.data.data[]' delivered=$(printf '%s' "$result" | jq -er '.data.data | length') remaining=$((remaining - delivered)) next_cursor=$(printf '%s' "$result" | jq -r '.data.metadata.nextCursor // empty') [ -z "$next_cursor" ] && break [ "$next_cursor" = "$cursor" ] && { echo "Repeated cursor" >&2; break; } cursor=$next_cursor done ``` ### Describe the audience in a prompt You can use Agentic Search when your input is a sentence. Inspect `data.interpretation.filters` and `warnings` before buying more pages. This example requests one page of up to 25 profiles, which costs at most 26 credits. Do not add explicit search filters alongside `prompt`. **Code Examples** ```bash [cURL] curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/search/people/prompt \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "CTOs at software companies in France", "perPage": 25}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const page = await client.search.peopleByPrompt({ prompt: "CTOs at software companies in France", perPage: 25 }); console.log("Filters:", page.interpretation?.filters); console.log("Warnings:", page.interpretation?.warnings); console.log(page.data, "credits:", page.quotas?.creditsConsumed); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: page = client.search.people_by_prompt( prompt="CTOs at software companies in France", per_page=25 ) if page.interpretation is not None: print("Filters:", page.interpretation.filters) print("Warnings:", page.interpretation.warnings) print(page.data, "next cursor:", page.next_cursor) ``` ### Find people similar to a seed profile You can use People lookalike with exactly one indexed seed `url` or person `id` and no extra search filters. Results exclude the seed. This example retrieves at most 50 lookalikes for at most 50 credits. **Code Examples** ```bash [cURL] remaining=50 cursor="" filters='{"url": "https://www.social.com/in/janedoe"}' while [ "$remaining" -gt 0 ]; do size=$((remaining < 25 ? remaining : 25)) body=$(printf '%s' "$filters" | jq --arg cursor "$cursor" --argjson size "$size" \ '. + {perPage: $size} + (if $cursor == "" then {} else {cursor: $cursor} end)') result=$(curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/search/people/lookalike \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" -d "$body") || break printf '%s' "$result" | jq -c '.data.data[]' delivered=$(printf '%s' "$result" | jq -er '.data.data | length') remaining=$((remaining - delivered)) next_cursor=$(printf '%s' "$result" | jq -r '.data.metadata.nextCursor // empty') [ -z "$next_cursor" ] && break [ "$next_cursor" = "$cursor" ] && { echo "Repeated cursor" >&2; break; } cursor=$next_cursor done ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); for await (const person of client.search.iterPeopleLookalike( { url: "https://www.social.com/in/janedoe", perPage: 25 }, { maxResults: 50 } )) { console.log(person.firstName, person.lastName); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: for person in client.search.iter_people_lookalike( url="https://www.social.com/in/janedoe", per_page=25, max_results=50 ): if person is not None: print(person.first_name, person.last_name) ``` ## What it costs People search and People lookalike cost **1 credit per profile delivered**. An empty page costs nothing, and counting a filtered search is free. Every result is a full profile, so no second fetch is needed to get the profile fields. Agentic Search costs **1 credit per page for the search, plus 1 credit per profile delivered**. The page fee still applies when no profile matches. SDK iterators use `maxResults` / `max_results` to cap returned profiles, but this limit does not cover the extra page fees. Budget prompt pages separately or set an [API key credit limit](https://docs.reversecontact.com/guides/api-key-management). > [!NOTE] > Count a query for free, then retrieve 100 profiles over four full pages of 25: filtered search costs **100 credits**. The same four full pages through Agentic Search cost 100 + 4 = **104 credits**. Each additional empty prompt page costs **1 credit**. ## Good to know Filters combine with AND. Supported arrays such as `currentPositionTitle` match any listed value. Title matching is token-based, so `CTO` can also match `Deputy CTO`. Use `currentCompanyIndustry` values from [Industry values](https://docs.reversecontact.com/reference/industries), and full `location.area` ids such as `MC:DACH` from [Area values](https://docs.reversecontact.com/reference/areas). The `currentFunction` filter accepts values such as `Marketing`, and `currentSeniority` accepts `Director` and `VP`. Check the [People search filter reference](https://docs.reversecontact.com/endpoints/search-people#request-body) for all accepted values and exclusions. For headcount, `currentCompanyEmployeeCountRange` matches whole size brackets: `{ "min": 51, "max": 200 }` matches the 51 to 200 bracket. Unknown fields and invalid closed-list values return `422` and cost 0 credits. Search count calls still count toward your [rate limits](https://docs.reversecontact.com/guides/rate-limits). Request 1 to 100 profiles per page, 25 by default. Keep filters unchanged and pass the opaque cursor back as `cursor`. Stop when `data.metadata.nextCursor` is `null` or your budget is spent. A short or empty page can still have a next cursor. For filtered People search, read successive pages within 10 minutes for a consistent cached result set. The [pagination reference](https://docs.reversecontact.com/endpoints/search-people#pagination-and-credits) describes what happens after that. For prompt searches, reuse the same prompt on the next page. You can send `data.interpretation.filters` to People search count for a free total after the first paid prompt page. A lookalike seed must already be indexed. An unindexed URL returns a free `404`. Search uses cached data, so use [profile freshness checks](https://docs.reversecontact.com/use-cases/keep-profiles-fresh) when you need a refresh. The examples print profiles for your application to store. To save people in a persistent Reverse Contact List and buy contact data, use [Lists: orchestrate enrichment](https://docs.reversecontact.com/guides/lists). For individual profiles, see [Get a work email or a phone number](https://docs.reversecontact.com/use-cases/get-a-work-email-or-phone). ## FAQ ### How do I count people matching search filters without spending credits? Send the People search filters to `POST /v2/search/people/count`, without `cursor` or `perPage`. People search count returns `data.count` and costs 0 credits, but the call counts toward request rate limits. ### How do I cap the credits spent on a people search? For filtered People search, request no more profiles than your remaining credit budget, or use an SDK iterator with `maxResults` / `max_results`. Each delivered profile costs 1 credit. Agentic Search adds 1 credit per page, so budget those pages separately or set an API key credit limit. ### Does an empty people search page mean there are no more results? No. A search page can be empty while `data.metadata.nextCursor` is still set. Continue with that cursor until the cursor is `null` or your budget is spent. ### Can I search for people using a sentence instead of filters? Yes. Agentic Search accepts a `prompt` and returns the applied filters and warnings in `data.interpretation`. Each page costs 1 credit plus 1 credit per profile delivered. You cannot add explicit search filters alongside the prompt. ### How do I find people similar to a Social profile? Send exactly one seed `url` or person `id` to People lookalike. The seed must already be indexed and is excluded from the results. Each delivered lookalike costs 1 credit. People lookalike does not accept additional search filters. --- # Build a list of companies > Build a company list with the search API. Count matches for free, filter by industry, headcount and location, use prompts, and cap credit spending. - Page: https://docs.reversecontact.com/use-cases/build-a-list-of-companies - Last verified: 2026-11-04 Find companies by industry, headcount and headquarters location, or describe the companies you want in a sentence. You can count a filtered search for free before retrieving profiles. Search returns full company profiles synchronously from the index, within your credit budget. ## Which endpoint | Input you have | Endpoint | Cost | Mode | | --- | --- | --- | --- | | Filters to count matches before buying profiles | [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count) | Free | Synchronous | | Explicit company filters | [Company search](https://docs.reversecontact.com/endpoints/search-companies) | 1 credit per company delivered. An empty page is free | Synchronous | | A description of the companies to find | [Agentic Company Search](https://docs.reversecontact.com/endpoints/search-companies-prompt) | 1 credit per page plus 1 per company delivered | Synchronous | The API routes use `/v2/search/organizations`. The reference pages linked above keep their existing company-search addresses. ## How it works Set `RC_API_KEY` to your [API key](https://docs.reversecontact.com/authentication). Install the [TypeScript SDK](https://docs.reversecontact.com/sdk/javascript), [Python SDK](https://docs.reversecontact.com/sdk/python) or [CLI](https://docs.reversecontact.com/guides/cli) for your chosen example. Shell examples use `jq`. Prompt search is an alternative to filtered search, with its own charges. ### Count the companies that match Count software companies headquartered in France, in the 51 to 200 employee bracket, founded in 2015 or later. Read `data.count` and reuse the same filters for the paid search. Leave `cursor` and `perPage` out of the count request. **Code Examples** ```bash [cURL] curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/search/organizations/count \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"industry": "Software Development", "employeeCountRange": {"min": 51, "max": 200}, "foundedYear": {"min": 2015}, "location": {"countryCode": "FR"}}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const count = await client.search.countOrganizations({ industry: "Software Development", employeeCountRange: { min: 51, max: 200 }, foundedYear: { min: 2015 }, location: { countryCode: "FR" } }); console.log("Matching companies:", count.data.count); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: count = client.search.count_organizations( industry="Software Development", employee_count_range={"min": 51, "max": 200}, founded_year={"min": 2015}, location={"countryCode": "FR"}, ) print("Matching companies:", count.data.count if count.data else None) ``` ### Retrieve the company profiles within budget Retrieve at most 100 companies for at most 100 credits. Request pages of up to 25 companies, with a smaller final request when needed. The raw API returns companies in `data.data` and the cursor in `data.metadata.nextCursor`. The SDKs handle pagination, and the CLI counts for free before searching. **Code Examples** ```bash [cURL] remaining=100 cursor="" filters='{"industry": "Software Development", "employeeCountRange": {"min": 51, "max": 200}, "foundedYear": {"min": 2015}, "location": {"countryCode": "FR"}}' while [ "$remaining" -gt 0 ]; do size=$((remaining < 25 ? remaining : 25)) body=$(printf '%s' "$filters" | jq --arg cursor "$cursor" --argjson size "$size" \ '. + {perPage: $size} + (if $cursor == "" then {} else {cursor: $cursor} end)') result=$(curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/search/organizations \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" -d "$body") || break printf '%s' "$result" | jq -c '.data.data[]' delivered=$(printf '%s' "$result" | jq -er '.data.data | length') remaining=$((remaining - delivered)) next_cursor=$(printf '%s' "$result" | jq -r '.data.metadata.nextCursor // empty') [ -z "$next_cursor" ] && break [ "$next_cursor" = "$cursor" ] && { echo "Repeated cursor" >&2; break; } cursor=$next_cursor done ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); for await (const company of client.search.iterOrganizations( { industry: "Software Development", employeeCountRange: { min: 51, max: 200 }, foundedYear: { min: 2015 }, location: { countryCode: "FR" }, perPage: 25 }, { maxResults: 100 } )) { console.log(company.companyName, company.websiteUrl); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: for company in client.search.iter_organizations( industry="Software Development", employee_count_range={"min": 51, "max": 200}, founded_year={"min": 2015}, location={"countryCode": "FR"}, per_page=25, max_results=100, ): if company is not None: print(company.company_name, company.website_url) ``` ```bash [CLI] remaining=100 cursor="" while [ "$remaining" -gt 0 ]; do size=$((remaining < 25 ? remaining : 25)) if [ -n "$cursor" ]; then result=$(rc search organizations --industry "Software Development" --size 51-200 --founded-year 2015- --country-code FR --limit "$size" --cursor "$cursor" --json) || break else result=$(rc search organizations --industry "Software Development" --size 51-200 --founded-year 2015- --country-code FR --limit "$size" --json) || break fi printf '%s' "$result" | jq -c '.data.data[]' delivered=$(printf '%s' "$result" | jq -er '.data.data | length') remaining=$((remaining - delivered)) next_cursor=$(printf '%s' "$result" | jq -r '.data.metadata.nextCursor // empty') [ -z "$next_cursor" ] && break [ "$next_cursor" = "$cursor" ] && { echo "Repeated cursor" >&2; break; } cursor=$next_cursor done ``` ### Search from a description You can use Agentic Company Search with a `prompt`. Inspect `data.interpretation.filters` and `warnings` before continuing. This example requests one page of up to 25 companies for at most 26 credits. Describe every criterion in the prompt instead of adding explicit filter fields. **Code Examples** ```bash [cURL] curl --fail-with-body -sS -X POST https://api.reversecontact.com/v2/search/organizations/prompt \ -H "Authorization: Bearer $RC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "Software companies headquartered in France with 51 to 200 employees, founded since 2015", "perPage": 25}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const page = await client.search.organizationsByPrompt({ prompt: "Software companies headquartered in France with 51 to 200 employees, founded since 2015", perPage: 25 }); console.log("Filters:", page.interpretation?.filters); console.log("Warnings:", page.interpretation?.warnings); console.log(page.data, "credits:", page.quotas?.creditsConsumed); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: page = client.search.organizations_by_prompt( prompt="Software companies headquartered in France with 51 to 200 employees, founded since 2015", per_page=25, ) if page.interpretation is not None: print("Filters:", page.interpretation.filters) print("Warnings:", page.interpretation.warnings) print(page.data, "next cursor:", page.next_cursor) ``` ## What it costs Company search costs **1 credit per company delivered**. An empty page costs nothing, and Company search count is free. The full company profile is included, so you do not need a second fetch to retrieve its fields. Agentic Company Search costs **1 credit per page for the search, plus 1 credit per company delivered**. The 1-credit page fee also applies to empty pages. Before a request runs, your available balance must cover `perPage` credits for filtered search, or `perPage + 1` for a prompt search. Otherwise, the API returns `402 INSUFFICIENT_CREDITS` without charging. > [!NOTE] > Count a query for free, then retrieve 100 companies over four full pages of 25: filtered search costs **100 credits**. The same four full pages through Agentic Company Search cost 100 + 4 = **104 credits**. An additional empty prompt page adds **1 credit**. SDK iterators use `maxResults` / `max_results` to cap profiles, but this limit does not cover the extra page fees of Agentic Company Search. Limit prompt requests page by page, or configure an [API key credit limit](https://docs.reversecontact.com/guides/api-key-management) to cap total spending. ## Good to know Company search and Company search count require at least one filter. The `maxDataAgeDate` filter cannot be used alone. Filters combine with AND. Use `industry` values such as `Software Development` from [Industry values](https://docs.reversecontact.com/reference/industries), and full `location.area` ids such as `MC:DACH` from [Area values](https://docs.reversecontact.com/reference/areas). Put country codes in `location.countryCode` (`FR`) and country names in `location.country` (`France`). The `employeeCountRange` filter matches whole size brackets. For example, `{ "min": 51, "max": 200 }` matches the 51 to 200 bracket. A range that contains no complete bracket returns no companies. Unknown fields and invalid closed-list values return `422` and cost 0 credits. The [Company search request reference](https://docs.reversecontact.com/endpoints/search-companies#request-body) lists the filters and their accepted values. Request 1 to 100 companies per page, 25 by default. Keep filters unchanged and pass `data.metadata.nextCursor` back as `cursor`. Stop only when the cursor is `null` or your budget is spent. A short or empty page can still have a next cursor, and there is no `page` parameter. For prompt search, resend the same prompt. After the first paid prompt page, send `data.interpretation.filters` to Company search count to get a free total. Search uses cached profiles. See [Keep profiles fresh](https://docs.reversecontact.com/use-cases/keep-profiles-fresh) when you need current company data, and check the [rate limits](https://docs.reversecontact.com/guides/rate-limits) when paging at scale. Search count calls still count toward those request limits. The examples print profiles for your application to store. To save companies in a persistent Reverse Contact List, use [Lists: orchestrate enrichment](https://docs.reversecontact.com/guides/lists). ## FAQ ### How do I build a company list by industry and employee count? Send `industry` and `employeeCountRange` to Company search count to count matches for free. Use the same filters with `POST /v2/search/organizations` to retrieve full company profiles at 1 credit per company delivered. ### Why does company search return no results for my employee range? The `employeeCountRange` filter must contain whole size brackets. A range such as 51 to 200 matches that bracket. A range that contains no complete bracket returns no companies. ### How do I cap company search credits? For filtered Company search, use an SDK iterator with `maxResults` / `max_results` or request no more companies than the remaining budget. Agentic Company Search adds 1 credit per page, so budget those fees separately or use an API key credit limit. ### Can I search for companies with a plain-language prompt? Yes. Agentic Company Search accepts a `prompt` and returns the interpreted filters and warnings. Each page costs 1 credit plus 1 per company delivered. You cannot add explicit filter fields alongside the prompt. ### How do I get the next page of company search results? Send `data.metadata.nextCursor` as `cursor`. Keep the same filters for Company search, or the same prompt for Agentic Company Search. Continue until the cursor is `null` or your budget is spent. An empty page alone does not mean the search has ended. --- # Endpoint transition on October 13, 2026 > /v2/search/persons is succeeded by /v2/search/people, and /v2/resolve by /v2/enrich. Both old endpoints were retired on October 13, 2026. - Page: https://docs.reversecontact.com/guides/endpoint-transition-october-2026 - Last verified: 2026-10-15 We've replaced two of the endpoints your integration might use: `/v2/search/persons` is succeeded by `/v2/search/people`, and `/v2/resolve/` is succeeded by `/v2/enrich/`. Both old endpoints were retired on October 13, 2026. > [!IMPORTANT] > Since October 13, 2026, calls to `/v2/search/persons`, `/v2/search/persons/available-fields` and the `/v2/resolve/` endpoints return `410 Gone` with the error code `ENDPOINT_GONE`, and `error.details.alternative` names the endpoint to call instead. These calls are not processed and consume no credits. ## What changes on search - Search returns the full profile directly, so the separate fetch call after each search disappears. - Search with no result costs nothing. - A free endpoint, `/v2/search/people/count`, tells you how many matches a query has before you spend anything. - Billing moves from per page to per profile delivered, and you control how many profiles come back with `perPage`, from 1 to 100. One profile returned costs 1 credit. - Pagination moves to a cursor: there's no page number anymore. [Migrating people search from persons to people](https://docs.reversecontact.com/guides/migrating-search-persons-to-people) walks through the migration step by step. ## Endpoints to call instead of resolve | Retired endpoint | Call instead | | ---------------- | ------------ | | `POST /v2/resolve/persons/email` | [`POST /v2/enrich/persons`](https://docs.reversecontact.com/endpoints/enrich-person) | | `POST /v2/resolve/persons/name` | [`POST /v2/enrich/persons`](https://docs.reversecontact.com/endpoints/enrich-person) | | `POST /v2/resolve/companies/live` | [`POST /v2/enrich/companies`](https://docs.reversecontact.com/endpoints/enrich-company) | ## Documentation for the new endpoints - [Search people](https://docs.reversecontact.com/endpoints/search-people) - [Search people count](https://docs.reversecontact.com/endpoints/search-people-count) - [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person) - [Enrich company](https://docs.reversecontact.com/endpoints/enrich-company) If you have any questions, please contact [support@reversecontact.com](mailto:support@reversecontact.com). ## FAQ ### Why do /v2/search/persons and /v2/resolve return 410 Gone? `/v2/search/persons` and `/v2/resolve/` were retired on October 13, 2026: `/v2/search/persons` is succeeded by `/v2/search/people`, and `/v2/resolve/` by `/v2/enrich/`. Since then, calls to `/v2/search/persons`, `/v2/search/persons/available-fields` and the `/v2/resolve/` endpoints return `410 Gone` with the error code `ENDPOINT_GONE`, and `error.details.alternative` names the endpoint to call instead. These calls are not processed and consume no credits. ### What replaces the /v2/resolve endpoint? `POST /v2/enrich/persons` replaces `/v2/resolve/persons/email` and `/v2/resolve/persons/name`, and `POST /v2/enrich/companies` replaces `/v2/resolve/companies/live`. See [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person) and [Enrich company](https://docs.reversecontact.com/endpoints/enrich-company). ### How much does /v2/search/people cost? `/v2/search/people` bills per profile delivered: one profile returned costs 1 credit, and `perPage`, from 1 to 100, controls how many profiles come back. A search with no result costs nothing, and the free `/v2/search/people/count` endpoint tells you how many matches a query has before you spend anything. ### Do I still need a fetch call after a people search? No. `/v2/search/people` returns the full profile directly, so the separate fetch call after each search disappears. ### How does pagination work in /v2/search/people? `/v2/search/people` paginates with a cursor, and there is no page number anymore. The `perPage` parameter, from 1 to 100, controls how many profiles come back. See [Migrating people search from persons to people](https://docs.reversecontact.com/guides/migrating-search-persons-to-people). --- # Migrating company search to the new endpoint > Move company search calls to /v2/search/organizations before /v2/search/companies goes into maintenance on November 9, 2026. - Page: https://docs.reversecontact.com/guides/migrating-search-companies-to-organizations - Last verified: 2026-10-16 `POST /v2/search/companies` goes into maintenance on **Monday, November 9, 2026**, for a few weeks. Move your company searches to `POST /v2/search/organizations` before that date: it is available now, returns full company profiles instead of search hits, paginates with cursors instead of page numbers, accepts three new filters, and has a free count endpoint. Until November 8, 2026, `/v2/search/companies` keeps working as it does today. From November 9, 2026 and during the maintenance, calls to `/v2/search/companies` and `/v2/search/companies/available-fields` return `410` with the error code `ENDPOINT_GONE`, `error.details.alternative` naming the endpoint to call instead and `error.details.docs` linking this guide. These calls are not processed and consume no credits. After the maintenance, `/v2/search/companies` comes back with a format compatible with `/v2/search/organizations`, so an integration you move to `/v2/search/organizations` now will not need a second migration. ## Timeline | Date | `/v2/search/companies` | `/v2/search/organizations` | | ---- | ---------------------- | -------------------------- | | Until November 8, 2026 | Works as today | Available | | From November 9, 2026, for a few weeks | Maintenance: `410` with `ENDPOINT_GONE`, 0 credits | Available | | After the maintenance | Back, in a format compatible with `/v2/search/organizations` | Available | ## What changes | Area | `POST /v2/search/companies` | `POST /v2/search/organizations` | | ---- | --------------------------- | ------------------------------- | | Path | `/v2/search/companies` | `/v2/search/organizations` | | Results | Search hits (name, size, industry, city and country) | Full company profiles, the same shape as the [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) endpoint | | Pagination | `page` and `perPage` (default `100`) | `cursor` and `perPage` (default `25`) | | Metadata | `currentPage`, `pageNumber`, `perPage`, `total` | `perPage`, `count`, `nextCursor` | | Total matches | `data.metadata.total` on every response | Free [count endpoint](https://docs.reversecontact.com/endpoints/search-companies-count) | | Pricing | 1 credit per page | 1 credit per company delivered | | Filters | `companyName`, `companyDomain`, `industry`, `employeeCountRange`, `location`, `maxDataAgeDate` | The same, plus `specialities`, `foundedYear` and `followersCount` | | Available fields | `GET /v2/search/companies/available-fields` | `GET /v2/search/organizations/available-fields` | ## Paths | Before | After | | ------ | ----- | | `POST /v2/search/companies` | `POST /v2/search/organizations` | | `GET /v2/search/companies/available-fields` | `GET /v2/search/organizations/available-fields` | | (no equivalent) | `POST /v2/search/organizations/count` | The method and the `Authorization: Bearer YOUR_API_KEY` header stay the same. Only the path changes. ## Results are full profiles The previous endpoint returned a light search hit, and the documented flow was to chain into the [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) endpoint for the full data of a hit. `/v2/search/organizations` returns complete profiles directly, so the second call is no longer needed. Before, a hit looked like this: ```json { "id": "com_01jbd5vewyenebxgaz869ffe5t", "name": "Acme Corp", "publicId": "acme", "linkedinId": "12345678", "linkedinUrl": "https://www.social.com/company/acme", "employeesCount": 452, "followersCount": 28512, "employeeCountRange": { "start": 201, "end": 500 }, "websiteUrl": "https://www.acmecorp.com", "tagline": "Building the future of work", "industry": "Software Development", "companyUpdateDate": "2026-03-18T09:12:44.000Z", "location": { "city": "San Francisco", "country": "US" } } ``` After, each item of `data.data` is a full company profile. Most fields keep their name (`id`, `publicId`, `linkedinId`, `linkedinUrl`, `employeesCount`, `followersCount`, `employeeCountRange`, `websiteUrl`, `tagline`, `industry`), three change, and the profile adds `description`, `phone`, `specialities`, `logoUrl`, `backgroundUrl`, `foundedOn` and `locations`: | Search hit | Full profile | | ---------- | ------------ | | `name` | `companyName` | | `location.city`, `location.country` | `headquarter.city`, `headquarter.country` (still an ISO code), next to the street, postal code and region of the headquarters | | `companyUpdateDate` | Not part of the full profile. Filter on freshness with `maxDataAgeDate`, or read `lastUpdate` from the free [Company profile status](https://docs.reversecontact.com/endpoints/check-company) endpoint | If you chained Company profile calls after a search, drop them: the data is already in the search response. ## Pagination moves from pages to cursors The previous endpoint used `page` (1 to 50) and `perPage` (1 to 100, default `100`), and reported `currentPage`, `pageNumber` and `total` in `data.metadata`. Company search uses cursor pagination: - `perPage` stays, with a new default of `25` (still 1 to 100). - `page` is gone, and sending it returns `422` with `UNKNOWN_FIELD`. Send the `nextCursor` value from the previous response as `cursor` to get the next page, with the same filters. - `nextCursor` is `null` on the last page. - The cursor is opaque: send it back unchanged, without parsing or altering it. - The 50-page limit is gone: a cursor reaches every matching company. Before: ```javascript const response = await fetch("https://api.reversecontact.com/v2/search/companies", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ industry: "Software Development", page: 2, perPage: 100 }) }); const payload = await response.json(); const hits = payload.data.data; console.log(`Page ${payload.data.metadata.currentPage} of ${payload.data.metadata.pageNumber}, ${payload.data.metadata.total} total`); ``` After: ```javascript const searchPage = async (cursor) => { const response = await fetch("https://api.reversecontact.com/v2/search/organizations", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ industry: "Software Development", perPage: 25, cursor }) }); return response.json(); }; const page = await searchPage(); console.log(`${page.data.metadata.count} companies in this page`); if (page.data.metadata.nextCursor) { const nextPage = await searchPage(page.data.metadata.nextCursor); } ``` ## Totals move to the count endpoint The previous `data.metadata.total` field is gone: `data.metadata.count` is the number of companies in the current page, not the size of the whole matching set. To size a query, call the free [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count) endpoint with the same filters: ```bash curl -X POST https://api.reversecontact.com/v2/search/organizations/count \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "industry": "Software Development" }' ``` The response is `data.count`, the total number of matching companies across all pages, and it costs 0 credits. ## Pricing changes The previous endpoint charged 1 credit per page, whatever the page contained, including a page with no result. Company search charges 1 credit per company delivered: a page that returns 25 companies costs 25 credits, and a page that returns no company costs 0 credits. Rejected filters still cost 0 credits. The same volume of results can therefore cost more than before when your pages are full, and less when many of your searches come back empty. To keep your spending under control: - Call the free [count endpoint](https://docs.reversecontact.com/endpoints/search-companies-count) first, with the same filters, to know how many companies a search will return before you spend anything. - Start with a small `perPage`, such as `10`, and raise it once you know your volumes. `perPage` caps what a single request can spend. - Keep sending searches that may find nothing: a search with no result costs 0 credits, where the previous endpoint billed 1 credit for an empty page. A request needs as many credits available as its `perPage` before it runs, otherwise it returns `402` with `INSUFFICIENT_CREDITS`. Only the companies delivered are then charged. ## Filter changes Existing filters keep their names and their values: `companyName`, `companyDomain`, `industry` (from the same [industry list](https://docs.reversecontact.com/reference/industries)), `employeeCountRange` (the same headcount brackets), `location` (`country`, `countryCode`, `region` and `city`, matched on the headquarters) and `maxDataAgeDate`. Company search also accepts filters the previous endpoint did not have: | Filter | What it does | | ------ | ------------ | | `specialities` | Match the specialities listed on the company profile, one value or a list (OR-matched) | | `foundedYear` | Keep companies founded within a year range, from 0 to 2100 | | `followersCount` | Keep companies within a follower count range | Two rules are stricter. A request needs at least one filter, and `maxDataAgeDate` cannot be that only filter: both return `422` with `NO_FILTER_PROVIDED`. The rest of the validation works as before: unknown fields, blank values and values outside a closed list return `422` with suggestions and cost 0 credits. Every filter is documented on the [Company search](https://docs.reversecontact.com/endpoints/search-companies#request-body) page. ## Available fields moves too If you discover filters dynamically, switch `GET /v2/search/companies/available-fields` to `GET /v2/search/organizations/available-fields`. The response keeps its shape, with two differences: `data.entity` is `"organization"`, and `data.fields` lists `cursor` instead of `page`, next to every accepted filter and the `allowedValues` of closed vocabularies such as industries. ## Migration checklist 1. Replace the search path with `POST /v2/search/organizations` and the available-fields path with `GET /v2/search/organizations/available-fields`. 2. Replace `page` with cursor handling: loop while `data.metadata.nextCursor` is not `null`. 3. Review your `perPage`: the default is now `25`, each delivered company costs 1 credit, and the balance must cover `perPage` before the call. 4. Replace `data.metadata.total` reads with a call to `POST /v2/search/organizations/count`. 5. Map the renamed fields: `name` becomes `companyName`, `location` becomes `headquarter`, and `companyUpdateDate` is gone. 6. Drop the Company profile calls you used to chain after a search: results are already full profiles. 7. Make sure every request carries at least one filter other than `maxDataAgeDate`. ## FAQ ### Does `/v2/search/companies` still work? Yes, until November 8, 2026. `POST /v2/search/companies` goes into maintenance on Monday, November 9, 2026, for a few weeks: during the maintenance, its calls return `410` with the error code `ENDPOINT_GONE`, consume no credits and name `POST /v2/search/organizations` in `error.details.alternative`. It then comes back with a format compatible with `POST /v2/search/organizations`, so move to `POST /v2/search/organizations` now: that integration will not need a second migration. ### How do I get the total number of matching companies with `/v2/search/organizations`? `POST /v2/search/organizations` does not return a total: `data.metadata.count` is only the number of companies in the current page. Call `POST /v2/search/organizations/count` with the same filters to get `data.count`, the total number of matching companies across all pages, for 0 credits. See [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count). ### How many credits does a company search cost? `POST /v2/search/organizations` charges 1 credit per company delivered: a page that returns 25 companies costs 25 credits, and a page that returns no company costs 0 credits. Rejected filters also cost 0 credits, and lowering `perPage` caps what a single request can spend. ### Which result fields change name in `/v2/search/organizations`? In `POST /v2/search/organizations` results, `name` becomes `companyName` and `location` becomes `headquarter`, whose `country` is still an ISO code. `companyUpdateDate` is not part of the full profile: filter with `maxDataAgeDate`, or read `lastUpdate` from the free Company profile status endpoint. ### Do I still need to call the Company profile endpoint after a company search? No. Each item of `data.data` in a `POST /v2/search/organizations` response is already a full company profile, with `description`, `specialities`, `headquarter`, `locations`, `foundedOn` and the other Company profile fields, so the Company profile calls you chained after a search can be dropped. See [Company profile](https://docs.reversecontact.com/endpoints/fetch-company). --- # Migrating people search from persons to people > Move people search calls from /v2/search/persons, retired on October 13, 2026, to /v2/search/people: cursor pagination, per-profile pricing and new filters. - Page: https://docs.reversecontact.com/guides/migrating-search-persons-to-people - Last verified: 2026-10-14 People search moved to `POST /v2/search/people`. It returns full profiles instead of search hits, paginates with cursors instead of page numbers, accepts a larger filter set, and has a free count endpoint. The previous endpoint, `POST /v2/search/persons`, was retired on October 13, 2026 and now returns `410 Gone`. It is no longer documented in the endpoint catalog. This guide lists everything that changes when you move to `/v2/search/people`, and the [endpoint transition overview](https://docs.reversecontact.com/guides/endpoint-transition-october-2026) sums up the retirement. ## What changes | Area | `POST /v2/search/persons` | `POST /v2/search/people` | | ---- | ------------------------- | ------------------------ | | Path | `/v2/search/persons` | `/v2/search/people` | | Results | Search hits (name, title, company, location) | Full profiles, the same shape as the [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) endpoint | | Pagination | `page` and `perPage` (default `100`) | `cursor` and `perPage` (default `25`) | | Metadata | `currentPage`, `pageNumber`, `perPage`, `total` | `perPage`, `count`, `nextCursor` | | Total matches | `data.metadata.total` on every response | Free [count endpoint](https://docs.reversecontact.com/endpoints/search-people-count) | | Pricing | 1 credit per page | 1 credit per profile delivered | | Available fields | `GET /v2/search/persons/available-fields` | `GET /v2/search/people/available-fields` | ## Paths | Before | After | | ------ | ----- | | `POST /v2/search/persons` | `POST /v2/search/people` | | `GET /v2/search/persons/available-fields` | `GET /v2/search/people/available-fields` | | (no equivalent) | `POST /v2/search/people/count` | The method and the `Authorization: Bearer YOUR_API_KEY` header stay the same. Only the path changes. ## Results are full profiles The previous endpoint returned a light search hit, and the documented flow was to chain into the [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) endpoint for the full data of a hit. People search returns complete profiles directly, so the second call is no longer needed. Before, a hit looked like this: ```json { "id": "prs_01h2x3y4z5a6b7c8d9e0f1g2h3", "publicId": "janedoe", "linkedinUrl": "https://www.social.com/in/janedoe", "firstName": "Jane", "lastName": "Doe", "headline": "VP of Marketing @ Acme Corp", "currentPositionTitle": "VP of Marketing", "currentCompanyName": "Acme Corp", "updateDate": "2026-03-18T09:12:44.000Z" } ``` After, each item of `data.data` is a full profile, with `currentPosition`, `experience`, `education`, `skills` and the other sections documented on the [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) endpoint. If you chained Person profile calls after a search, drop them: the data is already in the search response. ## Pagination moves from pages to cursors The previous endpoint used `page` (1 to 50) and `perPage` (1 to 100, default `100`), and reported `currentPage`, `pageNumber` and `total` in `data.metadata`. People search uses cursor pagination: - `perPage` stays, with a new default of `25` (still 1 to 100). - `page` is gone. Send the `nextCursor` value from the previous response as `cursor` to get the next page. - `nextCursor` is `null` on the last page. - The cursor is opaque: send it back unchanged, without parsing or altering it. Before: ```javascript const response = await fetch("https://api.reversecontact.com/v2/search/persons", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ currentPositionTitle: "VP of Marketing", page: 2, perPage: 100 }) }); const payload = await response.json(); const hits = payload.data.data; console.log(`Page ${payload.data.metadata.currentPage} of ${payload.data.metadata.pageNumber}, ${payload.data.metadata.total} total`); ``` After: ```javascript const searchPage = async (cursor) => { const response = await fetch("https://api.reversecontact.com/v2/search/people", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ currentSeniority: "VP", perPage: 25, cursor }) }); return response.json(); }; const page = await searchPage(); console.log(`${page.data.metadata.count} profiles in this page`); if (page.data.metadata.nextCursor) { const nextPage = await searchPage(page.data.metadata.nextCursor); } ``` ## Totals move to the count endpoint The previous `data.metadata.total` field is gone: `data.metadata.count` is the number of profiles in the current page, not the size of the whole matching set. To size a query, call the free [People search count](https://docs.reversecontact.com/endpoints/search-people-count) endpoint with the same filters: ```bash curl -X POST https://api.reversecontact.com/v2/search/people/count \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "currentSeniority": "VP" }' ``` The response is `data.count`, the total number of matching profiles across all pages, and it costs 0 credits. ## Pricing changes The previous endpoint charged 1 credit per page, whatever the page contained. People search charges 1 credit per profile delivered: a page that returns 25 profiles costs 25 credits, and a page that returns no profile costs 0 credits. Rejected filters still cost 0 credits. Two consequences for your migration: - `perPage` is now a credit control. Lower it to cap what a single request can spend. - Size queries first with the free count endpoint instead of reading `metadata.total` from a billed page. ## Filter changes Existing filters keep their names, including `currentPositionTitle`, `excludeCurrentPositionTitle`, `currentCompanyIndustry`, `currentCompanyEmployeeCountRange`, `location`, `followersCount` and `maxDataAgeDate`. People search also accepts filters the previous endpoint did not have: | Filter | What it does | | ------ | ------------ | | `currentFunction`, `excludeCurrentFunction` | Match the current function from a 32-value position taxonomy | | `currentSubFunction`, `excludeCurrentSubFunction` | Match the current sub-function from a 386-value taxonomy | | `currentSeniority`, `excludeCurrentSeniority` | Match the current seniority from a 10-value taxonomy | | `experience` | Require past experience in a function, sub-function or seniority, with an optional minimum tenure in months | | `positionTitle`, `companyName`, `companyId`, `companyLinkedinId`, `companyPublicId`, `companyIndustry` and their `exclude...` counterparts | Match current and past positions, not only the current one | | `excludeCurrentCompanyName`, `excludeCurrentCompanyId`, `excludeCurrentCompanyLinkedinId`, `excludeCurrentCompanyPublicId` | Exclude a specific current employer | | `isCurrentlyEmployed` | Restrict to profiles that have a current employer | | `skills` | Match the skills listed on the profile | | `languages`, `excludeLanguages` | Match or exclude the languages a person speaks | | `keywords` | Require every word somewhere in the profile, in any field | | `changedJobsWithinDays` | Keep people who started their current position within the last 30, 60, 90 or 120 days | | `currentContractType`, `contractType` and their `exclude...` counterparts | Match how people are employed (freelance, internship, part-time and five more), on the current position or any position | | `schoolName` | Match the schools a person attended | The taxonomy lists are documented on the [People search](https://docs.reversecontact.com/endpoints/search-people#position-taxonomy) page and returned dynamically by [available fields](https://docs.reversecontact.com/endpoints/search-people-available-fields). Validation works the same way: unknown fields, blank values and values outside a closed list return `422` with suggestions and cost 0 credits. ## Available fields moves too If you discover filters dynamically, switch `GET /v2/search/persons/available-fields` to `GET /v2/search/people/available-fields`. The response shape is unchanged: `data.entity` is `"person"` and `data.fields` lists every accepted filter, with `allowedValues` for closed vocabularies such as the position taxonomy. ## Migration checklist 1. Replace the search path with `POST /v2/search/people` and the available-fields path with `GET /v2/search/people/available-fields`. 2. Replace `page` with cursor handling: loop while `data.metadata.nextCursor` is not `null`. 3. Review your `perPage`: the default is now `25`, and each delivered profile costs 1 credit. 4. Replace `data.metadata.total` reads with a call to `POST /v2/search/people/count`. 5. Drop the Person profile calls you used to chain after a search: results are already full profiles. 6. Map your title filters: `currentPositionTitle` still matches the current position, and the new `positionTitle` matches current and past positions. ## FAQ ### Why does `/v2/search/persons` return `410 Gone`? `POST /v2/search/persons` was retired on October 13, 2026 and now returns `410 Gone`. People search now lives at `POST /v2/search/people`, with the same method and the same `Authorization: Bearer YOUR_API_KEY` header. See the [endpoint transition overview](https://docs.reversecontact.com/guides/endpoint-transition-october-2026). ### How do I get the total number of matching profiles with `/v2/search/people`? `POST /v2/search/people` does not return a total: `data.metadata.count` is only the number of profiles in the current page. Call `POST /v2/search/people/count` with the same filters to get `data.count`, the total number of matching profiles across all pages, for 0 credits. See [People search count](https://docs.reversecontact.com/endpoints/search-people-count). ### How does pagination work in `/v2/search/people`? `POST /v2/search/people` uses cursor pagination: send the `nextCursor` value from the previous response as `cursor` to get the next page, and stop when `nextCursor` is `null`. The cursor is opaque, so send it back unchanged, without parsing or altering it. `perPage` accepts 1 to 100 and defaults to `25`. ### How many credits does a people search cost? `POST /v2/search/people` charges 1 credit per profile delivered: a page that returns 25 profiles costs 25 credits, and a page that returns no profile costs 0 credits. Rejected filters also cost 0 credits, and lowering `perPage` caps what a single request can spend. ### Do I still need to call the Person profile endpoint after a people search? No. Each item of `data.data` in a `POST /v2/search/people` response is already a full profile, with `currentPosition`, `experience`, `education`, `skills` and the other Person profile sections, so the Person profile calls you chained after a search can be dropped. See [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile). --- # TypeScript and JavaScript SDK > Use the Reverse Contact TypeScript and JavaScript SDK to enrich profiles, search within a budget, find contacts, and manage Lists and async jobs. - Page: https://docs.reversecontact.com/sdk/javascript - Last verified: 2026-11-04 ## Introduction `@reverse-contact/sdk` is the official TypeScript and JavaScript client for the Reverse Contact API. It covers every endpoint with typed requests and results. The client handles safe retries, waits for asynchronous jobs, and pages through search results within a budget you set. It has no runtime dependencies. - Package: [@reverse-contact/sdk on npm](https://www.npmjs.com/package/@reverse-contact/sdk) - Source code and issues: [github.com/visum-run/reversecontact-node](https://github.com/visum-run/reversecontact-node) - License: Apache-2.0 The SDK runs on Node.js 22 and later, Bun, Deno, and edge runtimes that provide the standard Fetch API, `AbortController` and `crypto.randomUUID`. It ships as ESM and CommonJS. These guides describe the published SDK version 0.3.0. The [Python SDK](https://docs.reversecontact.com/sdk/python) is also available for Python 3.11 and later. Keep API keys on your server: browser construction is rejected by default. ## Install **Install** ```bash [npm] npm install @reverse-contact/sdk ``` ## First call Set `RC_API_KEY` on your server, then run this example. A successful person enrichment costs 2 credits. A lookup with no match is free. **First call** ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact(); const result = await client.enrich.person({ email: "jane.doe@acme.com" }); console.log(result?.data.person ?? "No confident match"); ``` ## Endpoints and methods | Endpoint | Method | | -------- | ------ | | [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person) | `client.enrich.person()` | | [Enrich company](https://docs.reversecontact.com/endpoints/enrich-company) | `client.enrich.company()` | | [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) | `client.fetch.person()` | | [Person profile live](https://docs.reversecontact.com/endpoints/fetch-profile-live) | `client.fetch.personLive()` | | [Person profile status](https://docs.reversecontact.com/endpoints/check-person) | `client.fetch.checkPerson()` | | [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) | `client.fetch.company()` | | [Company profile live](https://docs.reversecontact.com/endpoints/fetch-company-live) | `client.fetch.companyLive()` | | [Company profile status](https://docs.reversecontact.com/endpoints/check-company) | `client.fetch.checkCompany()` | | [People search](https://docs.reversecontact.com/endpoints/search-people) | `client.search.people()`, `client.search.iterPeople()` | | [Agentic Search](https://docs.reversecontact.com/endpoints/search-people-prompt) | `client.search.peopleByPrompt()`, `client.search.iterPeopleByPrompt()` | | [People lookalike](https://docs.reversecontact.com/endpoints/search-people-lookalike) | `client.search.peopleLookalike()`, `client.search.iterPeopleLookalike()` | | [People search count](https://docs.reversecontact.com/endpoints/search-people-count) | `client.search.countPeople()` | | [People auto-suggestion](https://docs.reversecontact.com/endpoints/search-people-auto-suggestion) | `client.search.peopleAutoSuggestion()` | | [People search available fields](https://docs.reversecontact.com/endpoints/search-people-available-fields) | `client.search.peopleFields()` | | [Company search](https://docs.reversecontact.com/endpoints/search-companies) | `client.search.organizations()`, `client.search.iterOrganizations()` | | [Agentic Company Search](https://docs.reversecontact.com/endpoints/search-companies-prompt) | `client.search.organizationsByPrompt()`, `client.search.iterOrganizationsByPrompt()` | | [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count) | `client.search.countOrganizations()` | | [Company search available fields](https://docs.reversecontact.com/endpoints/search-companies-available-fields) | `client.search.organizationsFields()` | | [Find Email](https://docs.reversecontact.com/endpoints/contact-email) | `client.contact.findEmail()` | | [Verify Email](https://docs.reversecontact.com/endpoints/contact-email-verify) | `client.contact.verifyEmail()` | | [Find Phone](https://docs.reversecontact.com/endpoints/contact-phone) | `client.contact.findPhone()` | | [All Lists](https://docs.reversecontact.com/endpoints/lists) | `client.lists.list()`, `client.lists.listAll()` | | [Create a List](https://docs.reversecontact.com/endpoints/list-create) | `client.lists.create()` | | [Get a List](https://docs.reversecontact.com/endpoints/list) | `client.lists.retrieve()` | | [Delete a List](https://docs.reversecontact.com/endpoints/list-delete) | `client.lists.delete()` | | [List members](https://docs.reversecontact.com/endpoints/list-members) | `client.lists.members.list()`, `client.lists.members.listAll()` | | [Add members](https://docs.reversecontact.com/endpoints/list-members-add) | `client.lists.members.add()` | | [Remove members](https://docs.reversecontact.com/endpoints/list-members-remove) | `client.lists.members.remove()` | | [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals) | `client.lists.importSignals()` | | [Read a List operation](https://docs.reversecontact.com/endpoints/list-operation) | `client.lists.operations.retrieve()`, `client.lists.operations.wait()` | | [Cancel a List operation](https://docs.reversecontact.com/endpoints/list-operation-cancel) | `client.lists.operations.cancel()` | | [Export a List](https://docs.reversecontact.com/endpoints/list-export) | `client.lists.export()` | | [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) | `client.bulk.enrichPersons()` | | [Job history](https://docs.reversecontact.com/endpoints/jobs) | `client.bulk.list()`, `client.bulk.listAll()` | | [Get a job](https://docs.reversecontact.com/endpoints/job) | `client.bulk.retrieve()`, `client.bulk.wait()` | | [Download job results](https://docs.reversecontact.com/endpoints/job-results) | `client.bulk.results()` | | [Read job results by page](https://docs.reversecontact.com/endpoints/job-rows) | `client.bulk.rows()`, `client.bulk.iterateRows()` | | [Cancel a job](https://docs.reversecontact.com/endpoints/job-cancel) | `client.bulk.cancel()` | | [Polling](https://docs.reversecontact.com/guides/polling) a job | `client.jobs.retrieve()`, `client.jobs.wait()` | | [Usage](https://docs.reversecontact.com/endpoints/usage) | `client.usage.retrieve()` | Look for the `TypeScript SDK` tab in endpoint code examples. For now, the List and Bulk enrichment examples are in [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/javascript/lists-and-bulk). Request fields use the API's camelCase names. ## Where to go next - [Install and configure](https://docs.reversecontact.com/sdk/javascript/install) - [First call](https://docs.reversecontact.com/sdk/javascript/first-call) - [client.enrich](https://docs.reversecontact.com/sdk/javascript/enrich) - [client.fetch](https://docs.reversecontact.com/sdk/javascript/fetch) - [client.search](https://docs.reversecontact.com/sdk/javascript/search) - [client.contact](https://docs.reversecontact.com/sdk/javascript/contact) - [client.lists](https://docs.reversecontact.com/sdk/javascript/lists) - [client.bulk](https://docs.reversecontact.com/sdk/javascript/bulk) - [client.jobs](https://docs.reversecontact.com/sdk/javascript/jobs) - [client.usage](https://docs.reversecontact.com/sdk/javascript/usage) - [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs) - [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/javascript/lists-and-bulk) - [Search within a budget](https://docs.reversecontact.com/sdk/javascript/search-budget) - [Filter values as constants](https://docs.reversecontact.com/sdk/javascript/filter-constants) - [Errors and retries](https://docs.reversecontact.com/sdk/javascript/errors-and-retries) - [Timeouts and cancellation](https://docs.reversecontact.com/sdk/javascript/timeouts) - [Client options](https://docs.reversecontact.com/sdk/javascript/client-options) - [Without the SDK](https://docs.reversecontact.com/sdk/javascript/without-the-sdk) ## API key Keep the key on your server and pass `apiKey`, or let the client read `RC_API_KEY`. See [Install and configure](https://docs.reversecontact.com/sdk/javascript/install). ## Lookups that find nothing Check for `null` when a lookup finds no profile. See [First call](https://docs.reversecontact.com/sdk/javascript/first-call) for response handling. ## Async jobs: live fetch, Find Email and Find Phone Use `job.wait()` to poll for free, for up to 30 minutes by default. A failed job raises `JobFailedError`. Save `job.id` to resume with `client.jobs.wait()` after a restart. See [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs). ### Webhook delivery instead of polling Receive results at an HTTPS webhook and parse deliveries in [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs). ## Lists and Bulk enrichment Use [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/javascript/lists-and-bulk) to create Lists, track imports, and read Bulk results as they come or download them. ## Search within a budget Count for free, then use the required `maxResults` iterator option to cap requested results. See [Search within a budget](https://docs.reversecontact.com/sdk/javascript/search-budget). ## Filter values as constants Use [Filter values as constants](https://docs.reversecontact.com/sdk/javascript/filter-constants) to autocomplete supported values in your editor. ## Errors and retries The SDK retries per-minute limits and HTTP 500, 502, 503 and 504 up to 4 times. Daily limits, key credit limits and missing credits are not retried. Read [Errors and retries](https://docs.reversecontact.com/sdk/javascript/errors-and-retries). ## Safe retries on Find Email and Find Phone Contact lookups reuse an idempotency key across retries, including HTTP `409 IDEMPOTENCY_KEY_IN_USE`. Outside per-minute rate limits, other 4xx errors are not retried. Read [Errors and retries](https://docs.reversecontact.com/sdk/javascript/errors-and-retries). ## Timeouts and cancellation See [Timeouts and cancellation](https://docs.reversecontact.com/sdk/javascript/timeouts) to set a timeout for each HTTP attempt or cancel local requests. ## Client options Set transport, logging, headers and per-request overrides with [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). ## Without the SDK To call the API with an HTTP client or generate one from OpenAPI, see [Without the SDK](https://docs.reversecontact.com/sdk/javascript/without-the-sdk). ## FAQ ### Is there an official Reverse Contact SDK? Yes. Reverse Contact publishes `@reverse-contact/sdk` on npm for TypeScript and JavaScript, and `reversecontact` on PyPI for Python 3.11 and later. The TypeScript and JavaScript SDK runs on Node.js 22 and later, Bun, Deno and edge runtimes. Both SDKs cover every API endpoint, handle safe retries, and wait for asynchronous jobs. ### Can I use @reverse-contact/sdk in the browser? No. Anyone can read an API key shipped in front-end code, so the `@reverse-contact/sdk` client throws when it is created in a browser. Call Reverse Contact from your server, keep the key in the `RC_API_KEY` environment variable, and send the browser only what it needs. ### Does @reverse-contact/sdk retry failed requests? Yes. `@reverse-contact/sdk` retries per-minute rate limits and `500`, `502`, `503` and `504` errors up to 4 times, after the delay the API asks for or with exponential backoff. It never retries daily limits, API key credit limits, missing credits or any other `4xx` error except `409 IDEMPOTENCY_KEY_IN_USE`. ### How do I cap the credits a Reverse Contact search spends? Call a free count method first, such as `client.search.countPeople()`. Then read results with a search iterator such as `client.search.iterPeople()`. The required `maxResults` option caps the number of profiles requested and billed. ### How do I wait for a live fetch or Find Email result with the SDK? Call `job.wait()` on the job that `client.fetch.personLive()` or `client.contact.findEmail()` returns. It polls for free until the job ends, 30 minutes at most by default, and raises `JobFailedError` when the job fails. Save `job.id` first, to resume with `client.jobs.wait()` after a restart. --- # Install and configure > Install the Reverse Contact TypeScript SDK on Node.js, Bun, Deno or edge runtimes, configure your API key, and create a server-side client. - Page: https://docs.reversecontact.com/sdk/javascript/install - Last verified: 2026-11-04 Install `@reverse-contact/sdk` and keep your API key in your server environment. These guides cover the published package version 0.3.0. ## Install **Install** ```bash [npm] npm install @reverse-contact/sdk ``` ```bash [pnpm] pnpm add @reverse-contact/sdk ``` ```bash [Yarn] yarn add @reverse-contact/sdk ``` ```bash [Bun] bun add @reverse-contact/sdk ``` ```bash [Deno] deno add npm:@reverse-contact/sdk ``` On Deno, run your script with `--allow-net`. Add `--allow-env` if you read the key from the environment. ## Runtime requirements The SDK runs on Node.js 22 and later, Bun, Deno, and edge runtimes that provide the standard Fetch API, `AbortController` and `crypto.randomUUID`. The package ships as ESM and CommonJS and has no runtime dependencies. ## API key Pass your API key with the `apiKey` option, or omit the option to let the client read `RC_API_KEY`. These guides use that environment variable, but you can use any server-side source, such as a secret manager. If you serve several workspaces, create one client per key. Keep the key on your server. Anyone can read a key shipped in front-end code, so the constructor throws when it runs in a browser. Setting `dangerouslyAllowBrowser: true` bypasses that check and exposes your key to every visitor. See [Authentication](https://docs.reversecontact.com/authentication) to create a key. **API key** ```bash [Terminal] export RC_API_KEY="your-api-key" ``` ## Create a client **Create a client** ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; // CommonJS: const { ReverseContact } = require("@reverse-contact/sdk"); const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const usage = await client.usage.retrieve(); console.log("Credits left:", usage.quotas?.workspace.credits.left); ``` `new ReverseContact()` also reads `RC_API_KEY` automatically. The constructor raises `ReverseContactError` when no key is available. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options) for configuration and [First call](https://docs.reversecontact.com/sdk/javascript/first-call) for enrichment. Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### Which runtimes support the Reverse Contact TypeScript SDK? `@reverse-contact/sdk` supports Node.js 22 and later, Bun, Deno, and edge runtimes with the standard Fetch API, `AbortController` and `crypto.randomUUID`. ### Which environment variable does the SDK use for the API key? `new ReverseContact()` reads `RC_API_KEY` when the `apiKey` constructor option is omitted. An explicit `apiKey` can come from any server-side secret source. ### Can I use @reverse-contact/sdk in the browser? The SDK constructor rejects browsers by default. Setting `dangerouslyAllowBrowser: true` bypasses that check and exposes the key to visitors. Keep the SDK client on your server. --- # First call > Enrich a person with the Reverse Contact TypeScript SDK, read profile data, credits and request metadata, and handle lookups that return no match. - Page: https://docs.reversecontact.com/sdk/javascript/first-call - Last verified: 2026-11-04 [Install and configure](https://docs.reversecontact.com/sdk/javascript/install) the client, then enrich one person. ## Enrich a person This call enriches a person from a work email. It costs **2 credits** when a person is found, and nothing when no profile matches. **Enrich a person** ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; // CommonJS: const { ReverseContact } = require("@reverse-contact/sdk"); const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.enrich.person({ email: "jane.doe@acme.com" }); if (result === null) { console.log("No confident match: nothing was charged"); } else { const { person } = result.data; console.log(person?.firstName, person?.lastName, person?.currentPositionTitle); console.log("Credits used:", result.quotas?.creditsConsumed); console.log("Request id:", result.metadata?.requestId); } ``` A non-null enrichment result has the fields below. Other JSON methods also return `quotas`, `metadata` and `response`. The shape of `data` depends on the method: - `data`: the `data` field of the API response. Enrich nests the profile in `data.person` or `data.company`. A cached fetch returns the profile directly in `data`, and Usage returns `data: null`. For a search page, `data` contains the results. For a job, `data` contains the acknowledgement. - `quotas`: your credits and rate limits after this call, with `creditsConsumed` for the call itself. - `metadata`: `requestId` and `executionTimeMs`. Quote the `requestId` when you contact support. - `response`: the original HTTP response, including headers. The SDK has already read the body, so use `data`. `quotas` and `metadata` are optional. Some quota values and `quotas.key` can be `null`. Do not treat a missing snapshot as a zero balance: `null`, `0` and an empty array have different meanings. ## Lookups that find nothing `enrich.person`, `enrich.company`, `fetch.person` and `fetch.company` return `null` when the API answers `404` because nothing matched. These responses cost no credits. Check for `null` and move on without retrying. A `404` outside a lookup, such as an unknown job id or a wrong base URL, raises `NotFoundError`. For complete signatures, see [client.enrich](https://docs.reversecontact.com/sdk/javascript/enrich) and [client.fetch](https://docs.reversecontact.com/sdk/javascript/fetch). Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### Where is the person in an enrichment response? `client.enrich.person()` returns the profile in `result.data.person` when a match is found. Cached `client.fetch.person()` returns the profile directly in `result.data`. ### Does a person lookup with no match cost credits? `client.enrich.person()` returns `null` for a no-match response and costs no credits. A successful person enrichment costs 2 credits. ### Where can I find the request id and credits used? `result.metadata?.requestId` identifies the request for support. `result.quotas?.creditsConsumed` reports the credits used by that call. --- # client.enrich > Use client.enrich in the Reverse Contact TypeScript SDK to match people and companies, with method signatures, parameters, costs and error handling. - Page: https://docs.reversecontact.com/sdk/javascript/enrich - Last verified: 2026-11-04 Look up people and companies from identifying details. You get a matched profile or `null`. | Method | Endpoint | Cost | | ------ | -------- | ---- | | `client.enrich.person()` | [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person) | 2 credits when a person is found, full profile included. A 404 is free. | | `client.enrich.company()` | [Enrich company](https://docs.reversecontact.com/endpoints/enrich-company) | 2 credits when a company is found, full profile included. A 404 is free. | Create `client` by following [Install and configure](https://docs.reversecontact.com/sdk/javascript/install). The `@reverse-contact/sdk` package exports `Body`, `Data`, `Query` and the other types used in the signatures. JSON responses contain `data` and optional `quotas` and `metadata`. They also include the original `response`, with its body already read. See [First call](https://docs.reversecontact.com/sdk/javascript/first-call) for response handling. HTTP errors use the [APIError subclasses and retry rules](https://docs.reversecontact.com/sdk/javascript/errors-and-retries): `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` and `InternalServerError`, according to the API response. Other statuses use `APIError`. ## person() Provide at least one identifying field: `email`, `firstName`, `lastName`, `companyDomain` or `companyName`. Add more details to help identify the right person. **Signature** ```typescript [TypeScript SDK] client.enrich.person( params: Body<"enrichPerson">, options?: RequestOptions ): Promise> | null> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"enrichPerson">` | Request body. See the fields and their requirements below. | | `params.email` | `string` | Professional email address to look up (e.g. `jane.doe@acme.com`). | | `params.firstName` | `string` | First name of the person to look up. | | `params.lastName` | `string` | Last name of the person to look up. | | `params.companyDomain` | `string` | Company domain to help disambiguate (e.g. `acme.com` or `https://acme.com`). | | `params.companyName` | `string` | Company name to help disambiguate (e.g. `Acme Corp`). | | `params.fullProfile` | `boolean` | Deprecated and ignored. Enrich always returns the full profile at the same cost. The API still accepts this field for compatibility, but you can remove it. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** `Result>` with the profile in `data.person`, or `null` for a no-match lookup. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. A lookup with no match returns `null`. An unknown route still raises `NotFoundError`. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.enrich.person({ email: "jane.doe@acme.com" }); console.log(result?.data.person ?? "No confident match"); ``` ## company() Provide a company domain. The full profile is included. `fullProfile` is deprecated and ignored, but existing requests can still use it. **Signature** ```typescript [TypeScript SDK] client.enrich.company( params: Body<"enrichCompany">, options?: RequestOptions ): Promise> | null> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"enrichCompany">` | Request body. See the fields and their requirements below. | | `params.domain` **required** | `string` | Company domain to look up. A bare domain or a full URL works (e.g. `acme.com` or `https://acme.com`). | | `params.fullProfile` | `boolean` | Deprecated and ignored. Enrich always returns the full profile at the same cost. The API still accepts this field for compatibility, but you can remove it. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** `Result>` with the profile in `data.company`, or `null` for a no-match lookup. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. A lookup with no match returns `null`. An unknown route still raises `NotFoundError`. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.enrich.company({ domain: "acme.com" }); console.log(result?.data.company ?? "No confident match"); ``` ## Related guides [First call](https://docs.reversecontact.com/sdk/javascript/first-call), [Errors and retries](https://docs.reversecontact.com/sdk/javascript/errors-and-retries). Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### How do I enrich a person from an email in TypeScript? Call `client.enrich.person({ email })`. A match returns the profile in `result.data.person` and costs 2 credits. ### What does client.enrich return when no profile matches? `client.enrich.person()` and `client.enrich.company()` return `null` for no-match lookups. Those lookups cost no credits. ### Does fullProfile change the enrichment cost? `fullProfile` is deprecated and ignored. Enrichment includes the full profile for 2 credits per match. --- # client.fetch > Use client.fetch in the Reverse Contact TypeScript SDK to read cached profiles, check freshness or start live jobs, with parameters, costs and examples. - Page: https://docs.reversecontact.com/sdk/javascript/fetch - Last verified: 2026-11-04 Read cached profiles, check availability, or start a live profile job. | Method | Endpoint | Cost | | ------ | -------- | ---- | | `client.fetch.person()` | [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) | 1 credit when the profile is found in the cache. A 404 is free. | | `client.fetch.personLive()` | [Person profile live](https://docs.reversecontact.com/endpoints/fetch-profile-live) | 2 credits per request, consumed when the job is created. | | `client.fetch.checkPerson()` | [Person profile status](https://docs.reversecontact.com/endpoints/check-person) | Free. Requires at least 2 workspace credits. A lower balance returns 402 NO_CREDITS. | | `client.fetch.company()` | [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) | 1 credit when the company is found in the cache. A 404 is free. | | `client.fetch.companyLive()` | [Company profile live](https://docs.reversecontact.com/endpoints/fetch-company-live) | 2 credits per request, consumed when the job is created. | | `client.fetch.checkCompany()` | [Company profile status](https://docs.reversecontact.com/endpoints/check-company) | Free. Requires at least 2 workspace credits. A lower balance returns 402 NO_CREDITS. | Create `client` by following [Install and configure](https://docs.reversecontact.com/sdk/javascript/install). The `@reverse-contact/sdk` package exports `Body`, `Data`, `Query` and the other types used in the signatures. JSON responses contain `data` and optional `quotas` and `metadata`. They also include the original `response`, with its body already read. See [First call](https://docs.reversecontact.com/sdk/javascript/first-call) for response handling. HTTP errors use the [APIError subclasses and retry rules](https://docs.reversecontact.com/sdk/javascript/errors-and-retries): `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` and `InternalServerError`, according to the API response. Other statuses use `APIError`. ## person() Provide `url` or `id`. If you supply both, `id` takes precedence. **Signature** ```typescript [TypeScript SDK] client.fetch.person( params: Body<"fetchPerson">, options?: RequestOptions ): Promise> | null> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"fetchPerson">` | Request body. See the fields and their requirements below. | | `params.url` **conditional** | `string` | Public Social profile URL (e.g. `https://social.com/in/janedoe`). Required unless `id` is given. | | `params.id` **conditional** | `string` | Reverse Contact person id from a previous response (e.g. `prs_01jbd5vewyenebxgaz869ffe5t`). Required unless `url` is given. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** `Result>` with the cached person directly in `data`, or `null` when no profile matches. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. A lookup with no match returns `null`. An unknown route still raises `NotFoundError`. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.fetch.person({ url: "https://social.com/in/janedoe" }); console.log(result?.data ?? "No cached profile"); ``` ## personLive() Provide exactly one of `url` or `id`. Omit `webhookUrl` to poll, or supply an HTTPS callback URL. **Signature** ```typescript [TypeScript SDK] client.fetch.personLive( params: Body<"livePerson">, options?: RequestOptions ): Promise> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"livePerson">` | Request body. See the fields and their requirements below. | | `params.url` **conditional** | `string` | Public Social profile URL (e.g. `https://social.com/in/janedoe`). Provide exactly one of `url` or `id`. | | `params.id` **conditional** | `string` | Reverse Contact person id (`prs_…`). The gateway resolves it to the current Social public slug before starting the scrape. | | `params.webhookUrl` | `string` | HTTPS push URL for webhook delivery. Omit this field to retrieve the result via [polling](https://docs.reversecontact.com/guides/polling) (recommended). See the [Webhooks guide](https://docs.reversecontact.com/guides/webhooks) when you need push delivery. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A `Job`, with the `webhookId` in `job.id`. Call `await job.wait()` to get the profile in `data.result`. Waiting is free. See [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs) for `wait()` options and webhook delivery. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. Waiting raises `JobFailedError` for an errored job and `JobTimeoutError` when the overall deadline expires. **Example** **Example** ```typescript [TypeScript SDK] const job = await client.fetch.personLive({ url: "https://social.com/in/janedoe" }); console.log("Save job id:", job.id); const done = await job.wait(); console.log(done.data.result?.firstName); ``` ## checkPerson() Provide `url` or `id`. If you supply both, `id` takes precedence. The check is free but requires at least 2 workspace credits. **Signature** ```typescript [TypeScript SDK] client.fetch.checkPerson( params: Body<"checkPerson">, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"checkPerson">` | Request body. See the fields and their requirements below. | | `params.url` **conditional** | `string` | Public Social profile URL (e.g. `https://social.com/in/janedoe`). Required unless `id` is given. | | `params.id` **conditional** | `string` | Reverse Contact person id from a previous response (e.g. `prs_01jbd5vewyenebxgaz869ffe5t`). Required unless `url` is given. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A response with profile availability and freshness in `data`. The check does not fetch a full profile. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.fetch.checkPerson({ url: "https://social.com/in/janedoe" }); console.log(result.data); ``` ## company() Provide `url` or `id`. If you supply both, `id` takes precedence. **Signature** ```typescript [TypeScript SDK] client.fetch.company( params: Body<"fetchCompany">, options?: RequestOptions ): Promise> | null> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"fetchCompany">` | Request body. See the fields and their requirements below. | | `params.url` **conditional** | `string` | Public Social company URL (e.g. `https://social.com/company/acme-corp`). Required unless `id` is given. | | `params.id` **conditional** | `string` | Reverse Contact company id from a previous response (e.g. `com_01jbd5vewyenebxgaz869ffe5t`). Required unless `url` is given. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** `Result>` with the cached company directly in `data`, or `null` when no company matches. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. A lookup with no match returns `null`. An unknown route still raises `NotFoundError`. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.fetch.company({ url: "https://social.com/company/acme" }); console.log(result?.data ?? "No cached company"); ``` ## companyLive() Provide exactly one of `url` or `id`. Omit `webhookUrl` to poll, or supply an HTTPS callback URL. **Signature** ```typescript [TypeScript SDK] client.fetch.companyLive( params: Body<"liveCompany">, options?: RequestOptions ): Promise> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"liveCompany">` | Request body. See the fields and their requirements below. | | `params.url` **conditional** | `string` | Public Social company URL (e.g. `https://social.com/company/acme-corp`). Provide exactly one of `url` or `id`. | | `params.id` **conditional** | `string` | Reverse Contact company id (`com_…`). The gateway resolves it to the current Social public slug before starting the scrape. | | `params.webhookUrl` | `string` | HTTPS push URL for webhook delivery. Omit this field to retrieve the result via [polling](https://docs.reversecontact.com/guides/polling) (recommended). See the [Webhooks guide](https://docs.reversecontact.com/guides/webhooks) when you need push delivery. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A `Job`, with the `webhookId` in `job.id`. Call `await job.wait()` to get the company in `data.result`. Waiting is free. See [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs) for `wait()` options and webhook delivery. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. Waiting raises `JobFailedError` for an errored job and `JobTimeoutError` when the overall deadline expires. **Example** **Example** ```typescript [TypeScript SDK] const job = await client.fetch.companyLive({ url: "https://social.com/company/acme" }); console.log("Save job id:", job.id); const done = await job.wait(); console.log(done.data.result); ``` ## checkCompany() Provide `url` or `id`. If you supply both, `id` takes precedence. The check is free but requires at least 2 workspace credits. **Signature** ```typescript [TypeScript SDK] client.fetch.checkCompany( params: Body<"checkCompany">, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"checkCompany">` | Request body. See the fields and their requirements below. | | `params.url` **conditional** | `string` | Public Social company URL (e.g. `https://social.com/company/acme-corp`). Required unless `id` is given. | | `params.id` **conditional** | `string` | Reverse Contact company id from a previous response (e.g. `com_01jbd5vewyenebxgaz869ffe5t`). Required unless `url` is given. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A response with company availability and freshness in `data`. The check does not fetch a full profile. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.fetch.checkCompany({ url: "https://social.com/company/acme" }); console.log(result.data); ``` ## Related guides [First call](https://docs.reversecontact.com/sdk/javascript/first-call), [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs). Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### Which SDK method reads a cached person profile? `client.fetch.person()` returns the cached person directly in `result.data`, or `null` when no cached profile matches. ### How do I get a live profile with the TypeScript SDK? Call `client.fetch.personLive()` or `client.fetch.companyLive()`, save `job.id`, and call `job.wait()` for the final result. ### Are profile availability checks free? `client.fetch.checkPerson()` and `client.fetch.checkCompany()` are free, but the workspace must have at least 2 credits. --- # client.search > Use client.search in the Reverse Contact TypeScript SDK for people and company searches, free counts, filter catalogs and bounded result pagination. - Page: https://docs.reversecontact.com/sdk/javascript/search - Last verified: 2026-11-04 Search people and companies, count matches, and set a limit on the paid results you retrieve. | Method | Endpoint | Cost | | ------ | -------- | ---- | | `client.search.people()` | [People search](https://docs.reversecontact.com/endpoints/search-people) | 1 credit per profile delivered. A page with no profile is free. | | `client.search.peopleByPrompt()` | [Agentic Search](https://docs.reversecontact.com/endpoints/search-people-prompt) | 1 credit per page for the search, plus 1 credit per profile delivered. | | `client.search.peopleLookalike()` | [People lookalike](https://docs.reversecontact.com/endpoints/search-people-lookalike) | 1 credit per profile delivered. A page with no profile is free. | | `client.search.countPeople()` | [People search count](https://docs.reversecontact.com/endpoints/search-people-count) | Free. | | `client.search.peopleAutoSuggestion()` | [People auto-suggestion](https://docs.reversecontact.com/endpoints/search-people-auto-suggestion) | Free. | | `client.search.organizations()` | [Company search](https://docs.reversecontact.com/endpoints/search-companies) | 1 credit per company delivered. A page with no company is free. | | `client.search.organizationsByPrompt()` | [Agentic Company Search](https://docs.reversecontact.com/endpoints/search-companies-prompt) | 1 credit per page for the search, plus 1 credit per company delivered. | | `client.search.countOrganizations()` | [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count) | Free. | | `client.search.peopleFields()` | [People search available fields](https://docs.reversecontact.com/endpoints/search-people-available-fields) | Free. | | `client.search.organizationsFields()` | [Company search available fields](https://docs.reversecontact.com/endpoints/search-companies-available-fields) | Free. | | `client.search.iterPeople()` | [People search](https://docs.reversecontact.com/endpoints/search-people) | 1 credit per profile delivered. A page with no profile is free. | | `client.search.iterPeopleByPrompt()` | [Agentic Search](https://docs.reversecontact.com/endpoints/search-people-prompt) | 1 credit per page for the search, plus 1 credit per profile delivered. | | `client.search.iterPeopleLookalike()` | [People lookalike](https://docs.reversecontact.com/endpoints/search-people-lookalike) | 1 credit per profile delivered. A page with no profile is free. | | `client.search.iterOrganizations()` | [Company search](https://docs.reversecontact.com/endpoints/search-companies) | 1 credit per company delivered. A page with no company is free. | | `client.search.iterOrganizationsByPrompt()` | [Agentic Company Search](https://docs.reversecontact.com/endpoints/search-companies-prompt) | 1 credit per page for the search, plus 1 credit per company delivered. | Create `client` by following [Install and configure](https://docs.reversecontact.com/sdk/javascript/install). The `@reverse-contact/sdk` package exports `Body`, `Data`, `Query` and the other types used in the signatures. JSON responses contain `data` and optional `quotas` and `metadata`. They also include the original `response`, with its body already read. See [First call](https://docs.reversecontact.com/sdk/javascript/first-call) for response handling. HTTP errors use the [APIError subclasses and retry rules](https://docs.reversecontact.com/sdk/javascript/errors-and-retries): `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` and `InternalServerError`, according to the API response. Other statuses use `APIError`. Page methods return a `Page` with the items in `data` and the next page identified by `nextCursor`. Use `hasNextPage()` to check the cursor. Call `getNextPage()` to request that page, or get `null` when there is no next page. Prompt pages also expose `interpretation`. See [Search within a budget](https://docs.reversecontact.com/sdk/javascript/search-budget). ## people() Provide at least one search filter. Pagination alone is not a filter. **Signature** ```typescript [TypeScript SDK] client.search.people( params: Omit, "perPage"> & { perPage?: number }, options?: RequestOptions ): Promise["data"][number], undefined>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Omit, "perPage"> & { perPage?: number }` | At least one search filter. See [People search](https://docs.reversecontact.com/endpoints/search-people) for every filter and its nested fields. | | `params.cursor` | `string` | Opaque cursor from `metadata.nextCursor`. Use it to request the next page | | `params.perPage` | `number` | Profiles per page (1 to 100, default `25`) | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A `Page` with results in `data` and pagination through `nextCursor`, `hasNextPage()` and `getNextPage()`. The page also includes optional `quotas` and `metadata`, and the original `response`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const page = await client.search.people({ firstName: "Jane", perPage: 10 }); for (const person of page.data) console.log(person.firstName); ``` ## peopleByPrompt() **Signature** ```typescript [TypeScript SDK] client.search.peopleByPrompt( params: Omit, "perPage"> & { perPage?: number }, options?: RequestOptions ): Promise["data"][number], Data<"searchPeopleByPrompt">["interpretation"]>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Omit, "perPage"> & { perPage?: number }` | Request body. See the fields and their requirements below. | | `params.cursor` | `string` | Opaque cursor from `metadata.nextCursor`. Use it to request the next page with the same `prompt` | | `params.prompt` **required** | `string` | Plain-language description of the people to find, in any language. At least 3 characters and at most 1000 tokens | | `params.perPage` | `number` | Profiles per page (1 to 100, default `25`) | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A `Page` with results in `data` and pagination through `nextCursor`, `hasNextPage()` and `getNextPage()`. The page also includes optional `quotas` and `metadata`, and the original `response`. `interpretation` contains the filters extracted from the prompt. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const page = await client.search.peopleByPrompt({ prompt: "Software founders in Paris", perPage: 10 }); console.log(page.interpretation, page.data); ``` ## peopleLookalike() Provide exactly one seed, `url` or `id`. The API rejects additional search filters. The seed must already be in the search index. **Signature** ```typescript [TypeScript SDK] client.search.peopleLookalike( params: Omit, "perPage"> & { perPage?: number }, options?: RequestOptions ): Promise["data"][number], undefined>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Omit, "perPage"> & { perPage?: number }` | Request body. See the fields and their requirements below. | | `params.url` **conditional** | `string` | Public Social profile URL of the seed (e.g. `https://social.com/in/janedoe`). Required unless `id` is given. | | `params.id` **conditional** | `string` | Reverse Contact person id of the seed (e.g. `prs_01jbd5vewyenebxgaz869ffe5t`), as returned in the `id` field of any person response. Required unless `url` is given. | | `params.cursor` | `string` | Opaque cursor from `metadata.nextCursor`. Use it to request the next page | | `params.perPage` | `number` | Profiles per page (1 to 100, default `25`) | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A `Page` with results in `data` and pagination through `nextCursor`, `hasNextPage()` and `getNextPage()`. The page also includes optional `quotas` and `metadata`, and the original `response`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. A seed absent from the search index raises `NotFoundError`, rather than returning `null`. **Example** **Example** ```typescript [TypeScript SDK] const page = await client.search.peopleLookalike({ url: "https://social.com/in/janedoe", perPage: 10 }); console.log(page.data); ``` ## countPeople() Provide at least one search filter. Count before requesting paid profiles. **Signature** ```typescript [TypeScript SDK] client.search.countPeople( params: Body<"countPeople">, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"countPeople">` | At least one search filter. See [People search count](https://docs.reversecontact.com/endpoints/search-people-count) for every filter and its nested fields. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A response envelope with the free match count in `data.count`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.search.countPeople({ firstName: "Jane" }); console.log("Matches:", result.data.count); ``` ## peopleAutoSuggestion() **Signature** ```typescript [TypeScript SDK] client.search.peopleAutoSuggestion( params: Body<"searchPeopleAutoSuggestion">, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"searchPeopleAutoSuggestion">` | Request body. See the fields and their requirements below. | | `params.field` **required** | `"skills"` | The filter to complete. `skills` is the only value today | | `params.query` **required** | `string` | Text entered so far, 1 to 100 characters. Case and accents are ignored | | `params.limit` | `number` | Maximum values returned, from 1 to 25. Defaults to 10 | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A response with suggested skill values in `data`. The response contains no profiles. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.search.peopleAutoSuggestion({ field: "skills", query: "machine l", limit: 10 }); console.log(result.data); ``` ## organizations() Provide at least one search filter. Pagination alone is not a filter. **Signature** ```typescript [TypeScript SDK] client.search.organizations( params: Omit, "perPage"> & { perPage?: number }, options?: RequestOptions ): Promise["data"][number], undefined>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Omit, "perPage"> & { perPage?: number }` | Request body. See the fields and their requirements below. | | `params.companyDomain` | `string` | Filter by website domain (e.g. `acme.com`). Disposable domains are rejected with `422` | | `params.companyName` | `string` | Filter by company name | | `params.followersCount` | `Body<"searchOrganizations">["followersCount"]` | Filter accepted by [Company search](https://docs.reversecontact.com/endpoints/search-companies). | | `params.location` | `string \| string[]` | Filter accepted by [Company search](https://docs.reversecontact.com/endpoints/search-companies). | | `params.maxDataAgeDate` | `string` | ISO 8601 datetime. Only returns companies refreshed after this date. Needs at least one other filter | | `params.cursor` | `string` | Opaque cursor from `metadata.nextCursor`. Use it to request the next page | | `params.industry` | `string \| string[]` | Industry, or array of industries: a company matches if its industry is any of them. See [Industry values](https://docs.reversecontact.com/reference/industries) for the full list | | `params.specialities` | `string \| string[]` | Speciality, or array of specialities: a company matches if it lists any of them. See [Specialities](https://docs.reversecontact.com/endpoints/search-companies#specialities) | | `params.employeeCountRange` | `Body<"searchOrganizations">["employeeCountRange"]` | Filter accepted by [Company search](https://docs.reversecontact.com/endpoints/search-companies). | | `params.foundedYear` | `Body<"searchOrganizations">["foundedYear"]` | Filter accepted by [Company search](https://docs.reversecontact.com/endpoints/search-companies). | | `params.perPage` | `number` | Companies per page (1 to 100, default `25`) | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A `Page` with results in `data` and pagination through `nextCursor`, `hasNextPage()` and `getNextPage()`. The page also includes optional `quotas` and `metadata`, and the original `response`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const page = await client.search.organizations({ companyName: "Acme", perPage: 10 }); console.log(page.data); ``` ## organizationsByPrompt() **Signature** ```typescript [TypeScript SDK] client.search.organizationsByPrompt( params: Omit, "perPage"> & { perPage?: number }, options?: RequestOptions ): Promise["data"][number], Data<"searchOrganizationsByPrompt">["interpretation"]>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Omit, "perPage"> & { perPage?: number }` | Request body. See the fields and their requirements below. | | `params.cursor` | `string` | Opaque cursor from `metadata.nextCursor`. Use it to request the next page with the same `prompt` | | `params.prompt` **required** | `string` | Plain-language description of the companies to find, in any language. At least 3 characters and at most 1000 tokens | | `params.perPage` | `number` | Companies per page (1 to 100, default `25`) | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A `Page` with results in `data` and pagination through `nextCursor`, `hasNextPage()` and `getNextPage()`. The page also includes optional `quotas` and `metadata`, and the original `response`. `interpretation` contains the filters extracted from the prompt. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const page = await client.search.organizationsByPrompt({ prompt: "Software companies in France", perPage: 10 }); console.log(page.interpretation, page.data); ``` ## countOrganizations() Provide at least one search filter. Counting companies is free. **Signature** ```typescript [TypeScript SDK] client.search.countOrganizations( params: Body<"countOrganizations">, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"countOrganizations">` | Request body. See the fields and their requirements below. | | `params.companyName` | `string` | Filter accepted by [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count). | | `params.companyDomain` | `string` | Filter accepted by [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count). | | `params.industry` | `string \| string[]` | Filter accepted by [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count). | | `params.specialities` | `string \| string[]` | Filter accepted by [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count). | | `params.employeeCountRange` | `Body<"countOrganizations">["employeeCountRange"]` | Filter accepted by [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count). | | `params.foundedYear` | `Body<"countOrganizations">["foundedYear"]` | Filter accepted by [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count). | | `params.followersCount` | `Body<"countOrganizations">["followersCount"]` | Filter accepted by [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count). | | `params.location` | `string \| string[]` | Filter accepted by [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count). | | `params.maxDataAgeDate` | `string` | Filter accepted by [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count). | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A response envelope with the free match count in `data.count`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.search.countOrganizations({ companyName: "Acme" }); console.log("Matches:", result.data.count); ``` ## peopleFields() **Signature** ```typescript [TypeScript SDK] client.search.peopleFields( options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A response envelope with the available people search filters in `data`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.search.peopleFields(); console.log(result.data); ``` ## organizationsFields() **Signature** ```typescript [TypeScript SDK] client.search.organizationsFields( options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A response envelope with the available company search filters in `data`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.search.organizationsFields(); console.log(result.data); ``` ## iterPeople() Use the same input as people(). Set `maxResults` to cap the results. With `maxResults: 0`, the iterator sends no request. **Signature** ```typescript [TypeScript SDK] client.search.iterPeople( params: Omit, "perPage"> & { perPage?: number }, options: SearchIteratorOptions ): AsyncGenerator["data"][number], void, unknown> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Omit, "perPage"> & { perPage?: number }` | At least one search filter. See [People search](https://docs.reversecontact.com/endpoints/search-people) for every filter and its nested fields. | | `params.cursor` | `string` | Opaque cursor from `metadata.nextCursor`. Use it to request the next page | | `params.perPage` | `number` | Profiles per page (1 to 100, default `25`) | | `options` **required** | `SearchIteratorOptions` | Required `maxResults`, a nonnegative safe integer. Also accepts request options. | **Returns** An `AsyncGenerator` of individual results. The iterator requests pages as needed. A repeated cursor raises `ReverseContactError`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. `ReverseContactError` also reports invalid `perPage` or `maxResults` values and repeated cursors. **Example** **Example** ```typescript [TypeScript SDK] for await (const result of client.search.iterPeople( { firstName: "Jane" }, { maxResults: 20 } )) { console.log(result); } ``` ## iterPeopleByPrompt() Use the same input as peopleByPrompt(). Set `maxResults` to cap the results. Prompt searches also charge per page. With `maxResults: 0`, the iterator sends no request. **Signature** ```typescript [TypeScript SDK] client.search.iterPeopleByPrompt( params: Omit, "perPage"> & { perPage?: number }, options: SearchIteratorOptions ): AsyncGenerator["data"][number], void, unknown> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Omit, "perPage"> & { perPage?: number }` | Request body. See the fields and their requirements below. | | `params.cursor` | `string` | Opaque cursor from `metadata.nextCursor`. Use it to request the next page with the same `prompt` | | `params.prompt` **required** | `string` | Plain-language description of the people to find, in any language. At least 3 characters and at most 1000 tokens | | `params.perPage` | `number` | Profiles per page (1 to 100, default `25`) | | `options` **required** | `SearchIteratorOptions` | Required `maxResults`, a nonnegative safe integer. Also accepts request options. | **Returns** An `AsyncGenerator` of individual results. The iterator requests pages as needed. A repeated cursor raises `ReverseContactError`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. `ReverseContactError` also reports invalid `perPage` or `maxResults` values and repeated cursors. **Example** **Example** ```typescript [TypeScript SDK] for await (const result of client.search.iterPeopleByPrompt( { prompt: "Software founders in Paris" }, { maxResults: 20 } )) { console.log(result); } ``` ## iterPeopleLookalike() Use the same input as peopleLookalike(). Set `maxResults` to cap the results. With `maxResults: 0`, the iterator sends no request. **Signature** ```typescript [TypeScript SDK] client.search.iterPeopleLookalike( params: Omit, "perPage"> & { perPage?: number }, options: SearchIteratorOptions ): AsyncGenerator["data"][number], void, unknown> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Omit, "perPage"> & { perPage?: number }` | Request body. See the fields and their requirements below. | | `params.url` **conditional** | `string` | Public Social profile URL of the seed (e.g. `https://social.com/in/janedoe`). Required unless `id` is given. | | `params.id` **conditional** | `string` | Reverse Contact person id of the seed (e.g. `prs_01jbd5vewyenebxgaz869ffe5t`), as returned in the `id` field of any person response. Required unless `url` is given. | | `params.cursor` | `string` | Opaque cursor from `metadata.nextCursor`. Use it to request the next page | | `params.perPage` | `number` | Profiles per page (1 to 100, default `25`) | | `options` **required** | `SearchIteratorOptions` | Required `maxResults`, a nonnegative safe integer. Also accepts request options. | **Returns** An `AsyncGenerator` of individual results. The iterator requests pages as needed. A repeated cursor raises `ReverseContactError`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. `ReverseContactError` also reports invalid `perPage` or `maxResults` values and repeated cursors. A seed absent from the search index raises `NotFoundError`, rather than returning `null`. **Example** **Example** ```typescript [TypeScript SDK] for await (const result of client.search.iterPeopleLookalike( { url: "https://social.com/in/janedoe" }, { maxResults: 20 } )) { console.log(result); } ``` ## iterOrganizations() Use the same input as organizations(). Set `maxResults` to cap the results. With `maxResults: 0`, the iterator sends no request. **Signature** ```typescript [TypeScript SDK] client.search.iterOrganizations( params: Omit, "perPage"> & { perPage?: number }, options: SearchIteratorOptions ): AsyncGenerator["data"][number], void, unknown> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Omit, "perPage"> & { perPage?: number }` | Request body. See the fields and their requirements below. | | `params.companyDomain` | `string` | Filter by website domain (e.g. `acme.com`). Disposable domains are rejected with `422` | | `params.companyName` | `string` | Filter by company name | | `params.followersCount` | `Body<"searchOrganizations">["followersCount"]` | Filter accepted by [Company search](https://docs.reversecontact.com/endpoints/search-companies). | | `params.location` | `string \| string[]` | Filter accepted by [Company search](https://docs.reversecontact.com/endpoints/search-companies). | | `params.maxDataAgeDate` | `string` | ISO 8601 datetime. Only returns companies refreshed after this date. Needs at least one other filter | | `params.cursor` | `string` | Opaque cursor from `metadata.nextCursor`. Use it to request the next page | | `params.industry` | `string \| string[]` | Industry, or array of industries: a company matches if its industry is any of them. See [Industry values](https://docs.reversecontact.com/reference/industries) for the full list | | `params.specialities` | `string \| string[]` | Speciality, or array of specialities: a company matches if it lists any of them. See [Specialities](https://docs.reversecontact.com/endpoints/search-companies#specialities) | | `params.employeeCountRange` | `Body<"searchOrganizations">["employeeCountRange"]` | Filter accepted by [Company search](https://docs.reversecontact.com/endpoints/search-companies). | | `params.foundedYear` | `Body<"searchOrganizations">["foundedYear"]` | Filter accepted by [Company search](https://docs.reversecontact.com/endpoints/search-companies). | | `params.perPage` | `number` | Companies per page (1 to 100, default `25`) | | `options` **required** | `SearchIteratorOptions` | Required `maxResults`, a nonnegative safe integer. Also accepts request options. | **Returns** An `AsyncGenerator` of individual results. The iterator requests pages as needed. A repeated cursor raises `ReverseContactError`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. `ReverseContactError` also reports invalid `perPage` or `maxResults` values and repeated cursors. **Example** **Example** ```typescript [TypeScript SDK] for await (const result of client.search.iterOrganizations( { companyName: "Acme" }, { maxResults: 20 } )) { console.log(result); } ``` ## iterOrganizationsByPrompt() Use the same input as organizationsByPrompt(). Set `maxResults` to cap the results. Prompt searches also charge per page. With `maxResults: 0`, the iterator sends no request. **Signature** ```typescript [TypeScript SDK] client.search.iterOrganizationsByPrompt( params: Omit, "perPage"> & { perPage?: number }, options: SearchIteratorOptions ): AsyncGenerator["data"][number], void, unknown> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Omit, "perPage"> & { perPage?: number }` | Request body. See the fields and their requirements below. | | `params.cursor` | `string` | Opaque cursor from `metadata.nextCursor`. Use it to request the next page with the same `prompt` | | `params.prompt` **required** | `string` | Plain-language description of the companies to find, in any language. At least 3 characters and at most 1000 tokens | | `params.perPage` | `number` | Companies per page (1 to 100, default `25`) | | `options` **required** | `SearchIteratorOptions` | Required `maxResults`, a nonnegative safe integer. Also accepts request options. | **Returns** An `AsyncGenerator` of individual results. The iterator requests pages as needed. A repeated cursor raises `ReverseContactError`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. `ReverseContactError` also reports invalid `perPage` or `maxResults` values and repeated cursors. **Example** **Example** ```typescript [TypeScript SDK] for await (const result of client.search.iterOrganizationsByPrompt( { prompt: "Software companies in France" }, { maxResults: 20 } )) { console.log(result); } ``` ## Related guides [Search within a budget](https://docs.reversecontact.com/sdk/javascript/search-budget), [Filter values as constants](https://docs.reversecontact.com/sdk/javascript/filter-constants). Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### Which SDK search iterator limits the number of results? Every search iterator requires `maxResults`. Use `iterPeople`, `iterPeopleByPrompt`, `iterPeopleLookalike`, `iterOrganizations` or `iterOrganizationsByPrompt`. ### Can I count people or companies without spending credits? `client.search.countPeople()` and `client.search.countOrganizations()` return free match counts in `result.data.count`. ### What does a prompt search return besides profiles? A prompt search returns a `Page` with results in `data` and the interpreted filters in `interpretation`. Each requested page adds 1 credit to the per-result cost. --- # client.contact > Use client.contact in the Reverse Contact TypeScript SDK to find emails and phone numbers or verify addresses, with job results, costs and safe retries. - Page: https://docs.reversecontact.com/sdk/javascript/contact - Last verified: 2026-11-04 Start jobs to find email addresses or phone numbers, or verify an email address. | Method | Endpoint | Cost | | ------ | -------- | ---- | | `client.contact.findEmail()` | [Find Email](https://docs.reversecontact.com/endpoints/contact-email) | 3 credits when a professional email is found, 5 for a personal one, regardless of emailType. An any lookup costs 3, 5 or 8 (8 when it finds both). A lookup that finds none is free. | | `client.contact.verifyEmail()` | [Verify Email](https://docs.reversecontact.com/endpoints/contact-email-verify) | 1 credit per verdict, regardless of the outcome. A check that fails is refunded. | | `client.contact.findPhone()` | [Find Phone](https://docs.reversecontact.com/endpoints/contact-phone) | 20 credits when a phone number is found. A lookup that finds none is free. | Create `client` by following [Install and configure](https://docs.reversecontact.com/sdk/javascript/install). The `@reverse-contact/sdk` package exports `Body`, `Data`, `Query` and the other types used in the signatures. JSON responses contain `data` and optional `quotas` and `metadata`. They also include the original `response`, with its body already read. See [First call](https://docs.reversecontact.com/sdk/javascript/first-call) for response handling. HTTP errors use the [APIError subclasses and retry rules](https://docs.reversecontact.com/sdk/javascript/errors-and-retries): `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` and `InternalServerError`, according to the API response. Other statuses use `APIError`. Contact methods generate an `Idempotency-Key` for each call and reuse that key on automatic retries. Save an explicit `idempotencyKey` to retry after restarting. See [Safe retries](https://docs.reversecontact.com/sdk/javascript/errors-and-retries). ## findEmail() Provide `url` or `id`. You can also use `fullName` with `companyDomain`, or `firstName` and `lastName` with `companyDomain`. If you supply both URL and id, id takes precedence. `emailType` defaults to `professional`. **Signature** ```typescript [TypeScript SDK] client.contact.findEmail( params: Body<"findContactEmail">, options?: IdempotentRequestOptions ): Promise> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"findContactEmail">` | Request body. See the fields and their requirements below. | | `params.url` **conditional** | `string` | Public Social profile URL, such as `https://www.social.com/in/janedoe`. Use this to look up a person by URL. | | `params.id` **conditional** | `string` | Reverse Contact person id from a previous response, such as `prs_01jbd5vewyenebxgaz869ffe5t`. Use this to look up a person by id. | | `params.fullName` **conditional** | `string` | Person's full name, such as `Jane Doe`. Use this with `companyDomain`. | | `params.firstName` **conditional** | `string` | Person's first name. Use this with `lastName` and `companyDomain` instead of `fullName`. | | `params.lastName` **conditional** | `string` | Person's last name. Use this with `firstName` and `companyDomain` instead of `fullName`. | | `params.companyDomain` **conditional** | `string` | Company domain, such as `acme.com`. Required when you look up a person by name. | | `params.emailType` | `string` | Kind of email to find: `professional` (default), `personal`, or `any` to accept either. Omit it to find professional emails. | | `params.webhookUrl` | `string` | HTTPS URL that receives the callback. Omit it to retrieve the result by polling instead. | | `options` | `IdempotentRequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). Also accepts `idempotencyKey`. The SDK generates a key if you omit it. | **Returns** A `Job`, with the `webhookId` in `job.id`. Call `await job.wait()` to get `data.result`, an array of `{ value, type }` email entries. Waiting is free. See [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs) for `wait()` options and webhook delivery. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. Waiting raises `JobFailedError` for an errored job and `JobTimeoutError` when the overall deadline expires. If you supply an invalid idempotency key, the SDK raises `ReverseContactError` before sending a request. **Example** **Example** ```typescript [TypeScript SDK] const job = await client.contact.findEmail({ fullName: "Jane Doe", companyDomain: "acme.com" }); console.log("Save job id:", job.id); const done = await job.wait(); for (const email of done.data.result ?? []) console.log(email.value, email.type); ``` ## verifyEmail() Provide the email address to verify. Addresses returned by Find Email are already verified. **Signature** ```typescript [TypeScript SDK] client.contact.verifyEmail( params: Body<"verifyContactEmail">, options?: IdempotentRequestOptions ): Promise> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"verifyContactEmail">` | Request body. See the fields and their requirements below. | | `params.email` **required** | `string` | The email address to check, such as `jane@acme.com`. | | `params.webhookUrl` | `string` | HTTPS URL that receives the result, including error outcomes. Omit it to poll for the verdict instead. | | `options` | `IdempotentRequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). Also accepts `idempotencyKey`. The SDK generates a key if you omit it. | **Returns** A `Job`, with the `webhookId` in `job.id`. Call `await job.wait()` to get `data.result` with `email` and `status`. The status is `valid`, `risky`, `invalid` or `unknown`. Waiting is free. See [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs) for `wait()` options and webhook delivery. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. Waiting raises `JobFailedError` for an errored job and `JobTimeoutError` when the overall deadline expires. If you supply an invalid idempotency key, the SDK raises `ReverseContactError` before sending a request. **Example** **Example** ```typescript [TypeScript SDK] const job = await client.contact.verifyEmail({ email: "jane@acme.com" }); console.log("Save job id:", job.id); const done = await job.wait(); console.log(done.data.result?.status); ``` ## findPhone() Provide `url` or `id`. You can also use `fullName` with `companyDomain`, or `firstName` and `lastName` with `companyDomain`. If you supply both URL and id, id takes precedence. **Signature** ```typescript [TypeScript SDK] client.contact.findPhone( params: Body<"findContactPhone">, options?: IdempotentRequestOptions ): Promise> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"findContactPhone">` | Request body. See the fields and their requirements below. | | `params.url` **conditional** | `string` | Public Social profile URL, such as `https://www.social.com/in/janedoe`. Use this to look up a person by URL. | | `params.id` **conditional** | `string` | Reverse Contact person id from a previous response, such as `prs_01jbd5vewyenebxgaz869ffe5t`. Use this to look up a person by id. | | `params.fullName` **conditional** | `string` | Person's full name, such as `Jane Doe`. Use this with `companyDomain`. | | `params.firstName` **conditional** | `string` | Person's first name. Use this with `lastName` and `companyDomain` instead of `fullName`. | | `params.lastName` **conditional** | `string` | Person's last name. Use this with `firstName` and `companyDomain` instead of `fullName`. | | `params.companyDomain` **conditional** | `string` | Company domain, such as `acme.com`. Required when you look up a person by name. | | `params.webhookUrl` | `string` | HTTPS URL that receives the callback. Omit it to retrieve the result by polling instead. | | `options` | `IdempotentRequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). Also accepts `idempotencyKey`. The SDK generates a key if you omit it. | **Returns** A `Job`, with the `webhookId` in `job.id`. Call `await job.wait()` to get `data.result`, an array of `{ value, type }` phone entries. Waiting is free. See [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs) for `wait()` options and webhook delivery. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. Waiting raises `JobFailedError` for an errored job and `JobTimeoutError` when the overall deadline expires. If you supply an invalid idempotency key, the SDK raises `ReverseContactError` before sending a request. **Example** **Example** ```typescript [TypeScript SDK] const job = await client.contact.findPhone({ url: "https://social.com/in/janedoe" }); console.log("Save job id:", job.id); const done = await job.wait(); for (const phone of done.data.result ?? []) console.log(phone.value, phone.type); ``` ## Related guides [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs), [Errors and retries](https://docs.reversecontact.com/sdk/javascript/errors-and-retries). Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### Does client.contact return email results immediately? Contact methods return a `Job` acknowledgement. Call `job.wait()` to read the final result in `data.result`. ### Which SDK method verifies an email address? `client.contact.verifyEmail()` starts a verification job. The result contains `email` and a status of `valid`, `risky`, `invalid` or `unknown`. ### How do I avoid duplicate contact jobs during retries? Contact methods generate an `Idempotency-Key` per call and reuse that key on automatic retries. Save an explicit `idempotencyKey` to retry after restarting. --- # client.lists > Use client.lists in the Reverse Contact TypeScript SDK to create Lists, manage members, import data, follow operations and export CSV with typed methods. - Page: https://docs.reversecontact.com/sdk/javascript/lists - Last verified: 2026-11-04 Create Lists, manage their members, and follow import operations. | Method | Endpoint | Cost | | ------ | -------- | ---- | | `client.lists.list()` | [All Lists](https://docs.reversecontact.com/endpoints/lists) | Free. | | `client.lists.create()` | [Create a List](https://docs.reversecontact.com/endpoints/list-create) | Free. | | `client.lists.retrieve()` | [Get a List](https://docs.reversecontact.com/endpoints/list) | Free. | | `client.lists.delete()` | [Delete a List](https://docs.reversecontact.com/endpoints/list-delete) | Free. | | `client.lists.importSignals()` | [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals) | Free to queue. Each new member costs its identity price. Each data family you request costs the same as its endpoint, up to your maxCredits. | | `client.lists.export()` | [Export a List](https://docs.reversecontact.com/endpoints/list-export) | Free. | | `client.lists.listAll()` | [All Lists](https://docs.reversecontact.com/endpoints/lists) | Free. | | `client.lists.members.list()` | [List members](https://docs.reversecontact.com/endpoints/list-members) | Free. | | `client.lists.members.add()` | [Add members](https://docs.reversecontact.com/endpoints/list-members-add) | Free to call. Each new person costs 1 credit from a Social profile URL (2 with live), 2 from an email or a name. Each new company costs 1 credit from a company page URL or a company id (2 when the page is read live), 2 from a domain. A member already on the List, or a Signal no one matches, is free. | | `client.lists.members.remove()` | [Remove members](https://docs.reversecontact.com/endpoints/list-members-remove) | Free. | | `client.lists.members.listAll()` | [List members](https://docs.reversecontact.com/endpoints/list-members) | Free. | | `client.lists.operations.retrieve()` | [Read a List operation](https://docs.reversecontact.com/endpoints/list-operation) | Free even with a zero balance. Does not count against rate limits. | | `client.lists.operations.cancel()` | [Cancel a List operation](https://docs.reversecontact.com/endpoints/list-operation-cancel) | Free. Lines that had not started are never charged. | | `client.lists.operations.wait()` | [Read a List operation](https://docs.reversecontact.com/endpoints/list-operation) | Free even with a zero balance. Does not count against rate limits. | Create `client` by following [Install and configure](https://docs.reversecontact.com/sdk/javascript/install). The examples use `listId`, `operationId` and `memberId` values saved from earlier responses. The `@reverse-contact/sdk` package exports `Body`, `Data`, `Query` and the other types used in the signatures. JSON responses contain `data` and optional `quotas` and `metadata`. They also include the original `response`, with its body already read. See [First call](https://docs.reversecontact.com/sdk/javascript/first-call) for response handling. HTTP errors use the [APIError subclasses and retry rules](https://docs.reversecontact.com/sdk/javascript/errors-and-retries): `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` and `InternalServerError`, according to the API response. Other statuses use `APIError`. ## list() **Signature** ```typescript [TypeScript SDK] client.lists.list( params?: Query<"listLists">, options?: RequestOptions ): Promise["items"][number], undefined>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` | `Query<"listLists">` | Query parameters. Omit to use the defaults. | | `params.perPage` | `number` | Lists per page, from 1 to 200. Defaults to 200. | | `params.cursor` | `string` | The `nextCursor` of the previous page. A malformed cursor, or one sent with other parameters, returns `400`. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A `Page` with results in `data` and pagination through `nextCursor`, `hasNextPage()` and `getNextPage()`. The page also includes optional `quotas` and `metadata`, and the original `response`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const page = await client.lists.list({ perPage: 20 }); for (const list of page.data) console.log(list.id, list.name); ``` ## create() The SDK generates an `Idempotency-Key` if you omit it and reuses the key on automatic retries. Save your own key if you need to repeat the request after a restart. **Signature** ```typescript [TypeScript SDK] client.lists.create( params: Body<"createList">, options?: IdempotentRequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"createList">` | Request body. See the fields and their requirements below. | | `params.name` **required** | `string` | List name, 1 to 200 characters. Spaces around it are removed. | | `params.entity` **required** | `"persons" \| "companies"` | `persons` for people, `companies` for companies | | `options` | `IdempotentRequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). Also accepts `idempotencyKey`. The SDK generates a key if you omit it. | **Returns** A response with the new List in `data`. Save `data.id` for later operations. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const created = await client.lists.create( { name: "Prospects", entity: "persons" }, { idempotencyKey: "prospects-2026-11-04" } ); console.log(created.data.id); ``` ## retrieve() **Signature** ```typescript [TypeScript SDK] client.lists.retrieve( listId: string, params?: Query<"getList">, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `listId` **required** | `string` | List UUID from `client.lists.create()` or `client.lists.list()`. | | `params` | `Query<"getList">` | Query parameters. Omit to use the defaults. | | `params.perPage` | `number` | Rows per page, from 1 to 1,000. Defaults to 100. | | `params.cursor` | `string` | The `nextCursor` of the previous page, sent with the same `kind`. A malformed cursor, or one sent with other parameters, returns `400`. | | `params.kind` | `"all" \| "members" \| "pending"` | `all` (default) combines members and Pending Intake by added date. Use `members` or `pending` to return only that kind | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A response envelope with List details, `data.members` and `data.nextCursor`. This method does not return a `Page` object. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.lists.retrieve(listId, { kind: "all", perPage: 100 }); console.log(result.data.members, result.data.nextCursor); ``` ## delete() **Signature** ```typescript [TypeScript SDK] client.lists.delete( listId: string, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `listId` **required** | `string` | List UUID from `client.lists.create()` or `client.lists.list()`. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A response envelope containing the deletion acknowledgement in `data`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.lists.delete(listId); console.log(result.data); ``` ## importSignals() Send `signals` or `sourceUrl`, not both. The SDK generates `operationId` when omitted. Keep your own operation UUID to resume after a restart. Inspect `clientAction` and `nextStep` before resending. You must set `maxCredits` when you request data families and the worst-case cost of the operation exceeds 25 credits. **Signature** ```typescript [TypeScript SDK] client.lists.importSignals( listId: string, params: Body<"importListSignals">, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `listId` **required** | `string` | List UUID from `client.lists.create()` or `client.lists.list()`. | | `params` **required** | `Body<"importListSignals">` | Request body. See the fields and their requirements below. | | `params.signals` **conditional** | `Body<"importListSignals">["signals"]` | 1 to 25,000 Signals, with the fields of [Add members](https://docs.reversecontact.com/endpoints/list-members-add#request-body), `externalId` included | | `params.sourceUrl` **conditional** | `string` | A public HTTPS CSV file or Google Sheet URL instead of `signals`: up to 25,000 rows and 25 MiB. Columns are matched from their headers (Social URL, email, first name, last name, company, domain). | | `params.live` | `boolean` | `true` fetches the profile of each Social profile URL live. Omitted or `false`, saved data is used. No effect on a companies List. | | `params.families` | `("profile" \| "contact_email" \| "contact_phone" \| "company")[]` | Data to buy for each member: `profile`, `company`, `contact_email`, `contact_phone`. A companies List takes `company` only. | | `params.maxCredits` **conditional** | `number` | The most this operation may spend. Required with `families` when the worst case is above 25 credits. If omitted, the ceiling is the worst-case cost of the import. | | `params.operationId` | `string` | Your UUID for this operation. Sending the same request again with it returns the same operation and never charges twice. | | `params.webhookUrl` | `string` | Public HTTPS URL that receives a callback each time the operation stops moving. See [Webhook callback](https://docs.reversecontact.com/endpoints/list-intake-signals#webhook-callback) | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** An operation acknowledgement in `data`, including `operationId` and `pollUrl`. Call `client.lists.operations.wait(listId, operationId)` to follow progress. See [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/javascript/lists-and-bulk). **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const imported = await client.lists.importSignals(listId, { signals: [{ email: "jane@acme.com", externalId: "crm-001" }], families: ["contact_email"], maxCredits: 100 }); console.log("Save operation id:", imported.data.operationId); const progress = await client.lists.operations.wait(listId, imported.data.operationId); console.log(progress.data.clientAction, progress.data.nextStep); ``` ## export() **Signature** ```typescript [TypeScript SDK] client.lists.export( listId: string, options?: RequestOptions ): Promise ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `listId` **required** | `string` | List UUID from `client.lists.create()` or `client.lists.list()`. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A CSV `string`, not a JSON response envelope. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options. **Example** **Example** ```typescript [TypeScript SDK] const csv = await client.lists.export(listId); console.log(csv); ``` ## listAll() **Signature** ```typescript [TypeScript SDK] client.lists.listAll( params?: Query<"listLists">, options?: ListIteratorOptions ): AsyncGenerator["items"][number], void, unknown> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` | `Query<"listLists">` | Query parameters. Omit to use the defaults. | | `params.perPage` | `number` | Lists per page, from 1 to 200. Defaults to 200. | | `params.cursor` | `string` | The `nextCursor` of the previous page. A malformed cursor, or one sent with other parameters, returns `400`. | | `options` | `ListIteratorOptions` | Optional `maxResults`, a nonnegative safe integer. Omit to read all pages. Also accepts request options. | **Returns** An `AsyncGenerator` of individual results. The iterator requests pages as needed. A repeated cursor raises `ReverseContactError`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. `ReverseContactError` also reports invalid `perPage` or `maxResults` values and repeated cursors. **Example** **Example** ```typescript [TypeScript SDK] for await (const list of client.lists.listAll()) { console.log(list.id, list.name); } ``` ## members.list() **Signature** ```typescript [TypeScript SDK] client.lists.members.list( listId: string, params?: Query<"listListMembers">, options?: RequestOptions ): Promise["items"][number], undefined>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `listId` **required** | `string` | List UUID from `client.lists.create()` or `client.lists.list()`. | | `params` | `Query<"listListMembers">` | Query parameters. Omit to use the defaults. | | `params.perPage` | `number` | Rows per page, from 1 to 1,000. Defaults to 100. | | `params.cursor` | `string` | The `nextCursor` of the previous page, sent with the same filters. A malformed cursor, or one sent with other filters, returns `400`. | | `params.kind` | `"all" \| "members" \| "pending"` | `all` (default) combines members and Pending Intake by added date. Use `members` or `pending` to return only that kind | | `params.externalId` | `string` | Returns members with this identifier from your system (1 to 200 characters) | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A `Page` with results in `data` and pagination through `nextCursor`, `hasNextPage()` and `getNextPage()`. The page also includes optional `quotas` and `metadata`, and the original `response`. Member pages also expose `memberCount`, `pendingIntakeCount` and `extras`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const page = await client.lists.members.list(listId, { kind: "members", perPage: 100 }); console.log(page.data, page.memberCount, page.pendingIntakeCount); ``` ## members.add() Add up to 1,000 Signals. If any Signal is invalid, the API rejects the whole request before charging. `error.details.signalIndex` identifies the invalid entry. **Signature** ```typescript [TypeScript SDK] client.lists.members.add( listId: string, params: Body<"addListMembers">, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `listId` **required** | `string` | List UUID from `client.lists.create()` or `client.lists.list()`. | | `params` **required** | `Body<"addListMembers">` | Request body. See the fields and their requirements below. | | `params.signals` **required** | `Body<"addListMembers">["signals"]` | 1 to 1,000 Signals, described below | | `params.source` | `"file" \| "paste"` | `paste` (default) or `file`: records how a Pending Intake line arrived | | `params.live` | `boolean` | `true` fetches the profile of a Social profile URL live (2 credits). Omitted or `false`, saved data is used (1 credit). No effect on a companies List. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A response with the outcome for each Signal in `data`. See the endpoint response structure for member and Pending Intake results. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.lists.members.add(listId, { signals: [{ email: "jane@acme.com", externalId: "crm-001" }] }); console.log(result.data); ``` ## members.remove() Send the member or Pending Intake ids to remove. The SDK sends the JSON body with DELETE. **Signature** ```typescript [TypeScript SDK] client.lists.members.remove( listId: string, params: Body<"removeListMembers">, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `listId` **required** | `string` | List UUID from `client.lists.create()` or `client.lists.list()`. | | `params` **required** | `Body<"removeListMembers">` | Request body. See the fields and their requirements below. | | `params.memberIds` **conditional** | `string[]` | Member ids (UUID) to remove, the `id` of rows with `kind: "member"` | | `params.pendingIds` **conditional** | `string[]` | Pending Intake ids (UUID) to dismiss, the `id` of rows with `kind: "pending"` | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A response envelope with the removal result in `data`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.lists.members.remove(listId, { memberIds: [memberId] }); console.log(result.data); ``` ## members.listAll() **Signature** ```typescript [TypeScript SDK] client.lists.members.listAll( listId: string, params?: Query<"listListMembers">, options?: ListIteratorOptions ): AsyncGenerator["items"][number], void, unknown> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `listId` **required** | `string` | List UUID from `client.lists.create()` or `client.lists.list()`. | | `params` | `Query<"listListMembers">` | Query parameters. Omit to use the defaults. | | `params.perPage` | `number` | Rows per page, from 1 to 1,000. Defaults to 100. | | `params.cursor` | `string` | The `nextCursor` of the previous page, sent with the same filters. A malformed cursor, or one sent with other filters, returns `400`. | | `params.kind` | `"all" \| "members" \| "pending"` | `all` (default) combines members and Pending Intake by added date. Use `members` or `pending` to return only that kind | | `params.externalId` | `string` | Returns members with this identifier from your system (1 to 200 characters) | | `options` | `ListIteratorOptions` | Optional `maxResults`, a nonnegative safe integer. Omit to read all pages. Also accepts request options. | **Returns** An `AsyncGenerator` of individual results. The iterator requests pages as needed. A repeated cursor raises `ReverseContactError`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. `ReverseContactError` also reports invalid `perPage` or `maxResults` values and repeated cursors. **Example** **Example** ```typescript [TypeScript SDK] for await (const member of client.lists.members.listAll(listId, { kind: "members" })) { console.log(member); } ``` ## operations.retrieve() Use `rows: "true"` to include a page of line results. `perPage` and `cursor` require `rows: "true"`. **Signature** ```typescript [TypeScript SDK] client.lists.operations.retrieve( listId: string, operationId: string, params?: Query<"getListOperation">, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `listId` **required** | `string` | List UUID from `client.lists.create()` or `client.lists.list()`. | | `operationId` **required** | `string` | Import UUID from `client.lists.importSignals()`. | | `params` | `Query<"getListOperation">` | Query parameters. Omit to use the defaults. | | `params.wait` | `number` | Hold the request up to this many seconds, 0 to 20, while the operation runs. It answers as soon as the operation settles, pauses or stops, or with the operation still running when the wait ends. Defaults to 0. | | `params.rows` | `"true" \| "false"` | `true` adds one page of per-line results | | `params.cursor` | `string` | With `rows=true`: the `rows.nextCursor` of the previous page | | `params.perPage` | `number` | With `rows=true`: lines per page, from 1 to 1,000. Defaults to 200. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** `Result` with progress, `clientAction`, `stopReason` and `nextStep` in `data`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.lists.operations.retrieve(listId, operationId, { rows: "true", wait: 20 }); console.log(result.data.clientAction, result.data.nextStep); ``` ## operations.cancel() Request cancellation of this List import. Lines that have not started are not charged. **Signature** ```typescript [TypeScript SDK] client.lists.operations.cancel( listId: string, operationId: string, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `listId` **required** | `string` | List UUID from `client.lists.create()` or `client.lists.list()`. | | `operationId` **required** | `string` | Import UUID from `client.lists.importSignals()`. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A response envelope with the operation cancellation result in `data`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.lists.operations.cancel(listId, operationId); console.log(result.data); ``` ## operations.wait() Follow `clientAction`, not just `status`. The SDK polls on `poll` and returns on `done`, `resume` or `reconcile`. Inspect `stopReason` and `nextStep`. The SDK never replays an operation that needs reconciliation. **Signature** ```typescript [TypeScript SDK] client.lists.operations.wait( listId: string, operationId: string, options?: WaitOptions ): Promise> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `listId` **required** | `string` | List UUID from `client.lists.create()` or `client.lists.list()`. | | `operationId` **required** | `string` | Import UUID from `client.lists.importSignals()`. | | `options` | `WaitOptions` | Optional overall `timeout`, `pollWait`, `onPoll`, `headers` and `signal`. See [Wait options](https://docs.reversecontact.com/sdk/javascript/timeouts). | **Returns** `Result` when `clientAction` becomes `done`, `resume` or `reconcile`. A pause or unsuccessful operation does not by itself raise `JobFailedError`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. `JobTimeoutError` means the overall wait deadline has expired. An unsuccessful terminal state does not raise `JobFailedError`. An unknown `clientAction` raises `ReverseContactError`. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.lists.operations.wait(listId, operationId, { timeout: 30 * 60 * 1000, onPoll: snapshot => { console.log(snapshot.data.status); } }); console.log(result.data.clientAction, result.data.nextStep); ``` ## Related guides [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/javascript/lists-and-bulk), [Timeouts and cancellation](https://docs.reversecontact.com/sdk/javascript/timeouts). Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### How do I read every member of a List with the SDK? Iterate over `client.lists.members.listAll(listId)`. Pagination is free and `maxResults` is optional. ### How do I wait for a List import to stop? Call `client.lists.operations.wait(listId, operationId)`. The method returns when `clientAction` is `done`, `resume` or `reconcile`. ### Does client.lists.export return JSON? `client.lists.export(listId)` returns CSV text as a string, not a JSON response envelope. --- # client.bulk > Use client.bulk in the Reverse Contact TypeScript SDK to enrich people in bulk, wait for the job, and read results as they come or as CSV or NDJSON. - Page: https://docs.reversecontact.com/sdk/javascript/bulk - Last verified: 2026-11-04 Submit people for Bulk enrichment, then check progress, wait for the job to stop, cancel the job, read its results as they come or download them. | Method | Endpoint | Cost | | ------ | -------- | ---- | | `client.bulk.enrichPersons()` | [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) | Free to send. Each item costs the same as its individual API call. Credits are reserved when the job is created and refunded for data that is not found. | | `client.bulk.list()` | [Job history](https://docs.reversecontact.com/endpoints/jobs) | Free. | | `client.bulk.retrieve()` | [Get a job](https://docs.reversecontact.com/endpoints/job) | Free even with a zero balance. Does not count against rate limits. | | `client.bulk.results()` | [Download job results](https://docs.reversecontact.com/endpoints/job-results) | Free even with a zero balance. | | `client.bulk.rows()` | [Read job results by page](https://docs.reversecontact.com/endpoints/job-rows) | Free even with a zero balance. Each page counts toward rate limits. | | `client.bulk.iterateRows()` | [Read job results by page](https://docs.reversecontact.com/endpoints/job-rows) | Free even with a zero balance. Each page counts toward rate limits. | | `client.bulk.cancel()` | [Cancel a job](https://docs.reversecontact.com/endpoints/job-cancel) | Free. Items that had not started are never charged. | | `client.bulk.wait()` | [Get a job](https://docs.reversecontact.com/endpoints/job) | Free even with a zero balance. Does not count against rate limits. | | `client.bulk.listAll()` | [Job history](https://docs.reversecontact.com/endpoints/jobs) | Free. | Create `client` by following [Install and configure](https://docs.reversecontact.com/sdk/javascript/install). The examples use a saved `jobId` from the relevant job family. The `@reverse-contact/sdk` package exports `Body`, `Data`, `Query` and the other types used in the signatures. JSON responses contain `data` and optional `quotas` and `metadata`. They also include the original `response`, with its body already read. See [First call](https://docs.reversecontact.com/sdk/javascript/first-call) for response handling. HTTP errors use the [APIError subclasses and retry rules](https://docs.reversecontact.com/sdk/javascript/errors-and-retries): `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` and `InternalServerError`, according to the API response. Other statuses use `APIError`. ## enrichPersons() Send 1 to 25,000 items. The SDK generates an `Idempotency-Key` if you omit it and reuses the key on automatic retries. Save your own key to repeat the same submission after a restart. **Signature** ```typescript [TypeScript SDK] client.bulk.enrichPersons( params: Body<"bulkEnrichPersons">, options?: IdempotentRequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` **required** | `Body<"bulkEnrichPersons">` | Request body. See the fields and their requirements below. | | `params.items` **required** | `Body<"bulkEnrichPersons">["items"]` | 1 to 25,000 people, see [Items](https://docs.reversecontact.com/endpoints/bulk-enrich-persons#items) | | `params.families` | `("profile" \| "contact_email" \| "contact_phone")[]` | What to look up for each item: `profile`, `contact_email`, `contact_phone`. Defaults to `["profile"]`. | | `params.emailType` | `string` | With `contact_email`, the kind of email to find: `professional` (default), `personal` or `any` | | `params.live` | `boolean` | `true` fetches the profile of a Social profile URL item live. Omitted or `false`, saved data is used. | | `params.name` | `string` | A name for the job, up to 255 characters, shown in the job history and in the app. Defaults to `Sent via API`. | | `params.webhookUrl` | `string` | Public HTTPS URL that receives one `POST` when the job settles. See [Webhook callback](https://docs.reversecontact.com/endpoints/bulk-enrich-persons#webhook-callback) | | `options` | `IdempotentRequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). Also accepts `idempotencyKey`. The SDK generates a key if you omit it. | **Returns** A response envelope with the background job in `data.job`. Save `data.job.id`, then call `client.bulk.wait(id)`. See [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/javascript/lists-and-bulk). This is not a `Job` object. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const created = await client.bulk.enrichPersons({ items: [{ linkedinUrl: "https://social.com/in/janedoe", externalId: "crm-001" }], families: ["profile"] }, { idempotencyKey: "crm-export-2026-11-04" }); console.log("Save job id:", created.data.job.id); const progress = await client.bulk.wait(created.data.job.id); console.log(progress.data.status); ``` ## list() Read background job history, including Bulk enrichment and List jobs allowed by the API key permissions. **Signature** ```typescript [TypeScript SDK] client.bulk.list( params?: Query<"listJobs">, options?: RequestOptions ): Promise["items"][number], undefined>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` | `Query<"listJobs">` | Query parameters. Omit to use the defaults. | | `params.type` | `string` | Filter by these comma-separated job types. See [Job types](https://docs.reversecontact.com/endpoints/jobs#job-types) | | `params.status` | `string` | Filter by these comma-separated statuses: `queued`, `running`, `paused`, `completed`, `cancelled`, `failed` | | `params.listId` | `string` | Return only jobs for this List (UUID) | | `params.perPage` | `number` | Jobs per page, from 1 to 100. Defaults to 50. | | `params.cursor` | `string` | The `nextCursor` of the previous page, sent with the same filters. A malformed cursor, or one sent with other filters, returns `400`. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A `Page` with results in `data` and pagination through `nextCursor`, `hasNextPage()` and `getNextPage()`. The page also includes optional `quotas` and `metadata`, and the original `response`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const page = await client.bulk.list({ type: "enrich_persons", perPage: 20 }); console.log(page.data); ``` ## retrieve() Read the current state of a background job. Use `wait` to hold the request while the job is queued or running. **Signature** ```typescript [TypeScript SDK] client.bulk.retrieve( jobId: string, params?: Query<"getJob">, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `jobId` **required** | `string` | Background job id from `created.data.job.id` or job history. | | `params` | `Query<"getJob">` | Query parameters. Omit to use the defaults. | | `params.wait` | `number` | Hold the request up to this many seconds, 0 to 20, while the job is queued or running. It answers as soon as the job settles or pauses, or with the job still running when the wait ends. Defaults to 0. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** `Result` with the current status, progress, credit usage and result links in `data`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.bulk.retrieve(jobId, { wait: 20 }); console.log(result.data.status, result.data.credits); ``` ## results() Download only when `links.results` is set, after the job completes or is cancelled. Failed jobs have no results file. **Signature** ```typescript [TypeScript SDK] client.bulk.results( jobId: string, params?: Query<"getJobResults">, options?: RequestOptions ): Promise ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `jobId` **required** | `string` | Background job id from `created.data.job.id` or job history. | | `params` | `Query<"getJobResults">` | Query parameters. Omit to use the defaults. | | `params.format` | `"csv" \| "ndjson"` | `csv` for the CSV file, `ndjson` for one `JobResultLine` JSON object per line. Defaults to `csv`. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A `string`: the CSV without its byte order mark, or with `format: "ndjson"` one JSON object per line. Prepend `"\uFEFF"` when saving the CSV for spreadsheet applications. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options. `ConflictError` with `JOB_RESULTS_NOT_READY` means the file is not ready. `ConflictError` with `JOB_RESULTS_FORMAT_UNAVAILABLE` means the job was stored before NDJSON existed: download its CSV. **Example** **Example** ```typescript [TypeScript SDK] const job = await client.bulk.retrieve(jobId); if (job.data.links.results) { const csv = await client.bulk.results(jobId); const ndjson = await client.bulk.results(jobId, { format: "ndjson" }); const items = ndjson.split("\n").filter(Boolean).map((line) => JSON.parse(line)); console.log(csv.length, items.length); } ``` ## rows() Read one page of results, in the order sent, from the start of the run. While the job runs, a page stops at the first item not finished: `data` is empty and `nextCursor` is set while the next item runs, so read it again after `Retry-After`. `nextCursor` is `null` once nothing more will come. Available from SDK 0.3.0. **Signature** ```typescript [TypeScript SDK] client.bulk.rows( jobId: string, params?: Query<"getJobRows">, options?: RequestOptions ): Promise>, Interpretation>>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `jobId` **required** | `string` | Background job id from `created.data.job.id` or job history. | | `params` | `Query<"getJobRows">` | Query parameters. Omit to read from the first item. | | `params.cursor` | `string` | `nextCursor` of the previous page, up to 512 characters. Without it, the page starts at the first item. | | `params.limit` | `number` | Items per page, 1 to 100. Defaults to 100. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A `Page` with `JobResultLine` items in `data`, `nextCursor` and `getNextPage()`, like `list()`. `extras.status` is the job status when the page was read. A page can hold fewer items than `limit`. The page also includes optional `quotas` and `metadata`, and the original `response`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. `ConflictError` with `JOB_RESULTS_FORMAT_UNAVAILABLE` means the job was stored before reading by page existed: download its CSV with `results()`. `RateLimitError` with `JOB_ROWS_BUSY` means too many pages are being read at once: the SDK retries it after `Retry-After`, as [Errors and retries](https://docs.reversecontact.com/sdk/javascript/errors-and-retries) describes. **Example** **Example** ```typescript [TypeScript SDK] const page = await client.bulk.rows(jobId, { limit: 100 }); for (const item of page.data) console.log(item.row, item.status); console.log(page.extras.status, page.nextCursor); ``` ## iterateRows() Read the results one item at a time, as they become ready, whatever the job is doing. While the next item runs, the iterator waits `Retry-After` (2 seconds by default) and reads the same position again. It ends with the job, or when the job is paused, since nothing moves until someone resumes it: read on later with `rows()` and the last cursor. A cancelled job still finishes the items it had started, within the hour, and the iterator waits for them too. Available from SDK 0.3.0. **Signature** ```typescript [TypeScript SDK] client.bulk.iterateRows( jobId: string, params?: Query<"getJobRows">, options?: RequestOptions ): AsyncGenerator ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `jobId` **required** | `string` | Background job id from `created.data.job.id` or job history. | | `params` | `Query<"getJobRows">` | `cursor` to start after a page you already read, and `limit`, 1 to 100 items per page. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. The `signal` also stops the wait between pages. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** An `AsyncGenerator` of `JobResultLine` items, in the order sent. The `@reverse-contact/sdk` package exports the `JobResultLine` type. **Errors** The errors of `rows()`. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. **Example** **Example** ```typescript [TypeScript SDK] for await (const item of client.bulk.iterateRows(jobId)) { console.log(item.row, item.input.externalId, item.status, item.creditsConsumed); } ``` ## cancel() Request a server-side stop. Items that have not started are not charged. **Signature** ```typescript [TypeScript SDK] client.bulk.cancel( jobId: string, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `jobId` **required** | `string` | Background job id from `created.data.job.id` or job history. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** A response envelope with the cancellation acknowledgement in `data`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.bulk.cancel(jobId); console.log(result.data); ``` ## wait() Wait while the background job is `queued` or `running`. A `paused` job returns control to your code even though the job can resume later. **Signature** ```typescript [TypeScript SDK] client.bulk.wait( jobId: string, options?: WaitOptions ): Promise> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `jobId` **required** | `string` | Background job id from `created.data.job.id` or job history. | | `options` | `WaitOptions` | Optional overall `timeout`, `pollWait`, `onPoll`, `headers` and `signal`. See [Wait options](https://docs.reversecontact.com/sdk/javascript/timeouts). | **Returns** `Result` when the status is `completed`, `cancelled`, `failed` or `paused`. A `failed` status does not raise `JobFailedError`. Check `data.status`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. `JobTimeoutError` means the overall wait deadline has expired. An unsuccessful terminal state does not raise `JobFailedError`. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.bulk.wait(jobId, { timeout: 30 * 60 * 1000 }); console.log(result.data.status, result.data.links.results); ``` ## listAll() **Signature** ```typescript [TypeScript SDK] client.bulk.listAll( params?: Query<"listJobs">, options?: ListIteratorOptions ): AsyncGenerator["items"][number], void, unknown> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `params` | `Query<"listJobs">` | Query parameters. Omit to use the defaults. | | `params.type` | `string` | Filter by these comma-separated job types. See [Job types](https://docs.reversecontact.com/endpoints/jobs#job-types) | | `params.status` | `string` | Filter by these comma-separated statuses: `queued`, `running`, `paused`, `completed`, `cancelled`, `failed` | | `params.listId` | `string` | Return only jobs for this List (UUID) | | `params.perPage` | `number` | Jobs per page, from 1 to 100. Defaults to 50. | | `params.cursor` | `string` | The `nextCursor` of the previous page, sent with the same filters. A malformed cursor, or one sent with other filters, returns `400`. | | `options` | `ListIteratorOptions` | Optional `maxResults`, a nonnegative safe integer. Omit to read all pages. Also accepts request options. | **Returns** An `AsyncGenerator` of individual results. The iterator requests pages as needed. A repeated cursor raises `ReverseContactError`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. `ReverseContactError` also reports invalid `perPage` or `maxResults` values and repeated cursors. **Example** **Example** ```typescript [TypeScript SDK] for await (const job of client.bulk.listAll({ type: "enrich_persons" })) { console.log(job.id, job.status); } ``` ## Related guides [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/javascript/lists-and-bulk), [Timeouts and cancellation](https://docs.reversecontact.com/sdk/javascript/timeouts). Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### How do I start a Bulk enrichment job in TypeScript? Call `client.bulk.enrichPersons()` with 1 to 25,000 items. Save `created.data.job.id` and use `client.bulk.wait()` to follow progress. ### Does client.bulk.wait throw when a Bulk job fails? `client.bulk.wait()` returns jobs in `completed`, `cancelled`, `failed` or `paused` states. Check `result.data.status`. A failed state does not raise `JobFailedError`. ### When can I download Bulk results? Call `client.bulk.results()` when `links.results` is set, after the job completes or is cancelled. The method returns CSV text, or NDJSON with `format: "ndjson"`. ### Can I read Bulk results before the job ends in TypeScript? Yes, from SDK 0.3.0. `client.bulk.iterateRows()` yields each result as soon as it is ready, in the order sent, and `client.bulk.rows()` reads one page at a time with a cursor. --- # client.jobs > Use client.jobs in the Reverse Contact TypeScript SDK to read or resume asynchronous live-fetch and contact jobs, handle failures and set wait deadlines. - Page: https://docs.reversecontact.com/sdk/javascript/jobs - Last verified: 2026-11-04 Read or wait for the webhook jobs started by live fetches and contact lookups. | Method | Endpoint | Cost | | ------ | -------- | ---- | | `client.jobs.retrieve()` | [Polling](https://docs.reversecontact.com/guides/polling) | Free and does not count toward rate limits. | | `client.jobs.wait()` | [Polling](https://docs.reversecontact.com/guides/polling) | Free and does not count toward rate limits. | Create `client` by following [Install and configure](https://docs.reversecontact.com/sdk/javascript/install). The examples use a saved `jobId` from the relevant job family. The `@reverse-contact/sdk` package exports `Body`, `Data`, `Query` and the other types used in the signatures. JSON responses contain `data` and optional `quotas` and `metadata`. They also include the original `response`, with its body already read. See [First call](https://docs.reversecontact.com/sdk/javascript/first-call) for response handling. HTTP errors use the [APIError subclasses and retry rules](https://docs.reversecontact.com/sdk/javascript/errors-and-retries): `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` and `InternalServerError`, according to the API response. Other statuses use `APIError`. `client.jobs` reads `/v2/webhooks/:webhookId`. Background jobs under `/v2/jobs/:jobId` use [client.bulk](https://docs.reversecontact.com/sdk/javascript/bulk). The `Job` returned by a live fetch or contact lookup also has `job.retrieve()` and `job.wait()` methods. See [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs). ## retrieve() Read the current state of a webhook job. Use the `webhookId` from a live fetch or contact lookup, not a background Bulk job id. **Signature** ```typescript [TypeScript SDK] client.jobs.retrieve( id: string, params?: { wait?: number; }, options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `id` **required** | `string` | Saved `job.id` / `webhookId` from a live fetch or contact lookup. | | `params` | `{ wait?: number; }` | Query parameters. Omit to use the defaults. | | `params.wait` | `number` | Hold a long poll for 0 to 20 seconds. Defaults to 0. | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** `Result>` with `data.status`, `data.result` and `data.creditsConsumed`. You can inspect the snapshot even when the job has failed. Use `wait()` to raise an error on job failure. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const snapshot = await client.jobs.retrieve(jobId, { wait: 20 }); console.log(snapshot.data.status, snapshot.data.result); ``` ## wait() Resume waiting for a saved live-fetch or contact job without submitting another paid lookup. The generic `T` describes the result type. The SDK does not validate the profile shape. **Signature** ```typescript [TypeScript SDK] client.jobs.wait( id: string, options?: WaitOptions> ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `id` **required** | `string` | Saved `job.id` / `webhookId` from a live fetch or contact lookup. | | `options` | `WaitOptions>` | Optional overall `timeout`, `pollWait`, `onPoll`, `headers` and `signal`. See [Wait options](https://docs.reversecontact.com/sdk/javascript/timeouts). | **Returns** `Result>` once `data.status` is `succeeded`. A failed job raises `JobFailedError` instead. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. Waiting raises `JobFailedError` for an errored job and `JobTimeoutError` when the overall deadline expires. **Example** **Example** ```typescript [TypeScript SDK] const done = await client.jobs.wait(jobId, { timeout: 30 * 60 * 1000 }); console.log(done.data.result, done.data.creditsConsumed); ``` ## Related guides [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs), [client.bulk](https://docs.reversecontact.com/sdk/javascript/bulk), [Timeouts and cancellation](https://docs.reversecontact.com/sdk/javascript/timeouts). Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### Which job ids does client.jobs accept? `client.jobs` accepts webhook job ids from live fetches and contact lookups. Use `client.bulk` for background Bulk job ids. ### Does client.jobs.wait start another paid lookup? `client.jobs.wait()` polls an existing job for free. Waiting does not submit a replacement lookup. ### How does client.jobs report a failed job? `client.jobs.retrieve()` returns the errored snapshot for inspection. `client.jobs.wait()` raises `JobFailedError` when the job status is `errored`. --- # client.usage > Use client.usage in the Reverse Contact TypeScript SDK to read workspace credits, API key limits and quota snapshots with a free authenticated request. - Page: https://docs.reversecontact.com/sdk/javascript/usage - Last verified: 2026-11-04 Read workspace credits and rate limits without spending credits. | Method | Endpoint | Cost | | ------ | -------- | ---- | | `client.usage.retrieve()` | [Usage](https://docs.reversecontact.com/endpoints/usage) | Free. | Create `client` by following [Install and configure](https://docs.reversecontact.com/sdk/javascript/install). The `@reverse-contact/sdk` package exports `Body`, `Data`, `Query` and the other types used in the signatures. JSON responses contain `data` and optional `quotas` and `metadata`. They also include the original `response`, with its body already read. See [First call](https://docs.reversecontact.com/sdk/javascript/first-call) for response handling. HTTP errors use the [APIError subclasses and retry rules](https://docs.reversecontact.com/sdk/javascript/errors-and-retries): `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` and `InternalServerError`, according to the API response. Other statuses use `APIError`. ## retrieve() Read balances and limits from `quotas`. The `data` field is `null`. **Signature** ```typescript [TypeScript SDK] client.usage.retrieve( options?: RequestOptions ): Promise>> ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `options` | `RequestOptions` | Optional per-request `timeout`, `maxRetries`, `headers` and `signal`. See [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). | **Returns** `Result` with optional `quotas`, `metadata` and the HTTP `response`. Read `quotas.workspace.credits`, workspace limits and key limits. Quota snapshots and `quotas.key` can be `null`. **Errors** Calls can raise the HTTP `APIError` subclasses listed above. Network failures or cancellation raise `APIConnectionError`, and attempt timeouts raise `APITimeoutError`. `ReverseContactError` reports invalid SDK options or an invalid successful JSON response. **Example** **Example** ```typescript [TypeScript SDK] const result = await client.usage.retrieve(); console.log(result.quotas?.workspace.credits.left); console.log(result.quotas?.workspace.minuteRateLimit?.left); ``` ## Related guides [First call](https://docs.reversecontact.com/sdk/javascript/first-call), [Client options](https://docs.reversecontact.com/sdk/javascript/client-options). Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### Does reading SDK usage cost credits? `client.usage.retrieve()` calls the free Usage endpoint. ### Why is data null in a usage response? Usage returns `data: null`. Workspace balances and rate limits are in the optional `quotas` field. ### Can usage quota snapshots be null? Quota snapshot values and `quotas.key` can be `null`. Check for missing values before using a balance or limit as a number. --- # Async jobs > Wait for live profiles, Find Email, Verify Email and Find Phone with the TypeScript SDK, resume saved job ids, and parse webhook deliveries. - Page: https://docs.reversecontact.com/sdk/javascript/async-jobs - Last verified: 2026-11-04 Live fetches and contact lookups return a `Job` before the result is ready. ## Start and wait for a job `fetch.personLive`, `fetch.companyLive`, `contact.findEmail`, `contact.verifyEmail` and `contact.findPhone` start a job and return a `Job` right away. `job.id` is the job's `webhookId`. Call `job.wait()` to poll until the job ends. When the job succeeds, the method returns its final state (`data.status`, `data.result`, `data.creditsConsumed`). When the job fails, the method raises `JobFailedError`. Polling is free and does not count toward your rate limit. **Live fetch** ```typescript [TypeScript SDK] import ReverseContact, { JobFailedError, JobTimeoutError } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.fetch.personLive({ url: "https://social.com/in/janedoe" }); // Save the id first: after a restart, client.jobs.wait(id) resumes without paying for a new job. console.log("Job id:", job.id); try { const done = await job.wait({ timeout: 30 * 60 * 1000 }); console.log(done.data.result?.firstName, "credits:", done.data.creditsConsumed); } catch (error) { if (error instanceof JobFailedError) { console.log("Job failed:", error.errorCode, "credits:", error.creditsConsumed); } else if (error instanceof JobTimeoutError) { console.log("Still running, resume later with client.jobs.wait:", error.jobId); } else { throw error; } } ``` How `wait()` behaves: - Each poll asks the API to hold the answer for up to 20 seconds (`pollWait`, 0 to 20), so the result arrives as soon as the job ends. Two polls start at least `Retry-After` seconds apart, 2 seconds when the header is missing. - `timeout` is the overall deadline in milliseconds, 30 minutes by default. When it passes, `wait()` raises `JobTimeoutError` with `jobId` and `lastStatus`. The job may still finish: resume it with `client.jobs.wait(jobId)`. - A job that ends with `status: "errored"` raises `JobFailedError`, with `errorCode`, `creditsConsumed`, `result` and `notFound`. The `notFound` field is `true` for `data-not-found`, `person-not-found`, `company-not-found`, `email-not-found` and `phone-not-found`. - `onPoll` is called with each state the SDK reads, and `signal` stops the wait (see [Timeouts and cancellation](https://docs.reversecontact.com/sdk/javascript/timeouts)). - The SDK never starts a new job after a failure. Your code decides whether to start another job, which can be billed again. `client.jobs.retrieve(id, { wait: 20 })` reads one state of a job without waiting for the end. [Polling](https://docs.reversecontact.com/guides/polling) describes the job lifecycle and how long a result stays available. ### Webhook delivery instead of polling Pass `webhookUrl` when you create the job to receive the result at that HTTPS URL as `{ webhookId, data }`. Use `parseWebhookDelivery` to check the payload shape and read `webhookId`, `data` and `isError`. The `isError` field is `true` when `data.errorCode` is a non-empty string. The helper does not validate the profile itself. **Webhook delivery** ```typescript [TypeScript SDK] import ReverseContact, { parseWebhookDelivery } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.fetch.personLive({ url: "https://social.com/in/janedoe", webhookUrl: "https://your-app.com/webhooks/reversecontact" }); console.log("Store this id to match the delivery:", job.id); // The route behind webhookUrl, as a Fetch API handler (Bun, Deno, edge runtimes). async function handleWebhook(request: Request): Promise { const delivery = parseWebhookDelivery(await request.json()); if (delivery.isError) console.log(delivery.webhookId, "failed:", delivery.data); else console.log(delivery.webhookId, "result:", delivery.data); // Answer within 10 seconds, then process the data: deduplicate on webhookId. return new Response(null, { status: 200 }); } ``` A job created with `webhookUrl` can still be polled with `job.wait()`. See [Webhooks](https://docs.reversecontact.com/guides/webhooks) for delivery failures and retries. ## Read a job without waiting for completion `job.retrieve({ wait: 20 })` reads one snapshot of the same job. `job.status` holds the initial acknowledgement status. Read `snapshot.data.status` for the current state. To retrieve or wait for a saved job id, use the `retrieve()` and `wait()` methods in the [client.jobs reference](https://docs.reversecontact.com/sdk/javascript/jobs). **Verify an email** ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact(); const job = await client.contact.verifyEmail({ email: "jane@acme.com" }); const snapshot = await job.retrieve({ wait: 20 }); console.log(snapshot.data.status); const done = await job.wait(); console.log(done.data.result?.status); ``` Verify Email returns `email` and a `status` of `valid`, `risky`, `invalid` or `unknown`. Find Email and Find Phone return arrays of `{ value, type }`. See [client.contact](https://docs.reversecontact.com/sdk/javascript/contact) and [client.fetch](https://docs.reversecontact.com/sdk/javascript/fetch) for their parameters. `parseWebhookDelivery` raises `ReverseContactError` if the delivery envelope is invalid. List operations and Bulk jobs use separate wait methods, described in [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/javascript/lists-and-bulk). Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### How do I wait for a live profile or Find Email result? Call `job.wait()` on the `Job` returned by a live fetch or contact lookup. The SDK polls for free, for up to 30 minutes by default. ### How do I resume waiting after my process restarts? Save `job.id` before waiting, then call `client.jobs.wait(jobId)` after restarting. Resuming a job avoids creating another paid lookup. ### What happens when an asynchronous SDK job fails? `job.wait()` raises `JobFailedError` with the job error code, credits consumed and any result. The SDK does not start a replacement job. ### Can I poll a job that also uses a webhook? A job created with `webhookUrl` can still be polled with `job.wait()`. `parseWebhookDelivery` checks the callback envelope and identifies error deliveries. --- # Lists and Bulk enrichment > Create persistent Lists or enrich up to 25,000 people with the TypeScript SDK, follow import operations and Bulk jobs, and download CSV results. - Page: https://docs.reversecontact.com/sdk/javascript/lists-and-bulk - Last verified: 2026-11-04 Save people in a List for ongoing work, or run a Bulk job when you need a results file. ## Enrich people in bulk From version 0.2.0, `client.lists` manages your [Lists](https://docs.reversecontact.com/guides/lists) and `client.bulk` runs [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) jobs. Use `bulk.enrichPersons` to start a job of up to 25,000 people. Then call `bulk.wait` to wait until the job is `completed`, `cancelled`, `failed` or `paused`. Once the job is completed or cancelled, `bulk.results` returns the results CSV as text without a byte order mark. Prepend `"\uFEFF"` when you save the file so a spreadsheet opens accented names correctly. The SDK sends an `Idempotency-Key` with each new job and generates one if you don't supply it. The SDK reuses that key on its retries, so a retried request never creates a second job. **Bulk enrichment** ```typescript [TypeScript SDK] import { writeFile } from "node:fs/promises"; import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const created = await client.bulk.enrichPersons({ name: "CRM export, November", items: [ { linkedinUrl: "https://social.com/in/janedoe", externalId: "crm-001" }, { firstName: "John", lastName: "Smith", companyDomain: "acme.com", externalId: "crm-002" } ], families: ["profile", "contact_email"] }); const jobId = created.data.job.id; const job = await client.bulk.wait(jobId); if (job.data.links.results) { await writeFile("results.csv", "\uFEFF" + (await client.bulk.results(jobId))); } console.log(job.data.status, "credits:", job.data.credits.used); ``` ### Read the results as they come From version 0.3.0, you can use the results before the job settles. `bulk.iterateRows` yields them one at a time, in the order sent, as soon as each one is ready. While the next item runs, it waits out the `Retry-After` of the API. It ends with the job, or when the job is paused: read on later with `bulk.rows` and the last cursor. A cancelled job first finishes the items it had started, within the hour. `bulk.rows` reads one page (`cursor`, `limit` up to 100), with the job status in `extras.status`. Each item is a `JobResultLine`, a type the SDK exports. `bulk.results(jobId, { format: "ndjson" })` returns the results file as NDJSON, one `JobResultLine` per line. The [client.bulk reference](https://docs.reversecontact.com/sdk/javascript/bulk) documents each method. **Results as they come** ```typescript [TypeScript] for await (const item of client.bulk.iterateRows(jobId)) { console.log(item.row, item.input.externalId, item.status, item.creditsConsumed); } // Or the whole file once the job settles, one JSON object per line: const ndjson = await client.bulk.results(jobId, { format: "ndjson" }); const items = ndjson.split("\n").filter(Boolean).map((line) => JSON.parse(line)); ``` ## Keep members in a List A List keeps its members. Call `lists.importSignals` to start an import, then `lists.operations.wait` to wait until `clientAction` is `done`, `resume` or `reconcile`. The wait method returns the operation so your code can decide what to do next. If you don't supply them, `lists.create` generates an `Idempotency-Key` and `lists.importSignals` generates an `operationId`. Pass your own `operationId` to resume a paused import after a restart. **Lists** ```typescript [TypeScript SDK] const list = await client.lists.create({ name: "Prospects", entity: "persons" }); const listId = list.data.id; const imported = await client.lists.importSignals(listId, { signals: [{ email: "jane@acme.com", externalId: "crm-001" }], families: ["contact_email"], maxCredits: 100 }); const operation = await client.lists.operations.wait(listId, imported.data.operationId); console.log(operation.data.clientAction, operation.data.nextStep); for await (const member of client.lists.members.listAll(listId)) console.log(member); const csv = await client.lists.export(listId); ``` `lists.listAll`, `lists.members.listAll` and `bulk.listAll` read every page for you. They are free, so they need no `maxResults`. ## Choose the right wait method `client.bulk.wait(jobId)` returns when the job is `completed`, `cancelled`, `failed` or `paused`. Inspect the returned state before downloading results. `client.lists.operations.wait(listId, operationId)` returns when `clientAction` is `done`, `resume` or `reconcile`. Neither method raises `JobFailedError` just because the operation stops unsuccessfully. Both can raise `JobTimeoutError` when the wait deadline passes. List imports and Bulk jobs do not return the `Job` object used by live fetch and contact lookups. See [client.lists](https://docs.reversecontact.com/sdk/javascript/lists), [client.bulk](https://docs.reversecontact.com/sdk/javascript/bulk) and [Timeouts and cancellation](https://docs.reversecontact.com/sdk/javascript/timeouts) for the signatures and options. Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### When should I use a List instead of Bulk enrichment? A List keeps its members for ongoing work. Bulk enrichment processes up to 25,000 people and returns a results CSV. ### Does bulk.wait throw when a Bulk job fails? `client.bulk.wait()` returns the job when the status is `completed`, `cancelled`, `failed` or `paused`. Inspect `job.data.status` and `job.data.links.results` before downloading a CSV. ### How do I resume a paused List import after restarting? Pass your saved `operationId` to `client.lists.importSignals()` and follow the returned operation. Inspect `clientAction` and `nextStep` before deciding to resume. ### How do I preserve accented names in a Bulk results CSV? `client.bulk.results()` returns CSV text without the byte order mark. Prepend `"\uFEFF"` when saving the text for spreadsheet applications. --- # Search within a budget > Count matches for free, cap requested results with maxResults, and paginate people or company searches with the Reverse Contact TypeScript SDK. - Page: https://docs.reversecontact.com/sdk/javascript/search-budget - Last verified: 2026-11-04 Count matches before reading paid search results. ## Count, then search Search costs 1 credit per profile or company delivered. The two Agentic Search endpoints add 1 credit per page. Count matches for free first, then read the results with an iterator. Every search iterator requires `maxResults` to cap the results you pay for. The iterator requests at most that many results and lowers `perPage` on its last request to stay within the cap. `maxResults: 0` makes no request. **Count, then search** ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const count = await client.search.countPeople({ currentFunction: "Marketing", location: { countryCode: "FR" } }); console.log("Matches:", count.data.count); // free for await (const person of client.search.iterPeople( { currentFunction: "Marketing", location: { countryCode: "FR" }, perPage: 50 }, { maxResults: 200 } )) { console.log(person.firstName, person.lastName); } ``` The iterators are `iterPeople`, `iterPeopleByPrompt`, `iterPeopleLookalike`, `iterOrganizations` and `iterOrganizationsByPrompt`. Set `perPage` between 1 and 100, or use the default of 25 results per page. An iterator fetches one page at a time, only when you read further. If the API repeats a cursor, the iterator stops with an error. To work page by page, call `client.search.people()`. The returned page has `data`, `nextCursor`, `count`, `quotas`, `hasNextPage()` and `getNextPage()`. Use `getNextPage()` to send the same filters with the next cursor. The method returns `null` after the last page. Prompt searches also return `interpretation`, which contains the filters derived from the prompt. `maxResults` caps delivered results, not total credits: Agentic Search adds a credit for each requested page. `hasNextPage()` only checks the cursor. `getNextPage()` sends a new request with the original filters. See [client.search](https://docs.reversecontact.com/sdk/javascript/search) for every method and [Filter values as constants](https://docs.reversecontact.com/sdk/javascript/filter-constants) for accepted catalog values. ## Read a page explicitly A `Page` is a synchronous iterable: `for...of` reads only the items already loaded in that page. `page[Symbol.iterator]()` does not request another page. Call `getNextPage(options?)` to fetch more results. The optional argument accepts `RequestOptions` such as `timeout` and `signal`. **Read one page, then the next** ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact(); const page = await client.search.people({ firstName: "Jane", perPage: 10 }); for (const person of page) console.log(person.firstName); if (page.hasNextPage()) { const nextPage = await page.getNextPage({ timeout: 30_000 }); console.log(nextPage?.data); } ``` Each requested page is billed for the results delivered. Use a bounded iterator when you need `maxResults` to apply across pages. Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### How do I cap the number of search results billed by the SDK? Set the required `maxResults` option on a search iterator. The SDK reduces the last request size to stay within the result budget, and `maxResults: 0` sends no request. ### Does maxResults cap all credits used by Agentic Search? `maxResults` caps returned profiles or companies. Agentic Search also costs 1 credit per requested page, in addition to 1 credit per result. ### Does the SDK fetch search pages before I need them? Search iterators fetch one page at a time as results are consumed. `getNextPage()` explicitly requests the next page and returns `null` after the last page. --- # Filter values as constants > Use typed industry, country, seniority and other filter constants in the Reverse Contact TypeScript SDK while accepting newly published values. - Page: https://docs.reversecontact.com/sdk/javascript/filter-constants - Last verified: 2026-11-04 Use exported constants to autocomplete search filter values in your editor. ## Use filter constants From version 0.1.6, the SDK exports constants for fixed filter lists: `Industry`, `CountryCode`, `JobFunction`, `JobSubFunction`, `Seniority`, `ContractType`, `Area` and `EmailType`. Each constant holds the filter value itself. For example, `Industry.SOFTWARE_DEVELOPMENT` is `"Software Development"`. Your editor autocompletes the values for each filter. A constant's name is its value in capitals, with `_` for spaces and punctuation and `AND` for `&`. Examples include `Seniority.OWNER_FOUNDER`, `Area.MC_DACH` and `ContractType.FULL_TIME`. **Filter with constants** ```typescript [TypeScript SDK] import ReverseContact, { Area, Industry, Seniority } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const count = await client.search.countPeople({ currentCompanyIndustry: Industry.SOFTWARE_DEVELOPMENT, currentSeniority: [Seniority.CXO, Seniority.OWNER_FOUNDER], location: { area: Area.MC_DACH } }); console.log("Matches:", count.data.count); ``` Filters also accept strings, so you can use newly added values without updating the SDK. The API returns HTTP `422` for an unknown value and charges 0 credits. See [client.search](https://docs.reversecontact.com/sdk/javascript/search) for the search and count methods, [Search within a budget](https://docs.reversecontact.com/sdk/javascript/search-budget) for pagination, and the [industry reference](https://docs.reversecontact.com/reference/industries) for industry values. Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### Which filter constants does the TypeScript SDK export? The SDK exports `Industry`, `CountryCode`, `JobFunction`, `JobSubFunction`, `Seniority`, `ContractType`, `Area` and `EmailType` for fixed filter lists. ### Can I use a new filter value before updating the SDK? Filter types also accept strings, so newly published API values work without updating the SDK. The API validates those values. ### What happens if a filter value is invalid? The API returns HTTP 422 for an unknown filter value and charges 0 credits. Use a supported constant or a value from the relevant API reference. --- # Errors and retries > Handle typed errors and automatic retries in the Reverse Contact TypeScript SDK, and reuse idempotency keys for Find Email, Verify Email and Find Phone. - Page: https://docs.reversecontact.com/sdk/javascript/errors-and-retries - Last verified: 2026-11-04 Catch SDK errors by class and preserve the request id when reporting an API failure. ## Error classes and retry policy Every SDK error extends `ReverseContactError`. Errors returned by the API extend `APIError` and include `status`, `code`, `message`, `details`, `requestId`, `headers` and `body`. | HTTP status | Error class | | ----------- | ----------- | | `400` | `BadRequestError` | | `401` | `AuthenticationError` | | `402` | `InsufficientCreditsError` | | `403` | `PermissionDeniedError` | | `404` | `NotFoundError` (a lookup with no match returns `null` instead) | | `409` | `ConflictError` | | `410` | `GoneError` | | `422` | `UnprocessableEntityError` | | `429` | `RateLimitError`, or `DailyLimitError` for a daily limit | | `451` | `UnavailableForLegalReasonsError` | | `5xx` | `InternalServerError` | | Any status with `KEY_CREDIT_LIMIT_REACHED` | `KeyCreditLimitError` | A network failure raises `APIConnectionError`. An attempt that times out raises `APITimeoutError`, a subclass of `APIConnectionError`. The SDK retries a request up to 4 times after the first attempt, 5 attempts in total: - For a `429` from the per-minute rate limit, the SDK uses `error.details.retryAfter` as the delay, or the `Retry-After` header if that value is missing. Otherwise, the SDK backs off. - For a `500`, `502`, `503` or `504`, the SDK uses exponential backoff: 2, 4, 8, then 15 seconds, plus up to 250 milliseconds of jitter. If the response includes `Retry-After`, the SDK uses that delay instead. - If the requested wait exceeds 60 seconds, the SDK raises the error at once. - A network failure or a timed-out attempt is retried on `GET` requests. A `POST` is retried only when the connection was never established, so a request the API may already have billed is never sent twice. Find Email, Verify Email and Find Phone are the exception, protected by their idempotency key (see below). The SDK never retries `DailyLimitError` (daily limits, `DAILY_LIMIT_EXCEEDED` and `DAILY_EXTRACTION_LIMIT_EXCEEDED`), `KeyCreditLimitError` (an API key credit limit) or `InsufficientCreditsError`. Other `4xx` errors are not retried either, except per-minute `429` rate limits and `409 IDEMPOTENCY_KEY_IN_USE` for Find Email, Verify Email and Find Phone. For a non-retryable error, change the request, key or account, or wait until the next day. Set `maxRetries: 0` to turn retries off. **Handle errors** ```typescript [TypeScript SDK] import ReverseContact, { APIError, DailyLimitError, KeyCreditLimitError } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); try { await client.enrich.company({ domain: "acme.com" }); } catch (error) { if (error instanceof DailyLimitError || error instanceof KeyCreditLimitError) { // Not retried: pause the work until the limit resets. console.error("Limit reached:", error.code, "retry after", error.retryAfter, "seconds"); } else if (error instanceof APIError) { console.error(error.status, error.code, error.message, "request id:", error.requestId); } else { throw error; } } ``` [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries) lists every error code of the API. ## Safe retries on Find Email and Find Phone Sending the same Find Email, Verify Email or Find Phone request twice would start two jobs, and both can be billed. To prevent duplicate jobs on automatic retries, `contact.findEmail`, `contact.verifyEmail` and `contact.findPhone` send an `Idempotency-Key` header with a new UUID on each call. The SDK reuses that key on every automatic retry of the call. For 24 hours, repeating the same key and body returns the first response without starting or billing another lookup. To retry safely after your process restarts, choose the key yourself, store it with the request, and pass it as `idempotencyKey`. `job.replayed` is `true` when the API returned the first response again. **Idempotency key** ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.contact.findEmail( { fullName: "Jane Doe", companyDomain: "acme.com", emailType: "any" }, { idempotencyKey: "crm-contact-4821-email" } ); console.log(job.id, job.replayed ? "first response returned again" : "new lookup"); const done = await job.wait(); for (const email of done.data.result ?? []) console.log(email.value, email.type); ``` Use 1 to 255 printable ASCII characters without spaces, and a new key for each new lookup. If the key is empty, longer than 255 characters, or contains a space or non-ASCII character, the SDK raises `ReverseContactError` before sending a request. The SDK retries `409 IDEMPOTENCY_KEY_IN_USE` after the delay in `Retry-After`. A `422 IDEMPOTENCY_KEY_REUSED` (same key, different body) and a `400 INVALID_IDEMPOTENCY_KEY` are not retried. After 24 hours, the same key can start and bill a new lookup. To read a job you already started, call `client.jobs.wait(id)`. List creation and Bulk submission also generate keys. List imports generate an `operationId`. See [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/javascript/lists-and-bulk). ## Local validation and job errors `ReverseContactError` also reports invalid SDK options, missing search budgets, repeated pagination cursors and malformed webhook envelopes. `JobTimeoutError` reports an expired wait deadline. `JobFailedError` reports a failed live-fetch or contact job from `job.wait()` or `client.jobs.wait()`. See [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs) and [Timeouts and cancellation](https://docs.reversecontact.com/sdk/javascript/timeouts). Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### Which requests does the TypeScript SDK retry automatically? The SDK retries per-minute rate limits and HTTP 500, 502, 503 and 504 up to 4 times after the initial attempt. Contact requests also retry HTTP 409 `IDEMPOTENCY_KEY_IN_USE`. ### Does the SDK retry daily or API key credit limits? The SDK does not retry `DailyLimitError`, `KeyCreditLimitError` or `InsufficientCreditsError`. Change the request or account limits, or wait for the relevant reset. ### How do I retry Find Email safely after a restart? Persist the request body and an `idempotencyKey`, then reuse both within 24 hours. For a job id already saved, call `client.jobs.wait()` to read the existing job. ### How do I disable automatic SDK retries? Set `maxRetries: 0` on the client or on an individual request. Wait helpers use their own polling options, described in the Async jobs guide. --- # Timeouts and cancellation > Set per-attempt HTTP timeouts, bound asynchronous job waits, and cancel local requests with AbortSignal in the Reverse Contact TypeScript SDK. - Page: https://docs.reversecontact.com/sdk/javascript/timeouts - Last verified: 2026-11-04 Use request timeouts for individual HTTP attempts and a wait deadline for the full polling loop. ## Request timeouts and cancellation Each HTTP attempt times out after 90 seconds by default, since some enrichments take more than 60 seconds. Pass request options as the last argument of any API method: `timeout` (milliseconds per attempt), `maxRetries`, `headers` and `signal`. Use an `AbortSignal` to cancel a request, the wait between retries, or `job.wait()`. A cancelled call raises `APIConnectionError`. Cancellation stops your code from waiting, without cancelling the job on the server. **Timeout and cancellation** ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const usage = await client.usage.retrieve({ timeout: 10_000, maxRetries: 1, signal: AbortSignal.timeout(30_000) }); console.log("Credits left:", usage.quotas?.workspace.credits.left); ``` ## Wait options `job.wait()`, `client.jobs.wait()`, `client.bulk.wait()` and `client.lists.operations.wait()` accept `WaitOptions`: | Option | Type | Description | | ------ | ---- | ----------- | | `timeout` | `number` | Overall deadline in milliseconds, 1,800,000 by default (30 minutes). Must be positive and finite. | | `pollWait` | `number` | How long each poll waits, in seconds. An integer from 0 to 20 that defaults to 20. | | `onPoll` | `(snapshot: Result) => void \| Promise` | Called with each response envelope. Read progress in `snapshot.data`. | | `headers` | `RequestInit["headers"]` | Headers for polling requests. | | `signal` | `AbortSignal` | Cancels the local wait. | `WaitOptions` has no `maxRetries`. Each poll uses 4 retries within the overall deadline. Polling reads allow at least `wait + 15` seconds for an HTTP attempt, but `wait()` remains bound by the overall deadline. When the wait deadline passes, the SDK raises `JobTimeoutError` with `jobId` and `lastStatus`. The server job may continue. See [Async jobs](https://docs.reversecontact.com/sdk/javascript/async-jobs) to resume waiting. To request a server-side stop for a Bulk job or List operation, use [client.bulk.cancel()](https://docs.reversecontact.com/sdk/javascript/bulk) or [client.lists.operations.cancel()](https://docs.reversecontact.com/sdk/javascript/lists). Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### What is the default HTTP timeout in the TypeScript SDK? Each HTTP attempt times out after 90 seconds by default. Set the request or client `timeout` in milliseconds to change that deadline. ### Does AbortSignal cancel a job on the server? `AbortSignal` stops the SDK request or local polling loop and raises `APIConnectionError`. The server job can continue. Bulk and List operations have separate cancel methods. ### How do I change the deadline for job.wait? Pass `timeout` in milliseconds to `job.wait()`. The wait deadline defaults to 30 minutes and raises `JobTimeoutError` when exceeded. --- # Client options > Configure the Reverse Contact TypeScript SDK client, override request options, inspect safe debug logs, and create a client with merged settings. - Page: https://docs.reversecontact.com/sdk/javascript/client-options - Last verified: 2026-11-04 Pass `ClientOptions` to `new ReverseContact()` or `client.withOptions()` to configure your integration. ## Constructor options | Option | Default | Meaning | | ------ | ------- | ------- | | `apiKey` | `RC_API_KEY` | Your API key | | `baseUrl` | `RC_BASE_URL`, then `https://api.reversecontact.com` | API origin | | `timeout` | `90_000` | Milliseconds per HTTP attempt | | `maxRetries` | `4` | Retries after the first attempt | | `defaultHeaders` | none | Headers added to every request | | `fetch` | the global `fetch` | A custom Fetch implementation, for a proxy or for tests | | `appInfo` | none | Your integration's `{ name, version?, url? }`. Name and version are added to the `User-Agent` | | `logLevel` | `off`, or `debug` when `RC_LOG=debug` | Logs each attempt to `console.debug` | | `dangerouslyAllowBrowser` | `false` | Allows the client in a browser, exposing your key | Debug logs show the method, path, status, attempt, retry delay and request id. They never show the API key, headers, query or body. Use `client.withOptions()` to create a new client with merged options. The original client stays unchanged: **Client options** ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY, timeout: 120_000, maxRetries: 2, appInfo: { name: "my-crm-sync", version: "1.4.0" } }); const debugClient = client.withOptions({ logLevel: "debug" }); await debugClient.usage.retrieve(); ``` The package also exports API types: `Person`, `Company`, `EnrichPersonParams`, `SearchPeopleParams`, `SearchOrganizationsParams`, `JobStatus` and `Quotas`. Use `Body<'operationId'>` and `Data<'operationId'>` for any operation. ## Per-request options `RequestOptions` accepts `timeout`, `maxRetries`, `headers` and `signal`. `IdempotentRequestOptions` adds `idempotencyKey` for contact lookups, List creation and Bulk submission. Put request options last, after path ids and any body or query parameters. For methods with optional query parameters, pass `{}` before the options when no filter is needed. `timeout` must be positive and finite. `maxRetries` must be a nonnegative integer. Invalid values raise `ReverseContactError`. See [Timeouts and cancellation](https://docs.reversecontact.com/sdk/javascript/timeouts) for wait options and [Errors and retries](https://docs.reversecontact.com/sdk/javascript/errors-and-retries) for retry rules. The `baseUrl` can include a gateway prefix. SDK routes already include `/v2`. The SDK accepts `appInfo.url`, but adds only the name and version to the `User-Agent`. `VERSION` exports the package version. After merging client and request headers, the SDK removes the reserved `X-Request-Origin` header. Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### How do I override SDK options for one request? Pass `RequestOptions` as the last argument, after any ids and body or query parameters. The options include `timeout`, `maxRetries`, `headers` and `signal`. ### Does client.withOptions change the original client? `client.withOptions()` creates a new client with merged options and headers. The original client remains unchanged. ### Does SDK debug logging include API keys or request bodies? Debug logs contain method, path, status, attempt, retry delay and request id. Debug logs exclude API keys, headers, query parameters and request bodies. --- # Without the SDK > Call the Reverse Contact API from any HTTP client, authenticate with a Bearer token, and use the OpenAPI specification to generate a typed client. - Page: https://docs.reversecontact.com/sdk/javascript/without-the-sdk - Last verified: 2026-11-04 Call Reverse Contact with the HTTP client that fits your application. ## Use an HTTP client The API accepts JSON over HTTPS, so you can use any HTTP client. Send your key in an `Authorization: Bearer` header, as shown in the cURL examples on each endpoint page. To generate your own typed client, use the [published OpenAPI specification](https://docs.reversecontact.com/openapi.json). Follow [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries) and [Polling](https://docs.reversecontact.com/guides/polling) to add the behavior the SDK handles for you. ## Find request examples Start with [Your first API call](https://docs.reversecontact.com/guides/first-api-call) or choose an endpoint in the [API reference](https://docs.reversecontact.com/api-reference). Each endpoint documents its request, response and credit cost. The [Python SDK overview](https://docs.reversecontact.com/sdk/python) covers the other official SDK. For the TypeScript client, see [Install and configure](https://docs.reversecontact.com/sdk/javascript/install). Return to the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). ## FAQ ### Can I use the Reverse Contact API without an SDK? The Reverse Contact API accepts JSON over HTTPS from any HTTP client. Authenticate with an `Authorization: Bearer` header. ### Where is the Reverse Contact OpenAPI specification? Use the OpenAPI specification at `https://docs.reversecontact.com/openapi.json` to generate a typed client. ### What must a custom API client handle? A custom client handles authentication, HTTP errors and retry rules, and polls asynchronous jobs when needed. The Error handling & retries and Polling guides describe those behaviors. --- # Python SDK > Use the official Reverse Contact Python SDK to enrich profiles, search within a budget, manage Lists and Bulk jobs, and wait for async results. - Page: https://docs.reversecontact.com/sdk/python - Last verified: 2026-11-04 ## Introduction `reversecontact` is the official Python client for the Reverse Contact API. You can call every API endpoint with typed models. The SDK handles safe retries, waits for asynchronous jobs, and pages through search results within a budget you set. Choose the synchronous client, `ReverseContact`, or the asynchronous client, `AsyncReverseContact`. Both have the same methods. - Package: [reversecontact on PyPI](https://pypi.org/project/reversecontact/) - Source code and issues: [github.com/visum-run/reversecontact-python](https://github.com/visum-run/reversecontact-python) - License: Apache-2.0 The SDK needs Python 3.11 or later. These pages document `reversecontact` 0.3.0. ## Install **Install** ```bash [Install] pip install reversecontact ``` ## API key Set `RC_API_KEY` or pass `api_key` to the client. Keep the key server-side. Follow [Install and configure](https://docs.reversecontact.com/sdk/python/install) to set up your client. ## First call This lookup costs 2 credits when a person is found and nothing when no profile matches. With `RC_API_KEY` set: **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.enrich.person(email="jane.doe@acme.com") if result is None: print("No confident match: nothing was charged") elif result.data and result.data.person: print(result.data.person.first_name, result.data.person.last_name) ``` [First call](https://docs.reversecontact.com/sdk/python/first-call) explains the response fields, credit usage and request ids. ## Endpoints and methods | Endpoint | Method | | -------- | ------ | | [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person) | `client.enrich.person()` | | [Enrich company](https://docs.reversecontact.com/endpoints/enrich-company) | `client.enrich.company()` | | [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) | `client.fetch.person()` | | [Person profile live](https://docs.reversecontact.com/endpoints/fetch-profile-live) | `client.fetch.person_live()` | | [Person profile status](https://docs.reversecontact.com/endpoints/check-person) | `client.fetch.check_person()` | | [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) | `client.fetch.company()` | | [Company profile live](https://docs.reversecontact.com/endpoints/fetch-company-live) | `client.fetch.company_live()` | | [Company profile status](https://docs.reversecontact.com/endpoints/check-company) | `client.fetch.check_company()` | | [People search](https://docs.reversecontact.com/endpoints/search-people) | `client.search.people()`, `client.search.iter_people()` | | [Agentic Search](https://docs.reversecontact.com/endpoints/search-people-prompt) | `client.search.people_by_prompt()`, `client.search.iter_people_by_prompt()` | | [People lookalike](https://docs.reversecontact.com/endpoints/search-people-lookalike) | `client.search.people_lookalike()`, `client.search.iter_people_lookalike()` | | [People search count](https://docs.reversecontact.com/endpoints/search-people-count) | `client.search.count_people()` | | [People auto-suggestion](https://docs.reversecontact.com/endpoints/search-people-auto-suggestion) | `client.search.people_auto_suggestion()` | | [People search available fields](https://docs.reversecontact.com/endpoints/search-people-available-fields) | `client.search.people_fields()` | | [Company search](https://docs.reversecontact.com/endpoints/search-companies) | `client.search.organizations()`, `client.search.iter_organizations()` | | [Agentic Company Search](https://docs.reversecontact.com/endpoints/search-companies-prompt) | `client.search.organizations_by_prompt()`, `client.search.iter_organizations_by_prompt()` | | [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count) | `client.search.count_organizations()` | | [Company search available fields](https://docs.reversecontact.com/endpoints/search-companies-available-fields) | `client.search.organizations_fields()` | | [Find Email](https://docs.reversecontact.com/endpoints/contact-email) | `client.contact.find_email()` | | [Verify Email](https://docs.reversecontact.com/endpoints/contact-email-verify) | `client.contact.verify_email()` | | [Find Phone](https://docs.reversecontact.com/endpoints/contact-phone) | `client.contact.find_phone()` | | [All Lists](https://docs.reversecontact.com/endpoints/lists) | `client.lists.list()`, `client.lists.list_all()` | | [Create a List](https://docs.reversecontact.com/endpoints/list-create) | `client.lists.create()` | | [Get a List](https://docs.reversecontact.com/endpoints/list) | `client.lists.retrieve()` | | [Delete a List](https://docs.reversecontact.com/endpoints/list-delete) | `client.lists.delete()` | | [List members](https://docs.reversecontact.com/endpoints/list-members) | `client.lists.members.list()`, `client.lists.members.list_all()` | | [Add members](https://docs.reversecontact.com/endpoints/list-members-add) | `client.lists.members.add()` | | [Remove members](https://docs.reversecontact.com/endpoints/list-members-remove) | `client.lists.members.remove()` | | [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals) | `client.lists.import_signals()` | | [Read a List operation](https://docs.reversecontact.com/endpoints/list-operation) | `client.lists.operations.retrieve()`, `client.lists.operations.wait()` | | [Cancel a List operation](https://docs.reversecontact.com/endpoints/list-operation-cancel) | `client.lists.operations.cancel()` | | [Export a List](https://docs.reversecontact.com/endpoints/list-export) | `client.lists.export()` | | [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) | `client.bulk.enrich_persons()` | | [Job history](https://docs.reversecontact.com/endpoints/jobs) | `client.bulk.list()`, `client.bulk.list_all()` | | [Get a job](https://docs.reversecontact.com/endpoints/job) | `client.bulk.retrieve()`, `client.bulk.wait()` | | [Download job results](https://docs.reversecontact.com/endpoints/job-results) | `client.bulk.results()` | | [Read job results by page](https://docs.reversecontact.com/endpoints/job-rows) | `client.bulk.rows()`, `client.bulk.iter_rows()` | | [Cancel a job](https://docs.reversecontact.com/endpoints/job-cancel) | `client.bulk.cancel()` | | [Polling](https://docs.reversecontact.com/guides/polling) a job | `client.jobs.retrieve()`, `client.jobs.wait()` | | [Usage](https://docs.reversecontact.com/endpoints/usage) | `client.usage.retrieve()` | Each endpoint page shows its call in the `Python SDK` tab of its code examples. For now, the List and Bulk enrichment examples are in [Lists and Bulk enrichment](#lists-and-bulk-enrichment) instead. ## Where to go next - [Install and configure](https://docs.reversecontact.com/sdk/python/install) - [First call](https://docs.reversecontact.com/sdk/python/first-call) - [client.enrich](https://docs.reversecontact.com/sdk/python/enrich) - [client.fetch](https://docs.reversecontact.com/sdk/python/fetch) - [client.search](https://docs.reversecontact.com/sdk/python/search) - [client.contact](https://docs.reversecontact.com/sdk/python/contact) - [client.lists](https://docs.reversecontact.com/sdk/python/lists) - [client.bulk](https://docs.reversecontact.com/sdk/python/bulk) - [client.jobs](https://docs.reversecontact.com/sdk/python/jobs) - [client.usage](https://docs.reversecontact.com/sdk/python/usage) - [Async client](https://docs.reversecontact.com/sdk/python/async-client) - [Async jobs](https://docs.reversecontact.com/sdk/python/async-jobs) - [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/python/lists-and-bulk) - [Search within a budget](https://docs.reversecontact.com/sdk/python/search-budget) - [Filter values as constants](https://docs.reversecontact.com/sdk/python/filter-constants) - [Errors and retries](https://docs.reversecontact.com/sdk/python/errors-and-retries) - [Timeouts and cancellation](https://docs.reversecontact.com/sdk/python/timeouts) - [Client options](https://docs.reversecontact.com/sdk/python/client-options) - [Without the SDK](https://docs.reversecontact.com/sdk/python/without-the-sdk) ## Async client Use `AsyncReverseContact` with `await` and `async for`, as shown in [Async client](https://docs.reversecontact.com/sdk/python/async-client). ## Lookups that find nothing Enrich and cached Fetch return `None` when no profile matches, at no credit cost. See how to handle [lookups that find nothing](https://docs.reversecontact.com/sdk/python/first-call#lookups-that-find-nothing). ## Async jobs: live fetch, Find Email and Find Phone Save `job.id`, then call `job.wait()` to poll for free. The default deadline is 30 minutes. Failed jobs raise `JobFailedError`. See the examples in [Async jobs: live fetch, Find Email and Find Phone](https://docs.reversecontact.com/sdk/python/async-jobs). ### Webhook delivery instead of polling Pass `webhook_url` and parse callbacks with `parse_webhook_delivery`, as shown in [Webhook delivery](https://docs.reversecontact.com/sdk/python/async-jobs#webhook-delivery-instead-of-polling). ## Lists and Bulk enrichment [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/python/lists-and-bulk) shows how to manage persistent Lists, run Bulk jobs, and read their results as they come or export them. ## Search within a budget Count for free, then cap paid results with the search iterator's required `max_results` argument. Follow the example in [Search within a budget](https://docs.reversecontact.com/sdk/python/search-budget). ## Filter values as constants Use [string enums for fixed search values](https://docs.reversecontact.com/sdk/python/filter-constants) or pass plain strings. ## Errors and retries The SDK retries per-minute rate limits and HTTP 500, 502, 503 and 504 up to four times. Daily limits, key credit limits, insufficient credits and other 4xx responses are not retried, except contact `409 IDEMPOTENCY_KEY_IN_USE`. Read [Errors and retries](https://docs.reversecontact.com/sdk/python/errors-and-retries). ## Safe retries on Find Email and Find Phone Contact methods generate idempotency keys and reuse them on retries. See how this makes [retries safe on Find Email and Find Phone](https://docs.reversecontact.com/sdk/python/errors-and-retries#safe-retries-on-find-email-and-find-phone). ## Timeouts and cancellation [Timeouts and cancellation](https://docs.reversecontact.com/sdk/python/timeouts) covers per-request timeouts, overall polling deadlines and cancelling local asyncio waits. ## Client options Configure connection pools, defaults and logging with [Client options](https://docs.reversecontact.com/sdk/python/client-options). ## Without the SDK To use an HTTPS client and the published OpenAPI specification, see [Without the SDK](https://docs.reversecontact.com/sdk/python/without-the-sdk). ## FAQ ### Is there an official Reverse Contact SDK? Yes. The official Python SDK is `reversecontact` on PyPI and requires Python 3.11 or later. Install the package with `pip install reversecontact`. Choose the synchronous client, `ReverseContact`, or the asynchronous client, `AsyncReverseContact`. For TypeScript and JavaScript, the official SDK is `@reverse-contact/sdk` on npm. ### Does the reversecontact Python SDK retry failed requests? Yes. The `reversecontact` SDK retries per-minute rate limits and `500`, `502`, `503` and `504` errors up to 4 times, after the delay the API asks for or with exponential backoff. It never retries daily limits, API key credit limits, missing credits or any other `4xx` error except `409 IDEMPOTENCY_KEY_IN_USE`. ### How do I cap the credits a Reverse Contact search spends in Python? Call a free count method first, such as `client.search.count_people()`. Then read results with a search iterator such as `client.search.iter_people()`. The required `max_results` argument caps the number of profiles requested and billed. ### How do I wait for a live fetch or Find Email result in Python? Call `job.wait()` on the job that `client.fetch.person_live()` or `client.contact.find_email()` returns. The method polls for free until the job ends, up to 30 minutes by default. A failed job raises `JobFailedError`. Save `job.id` first so you can resume with `client.jobs.wait()` after a restart. ### What does the reversecontact SDK return when a lookup finds nothing? `client.enrich.person()`, `client.enrich.company()`, `client.fetch.person()` and `client.fetch.company()` return `None` when nothing matches. That answer costs no credits, so there is nothing to retry. --- # Install and configure > Install the Reverse Contact Python SDK, configure your API key, and create sync or async clients with Python 3.11 or later and typed responses. - Page: https://docs.reversecontact.com/sdk/python/install - Last verified: 2026-11-04 `reversecontact` is the official Python client for the Reverse Contact API, distributed under the Apache-2.0 license. These pages describe the published 0.3.0 package. ## Runtime requirements The SDK needs Python 3.11 or later, with `httpx>=0.27,<1` and `pydantic>=2.7,<3`. The package includes type hints for mypy and your editor. - Package: [reversecontact on PyPI](https://pypi.org/project/reversecontact/) - Source code and issues: [reversecontact-python on GitHub](https://github.com/visum-run/reversecontact-python) ## Install **Install** ```bash [pip] pip install reversecontact ``` ```bash [uv] uv add reversecontact ``` ```bash [Poetry] poetry add reversecontact ``` ## API key Pass your API key to the client as `api_key`, or set the `RC_API_KEY` environment variable. When you omit `api_key`, the client reads `RC_API_KEY`. The examples in these guides use the environment variable. You can use any server-side source, such as a secret manager. If you serve several workspaces, create one client per key. Keep the key on your server, out of your code and anything a browser loads. See [Authentication](https://docs.reversecontact.com/authentication) to create a key. **Terminal** ```bash [Terminal] export RC_API_KEY="your-api-key" ``` ## Create and close a client Use a context manager to close the SDK's HTTP connection pool when your work ends. With `RC_API_KEY` set, create the client without passing a key: **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: print(client.usage.retrieve().quotas) ``` You can also pass the key explicitly with `ReverseContact(api_key=your_server_side_key)`, then call `client.close()` when finished. A missing or empty key raises `ReverseContactError` before the SDK sends an HTTP request. The SDK sends the key in an `Authorization: Bearer` header. For asyncio applications, use `async with AsyncReverseContact()` or `await client.close()`. See the example in [Async client](https://docs.reversecontact.com/sdk/python/async-client). Close any custom `http_client` yourself. [Client options](https://docs.reversecontact.com/sdk/python/client-options) covers base URLs, connection pools, logging and per-client defaults. Once your client is configured, make your [first call](https://docs.reversecontact.com/sdk/python/first-call). Return to the [Python SDK overview](https://docs.reversecontact.com/sdk/python). ## FAQ ### Which Python versions does reversecontact support? The `reversecontact` package requires Python 3.11 or later, httpx 0.27 or later below 1, and Pydantic 2.7 or later below 3. ### How do I set the API key for the Python SDK? Set the `RC_API_KEY` environment variable or pass `api_key` to `ReverseContact` or `AsyncReverseContact`. Keep the key on your server. ### Do I need to close the Python SDK client? Use `with ReverseContact()` or `async with AsyncReverseContact()` to close the SDK-owned connection pool. Close any custom `http_client` yourself. --- # First call > Make your first Reverse Contact API call in Python, read typed data and credit usage, and handle a lookup that returns no matching profile. - Page: https://docs.reversecontact.com/sdk/python/first-call - Last verified: 2026-11-04 Follow [Install and configure](https://docs.reversecontact.com/sdk/python/install) to create an API key. Then enrich a person and inspect the response. ## Enrich a person This call enriches a person from a work email. It costs **2 credits** when a person is found, and nothing when no profile matches. **Enrich a person** ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.enrich.person(email="jane.doe@acme.com") if result is None: print("No confident match: nothing was charged") elif result.data is not None and result.data.person is not None: person = result.data.person print(person.first_name, person.last_name, person.current_position_title) print("Credits used:", result.quotas.credits_consumed if result.quotas else None) print("Request id:", result.metadata.request_id if result.metadata else None) ``` A JSON API call returns a `Result`, `Page` or `Job` with these four attributes. CSV downloads return text, and iterators yield items instead: - `data`: the `data` field of the API response, as a Pydantic model. Enrich nests the profile in `data.person` or `data.company`. A cached fetch returns the profile directly in `data`, while Usage returns `data` as `None`. For a search page, `data` holds the results. For a job, it holds the acknowledgement. - `quotas`: your credits and rate limits after this call, with `credits_consumed` for the call itself. - `metadata`: `request_id` and `execution_time_ms`. Quote the `request_id` when you contact support. - `http_response`: the original `httpx.Response`, headers included. Arguments and model attributes use snake_case (`full_name`, `first_name`). You can also pass arguments with the API's camelCase names. `model.to_dict()` returns the API shape with its camelCase names. You can read fields the API adds later through `model.model_extra`. Check fields before use, since any field can be `None`. ## Lookups that find nothing `enrich.person`, `enrich.company`, `fetch.person` and `fetch.company` return `None` when the API answers `404` because nothing matched. A no-match response costs no credits and needs no retry. Check for `None` and move on. Other `404` responses, such as an unknown job id or a wrong base URL, raise `NotFoundError`. See [client.enrich](https://docs.reversecontact.com/sdk/python/enrich) for lookup parameters and [client.fetch](https://docs.reversecontact.com/sdk/python/fetch) to retrieve profiles by URL or id. [Errors and retries](https://docs.reversecontact.com/sdk/python/errors-and-retries) explains how to handle failed requests. Return to the [Python SDK overview](https://docs.reversecontact.com/sdk/python). ## FAQ ### How do I enrich a person from an email in Python? Call `client.enrich.person(email="jane.doe@acme.com")`. A match is in `result.data.person` and costs 2 credits. If no profile matches, the call returns `None` and costs no credits. ### Does the Python SDK use snake_case or camelCase? SDK arguments and typed model attributes use snake_case. API camelCase arguments also work, and `model.to_dict()` returns camelCase API fields. ### Where can I find the request id and credits consumed? Check that `metadata` and `quotas` are present, then read `result.metadata.request_id` and `result.quotas.credits_consumed`. ### What does a Python lookup return when no profile matches? `enrich.person`, `enrich.company`, `fetch.person` and `fetch.company` return `None` for no-match responses. Other 404 responses raise `NotFoundError`. --- # client.enrich > Look up people and companies with client.enrich in the Python SDK, including every method, input, typed return value, error and credit cost. - Page: https://docs.reversecontact.com/sdk/python/enrich - Last verified: 2026-11-04 Find a person from identifying details or a company from its domain. | Method | Endpoint | Cost | | ------ | -------- | ---- | | [`person()`](#person) | [POST /v2/enrich/persons](https://docs.reversecontact.com/endpoints/enrich-person) | 2 credits when a person is found, full profile included. A 404 is free. | | [`company()`](#company) | [POST /v2/enrich/companies](https://docs.reversecontact.com/endpoints/enrich-company) | 2 credits when a company is found, full profile included. A 404 is free. | ## Calling conventions Examples assume `RC_API_KEY` is set. Replace example ids with ids returned by your own requests. Signatures show the published 0.3.0 synchronous methods. Use the same arguments with the [Python async client](https://docs.reversecontact.com/sdk/python/async-client). `models` refers to `reversecontact.types`. `NOT_GIVEN` and `NotGiven` are the SDK's internal markers for omitted arguments. Leave these arguments out of your calls. For API body parameters, passing `None` sends an explicit null. Transport options use `None` to inherit their defaults. `Timeout` means a number of seconds or `httpx.Timeout`. Supply parameters marked **Required** to Python or to the API, even when the generated signature permits omission. JSON response objects expose `data`, `quotas`, `metadata` and `http_response`. Check optional fields for `None`. See [First call](https://docs.reversecontact.com/sdk/python/first-call) for the response envelope. CSV methods return text and iterators yield items. HTTP failures raise an `APIError` subclass based on the response: `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` or `InternalServerError`. See the [error mapping and retry rules](https://docs.reversecontact.com/sdk/python/errors-and-retries). ## person Provide at least one of `email`, `first_name`, `last_name`, `company_domain` or `company_name`. Inputs are independent. `full_profile` remains accepted but is deprecated and ignored by the API. **Python SDK** ```python [Python SDK] def person( *, email: str | NotGiven | None = NOT_GIVEN, first_name: str | NotGiven | None = NOT_GIVEN, last_name: str | NotGiven | None = NOT_GIVEN, company_domain: str | NotGiven | None = NOT_GIVEN, company_name: str | NotGiven | None = NOT_GIVEN, full_profile: bool | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.EnrichPersonData] | None: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `email` | `str \| NotGiven \| None` | Optional. Professional email address to look up (e.g. `jane.doe@acme.com`). | | `first_name` | `str \| NotGiven \| None` | Optional. First name of the person to look up. | | `last_name` | `str \| NotGiven \| None` | Optional. Last name of the person to look up. | | `company_domain` | `str \| NotGiven \| None` | Optional. Company domain to help disambiguate (e.g. `acme.com` or `https://acme.com`). | | `company_name` | `str \| NotGiven \| None` | Optional. Company name to help disambiguate (e.g. `Acme Corp`). | | `full_profile` | `bool \| NotGiven \| None` | Optional. Deprecated and ignored. Enrich always returns the full profile at the same cost. You can remove this field, though the API still accepts it so existing requests keep working. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.EnrichPersonData] | None`. `data.person` is the matched person. `data.alternative_persons` contains other candidates, and `data.is_full_profile` identifies the profile shape. A no-match lookup returns `None` at no credit cost. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. No-match 404 responses return `None`. Other 404 responses raise `NotFoundError`. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.enrich.person(email="jane.doe@acme.com") print(result.data if result is not None else None) ``` ## company Provide `domain`. The API requires this field even though the generated Python signature permits omission. `full_profile` is deprecated and ignored. **Python SDK** ```python [Python SDK] def company( *, domain: str | NotGiven | None = NOT_GIVEN, full_profile: bool | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.EnrichCompanyData] | None: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `domain` | `str \| NotGiven \| None` | **Required.** Company domain to look up. A bare domain or a full URL works (e.g. `acme.com` or `https://acme.com`). | | `full_profile` | `bool \| NotGiven \| None` | Optional. Deprecated and ignored. Enrich always returns the full profile at the same cost. You can remove this field, though the API still accepts it so existing requests keep working. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.EnrichCompanyData] | None`. `data.company` is the matched company and `data.is_full_profile` identifies the profile shape. A no-match lookup returns `None` at no credit cost. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. No-match 404 responses return `None`. Other 404 responses raise `NotFoundError`. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.enrich.company(domain="acme.com") print(result.data if result is not None else None) ``` ## Related guides See the [Python SDK overview](https://docs.reversecontact.com/sdk/python) for setup and [First call](https://docs.reversecontact.com/sdk/python/first-call) for an example. Use [Fetch](https://docs.reversecontact.com/sdk/python/fetch) when you already have a profile URL or id. ## FAQ ### What does client.enrich return when nothing matches? `client.enrich.person()` and `client.enrich.company()` return `None` for a no-match lookup. A no-match lookup costs no credits. ### Is full_profile required for Python enrichment? `full_profile` is accepted by the Python SDK but deprecated and ignored by the API. Enrichment includes the full profile at the same 2-credit match price. ### Where is the enriched person in the Python response? The enriched person is in `result.data.person`. The enriched company is in `result.data.company`. Check the response and nested fields for `None` before reading them. --- # client.fetch > Fetch cached or live person and company profiles with the Python SDK, check profile status, and inspect each method, return type, error and cost. - Page: https://docs.reversecontact.com/sdk/python/fetch - Last verified: 2026-11-04 Read cached person or company profiles, check availability, or start a live fetch job. | Method | Endpoint | Cost | | ------ | -------- | ---- | | [`person()`](#person) | [POST /v2/fetch/persons](https://docs.reversecontact.com/endpoints/fetch-profile) | 1 credit when the profile is found in the cache. A 404 is free. | | [`person_live()`](#person_live) | [POST /v2/fetch/persons/live](https://docs.reversecontact.com/endpoints/fetch-profile-live) | 2 credits per request, consumed when the job is created. | | [`check_person()`](#check_person) | [POST /v2/fetch/persons/check](https://docs.reversecontact.com/endpoints/check-person) | Free. Needs at least 2 credits in the workspace: below that, 402 NO_CREDITS. | | [`company()`](#company) | [POST /v2/fetch/companies](https://docs.reversecontact.com/endpoints/fetch-company) | 1 credit when the company is found in the cache. A 404 is free. | | [`company_live()`](#company_live) | [POST /v2/fetch/companies/live](https://docs.reversecontact.com/endpoints/fetch-company-live) | 2 credits per request, consumed when the job is created. | | [`check_company()`](#check_company) | [POST /v2/fetch/companies/check](https://docs.reversecontact.com/endpoints/check-company) | Free. Needs at least 2 credits in the workspace: below that, 402 NO_CREDITS. | ## Calling conventions Examples assume `RC_API_KEY` is set. Replace example ids with ids returned by your own requests. Signatures show the published 0.3.0 synchronous methods. Use the same arguments with the [Python async client](https://docs.reversecontact.com/sdk/python/async-client). `models` refers to `reversecontact.types`. `NOT_GIVEN` and `NotGiven` are the SDK's internal markers for omitted arguments. Leave these arguments out of your calls. For API body parameters, passing `None` sends an explicit null. Transport options use `None` to inherit their defaults. `Timeout` means a number of seconds or `httpx.Timeout`. Supply parameters marked **Required** to Python or to the API, even when the generated signature permits omission. JSON response objects expose `data`, `quotas`, `metadata` and `http_response`. Check optional fields for `None`. See [First call](https://docs.reversecontact.com/sdk/python/first-call) for the response envelope. CSV methods return text and iterators yield items. HTTP failures raise an `APIError` subclass based on the response: `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` or `InternalServerError`. See the [error mapping and retry rules](https://docs.reversecontact.com/sdk/python/errors-and-retries). ## person Supply a person `url` or `id`. **Python SDK** ```python [Python SDK] def person( *, url: str | NotGiven | None = NOT_GIVEN, id: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.PersonFull] | None: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `url` | `str \| NotGiven \| None` | **Required for the selected strategy.** Public Social profile URL (e.g. `https://social.com/in/janedoe`). Required unless `id` is given. | | `id` | `str \| NotGiven \| None` | **Required for the selected strategy.** Reverse Contact person id from a previous response (e.g. `prs_01jbd5vewyenebxgaz869ffe5t`). Required unless `url` is given. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.PersonFull] | None`. `data` is the cached person profile. A no-match lookup returns `None` at no credit cost. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. No-match 404 responses return `None`. Other 404 responses raise `NotFoundError`. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.fetch.person(url="https://social.com/in/janedoe") print(result.data if result is not None else None) ``` ## person_live Start an asynchronous enrichment job with a person `url` or `id`. **Python SDK** ```python [Python SDK] def person_live( *, url: str | NotGiven | None = NOT_GIVEN, id: str | NotGiven | None = NOT_GIVEN, webhook_url: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Job[models.PersonFull]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `url` | `str \| NotGiven \| None` | **Required for the selected strategy.** Public Social profile URL (e.g. `https://social.com/in/janedoe`). Provide exactly one of `url` or `id`. | | `id` | `str \| NotGiven \| None` | **Required for the selected strategy.** Reverse Contact person id (`prs_…`). The gateway resolves it to the current Social public slug before starting the scrape. | | `webhook_url` | `str \| NotGiven \| None` | Optional. HTTPS push URL for webhook delivery. Omit this field to retrieve the result via [polling](https://docs.reversecontact.com/guides/polling) (recommended). See the [Webhooks guide](https://docs.reversecontact.com/guides/webhooks) when you need push delivery. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Job[models.PersonFull]`. A job acknowledgement with `id`, `data`, `quotas`, `metadata`, `http_response` and `replayed`. Save `job.id` before waiting. Call `job.wait()` to get `Result[JobSnapshot[T]]` with the final value in `data.result`. See [Async jobs](https://docs.reversecontact.com/sdk/python/async-jobs) for details. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options or a missing job id in the acknowledgement. `wait()` raises `JobFailedError` for an errored job and `JobTimeoutError` when its overall deadline expires. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: job = client.fetch.person_live(url="https://social.com/in/janedoe") print("Save this job id:", job.id) done = job.wait() print(done.data.result if done.data else None) ``` ## check_person Supply a person `url` or `id`. The call is free but requires at least 2 credits in the workspace. **Python SDK** ```python [Python SDK] def check_person( *, url: str | NotGiven | None = NOT_GIVEN, id: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.CheckPersonData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `url` | `str \| NotGiven \| None` | **Required for the selected strategy.** Public Social profile URL (e.g. `https://social.com/in/janedoe`). Required unless `id` is given. | | `id` | `str \| NotGiven \| None` | **Required for the selected strategy.** Reverse Contact person id from a previous response (e.g. `prs_01jbd5vewyenebxgaz869ffe5t`). Required unless `url` is given. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.CheckPersonData]`. `data` contains `exists`, `last_update`, `experience_count`, `skill_count` and `school_count`. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.fetch.check_person(url="https://social.com/in/janedoe") print(result.data if result is not None else None) ``` ## company Supply a company `url` or `id`. **Python SDK** ```python [Python SDK] def company( *, url: str | NotGiven | None = NOT_GIVEN, id: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.CompanyFull] | None: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `url` | `str \| NotGiven \| None` | **Required for the selected strategy.** Public Social company URL (e.g. `https://social.com/company/acme-corp`). Required unless `id` is given. | | `id` | `str \| NotGiven \| None` | **Required for the selected strategy.** Reverse Contact company id from a previous response (e.g. `com_01jbd5vewyenebxgaz869ffe5t`). Required unless `url` is given. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.CompanyFull] | None`. `data` is the cached company profile. A no-match lookup returns `None` at no credit cost. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. No-match 404 responses return `None`. Other 404 responses raise `NotFoundError`. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.fetch.company(url="https://social.com/company/acme") print(result.data if result is not None else None) ``` ## company_live Start an asynchronous enrichment job with a company `url` or `id`. **Python SDK** ```python [Python SDK] def company_live( *, url: str | NotGiven | None = NOT_GIVEN, id: str | NotGiven | None = NOT_GIVEN, webhook_url: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Job[models.CompanyFull]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `url` | `str \| NotGiven \| None` | **Required for the selected strategy.** Public Social company URL (e.g. `https://social.com/company/acme-corp`). Provide exactly one of `url` or `id`. | | `id` | `str \| NotGiven \| None` | **Required for the selected strategy.** Reverse Contact company id (`com_…`). The gateway resolves it to the current Social public slug before starting the scrape. | | `webhook_url` | `str \| NotGiven \| None` | Optional. HTTPS push URL for webhook delivery. Omit this field to retrieve the result via [polling](https://docs.reversecontact.com/guides/polling) (recommended). See the [Webhooks guide](https://docs.reversecontact.com/guides/webhooks) when you need push delivery. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Job[models.CompanyFull]`. A job acknowledgement with `id`, `data`, `quotas`, `metadata`, `http_response` and `replayed`. Save `job.id` before waiting. Call `job.wait()` to get `Result[JobSnapshot[T]]` with the final value in `data.result`. See [Async jobs](https://docs.reversecontact.com/sdk/python/async-jobs) for details. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options or a missing job id in the acknowledgement. `wait()` raises `JobFailedError` for an errored job and `JobTimeoutError` when its overall deadline expires. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: job = client.fetch.company_live(url="https://social.com/company/acme") print("Save this job id:", job.id) done = job.wait() print(done.data.result if done.data else None) ``` ## check_company Supply a company `url` or `id`. The call is free but requires at least 2 credits in the workspace. **Python SDK** ```python [Python SDK] def check_company( *, url: str | NotGiven | None = NOT_GIVEN, id: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.CheckCompanyData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `url` | `str \| NotGiven \| None` | **Required for the selected strategy.** Public Social company URL (e.g. `https://social.com/company/acme-corp`). Required unless `id` is given. | | `id` | `str \| NotGiven \| None` | **Required for the selected strategy.** Reverse Contact company id from a previous response (e.g. `com_01jbd5vewyenebxgaz869ffe5t`). Required unless `url` is given. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.CheckCompanyData]`. `data` contains `exists`, `last_update` and `speciality_count`. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.fetch.check_company(url="https://social.com/company/acme") print(result.data if result is not None else None) ``` ## Related guides See the [Python SDK overview](https://docs.reversecontact.com/sdk/python), [Async jobs](https://docs.reversecontact.com/sdk/python/async-jobs) and [Jobs](https://docs.reversecontact.com/sdk/python/jobs). ## FAQ ### How do I fetch a cached person profile in Python? Call `client.fetch.person()` with `url` or `id`. The profile is in `result.data`. A no-match response is `None` and costs no credits. ### How do I wait for a live profile in Python? Call `wait()` on the job returned by `client.fetch.person_live()` or `client.fetch.company_live()`. Save `job.id` before waiting to resume polling later. ### Does checking profile status cost credits? `client.fetch.check_person()` and `client.fetch.check_company()` are free, but the workspace must have at least 2 credits. --- # client.search > Search and count people or companies with the Python SDK, inspect filter fields, and use budgeted iterators with documented parameters and costs. - Page: https://docs.reversecontact.com/sdk/python/search - Last verified: 2026-11-04 Search people and companies, count matches, inspect filters, and iterate within a result budget. | Method | Endpoint | Cost | | ------ | -------- | ---- | | [`people()`](#people) | [POST /v2/search/people](https://docs.reversecontact.com/endpoints/search-people) | 1 credit per profile delivered. A page with no profile is free. | | [`iter_people()`](#iter_people) | [POST /v2/search/people](https://docs.reversecontact.com/endpoints/search-people) | 1 credit per profile delivered. A page with no profile is free. | | [`people_by_prompt()`](#people_by_prompt) | [POST /v2/search/people/prompt](https://docs.reversecontact.com/endpoints/search-people-prompt) | 1 credit per page for the search, plus 1 credit per profile delivered. | | [`iter_people_by_prompt()`](#iter_people_by_prompt) | [POST /v2/search/people/prompt](https://docs.reversecontact.com/endpoints/search-people-prompt) | 1 credit per page for the search, plus 1 credit per profile delivered. | | [`people_lookalike()`](#people_lookalike) | [POST /v2/search/people/lookalike](https://docs.reversecontact.com/endpoints/search-people-lookalike) | 1 credit per profile delivered. A page with no profile is free. | | [`iter_people_lookalike()`](#iter_people_lookalike) | [POST /v2/search/people/lookalike](https://docs.reversecontact.com/endpoints/search-people-lookalike) | 1 credit per profile delivered. A page with no profile is free. | | [`count_people()`](#count_people) | [POST /v2/search/people/count](https://docs.reversecontact.com/endpoints/search-people-count) | Free. | | [`people_auto_suggestion()`](#people_auto_suggestion) | [POST /v2/search/people/auto-suggestion](https://docs.reversecontact.com/endpoints/search-people-auto-suggestion) | Free. | | [`organizations()`](#organizations) | [POST /v2/search/organizations](https://docs.reversecontact.com/endpoints/search-companies) | 1 credit per company delivered. A page with no company is free. | | [`iter_organizations()`](#iter_organizations) | [POST /v2/search/organizations](https://docs.reversecontact.com/endpoints/search-companies) | 1 credit per company delivered. A page with no company is free. | | [`organizations_by_prompt()`](#organizations_by_prompt) | [POST /v2/search/organizations/prompt](https://docs.reversecontact.com/endpoints/search-companies-prompt) | 1 credit per page for the search, plus 1 credit per company delivered. | | [`iter_organizations_by_prompt()`](#iter_organizations_by_prompt) | [POST /v2/search/organizations/prompt](https://docs.reversecontact.com/endpoints/search-companies-prompt) | 1 credit per page for the search, plus 1 credit per company delivered. | | [`count_organizations()`](#count_organizations) | [POST /v2/search/organizations/count](https://docs.reversecontact.com/endpoints/search-companies-count) | Free. | | [`people_fields()`](#people_fields) | [GET /v2/search/people/available-fields](https://docs.reversecontact.com/endpoints/search-people-available-fields) | Free. | | [`organizations_fields()`](#organizations_fields) | [GET /v2/search/organizations/available-fields](https://docs.reversecontact.com/endpoints/search-companies-available-fields) | Free. | ## Calling conventions Examples assume `RC_API_KEY` is set. Replace example ids with ids returned by your own requests. Signatures show the published 0.3.0 synchronous methods. Use the same arguments with the [Python async client](https://docs.reversecontact.com/sdk/python/async-client). `models` refers to `reversecontact.types`. `NOT_GIVEN` and `NotGiven` are the SDK's internal markers for omitted arguments. Leave these arguments out of your calls. For API body parameters, passing `None` sends an explicit null. Transport options use `None` to inherit their defaults. `Timeout` means a number of seconds or `httpx.Timeout`. Supply parameters marked **Required** to Python or to the API, even when the generated signature permits omission. JSON response objects expose `data`, `quotas`, `metadata` and `http_response`. Check optional fields for `None`. See [First call](https://docs.reversecontact.com/sdk/python/first-call) for the response envelope. CSV methods return text and iterators yield items. HTTP failures raise an `APIError` subclass based on the response: `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` or `InternalServerError`. See the [error mapping and retry rules](https://docs.reversecontact.com/sdk/python/errors-and-retries). Set `max_results` when you use a search iterator. The iterator reduces the final page to the remaining budget and makes no request for `0`. A `Page` has `data`, `next_cursor`, `count`, `quotas`, `has_next_page()` and `get_next_page()`. Prompt searches also expose `interpretation`. See [Search within a budget](https://docs.reversecontact.com/sdk/python/search-budget). ## people Call [People search](https://docs.reversecontact.com/endpoints/search-people) with `client.search.people()`. **Python SDK** ```python [Python SDK] def people( *, first_name: str | NotGiven | None = NOT_GIVEN, last_name: str | NotGiven | None = NOT_GIVEN, headline: str | NotGiven | None = NOT_GIVEN, is_open_to_work: bool | NotGiven | None = NOT_GIVEN, is_currently_employed: bool | NotGiven | None = NOT_GIVEN, is_premium: bool | NotGiven | None = NOT_GIVEN, is_verified: bool | NotGiven | None = NOT_GIVEN, followers_count: models.SearchPeopleParamsFollowersCount | dict[str, object] | NotGiven | None = NOT_GIVEN, connections_count: models.SearchPeopleParamsConnectionsCount | dict[str, object] | NotGiven | None = NOT_GIVEN, position_title: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_position_title: str | list[str] | NotGiven | None = NOT_GIVEN, company_name: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_name: str | list[str] | NotGiven | None = NOT_GIVEN, company_industry: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_industry: str | list[str] | NotGiven | None = NOT_GIVEN, company_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_id: str | list[str] | NotGiven | None = NOT_GIVEN, company_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, company_public_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_public_id: str | list[str] | NotGiven | None = NOT_GIVEN, current_position_title: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_position_title: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_name: str | NotGiven | None = NOT_GIVEN, exclude_current_company_name: str | NotGiven | None = NOT_GIVEN, current_company_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_company_id: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_company_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_public_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_company_public_id: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_industry: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_employee_count_range: models.SearchPeopleParamsCurrentCompanyEmployeeCountRange | dict[str, object] | NotGiven | None = NOT_GIVEN, current_function: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_function: str | list[str] | NotGiven | None = NOT_GIVEN, current_sub_function: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_sub_function: str | list[str] | NotGiven | None = NOT_GIVEN, current_seniority: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_seniority: str | list[str] | NotGiven | None = NOT_GIVEN, current_contract_type: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_contract_type: str | list[str] | NotGiven | None = NOT_GIVEN, contract_type: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_contract_type: str | list[str] | NotGiven | None = NOT_GIVEN, school_name: str | list[str] | NotGiven | None = NOT_GIVEN, school_id: str | list[str] | NotGiven | None = NOT_GIVEN, school_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, total_experience_months: models.SearchPeopleParamsTotalExperienceMonths | dict[str, object] | NotGiven | None = NOT_GIVEN, experience: list[models.SearchPeopleParamsExperienceItem | dict[str, object]] | NotGiven | None = NOT_GIVEN, skills: str | list[str] | NotGiven | None = NOT_GIVEN, languages: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_languages: str | list[str] | NotGiven | None = NOT_GIVEN, keywords: str | NotGiven | None = NOT_GIVEN, changed_jobs_within_days: int | NotGiven | None = NOT_GIVEN, location: models.SearchPeopleParamsLocation | dict[str, object] | NotGiven | None = NOT_GIVEN, max_data_age_date: str | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, per_page: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Page[models.PersonFull]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `first_name` | `str \| NotGiven \| None` | Optional. Filter by first name | | `last_name` | `str \| NotGiven \| None` | Optional. Filter by last name | | `headline` | `str \| NotGiven \| None` | Optional. Match keywords found in the profile headline | | `is_open_to_work` | `bool \| NotGiven \| None` | Optional. Restrict to profiles flagged open-to-work | | `is_currently_employed` | `bool \| NotGiven \| None` | Optional. Restrict to profiles that have a current employer | | `is_premium` | `bool \| NotGiven \| None` | Optional. Restrict to Social Premium members | | `is_verified` | `bool \| NotGiven \| None` | Optional. Restrict to profiles with a verification badge | | `followers_count` | `models.SearchPeopleParamsFollowersCount \| dict[str, object] \| NotGiven \| None` | Optional. Follower count range, with inclusive `min` and `max` bounds. | | `connections_count` | `models.SearchPeopleParamsConnectionsCount \| dict[str, object] \| NotGiven \| None` | Optional. Connection count range, bounds inclusive. One bound is enough: { "min": 501 } keeps profiles with more than 500 connections | | `position_title` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past position title, or array of titles (OR-matched) | | `exclude_position_title` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past position title, or array of titles, to exclude | | `company_name` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company name, or array of names (OR-matched) | | `exclude_company_name` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company name, or array of names, to exclude | | `company_industry` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company industry, from the same industry list | | `exclude_company_industry` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company industry, or array, to exclude | | `company_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company `com_...` id, or array of ids | | `exclude_company_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company `com_...` id, or array of ids, to exclude | | `company_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company Social identifier, or array of identifiers | | `exclude_company_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company Social identifier, or array, to exclude | | `company_public_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company public ID or slug, or array | | `exclude_company_public_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company public ID or slug, or array, to exclude | | `current_position_title` | `str \| list[str] \| NotGiven \| None` | Optional. Current position title, or array of titles: a person matches if their current title matches any of them | | `exclude_current_position_title` | `str \| list[str] \| NotGiven \| None` | Optional. Current position title, or array of titles, to exclude from results | | `current_company_name` | `str \| NotGiven \| None` | Optional. Match the current employer name | | `exclude_current_company_name` | `str \| NotGiven \| None` | Optional. Exclude the current employer name | | `current_company_id` | `str \| list[str] \| NotGiven \| None` | Optional. Reverse Contact internal id (`com_...`) of the current employer | | `exclude_current_company_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer `com_...` id, or array of ids, to exclude | | `current_company_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer Social identifier, or array of identifiers | | `exclude_current_company_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer Social identifier, or array, to exclude | | `current_company_public_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer public ID or slug, or array | | `exclude_current_company_public_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer public ID or slug, or array, to exclude | | `current_company_industry` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer industry. See [Industry values](https://docs.reversecontact.com/reference/industries) for the full list | | `current_company_employee_count_range` | `models.SearchPeopleParamsCurrentCompanyEmployeeCountRange \| dict[str, object] \| NotGiven \| None` | Optional. Current employer size range, with inclusive `min` and `max` bounds. | | `current_function` | `str \| list[str] \| NotGiven \| None` | Optional. Current function from the [position taxonomy](https://docs.reversecontact.com/endpoints/search-people#position-taxonomy) | | `exclude_current_function` | `str \| list[str] \| NotGiven \| None` | Optional. Current function, or array of functions, to exclude | | `current_sub_function` | `str \| list[str] \| NotGiven \| None` | Optional. Current sub-function from the [position taxonomy](https://docs.reversecontact.com/endpoints/search-people#position-taxonomy) | | `exclude_current_sub_function` | `str \| list[str] \| NotGiven \| None` | Optional. Current sub-function, or array of sub-functions, to exclude | | `current_seniority` | `str \| list[str] \| NotGiven \| None` | Optional. Current seniority from the [position taxonomy](https://docs.reversecontact.com/endpoints/search-people#position-taxonomy) | | `exclude_current_seniority` | `str \| list[str] \| NotGiven \| None` | Optional. Current seniority, or array of seniorities, to exclude | | `current_contract_type` | `str \| list[str] \| NotGiven \| None` | Optional. Contract type of the current position, or array (OR-matched). See [Contract types](https://docs.reversecontact.com/endpoints/search-people#contract-types) | | `exclude_current_contract_type` | `str \| list[str] \| NotGiven \| None` | Optional. Contract type of the current position, or array, to exclude. Requires a positive filter alongside it | | `contract_type` | `str \| list[str] \| NotGiven \| None` | Optional. Contract type of any position, current or past, or array (OR-matched) | | `exclude_contract_type` | `str \| list[str] \| NotGiven \| None` | Optional. Contract type of any position, current or past, or array, to exclude. Requires a positive filter alongside it | | `school_name` | `str \| list[str] \| NotGiven \| None` | Optional. School name, or array of names: a person matches if they attended any of them. See [Schools](https://docs.reversecontact.com/endpoints/search-people#schools) | | `school_id` | `str \| list[str] \| NotGiven \| None` | Optional. Reverse Contact school id (`sch_...`), or array. See [Schools](https://docs.reversecontact.com/endpoints/search-people#schools) | | `school_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. School Social identifier, or array. See [Schools](https://docs.reversecontact.com/endpoints/search-people#schools) | | `total_experience_months` | `models.SearchPeopleParamsTotalExperienceMonths \| dict[str, object] \| NotGiven \| None` | Optional. Total work experience in months, with inclusive `min` and `max` bounds. | | `experience` | `list[models.SearchPeopleParamsExperienceItem \| dict[str, object]] \| NotGiven \| None` | Optional. Past-experience requirements. All entries are AND-combined | | `skills` | `str \| list[str] \| NotGiven \| None` | Optional. Skill name, or array of skill names: a person matches if they list any of them. See [Skills](https://docs.reversecontact.com/endpoints/search-people#skills) | | `languages` | `str \| list[str] \| NotGiven \| None` | Optional. Language name in English, or array: a person matches if they speak any of them. See [Languages](https://docs.reversecontact.com/endpoints/search-people#languages) | | `exclude_languages` | `str \| list[str] \| NotGiven \| None` | Optional. Language name in English, or array, to exclude. Requires a positive filter alongside it | | `keywords` | `str \| NotGiven \| None` | Optional. Words searched across the whole profile: every word must appear, in any field. See [Keywords](https://docs.reversecontact.com/endpoints/search-people#keywords) | | `changed_jobs_within_days` | `int \| NotGiven \| None` | Optional. `30`, `60`, `90` or `120`: the current position started within that many days. See [Recent job change](https://docs.reversecontact.com/endpoints/search-people#recent-job-change) | | `location` | `models.SearchPeopleParamsLocation \| dict[str, object] \| NotGiven \| None` | Optional. Location filters such as `countryCode`, `area`, `region` and `city`. | | `max_data_age_date` | `str \| NotGiven \| None` | Optional. ISO 8601 datetime. Only returns profiles refreshed after this date | | `cursor` | `str \| NotGiven \| None` | Optional. Opaque cursor returned as `metadata.nextCursor`. Use this cursor to request the next page | | `per_page` | `int \| NotGiven \| None` | Optional. Profiles per page (1 to 100, default `25`) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Page[models.PersonFull]`. A page with records in `data`, a `next_cursor`, `has_next_page()` and `get_next_page()`. The next-page method reuses the filters and returns `None` after the last page. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: page = client.search.people(current_function="Marketing", per_page=10) for item in page.data: if item is not None: print(item.to_dict()) ``` ## iter_people Call [People search](https://docs.reversecontact.com/endpoints/search-people) with `client.search.iter_people()`. **Python SDK** ```python [Python SDK] def iter_people( *, first_name: str | NotGiven | None = NOT_GIVEN, last_name: str | NotGiven | None = NOT_GIVEN, headline: str | NotGiven | None = NOT_GIVEN, is_open_to_work: bool | NotGiven | None = NOT_GIVEN, is_currently_employed: bool | NotGiven | None = NOT_GIVEN, is_premium: bool | NotGiven | None = NOT_GIVEN, is_verified: bool | NotGiven | None = NOT_GIVEN, followers_count: models.SearchPeopleParamsFollowersCount | dict[str, object] | NotGiven | None = NOT_GIVEN, connections_count: models.SearchPeopleParamsConnectionsCount | dict[str, object] | NotGiven | None = NOT_GIVEN, position_title: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_position_title: str | list[str] | NotGiven | None = NOT_GIVEN, company_name: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_name: str | list[str] | NotGiven | None = NOT_GIVEN, company_industry: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_industry: str | list[str] | NotGiven | None = NOT_GIVEN, company_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_id: str | list[str] | NotGiven | None = NOT_GIVEN, company_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, company_public_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_public_id: str | list[str] | NotGiven | None = NOT_GIVEN, current_position_title: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_position_title: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_name: str | NotGiven | None = NOT_GIVEN, exclude_current_company_name: str | NotGiven | None = NOT_GIVEN, current_company_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_company_id: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_company_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_public_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_company_public_id: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_industry: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_employee_count_range: models.SearchPeopleParamsCurrentCompanyEmployeeCountRange | dict[str, object] | NotGiven | None = NOT_GIVEN, current_function: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_function: str | list[str] | NotGiven | None = NOT_GIVEN, current_sub_function: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_sub_function: str | list[str] | NotGiven | None = NOT_GIVEN, current_seniority: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_seniority: str | list[str] | NotGiven | None = NOT_GIVEN, current_contract_type: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_contract_type: str | list[str] | NotGiven | None = NOT_GIVEN, contract_type: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_contract_type: str | list[str] | NotGiven | None = NOT_GIVEN, school_name: str | list[str] | NotGiven | None = NOT_GIVEN, school_id: str | list[str] | NotGiven | None = NOT_GIVEN, school_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, total_experience_months: models.SearchPeopleParamsTotalExperienceMonths | dict[str, object] | NotGiven | None = NOT_GIVEN, experience: list[models.SearchPeopleParamsExperienceItem | dict[str, object]] | NotGiven | None = NOT_GIVEN, skills: str | list[str] | NotGiven | None = NOT_GIVEN, languages: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_languages: str | list[str] | NotGiven | None = NOT_GIVEN, keywords: str | NotGiven | None = NOT_GIVEN, changed_jobs_within_days: int | NotGiven | None = NOT_GIVEN, location: models.SearchPeopleParamsLocation | dict[str, object] | NotGiven | None = NOT_GIVEN, max_data_age_date: str | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, per_page: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, max_results: int, **extra: object, ) -> Iterator[models.PersonFull | None]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `first_name` | `str \| NotGiven \| None` | Optional. Filter by first name | | `last_name` | `str \| NotGiven \| None` | Optional. Filter by last name | | `headline` | `str \| NotGiven \| None` | Optional. Match keywords found in the profile headline | | `is_open_to_work` | `bool \| NotGiven \| None` | Optional. Restrict to profiles flagged open-to-work | | `is_currently_employed` | `bool \| NotGiven \| None` | Optional. Restrict to profiles that have a current employer | | `is_premium` | `bool \| NotGiven \| None` | Optional. Restrict to Social Premium members | | `is_verified` | `bool \| NotGiven \| None` | Optional. Restrict to profiles with a verification badge | | `followers_count` | `models.SearchPeopleParamsFollowersCount \| dict[str, object] \| NotGiven \| None` | Optional. Follower count range, with inclusive `min` and `max` bounds. | | `connections_count` | `models.SearchPeopleParamsConnectionsCount \| dict[str, object] \| NotGiven \| None` | Optional. Connection count range, bounds inclusive. One bound is enough: { "min": 501 } keeps profiles with more than 500 connections | | `position_title` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past position title, or array of titles (OR-matched) | | `exclude_position_title` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past position title, or array of titles, to exclude | | `company_name` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company name, or array of names (OR-matched) | | `exclude_company_name` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company name, or array of names, to exclude | | `company_industry` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company industry, from the same industry list | | `exclude_company_industry` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company industry, or array, to exclude | | `company_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company `com_...` id, or array of ids | | `exclude_company_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company `com_...` id, or array of ids, to exclude | | `company_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company Social identifier, or array of identifiers | | `exclude_company_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company Social identifier, or array, to exclude | | `company_public_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company public ID or slug, or array | | `exclude_company_public_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company public ID or slug, or array, to exclude | | `current_position_title` | `str \| list[str] \| NotGiven \| None` | Optional. Current position title, or array of titles: a person matches if their current title matches any of them | | `exclude_current_position_title` | `str \| list[str] \| NotGiven \| None` | Optional. Current position title, or array of titles, to exclude from results | | `current_company_name` | `str \| NotGiven \| None` | Optional. Match the current employer name | | `exclude_current_company_name` | `str \| NotGiven \| None` | Optional. Exclude the current employer name | | `current_company_id` | `str \| list[str] \| NotGiven \| None` | Optional. Reverse Contact internal id (`com_...`) of the current employer | | `exclude_current_company_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer `com_...` id, or array of ids, to exclude | | `current_company_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer Social identifier, or array of identifiers | | `exclude_current_company_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer Social identifier, or array, to exclude | | `current_company_public_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer public ID or slug, or array | | `exclude_current_company_public_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer public ID or slug, or array, to exclude | | `current_company_industry` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer industry. See [Industry values](https://docs.reversecontact.com/reference/industries) for the full list | | `current_company_employee_count_range` | `models.SearchPeopleParamsCurrentCompanyEmployeeCountRange \| dict[str, object] \| NotGiven \| None` | Optional. Current employer size range, with inclusive `min` and `max` bounds. | | `current_function` | `str \| list[str] \| NotGiven \| None` | Optional. Current function from the [position taxonomy](https://docs.reversecontact.com/endpoints/search-people#position-taxonomy) | | `exclude_current_function` | `str \| list[str] \| NotGiven \| None` | Optional. Current function, or array of functions, to exclude | | `current_sub_function` | `str \| list[str] \| NotGiven \| None` | Optional. Current sub-function from the [position taxonomy](https://docs.reversecontact.com/endpoints/search-people#position-taxonomy) | | `exclude_current_sub_function` | `str \| list[str] \| NotGiven \| None` | Optional. Current sub-function, or array of sub-functions, to exclude | | `current_seniority` | `str \| list[str] \| NotGiven \| None` | Optional. Current seniority from the [position taxonomy](https://docs.reversecontact.com/endpoints/search-people#position-taxonomy) | | `exclude_current_seniority` | `str \| list[str] \| NotGiven \| None` | Optional. Current seniority, or array of seniorities, to exclude | | `current_contract_type` | `str \| list[str] \| NotGiven \| None` | Optional. Contract type of the current position, or array (OR-matched). See [Contract types](https://docs.reversecontact.com/endpoints/search-people#contract-types) | | `exclude_current_contract_type` | `str \| list[str] \| NotGiven \| None` | Optional. Contract type of the current position, or array, to exclude. Requires a positive filter alongside it | | `contract_type` | `str \| list[str] \| NotGiven \| None` | Optional. Contract type of any position, current or past, or array (OR-matched) | | `exclude_contract_type` | `str \| list[str] \| NotGiven \| None` | Optional. Contract type of any position, current or past, or array, to exclude. Requires a positive filter alongside it | | `school_name` | `str \| list[str] \| NotGiven \| None` | Optional. School name, or array of names: a person matches if they attended any of them. See [Schools](https://docs.reversecontact.com/endpoints/search-people#schools) | | `school_id` | `str \| list[str] \| NotGiven \| None` | Optional. Reverse Contact school id (`sch_...`), or array. See [Schools](https://docs.reversecontact.com/endpoints/search-people#schools) | | `school_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. School Social identifier, or array. See [Schools](https://docs.reversecontact.com/endpoints/search-people#schools) | | `total_experience_months` | `models.SearchPeopleParamsTotalExperienceMonths \| dict[str, object] \| NotGiven \| None` | Optional. Total work experience in months, with inclusive `min` and `max` bounds. | | `experience` | `list[models.SearchPeopleParamsExperienceItem \| dict[str, object]] \| NotGiven \| None` | Optional. Past-experience requirements. All entries are AND-combined | | `skills` | `str \| list[str] \| NotGiven \| None` | Optional. Skill name, or array of skill names: a person matches if they list any of them. See [Skills](https://docs.reversecontact.com/endpoints/search-people#skills) | | `languages` | `str \| list[str] \| NotGiven \| None` | Optional. Language name in English, or array: a person matches if they speak any of them. See [Languages](https://docs.reversecontact.com/endpoints/search-people#languages) | | `exclude_languages` | `str \| list[str] \| NotGiven \| None` | Optional. Language name in English, or array, to exclude. Requires a positive filter alongside it | | `keywords` | `str \| NotGiven \| None` | Optional. Words searched across the whole profile: every word must appear, in any field. See [Keywords](https://docs.reversecontact.com/endpoints/search-people#keywords) | | `changed_jobs_within_days` | `int \| NotGiven \| None` | Optional. `30`, `60`, `90` or `120`: the current position started within that many days. See [Recent job change](https://docs.reversecontact.com/endpoints/search-people#recent-job-change) | | `location` | `models.SearchPeopleParamsLocation \| dict[str, object] \| NotGiven \| None` | Optional. Location filters such as `countryCode`, `area`, `region` and `city`. | | `max_data_age_date` | `str \| NotGiven \| None` | Optional. ISO 8601 datetime. Only returns profiles refreshed after this date | | `cursor` | `str \| NotGiven \| None` | Optional. Opaque cursor returned as `metadata.nextCursor`. Use this cursor to request the next page | | `per_page` | `int \| NotGiven \| None` | Optional. Profiles per page (1 to 100, default `25`) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `max_results` | `int` | **Required.** Maximum number of paid results to request. Must be a non-negative integer. `0` makes no request. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Iterator[models.PersonFull | None]`. An iterator of typed records. Entries can be `None`. The iterator fetches pages only as you advance. `max_results` caps paid results, not prompt page fees. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options, invalid `max_results` or `per_page`, or a repeated pagination cursor. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: for item in client.search.iter_people(current_function="Marketing", max_results=10): if item is not None: print(item.to_dict()) ``` ## people_by_prompt Call [Agentic Search](https://docs.reversecontact.com/endpoints/search-people-prompt) with `client.search.people_by_prompt()`. **Python SDK** ```python [Python SDK] def people_by_prompt( *, prompt: str | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, per_page: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Page[models.PersonFull]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `prompt` | `str \| NotGiven \| None` | **Required.** Plain-language description of the people to find, in any language. At least 3 characters and at most 1000 tokens | | `cursor` | `str \| NotGiven \| None` | Optional. Opaque cursor returned as `metadata.nextCursor`. Use this cursor to request the next page with the same `prompt` | | `per_page` | `int \| NotGiven \| None` | Optional. Profiles per page (1 to 100, default `25`) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Page[models.PersonFull]`. A page with records in `data`, a `next_cursor`, `has_next_page()` and `get_next_page()`. The next-page method reuses the filters and returns `None` after the last page. `interpretation` contains the filters inferred from the prompt. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: page = client.search.people_by_prompt(prompt="Marketing leaders in France", per_page=10) for item in page.data: if item is not None: print(item.to_dict()) ``` ## iter_people_by_prompt Call [Agentic Search](https://docs.reversecontact.com/endpoints/search-people-prompt) with `client.search.iter_people_by_prompt()`. **Python SDK** ```python [Python SDK] def iter_people_by_prompt( *, prompt: str | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, per_page: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, max_results: int, **extra: object, ) -> Iterator[models.PersonFull | None]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `prompt` | `str \| NotGiven \| None` | **Required.** Plain-language description of the people to find, in any language. At least 3 characters and at most 1000 tokens | | `cursor` | `str \| NotGiven \| None` | Optional. Opaque cursor returned as `metadata.nextCursor`. Use this cursor to request the next page with the same `prompt` | | `per_page` | `int \| NotGiven \| None` | Optional. Profiles per page (1 to 100, default `25`) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `max_results` | `int` | **Required.** Maximum number of paid results to request. Must be a non-negative integer. `0` makes no request. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Iterator[models.PersonFull | None]`. An iterator of typed records. Entries can be `None`. The iterator fetches pages only as you advance. `max_results` caps paid results, not prompt page fees. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options, invalid `max_results` or `per_page`, or a repeated pagination cursor. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: for item in client.search.iter_people_by_prompt(prompt="Marketing leaders in France", max_results=10): if item is not None: print(item.to_dict()) ``` ## people_lookalike Provide exactly one seed, `url` or `id`, to `client.search.people_lookalike()`. Sending both returns an HTTP 422 response. See [People lookalike](https://docs.reversecontact.com/endpoints/search-people-lookalike). **Python SDK** ```python [Python SDK] def people_lookalike( *, url: str | NotGiven | None = NOT_GIVEN, id: str | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, per_page: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Page[models.PersonFull]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `url` | `str \| NotGiven \| None` | **Required for the selected strategy.** Public Social profile URL of the seed (e.g. `https://social.com/in/janedoe`). Required unless `id` is given. | | `id` | `str \| NotGiven \| None` | **Required for the selected strategy.** Reverse Contact person id of the seed (e.g. `prs_01jbd5vewyenebxgaz869ffe5t`), as returned in the `id` field of any person response. Required unless `url` is given. | | `cursor` | `str \| NotGiven \| None` | Optional. Opaque cursor returned as `metadata.nextCursor`. Use this cursor to request the next page | | `per_page` | `int \| NotGiven \| None` | Optional. Profiles per page (1 to 100, default `25`) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Page[models.PersonFull]`. A page with records in `data`, a `next_cursor`, `has_next_page()` and `get_next_page()`. The next-page method reuses the filters and returns `None` after the last page. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: page = client.search.people_lookalike(url="https://social.com/in/janedoe", per_page=10) for item in page.data: if item is not None: print(item.to_dict()) ``` ## iter_people_lookalike Provide exactly one seed, `url` or `id`, to `client.search.iter_people_lookalike()`. Sending both returns an HTTP 422 response. See [People lookalike](https://docs.reversecontact.com/endpoints/search-people-lookalike). **Python SDK** ```python [Python SDK] def iter_people_lookalike( *, url: str | NotGiven | None = NOT_GIVEN, id: str | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, per_page: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, max_results: int, **extra: object, ) -> Iterator[models.PersonFull | None]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `url` | `str \| NotGiven \| None` | **Required for the selected strategy.** Public Social profile URL of the seed (e.g. `https://social.com/in/janedoe`). Required unless `id` is given. | | `id` | `str \| NotGiven \| None` | **Required for the selected strategy.** Reverse Contact person id of the seed (e.g. `prs_01jbd5vewyenebxgaz869ffe5t`), as returned in the `id` field of any person response. Required unless `url` is given. | | `cursor` | `str \| NotGiven \| None` | Optional. Opaque cursor returned as `metadata.nextCursor`. Use this cursor to request the next page | | `per_page` | `int \| NotGiven \| None` | Optional. Profiles per page (1 to 100, default `25`) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `max_results` | `int` | **Required.** Maximum number of paid results to request. Must be a non-negative integer. `0` makes no request. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Iterator[models.PersonFull | None]`. An iterator of typed records. Entries can be `None`. The iterator fetches pages only as you advance. `max_results` caps paid results, not prompt page fees. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options, invalid `max_results` or `per_page`, or a repeated pagination cursor. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: for item in client.search.iter_people_lookalike(url="https://social.com/in/janedoe", max_results=10): if item is not None: print(item.to_dict()) ``` ## count_people Call [People search count](https://docs.reversecontact.com/endpoints/search-people-count) with `client.search.count_people()`. **Python SDK** ```python [Python SDK] def count_people( *, first_name: str | NotGiven | None = NOT_GIVEN, last_name: str | NotGiven | None = NOT_GIVEN, headline: str | NotGiven | None = NOT_GIVEN, is_open_to_work: bool | NotGiven | None = NOT_GIVEN, is_currently_employed: bool | NotGiven | None = NOT_GIVEN, is_premium: bool | NotGiven | None = NOT_GIVEN, is_verified: bool | NotGiven | None = NOT_GIVEN, followers_count: models.CountPeopleParamsFollowersCount | dict[str, object] | NotGiven | None = NOT_GIVEN, connections_count: models.CountPeopleParamsConnectionsCount | dict[str, object] | NotGiven | None = NOT_GIVEN, position_title: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_position_title: str | list[str] | NotGiven | None = NOT_GIVEN, company_name: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_name: str | list[str] | NotGiven | None = NOT_GIVEN, company_industry: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_industry: str | list[str] | NotGiven | None = NOT_GIVEN, company_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_id: str | list[str] | NotGiven | None = NOT_GIVEN, company_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, company_public_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_company_public_id: str | list[str] | NotGiven | None = NOT_GIVEN, current_position_title: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_position_title: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_name: str | NotGiven | None = NOT_GIVEN, exclude_current_company_name: str | NotGiven | None = NOT_GIVEN, current_company_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_company_id: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_company_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_public_id: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_company_public_id: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_industry: str | list[str] | NotGiven | None = NOT_GIVEN, current_company_employee_count_range: models.CountPeopleParamsCurrentCompanyEmployeeCountRange | dict[str, object] | NotGiven | None = NOT_GIVEN, current_function: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_function: str | list[str] | NotGiven | None = NOT_GIVEN, current_sub_function: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_sub_function: str | list[str] | NotGiven | None = NOT_GIVEN, current_seniority: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_seniority: str | list[str] | NotGiven | None = NOT_GIVEN, current_contract_type: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_current_contract_type: str | list[str] | NotGiven | None = NOT_GIVEN, contract_type: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_contract_type: str | list[str] | NotGiven | None = NOT_GIVEN, school_name: str | list[str] | NotGiven | None = NOT_GIVEN, school_id: str | list[str] | NotGiven | None = NOT_GIVEN, school_linkedin_id: str | list[str] | NotGiven | None = NOT_GIVEN, total_experience_months: models.CountPeopleParamsTotalExperienceMonths | dict[str, object] | NotGiven | None = NOT_GIVEN, experience: list[models.CountPeopleParamsExperienceItem | dict[str, object]] | NotGiven | None = NOT_GIVEN, skills: str | list[str] | NotGiven | None = NOT_GIVEN, languages: str | list[str] | NotGiven | None = NOT_GIVEN, exclude_languages: str | list[str] | NotGiven | None = NOT_GIVEN, keywords: str | NotGiven | None = NOT_GIVEN, changed_jobs_within_days: int | NotGiven | None = NOT_GIVEN, location: models.CountPeopleParamsLocation | dict[str, object] | NotGiven | None = NOT_GIVEN, max_data_age_date: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.SearchPeopleCountData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `first_name` | `str \| NotGiven \| None` | Optional. Filter by first name | | `last_name` | `str \| NotGiven \| None` | Optional. Filter by last name | | `headline` | `str \| NotGiven \| None` | Optional. Match keywords found in the profile headline | | `is_open_to_work` | `bool \| NotGiven \| None` | Optional. Restrict to profiles flagged open-to-work | | `is_currently_employed` | `bool \| NotGiven \| None` | Optional. Restrict to profiles that have a current employer | | `is_premium` | `bool \| NotGiven \| None` | Optional. Restrict to Social Premium members | | `is_verified` | `bool \| NotGiven \| None` | Optional. Restrict to profiles with a verification badge | | `followers_count` | `models.CountPeopleParamsFollowersCount \| dict[str, object] \| NotGiven \| None` | Optional. Follower count range, with inclusive `min` and `max` bounds. | | `connections_count` | `models.CountPeopleParamsConnectionsCount \| dict[str, object] \| NotGiven \| None` | Optional. Connection count range, bounds inclusive. One bound is enough: { "min": 501 } keeps profiles with more than 500 connections | | `position_title` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past position title, or array of titles (OR-matched) | | `exclude_position_title` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past position title, or array of titles, to exclude | | `company_name` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company name, or array of names (OR-matched) | | `exclude_company_name` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company name, or array of names, to exclude | | `company_industry` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company industry, from the same industry list | | `exclude_company_industry` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company industry, or array, to exclude | | `company_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company `com_...` id, or array of ids | | `exclude_company_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company `com_...` id, or array of ids, to exclude | | `company_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company Social identifier, or array of identifiers | | `exclude_company_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company Social identifier, or array, to exclude | | `company_public_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company public ID or slug, or array | | `exclude_company_public_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current or past company public ID or slug, or array, to exclude | | `current_position_title` | `str \| list[str] \| NotGiven \| None` | Optional. Current position title, or array of titles: a person matches if their current title matches any of them | | `exclude_current_position_title` | `str \| list[str] \| NotGiven \| None` | Optional. Current position title, or array of titles, to exclude from results | | `current_company_name` | `str \| NotGiven \| None` | Optional. Match the current employer name | | `exclude_current_company_name` | `str \| NotGiven \| None` | Optional. Exclude the current employer name | | `current_company_id` | `str \| list[str] \| NotGiven \| None` | Optional. Reverse Contact internal id (`com_...`) of the current employer | | `exclude_current_company_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer `com_...` id, or array of ids, to exclude | | `current_company_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer Social identifier, or array of identifiers | | `exclude_current_company_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer Social identifier, or array, to exclude | | `current_company_public_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer public ID or slug, or array | | `exclude_current_company_public_id` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer public ID or slug, or array, to exclude | | `current_company_industry` | `str \| list[str] \| NotGiven \| None` | Optional. Current employer industry. See [Industry values](https://docs.reversecontact.com/reference/industries) for the full list | | `current_company_employee_count_range` | `models.CountPeopleParamsCurrentCompanyEmployeeCountRange \| dict[str, object] \| NotGiven \| None` | Optional. Current employer size range, with inclusive `min` and `max` bounds. | | `current_function` | `str \| list[str] \| NotGiven \| None` | Optional. Current function from the [position taxonomy](https://docs.reversecontact.com/endpoints/search-people#position-taxonomy) | | `exclude_current_function` | `str \| list[str] \| NotGiven \| None` | Optional. Current function, or array of functions, to exclude | | `current_sub_function` | `str \| list[str] \| NotGiven \| None` | Optional. Current sub-function from the [position taxonomy](https://docs.reversecontact.com/endpoints/search-people#position-taxonomy) | | `exclude_current_sub_function` | `str \| list[str] \| NotGiven \| None` | Optional. Current sub-function, or array of sub-functions, to exclude | | `current_seniority` | `str \| list[str] \| NotGiven \| None` | Optional. Current seniority from the [position taxonomy](https://docs.reversecontact.com/endpoints/search-people#position-taxonomy) | | `exclude_current_seniority` | `str \| list[str] \| NotGiven \| None` | Optional. Current seniority, or array of seniorities, to exclude | | `current_contract_type` | `str \| list[str] \| NotGiven \| None` | Optional. Contract type of the current position, or array (OR-matched). See [Contract types](https://docs.reversecontact.com/endpoints/search-people#contract-types) | | `exclude_current_contract_type` | `str \| list[str] \| NotGiven \| None` | Optional. Contract type of the current position, or array, to exclude. Requires a positive filter alongside it | | `contract_type` | `str \| list[str] \| NotGiven \| None` | Optional. Contract type of any position, current or past, or array (OR-matched) | | `exclude_contract_type` | `str \| list[str] \| NotGiven \| None` | Optional. Contract type of any position, current or past, or array, to exclude. Requires a positive filter alongside it | | `school_name` | `str \| list[str] \| NotGiven \| None` | Optional. School name, or array of names: a person matches if they attended any of them. See [Schools](https://docs.reversecontact.com/endpoints/search-people#schools) | | `school_id` | `str \| list[str] \| NotGiven \| None` | Optional. Reverse Contact school id (`sch_...`), or array. See [Schools](https://docs.reversecontact.com/endpoints/search-people#schools) | | `school_linkedin_id` | `str \| list[str] \| NotGiven \| None` | Optional. School Social identifier, or array. See [Schools](https://docs.reversecontact.com/endpoints/search-people#schools) | | `total_experience_months` | `models.CountPeopleParamsTotalExperienceMonths \| dict[str, object] \| NotGiven \| None` | Optional. Total work experience in months, with inclusive `min` and `max` bounds. | | `experience` | `list[models.CountPeopleParamsExperienceItem \| dict[str, object]] \| NotGiven \| None` | Optional. Past-experience requirements. All entries are AND-combined | | `skills` | `str \| list[str] \| NotGiven \| None` | Optional. Skill name, or array of skill names: a person matches if they list any of them. See [Skills](https://docs.reversecontact.com/endpoints/search-people#skills) | | `languages` | `str \| list[str] \| NotGiven \| None` | Optional. Language name in English, or array: a person matches if they speak any of them. See [Languages](https://docs.reversecontact.com/endpoints/search-people#languages) | | `exclude_languages` | `str \| list[str] \| NotGiven \| None` | Optional. Language name in English, or array, to exclude. Requires a positive filter alongside it | | `keywords` | `str \| NotGiven \| None` | Optional. Words searched across the whole profile: every word must appear, in any field. See [Keywords](https://docs.reversecontact.com/endpoints/search-people#keywords) | | `changed_jobs_within_days` | `int \| NotGiven \| None` | Optional. `30`, `60`, `90` or `120`: the current position started within that many days. See [Recent job change](https://docs.reversecontact.com/endpoints/search-people#recent-job-change) | | `location` | `models.CountPeopleParamsLocation \| dict[str, object] \| NotGiven \| None` | Optional. Location filters such as `countryCode`, `area`, `region` and `city`. | | `max_data_age_date` | `str \| NotGiven \| None` | Optional. ISO 8601 datetime. Only returns profiles refreshed after this date | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.SearchPeopleCountData]`. `data.count` is the number of matching people. The call does not extract profiles. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.search.count_people(current_function="Marketing") print(result.data if result is not None else None) ``` ## people_auto_suggestion Call [People auto-suggestion](https://docs.reversecontact.com/endpoints/search-people-auto-suggestion) with `client.search.people_auto_suggestion()`. **Python SDK** ```python [Python SDK] def people_auto_suggestion( *, field: str | NotGiven | None = NOT_GIVEN, query: str | NotGiven | None = NOT_GIVEN, limit: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.SearchPeopleAutoSuggestionData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `field` | `str \| NotGiven \| None` | **Required.** The filter to complete. `skills` is the only value today | | `query` | `str \| NotGiven \| None` | **Required.** What has been typed so far, 1 to 100 characters. Case and accents are ignored | | `limit` | `int \| NotGiven \| None` | Optional. Maximum values returned, from 1 to 25. Defaults to 10 | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.SearchPeopleAutoSuggestionData]`. `data` contains the selected `field` and its `suggestions`. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.search.people_auto_suggestion(field="currentPositionTitle", query="market", limit=5) print(result.data if result is not None else None) ``` ## organizations Provide at least one company filter. Use `max_data_age_date` with another filter. When `followers_count` is the only filter, supply both `min` and `max`. Call [Company search](https://docs.reversecontact.com/endpoints/search-companies) with `client.search.organizations()`. **Python SDK** ```python [Python SDK] def organizations( *, company_name: str | NotGiven | None = NOT_GIVEN, company_domain: str | NotGiven | None = NOT_GIVEN, industry: str | list[str] | NotGiven | None = NOT_GIVEN, specialities: str | list[str] | NotGiven | None = NOT_GIVEN, employee_count_range: models.SearchOrganizationsParamsEmployeeCountRange | dict[str, object] | NotGiven | None = NOT_GIVEN, founded_year: models.SearchOrganizationsParamsFoundedYear | dict[str, object] | NotGiven | None = NOT_GIVEN, followers_count: models.SearchOrganizationsParamsFollowersCount | dict[str, object] | NotGiven | None = NOT_GIVEN, location: models.SearchOrganizationsParamsLocation | dict[str, object] | NotGiven | None = NOT_GIVEN, max_data_age_date: str | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, per_page: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Page[models.CompanyFull]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `company_name` | `str \| NotGiven \| None` | Optional. Filter by company name | | `company_domain` | `str \| NotGiven \| None` | Optional. Filter by website domain (e.g. `acme.com`). Disposable domains are rejected with `422` | | `industry` | `str \| list[str] \| NotGiven \| None` | Optional. Industry, or array of industries: a company matches if its industry is any of them. See [Industry values](https://docs.reversecontact.com/reference/industries) for the full list | | `specialities` | `str \| list[str] \| NotGiven \| None` | Optional. Speciality, or array of specialities: a company matches if it lists any of them. See [Specialities](https://docs.reversecontact.com/endpoints/search-companies#specialities) | | `employee_count_range` | `models.SearchOrganizationsParamsEmployeeCountRange \| dict[str, object] \| NotGiven \| None` | Optional. Company size range, with inclusive `min` and `max` bounds. | | `founded_year` | `models.SearchOrganizationsParamsFoundedYear \| dict[str, object] \| NotGiven \| None` | Optional. Founding year range, with inclusive `min` and `max` bounds. | | `followers_count` | `models.SearchOrganizationsParamsFollowersCount \| dict[str, object] \| NotGiven \| None` | Optional. Follower count range, with inclusive `min` and `max` bounds. | | `location` | `models.SearchOrganizationsParamsLocation \| dict[str, object] \| NotGiven \| None` | Optional. Headquarters location. Each property accepts one value or an OR-matched list | | `max_data_age_date` | `str \| NotGiven \| None` | Optional. ISO 8601 datetime. Only returns companies refreshed after this date. Needs at least one other filter | | `cursor` | `str \| NotGiven \| None` | Optional. Opaque cursor returned as `metadata.nextCursor`. Use this cursor to request the next page | | `per_page` | `int \| NotGiven \| None` | Optional. Companies per page (1 to 100, default `25`) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Page[models.CompanyFull]`. A page with records in `data`, a `next_cursor`, `has_next_page()` and `get_next_page()`. The next-page method reuses the filters and returns `None` after the last page. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: page = client.search.organizations(industry="Software Development", per_page=10) for item in page.data: if item is not None: print(item.to_dict()) ``` ## iter_organizations Provide at least one company filter. Use `max_data_age_date` with another filter. When `followers_count` is the only filter, supply both `min` and `max`. Call [Company search](https://docs.reversecontact.com/endpoints/search-companies) with `client.search.iter_organizations()`. **Python SDK** ```python [Python SDK] def iter_organizations( *, company_name: str | NotGiven | None = NOT_GIVEN, company_domain: str | NotGiven | None = NOT_GIVEN, industry: str | list[str] | NotGiven | None = NOT_GIVEN, specialities: str | list[str] | NotGiven | None = NOT_GIVEN, employee_count_range: models.SearchOrganizationsParamsEmployeeCountRange | dict[str, object] | NotGiven | None = NOT_GIVEN, founded_year: models.SearchOrganizationsParamsFoundedYear | dict[str, object] | NotGiven | None = NOT_GIVEN, followers_count: models.SearchOrganizationsParamsFollowersCount | dict[str, object] | NotGiven | None = NOT_GIVEN, location: models.SearchOrganizationsParamsLocation | dict[str, object] | NotGiven | None = NOT_GIVEN, max_data_age_date: str | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, per_page: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, max_results: int, **extra: object, ) -> Iterator[models.CompanyFull | None]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `company_name` | `str \| NotGiven \| None` | Optional. Filter by company name | | `company_domain` | `str \| NotGiven \| None` | Optional. Filter by website domain (e.g. `acme.com`). Disposable domains are rejected with `422` | | `industry` | `str \| list[str] \| NotGiven \| None` | Optional. Industry, or array of industries: a company matches if its industry is any of them. See [Industry values](https://docs.reversecontact.com/reference/industries) for the full list | | `specialities` | `str \| list[str] \| NotGiven \| None` | Optional. Speciality, or array of specialities: a company matches if it lists any of them. See [Specialities](https://docs.reversecontact.com/endpoints/search-companies#specialities) | | `employee_count_range` | `models.SearchOrganizationsParamsEmployeeCountRange \| dict[str, object] \| NotGiven \| None` | Optional. Company size range, with inclusive `min` and `max` bounds. | | `founded_year` | `models.SearchOrganizationsParamsFoundedYear \| dict[str, object] \| NotGiven \| None` | Optional. Founding year range, with inclusive `min` and `max` bounds. | | `followers_count` | `models.SearchOrganizationsParamsFollowersCount \| dict[str, object] \| NotGiven \| None` | Optional. Follower count range, with inclusive `min` and `max` bounds. | | `location` | `models.SearchOrganizationsParamsLocation \| dict[str, object] \| NotGiven \| None` | Optional. Headquarters location. Each property accepts one value or an OR-matched list | | `max_data_age_date` | `str \| NotGiven \| None` | Optional. ISO 8601 datetime. Only returns companies refreshed after this date. Needs at least one other filter | | `cursor` | `str \| NotGiven \| None` | Optional. Opaque cursor returned as `metadata.nextCursor`. Use this cursor to request the next page | | `per_page` | `int \| NotGiven \| None` | Optional. Companies per page (1 to 100, default `25`) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `max_results` | `int` | **Required.** Maximum number of paid results to request. Must be a non-negative integer. `0` makes no request. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Iterator[models.CompanyFull | None]`. An iterator of typed records. Entries can be `None`. The iterator fetches pages only as you advance. `max_results` caps paid results, not prompt page fees. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options, invalid `max_results` or `per_page`, or a repeated pagination cursor. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: for item in client.search.iter_organizations(industry="Software Development", max_results=10): if item is not None: print(item.to_dict()) ``` ## organizations_by_prompt Call [Agentic Company Search](https://docs.reversecontact.com/endpoints/search-companies-prompt) with `client.search.organizations_by_prompt()`. **Python SDK** ```python [Python SDK] def organizations_by_prompt( *, prompt: str | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, per_page: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Page[models.CompanyFull]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `prompt` | `str \| NotGiven \| None` | **Required.** Plain-language description of the companies to find, in any language. At least 3 characters and at most 1000 tokens | | `cursor` | `str \| NotGiven \| None` | Optional. Opaque cursor returned as `metadata.nextCursor`. Use this cursor to request the next page with the same `prompt` | | `per_page` | `int \| NotGiven \| None` | Optional. Companies per page (1 to 100, default `25`) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Page[models.CompanyFull]`. A page with records in `data`, a `next_cursor`, `has_next_page()` and `get_next_page()`. The next-page method reuses the filters and returns `None` after the last page. `interpretation` contains the filters inferred from the prompt. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: page = client.search.organizations_by_prompt(prompt="Software companies in France", per_page=10) for item in page.data: if item is not None: print(item.to_dict()) ``` ## iter_organizations_by_prompt Call [Agentic Company Search](https://docs.reversecontact.com/endpoints/search-companies-prompt) with `client.search.iter_organizations_by_prompt()`. **Python SDK** ```python [Python SDK] def iter_organizations_by_prompt( *, prompt: str | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, per_page: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, max_results: int, **extra: object, ) -> Iterator[models.CompanyFull | None]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `prompt` | `str \| NotGiven \| None` | **Required.** Plain-language description of the companies to find, in any language. At least 3 characters and at most 1000 tokens | | `cursor` | `str \| NotGiven \| None` | Optional. Opaque cursor returned as `metadata.nextCursor`. Use this cursor to request the next page with the same `prompt` | | `per_page` | `int \| NotGiven \| None` | Optional. Companies per page (1 to 100, default `25`) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `max_results` | `int` | **Required.** Maximum number of paid results to request. Must be a non-negative integer. `0` makes no request. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Iterator[models.CompanyFull | None]`. An iterator of typed records. Entries can be `None`. The iterator fetches pages only as you advance. `max_results` caps paid results, not prompt page fees. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options, invalid `max_results` or `per_page`, or a repeated pagination cursor. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: for item in client.search.iter_organizations_by_prompt(prompt="Software companies in France", max_results=10): if item is not None: print(item.to_dict()) ``` ## count_organizations Provide at least one company filter. Use `max_data_age_date` with another filter. When `followers_count` is the only filter, supply both `min` and `max`. Call [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count) with `client.search.count_organizations()`. **Python SDK** ```python [Python SDK] def count_organizations( *, company_name: str | NotGiven | None = NOT_GIVEN, company_domain: str | NotGiven | None = NOT_GIVEN, industry: str | list[str] | NotGiven | None = NOT_GIVEN, specialities: str | list[str] | NotGiven | None = NOT_GIVEN, employee_count_range: models.CountOrganizationsParamsEmployeeCountRange | dict[str, object] | NotGiven | None = NOT_GIVEN, founded_year: models.CountOrganizationsParamsFoundedYear | dict[str, object] | NotGiven | None = NOT_GIVEN, followers_count: models.CountOrganizationsParamsFollowersCount | dict[str, object] | NotGiven | None = NOT_GIVEN, location: models.CountOrganizationsParamsLocation | dict[str, object] | NotGiven | None = NOT_GIVEN, max_data_age_date: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.SearchOrganizationsCountData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `company_name` | `str \| NotGiven \| None` | Optional. Filter by company name | | `company_domain` | `str \| NotGiven \| None` | Optional. Filter by website domain (e.g. `acme.com`). Disposable domains are rejected with `422` | | `industry` | `str \| list[str] \| NotGiven \| None` | Optional. Industry, or array of industries: a company matches if its industry is any of them. See [Industry values](https://docs.reversecontact.com/reference/industries) for the full list | | `specialities` | `str \| list[str] \| NotGiven \| None` | Optional. Speciality, or array of specialities: a company matches if it lists any of them. See [Specialities](https://docs.reversecontact.com/endpoints/search-companies#specialities) | | `employee_count_range` | `models.CountOrganizationsParamsEmployeeCountRange \| dict[str, object] \| NotGiven \| None` | Optional. Company size range, with inclusive `min` and `max` bounds. | | `founded_year` | `models.CountOrganizationsParamsFoundedYear \| dict[str, object] \| NotGiven \| None` | Optional. Founding year range, with inclusive `min` and `max` bounds. | | `followers_count` | `models.CountOrganizationsParamsFollowersCount \| dict[str, object] \| NotGiven \| None` | Optional. Follower count range, with inclusive `min` and `max` bounds. | | `location` | `models.CountOrganizationsParamsLocation \| dict[str, object] \| NotGiven \| None` | Optional. Headquarters location. Each property accepts one value or an OR-matched list | | `max_data_age_date` | `str \| NotGiven \| None` | Optional. ISO 8601 datetime. Only returns companies refreshed after this date. Needs at least one other filter | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.SearchOrganizationsCountData]`. `data.count` is the number of matching companies. The call does not extract companies. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.search.count_organizations(industry="Software Development") print(result.data if result is not None else None) ``` ## people_fields Call [People search available fields](https://docs.reversecontact.com/endpoints/search-people-available-fields) with `client.search.people_fields()`. **Python SDK** ```python [Python SDK] def people_fields( *, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.SearchFieldsData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.SearchFieldsData]`. `data.fields` describes supported people filters and allowed values. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.search.people_fields() print(result.data if result is not None else None) ``` ## organizations_fields Call [Company search available fields](https://docs.reversecontact.com/endpoints/search-companies-available-fields) with `client.search.organizations_fields()`. **Python SDK** ```python [Python SDK] def organizations_fields( *, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.SearchFieldsData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.SearchFieldsData]`. `data.fields` describes supported company filters and allowed values. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.search.organizations_fields() print(result.data if result is not None else None) ``` ## Related guides See the [Python SDK overview](https://docs.reversecontact.com/sdk/python), [Search budget](https://docs.reversecontact.com/sdk/python/search-budget) and [Filter constants](https://docs.reversecontact.com/sdk/python/filter-constants). ## FAQ ### Which Python search methods require max_results? `iter_people`, `iter_people_by_prompt`, `iter_people_lookalike`, `iter_organizations` and `iter_organizations_by_prompt` require a non-negative `max_results`. ### How do I count people without paying for results? Call `client.search.count_people()` with the search filters. The count is in `result.data.count`, and the call is free. ### What does a Python search page return? A search `Page` exposes `data`, `next_cursor`, `count`, `quotas`, `has_next_page()` and `get_next_page()`. Prompt pages also expose `interpretation`. ### How do I list Python SDK search filters? Call the free `client.search.people_fields()` or `client.search.organizations_fields()` methods and inspect `result.data.fields`. --- # client.contact > Find email addresses and phone numbers or verify an email with the Python SDK, then wait for typed job results with safe idempotent retries. - Page: https://docs.reversecontact.com/sdk/python/contact - Last verified: 2026-11-04 Find email addresses and phone numbers or verify an email. Each method returns a job handle. | Method | Endpoint | Cost | | ------ | -------- | ---- | | [`find_email()`](#find_email) | [POST /v2/contact/email](https://docs.reversecontact.com/endpoints/contact-email) | 3 credits when a professional email is found, 5 for a personal one, regardless of emailType. An any lookup costs 3, 5 or 8 (8 when it finds both). A lookup that finds none is free. | | [`verify_email()`](#verify_email) | [POST /v2/contact/email/verify](https://docs.reversecontact.com/endpoints/contact-email-verify) | 1 credit per verdict, whatever it says. A check that fails is refunded. | | [`find_phone()`](#find_phone) | [POST /v2/contact/phone](https://docs.reversecontact.com/endpoints/contact-phone) | 20 credits when a phone number is found. A lookup that finds none is free. | ## Calling conventions Examples assume `RC_API_KEY` is set. Replace example ids with ids returned by your own requests. Signatures show the published 0.3.0 synchronous methods. Use the same arguments with the [Python async client](https://docs.reversecontact.com/sdk/python/async-client). `models` refers to `reversecontact.types`. `NOT_GIVEN` and `NotGiven` are the SDK's internal markers for omitted arguments. Leave these arguments out of your calls. For API body parameters, passing `None` sends an explicit null. Transport options use `None` to inherit their defaults. `Timeout` means a number of seconds or `httpx.Timeout`. Supply parameters marked **Required** to Python or to the API, even when the generated signature permits omission. JSON response objects expose `data`, `quotas`, `metadata` and `http_response`. Check optional fields for `None`. See [First call](https://docs.reversecontact.com/sdk/python/first-call) for the response envelope. CSV methods return text and iterators yield items. HTTP failures raise an `APIError` subclass based on the response: `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` or `InternalServerError`. See the [error mapping and retry rules](https://docs.reversecontact.com/sdk/python/errors-and-retries). All three methods generate an `Idempotency-Key` when omitted and reuse that key on automatic retries. See [Safe retries](https://docs.reversecontact.com/sdk/python/errors-and-retries#safe-retries-on-find-email-and-find-phone). ## find_email Supply `url`, `id`, `full_name` with `company_domain`, or `first_name` and `last_name` with `company_domain`. The API defaults `email_type` to `professional`. **Python SDK** ```python [Python SDK] def find_email( *, url: str | NotGiven | None = NOT_GIVEN, id: str | NotGiven | None = NOT_GIVEN, full_name: str | NotGiven | None = NOT_GIVEN, first_name: str | NotGiven | None = NOT_GIVEN, last_name: str | NotGiven | None = NOT_GIVEN, company_domain: str | NotGiven | None = NOT_GIVEN, email_type: str | NotGiven | None = NOT_GIVEN, webhook_url: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, idempotency_key: str | None = None, **extra: object, ) -> Job[list[ContactValue | None]]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `url` | `str \| NotGiven \| None` | **Required for the selected strategy.** Public Social profile URL, such as `https://www.social.com/in/janedoe`. Use this for the URL strategy. | | `id` | `str \| NotGiven \| None` | **Required for the selected strategy.** Reverse Contact person id from a previous response, such as `prs_01jbd5vewyenebxgaz869ffe5t`. Use this for the id strategy. | | `full_name` | `str \| NotGiven \| None` | **Required for the selected strategy.** Person's full name, such as `Jane Doe`. Use this with `company_domain`. | | `first_name` | `str \| NotGiven \| None` | **Required for the selected strategy.** Person's first name. Use this with `last_name` and `company_domain` instead of `full_name`. | | `last_name` | `str \| NotGiven \| None` | **Required for the selected strategy.** Person's last name. Use this with `first_name` and `company_domain` instead of `full_name`. | | `company_domain` | `str \| NotGiven \| None` | **Required for the selected strategy.** Company domain, such as `acme.com`. Required for the name-based strategies. | | `email_type` | `str \| NotGiven \| None` | Optional. Kind of email to find: `professional` (default), `personal`, or `any` to accept either. Omit it to find professional emails. | | `webhook_url` | `str \| NotGiven \| None` | Optional. HTTPS URL that receives the callback. Omit it to retrieve the result by polling instead. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `idempotency_key` | `str \| None` | Optional. Request-specific key, 1 to 255 printable ASCII characters without spaces. The SDK generates a key when omitted and reuses it on retries. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Job[list[ContactValue | None]]`. A job acknowledgement with `id`, `data`, `quotas`, `metadata`, `http_response` and `replayed`. Save `job.id` before waiting. Call `job.wait()` to get `Result[JobSnapshot[T]]` with the final value in `data.result`. The final value is a list of `ContactValue` models with `value` and `type`. Entries can be `None`. See [Async jobs](https://docs.reversecontact.com/sdk/python/async-jobs) for details. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options or a missing job id in the acknowledgement. Invalid `idempotency_key` values raise `ReverseContactError` before the request. `wait()` raises `JobFailedError` for an errored job and `JobTimeoutError` when its overall deadline expires. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: job = client.contact.find_email(url="https://social.com/in/janedoe") print("Save this job id:", job.id) done = job.wait() print(done.data.result if done.data else None) ``` ## verify_email Provide the required `email` to start a verification job. Call `wait()` to get the verdict. **Python SDK** ```python [Python SDK] def verify_email( *, email: str | NotGiven | None = NOT_GIVEN, webhook_url: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, idempotency_key: str | None = None, **extra: object, ) -> Job[models.ContactEmailVerifyResult]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `email` | `str \| NotGiven \| None` | **Required.** The email address to check, such as `jane@acme.com`. | | `webhook_url` | `str \| NotGiven \| None` | Optional. HTTPS URL that receives the result, including error outcomes. Omit it to poll for the verdict instead. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `idempotency_key` | `str \| None` | Optional. Request-specific key, 1 to 255 printable ASCII characters without spaces. The SDK generates a key when omitted and reuses it on retries. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Job[models.ContactEmailVerifyResult]`. A job acknowledgement with `id`, `data`, `quotas`, `metadata`, `http_response` and `replayed`. Save `job.id` before waiting. Call `job.wait()` to get `Result[JobSnapshot[T]]` with the final value in `data.result`. The final value contains `email` and `status`. See [Async jobs](https://docs.reversecontact.com/sdk/python/async-jobs) for details. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options or a missing job id in the acknowledgement. Invalid `idempotency_key` values raise `ReverseContactError` before the request. `wait()` raises `JobFailedError` for an errored job and `JobTimeoutError` when its overall deadline expires. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: job = client.contact.verify_email(email="jane@acme.com") print("Save this job id:", job.id) done = job.wait() print(done.data.result if done.data else None) ``` ## find_phone Supply `url`, `id`, `full_name` with `company_domain`, or `first_name` and `last_name` with `company_domain`. **Python SDK** ```python [Python SDK] def find_phone( *, url: str | NotGiven | None = NOT_GIVEN, id: str | NotGiven | None = NOT_GIVEN, full_name: str | NotGiven | None = NOT_GIVEN, first_name: str | NotGiven | None = NOT_GIVEN, last_name: str | NotGiven | None = NOT_GIVEN, company_domain: str | NotGiven | None = NOT_GIVEN, webhook_url: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, idempotency_key: str | None = None, **extra: object, ) -> Job[list[ContactValue | None]]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `url` | `str \| NotGiven \| None` | **Required for the selected strategy.** Public Social profile URL, such as `https://www.social.com/in/janedoe`. Use this for the URL strategy. | | `id` | `str \| NotGiven \| None` | **Required for the selected strategy.** Reverse Contact person id from a previous response, such as `prs_01jbd5vewyenebxgaz869ffe5t`. Use this for the id strategy. | | `full_name` | `str \| NotGiven \| None` | **Required for the selected strategy.** Person's full name, such as `Jane Doe`. Use this with `company_domain`. | | `first_name` | `str \| NotGiven \| None` | **Required for the selected strategy.** Person's first name. Use this with `last_name` and `company_domain` instead of `full_name`. | | `last_name` | `str \| NotGiven \| None` | **Required for the selected strategy.** Person's last name. Use this with `first_name` and `company_domain` instead of `full_name`. | | `company_domain` | `str \| NotGiven \| None` | **Required for the selected strategy.** Company domain, such as `acme.com`. Required for the name-based strategies. | | `webhook_url` | `str \| NotGiven \| None` | Optional. HTTPS URL that receives the callback. Omit it to retrieve the result by polling instead. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `idempotency_key` | `str \| None` | Optional. Request-specific key, 1 to 255 printable ASCII characters without spaces. The SDK generates a key when omitted and reuses it on retries. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Job[list[ContactValue | None]]`. A job acknowledgement with `id`, `data`, `quotas`, `metadata`, `http_response` and `replayed`. Save `job.id` before waiting. Call `job.wait()` to get `Result[JobSnapshot[T]]` with the final value in `data.result`. The final value is a list of `ContactValue` models with `value` and `type`. Entries can be `None`. See [Async jobs](https://docs.reversecontact.com/sdk/python/async-jobs) for details. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options or a missing job id in the acknowledgement. Invalid `idempotency_key` values raise `ReverseContactError` before the request. `wait()` raises `JobFailedError` for an errored job and `JobTimeoutError` when its overall deadline expires. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: job = client.contact.find_phone(url="https://social.com/in/janedoe") print("Save this job id:", job.id) done = job.wait() print(done.data.result if done.data else None) ``` ## Related guides See the [Python SDK overview](https://docs.reversecontact.com/sdk/python), [Async jobs](https://docs.reversecontact.com/sdk/python/async-jobs) and [Errors and retries](https://docs.reversecontact.com/sdk/python/errors-and-retries). ## FAQ ### Does find_email return an email immediately? `client.contact.find_email()` returns a `Job`. Call `job.wait()` and read `done.data.result` for the list of contact values. ### What does verify_email return in Python? `client.contact.verify_email()` returns a job whose final result has `email` and `status`. Read that result after `job.wait()`. ### How do I avoid duplicate contact jobs on retry? Contact methods generate an idempotency key for each call and reuse that key on automatic retries. Supply a stored `idempotency_key` to safely retry the same body after a restart. --- # client.lists > Manage Lists, members and import operations with the Python SDK, including method signatures, parameters, return values, credit costs and errors. - Page: https://docs.reversecontact.com/sdk/python/lists - Last verified: 2026-11-04 Manage persistent Lists, their members and their import operations. | Method | Endpoint | Cost | | ------ | -------- | ---- | | [`list()`](#list) | [GET /v2/lists](https://docs.reversecontact.com/endpoints/lists) | Free. | | [`list_all()`](#list_all) | [GET /v2/lists](https://docs.reversecontact.com/endpoints/lists) | Free. | | [`create()`](#create) | [POST /v2/lists](https://docs.reversecontact.com/endpoints/list-create) | Free. | | [`retrieve()`](#retrieve) | [GET /v2/lists/{listId}](https://docs.reversecontact.com/endpoints/list) | Free. | | [`delete()`](#delete) | [DELETE /v2/lists/{listId}](https://docs.reversecontact.com/endpoints/list-delete) | Free. | | [`import_signals()`](#import_signals) | [POST /v2/lists/{listId}/intake/signals](https://docs.reversecontact.com/endpoints/list-intake-signals) | Free to queue. Each new member costs its identity price. Each data family you request costs its endpoint price, up to your maxCredits. | | [`export()`](#export) | [GET /v2/lists/{listId}/export](https://docs.reversecontact.com/endpoints/list-export) | Free. | | [`members.list()`](#memberslist) | [GET /v2/lists/{listId}/members](https://docs.reversecontact.com/endpoints/list-members) | Free. | | [`members.list_all()`](#memberslist_all) | [GET /v2/lists/{listId}/members](https://docs.reversecontact.com/endpoints/list-members) | Free. | | [`members.add()`](#membersadd) | [POST /v2/lists/{listId}/members](https://docs.reversecontact.com/endpoints/list-members-add) | Free to call. Each new person costs 1 credit from a Social profile URL (2 with live), 2 from an email or a name. Each new company costs 1 credit from a company page URL or a company id (2 when the page is read live), 2 from a domain. A member already on the List, or a Signal with no match, is free. | | [`members.remove()`](#membersremove) | [DELETE /v2/lists/{listId}/members](https://docs.reversecontact.com/endpoints/list-members-remove) | Free. | | [`operations.retrieve()`](#operationsretrieve) | [GET /v2/lists/{listId}/operations/{operationId}/progress](https://docs.reversecontact.com/endpoints/list-operation) | Free, even at a zero balance. Never counts toward rate limits. | | [`operations.cancel()`](#operationscancel) | [POST /v2/lists/{listId}/operations/{operationId}/cancel](https://docs.reversecontact.com/endpoints/list-operation-cancel) | Free. Lines that had not started are never charged. | | [`operations.wait()`](#operationswait) | [GET /v2/lists/{listId}/operations/{operationId}/progress](https://docs.reversecontact.com/endpoints/list-operation) | Free, even at a zero balance. Never counts toward rate limits. | ## Calling conventions Examples assume `RC_API_KEY` is set. Replace example ids with ids returned by your own requests. Signatures show the published 0.3.0 synchronous methods. Use the same arguments with the [Python async client](https://docs.reversecontact.com/sdk/python/async-client). `models` refers to `reversecontact.types`. `NOT_GIVEN` and `NotGiven` are the SDK's internal markers for omitted arguments. Leave these arguments out of your calls. For API body parameters, passing `None` sends an explicit null. Transport options use `None` to inherit their defaults. `Timeout` means a number of seconds or `httpx.Timeout`. Supply parameters marked **Required** to Python or to the API, even when the generated signature permits omission. JSON response objects expose `data`, `quotas`, `metadata` and `http_response`. Check optional fields for `None`. See [First call](https://docs.reversecontact.com/sdk/python/first-call) for the response envelope. CSV methods return text and iterators yield items. HTTP failures raise an `APIError` subclass based on the response: `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` or `InternalServerError`. See the [error mapping and retry rules](https://docs.reversecontact.com/sdk/python/errors-and-retries). ## list Call [All Lists](https://docs.reversecontact.com/endpoints/lists) with `client.lists.list()`. **Python SDK** ```python [Python SDK] def list( *, per_page: int | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Page[models.ListListsItem]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `per_page` | `int \| NotGiven \| None` | Optional. Lists per page, from 1 to 200. Defaults to 200. | | `cursor` | `str \| NotGiven \| None` | Optional. The `nextCursor` of the previous page. A malformed cursor, or one sent with other parameters, returns `400`. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Page[models.ListListsItem]`. A page with records in `data`, a `next_cursor`, `has_next_page()` and `get_next_page()`. The next-page method reuses the filters and returns `None` after the last page. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: page = client.lists.list(per_page=20) for item in page.data: if item is not None: print(item.to_dict()) ``` ## list_all Call [All Lists](https://docs.reversecontact.com/endpoints/lists) with `client.lists.list_all()`. **Python SDK** ```python [Python SDK] def list_all( *, per_page: int | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, max_results: int | None = None, **extra: object, ) -> Iterator[models.ListListsItem | None]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `per_page` | `int \| NotGiven \| None` | Optional. Lists per page, from 1 to 200. Defaults to 200. | | `cursor` | `str \| NotGiven \| None` | Optional. The `nextCursor` of the previous page. A malformed cursor, or one sent with other parameters, returns `400`. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `max_results` | `int \| None` | Optional. Maximum items yielded. `None` reads every page. `0` makes no request. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Iterator[models.ListListsItem | None]`. An iterator of typed records. Entries can be `None`. The iterator fetches pages only as you advance. Pagination is free. Omit `max_results` to read all pages. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options, invalid `max_results` or `per_page`, or a repeated pagination cursor. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: for item in client.lists.list_all(max_results=20): if item is not None: print(item.to_dict()) ``` ## create Call [Create a List](https://docs.reversecontact.com/endpoints/list-create) with `client.lists.create()`. **Python SDK** ```python [Python SDK] def create( *, name: str | NotGiven | None = NOT_GIVEN, entity: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, idempotency_key: str | None = None, **extra: object, ) -> Result[models.CreateListData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `name` | `str \| NotGiven \| None` | **Required.** List name, 1 to 200 characters. Spaces around it are removed. | | `entity` | `str \| NotGiven \| None` | **Required.** `persons` for people, `companies` for companies | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `idempotency_key` | `str \| None` | Optional. Key for this creation request. The SDK generates a key when omitted and reuses it on automatic retries. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.CreateListData]`. `data` contains the new List, including `id`, `name`, `entity` and member counts. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.lists.create(name="Prospects", entity="persons") print(result.data if result is not None else None) ``` ## retrieve Call [Get a List](https://docs.reversecontact.com/endpoints/list) with `client.lists.retrieve()`. **Python SDK** ```python [Python SDK] def retrieve( list_id: str, *, per_page: int | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, kind: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.GetListData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `list_id` | `str` | **Required.** List id (UUID) | | `per_page` | `int \| NotGiven \| None` | Optional. Rows per page, from 1 to 1,000. Defaults to 100. | | `cursor` | `str \| NotGiven \| None` | Optional. The `nextCursor` of the previous page, sent with the same `kind`. A malformed cursor, or one sent with other parameters, returns `400`. | | `kind` | `str \| NotGiven \| None` | Optional. `all` (default) mixes members and Pending Intake by added date, `members` or `pending` returns one kind only | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.GetListData]`. `data.list` contains List details. `data.members` contains one member page, and `data.next_cursor` holds its continuation cursor. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.lists.retrieve("00000000-0000-4000-8000-000000000001") print(result.data if result is not None else None) ``` ## delete Call [Delete a List](https://docs.reversecontact.com/endpoints/list-delete) with `client.lists.delete()`. **Python SDK** ```python [Python SDK] def delete( list_id: str, *, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.DeleteListData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `list_id` | `str` | **Required.** Id (UUID) of the List to delete | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.DeleteListData]`. `data.deleted` indicates the deletion outcome. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.lists.delete("00000000-0000-4000-8000-000000000001") print(result.data if result is not None else None) ``` ## import_signals Send `signals` or `source_url` according to the [import request rules](https://docs.reversecontact.com/endpoints/list-intake-signals). Save `operation_id` to resume a paused import. **Python SDK** ```python [Python SDK] def import_signals( list_id: str, *, signals: list[models.ImportListSignalsParamsSignalsItem | dict[str, object]] | NotGiven | None = NOT_GIVEN, source_url: str | NotGiven | None = NOT_GIVEN, live: bool | NotGiven | None = NOT_GIVEN, families: list[str] | NotGiven | None = NOT_GIVEN, max_credits: int | NotGiven | None = NOT_GIVEN, operation_id: str | NotGiven | None = NOT_GIVEN, webhook_url: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.ImportListSignalsData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `list_id` | `str` | **Required.** List id (UUID). | | `signals` | `list[models.ImportListSignalsParamsSignalsItem \| dict[str, object]] \| NotGiven \| None` | **Required for the selected strategy.** 1 to 25,000 Signals, with the fields of [Add members](https://docs.reversecontact.com/endpoints/list-members-add#request-body), `externalId` included | | `source_url` | `str \| NotGiven \| None` | **Required for the selected strategy.** A public HTTPS CSV file or Google Sheet URL instead of `signals`: up to 25,000 rows and 25 MiB. Columns are matched from their headers (Social URL, email, first name, last name, company, domain). | | `live` | `bool \| NotGiven \| None` | Optional. `true` fetches the profile of each Social profile URL live. Omitted or `false`, saved data is used. No effect on a companies List. | | `families` | `list[str] \| NotGiven \| None` | Optional. Data to buy for each member: `profile`, `company`, `contact_email`, `contact_phone`. A companies List takes `company` only. | | `max_credits` | `int \| NotGiven \| None` | **Required for the selected strategy.** The most this operation may spend. Required with `families` when the worst case is above 25 credits. Omitted, the ceiling is the worst case of the import. | | `operation_id` | `str \| NotGiven \| None` | Optional. Identifier for this import. The SDK generates a UUID when omitted. Reuse a stored id to resume the import. | | `webhook_url` | `str \| NotGiven \| None` | Optional. Public HTTPS URL that receives a notification whenever the operation stops progressing. See [Webhook callback](https://docs.reversecontact.com/endpoints/list-intake-signals#webhook-callback) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.ImportListSignalsData]`. `data.operation_id` identifies the import. The acknowledgement also exposes `poll_url`, `total_rows`, `duplicate_count` and `job`. Follow the import with `client.lists.operations.wait(list_id, operation_id)`. See [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/python/lists-and-bulk). **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.lists.import_signals( "00000000-0000-4000-8000-000000000001", signals=[{"email": "jane@acme.com", "externalId": "crm-001"}], families=["contact_email"], max_credits=100, ) print(result.data if result is not None else None) ``` ## export Call [Export a List](https://docs.reversecontact.com/endpoints/list-export) with `client.lists.export()`. **Python SDK** ```python [Python SDK] def export( list_id: str, *, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> str: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `list_id` | `str` | **Required.** List id (UUID) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `str`. The List export as CSV text, ready to write to a UTF-8 file. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: csv_text = client.lists.export("00000000-0000-4000-8000-000000000001") print(csv_text) ``` ## members.list Call [List members](https://docs.reversecontact.com/endpoints/list-members) with `client.lists.members.list()`. **Python SDK** ```python [Python SDK] def list( list_id: str, *, per_page: int | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, kind: str | NotGiven | None = NOT_GIVEN, external_id: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Page[models.ListListMembersItem]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `list_id` | `str` | **Required.** List id (UUID) | | `per_page` | `int \| NotGiven \| None` | Optional. Rows per page, from 1 to 1,000. Defaults to 100. | | `cursor` | `str \| NotGiven \| None` | Optional. The `nextCursor` of the previous page, sent with the same filters. A malformed cursor, or one sent with other filters, returns `400`. | | `kind` | `str \| NotGiven \| None` | Optional. `all` (default) mixes members and Pending Intake by added date, `members` or `pending` returns one kind only | | `external_id` | `str \| NotGiven \| None` | Optional. Returns members with this identifier from your system (1 to 200 characters) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Page[models.ListListMembersItem]`. A page with records in `data`, a `next_cursor`, `has_next_page()` and `get_next_page()`. The next-page method reuses the filters and returns `None` after the last page. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: page = client.lists.members.list("00000000-0000-4000-8000-000000000001", per_page=20) for item in page.data: if item is not None: print(item.to_dict()) ``` ## members.list_all Call [List members](https://docs.reversecontact.com/endpoints/list-members) with `client.lists.members.list_all()`. **Python SDK** ```python [Python SDK] def list_all( list_id: str, *, per_page: int | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, kind: str | NotGiven | None = NOT_GIVEN, external_id: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, max_results: int | None = None, **extra: object, ) -> Iterator[models.ListListMembersItem | None]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `list_id` | `str` | **Required.** List id (UUID) | | `per_page` | `int \| NotGiven \| None` | Optional. Rows per page, from 1 to 1,000. Defaults to 100. | | `cursor` | `str \| NotGiven \| None` | Optional. The `nextCursor` of the previous page, sent with the same filters. A malformed cursor, or one sent with other filters, returns `400`. | | `kind` | `str \| NotGiven \| None` | Optional. `all` (default) mixes members and Pending Intake by added date, `members` or `pending` returns one kind only | | `external_id` | `str \| NotGiven \| None` | Optional. Returns members with this identifier from your system (1 to 200 characters) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `max_results` | `int \| None` | Optional. Maximum items yielded. `None` reads every page. `0` makes no request. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Iterator[models.ListListMembersItem | None]`. An iterator of typed records. Entries can be `None`. The iterator fetches pages only as you advance. Pagination is free. Omit `max_results` to read all pages. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options, invalid `max_results` or `per_page`, or a repeated pagination cursor. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: for item in client.lists.members.list_all("00000000-0000-4000-8000-000000000001", max_results=20): if item is not None: print(item.to_dict()) ``` ## members.add Call [Add members](https://docs.reversecontact.com/endpoints/list-members-add) with `client.lists.members.add()`. **Python SDK** ```python [Python SDK] def add( list_id: str, *, signals: list[models.AddListMembersParamsSignalsItem | dict[str, object]] | NotGiven | None = NOT_GIVEN, source: str | NotGiven | None = NOT_GIVEN, live: bool | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.AddListMembersData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `list_id` | `str` | **Required.** List id (UUID). | | `signals` | `list[models.AddListMembersParamsSignalsItem \| dict[str, object]] \| NotGiven \| None` | **Required.** 1 to 1,000 Signals, described below | | `source` | `str \| NotGiven \| None` | Optional. `paste` (default) or `file`: how the Pending Intake line was added | | `live` | `bool \| NotGiven \| None` | Optional. `true` fetches the profile of a Social profile URL live (2 credits). Omitted or `false`, saved data is used (1 credit). No effect on a companies List. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.AddListMembersData]`. `data` reports `inserted`, `skipped`, `refused`, `unresolved_count`, `stopped_reason`, `credits_consumed` and per-signal `results`. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.lists.members.add( "00000000-0000-4000-8000-000000000001", signals=[{"email": "jane@acme.com", "externalId": "crm-001"}], ) print(result.data if result is not None else None) ``` ## members.remove Provide at least one id in `member_ids` or `pending_ids`, with at most 25,000 ids combined. **Python SDK** ```python [Python SDK] def remove( list_id: str, *, member_ids: list[str] | NotGiven | None = NOT_GIVEN, pending_ids: list[str] | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.RemoveListMembersData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `list_id` | `str` | **Required.** List id (UUID). | | `member_ids` | `list[str] \| NotGiven \| None` | **Required for the selected strategy.** Member ids (UUID) to remove, the `id` of rows with `kind: "member"` | | `pending_ids` | `list[str] \| NotGiven \| None` | **Required for the selected strategy.** Pending Intake ids (UUID) to dismiss, the `id` of rows with `kind: "pending"` | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.RemoveListMembersData]`. `data.removed` counts removed members. `data.dismissed` counts dismissed Pending Intake lines. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.lists.members.remove( "00000000-0000-4000-8000-000000000001", member_ids=["00000000-0000-4000-8000-000000000002"], ) print(result.data if result is not None else None) ``` ## operations.retrieve Call [Read a List operation](https://docs.reversecontact.com/endpoints/list-operation) with `client.lists.operations.retrieve()`. **Python SDK** ```python [Python SDK] def retrieve( list_id: str, operation_id: str, *, wait: int | NotGiven | None = NOT_GIVEN, rows: str | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, per_page: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.GetListOperationData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `list_id` | `str` | **Required.** List id (UUID) | | `operation_id` | `str` | **Required.** The `operation_id` of the import acknowledgement | | `wait` | `int \| NotGiven \| None` | Optional. Hold the request up to this many seconds, 0 to 20, while the operation runs. It answers as soon as the operation settles, pauses or stops, or with the operation still running when the wait ends. Defaults to 0. | | `rows` | `str \| NotGiven \| None` | Optional. `true` adds one page of per-line results | | `cursor` | `str \| NotGiven \| None` | Optional. With `rows=true`: the `rows.nextCursor` of the previous page | | `per_page` | `int \| NotGiven \| None` | Optional. With `rows=true`: lines per page, from 1 to 1,000. Defaults to 200. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.GetListOperationData]`. `data` contains operation progress, `client_action`, `next_step`, credits and optional `rows`. This reads one state without waiting for the import to finish. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options or a `wait` outside the integer range 0 to 20. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.lists.operations.retrieve( "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002", wait=20, rows="true", ) print(result.data if result is not None else None) ``` ## operations.cancel Call [Cancel a List operation](https://docs.reversecontact.com/endpoints/list-operation-cancel) with `client.lists.operations.cancel()`. **Python SDK** ```python [Python SDK] def cancel( list_id: str, operation_id: str, *, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.CancelListOperationData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `list_id` | `str` | **Required.** List id (UUID) | | `operation_id` | `str` | **Required.** The `operation_id` of the import acknowledgement | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.CancelListOperationData]`. `data` is the operation state after the cancellation request. Inspect `status` and `client_action`. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.lists.operations.cancel( "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002", ) print(result.data if result is not None else None) ``` ## operations.wait Call [Read a List operation](https://docs.reversecontact.com/endpoints/list-operation) with `client.lists.operations.wait()`. **Python SDK** ```python [Python SDK] def wait( list_id: str, operation_id: str, *, timeout: float = 1800, poll_wait: int = 20, on_poll: Callable[[Result[GetListOperationData]], None] | None = None, headers: Mapping[str, str] | None = None, ) -> Result[GetListOperationData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `list_id` | `str` | **Required.** List id (UUID) | | `operation_id` | `str` | **Required.** The `operation_id` of the import acknowledgement | | `timeout` | `float` | Optional. Overall polling deadline in seconds. Default `1800`. | | `poll_wait` | `int` | Optional. Long-poll duration from `0` to `20` seconds. Default `20`. | | `on_poll` | `Callable[[Result[GetListOperationData]], None] \| None` | Optional. Synchronous callback called with each successful poll response. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | **Returns** `Result[GetListOperationData]`. `data` is the operation state when `client_action` is no longer `poll`, such as `done`, `resume` or `reconcile`. Inspect `next_step`. Failed or paused operations are returned without `JobFailedError`. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid `poll_wait` or `timeout`. `JobTimeoutError` reports an expired deadline. Failed or paused server states are returned for inspection. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.lists.operations.wait( "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002", ) print(result.data if result is not None else None) ``` ## Related guides See the [Python SDK overview](https://docs.reversecontact.com/sdk/python), [Lists and bulk](https://docs.reversecontact.com/sdk/python/lists-and-bulk) and [Bulk](https://docs.reversecontact.com/sdk/python/bulk). ## FAQ ### How do I read every member of a List in Python? Use `client.lists.members.list_all(list_id)`. The free iterator follows every page unless you set an optional `max_results` cap. ### How do I follow a List import in Python? Save the `operation_id` returned by `client.lists.import_signals()` and call `client.lists.operations.wait(list_id, operation_id)`. Inspect `client_action` and `next_step`. ### Does lists.operations.wait raise on a paused import? `client.lists.operations.wait()` returns when `client_action` stops being `poll`, including paused or failed states. If the wait exceeds its deadline, the SDK raises `JobTimeoutError`. ### What does lists.export return in Python? `client.lists.export(list_id)` returns the List export as CSV text, ready to write to a UTF-8 file. --- # client.bulk > Start, monitor and cancel Bulk enrichment jobs with the Python SDK, read results as they come, or download them as CSV or NDJSON. - Page: https://docs.reversecontact.com/sdk/python/bulk - Last verified: 2026-11-04 Start Bulk enrichment jobs, follow progress, browse job history, read results as they come and download them. | Method | Endpoint | Cost | | ------ | -------- | ---- | | [`enrich_persons()`](#enrich_persons) | [POST /v2/enrich/persons/bulk](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) | Free to send. Each item costs the same as its individual call. Credits are reserved when the job is created and refunded for what is not found. | | [`list()`](#list) | [GET /v2/jobs](https://docs.reversecontact.com/endpoints/jobs) | Free. | | [`list_all()`](#list_all) | [GET /v2/jobs](https://docs.reversecontact.com/endpoints/jobs) | Free. | | [`retrieve()`](#retrieve) | [GET /v2/jobs/{jobId}](https://docs.reversecontact.com/endpoints/job) | Free, even at a zero balance. Never counts toward rate limits. | | [`results()`](#results) | [GET /v2/jobs/{jobId}/results](https://docs.reversecontact.com/endpoints/job-results) | Free, even at a zero balance. | | [`rows()`](#rows) | [GET /v2/jobs/{jobId}/rows](https://docs.reversecontact.com/endpoints/job-rows) | Free, even at a zero balance. Each page counts toward rate limits. | | [`iter_rows()`](#iter_rows) | [GET /v2/jobs/{jobId}/rows](https://docs.reversecontact.com/endpoints/job-rows) | Free, even at a zero balance. Each page counts toward rate limits. | | [`cancel()`](#cancel) | [POST /v2/jobs/{jobId}/cancel](https://docs.reversecontact.com/endpoints/job-cancel) | Free. Items that had not started are never charged. | | [`wait()`](#wait) | [GET /v2/jobs/{jobId}](https://docs.reversecontact.com/endpoints/job) | Free, even at a zero balance. Never counts toward rate limits. | ## Calling conventions Examples assume `RC_API_KEY` is set. Replace example ids with ids returned by your own requests. Signatures show the published 0.3.0 synchronous methods. Use the same arguments with the [Python async client](https://docs.reversecontact.com/sdk/python/async-client). `models` refers to `reversecontact.types`. `NOT_GIVEN` and `NotGiven` are the SDK's internal markers for omitted arguments. Leave these arguments out of your calls. For API body parameters, passing `None` sends an explicit null. Transport options use `None` to inherit their defaults. `Timeout` means a number of seconds or `httpx.Timeout`. Supply parameters marked **Required** to Python or to the API, even when the generated signature permits omission. JSON response objects expose `data`, `quotas`, `metadata` and `http_response`. Check optional fields for `None`. See [First call](https://docs.reversecontact.com/sdk/python/first-call) for the response envelope. CSV methods return text and iterators yield items. HTTP failures raise an `APIError` subclass based on the response: `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` or `InternalServerError`. See the [error mapping and retry rules](https://docs.reversecontact.com/sdk/python/errors-and-retries). ## enrich_persons Send 1 to 25,000 items. Nested item dictionaries use the API names, such as `linkedinUrl`, `firstName` and `externalId`. **Python SDK** ```python [Python SDK] def enrich_persons( *, items: list[models.BulkEnrichPersonsParamsItemsItem | dict[str, object]] | NotGiven | None = NOT_GIVEN, families: list[str] | NotGiven | None = NOT_GIVEN, email_type: str | NotGiven | None = NOT_GIVEN, live: bool | NotGiven | None = NOT_GIVEN, name: str | NotGiven | None = NOT_GIVEN, webhook_url: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, idempotency_key: str | None = None, **extra: object, ) -> Result[models.BulkEnrichPersonsData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `items` | `list[models.BulkEnrichPersonsParamsItemsItem \| dict[str, object]] \| NotGiven \| None` | **Required.** 1 to 25,000 people, see [Items](https://docs.reversecontact.com/endpoints/bulk-enrich-persons#items) | | `families` | `list[str] \| NotGiven \| None` | Optional. What to look up for each item: `profile`, `contact_email`, `contact_phone`. Defaults to `["profile"]`. | | `email_type` | `str \| NotGiven \| None` | Optional. With `contact_email`, the kind of email to find: `professional` (default), `personal` or `any` | | `live` | `bool \| NotGiven \| None` | Optional. `true` fetches the profile of a Social profile URL item live. Omitted or `false`, saved data is used. | | `name` | `str \| NotGiven \| None` | Optional. A name for the job, up to 255 characters, shown in the job history and in the app. Defaults to `Sent via API`. | | `webhook_url` | `str \| NotGiven \| None` | Optional. Public HTTPS URL that receives one `POST` when the job settles. See [Webhook callback](https://docs.reversecontact.com/endpoints/bulk-enrich-persons#webhook-callback) | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `idempotency_key` | `str \| None` | Optional. Key for this creation request. The SDK generates a key when omitted and reuses it on automatic retries. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.BulkEnrichPersonsData]`. `data.job.id` identifies the Bulk job. The acknowledgement also contains `duplicate_count`, `credits_estimated` and `not_run_for_credits`. Follow the job with `client.bulk.wait(job_id)`. See [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/python/lists-and-bulk). **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.bulk.enrich_persons( items=[{"linkedinUrl": "https://social.com/in/janedoe", "externalId": "crm-001"}], families=["profile"], name="CRM enrichment", ) print(result.data if result is not None else None) ``` ## list Call [Job history](https://docs.reversecontact.com/endpoints/jobs) with `client.bulk.list()`. **Python SDK** ```python [Python SDK] def list( *, type: str | NotGiven | None = NOT_GIVEN, status: str | NotGiven | None = NOT_GIVEN, list_id: str | NotGiven | None = NOT_GIVEN, per_page: int | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Page[models.ListJobsItem]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `type` | `str \| NotGiven \| None` | Optional. Filter by these comma-separated types. See [Job types](https://docs.reversecontact.com/endpoints/jobs#job-types) | | `status` | `str \| NotGiven \| None` | Optional. Filter by these comma-separated statuses: `queued`, `running`, `paused`, `completed`, `cancelled`, `failed` | | `list_id` | `str \| NotGiven \| None` | Optional. Return only jobs for this List (UUID) | | `per_page` | `int \| NotGiven \| None` | Optional. Jobs per page, from 1 to 100. Defaults to 50. | | `cursor` | `str \| NotGiven \| None` | Optional. The `nextCursor` of the previous page, sent with the same filters. A malformed cursor, or one sent with other filters, returns `400`. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Page[models.ListJobsItem]`. A page with records in `data`, a `next_cursor`, `has_next_page()` and `get_next_page()`. The next-page method reuses the filters and returns `None` after the last page. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: page = client.bulk.list(per_page=20) for item in page.data: if item is not None: print(item.to_dict()) ``` ## list_all Read every job-history page with `client.bulk.list_all(per_page=100)`. In SDK 0.3.0, omitting `per_page` starts the iterator at a page size of 200, reduced by any remaining `max_results` cap. The API accepts at most 100 per page, so set `per_page` explicitly to a value from 1 to 100. **Python SDK** ```python [Python SDK] def list_all( *, type: str | NotGiven | None = NOT_GIVEN, status: str | NotGiven | None = NOT_GIVEN, list_id: str | NotGiven | None = NOT_GIVEN, per_page: int | NotGiven | None = NOT_GIVEN, cursor: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, max_results: int | None = None, **extra: object, ) -> Iterator[models.ListJobsItem | None]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `type` | `str \| NotGiven \| None` | Optional. Filter by these comma-separated types. See [Job types](https://docs.reversecontact.com/endpoints/jobs#job-types) | | `status` | `str \| NotGiven \| None` | Optional. Filter by these comma-separated statuses: `queued`, `running`, `paused`, `completed`, `cancelled`, `failed` | | `list_id` | `str \| NotGiven \| None` | Optional. Return only jobs for this List (UUID) | | `per_page` | `int \| NotGiven \| None` | Optional in the signature. Set 1 to 100 explicitly: SDK 0.3.0 otherwise starts from 200 before applying `max_results`, above the API maximum of 100. | | `cursor` | `str \| NotGiven \| None` | Optional. The `nextCursor` of the previous page, sent with the same filters. A malformed cursor, or one sent with other filters, returns `400`. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `max_results` | `int \| None` | Optional. Maximum items yielded. `None` reads every page. `0` makes no request. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Iterator[models.ListJobsItem | None]`. An iterator of typed records. Entries can be `None`. The iterator fetches pages only as you advance. Pagination is free. Omit `max_results` to read all pages. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options, invalid `max_results` or `per_page`, or a repeated pagination cursor. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: for item in client.bulk.list_all(per_page=100, max_results=20): if item is not None: print(item.to_dict()) ``` ## retrieve Call [Get a job](https://docs.reversecontact.com/endpoints/job) with `client.bulk.retrieve()`. **Python SDK** ```python [Python SDK] def retrieve( job_id: str, *, wait: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.GetJobData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `job_id` | `str` | **Required.** Bulk job id (UUID). | | `wait` | `int \| NotGiven \| None` | Optional. Hold the request up to this many seconds, 0 to 20, while the job is queued or running. It answers as soon as the job settles or pauses, or with the job still running when the wait ends. Defaults to 0. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.GetJobData]`. `data` contains one job state, including `status`, `progress`, `credits`, `error` and `links`. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options or a `wait` outside the integer range 0 to 20. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.bulk.retrieve("00000000-0000-4000-8000-000000000001", wait=20) print(result.data if result is not None else None) ``` ## results Results are available once the job is completed or cancelled, as CSV (the default) or with `format="ndjson"` as one `JobResultLine` JSON object per line. **Python SDK** ```python [Python SDK] def results( job_id: str, *, format: str | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> str: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `job_id` | `str` | **Required.** Bulk job id (UUID). | | `format` | `str \| NotGiven \| None` | Optional. `csv` or `ndjson`. Defaults to `csv`. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `str`. The completed or cancelled job's results as text, one row per item in input order: the CSV, or NDJSON with `format="ndjson"`. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. `ConflictError` with `JOB_RESULTS_NOT_READY` means the file is not ready. `ConflictError` with `JOB_RESULTS_FORMAT_UNAVAILABLE` means the job was stored before NDJSON existed: download its CSV. **Example** **Python SDK** ```python [Python SDK] import json from reversecontact import ReverseContact with ReverseContact() as client: job_id = "00000000-0000-4000-8000-000000000001" csv_text = client.bulk.results(job_id) ndjson = client.bulk.results(job_id, format="ndjson") items = [json.loads(line) for line in ndjson.splitlines() if line] print(len(csv_text), len(items)) ``` ## rows Read one page of results, in the order sent, from the start of the run. While the job runs, a page stops at the first item not finished: `data` is empty and `next_cursor` is set while the next item runs, so read it again after `Retry-After`. `next_cursor` is `None` once nothing more will come. Available from SDK 0.3.0. **Python SDK** ```python [Python SDK] def rows( job_id: str, *, cursor: str | NotGiven | None = NOT_GIVEN, limit: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Page[models.JobResultLine]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `job_id` | `str` | **Required.** Bulk job id (UUID). | | `cursor` | `str \| NotGiven \| None` | Optional. `next_cursor` of the previous page, up to 512 characters. Without it, the page starts at the first item. | | `limit` | `int \| NotGiven \| None` | Optional. Items per page, 1 to 100. Defaults to 100. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Page[models.JobResultLine]`, with the items in `data`, `next_cursor`, `has_next_page()` and `get_next_page()`. `extras["status"]` is the job status when the page was read. A page can hold fewer items than `limit`. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. `ConflictError` with `JOB_RESULTS_FORMAT_UNAVAILABLE` means the job was stored before reading by page existed: download its CSV with `results()`. `RateLimitError` with `JOB_ROWS_BUSY` means too many pages are being read at once: the SDK retries it as the [retry rules](https://docs.reversecontact.com/sdk/python/errors-and-retries) describe. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: page = client.bulk.rows("00000000-0000-4000-8000-000000000001", limit=100) for item in page.data: print(item.row, item.status) print(page.extras["status"], page.next_cursor) ``` ## iter_rows Read the results one item at a time, in the order sent, as they become ready, whatever the job is doing. While the next item runs, the iterator waits `Retry-After` (2 seconds by default) and reads the same cursor again. It ends when `next_cursor` is `None`, or when the job is paused, since nothing moves until someone resumes it: read on later with `rows()` and the last cursor. A cancelled job still finishes the items it had started, within the hour, and the iterator waits for them too. Available from SDK 0.3.0. The async client has the same method, used with `async for`. **Python SDK** ```python [Python SDK] def iter_rows( job_id: str, *, cursor: str | None = None, limit: int | None = None, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, ) -> Iterator[models.JobResultLine | None]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `job_id` | `str` | **Required.** Bulk job id (UUID). | | `cursor` | `str \| None` | Optional. Start after a page you already read. | | `limit` | `int \| None` | Optional. Items per page, 1 to 100. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | **Returns** An iterator of `models.JobResultLine` items, in the order sent. **Errors** The errors of `rows()`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: for item in client.bulk.iter_rows("00000000-0000-4000-8000-000000000001"): print(item.row, item.input.get("externalId"), item.status, item.credits_consumed) ``` ## cancel Call [Cancel a job](https://docs.reversecontact.com/endpoints/job-cancel) with `client.bulk.cancel()`. **Python SDK** ```python [Python SDK] def cancel( job_id: str, *, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.CancelJobData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `job_id` | `str` | **Required.** Bulk job id (UUID). | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.CancelJobData]`. `data` contains the job state after cancellation, including `status`, `progress` and `credits`. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.bulk.cancel("00000000-0000-4000-8000-000000000001") print(result.data if result is not None else None) ``` ## wait Call [Get a job](https://docs.reversecontact.com/endpoints/job) with `client.bulk.wait()`. **Python SDK** ```python [Python SDK] def wait( job_id: str, *, timeout: float = 1800, poll_wait: int = 20, on_poll: Callable[[Result[GetJobData]], None] | None = None, headers: Mapping[str, str] | None = None, ) -> Result[GetJobData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `job_id` | `str` | **Required.** Bulk job id (UUID). | | `timeout` | `float` | Optional. Overall polling deadline in seconds. Default `1800`. | | `poll_wait` | `int` | Optional. Long-poll duration from `0` to `20` seconds. Default `20`. | | `on_poll` | `Callable[[Result[GetJobData]], None] \| None` | Optional. Synchronous callback called with each successful poll response. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | **Returns** `Result[GetJobData]`. `data` contains the job when the status leaves `queued` or `running`, including `completed`, `cancelled`, `failed` or `paused`. Inspect `status`. Failed and paused jobs are returned without `JobFailedError`. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid `poll_wait` or `timeout`. `JobTimeoutError` reports an expired deadline. Failed or paused server states are returned for inspection. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.bulk.wait("00000000-0000-4000-8000-000000000001") print(result.data if result is not None else None) ``` ## Related guides See the [Python SDK overview](https://docs.reversecontact.com/sdk/python), [Lists and bulk](https://docs.reversecontact.com/sdk/python/lists-and-bulk) and [Lists](https://docs.reversecontact.com/sdk/python/lists). ## FAQ ### Does bulk.enrich_persons return a Job handle? `client.bulk.enrich_persons()` returns a `Result` acknowledgement. Read `data.job.id`, then call `client.bulk.wait(job_id)`. ### Does bulk.wait raise when the Bulk job fails? `client.bulk.wait()` returns completed, cancelled, failed or paused job states. Inspect `data.status`. If the wait exceeds its deadline, the SDK raises `JobTimeoutError`. ### How do I download Bulk enrichment results in Python? Once the job is completed or cancelled, call `client.bulk.results(job_id)` to download the CSV as text, or `client.bulk.results(job_id, format="ndjson")` for NDJSON. From SDK 0.3.0, `client.bulk.iter_rows(job_id)` reads each result as soon as it is ready. ### How do I list all Bulk jobs in Python? Use the free `client.bulk.list_all(per_page=100)` iterator. With SDK 0.3.0, set `per_page` explicitly. `max_results` is optional and limits how many job records the iterator yields. --- # client.jobs > Retrieve live fetch and contact job states in Python, resume polling with a saved webhook id, and handle job failures, deadlines and results. - Page: https://docs.reversecontact.com/sdk/python/jobs - Last verified: 2026-11-04 Read or wait for a live fetch or contact job by its saved webhook id. | Method | Endpoint | Cost | | ------ | -------- | ---- | | [`retrieve()`](#retrieve) | [GET /v2/webhooks/{webhookId}](https://docs.reversecontact.com/guides/polling) | Free (0 credits), and does not count toward your per-minute rate limit. | | [`wait()`](#wait) | [GET /v2/webhooks/{webhookId}](https://docs.reversecontact.com/guides/polling) | Free (0 credits), and does not count toward your per-minute rate limit. | ## Calling conventions Examples assume `RC_API_KEY` is set. Replace example ids with ids returned by your own requests. Signatures show the published 0.3.0 synchronous methods. Use the same arguments with the [Python async client](https://docs.reversecontact.com/sdk/python/async-client). `models` refers to `reversecontact.types`. `NOT_GIVEN` and `NotGiven` are the SDK's internal markers for omitted arguments. Leave these arguments out of your calls. For API body parameters, passing `None` sends an explicit null. Transport options use `None` to inherit their defaults. `Timeout` means a number of seconds or `httpx.Timeout`. Supply parameters marked **Required** to Python or to the API, even when the generated signature permits omission. JSON response objects expose `data`, `quotas`, `metadata` and `http_response`. Check optional fields for `None`. See [First call](https://docs.reversecontact.com/sdk/python/first-call) for the response envelope. CSV methods return text and iterators yield items. HTTP failures raise an `APIError` subclass based on the response: `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` or `InternalServerError`. See the [error mapping and retry rules](https://docs.reversecontact.com/sdk/python/errors-and-retries). For Bulk job ids, use [client.bulk](https://docs.reversecontact.com/sdk/python/bulk). For List operation ids, use [client.lists](https://docs.reversecontact.com/sdk/python/lists). ## retrieve Read the current job state with `client.jobs.retrieve()`. See [Polling](https://docs.reversecontact.com/guides/polling) for details. **Python SDK** ```python [Python SDK] def retrieve( webhook_id: str, *, wait: int | NotGiven | None = NOT_GIVEN, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[models.PollWebhookData]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `webhook_id` | `str` | **Required.** Saved webhookId from a live fetch or contact job. | | `wait` | `int \| NotGiven \| None` | Optional. Seconds the API may hold the request while the job runs, from `0` to `20`. Absent or `0`: the API answers at once, as a normal poll. Any other value returns `422` with `VALIDATION_ERROR`. | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[models.PollWebhookData]`. `data` is one job snapshot, including `status`, `error_code`, `result`, `delivery_status` and `credits_consumed`. `result` is decoded JSON. An `errored` job is returned without raising `JobFailedError`. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options or a `wait` outside the integer range 0 to 20. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.jobs.retrieve("00000000-0000-4000-8000-000000000001", wait=20) print(result.data if result is not None else None) ``` ## wait Wait for the job to finish with `client.jobs.wait()`. See [Polling](https://docs.reversecontact.com/guides/polling) for details. **Python SDK** ```python [Python SDK] def wait( job_id: str, *, timeout: float = 1800, poll_wait: int = 20, on_poll: Callable[[Result[JobSnapshot[object]]], None] | None = None, headers: Mapping[str, str] | None = None, ) -> Result[JobSnapshot[object]]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `job_id` | `str` | **Required.** Saved `webhookId` from a live fetch or contact job, also available as `job.id`. | | `timeout` | `float` | Optional. Overall polling deadline in seconds. Default `1800`. | | `poll_wait` | `int` | Optional. Long-poll duration from `0` to `20` seconds. Default `20`. | | `on_poll` | `Callable[[Result[JobSnapshot[object]]], None] \| None` | Optional. Synchronous callback called with each successful poll response. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | **Returns** `Result[JobSnapshot[object]]`. `data` is the successful job snapshot. `data.result` is decoded JSON and does not use the original job handle's typed result parser. No new enrichment job is created. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid `poll_wait` or `timeout`. `wait()` raises `JobFailedError` for an errored job and `JobTimeoutError` when its overall deadline expires. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.jobs.wait("00000000-0000-4000-8000-000000000001") print(result.data if result is not None else None) ``` ## Related guides See the [Python SDK overview](https://docs.reversecontact.com/sdk/python), [Async jobs](https://docs.reversecontact.com/sdk/python/async-jobs), [Fetch](https://docs.reversecontact.com/sdk/python/fetch) and [Contact](https://docs.reversecontact.com/sdk/python/contact). ## FAQ ### Which job id does client.jobs accept? `client.jobs` accepts the `webhookId` returned by live fetch and contact endpoints, also exposed as `job.id`. Bulk jobs use `client.bulk`. ### Does jobs.retrieve raise JobFailedError? `client.jobs.retrieve()` returns an `errored` state as data. `client.jobs.wait()` raises `JobFailedError` for that state. ### Does resuming jobs.wait create another paid job? `client.jobs.wait(job_id)` polls the existing job for free. Resuming does not create or bill another enrichment job. --- # client.usage > Read Reverse Contact API credit balances and rate limits with client.usage.retrieve in Python, including request options, response fields and errors. - Page: https://docs.reversecontact.com/sdk/python/usage - Last verified: 2026-11-04 Read workspace credits, API key quotas and rate limits without spending credits. | Method | Endpoint | Cost | | ------ | -------- | ---- | | [`retrieve()`](#retrieve) | [GET /v2/usage](https://docs.reversecontact.com/endpoints/usage) | Free. | ## Calling conventions Examples assume `RC_API_KEY` is set. Signatures show the published 0.3.0 synchronous methods. Use the same arguments with the [Python async client](https://docs.reversecontact.com/sdk/python/async-client). `models` refers to `reversecontact.types`. `NOT_GIVEN` and `NotGiven` are the SDK's internal markers for omitted arguments. Leave these arguments out of your calls. For API body parameters, passing `None` sends an explicit null. Transport options use `None` to inherit their defaults. `Timeout` means a number of seconds or `httpx.Timeout`. Supply parameters marked **Required** to Python or to the API, even when the generated signature permits omission. JSON response objects expose `data`, `quotas`, `metadata` and `http_response`. Check optional fields for `None`. See [First call](https://docs.reversecontact.com/sdk/python/first-call) for the response envelope. CSV methods return text and iterators yield items. HTTP failures raise an `APIError` subclass based on the response: `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` or `InternalServerError`. See the [error mapping and retry rules](https://docs.reversecontact.com/sdk/python/errors-and-retries). ## retrieve Check your quotas with `client.usage.retrieve()`. See the [Usage endpoint](https://docs.reversecontact.com/endpoints/usage) for details. **Python SDK** ```python [Python SDK] def retrieve( *, timeout: Timeout | None = None, max_retries: int | None = None, headers: Mapping[str, str] | None = None, **extra: object, ) -> Result[None]: ... ``` **Parameters** | Name | Type | Description | | ---- | ---- | ----------- | | `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. | | `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. | | `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. | | `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. | **Returns** `Result[None]`. `data` is `None`. Read `quotas` for credits and rate limits, and `metadata` for request diagnostics. **Errors** `APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`. **Example** **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: result = client.usage.retrieve() if result.quotas and result.quotas.workspace and result.quotas.workspace.credits: print(result.quotas.workspace.credits.left) ``` ## Related guides See the [Python SDK overview](https://docs.reversecontact.com/sdk/python), [First call](https://docs.reversecontact.com/sdk/python/first-call) and [Client options](https://docs.reversecontact.com/sdk/python/client-options). ## FAQ ### Does checking usage cost credits? `client.usage.retrieve()` returns account quotas and costs 0 credits. ### Why is data None in the Python usage response? The Usage endpoint returns account information in `quotas`. That is why `client.usage.retrieve()` returns `data` as `None`. ### How do I read my remaining credits in Python? Read `result.quotas.workspace.credits.left` after checking that `quotas`, `workspace` and `credits` are present. --- # Async client > Use the Reverse Contact async Python client with asyncio, await API responses and job results, and read search or List iterators with async for. - Page: https://docs.reversecontact.com/sdk/python/async-client - Last verified: 2026-11-04 `AsyncReverseContact` has the same resources, methods and arguments as the synchronous client. Await each call, and read search iterators with `async for`. **Async client** ```python [Python SDK] import asyncio import os from reversecontact import AsyncReverseContact async def main() -> None: async with AsyncReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = await client.enrich.company(domain="acme.com") if result is not None and result.data is not None and result.data.company is not None: print(result.data.company.name, result.data.company.industry) async for person in client.search.iter_people(current_function="Marketing", max_results=10): if person is not None: print(person.first_name, person.last_name) asyncio.run(main()) ``` ## Await pages and jobs Search page methods return an `AsyncPage`. Call `has_next_page()` synchronously and await `get_next_page()`. Live fetch and contact methods return an `AsyncJob`. Await both `job.retrieve()` and `job.wait()`. **Python SDK** ```python [Python SDK] import asyncio from reversecontact import AsyncReverseContact async def main() -> None: async with AsyncReverseContact() as client: job = await client.contact.verify_email(email="jane@acme.com") print("Job id:", job.id) done = await job.wait() print(done.data.result if done.data else None) asyncio.run(main()) ``` ## Close and cancel Use `async with` to close the SDK-owned connection pool, or call `await client.close()`. If you supply your own HTTP client, pass an `httpx.AsyncClient` and close that client yourself. Use `asyncio.timeout()` or cancel an asyncio task to stop waiting locally. See [Timeouts and cancellation](https://docs.reversecontact.com/sdk/python/timeouts) for deadline behavior and [Async jobs](https://docs.reversecontact.com/sdk/python/async-jobs) for handling job failures. Read search iterators and the free `list_all()` iterators with `async for` directly, without awaiting the iterator itself. Return to the [Python SDK overview](https://docs.reversecontact.com/sdk/python). ## FAQ ### How do I call Reverse Contact from asyncio? Create `AsyncReverseContact` inside `async with` and await API calls. The async client exposes the same resources and arguments as `ReverseContact`. ### Should I await a Python SDK search iterator? Read an `AsyncReverseContact` search iterator with `async for`. Do not await the iterator itself. Await page methods and `get_next_page()` instead. ### How do I wait for an async Python job? Await the method that creates an `AsyncJob`, then await `job.wait()`. Save `job.id` so `client.jobs.wait(job_id)` can resume polling later. --- # Async jobs > Wait for live profiles, Find Email, Verify Email and Find Phone jobs in Python, resume polling by id, or parse webhook deliveries from the API. - Page: https://docs.reversecontact.com/sdk/python/async-jobs - Last verified: 2026-11-04 `fetch.person_live`, `fetch.company_live`, `contact.find_email`, `contact.verify_email` and `contact.find_phone` start a job and return a job handle right away. `job.id` is the job's `webhookId`. Call `job.wait()` to poll the job until it ends. When the job succeeds, the method returns its final state in `data` (`status`, `result`, `credits_consumed`). When the job fails, the method raises `JobFailedError`. Polling is free and does not count toward your rate limit. **Live fetch** ```python [Python SDK] import os from reversecontact import JobFailedError, JobTimeoutError, ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.fetch.person_live(url="https://social.com/in/janedoe") # Save the id first: after a restart, client.jobs.wait(id) resumes without paying for a new job. print("Job id:", job.id) try: done = job.wait(timeout=1800) if done.data is not None and done.data.result is not None: print(done.data.result.first_name, "credits:", done.data.credits_consumed) except JobFailedError as error: print("Job failed:", error.error_code, "credits:", error.credits_consumed) except JobTimeoutError as error: print("Still running, resume later with client.jobs.wait:", error.job_id) ``` How `wait()` behaves: - Each poll asks the API to hold the answer for up to 20 seconds (`poll_wait`, 0 to 20), so the result arrives as soon as the job ends. Two polls start at least `Retry-After` seconds apart, 2 seconds when the header is missing. - `timeout` is the overall deadline in seconds, 1800 (30 minutes) by default. When the deadline passes, `wait()` raises `JobTimeoutError` with `job_id` and `last_status`. The job may still finish. Resume polling with `client.jobs.wait(job_id)`. The synchronous client can exceed the deadline by the length of the poll in progress. - A job that ends with `status` `errored` raises `JobFailedError`, with `error_code`, `credits_consumed`, `result` and `not_found`. The `not_found` field is `True` for `data-not-found`, `person-not-found`, `company-not-found`, `email-not-found` and `phone-not-found`. - The SDK calls `on_poll` with each state it reads. - The SDK never starts a new job after a failure. Your code decides whether to start another job, since the new job can be billed again. `client.jobs.retrieve(job_id, wait=20)` reads one state of a job without waiting for the end. [Polling](https://docs.reversecontact.com/guides/polling) describes the job lifecycle and how long a result stays available. ### Webhook delivery instead of polling Pass `webhook_url` when you create the job. The API sends the result to that HTTPS URL as `{webhookId, data}`. Use `parse_webhook_delivery` to parse the decoded body, a JSON string or bytes. The helper checks the delivery's shape and returns `webhook_id`, `data` and `is_error`. The `is_error` field is `True` when `data.errorCode` is a non-empty string. A malformed delivery raises `ReverseContactError`. The helper does not validate the profile itself. **Webhook delivery** ```python [Python SDK] import os from reversecontact import ReverseContact, parse_webhook_delivery with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.fetch.person_live( url="https://social.com/in/janedoe", webhook_url="https://your-app.com/webhooks/reversecontact", ) print("Store this id to match the delivery:", job.id) # Call this from the route behind webhook_url with the raw request body. def handle_webhook(body: bytes) -> None: delivery = parse_webhook_delivery(body) if delivery.is_error: print(delivery.webhook_id, "failed:", delivery.data) else: print(delivery.webhook_id, "result:", delivery.data) # Answer within 10 seconds, then process the data: deduplicate on webhook_id. ``` You can still poll a job created with `webhook_url` by calling `job.wait()`. See [Webhooks](https://docs.reversecontact.com/guides/webhooks) for delivery failures and retries. ## Read one state from a job handle `job.retrieve(wait=20)` returns one typed `Result[JobSnapshot[T]]`. The profile or contact result keeps the type of the method that created the job. Unlike `wait()`, `retrieve()` returns an `errored` state without raising `JobFailedError`. **Python SDK** ```python [Python SDK] from reversecontact import ReverseContact with ReverseContact() as client: job = client.contact.find_phone(url="https://social.com/in/janedoe") state = job.retrieve(wait=20) if state.data: print(state.data.status, state.data.result) ``` After a restart, use [client.jobs](https://docs.reversecontact.com/sdk/python/jobs) to retrieve a job or wait for the result by id. `client.jobs.wait(job_id)` returns the endpoint-specific `result` as decoded JSON. The original `Job` handle parses that result into its typed model. [client.fetch](https://docs.reversecontact.com/sdk/python/fetch) and [client.contact](https://docs.reversecontact.com/sdk/python/contact) list the job-creating methods. Bulk jobs and List operations use their own helpers, described in [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/python/lists-and-bulk). Return to the [Python SDK overview](https://docs.reversecontact.com/sdk/python). ## FAQ ### How do I wait for a live fetch or Find Email result in Python? Save `job.id` and call `job.wait()`. Polling is free and waits up to 1800 seconds by default. Resume after a restart with `client.jobs.wait(job_id)`. ### What happens when a Python SDK job fails? `job.wait()` raises `JobFailedError` for an `errored` job. The exception includes `job_id`, `error_code`, `credits_consumed`, `result` and `not_found`. ### Does JobTimeoutError cancel the enrichment job? `JobTimeoutError` ends local waiting, without cancelling the remote job. Resume polling with the exception's `job_id`. ### Can I use webhooks and polling together in Python? Pass `webhook_url` to a live fetch or contact method and parse deliveries with `parse_webhook_delivery`. The same job remains available through `job.wait()`. --- # Lists and Bulk enrichment > Manage persistent Lists and run Bulk enrichment in Python, follow operation progress, export CSV results, and reuse identifiers after a restart. - Page: https://docs.reversecontact.com/sdk/python/lists-and-bulk - Last verified: 2026-11-04 From version 0.2.0, `client.lists` manages your [Lists](https://docs.reversecontact.com/guides/lists) and `client.bulk` runs [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) jobs. `bulk.enrich_persons` starts a job of up to 25,000 people. Use `bulk.wait` to wait until the job is `completed`, `cancelled`, `failed` or `paused`. Once the job is completed or cancelled, `bulk.results` returns the results CSV as text. The SDK sends an `Idempotency-Key` with each new job, generating one if you omit the key. The SDK reuses the key on its own retries, so a retried request never creates a second job. Items keep the API's camelCase field names. **Bulk enrichment** ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: created = client.bulk.enrich_persons( name="CRM export, November", items=[ {"linkedinUrl": "https://social.com/in/janedoe", "externalId": "crm-001"}, {"firstName": "John", "lastName": "Smith", "companyDomain": "acme.com", "externalId": "crm-002"}, ], families=["profile", "contact_email"], ) if created.data is None or created.data.job is None or created.data.job.id is None: raise RuntimeError("The response is missing the bulk job id") job_id = created.data.job.id job = client.bulk.wait(job_id) if job.data and job.data.links and job.data.links.results: with open("results.csv", "w", encoding="utf-8") as file: file.write(client.bulk.results(job_id)) if job.data: print(job.data.status, "credits:", job.data.credits.used if job.data.credits else None) ``` From version 0.3.0, you can use the results before the job settles. `bulk.iter_rows` yields them one at a time, in the order sent, as soon as each one is ready. While the next item runs, it waits out the `Retry-After` of the API. It ends with the job, or when the job is paused: read on later with `bulk.rows` and the last cursor. A cancelled job first finishes the items it had started, within the hour. `bulk.rows` reads one page (`cursor`, `limit` up to 100), with the job status in `extras["status"]`. Each item is a `JobResultLine`, exported by `reversecontact.types`. `bulk.results(job_id, format="ndjson")` returns the results file as NDJSON, one `JobResultLine` per line. The [client.bulk reference](https://docs.reversecontact.com/sdk/python/bulk) documents each method. **Results as they come** ```python [Python] import json for item in client.bulk.iter_rows(job_id): print(item.row, item.input.get("externalId"), item.status, item.credits_consumed) # Or the whole file once the job settles, one JSON object per line: ndjson = client.bulk.results(job_id, format="ndjson") items = [json.loads(line) for line in ndjson.splitlines() if line] ``` A List keeps its members. Start an import with `lists.import_signals`, then call `lists.operations.wait`. The helper returns when the operation's `client_action` is `done`, `resume` or `reconcile`, so your code can decide what comes next. If you omit the identifiers, `lists.create` sends an `Idempotency-Key` and `lists.import_signals` sends an `operation_id`. To resume a paused import after a restart, pass your own `operation_id`. **Lists** ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: created = client.lists.create(name="Prospects", entity="persons") if created.data is None or created.data.id is None: raise RuntimeError("The response is missing the List id") list_id = created.data.id imported = client.lists.import_signals( list_id, signals=[{"email": "jane@acme.com", "externalId": "crm-001"}], families=["contact_email"], max_credits=100, ) if imported.data is None or imported.data.operation_id is None: raise RuntimeError("The response is missing the operation id") operation = client.lists.operations.wait(list_id, imported.data.operation_id) if operation.data: print(operation.data.client_action, operation.data.next_step) for member in client.lists.members.list_all(list_id): print(member) csv_text = client.lists.export(list_id) ``` `lists.list_all`, `lists.members.list_all` and `bulk.list_all` read every page for you. They are free, so they need no `max_results`. ## Choose the right wait method `client.bulk.wait(job_id)` returns when the job leaves `queued` or `running`, including when the job is `failed` or `paused`. Inspect `data.status` to decide what to do next. `client.lists.operations.wait(list_id, operation_id)` returns when `data.client_action` is no longer `poll`. Inspect `client_action` and `next_step`. Neither helper raises `JobFailedError` for those returned states. Both helpers accept `timeout` (1800 seconds by default), `poll_wait` (20 by default) and `on_poll`. If the deadline expires, the helper raises `JobTimeoutError` with the job or operation id. For a List operation, resume with `client.lists.operations.wait(list_id, operation_id)`. For `bulk.list_all()`, pass `per_page=100` explicitly. SDK 0.3.0 otherwise starts from 200 items per page before applying `max_results`, but the API accepts at most 100. The free `list_all()` iterators accept an optional `max_results` if you want to stop early. Without a cap, the iterators read all pages. The [client.lists reference](https://docs.reversecontact.com/sdk/python/lists) and [client.bulk reference](https://docs.reversecontact.com/sdk/python/bulk) describe each method, cost and return type. Return to the [Python SDK overview](https://docs.reversecontact.com/sdk/python). ## FAQ ### How many people can I send to Python Bulk enrichment? `client.bulk.enrich_persons()` accepts up to 25,000 people per job. Each item retains the API camelCase field names, such as `linkedinUrl` and `externalId`. ### Does bulk.wait raise JobFailedError for a failed job? `client.bulk.wait()` returns failed and paused jobs so your code can inspect `data.status`. The helper raises `JobTimeoutError` if the waiting deadline expires. ### How do I resume a paused List import in Python? Pass your stored `operation_id` to `client.lists.import_signals()`, then follow progress with `client.lists.operations.wait(list_id, operation_id)`. ### Do List and Bulk iterators need max_results? The free `lists.list_all()`, `lists.members.list_all()` and `bulk.list_all()` iterators accept an optional `max_results`, with no cap by default. --- # Search within a budget > Count matching profiles for free, cap paid Python search results with max_results, and paginate people or company searches within your budget. - Page: https://docs.reversecontact.com/sdk/python/search-budget - Last verified: 2026-11-04 Search costs 1 credit per profile or company delivered. The two Agentic Search endpoints add 1 credit per page. Count matches for free first, then read the results with an iterator. Every search iterator requires `max_results` to cap the results you pay for. The iterator requests at most that many results and lowers the page size on its last request. `max_results=0` makes no request. **Count, then search** ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: count = client.search.count_people( current_function="Marketing", location={"countryCode": "FR"} ) print("Matches:", count.data.count if count.data else None) # free for person in client.search.iter_people( current_function="Marketing", location={"countryCode": "FR"}, per_page=50, max_results=200, ): if person is not None: print(person.first_name, person.last_name) ``` The iterators are `iter_people`, `iter_people_by_prompt`, `iter_people_lookalike`, `iter_organizations` and `iter_organizations_by_prompt`. Set `per_page` from 1 to 100, or use the default of 25 results. An iterator fetches one page at a time, only when you read further. If the API repeats a cursor, the iterator stops with an error. To work page by page, call a page method such as `client.search.people()`. The returned page has `data`, `next_cursor`, `count`, `quotas`, `has_next_page()` and `get_next_page()`. Call `get_next_page()` to resend the same filters with the next cursor. The method returns `None` after the last page. Each page you request is billed for the profiles it returns. Prompt searches also expose `interpretation`, the filters extracted from the prompt. ## Separate result credits from prompt fees `max_results` limits the number of profiles requested, not an absolute credit amount. Agentic Search adds 1 credit for each requested page, including an empty page. Budget for both returned results and prompt page fees. A negative or non-integer `max_results`, or an iterator `per_page` outside 1 to 100, raises `ReverseContactError`. If you stop iterating, the SDK fetches no further pages. Results already delivered remain billed. See [client.search](https://docs.reversecontact.com/sdk/python/search) for all 15 methods and their parameters. Use [Filter values as constants](https://docs.reversecontact.com/sdk/python/filter-constants) to choose typed values from fixed lists. Return to the [Python SDK overview](https://docs.reversecontact.com/sdk/python). ## FAQ ### How do I cap the credits a Reverse Contact search spends in Python? Use a free count method, then pass `max_results` to a search iterator to cap paid results. For prompt searches, also budget 1 credit per requested page. ### Does max_results=0 call the API? A search iterator with `max_results=0` makes no HTTP request. ### What page size can I use for Python search iterators? Search iterators accept `per_page` from 1 to 100, with a default of 25. The final request reduces the page size to the remaining `max_results` budget. ### How do I read search results one page at a time? Use a page method such as `client.search.people()`, then `page.has_next_page()` and `page.get_next_page()`. Each requested page is billed for delivered results. --- # Filter values as constants > Use Python string enums for industries, countries, job functions and other fixed search filters, while still accepting new API values as strings. - Page: https://docs.reversecontact.com/sdk/python/filter-constants - Last verified: 2026-11-04 From version 0.1.6, you can use string enums for values from fixed lists: `Industry`, `CountryCode`, `JobFunction`, `JobSubFunction`, `Seniority`, `ContractType`, `Area` and `EmailType`. Each member equals its string value: `Industry.SOFTWARE_DEVELOPMENT == "Software Development"`. Member names use the value in capitals, with `_` for spaces and punctuation and `AND` for `&`. Examples include `Seniority.OWNER_FOUNDER`, `Area.MC_DACH` and `ContractType.FULL_TIME`. **Filter with constants** ```python [Python SDK] import os from reversecontact import Area, Industry, ReverseContact, Seniority with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: count = client.search.count_people( current_company_industry=Industry.SOFTWARE_DEVELOPMENT, current_seniority=[Seniority.CXO, Seniority.OWNER_FOUNDER], location={"area": Area.MC_DACH}, ) print("Matches:", count.data.count if count.data else None) ``` Filters also accept plain strings. You can use a value added to a list after your SDK version without updating the package. A value the API does not know returns HTTP `422` and costs 0 credits. ## Read available values Call `people_fields()` or `organizations_fields()` on [client.search](https://docs.reversecontact.com/sdk/python/search) to read the API's available search fields and allowed values. Both calls are free. The package also exports `EmailVerificationStatus` for [Verify Email](https://docs.reversecontact.com/sdk/python/contact#verify_email) verdicts. This enum describes results rather than search filters. For an example of searching with filters, see [Search within a budget](https://docs.reversecontact.com/sdk/python/search-budget). Return to the [Python SDK overview](https://docs.reversecontact.com/sdk/python). ## FAQ ### Which filter enums does the Python SDK export? The SDK exports `Industry`, `CountryCode`, `JobFunction`, `JobSubFunction`, `Seniority`, `ContractType`, `Area` and `EmailType` as string enums. ### Can I pass a string instead of a Python SDK enum? Filters accept plain strings, so newly supported API values work without upgrading the SDK. An unknown API value returns HTTP 422 and costs no credits. ### How do I list allowed search values in Python? Call `client.search.people_fields()` or `client.search.organizations_fields()` to read the API's available fields and allowed values at no credit cost. --- # Errors and retries > Handle Python SDK exceptions, understand automatic retries and rate limits, and reuse idempotency keys safely for Find Email and Find Phone. - Page: https://docs.reversecontact.com/sdk/python/errors-and-retries - Last verified: 2026-11-04 SDK-defined errors inherit from `ReverseContactError`. If a response cannot be parsed into the declared model, Pydantic can also raise `ValidationError`. Errors returned by the API inherit from `APIError`, with `status`, `code`, `message`, `details`, `request_id`, `retry_after`, `headers` and `body`. | HTTP status | Error class | | ----------- | ----------- | | `400` | `BadRequestError` | | `401` | `AuthenticationError` | | `402` | `InsufficientCreditsError` | | `403` | `PermissionDeniedError` | | `404` | `NotFoundError` (a lookup with no match returns `None` instead) | | `409` | `ConflictError` | | `410` | `GoneError` | | `422` | `UnprocessableEntityError` | | `429` | `RateLimitError`, or `DailyLimitError` for a daily limit | | `451` | `UnavailableForLegalReasonsError` | | `5xx` | `InternalServerError` | | Any status with `KEY_CREDIT_LIMIT_REACHED` | `KeyCreditLimitError` | A network failure raises `APIConnectionError`. If an attempt runs out of time, the SDK raises `APITimeoutError`, a subclass of that error. The SDK retries a request up to 4 times after the first attempt, 5 attempts in total: - For a `429` from the per-minute rate limit, the SDK first checks `error.details.retryAfter` for the retry delay, then the `Retry-After` header. If neither supplies a delay, the SDK backs off. - For a `500`, `502`, `503` or `504`, the SDK backs off exponentially: 2, 4, 8, then 15 seconds, plus up to 250 milliseconds of jitter. If the response sends `Retry-After`, the SDK uses that delay instead. - If the requested wait exceeds 60 seconds, the SDK raises the error at once instead of waiting. - The SDK retries network failures and timed-out attempts on `GET` requests. For a `DELETE`, the SDK also retries a transport failure when no HTTP response was received. For a `POST`, the SDK retries only when the connection was never established. This prevents sending a request twice when the API may already have billed it. Find Email and Find Phone are exceptions, protected by their idempotency key (see below). The SDK never retries `DailyLimitError` (the daily limits, `DAILY_LIMIT_EXCEEDED` and `DAILY_EXTRACTION_LIMIT_EXCEEDED`), `KeyCreditLimitError` (an API key credit limit) or `InsufficientCreditsError`. Other `4xx` errors are not retried either, except `409 IDEMPOTENCY_KEY_IN_USE` for Find Email and Find Phone. To resolve these errors, change the request, key or account, or wait until the next day. Set `max_retries=0` to turn retries off. **Handle errors** ```python [Python SDK] import os from reversecontact import APIError, DailyLimitError, KeyCreditLimitError, ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: try: client.enrich.company(domain="acme.com") except (DailyLimitError, KeyCreditLimitError) as error: # Not retried: pause the work until the limit resets. print("Limit reached:", error.code, "retry after", error.retry_after, "seconds") except APIError as error: print(error.status, error.code, error.message, "request id:", error.request_id) ``` [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries) lists every error code of the API. ## Safe retries on Find Email and Find Phone Sending the same Find Email, Verify Email or Find Phone request twice would start two jobs, and both can be billed. `contact.find_email`, `contact.verify_email` and `contact.find_phone` send an `Idempotency-Key` header with a new UUID on each call. Each automatic retry of that call reuses the key. For 24 hours, the API returns the first response for a repeated key and body, without starting or billing another lookup. To retry safely after your process restarts, choose the key yourself, store it with the request, and pass it as `idempotency_key`. `job.replayed` is `True` when the API returned the first response again. **Idempotency key** ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.contact.find_email( full_name="Jane Doe", company_domain="acme.com", email_type="any", idempotency_key="crm-contact-4821-email", ) print(job.id, "first response returned again" if job.replayed else "new lookup") done = job.wait() emails = done.data.result if done.data and done.data.result else [] for email in emails: if email is not None: print(email.value, email.type) ``` Use 1 to 255 printable ASCII characters without spaces, and a new key for each new lookup. A key that is empty, longer than 255 characters, or contains a space or a non-ASCII character raises `ReverseContactError` before any request is sent. The SDK retries `409 IDEMPOTENCY_KEY_IN_USE` after its `Retry-After` delay. The SDK does not retry `422 IDEMPOTENCY_KEY_REUSED` (same key, different body) or `400 INVALID_IDEMPOTENCY_KEY`. After 24 hours, the same key can start and bill a new lookup. To read a job you already started, call `client.jobs.wait(job_id)`. ## List and Bulk retry identifiers `lists.create()` and `bulk.enrich_persons()` generate an `Idempotency-Key` when you omit one and reuse that key on automatic retries. `lists.import_signals()` generates an `operation_id` when omitted. Save your own identifiers to continue the same work after a restart. See [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/python/lists-and-bulk). These methods do not use the contact endpoints' special retry rule for ambiguous POST transport failures or `409 IDEMPOTENCY_KEY_IN_USE`. Use [client.contact](https://docs.reversecontact.com/sdk/python/contact) for the three contact methods protected by that rule. `APIError` also exposes `http_response`, `alternative` and `reset_at`. `KeyCreditLimitError` adds `period`, `limit`, `used` and `required`. See [Timeouts and cancellation](https://docs.reversecontact.com/sdk/python/timeouts) for HTTP and overall job deadlines. Return to the [Python SDK overview](https://docs.reversecontact.com/sdk/python). ## FAQ ### Does the reversecontact Python SDK retry failed requests? The SDK retries per-minute rate limits and HTTP 500, 502, 503 and 504 up to four times by default. Daily limits, key credit limits and insufficient credits are not retried. ### How do I disable automatic retries in Python? Pass `max_retries=0` to the client or an individual API method. That setting disables the request transport's automatic retries. ### How do I retry Find Email without creating another job? Store a request-specific `idempotency_key` and reuse that key with the same body. Contact endpoints return the first response for 24 hours. Check `job.replayed` to identify a replay. ### Which Python exception represents an invalid API key? An HTTP 401 response raises `AuthenticationError`, a subclass of `APIError` and `ReverseContactError`. Inspect `request_id`, `status`, `code` and `message` for diagnostics. --- # Timeouts and cancellation > Configure HTTP timeouts, set an overall deadline for Python job polling, and cancel asyncio waits without starting duplicate enrichment requests. - Page: https://docs.reversecontact.com/sdk/python/timeouts - Last verified: 2026-11-04 Each HTTP attempt times out after 90 seconds by default, since some enrichments take more than 60 seconds. Every API method accepts the keyword arguments `timeout` (seconds per attempt, or an `httpx.Timeout`), `max_retries` and `headers`. With `AsyncReverseContact`, use `asyncio.timeout()` or cancel the call's asyncio task. The synchronous client has no cancel handle. Limit each call with `timeout`, and each job wait with `job.wait(timeout=...)`. Cancellation stops your code from waiting but does not cancel a job on the server. **Timeout and cancellation** ```python [Python SDK] import asyncio import os from reversecontact import AsyncReverseContact async def main() -> None: async with AsyncReverseContact(api_key=os.environ["RC_API_KEY"]) as client: async with asyncio.timeout(30): usage = await client.usage.retrieve(timeout=10.0, max_retries=1) if usage.quotas and usage.quotas.workspace and usage.quotas.workspace.credits: print("Credits left:", usage.quotas.workspace.credits.left) asyncio.run(main()) ``` ## HTTP attempts and polling deadlines On an API request, `timeout` limits each HTTP attempt through httpx's timeout settings. Retries can extend the total elapsed time. For polling requests, finite HTTP timeout components increase to at least `wait + 15` seconds. A `wait()` helper then caps those components at the time remaining before its overall deadline. On `job.wait()`, `client.jobs.wait()`, `client.bulk.wait()` and `client.lists.operations.wait()`, `timeout` is an overall deadline. The default is 1800 seconds. The helpers accept `poll_wait` from 0 to 20 and raise `ReverseContactError` for invalid polling values or a non-finite, negative timeout. A synchronous wait can overrun by one bounded HTTP phase. An async wait uses an asyncio deadline. The wait helpers handle polling failures separately from per-request `max_retries`. They retry eligible consecutive API or connection failures up to four times, within the deadline. A successful poll resets the retry count. ## Stop server-side bulk work Local asyncio cancellation does not cancel server-side work. For Bulk jobs, call [client.bulk.cancel()](https://docs.reversecontact.com/sdk/python/bulk#cancel). For a List operation, call [client.lists.operations.cancel()](https://docs.reversecontact.com/sdk/python/lists#operationscancel). These are explicit API operations with their own return values. See [Async jobs](https://docs.reversecontact.com/sdk/python/async-jobs) to resume a timed-out enrichment and [Errors and retries](https://docs.reversecontact.com/sdk/python/errors-and-retries) for transport error handling. Return to the [Python SDK overview](https://docs.reversecontact.com/sdk/python). ## FAQ ### What is the default Python SDK request timeout? The SDK configures a 90-second timeout per HTTP attempt. Pass `timeout` to the client or API method to change the httpx timeout settings. ### How is job.wait timeout different from a request timeout? `job.wait(timeout=1800)` sets an overall polling deadline. A request timeout applies to each HTTP attempt. Retries can extend the total elapsed time. ### Does cancelling an asyncio task cancel a Reverse Contact job? Asyncio cancellation stops local waiting. Use `bulk.cancel()` or `lists.operations.cancel()` for explicit server-side Bulk or List cancellation. ### Can a synchronous job wait exceed its deadline? A synchronous wait can exceed its deadline by one bounded HTTP phase. `JobTimeoutError` retains the job id and last status for resuming later. --- # Client options > Configure the Reverse Contact Python client with timeouts, retries, custom httpx connections, shared options, application metadata and debug logging. - Page: https://docs.reversecontact.com/sdk/python/client-options - Last verified: 2026-11-04 `ReverseContact` and `AsyncReverseContact` accept these keyword-only constructor options. | Argument | Default | Meaning | | -------- | ------- | ------- | | `api_key` | `RC_API_KEY` | Your API key | | `base_url` | `RC_BASE_URL`, then `https://api.reversecontact.com` | API origin | | `timeout` | `90` | Seconds per HTTP attempt, or an `httpx.Timeout` | | `max_retries` | `4` | Retries after the first attempt | | `default_headers` | none | Headers added to every request | | `http_client` | a client the SDK creates | Your own `httpx.Client` or `httpx.AsyncClient`, for a proxy or connection pool settings | | `sleep` | `time.sleep` or `asyncio.sleep` | Function used for retry and polling delays. Async clients require an awaitable | | `clock` | `time.monotonic` | Monotonic clock used for polling deadlines | | `app_info` | none | `{"name": ..., "version": ...}` of your integration, added to the `User-Agent` | Close the client when you are done, with a `with` block or `client.close()`. If you pass an `http_client`, close that client yourself. Use `client.with_options()` to change options in a copy that shares the connection pool. Debug logging is off by default. Set `RC_LOG=debug` before creating the client, or set the `reversecontact` logger to `logging.DEBUG`. The logs show the method, path, status, attempt, retry delay and request id. They never show the API key, the request body or the query values. **Client options** ```python [Python SDK] import logging import os from reversecontact import ReverseContact logging.basicConfig() logging.getLogger("reversecontact").setLevel(logging.DEBUG) with ReverseContact( api_key=os.environ["RC_API_KEY"], timeout=120.0, max_retries=2, app_info={"name": "my-crm-sync", "version": "1.4.0"}, ) as client: fast = client.with_options(timeout=30.0, max_retries=0) print(fast.usage.retrieve().quotas) ``` The package also exports its models, such as `Person` and `Company`, and the request and response models of every operation in `reversecontact.types`. ## Copy options without a new pool `client.with_options()` accepts `api_key`, `base_url`, `timeout`, `max_retries`, `default_headers`, `app_info`, `sleep` and `clock`. Options you omit keep their existing values. Additional `default_headers` merge with the current headers. The returned client uses the original HTTP pool. Closing the copy leaves that pool open. Keep the original client open while using its copies. Read `client.base_url` for the configured API origin, without a trailing slash. API methods accept per-request `timeout`, `max_retries` and `headers`. Wait helpers have overall deadline controls, described in [Timeouts and cancellation](https://docs.reversecontact.com/sdk/python/timeouts). `max_retries` must be a non-negative integer. Invalid values raise `ReverseContactError`. See [Install and configure](https://docs.reversecontact.com/sdk/python/install) to create a client and [Async client](https://docs.reversecontact.com/sdk/python/async-client) to manage async connections. Return to the [Python SDK overview](https://docs.reversecontact.com/sdk/python). ## FAQ ### How do I change the Python SDK API base URL? Pass `base_url` to the client. Without an explicit value, the SDK reads `RC_BASE_URL`, then defaults to `https://api.reversecontact.com`. ### Does with_options create a new connection pool? `client.with_options()` shares the original pool and returns a client with copied options. Closing the copy leaves the shared pool open. ### How do I enable Python SDK debug logs? Set `RC_LOG=debug` before creating the client or set the `reversecontact` logger to `logging.DEBUG`. Logs omit API keys, request bodies and query values. ### Can I supply my own httpx client? Pass an `httpx.Client` to `ReverseContact` or an `httpx.AsyncClient` to `AsyncReverseContact` using `http_client`. Close the supplied client yourself. --- # Without the SDK > Call the Reverse Contact API from Python with a standard HTTPS client, authenticate with a Bearer key, and use OpenAPI to generate your own models. - Page: https://docs.reversecontact.com/sdk/python/without-the-sdk - Last verified: 2026-11-04 The API uses JSON over HTTPS and works with any HTTP client. Send your key in an `Authorization: Bearer` header, as shown in the cURL examples on each endpoint page. To generate your own typed models, use the OpenAPI spec at `https://docs.reversecontact.com/openapi.json`. Follow the rules in [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries) and [Polling](https://docs.reversecontact.com/guides/polling). The SDK handles these for you. ## Send a request with httpx The free [Usage endpoint](https://docs.reversecontact.com/endpoints/usage) returns workspace credit information in `quotas`. **Python** ```python [Python] import os import httpx with httpx.Client(timeout=90.0) as client: response = client.get( "https://api.reversecontact.com/v2/usage", headers={"Authorization": f"Bearer {os.environ['RC_API_KEY']}"}, ) response.raise_for_status() print(response.json().get("quotas")) ``` Install `httpx` in your application before running this example. Raw HTTP responses use the API's camelCase fields. The SDK converts known fields to Python model attributes for you. ## Choose what your integration handles With a raw HTTP client, implement the API's retry and polling rules in your own code. The [Python SDK error guide](https://docs.reversecontact.com/sdk/python/errors-and-retries) and [Async jobs](https://docs.reversecontact.com/sdk/python/async-jobs) explain how `reversecontact` handles retries and jobs. For JavaScript or TypeScript applications, see the [TypeScript SDK overview](https://docs.reversecontact.com/sdk/javascript). Return to the [Python SDK overview](https://docs.reversecontact.com/sdk/python). ## FAQ ### Can I call Reverse Contact from Python without the SDK? The API uses JSON over HTTPS. Any Python HTTP client can send an `Authorization: Bearer` header with your API key. ### Where can I get the Reverse Contact OpenAPI specification? The OpenAPI specification is published at `https://docs.reversecontact.com/openapi.json` for generating your own typed models. ### Does a raw HTTP client handle Reverse Contact jobs automatically? With a raw HTTP client, you write the polling and retry logic. The `reversecontact` SDK provides typed results, retries and job waiting. --- # Use Reverse Contact from an AI agent > Connect Claude, ChatGPT, Cursor or a coding agent to the Reverse Contact API using MCP, the rc CLI or an agent brief, with docs your agent can read. - Page: https://docs.reversecontact.com/ai-agents - Last verified: 2026-11-04 Connect your chat assistant through MCP, or give your coding agent the agent skill and the `rc` CLI. You can also give any agent the agent brief. All three options use the same Reverse Contact API, with the same credit costs for each API call. ## Pick how your agent connects ### Chat assistants: the MCP server Use the API MCP server to work with Reverse Contact data in a Claude, ChatGPT or Cursor chat. Connect to `https://api.reversecontact.com/mcp` and sign in with OAuth or an API key, depending on your client. Follow [MCP setup by client](https://docs.reversecontact.com/guides/mcp#setup-by-client) to connect your assistant. The [available MCP tools](https://docs.reversecontact.com/guides/mcp#available-tools) cover enrichment, search, contact details, Lists, bulk enrichment, goals and utility calls. ### Coding agents: the agent skill and rc Use the `reversecontact-cli` skill when a coding agent writes or runs scripts. The skill teaches the agent to choose commands, read JSON results and exit codes, check credits and follow asynchronous jobs with the `rc` CLI. First [install rc](https://docs.reversecontact.com/guides/cli#install) and give the agent an API key through the `RC_API_KEY` environment variable. Then install the skill: **Install the skill** ```bash [Terminal] npx skills add visum-run/skills ``` The skill works with agents that read Agent Skills. The [Agent skills guide](https://docs.reversecontact.com/guides/agent-skills) includes manual installation instructions and an example session. For other authentication options, see [CLI sign-in](https://docs.reversecontact.com/guides/cli#sign-in). ### Any agent: the agent brief Paste the [agent brief](https://docs.reversecontact.com/agents#brief-to-paste-into-your-agent) into your agent's system prompt or instructions. Provide an API key through `RC_API_KEY`. The brief links to the API spec and documentation and gives the rules for credits, missing data, asynchronous results and retries. ## Give your agent the docs Give your agent the documentation index to find pages by task, or a Markdown copy to read one page. If your agent uses tools, connect the documentation MCP server to search and read pages. These resources describe the API and its behavior. | Resource | Address | What it is for | | -------- | ------- | -------------- | | Documentation index | [llms.txt](https://docs.reversecontact.com/llms.txt) | Every page as a Markdown link, organized by developer task, then the endpoint reference by family | | Full documentation | [llms-full.txt](https://docs.reversecontact.com/llms-full.txt) | Every documentation page in one text file | | Markdown copy of a page | Append `.md` to the page address, for example [agents.md](https://docs.reversecontact.com/agents.md) | Read one page as plain Markdown, with links to its web page, the index and the endpoint catalog | | Endpoint catalog | [catalog.json](https://docs.reversecontact.com/catalog.json) | Every endpoint with its method, path, credit cost, mode, status and docs page. Also lists replacements for retired endpoints and those under maintenance, and the last verified date for every page | | OpenAPI spec | [openapi.json](https://docs.reversecontact.com/openapi.json) or [openapi.yaml](https://docs.reversecontact.com/openapi.yaml) | Every operation with its parameters and responses, docs page (`externalDocs`), credit cost (`x-credits`), mode (`x-mode`) and status (`x-availability`) | | Documentation MCP server | `https://docs.reversecontact.com/mcp` | Search and read documentation over Streamable HTTP with `search_docs`, `get_page`, `list_pages` and `list_endpoints`. No authentication required | The documentation MCP server at `https://docs.reversecontact.com/mcp` reads the docs. To call the API, connect to `https://api.reversecontact.com/mcp` with OAuth or an API key, as described in [Connect AI agents (MCP)](https://docs.reversecontact.com/guides/mcp). ## FAQ ### How do I connect Claude to the Reverse Contact API? Connect Claude to the API MCP server at `https://api.reversecontact.com/mcp` using the MCP setup guide for your Claude client. The API MCP server accepts OAuth sign-in or an API key, depending on the client. ### How do I install the Reverse Contact agent skill? Install the `rc` CLI and provide an API key through `RC_API_KEY`. Then run `npx skills add visum-run/skills`. The `reversecontact-cli` skill teaches coding agents that read Agent Skills to use the CLI. ### What is the difference between the API MCP server and the docs MCP server? The API MCP server at `https://api.reversecontact.com/mcp` calls the Reverse Contact API and requires OAuth sign-in or an API key. The documentation MCP server at `https://docs.reversecontact.com/mcp` searches and reads documentation without authentication. ### Do AI agents use the same credits as the REST API? The API MCP server, the `rc` CLI and an agent following the agent brief all use the same Reverse Contact API. Each API call costs the same credits regardless of how the agent connects. ### How do I give Cursor the Reverse Contact API docs? Give Cursor `https://docs.reversecontact.com/llms.txt` to find pages, or append `.md` to a documentation page address to read the Markdown copy. The documentation MCP server at `https://docs.reversecontact.com/mcp` also provides tools to search and read the docs without authentication. --- # Connect AI agents (MCP) > Set up the Reverse Contact MCP server in Claude, ChatGPT, Codex or Cursor to enrich profiles, search people and companies, and find emails. - Page: https://docs.reversecontact.com/guides/mcp - Last verified: 2026-11-04 ## What is MCP The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) is an open standard that lets AI assistants call external tools directly. Once connected, your AI agent can enrich profiles, search people and companies, find email addresses and phone numbers, keep people in Lists and enrich files in bulk when you ask in plain language. Reverse Contact exposes 37 tools via MCP, with the same data, credits and rate limits as the REST API. ## Prerequisites - A Reverse Contact account - An MCP-compatible client (claude.ai, Cowork, Claude Desktop, Claude Code, ChatGPT, Codex, Cursor, etc.) Two ways to authenticate: - Sign in with your account (OAuth): clients with OAuth support (claude.ai, Cowork, Claude Desktop, Claude Code, mobile, ChatGPT) connect with no API key. The first connection opens your browser, you approve the access, and a dedicated API key named `MCP - {app}` is created in your workspace automatically. - API key: every other client authenticates with an active API key (`rc_*`) in the `Authorization` header. Create one from [API Keys](https://app.reversecontact.com/api-keys). ## Server details | Property | Value | |----------|-------| | URL | `https://api.reversecontact.com/mcp` | | Transport | Streamable HTTP (POST only) | | Authentication | OAuth 2.1 (sign in) or `Authorization: Bearer rc_your_api_key` | | Protocol | JSON-RPC 2.0 | ## Setup by client For clients using an API key, replace `rc_your_api_key` with your actual key in the examples below. ### Claude.ai, Claude Desktop, Cowork and mobile Reverse Contact is published in the Claude connector directory, so there is nothing to paste and no API key involved: 1. Open [our page in the Claude connector directory](https://claude.ai/directory/reverse-contact). 2. Click Connect: your browser opens the Reverse Contact consent page. Sign in if needed, pick the workspace shown, and approve. The connector is then available in claude.ai chats, Claude Desktop, Cowork, Claude Code on the web and the mobile apps. To revoke it later, delete its `MCP - {app}` key from [API Keys](https://app.reversecontact.com/api-keys). To add it by hand, for example when your organization restricts the directory, open Settings > Connectors > Add custom connector, paste `https://api.reversecontact.com/mcp` as the URL, confirm, then click Connect and approve the same consent page. ### Claude Code Claude Code supports HTTP MCP servers natively. Add Reverse Contact with one command in your terminal, no API key needed: **Terminal** ```bash [bash] claude mcp add --transport http reversecontact https://api.reversecontact.com/mcp ``` Then start a Claude Code session and run `/mcp`: select `reversecontact` and authenticate. Your browser opens the consent page, you approve, and the tools come online. For headless or CI environments where the browser flow is not an option, pass an API key explicitly instead: **Terminal** ```bash [bash] claude mcp add --transport http reversecontact \ https://api.reversecontact.com/mcp \ --header "Authorization: Bearer rc_your_api_key" ``` Alternatively, add a `.mcp.json` file at the root of your project to share the server with your team: **.mcp.json** ```json [JSON] { "mcpServers": { "reversecontact": { "type": "http", "url": "https://api.reversecontact.com/mcp", "headers": { "Authorization": "Bearer rc_your_api_key" } } } } ``` ### ChatGPT Reverse Contact is published in the ChatGPT plugin directory, so there is nothing to paste and no API key involved: 1. Open [our page in the ChatGPT plugin directory](https://chatgpt.com/plugins/plugin_asdk_app_6a709e3aa59c81919cf58da3451d6bc2). 2. Click Add: your browser opens the Reverse Contact consent page. Sign in if needed, pick the workspace shown, and approve. The connector is then available in ChatGPT. Enable it from the composer (plus menu) in any chat. To revoke it later, delete its `MCP - {app}` key from [API Keys](https://app.reversecontact.com/api-keys). To add it by hand, for example when your organization restricts the directory, enable Developer mode under Settings > Connectors > Advanced settings (paid ChatGPT plan: Plus, Pro, Business, Enterprise or Edu), then click Create to add a connector: name it `Reverse Contact`, paste `https://api.reversecontact.com/mcp` as the URL, keep Authentication on OAuth, and approve the same consent page. ### Codex The Codex CLI reads the bearer token from an environment variable, so the key never lands in a config file: **Terminal** ```bash [bash] export RC_API_KEY="rc_your_api_key" codex mcp add reversecontact \ --url https://api.reversecontact.com/mcp \ --bearer-token-env-var RC_API_KEY ``` Add the `export` line to your shell profile so the variable is set whenever Codex launches. This writes the server to `~/.codex/config.toml`: **config.toml** ```toml [TOML] [mcp_servers.reversecontact] url = "https://api.reversecontact.com/mcp" bearer_token_env_var = "RC_API_KEY" ``` ### Cursor Create a `.cursor/mcp.json` file at the root of your project. Cursor connects over HTTP natively, keyed on `url` (no `type` field): **.cursor/mcp.json** ```json [JSON] { "mcpServers": { "reversecontact": { "url": "https://api.reversecontact.com/mcp", "headers": { "Authorization": "Bearer rc_your_api_key" } } } } ``` ### Gemini CLI Gemini CLI supports remote Streamable HTTP MCP servers. Add Reverse Contact to `~/.gemini/settings.json` for your user account, or `.gemini/settings.json` for one project: **settings.json** ```json [JSON] { "mcpServers": { "reversecontact": { "httpUrl": "https://api.reversecontact.com/mcp", "headers": { "Authorization": "Bearer rc_your_api_key" } } } } ``` Run `gemini mcp list` to check the connection. The Gemini app's custom connector surface is separate and does not support this setup yet. ### Other clients (mcp-remote) Most clients that support HTTP MCP servers accept the same URL and `Authorization: Bearer rc_your_api_key` header through their own config format. If your MCP client only supports stdio transport, use `mcp-remote` as a bridge: **mcp-remote** ```bash [bash] npx mcp-remote https://api.reversecontact.com/mcp \ --header "Authorization: Bearer rc_your_api_key" ``` This works with any client that supports stdio-based MCP servers. ## Available tools ### Enrichment | Tool | What it does | Credits | Response | |------|-------------|---------|----------| | `enrich_person` | Get a person's Social profile from cache, or find it by email/name | 1 (cached) / 2 (by email/name, if found) | Instant | | `enrich_person_live` | Force a fresh social profile scrape for a person profile from a URL or `prs_…` id | 2 (live) | Async | | `enrich_company` | Get a company's Social profile from cache, or find it by domain (description, size, HQ, industry) | 1 (cached) / 2 (by domain, if found) | Instant | | `enrich_company_live` | Force a fresh social profile scrape for a company profile from a URL or `com_…` id | 2 (live) | Async | ### Search | Tool | What it does | Credits | Response | |------|-------------|---------|----------| | `search_persons` | Search people by name, title, company, industry, location, skills, languages, keywords or recent job change | 1 / profile delivered | Instant | | `search_persons_lookalike` | Find people who look like one seed profile, from its Social profile URL | 1 / profile delivered | Instant | | `count_persons` | Count the people matching the `search_persons` filters, without returning any profile | 0 | Instant | | `search_companies` | Search companies by name, domain, industry, specialities, size, founding year, followers or location, and get full company profiles | 1 / company delivered | Instant | | `count_companies` | Count the companies matching the `search_companies` filters, without returning any profile | 0 | Instant | | `list_person_search_filters` | List the filters accepted by `search_persons` and `count_persons`, with their allowed values | 0 | Instant | | `list_company_search_filters` | List the filters accepted by `search_companies` and `count_companies`, with their allowed values | 0 | Instant | `count_persons` accepts exactly the filters of `search_persons`, without `cursor` or `perPage`, and returns the total across all pages: the same number as [People search count](https://docs.reversecontact.com/endpoints/search-people-count). `search_companies` runs [Company search](https://docs.reversecontact.com/endpoints/search-companies): it returns full company profiles, the same shape as the Company profile endpoint, with cursor pagination (`perPage` from 1 to 100, default 25, and `cursor` set to the previous `nextCursor`), and an empty page costs 0 credits. `count_companies` accepts exactly its filters, without `cursor` or `perPage`, and returns the same total as [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count). `search_persons_lookalike` reads its criteria from the seed profile: its current function and sub-function with their tenure, its seniority and its location. It takes only `url`, `perPage` (1 to 100, default 25) and `cursor`, never returns the seed, and returns full profiles shaped like `search_persons`, with the same cursor pagination. A search filter is refused with `LOOKALIKE_FILTERS_NOT_SUPPORTED`, and a page with no profile or a seed that is not in the search index yet costs 0 credits. It serves the same results as [People lookalike](https://docs.reversecontact.com/endpoints/search-people-lookalike). The filter catalogs return every filter with its type, description and example. An allowed-value list longer than 50 entries (industries, sub-functions, country codes) is returned as its size in `allowedValuesCount`; name the field in the `fields` parameter to get its complete list. They serve the same catalogs as [People search available fields](https://docs.reversecontact.com/endpoints/search-people-available-fields) and [Company search available fields](https://docs.reversecontact.com/endpoints/search-companies-available-fields). ### Contact | Tool | What it does | Credits | Response | |------|-------------|---------|----------| | `find_email` | Find a person's email addresses from a Social profile URL, or from a name and company domain: professional by default, personal or either with `emailType` | 3 for a professional email, 5 for a personal one, whatever the `emailType`: `any` costs 3, 5 or 8 (refunded if no email found) | Async | | `find_phone` | Find a person's phone numbers from a Social profile URL, or from a name and company domain | 20 (refunded if no phone number found) | Async | Both tools take a Social profile `url`, or a name (`fullName`, or `firstName` and `lastName`) with `companyDomain`. The credits are debited when the job is created and refunded when nothing is found. `find_email` debits 3 credits, or 5 when `emailType` is `personal` or `any`, and adjusts an `any` lookup to what it found: 3 for a professional email only, 5 for a personal one only, 8 for both (see [Credits by email type](https://docs.reversecontact.com/endpoints/contact-email#credits-by-email-type)). Both tools return a `taskId` (with `webhookId` and `pollUrl`), and the result is retrieved with `check_task`, as described in [Async workflow](#async-workflow). A workspace without phone finding access gets `CONTACT_PHONE_FINDER_DISABLED` from `find_phone` and is not charged. They run the same lookups as [Find Email](https://docs.reversecontact.com/endpoints/contact-email) and [Find Phone](https://docs.reversecontact.com/endpoints/contact-phone). ### Lists | Tool | What it does | Credits | Response | |------|-------------|---------|----------| | `create_list` | Create an empty List of people or of companies, kept in the workspace and shown in the app | 0 | Instant | | `list_lists` | List the Lists of the workspace, most recently updated first | 0 | Instant | | `get_list` | Read one List and a page of its members and Pending Intake, or find members by your `externalId` | 0 | Instant | | `add_list_members` | Add up to 1,000 people or companies now and get one result per Signal | 1 or 2 per new member | Instant | | `import_to_list` | Import up to 25,000 Signals, or a public CSV file or Google Sheet, and buy profiles, companies, emails or phone numbers for the members | Per new member and per family bought, within `maxCredits` | Async | | `get_list_operation` | Follow an import, with `wait` up to 20 seconds, and read its per-line results | 0 | Instant | | `cancel_list_operation` | Stop an import: lines that had not started are never charged | 0 | Instant | | `remove_list_members` | Remove members from a List, or dismiss Pending Intake lines | 0 | Instant | | `export_list` | Prepare a CSV of the List and return a download link valid for 15 minutes | 0 | Instant | | `delete_list` | Delete a List with its members, once the user asked for it | 0 | Instant | The List tools run the [List endpoints](https://docs.reversecontact.com/guides/lists) with the same credits. With `families` and a worst case above 25 credits, `import_to_list` needs `maxCredits`, the amount the user agreed to spend: the import stops at that ceiling, and `import_to_list` called again with the same `operationId` resumes it without charging anything twice. `export_list` and `get_job_results` return a link for the user, not the rows, and a workspace exports one List at a time. `delete_list` takes `confirm: true` to state that the user asked for the deletion. ### Bulk enrichment | Tool | What it does | Credits | Response | |------|-------------|---------|----------| | `bulk_enrich_persons` | Enrich up to 25,000 people in one job with their profile, an email or a phone number, without a List | Each item as its unit call, only for what is found, within `maxCredits` | Async | | `get_job` | Follow a job, with `wait` up to 20 seconds | 0 | Instant | | `get_job_results` | Return a download link for the results file, CSV or NDJSON with `format`, valid for 15 minutes, once the job is completed or cancelled | 0 | Instant | | `get_job_rows` | Read a page of results into the conversation, even while the job runs: 10 rows by default, 20 at most, as a summary unless `detail` is `full` | 0 | Instant | | `cancel_job` | Stop a job an API key or an AI assistant started: items that had not started are never charged | 0 | Instant | | `list_jobs` | List every job of the workspace, newest first, whoever started it: Bulk enrichment, List imports and Search exports | 0 | Instant | `bulk_enrich_persons` runs [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons): the credits are reserved when the job starts and what an item does not find is given back. Above 25 credits reserved, it needs `maxCredits`, the amount the user agreed to spend: a job that would reserve more is refused with `JOB_MAX_CREDITS`, which gives the credits the job needs, and nothing is reserved. Pass `idempotencyKey` to retry the call safely. `get_job_rows` runs [Read job results by page](https://docs.reversecontact.com/endpoints/job-rows): a page stops at the first item not finished, and `nextCursor`, passed back as `cursor`, reads on. A summary row gives who was found, their title, company and location; `detail: full` returns the whole profile, which is heavy, so keep it for the few rows that need it. For the whole file, `get_job_results` returns a link for the user: a large result never goes through the conversation. ### Goals A goal keeps a long task on your Lists going from one conversation to the next: the request it serves, criteria the server checks on the Lists, the Lists it works on and a credit budget. A new conversation, or another AI assistant, finds the goal with `list_goals` and resumes it with `get_goal`. | Tool | What it does | Credits | Response | |------|-------------|---------|----------| | `create_goal` | Open a goal: the request it serves, the criteria to meet on your Lists, the Lists it works on and its credit budget | 0 | Instant | | `list_goals` | List the goals of the workspace, newest first: the open ones (active, paused or blocked) by default, every goal, or one status | 0 | Instant | | `get_goal` | Read where a goal stands, to resume it: each criterion measured now on the Lists, the budget spent, held and left, the operations still running, the plan and the decisions taken | 0 | Instant | | `update_goal` | Steer a goal: revise it, pause, block or cancel it, complete it, record a decision, update the plan, or take over from an assistant that stopped | 0 | Instant | The criteria count members of your Lists: the members of a List, the members with a profile, company details, an email or a phone number, or the members with a value in one of your columns. `update_goal` with `complete` closes the goal only when every criterion holds, and `accept_partial` closes it with a reason when the result is enough. A goal described in words (`described` criteria) has nothing to measure and closes with `accept_partial`. The assistant that opens a goal is its Pilot. A goal has one Pilot at a time, and the Pilot's lease runs for 15 minutes after its last action: once it lapses, another assistant takes over with `update_goal` (`take_over`), and the goal keeps its budget. Pass the `goalId` to a billed tool (`enrich_person`, `enrich_person_live`, `enrich_company`, `enrich_company_live`, `search_persons`, `search_persons_lookalike`, `search_companies`, `find_email`, `find_phone`, `add_list_members` or `import_to_list`) and the call spends within the goal's budget: it runs without asking the user again, `import_to_list` included above 25 credits, and a call that would go past the budget left is refused with `GOAL_BUDGET_REFUSED` and its cost, with nothing charged. Only the Pilot spends on a goal, and only while the goal is active. Set `budget.maxCredits` to the amount the user agreed to spend: a goal without a budget puts no ceiling on the calls that carry its `goalId`, and raising the budget takes `update_goal` with `revise`. Goals are free and need the Lists scope on the API key. A workspace keeps up to 20 open goals, and one at a time during the free trial. An open goal with no Pilot and nothing running for 30 days is paused. ### Utility (free) | Tool | What it does | Credits | Response | |------|-------------|---------|----------| | `check_person` | Check whether a person profile is cached and how fresh it is, from a Social URL or `prs_…` id | 0 | Instant | | `check_company` | Check whether a company profile is cached and how fresh it is, from a Social URL or `com_…` id | 0 | Instant | | `check_usage` | Check your credit balance and rate limit status | 0 | Instant | | `check_task` | Poll the status of an async operation | 0 | Instant | `enrich_company` takes a Social `url` or a `domain`. A domain runs [Enrich company](https://docs.reversecontact.com/endpoints/enrich-company) and returns the full company profile under `data.company`, and a domain that matches no company costs 0 credits. When you send both, the `url` is used. ## Example prompts Once the MCP server is connected, ask your AI agent in natural language. These examples map to the tools above and use the same credits as the REST API. 1. Enrich a person from an email: > Enrich the Social profile for `alex@acme.com` and summarize current title, company, and recent experience. 2. Find a company from a domain: > Enrich the company with domain `stripe.com` and return its industry, employee range, headquarters, and a one-paragraph description. 3. Search people with filters: > Find people who are Heads of Sales or VPs of Sales at SaaS companies in the United States. Return the first page of results with name, title, and company. 4. Size an audience before paying for it: > How many sales directors work in France? Count them first, and return the first 25 profiles only if there are fewer than 5,000. 5. Search companies with filters: > How many fintech companies with 51 to 200 employees, founded since 2015, are headquartered in France? Count them first, then list the first 25 with name, domain, and size. 6. Check usage before a batch: > Check my Reverse Contact credit balance and rate limit status before I run a larger enrichment batch. 7. Run an async live enrichment: > Live-enrich the Social profile `https://www.social.com/in/example` with `enrich_person_live` and, if the tool returns a task id, poll until the result is ready, then summarize the profile. 8. Find people like a profile: > Find 25 people who look like `https://www.social.com/in/example` and list their name, title, company and location. 9. Find a professional email: > Find the professional email address of Jane Doe at `acme.com`, wait for the result, then give me the address. 10. Keep people in a List and buy their emails: > Create a List called "Fintech founders", add the people of my last search to it, then find their professional emails. Tell me the worst case cost first and ask me before you spend it. 11. Enrich a file in bulk: > Enrich these 300 Social profile URLs with their profile and professional email in one bulk job, wait for it to finish, then give me the link to the results CSV. 12. Work toward a goal across conversations: > Open a goal: my List "Fintech founders" should hold 200 people with a professional email, within a budget of 1,000 credits. Start now, and when I come back in a new conversation, resume the goal and tell me where it stands. For live enrichments and contact lookups, the agent receives a `taskId` from `enrich_person_live`, `enrich_company_live`, `find_email` or `find_phone` and should call `check_task` until the status is `succeeded` or `errored`. You usually do not need to manage that loop yourself. ## Async workflow The live tools (`enrich_person_live`, `enrich_company_live`) and the contact tools (`find_email`, `find_phone`) run asynchronously. They return a `taskId` instead of data: **Async response** ```json [JSON] { "status": "created", "webhookId": "abc123", "pollUrl": "/v2/webhooks/abc123", "taskId": "abc123", "message": "Scraping LinkedIn profile. Get the result with check_task (wait: 20)." } ``` Use `check_task` with the `taskId` to poll for results, with `wait: 20` so each call waits for the task to finish. Tasks typically complete in 10 to 60 seconds. While a task runs, `check_task` answers with the status `in_progress`, and the task ends `succeeded` or `errored`. When the task has succeeded, `check_task` returns the data payload. Your AI agent handles this polling automatically. ## Credits and rate limits MCP tools consume the same credits and respect the same rate limits as the REST API: - MCP calls use the same endpoint pricing as REST. - MCP calls count toward the same gateway RPM limits as REST API calls. `count_persons` and `count_companies` are free but still count, like their REST endpoints; `check_usage`, `check_task` and the two filter catalogs do not. Use `check_usage` to inspect your current balance before expensive operations, and `count_persons` or `count_companies` to size a search for free before `search_persons` or `search_companies` bills each profile or company delivered. [Rate limits](https://docs.reversecontact.com/guides/rate-limits) covers quotas and throttling. ## Troubleshooting | Error | Cause | Fix | |-------|-------|-----| | `Authentication required` (code -32001) | Missing or invalid credentials | OAuth clients: re-authenticate from the client (the access expires if its key is deleted). API key clients: check the key is active and the `Bearer` prefix is present | | `SSE streaming not supported` (code -32601) | Client sent a GET request | Your client must use POST. Use `mcp-remote` if it defaults to SSE | | `RATE_LIMIT_EXCEEDED` | Too many requests per minute | Wait for the rate limit window to reset. Check limits with `check_usage` | | `INSUFFICIENT_CREDITS` | Not enough credits for the operation | Top up credits from the dashboard | | `KEY_CREDIT_LIMIT_REACHED` | The API key the client uses reached one of its credit limits (0 credits) | Wait for the daily or monthly reset, or raise or reset the limit in [API Keys](https://app.reversecontact.com/api-keys) | | `LOOKALIKE_FILTERS_NOT_SUPPORTED` | `search_persons_lookalike` received a search filter (0 credits) | Send only `url`, `perPage` and `cursor`, or use `search_persons` to combine filters | | `CONTACT_PHONE_FINDER_DISABLED` | Phone finding is not enabled for the workspace (0 credits) | Use `find_email`, or ask us to enable phone finding | | `JOB_MAX_CREDITS` | `bulk_enrich_persons` would reserve more than 25 credits without `maxCredits`, or more than `maxCredits` (0 credits) | Ask the user to agree to the credits the error gives, then call again with that `maxCredits`, or send fewer items | | `GOAL_BUDGET_REFUSED` | A call with a `goalId` does not fit the goal: the budget left is too small, the goal is not active, the tool is outside the goal, or the workspace lacks credits (0 credits) | Read the goal with `get_goal`, then ask the user to raise the budget (`update_goal` with `revise`) or to resume the goal | | `GOAL_PILOT_CONFLICT` | Another assistant is the Pilot of the goal (0 credits) | Wait until its lease lapses, 15 minutes after its last action, then call `update_goal` with `take_over` | | `GOAL_LIMIT_REACHED` | The workspace already has 20 open goals, or 1 during the free trial (0 credits) | Complete or cancel a goal with `update_goal`, then open the new one | | Connection timeout | Client cannot reach the server | Verify the URL is `https://api.reversecontact.com/mcp` and your network allows HTTPS | ## FAQ ### Do I need an API key to use the Reverse Contact MCP server? No, not with a client that supports OAuth (claude.ai, Cowork, Claude Desktop, Claude Code, mobile and ChatGPT): you sign in with your account, approve the access in your browser, and a dedicated API key named `MCP - {app}` is created in your workspace automatically. Every other MCP client authenticates with an active Reverse Contact API key (`rc_*`) in the `Authorization` header. ### Do Reverse Contact MCP tools cost the same as REST API calls? Yes, Reverse Contact MCP tools use the same endpoint pricing and count toward the same gateway RPM limits as the REST API. `count_persons` and `count_companies` are free but still count toward RPM, while `check_usage`, `check_task`, `list_person_search_filters` and `list_company_search_filters` do not. Details on quotas and throttling are in [Rate limits](https://docs.reversecontact.com/guides/rate-limits). ### How long does a live enrichment or email lookup take over MCP? The asynchronous Reverse Contact MCP tools (`enrich_person_live`, `enrich_company_live`, `find_email` and `find_phone`) return a `taskId` instead of data, and tasks typically complete in 10 to 60 seconds. The AI agent calls `check_task` with that `taskId` until the status is `succeeded` or `errored`, so you usually do not need to manage that loop yourself. The full flow is in [Async workflow](#async-workflow). ### Am I charged when the MCP email or phone finder finds nothing? No. The Reverse Contact MCP tools `find_email` and `find_phone` debit credits when the job is created and refund them when no email or phone number is found. `find_email` is billed on the emails found, not on `emailType`: 3 credits for a professional email and 5 for a personal one, so a lookup with `emailType` set to `any` costs 3, 5 or 8 (8 only when it finds both), and `find_phone` costs 20 credits. ### Can the Reverse Contact MCP server look up a company by its domain? Yes. The Reverse Contact MCP tool `enrich_company` takes a `domain` and returns the full company profile. It costs 2 credits when a company is found and 0 when none is. With a Social `url` instead, `enrich_company` costs 1 credit, and the `url` is used when both are sent. --- # Agent skills > Install the Reverse Contact skill so coding agents such as Claude Code can enrich, fetch, check and search with the rc CLI, at the lowest credit cost. - Page: https://docs.reversecontact.com/guides/agent-skills - Last verified: 2026-10-15 ## Introduction An agent skill is a folder of instructions (`SKILL.md` and its references) that a coding agent loads when a task calls for it. The `reversecontact-cli` skill teaches an agent to drive the [Reverse Contact CLI](https://docs.reversecontact.com/guides/cli): enrich people and companies, fetch by Social URL or id, search, check freshness for free, find contact emails and phone numbers, follow live jobs and inspect credits. It is published in the public repository [visum-run/skills](https://github.com/visum-run/skills) and works with any agent that reads Agent Skills, such as Claude Code. Each CLI release publishes its skill there. ## What the agent learns - It picks the cheapest command for the input it has. With a profile URL or a Reverse Contact id, it fetches (1 credit) rather than enriches (2 credits), and runs the free `rc check` first when freshness matters. It enriches only from an email, a name or a domain. - Before a batch, it reads the credit balance and the rate limits with `rc usage`, which is free. - It reads results from the JSON output and the exit codes of `rc`: a not-found result exits `0` and costs nothing, `3` means no valid API key, `4` a network problem. - It starts a live fetch, an email lookup or a phone lookup, keeps the job id, and reads the result later with `rc task` instead of waiting in a loop. - It lists the filters the API accepts with `rc search fields people` and `rc search fields organizations`, which are free, instead of inventing search filters. ## Install The skill needs the `rc` binary and an API key: see [Install](https://docs.reversecontact.com/guides/cli#install) and [Sign in](https://docs.reversecontact.com/guides/cli#sign-in) on the CLI page. Give the agent the key through the `RC_API_KEY` environment variable, which writes nothing to disk. Then add the skill to your agent with the skills CLI: **Install the skill** ```bash [Terminal] npx skills add visum-run/skills ``` To install it by hand, copy the `reversecontact-cli` folder of the [repository](https://github.com/visum-run/skills) into the skills folder of your agent. For Claude Code, that is `.claude/skills/` in a project, or `~/.claude/skills/` for every project. ## A worked session You ask the agent: "Who is jane.doe@acme.com, and what is her current role?" With the skill loaded, the agent runs: **Agent session** ```bash [Terminal] # 1. Is a valid API key available? Exit code 3 means no. rc auth status --json # 2. An email, no profile URL: enrich, keeping only the fields needed. rc enrich person --email jane.doe@acme.com \ --fields firstName,lastName,currentPositionTitle,currentCompanyName,linkedinUrl ``` With `--fields`, `rc` prints only those fields, flat, without the response envelope, and the agent answers from them. Had no one matched, `rc` would have exited `0` and charged nothing, and the agent would say so rather than guess. ## Skill or MCP server Both reach the same API, with the same credits. | Prefer | When | | ------ | ---- | | The [MCP server](https://docs.reversecontact.com/guides/mcp) | A conversational agent (Claude, ChatGPT, Cursor) works with Reverse Contact data in a chat | | The skill and the CLI | A coding agent writes or runs scripts: batches, pipes, exit codes, keeping only some fields | ## FAQ ### How do I install the Reverse Contact agent skill? Install the `rc` CLI and give it an API key, then run `npx skills add visum-run/skills`. To install it by hand, copy the `reversecontact-cli` folder of the visum-run/skills repository into your agent's skills folder, such as `.claude/skills/` for Claude Code. ### Does the Reverse Contact agent skill cost credits? The skill itself is free. The commands it runs are billed like the Reverse Contact API endpoints they call, and the skill steers the agent to the cheapest one: a free `rc check` before a 1-credit `rc fetch`, and `rc fetch` rather than a 2-credit `rc enrich` when a profile URL is known. ### Should I use the Reverse Contact agent skill or the MCP server? Use the MCP server for a conversational agent working in a chat, and the skill with the `rc` CLI for a coding agent that writes or runs scripts. Both call the same Reverse Contact API with the same credits. --- # Agent brief > Everything an AI agent needs to call the Reverse Contact API: the entry points, and a brief to paste into its instructions. - Page: https://docs.reversecontact.com/agents - Last verified: 2026-10-15 ## Introduction This page is for developers who build an AI agent on the Reverse Contact API, and for the agents themselves. An agent finds the API's description at the entry points below. The brief after them goes into the agent's instructions, with the rules that keep the agent from spending credits for nothing or misreading an answer. The Markdown copy of this page is at `https://docs.reversecontact.com/agents.md`. ## Entry points | Resource | Address | What it is for | | -------- | ------- | -------------- | | OpenAPI spec | `https://docs.reversecontact.com/openapi.json` and `/openapi.yaml` | Every operation with its parameters and responses, its docs page, its credit cost (`x-credits`) and its status (`x-availability`) | | Endpoint catalog | `https://docs.reversecontact.com/catalog.json` | Every endpoint with its method, path, credit cost, mode and status, the retired endpoints and their replacement, and every page | | Documentation index | `https://docs.reversecontact.com/llms.txt` | Every page of this documentation by task, as Markdown links; `llms-full.txt` holds them all in one file | | API MCP server | `https://api.reversecontact.com/mcp` | Tools that call the API from an agent: see [Connect AI agents](https://docs.reversecontact.com/guides/mcp) | | Documentation MCP server | `https://docs.reversecontact.com/mcp` | Tools that search and read this documentation, with no authentication | | CLI and agent skill | `rc`, and `npx skills add visum-run/skills` | The API from a terminal or a coding agent: see [CLI](https://docs.reversecontact.com/guides/cli) and [Agent skills](https://docs.reversecontact.com/guides/agent-skills) | | Base URL | `https://api.reversecontact.com` | Every endpoint: the paths start with `/v2`, as in the spec | | Authentication | `Authorization: Bearer rc_your_api_key` | Every request: see [Authentication](https://docs.reversecontact.com/authentication) | ## Brief to paste into your agent Copy this block into the system prompt or the instructions of your agent, and give it the API key in the `RC_API_KEY` environment variable. **Agent brief** ```text [Brief] You can call the Reverse Contact API for B2B people and company data. Spec (OpenAPI 3.1): https://docs.reversecontact.com/openapi.json Endpoints with their credit cost and status: https://docs.reversecontact.com/catalog.json Documentation index: https://docs.reversecontact.com/llms.txt Base URL: https://api.reversecontact.com (every path below starts with /v2) Auth: Authorization: Bearer $RC_API_KEY (keys start with rc_) - Requests and responses are JSON. Every response has success, data and error. Most also carry quotas and metadata, but a 401, 410 or 429 has no quotas. - Each operation's x-credits field gives its cost. When a response has quotas, quotas.creditsConsumed says what the call cost. - Check the balance first with GET /v2/usage (free). - With a profile URL: POST /v2/fetch/persons/check (free, but it needs at least 2 credits in the workspace) says whether the profile exists and when it was last updated. Then POST /v2/fetch/persons (1 credit) for the cached copy, or POST /v2/fetch/persons/live (2 credits) when it is missing or too old. - With an email or a name instead: POST /v2/enrich/persons (2 credits, only when a person is found). - Size a people search with POST /v2/search/people/count (free) before POST /v2/search/people (1 credit per profile delivered). GET /v2/search/people/available-fields (free) lists the filters: an unknown filter returns 422 and costs nothing. - Companies work the same way: POST /v2/search/organizations/count (free) before POST /v2/search/organizations (1 credit per company delivered, full profiles), and GET /v2/search/organizations/available-fields (free) for the filters. - On enrich, a 404 means no confident match; on the cached fetch, a profile not in the cache. Both are free, and the API never returns a guess instead. Report "not found"; do not retry. - null in a field means unknown. It is not zero and not an empty value. - Live fetches, Find Email and Find Phone answer with data.webhookId. Poll GET /v2/webhooks/{webhookId}?wait=20 (free, not rate limited) until status is succeeded or errored, with an HTTP timeout above 20 seconds. Each call answers as soon as the job ends, or after at most 20 seconds with the job still running. While it runs, start two polls at least Retry-After seconds apart (2 if the header is missing): after a held poll, poll again right away; after an early answer, pause first. A 5xx on a poll: wait its Retry-After if it has one, else the 5xx rule below. - 429: wait error.details.retryAfter seconds, then retry. 500, 502, 503 or 504: retry with exponential backoff, a few times at most. Any other error: fix the request, do not retry. - 451: the person or company is blocked under GDPR or CCPA. Drop the record and never retry it. - /v2/search/persons is retired (410 Gone): use /v2/search/people. - /v2/search/companies goes into maintenance on November 9, 2026 (410 while it lasts): use /v2/search/organizations. ``` ## Why these rules Each line of the brief comes from a page of this documentation: - Free checks and counts before paid calls: [Data freshness](https://docs.reversecontact.com/guides/data-freshness), [People search count](https://docs.reversecontact.com/endpoints/search-people-count), [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count) and [Credits](https://docs.reversecontact.com/guides/credits). - A `404` rather than a guess, and `null` as unknown: [Data quality](https://docs.reversecontact.com/guides/data-quality) and [Missing and empty fields](https://docs.reversecontact.com/guides/missing-and-empty-fields). - Jobs and polling: [Polling](https://docs.reversecontact.com/guides/polling). - What to retry: [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries). - The retired endpoints: [Endpoint transition on October 13, 2026](https://docs.reversecontact.com/guides/endpoint-transition-october-2026). `/v2/search/companies` during its maintenance: [Migrating company search to the new endpoint](https://docs.reversecontact.com/guides/migrating-search-companies-to-organizations). ## FAQ ### How do I give an AI agent access to the Reverse Contact API? Give the agent an API key in the `RC_API_KEY` environment variable and paste the brief of this page into its instructions: it points to the OpenAPI spec at `https://docs.reversecontact.com/openapi.json` and states the credit and retry rules. A conversational agent can instead connect to the Reverse Contact MCP server at `https://api.reversecontact.com/mcp`. ### How does an agent know what a Reverse Contact endpoint costs? Each operation of the Reverse Contact OpenAPI spec carries an `x-credits` field with its cost, and `https://docs.reversecontact.com/catalog.json` lists every endpoint with the same information. After a call, `quotas.creditsConsumed` says what it cost, in the responses that carry `quotas` (a `401`, `410` or `429` does not). ### Is there an MCP server for the Reverse Contact documentation? Yes. `https://docs.reversecontact.com/mcp` is an MCP server, over Streamable HTTP and with no authentication, whose tools search and read this documentation. It is separate from the API MCP server at `https://api.reversecontact.com/mcp`, which calls the API itself. --- # Command line interface (CLI) > Install rc, the Reverse Contact command line tool, sign in, and check, fetch, enrich and search, enrich whole files in bulk and fill your Lists from your terminal or a script. - Page: https://docs.reversecontact.com/guides/cli - Last verified: 2026-11-04 ## Introduction `rc` is the official Reverse Contact command line tool: one binary for macOS, Linux and Windows that calls the same API with the same credits and rate limits, and prints JSON ready for `jq` or a script. The [agent skill](https://docs.reversecontact.com/guides/agent-skills) teaches coding agents to use it. The current release is `rc` 0.7.0. ## Install **Install** ```bash [macOS and Linux] curl -fsSL https://app.reversecontact.com/cli/install.sh | sh ``` ```bash [Homebrew] brew install visum-run/tap/rc ``` ```powershell [Windows] powershell -c "irm https://app.reversecontact.com/cli/install.ps1 | iex" ``` The install scripts download the binary for your platform, check it against the published SHA-256 checksum, and install it in `~/.local/bin` on macOS and Linux, or `%LOCALAPPDATA%\Programs\rc` on Windows. Set `RC_INSTALL_DIR` to install elsewhere, or `RC_VERSION` to pin a version. Run `rc version` to see the installed version, and whether a newer one is out: it reads `https://app.reversecontact.com/cli/manifest.json`. ## Sign in The CLI needs an API key from [API Keys](https://app.reversecontact.com/api-keys). It looks for one in this order: the `--api-key` flag, the `RC_API_KEY` environment variable, then the key stored by `rc auth login`. **Sign in** ```bash [Browser] # Opens a page where you approve the access from your workspace. rc auth login --browser ``` ```bash [API key] rc auth login rc_your_api_key ``` ```bash [Script or CI] # Nothing written to disk: export RC_API_KEY=rc_your_api_key # Or store the key from a secret manager: echo "$RC_API_KEY" | rc auth login --with-token ``` `rc auth status` shows where the key comes from and the credit balance of the workspace; it exits with code `3` when no valid key is found. `rc auth logout` removes the stored key. With `--browser` on a machine without a browser, add `--no-open` to print the address to open instead. The stored key lives in `~/.config/reversecontact/config.json` on macOS and Linux (`$XDG_CONFIG_HOME/reversecontact/config.json` when it is set), and in `%APPDATA%\reversecontact\config.json` on Windows. ## Commands Each command calls the API and costs the same credits as the API calls it makes. | Command | What it does | Credits | | ------- | ------------ | ------- | | `rc check person` or `rc check company` with `--url` or `--id` | Whether a profile exists and when it was last updated ([Person profile status](https://docs.reversecontact.com/endpoints/check-person)) | Free | | `rc fetch person` or `rc fetch company` with `--url` or `--id` | The cached profile ([Person profile](https://docs.reversecontact.com/endpoints/fetch-profile)) | 1 when found, a `404` is free | | `rc fetch person --url ... --live` | A fresh copy, as a job ([Person profile live](https://docs.reversecontact.com/endpoints/fetch-profile-live)) | 2 per request | | `rc enrich person` with `--email`, or `--first-name`, `--last-name` and `--company-domain` or `--company-name` | The person behind an email or a name ([Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person)) | 2 when found, a `404` is free | | `rc enrich company --domain` | The company behind a domain ([Enrich company](https://docs.reversecontact.com/endpoints/enrich-company)) | 2 when found, a `404` is free | | `rc search people` with filters | People matching filters: the CLI counts them first with the free [People search count](https://docs.reversecontact.com/endpoints/search-people-count), then runs [People search](https://docs.reversecontact.com/endpoints/search-people). The next page comes with `--cursor` | 1 per profile delivered, the count is free | | `rc search organizations` with filters | Companies matching filters, as full profiles: the CLI counts them first with the free [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count), then runs [Company search](https://docs.reversecontact.com/endpoints/search-companies). `--limit` sets the page size and the next page comes with `--cursor` | 1 per company delivered, the count is free | | `rc search fields people` or `rc search fields organizations` | The filters each search accepts ([People search available fields](https://docs.reversecontact.com/endpoints/search-people-available-fields), [Company search available fields](https://docs.reversecontact.com/endpoints/search-companies-available-fields)) | Free | | `rc contact email` with `--url` or `--id`, or a name and `--company-domain` | A professional email, or a personal one with `--type personal` (`--type any` accepts either), as a job ([Find Email](https://docs.reversecontact.com/endpoints/contact-email)) | 3 for a professional email, 5 for a personal one, when found: `--type any` costs 3, 5 or 8 | | `rc contact phone` with `--url` or `--id`, or a name and `--company-domain` | A phone number, as a job ([Find Phone](https://docs.reversecontact.com/endpoints/contact-phone)) | 20 when a number is found | | `rc task ` | The status and result of a job ([Polling](https://docs.reversecontact.com/guides/polling)) | Free | | `rc bulk create --file people.csv` | Enrich up to 25,000 people from a CSV, NDJSON or JSON file in one job ([Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons)), see [Bulk enrichment](#bulk-enrichment) below | Each item what its unit call costs, only for what is found | | `rc bulk get ` | A Bulk enrichment job and its progress ([Get a job](https://docs.reversecontact.com/endpoints/job)); `--wait` follows it until it settles | Free | | `rc bulk list` | The jobs of your workspace, newest first, with `--status` ([Job history](https://docs.reversecontact.com/endpoints/jobs)) | Free | | `rc bulk rows ` | A page of results, even while the job runs ([Read job results by page](https://docs.reversecontact.com/endpoints/job-rows)); `--all` reads every result until nothing more will come | Free | | `rc bulk results ` | The results file of a settled job, CSV or NDJSON ([Download job results](https://docs.reversecontact.com/endpoints/job-results)) | Free | | `rc bulk cancel ` | Stop a job: items that had not started are never charged ([Cancel a job](https://docs.reversecontact.com/endpoints/job-cancel)) | Free | | `rc lists read ` | A page of the members of a List ([List members](https://docs.reversecontact.com/endpoints/list-members)), with `--kind`, `--page-size` and `--cursor` | Free | | `rc lists add ` | Add people or companies to a List, from flags or a `--signals-file` of up to 1,000 ([Add members](https://docs.reversecontact.com/endpoints/list-members-add)) | Per new member, as [Add members](https://docs.reversecontact.com/endpoints/list-members-add#credits): 1 or 2 credits. A member already on the List is free | | `rc lists export ` | The List as CSV, to stdout or `--out` ([Export a List](https://docs.reversecontact.com/endpoints/list-export)) | Free | | `rc usage` | The credit balance, the rate limits and the credit limits of the API key ([Usage](https://docs.reversecontact.com/endpoints/usage)) | Free | When you already have a profile URL or a Reverse Contact id, `rc fetch` (1 credit) is cheaper than `rc enrich` (2 credits), and a free `rc check` first tells you whether the cached copy is recent enough. `rc enrich person` does not take a profile URL. Without `--fields`, the JSON output is the full API response, and its `quotas.creditsConsumed` says what the call consumed. Both searches take `--area` with a zone id from the [area values](https://docs.reversecontact.com/reference/areas), such as `MR:EMEA` or `MC:DACH`, comma-separated for several zones: it adds up with `--country-code`. `rc search people` also takes `--connections-count` as a range, such as `501-` for more than 500 connections. `rc search companies` and `rc search fields companies` call `/v2/search/companies`, which goes into maintenance on November 9, 2026, for a few weeks: use `rc search organizations` and `rc search fields organizations` instead. See [Migrating company search to the new endpoint](https://docs.reversecontact.com/guides/migrating-search-companies-to-organizations). **Examples** ```bash [Enrich] rc enrich person --email jane.doe@acme.com --fields firstName,lastName,currentPositionTitle ``` ```bash [Check, then fetch] rc check person --url https://social.com/in/janedoe rc fetch person --url https://social.com/in/janedoe ``` ```bash [Search people] rc search people --title CTO --country France --limit 25 # The next page: pass back metadata.nextCursor of the previous one. rc search people --title CTO --country France --limit 25 --cursor "$NEXT_CURSOR" ``` ```bash [Search companies] rc search organizations --industry "Software Development" --size 51-200 --founded-year 2015- --country-code FR --limit 25 # The next page: pass back metadata.nextCursor of the previous one. rc search organizations --industry "Software Development" --size 51-200 --founded-year 2015- --country-code FR --limit 25 --cursor "$NEXT_CURSOR" ``` ```bash [Balance] rc usage --json | jq '.quotas.workspace.credits.left' # What the API key may still spend today, when it has a daily credit limit: rc usage --json | jq '.quotas.key.creditLimits.daily.left' ``` ## In scripts Output is JSON whenever stdout is not a terminal, so pipes and scripts get JSON without a flag; `--json` forces it everywhere. Without `--fields`, stdout gets the full API response; with it, only the fields you picked, flat, without the response envelope. An error goes to stderr as `{"error":{"code":"...","message":"...","hint":"..."}}`. | Exit code | Meaning | | --------- | ------- | | `0` | Success, including a not-found result (`success: false`, 0 credits) | | `1` | API error, including `402` (not enough credits), `403` (access denied: key permissions or a suspended workspace), a credit limit of the API key reached (`key_credit_limit_reached`) and a `429` still refused after the retries. With `--wait`, also a live job that ended `errored`, and a Bulk enrichment job that failed, is paused, or was still running when `--max-wait` ran out | | `2` | Usage error: a missing input, a bad value or two flags that cannot go together | | `3` | Authentication error: no API key, or a key the API refuses with `401` | | `4` | Network error, request timeout, or a `--wait` on a live job that ran out of time | A not-found result exits `0` on purpose, so a batch keeps going: add `--fail-on-not-found` to `rc enrich` to exit `1` instead. Other options common to the commands: `--fields` keeps only the listed fields (comma-separated, dotted paths such as `location.city`), `--quiet` hides spinners and credit hints, and `--timeout` sets the request timeout in seconds (30 by default). On a `429`, the CLI makes 3 attempts in all: it retries twice, after 1 then 2 seconds. A credit limit of the API key is never retried: the daily limit reopens at 00:00 UTC, the monthly one on the 1st, and the total one when someone raises or resets it in [API Keys](https://app.reversecontact.com/api-keys). ## Live jobs `rc fetch ... --live`, `rc contact email` and `rc contact phone` return a job id at once: read it later with `rc task `, which returns the job status and its `result` when it succeeded. Without `--wait`, `rc task` exits `0` whatever the status: read `data.status` (`succeeded` or `errored`). With `--wait`, a job that ended `errored` exits `1`. Results usually arrive within seconds but can take tens of minutes, so read the job later with `rc task` rather than waiting in a loop. To block until a live fetch finishes, add `--wait` with a `--timeout` in seconds that leaves the job enough time: `rc fetch person --url https://social.com/in/janedoe --live --wait --timeout 300`. When the time runs out, `rc` exits `4` and the job keeps running: read it with `rc task`. ## Bulk enrichment `rc bulk create --file people.csv` starts a [Bulk enrichment](https://docs.reversecontact.com/guides/bulk-enrichment) job from a CSV, NDJSON or JSON file of up to 25,000 people, or from stdin with `--file -`. In a CSV, the header names the fields of each item, `linkedinUrl`, `email`, `firstName`, `lastName`, `companyDomain`, `companyName` and `externalId`, whatever their case or separators (`first_name` works too); other columns are left out and listed on stderr. `--families` takes `profile` (the default), `contact_email` and `contact_phone`, comma-separated, `--type` the kind of email (`professional`, `personal` or `any`), and `--live` reads each profile URL live. `--name` names the job in the history and `--webhook-url` receives its `job.updated` event. The CLI sends an `Idempotency-Key` with each job. When the answer is lost, it prints that key: the same command with `--idempotency-key` and the same file starts and bills nothing twice. With `--wait`, the CLI follows the job until it settles, with a progress bar on stderr at a terminal, and `-o results.csv` then writes the results (`.ndjson` for NDJSON). `--max-wait` stops following after that many seconds, 21,600 (6 hours) by default: the job keeps running, and `rc bulk get --wait` follows it again. `rc bulk rows --all` writes every result as one JSON object per line as soon as it is ready, to stdout or to `-o`: it waits while the next item runs, and on a cancelled job it says on stderr that it waits for the items in flight, within the hour. `rc bulk results -o results.ndjson` downloads the file of a settled job, streamed to disk and written under its name only once whole; `--format csv` or `--format ndjson` picks the format when the file name does not. Reading the results is free. **Bulk enrichment** ```bash [Start, wait, download] rc bulk create --file people.csv --families profile,contact_email --wait -o results.csv ``` ```bash [Results as they come] JOB_ID=$(rc bulk create --file people.csv --json | jq -r '.data.job.id') rc bulk rows "$JOB_ID" --all -o results.ndjson ``` ## Lists `rc lists read ` reads a page of the members of a [List](https://docs.reversecontact.com/guides/lists): `--kind` picks `all` (the default), `members` or `pending`, `--page-size` the rows per page, and `--cursor` the next page. `rc lists add ` adds one person or company from `--url`, `--email`, `--first-name` and `--last-name` with `--company-domain` or `--company-name`, or `--domain` on a companies List, or up to 1,000 Signals from a JSON array with `--signals-file` (`-` reads stdin). A Signal that matches no one waits as Pending Intake and costs nothing. `rc lists export --out list.csv` writes the List as CSV, and `--fail-on-empty` exits `1` when it has no member. ## Environment variables | Variable | Effect | | -------- | ------ | | `RC_API_KEY` | The API key, ahead of the stored one | | `RC_BASE_URL` | The API address (`https://api.reversecontact.com` by default) | | `RC_OUTPUT` | `json` or `human`, whatever the terminal | | `RC_DEBUG=1` | Print the stack trace of an error | | `RC_NO_UPDATE_CHECK=1` | Turn off the update check of `rc version` | ## FAQ ### Does the Reverse Contact CLI cost more than the API? No. Each `rc` command calls the Reverse Contact API and is billed exactly like it, with the same credits and the same rate limits: `rc check` and `rc usage` are free, `rc fetch` costs 1 credit when the profile is found, `rc enrich` costs 2 credits when a match is found, and the count that `rc search people` and `rc search organizations` run before their search is free. ### How do I use the Reverse Contact CLI in CI without a browser? Set the `RC_API_KEY` environment variable, which writes nothing to disk, or pipe the key into `rc auth login --with-token`. `rc auth status` exits with code `3` when no valid key is found, so it works as a quick check before a job. ### How do I enrich a whole CSV file with the Reverse Contact CLI? Run `rc bulk create --file people.csv --wait -o results.csv`. It sends up to 25,000 people in one Bulk enrichment job, follows the job until it settles and writes one result row per person. Each person costs what its unit call costs, only for what is found, and `rc bulk rows --all` reads the results while the job still runs. ### How does a script tell a not-found result from an error with the Reverse Contact CLI? A not-found result exits with code `0` and costs 0 credits; without `--fields`, the JSON output also says `success: false`. Errors exit with code `1` (API), `2` (usage), `3` (authentication) or `4` (network), with the error on stderr. Add `--fail-on-not-found` to `rc enrich` to make a not-found result exit `1`. --- # API reference > Common response structure, shared types, and error codes for all endpoints of the Reverse Contact API: success, data, error, quotas and metadata. - Page: https://docs.reversecontact.com/api-reference - Last verified: 2026-11-04 Every endpoint returns the same JSON response envelope, and the shared types and error codes below apply across all endpoints. ## Response envelope The envelope has five top-level fields: **Response** | Field | Type | Description | | ---------- | --------------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `data` | object \| null | The endpoint-specific result payload (`null` on error or when no data is returned) | **error** | Field | Type | Description | | ---------- | ----------------- | -------------------------------------------- | | `code` | string | Machine-readable error code (e.g. `API_KEY_MISSING`, `API_KEY_MALFORMED`, `VALIDATION_ERROR`, `PAGINATION_LIMIT_EXCEEDED`) | | `message` | string | Human-readable error description | | `details` | object | Additional context (only present on some errors) | `null` when `success` is `true`. > [!TIP] > Include `metadata.requestId` from error responses when you contact support: it speeds up troubleshooting. **quotas** | Field | Type | Description | | ------------------ | ------ | -------------------------------------------- | | `creditsConsumed` | number | Credits used for this request after pricing resolution | **workspace** | Field | Type | Description | | --------------------- | -------------- | ------------------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap (Enterprise only) | **credits** | Field | Type | Description | | ------- | ------ | ------------------------ | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** | Field | Type | Description | | ----------- | -------------- | ---------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string \| null | When the daily window resets (ISO 8601) | `null` when no daily limit is configured. **minuteRateLimit** | Field | Type | Description | | ----------- | -------------- | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string \| null | When the minute window resets (ISO 8601) | **key** | Field | Type | Description | | ----------------- | -------------- | ------------------------------------------------------- | | `id` | string | API key identifier | **dailyLimit** | Field | Type | Description | | ----------- | -------------- | ---------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string \| null | When the daily window resets (ISO 8601) | `null` when no daily limit is configured. **minuteRateLimit** | Field | Type | Description | | ----------- | -------------- | ------------------------------------------ | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string \| null | When the minute window resets (ISO 8601) | **creditLimits** | Field | Type | Description | | --------- | -------------- | --------------------------------------------------------------------------- | | `daily` | object \| null | Credits the key may spend per day (UTC), resets at 00:00 UTC | | `monthly` | object \| null | Credits the key may spend per calendar month (UTC), resets on the 1st | | `total` | object \| null | Credits the key may spend in total, from when the limit was set or reset | Each limit has `limit`, `used` (this request included), `left` and `nextReset` (ISO 8601, `null` for `total`). A limit the key does not have is `null`. See [API key management](https://docs.reversecontact.com/guides/api-key-management#_4-set-credit-limits-optional). The `key` field is `null` when the request is made from the dashboard (no API key). When authentication or quota context is unavailable (e.g. invalid API key), `quotas` may be omitted. **metadata** | Field | Type | Description | | ----------------- | --------------- | --------------------------------------------------------------------------- | | `requestId` | string | Unique identifier for this request (useful for support inquiries) | | `executionTimeMs` | number | Total execution time in milliseconds | | `updatedAt` | string | Last upstream update timestamp (ISO 8601). Present only when available. | ## Shared types Some types are reused across endpoints. They are documented here once, and endpoint pages refer to them by name. **DateRange** | Field | Type | Description | | ------- | -------------- | ---------------------------- | | `start` | string \| null | Start date (ISO 8601) | | `end` | string \| null | End date (ISO 8601, null if current) | ## Error codes | Status | Description | | ------- | ---------------------------------------------------------------- | | `400` | Invalid JSON body | | `401` | Missing, malformed, invalid, or expired API key | | `402` | Not enough credits for the request (`INSUFFICIENT_CREDITS`): purchase more to continue. The free /check endpoints return `NO_CREDITS` below 2 credits, and the free count endpoints return it on a trial with no credit left | | `403` | Workspace disabled (`WORKSPACE_DISABLED`), API key lacks permission, or API key reached its total credit limit (`KEY_CREDIT_LIMIT_REACHED`) | | `404` | Data not found | | `409` | A request with the same `Idempotency-Key` is still being processed (`IDEMPOTENCY_KEY_IN_USE`), or a job is not in a state that allows the call (`JOB_RESULTS_NOT_READY`, `JOB_NOT_CANCELLABLE`, or `JOB_RESULTS_FORMAT_UNAVAILABLE` for a job stored before NDJSON) | | `413` | The JSON body is over 25 MB (`REQUEST_BODY_TOO_LARGE`): send a smaller body | | `422` | V2 schema/validation error, `INVALID_LINKEDIN_URL`, or disposable email domain | | `422` | `PAGINATION_LIMIT_EXCEEDED` when `perPage` exceeds `100` on people or company search. Details: `{ "field": "perPage", "max": 100 }` | | `429` | Rate limit exceeded, or the API key reached its daily or monthly credit limit (`KEY_CREDIT_LIMIT_REACHED`): wait `error.details.retryAfter` seconds and retry | | `451` | Data subject blocked: this person or company asked not to be processed under GDPR or CCPA. Not charged, do not retry | | `5xx` | Temporary upstream or platform issue: retry with backoff | > [!TIP] > For `429` errors, wait the number of seconds given in `error.details.retryAfter`, then retry. A `429` response carries no `quotas` object. > [!TIP] > For `5xx` errors, use exponential backoff starting at 1 second. `WORKSPACE_DISABLED` is `403` for both dashboard and API-key authentication. `INVALID_LINKEDIN_URL` and `VALIDATION_ERROR` return `422`. > [!NOTE] > For async endpoints, the initial response is a standard V2 acknowledgement containing a `webhookId`. Async endpoints support polling or a [webhook callback](https://docs.reversecontact.com/guides/webhooks); [Find Email](https://docs.reversecontact.com/endpoints/contact-email) delivers an email array or an error code in its result. Webhook callback payloads do not use the full V2 envelope. ## FAQ ### What fields does every Reverse Contact API response contain? Every Reverse Contact API response uses the same JSON envelope with five top-level fields: `success`, `data`, `error`, `quotas` and `metadata`. `data` is `null` on error or when no data is returned, and `error` is `null` when `success` is `true`. ### How do I check how many credits a request used? Read `quotas.creditsConsumed` in the response: it is the number of credits used for that request after pricing resolution. `quotas.workspace.credits` gives the `total`, `used` and `left` credits of your plan. When authentication or quota context is unavailable (for example, an invalid API key), `quotas` may be omitted. ### How should I retry `429` and `5xx` errors? For a `429` (rate limit exceeded), wait the number of seconds given in `error.details.retryAfter`, then retry: a `429` response carries no `quotas` object. For a `5xx` (temporary upstream or platform issue), retry with exponential backoff starting at 1 second. ### What is the maximum `perPage` for people and company search? `perPage` cannot exceed `100` on people or company search, and both paginate with a cursor rather than a page number. A request above that limit returns `422` with the code `PAGINATION_LIMIT_EXCEEDED` and details such as `{ "field": "perPage", "max": 100 }`. ### What should I include when I contact support about a failed request? Include the `metadata.requestId` value from the error response. `metadata.requestId` is the unique identifier of the request, and it speeds up troubleshooting. --- # Industry values > Complete list of the 490 accepted industry values for the industry filters of Company search and People search, and how each value is matched. - Page: https://docs.reversecontact.com/reference/industries - Last verified: 2026-10-15 The `industry` parameter in [Company search](https://docs.reversecontact.com/endpoints/search-companies) and the `currentCompanyIndustry`, `companyIndustry` and `excludeCompanyIndustry` filters in [People search](https://docs.reversecontact.com/endpoints/search-people) accept one or more values from the list below. Values must come from this list. You can pass a single string or an array of strings, and an array is OR-matched. Matching is tolerant of cosmetic differences: case, surrounding spaces, accents and `&` versus `and` are normalized, so `accounting` resolves to `Accounting`. A value that is not on the list is rejected with HTTP `422` and `error.code` `INVALID_FILTER_VALUE`, along with the closest valid values. It is never silently ignored, because the filter is matched exactly and an unlisted value can never return a result. ```json { "industry": ["Software Development", "Technology, Information and Internet"] } ``` ## All industries (490) - Abrasives and Nonmetallic Minerals Manufacturing - Accessible Architecture and Design - Accessible Hardware Manufacturing - Accommodation and Food Services - Accounting - Administration of Justice - Administrative and Support Services - Advertising Services - Agricultural Chemical Manufacturing - Agriculture, Construction, Mining Machinery Manufacturing - Air, Water, and Waste Program Management - Airlines and Aviation - Alternative Dispute Resolution - Alternative Fuel Vehicle Manufacturing - Alternative Medicine - Ambulance Services - Amusement Parks and Arcades - Animal Feed Manufacturing - Animation - Animation and Post-production - Apparel & Fashion - Apparel Manufacturing - Appliances, Electrical, and Electronics Manufacturing - Architectural and Structural Metal Manufacturing - Architecture and Planning - Armed Forces - Artificial Rubber and Synthetic Fiber Manufacturing - Artists and Writers - Arts & Crafts - Audio and Video Equipment Manufacturing - Automation Machinery Manufacturing - Automotive - Aviation & Aerospace - Aviation and Aerospace Component Manufacturing - Baked Goods Manufacturing - Banking - Bars, Taverns, and Nightclubs - Bed-and-Breakfasts, Hostels, Homestays - Beverage Manufacturing - Biomass Electric Power Generation - Biotechnology - Biotechnology Research - Blockchain Services - Blogs - Boilers, Tanks, and Shipping Container Manufacturing - Book and Periodical Publishing - Book Publishing - Breweries - Broadcast Media Production and Distribution - Building Construction - Building Equipment Contractors - Building Finishing Contractors - Building Materials - Building Structure and Exterior Contractors - Business Consulting and Services - Business Content - Business Intelligence Platforms - Business Supplies & Equipment - Cable and Satellite Programming - Capital Markets - Caterers - Chemical Manufacturing - Chemical Raw Materials Manufacturing - Child Day Care Services - Chiropractors - Circuses and Magic Shows - Civic and Social Organizations - Civil Engineering - Claims Adjusting, Actuarial Services - Clay and Refractory Products Manufacturing - Climate Data and Analytics - Climate Technology Product Manufacturing - Coal Mining - Collection Agencies - Commercial and Industrial Equipment Rental - Commercial and Industrial Machinery Maintenance - Commercial and Service Industry Machinery Manufacturing - Commercial Real Estate - Communications Equipment Manufacturing - Community Development and Urban Planning - Community Services - Computer and Network Security - Computer Games - Computer Hardware - Computer Hardware Manufacturing - Computer Networking - Computer Networking Products - Computers and Electronics Manufacturing - Conservation Programs - Construction - Construction Hardware Manufacturing - Consumer Electronics - Consumer Goods - Consumer Goods Rental - Consumer Services - Correctional Institutions - Cosmetics - Cosmetology and Barber Schools - Courts of Law - Credit Intermediation - Cutlery and Handtool Manufacturing - Dairy - Dairy Product Manufacturing - Dance Companies - Data Infrastructure and Analytics - Data Security Software Products - Death Care Services - Defense & Space - Defense and Space Manufacturing - Dentists - Design - Design Services - Desktop Computing Software Products - Digital Accessibility Services - Distilleries - E-Learning Providers - E-learning - Economic Programs - Education - Education Administration Programs - Education Management - Electric Lighting Equipment Manufacturing - Electric Power Generation - Electric Power Transmission, Control, and Distribution - Electrical Equipment Manufacturing - Electronic and Precision Equipment Maintenance - Embedded Software Products - Emergency and Relief Services - Energy Technology - Engineering Services - Engines and Power Transmission Equipment Manufacturing - Entertainment - Entertainment Providers - Environmental Quality Programs - Environmental Services - Equipment Rental Services - Events Services - Executive Offices - Executive Search Services - Fabricated Metal Products - Facilities Services - Family Planning Centers - Farming - Farming, Ranching, Forestry - Fashion Accessories Manufacturing - Financial Services - Fine Art - Fine Arts Schools - Fire Protection - Fisheries - Flight Training - Food & Beverages - Food and Beverage Manufacturing - Food and Beverage Retail - Food and Beverage Services - Food Production - Footwear and Leather Goods Repair - Footwear Manufacturing - Forestry and Logging - Fossil Fuel Electric Power Generation - Freight and Package Transportation - Fruit and Vegetable Preserves Manufacturing - Fuel Cell Manufacturing - Fundraising - Funds and Trusts - Funeral Services - Furniture - Furniture and Home Furnishings Manufacturing - Gambling Facilities and Casinos - Geothermal Electric Power Generation - Glass Product Manufacturing - Glass, Ceramics and Concrete Manufacturing - Golf Courses and Country Clubs - Government Administration - Government Relations - Government Relations Services - Graphic Design - Ground Passenger Transportation - Health and Human Services - Health Wellness & Fitness - Higher Education - Highway, Street, and Bridge Construction - Historical Sites - Holding Companies - Home Health Care Services - Horticulture - Hospitality - Hospitals - Hospitals and Health Care - Hotels and Motels - Household and Institutional Furniture Manufacturing - Household Appliance Manufacturing - Household Services - Housing and Community Development - Housing Programs - Human Resources - Human Resources Services - HVAC and Refrigeration Equipment Manufacturing - Hydroelectric Power Generation - Import & Export - Individual and Family Services - Industrial Automation - Industrial Machinery Manufacturing - Industry Associations - Information Services - Information Technology & Services - Insurance - Insurance Agencies and Brokerages - Insurance and Employee Benefit Funds - Insurance Carriers - Interior Design - International Affairs - International Trade and Development - Internet Marketplace Platforms - Internet News - Internet Publishing - Interurban and Rural Bus Services - Investment Advice - Investment Banking - Investment Management - IT Services and IT Consulting - IT System Custom Software Development - IT System Data Services - IT System Design Services - IT System Installation and Disposal - IT System Operations and Maintenance - IT System Testing and Evaluation - IT System Training and Support - Janitorial Services - Landscaping Services - Language Schools - Laundry and Drycleaning Services - Law Enforcement - Law Practice - Leasing Non-residential Real Estate - Leasing Residential Real Estate - Leather Product Manufacturing - Legal Services - Legislative Offices - Leisure, Travel & Tourism - Libraries - Lime and Gypsum Products Manufacturing - Loan Brokers - Luxury Goods & Jewelry - Machinery Manufacturing - Magnetic and Optical Media Manufacturing - Manufacturing - Maritime - Maritime Transportation - Market Research - Marketing Services - Mattress and Blinds Manufacturing - Measuring and Control Instrument Manufacturing - Meat Products Manufacturing - Mechanical Or Industrial Engineering - Media and Telecommunications - Media Production - Medical and Diagnostic Laboratories - Medical Device - Medical Equipment Manufacturing - Medical Practices - Mental Health Care - Metal Ore Mining - Metal Treatments - Metal Valve, Ball, and Roller Manufacturing - Metalworking Machinery Manufacturing - Military and International Affairs - Mining - Mobile Computing Software Products - Mobile Food Services - Mobile Gaming Apps - Motor Vehicle Manufacturing - Motor Vehicle Parts Manufacturing - Movies and Sound Recording - Movies, Videos and Sound - Museums - Museums, Historical Sites, and Zoos - Music - Musicians - Nanotechnology Research - Natural Gas Distribution - Natural Gas Extraction - Newspaper Publishing - Non-profit Organization Management - Non-profit Organizations - Nonmetallic Mineral Mining - Nonresidential Building Construction - Nuclear Electric Power Generation - Nursing Homes and Residential Care Facilities - Office Administration - Office Furniture and Fixtures Manufacturing - Oil and Coal Product Manufacturing - Oil and Gas - Oil Extraction - Oil, Gas, and Mining - Online and Mail Order Retail - Online Audio and Video Media - Online Media - Operations Consulting - Optometrists - Outpatient Care Centers - Outsourcing and Offshoring Consulting - Outsourcing/Offshoring - Packaging & Containers - Packaging and Containers Manufacturing - Paint, Coating, and Adhesive Manufacturing - Paper & Forest Products - Paper and Forest Product Manufacturing - Pension Funds - Performing Arts - Performing Arts and Spectator Sports - Periodical Publishing - Personal and Laundry Services - Personal Care Product Manufacturing - Personal Care Services - Pet Services - Pharmaceutical Manufacturing - Philanthropic Fundraising Services - Philanthropy - Photography - Physical, Occupational and Speech Therapists - Physicians - Pipeline Transportation - Plastics and Rubber Product Manufacturing - Plastics Manufacturing - Political Organizations - Postal Services - Primary and Secondary Education - Primary Metal Manufacturing - Printing Services - Professional Organizations - Professional Services - Professional Training and Coaching - Program Development - Public Assistance Programs - Public Health - Public Policy - Public Policy Offices - Public Relations and Communications Services - Public Safety - Racetracks - Radio and Television Broadcasting - Rail Transportation - Railroad Equipment Manufacturing - Ranching - Ranching and Fisheries - Real Estate - Real Estate Agents and Brokers - Real Estate and Equipment Rental Services - Recreational Facilities - Regenerative Design - Religious Institutions - Renewable Energy Equipment Manufacturing - Renewable Energy Power Generation - Renewable Energy Semiconductor Manufacturing - Renewables & Environment - Repair and Maintenance - Research - Research Services - Residential Building Construction - Restaurants - Retail - Retail Apparel and Fashion - Retail Appliances, Electrical, and Electronic Equipment - Retail Art Dealers - Retail Art Supplies - Retail Books and Printed News - Retail Building Materials and Garden Equipment - Retail Florists - Retail Furniture and Home Furnishings - Retail Gasoline - Retail Groceries - Retail Health and Personal Care Products - Retail Luxury Goods and Jewelry - Retail Motor Vehicles - Retail Musical Instruments - Retail Office Equipment - Retail Office Supplies and Gifts - Retail Pharmacies - Retail Recyclable Materials & Used Merchandise - Reupholstery and Furniture Repair - Robot Manufacturing - Robotics Engineering - Rubber Products Manufacturing - Satellite Telecommunications - Savings Institutions - School and Employee Bus Services - Seafood Product Manufacturing - Secretarial Schools - Securities and Commodity Exchanges - Security and Investigations - Security Guards and Patrol Services - Security Systems Services - Semiconductor Manufacturing - Semiconductors - Services for Renewable Energy - Services for the Elderly and Disabled - Sheet Music Publishing - Shipbuilding - Shuttles and Special Needs Transportation Services - Sightseeing Transportation - Skiing Facilities - Smart Meter Manufacturing - Soap and Cleaning Product Manufacturing - Social Networking Platforms - Software Development - Solar Electric Power Generation - Sound Recording - Space Research and Technology - Specialty Trade Contractors - Spectator Sports - Sporting Goods - Sporting Goods Manufacturing - Sports and Recreation Instruction - Sports Teams and Clubs - Spring and Wire Product Manufacturing - Staffing and Recruiting - Steam and Air-Conditioning Supply - Strategic Management Services - Subdivision of Land - Sugar and Confectionery Product Manufacturing - Surveying and Mapping Services - Taxi and Limousine Services - Technical and Vocational Training - Technology, Information and Internet - Technology, Information and Media - Telecommunications - Telecommunications Carriers - Telephone Call Centers - Temporary Help Services - Textile Manufacturing - Theater Companies - Think Tanks - Tobacco - Tobacco Manufacturing - Translation and Localization - Transportation Equipment Manufacturing - Transportation Programs - Transportation, Logistics, Supply Chain and Storage - Transportation/Trucking/Railroad - Travel Arrangements - Truck Transportation - Trusts and Estates - Turned Products and Fastener Manufacturing - Urban Transit Services - Utilities - Utilities Administration - Utility System Construction - Vehicle Repair and Maintenance - Venture Capital and Private Equity Principals - Veterinary - Veterinary Services - Vocational Rehabilitation Services - Warehousing - Warehousing and Storage - Waste Collection - Waste Treatment and Disposal - Water Supply and Irrigation Systems - Water, Waste, Steam, and Air Conditioning Services - Wellness and Fitness Services - Wholesale - Wholesale Alcoholic Beverages - Wholesale Apparel and Sewing Supplies - Wholesale Appliances, Electrical, and Electronics - Wholesale Building Materials - Wholesale Chemical and Allied Products - Wholesale Computer Equipment - Wholesale Drugs and Sundries - Wholesale Food and Beverage - Wholesale Footwear - Wholesale Furniture and Home Furnishings - Wholesale Hardware, Plumbing, Heating Equipment - Wholesale Import and Export - Wholesale Luxury Goods and Jewelry - Wholesale Machinery - Wholesale Metals and Minerals - Wholesale Motor Vehicles and Parts - Wholesale Paper Products - Wholesale Petroleum and Petroleum Products - Wholesale Photography Equipment and Supplies - Wholesale Raw Farm Products - Wholesale Recyclable Materials - Wind Electric Power Generation - Wine & Spirits - Wineries - Wireless Services - Women's Handbag Manufacturing - Wood Product Manufacturing - Writing and Editing - Zoos and Botanical Gardens ## FAQ ### Which industry values can I use in Reverse Contact search filters? The `industry` parameter of Company search and the `currentCompanyIndustry`, `companyIndustry` and `excludeCompanyIndustry` filters of People search accept only values from the Reverse Contact industry list, which contains 490 industries such as `Software Development` and `Technology, Information and Internet`. ### Is the industry filter case-sensitive? No. Industry matching in Reverse Contact search is tolerant of cosmetic differences: case, surrounding spaces, accents and `&` versus `and` are normalized, so `accounting` resolves to `Accounting`. The value itself must still come from the industry list. ### What happens if I send an industry value that is not on the list? A value passed to `industry`, `currentCompanyIndustry`, `companyIndustry` or `excludeCompanyIndustry` that is not on the Reverse Contact industry list is rejected with HTTP `422` and `error.code` `INVALID_FILTER_VALUE`, along with the closest valid values. An unlisted value is never silently ignored, because the industry filter is matched exactly and an unlisted value can never return a result. ### Can I filter on several industries at once? Yes. The `industry` parameter of Company search and the `currentCompanyIndustry`, `companyIndustry` and `excludeCompanyIndustry` filters of People search accept a single string or an array of strings, and an array is OR-matched, for example `["Software Development", "Technology, Information and Internet"]`. --- # Area values > Find the 62 location.area zone ids for People search and Company search, their member countries, and rules for combining location filters. - Page: https://docs.reversecontact.com/reference/areas - Last verified: 2026-10-30 The `location.area` filter of [People search](https://docs.reversecontact.com/endpoints/search-people) and [Company search](https://docs.reversecontact.com/endpoints/search-companies), and of their count endpoints, selects a whole zone in one value: a main region such as EMEA, a UN region or subregion, a market cluster such as DACH or the Nordics, or an institutional group such as the European Union. People search matches the person's country, Company search the country of the headquarters. Pass one zone id or an array of ids from the tables below. A zone matches every person or company whose country is one of its member countries. ```json { "currentPositionTitle": "Head of Sales", "location": { "area": ["MC:DACH", "MC:NORD"], "countryCode": "NL" } } ``` This request returns heads of sales in Germany, Austria, Switzerland, the five Nordic countries and the Netherlands. ## How zones combine - Zones add up. An array of zones is OR-matched, and the zones add up with `countryCode` and `country`: `{ "area": "MC:DACH", "countryCode": "FR" }` covers Germany, Austria, Switzerland and France. - `region` and `city` narrow the result: they are combined with the zones and countries with AND. - Overlapping zones count each country once: `["IG:EU", "IG:EEA"]` covers the 30 countries of the European Economic Area. - There is no exclusion. A zone only adds countries: to leave a country out, list the countries you want in `countryCode` instead. - `area` accepts at most 20 values, like every location sub-field. ## Accepted values - Only the full ids of the tables below are accepted, in any case: `mc:dach` is read as `MC:DACH`. - An acronym or a name, such as `"DACH"` or `"Europe"`, returns HTTP `422` with `error.code` `INVALID_FILTER_VALUE` and the closest ids in `details.suggestions` (`["MC:DACH"]` for `"DACH"`). The request costs 0 credits. - MENAT, MENAP and SAARC are not zones of their own: rebuild them from a zone and a few countries. MENAT is `MC:MENA` with `countryCode` `"TR"`, MENAP is `MC:MENA` with `["AF", "PK"]`, and SAARC is `MC:INSC` with `"AF"`. Sending one of these names returns HTTP `422` with the zone in `details.suggestions` and the countries to add in `details.countryCodes`. - Three UN ids cover exactly the same countries as a main region and return the same results: `GR:AMR` and `MR:AMER`, `GSR:NOAM` and `MR:NAMER`, `GSR:LAC` and `MR:LATAM`. - Country codes are ISO 3166-1 alpha-2, plus `XK` for Kosovo. - The available-fields endpoints of [People search](https://docs.reversecontact.com/endpoints/search-people-available-fields) and [Company search](https://docs.reversecontact.com/endpoints/search-companies-available-fields) return the same catalogue as data: the ids in `allowedValues` of `location.area`, and each zone with its member countries in `areaCatalog`. - The prompt endpoints ([People search by prompt](https://docs.reversecontact.com/endpoints/search-people-prompt), [Company search by prompt](https://docs.reversecontact.com/endpoints/search-companies-prompt)) turn a region named in the prompt, such as "in the Nordics" or "across EMEA", into its zone id. ## All zones (62) ### Main regions (5) | Id | Zone | Countries | | --- | --- | --- | | `MR:APAC` | Asia-Pacific (APAC) | 61: AF, AS, AU, BD, BN, BT, CC, CK, CN, CX, FJ, FM, GU, HK, HM, ID, IN, JP, KG, KH, KI, KP, KR, KZ, LA, LK, MH, MM, MN, MO, MP, MV, MY, NC, NF, NP, NR, NU, NZ, PF, PG, PH, PK, PN, PW, SB, SG, TH, TJ, TK, TL, TM, TO, TV, TW, UM, UZ, VN, VU, WF, WS | | `MR:EMEA` | Europe, Middle East & Africa (EMEA) | 131: AD, AE, AL, AM, AO, AT, AX, AZ, BA, BE, BF, BG, BH, BI, BJ, BW, BY, CD, CF, CG, CH, CI, CM, CV, CY, CZ, DE, DJ, DK, DZ, EE, EG, EH, ER, ES, ET, FI, FO, FR, GA, GB, GE, GG, GH, GI, GM, GN, GQ, GR, GW, HR, HU, IE, IL, IM, IO, IQ, IR, IS, IT, JE, JO, KE, KM, KW, LB, LI, LR, LS, LT, LU, LV, LY, MA, MC, MD, ME, MG, MK, ML, MR, MT, MU, MW, MZ, NA, NE, NG, NL, NO, OM, PL, PS, PT, QA, RE, RO, RS, RU, RW, SA, SC, SD, SE, SH, SI, SJ, SK, SL, SM, SN, SO, SS, ST, SY, SZ, TD, TF, TG, TN, TR, TZ, UA, UG, VA, XK, YE, YT, ZA, ZM, ZW | | `MR:AMER` | Americas (AMER) | 57: AG, AI, AR, AW, BB, BL, BM, BO, BQ, BR, BS, BV, BZ, CA, CL, CO, CR, CU, CW, DM, DO, EC, FK, GD, GF, GL, GP, GS, GT, GY, HN, HT, JM, KN, KY, LC, MF, MQ, MS, MX, NI, PA, PE, PM, PR, PY, SR, SV, SX, TC, TT, US, UY, VC, VE, VG, VI | | `MR:NAMER` | North America (NAMER) | 5: BM, CA, GL, PM, US | | `MR:LATAM` | Latin America & Caribbean (LATAM), also Latin America | 52: AG, AI, AR, AW, BB, BL, BO, BQ, BR, BS, BV, BZ, CL, CO, CR, CU, CW, DM, DO, EC, FK, GD, GF, GP, GS, GT, GY, HN, HT, JM, KN, KY, LC, MF, MQ, MS, MX, NI, PA, PE, PR, PY, SR, SV, SX, TC, TT, UY, VC, VE, VG, VI | ### Geographic regions (5) | Id | Zone | Countries | | --- | --- | --- | | `GR:AFR` | Africa (AFR), also African Union | 60: AO, BF, BI, BJ, BW, CD, CF, CG, CI, CM, CV, DJ, DZ, EG, EH, ER, ET, GA, GH, GM, GN, GQ, GW, IO, KE, KM, LR, LS, LY, MA, MG, ML, MR, MU, MW, MZ, NA, NE, NG, RE, RW, SC, SD, SH, SL, SN, SO, SS, ST, SZ, TD, TF, TG, TN, TZ, UG, YT, ZA, ZM, ZW | | `GR:ASI` | Asia (ASI) | 51: AE, AF, AM, AZ, BD, BH, BN, BT, CN, CY, GE, HK, ID, IL, IN, IQ, IR, JO, JP, KG, KH, KP, KR, KW, KZ, LA, LB, LK, MM, MN, MO, MV, MY, NP, OM, PH, PK, PS, QA, SA, SG, SY, TH, TJ, TL, TM, TR, TW, UZ, VN, YE | | `GR:EUR` | Europe (EUR) | 52: AD, AL, AT, AX, BA, BE, BG, BY, CH, CZ, DE, DK, EE, ES, FI, FO, FR, GB, GG, GI, GR, HR, HU, IE, IM, IS, IT, JE, LI, LT, LU, LV, MC, MD, ME, MK, MT, NL, NO, PL, PT, RO, RS, RU, SE, SI, SJ, SK, SM, UA, VA, XK | | `GR:OCE` | Oceania (OCE) | 29: AS, AU, CC, CK, CX, FJ, FM, GU, HM, KI, MH, MP, NC, NF, NR, NU, NZ, PF, PG, PN, PW, SB, TK, TO, TV, UM, VU, WF, WS | | `GR:AMR` | Americas (AMR), same countries as `MR:AMER` | 57: AG, AI, AR, AW, BB, BL, BM, BO, BQ, BR, BS, BV, BZ, CA, CL, CO, CR, CU, CW, DM, DO, EC, FK, GD, GF, GL, GP, GS, GT, GY, HN, HT, JM, KN, KY, LC, MF, MQ, MS, MX, NI, PA, PE, PM, PR, PY, SR, SV, SX, TC, TT, US, UY, VC, VE, VG, VI | ### Geographic subregions (24) | Id | Zone | Countries | | --- | --- | --- | | `GSR:ANZ` | Australia and New Zealand (ANZ) | 6: AU, CC, CX, HM, NF, NZ | | `GSR:CAM` | Central America (CAM) | 8: BZ, CR, GT, HN, MX, NI, PA, SV | | `GSR:CAR` | Caribbean (CAR) | 28: AG, AI, AW, BB, BL, BQ, BS, CU, CW, DM, DO, GD, GP, HT, JM, KN, KY, LC, MF, MQ, MS, PR, SX, TC, TT, VC, VG, VI | | `GSR:CAS` | Central Asia (CAS) | 5: KG, KZ, TJ, TM, UZ | | `GSR:EAF` | Eastern Africa (EAF) | 22: BI, DJ, ER, ET, IO, KE, KM, MG, MU, MW, MZ, RE, RW, SC, SO, SS, TF, TZ, UG, YT, ZM, ZW | | `GSR:EAS` | Eastern Asia (EAS), also North-east Asia, Northeast Asia | 8: CN, HK, JP, KP, KR, MN, MO, TW | | `GSR:EEU` | Eastern Europe (EEU) | 10: BG, BY, CZ, HU, MD, PL, RO, RU, SK, UA | | `GSR:MAF` | Middle Africa (MAF) | 9: AO, CD, CF, CG, CM, GA, GQ, ST, TD | | `GSR:MEL` | Melanesia (MEL) | 5: FJ, NC, PG, SB, VU | | `GSR:MIC` | Micronesia (MIC) | 8: FM, GU, KI, MH, MP, NR, PW, UM | | `GSR:NAF` | Northern Africa (NAF) | 7: DZ, EG, EH, LY, MA, SD, TN | | `GSR:NEU` | Northern Europe (NEU) | 16: AX, DK, EE, FI, FO, GB, GG, IE, IM, IS, JE, LT, LV, NO, SE, SJ | | `GSR:POLY` | Polynesia (POLY) | 10: AS, CK, NU, PF, PN, TK, TO, TV, WF, WS | | `GSR:SAF` | Southern Africa (SAF) | 5: BW, LS, NA, SZ, ZA | | `GSR:SAM` | South America (SAM) | 16: AR, BO, BR, BV, CL, CO, EC, FK, GF, GS, GY, PE, PY, SR, UY, VE | | `GSR:SEA` | South-eastern Asia (SEA), also Southeast Asia, ASEAN, Association of Southeast Asian Nations | 11: BN, ID, KH, LA, MM, MY, PH, SG, TH, TL, VN | | `GSR:SEU` | Southern Europe (SEU) | 17: AD, AL, BA, ES, GI, GR, HR, IT, ME, MK, MT, PT, RS, SI, SM, VA, XK | | `GSR:SSA` | Sub-Saharan Africa (SSA) | 53: AO, BF, BI, BJ, BW, CD, CF, CG, CI, CM, CV, DJ, ER, ET, GA, GH, GM, GN, GQ, GW, IO, KE, KM, LR, LS, MG, ML, MR, MU, MW, MZ, NA, NE, NG, RE, RW, SC, SH, SL, SN, SO, SS, ST, SZ, TD, TF, TG, TZ, UG, YT, ZA, ZM, ZW | | `GSR:WAF` | Western Africa (WAF) | 17: BF, BJ, CI, CV, GH, GM, GN, GW, LR, ML, MR, NE, NG, SH, SL, SN, TG | | `GSR:WAS` | Western Asia (WAS) | 18: AE, AM, AZ, BH, CY, GE, IL, IQ, JO, KW, LB, OM, PS, QA, SA, SY, TR, YE | | `GSR:WEU` | Western Europe (WEU) | 9: AT, BE, CH, DE, FR, LI, LU, MC, NL | | `GSR:NOAM` | Northern America (NOAM), same countries as `MR:NAMER` | 5: BM, CA, GL, PM, US | | `GSR:LAC` | Latin America and the Caribbean (LAC), same countries as `MR:LATAM` | 52: AG, AI, AR, AW, BB, BL, BO, BQ, BR, BS, BV, BZ, CL, CO, CR, CU, CW, DM, DO, EC, FK, GD, GF, GP, GS, GT, GY, HN, HT, JM, KN, KY, LC, MF, MQ, MS, MX, NI, PA, PE, PR, PY, SR, SV, SX, TC, TT, UY, VC, VE, VG, VI | | `GSR:SAS` | Southern Asia (SAS) | 9: AF, BD, BT, IN, IR, LK, MV, NP, PK | ### Market clusters (19) | Id | Zone | Countries | | --- | --- | --- | | `MC:BALT` | Baltics (BALT) | 3: EE, LT, LV | | `MC:BENELUX` | Belgium, Netherlands & Luxembourg (BENELUX) | 3: BE, LU, NL | | `MC:CEE` | Central & Eastern Europe (CEE) | 19: AL, BA, BG, CZ, EE, HR, HU, LT, LV, MD, ME, MK, PL, RO, RS, SI, SK, UA, XK | | `MC:DACH` | Germany, Austria & Switzerland (DACH) | 3: AT, CH, DE | | `MC:ENAFR` | Anglophone Africa (ENAFR) | 23: BI, BW, CM, GH, GM, KE, LR, LS, MU, MW, NA, NG, RW, SC, SD, SL, SS, SZ, TZ, UG, ZA, ZM, ZW | | `MC:FRAFR` | Francophone Africa (FRAFR) | 26: BF, BI, BJ, CD, CF, CG, CI, CM, DJ, DZ, GA, GN, GQ, KM, MA, MG, ML, MR, MU, NE, RW, SC, SN, TD, TG, TN | | `MC:GCR` | Greater China (GCR) | 4: CN, HK, MO, TW | | `MC:IBER` | Iberia (IBER) | 2: ES, PT | | `MC:INSC` | Indian Subcontinent (INSC), also South Asia | 7: BD, BT, IN, LK, MV, NP, PK | | `MC:JPKR` | Japan & South Korea (JPKR) | 2: JP, KR | | `MC:LUSAFR` | Lusophone Africa (LUSAFR) | 5: AO, CV, GW, MZ, ST | | `MC:MAGH` | Maghreb (MAGH) | 6: DZ, EH, LY, MA, MR, TN | | `MC:ME` | Middle East (ME) | 17: AE, BH, CY, EG, IL, IQ, IR, JO, KW, LB, OM, PS, QA, SA, SY, TR, YE | | `MC:MENA` | Middle East & North Africa (MENA) | 21: AE, BH, DZ, EG, EH, IL, IQ, IR, JO, KW, LB, LY, MA, OM, PS, QA, SA, SD, SY, TN, YE | | `MC:NORD` | Nordics (NORD) | 5: DK, FI, IS, NO, SE | | `MC:PACI` | Pacific Islands (PACI) | 23: AS, CK, FJ, FM, GU, KI, MH, MP, NC, NR, NU, PF, PG, PN, PW, SB, TK, TO, TV, UM, VU, WF, WS | | `MC:SCAN` | Scandinavia (SCAN) | 3: DK, NO, SE | | `MC:SEE` | South-eastern Europe (SEE) | 11: AL, BA, BG, GR, HR, ME, MK, RO, RS, SI, XK | | `MC:UKI` | United Kingdom & Ireland (UKI) | 2: GB, IE | ### Institutional and economic groups (9) | Id | Zone | Countries | | --- | --- | --- | | `IG:APEC` | Asia-Pacific Economic Cooperation (APEC) | 21: AU, BN, CA, CL, CN, HK, ID, JP, KR, MX, MY, NZ, PE, PG, PH, RU, SG, TH, TW, US, VN | | `IG:EEA` | European Economic Area (EEA), also GDPR | 30: AT, BE, BG, CY, CZ, DE, DK, EE, ES, FI, FR, GR, HR, HU, IE, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK | | `IG:EFTA` | European Free Trade Association (EFTA) | 4: CH, IS, LI, NO | | `IG:EU` | European Union (EU) | 27: AT, BE, BG, CY, CZ, DE, DK, EE, ES, FI, FR, GR, HR, HU, IE, IT, LT, LU, LV, MT, NL, PL, PT, RO, SE, SI, SK | | `IG:EZ` | Euro area (EZ) | 21: AT, BE, BG, CY, DE, EE, ES, FI, FR, GR, HR, IE, IT, LT, LU, LV, MT, NL, PT, SI, SK | | `IG:GCC` | Gulf Cooperation Council (GCC) | 6: AE, BH, KW, OM, QA, SA | | `IG:RCEP` | Regional Comprehensive Economic Partnership (RCEP) | 15: AU, BN, CN, ID, JP, KH, KR, LA, MM, MY, NZ, PH, SG, TH, VN | | `IG:SCH` | Schengen Area (SCH) | 29: AT, BE, BG, CH, CZ, DE, DK, EE, ES, FI, FR, GR, HR, HU, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK | | `IG:USMCA` | United States-Mexico-Canada Agreement (USMCA) | 3: CA, MX, US | ## FAQ ### How do I search people in EMEA or another region with the Reverse Contact API? Set `location.area` to the zone id in People search, for example `{ "location": { "area": "MR:EMEA" } }`. The filter matches people whose country is one of the zone's member countries. Company search takes the same `location.area` filter and matches the country of the headquarters. The 62 zone ids are listed on the Reverse Contact area values page. ### Can I combine a zone with specific countries? Yes. `location.area` and `location.countryCode` add up: `{ "area": "MC:DACH", "countryCode": "FR" }` covers Germany, Austria, Switzerland and France. `location.region` and `location.city` narrow the result further. ### Can I exclude a country from a zone? No. The `location.area` filter only adds countries, and there is no exclusion filter. To leave a country out, list the countries you want in `location.countryCode`, up to 20 values. ### Why is "DACH" rejected by the location.area filter? The `location.area` filter accepts full zone ids only, such as `MC:DACH`. A bare acronym or a region name returns HTTP 422 with `error.code` `INVALID_FILTER_VALUE` and the matching ids in `details.suggestions`, and the request costs 0 credits. --- # Enrich profile > Get a person's full professional profile from any details you have, such as an email, a name or a company. 2 credits, charged only when a person is found. - Page: https://docs.reversecontact.com/endpoints/enrich-person - Endpoint: `POST https://api.reversecontact.com/v2/enrich/persons` - Credits: 2 credits when a person is found, full profile included. A 404 is free. - Answer: synchronous. - Available in: REST API, MCP tool `enrich_person`, TypeScript and Python SDKs, CLI command `rc enrich person`. - Last verified: 2026-10-15 ## Introduction The Enrich Profile endpoint returns a person's full professional profile from any combination of identifying details. Every input is optional and independent: provide at least one. The more fields you include, the more accurate the match. This endpoint costs **2 credits**, charged only when a person is found. The full profile is included at no extra credit. A not-found response (`404`) is always free: no credits are consumed. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body All fields are optional and independent. Provide at least one of `email`, `firstName`, `lastName`, `companyDomain`, or `companyName`. Any combination works, and the more identifying fields you provide, the more accurate the match. | Name | Type | Required | Description | | --------------- | ------ | -------- | --------------------------------------------------------------------- | | `email` | string | No | Professional email address to look up (e.g. `jane.doe@acme.com`). | | `firstName` | string | No | First name of the person to look up. | | `lastName` | string | No | Last name of the person to look up. | | `companyDomain` | string | No | Company domain to help disambiguate (e.g. `acme.com` or `https://acme.com`). | | `companyName` | string | No | Company name to help disambiguate (e.g. `Acme Corp`). | | `fullProfile` | boolean | No | Deprecated and ignored. Enrich always returns the full profile, so this field no longer changes the response or the cost. It is still accepted so existing requests keep working, and you can remove it. | ## Best practices Every field you add narrows the search, so you trade match rate for confidence. Fewer fields mean a higher match rate but lower confidence. Sending only `firstName: "Pierre"` and `lastName: "Dupont"` will almost always return someone, because there are hundreds of Pierre Duponts, and nothing in that response tells you it is the one you meant. When more than one person matches your request, the other candidates are returned under `data.alternativePersons` (see Response structure below). More fields mean a lower match rate but higher confidence. Add `companyDomain: "acme.com"` and the request only matches if a Pierre Dupont is known at Acme. You get more `404`s, and the matches you do get are far more likely to be the right person. Which side to lean on depends on what a wrong match costs you. For outbound or CRM writes, prefer precision: a wrong profile is worse than no profile. For coverage-oriented work where a human reviews the output, a looser query is fine. ## Response structure The person is returned nested under `data.person`, other candidates under `data.alternativePersons`, and `data.isFullProfile` tells you which shape `data.person` has. ### Full profile `data.person` is the full profile: the same person shape as `/v2/fetch/persons`, with full-profile fields such as `summary`, `photoUrl`, `currentPosition`, `experience`, `education`, `skills`, `languages`, `certifications`, and more. It also keeps the enrich fields `currentPositionTitle`, `currentCompanyId`, `currentCompanyName`, `currentCompanyLinkedinId` and `updateDate`. Each item of `data.alternativePersons` is a full profile too, with the same enrich fields, at no extra credit. The full profile is a superset of the light enrich shape: every light field keeps its name, its place and its type, and the full-profile fields are added next to them. An integration built on the light shape keeps working unchanged. The full-profile field reference, including the items of `experience`, `education`, `languages`, `certifications`, `recommendations` and `testScores`, is documented on the [Person Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-profile). ### Light fallback In rare cases the full profile cannot be retrieved in time. The response then has `data.isFullProfile` set to `false`, and `data.person` and `data.alternativePersons` contain only the light shape: the fields listed under `alternativePersons` below. The request still costs 2 credits. An alternative candidate can also come back in the light shape on its own, while `data.person` is full, when its full profile could not be retrieved in time. A full item always carries `experience`: check for it on each alternative. To complete a light result, call the [Person Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-profile) (`/v2/fetch/persons`, 1 credit for a cached profile) with the `id` or the `linkedinUrl` returned in `data.person`: ```json { "id": "prs_9f8e7d6c5b4a" } ``` | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (EnrichPersonResult): The response payload (`null` on error responses) | Field | Type | Description | | --------------- | ------- | ---------------------------------------------------------------------------------------------------- | | `isFullProfile` | boolean | `true` when `person` is the full profile. `false` when only the light shape could be returned in time (see [Light fallback](#light-fallback)) | **person** (EnrichedPersonFull): The matched person: the full profile, or the light shape when `isFullProfile` is `false` | Field | Type | Description | | -------------------------- | ---------------------- | ---------------------------------------------------------------------------- | | `id` | string | Internal identifier for the person (`prs_...`). Pass it as `id` to the [Person Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-profile) to fetch this profile again | | `publicId` | string | Public profile slug (the `/in/xxx` part of the URL) | | `memberId` | string | Social internal member ID | | `linkedinUrl` | string | Full profile URL | | `firstName` | string \| null | First name | | `lastName` | string \| null | Last name | | `headline` | string \| null | Profile headline | | `summary` | string \| null | About / summary section | | `pronoun` | string \| null | Preferred pronoun (e.g. `"she/her"`, `"he/him"`) | | `isOpenToWork` | boolean | Whether the person is open to work | | `hasPremium` | boolean | Whether the person has Social Premium | | `hasVerificationBadge` | boolean | Whether the profile is verified | | `isInRemembrance` | boolean | Whether the profile is memorialized | | `photoUrl` | string \| null | Profile photo URL | | `backgroundUrl` | string \| null | Background / banner image URL | | `creationDate` | string \| null | Account creation date (ISO 8601) | | `followersCount` | number \| null | Number of followers | | `connectionsCount` | number \| null | Number of connections | | `experience` | PersonExperience[] | Professional experiences, same shape as `currentPosition` | | `education` | PersonEducation[] | Education history | | `skills` | string[] | List of skill names | | `languages` | PersonLanguage[] | Spoken languages | | `certifications` | PersonCertification[] | Professional certifications | | `recommendations` | PersonRecommendation[] | Received recommendations | | `testScores` | PersonTestScore[] | Standardized test scores | | `currentPositionTitle` | string \| null | Current job title ([how it is chosen](https://docs.reversecontact.com/guides/current-position-resolution)) | | `currentCompanyId` | string \| null | Internal identifier of the current company | | `currentCompanyName` | string \| null | Current company name | | `currentCompanyLinkedinId` | string \| null | Social identifier of the current company (useful to chain into a company fetch) | | `updateDate` | string \| null | When the record was last refreshed (ISO 8601) | **location** (EnrichedPersonLocation): Geographic location (`null` if unknown) | Field | Type | Description | | ------------- | -------------- | ------------------------------ | | `city` | string | City | | `state` | string | State or region | | `country` | string | Country name | | `countryCode` | string | ISO country code (e.g. `"US"`) | | `rawLocation` | string | Raw location string as displayed on the profile (`null` if unavailable) | **currentPosition** (CurrentPosition): Current job position (`null` if no current position) | Field | Type | Description | | ------------------- | ----------------- | ---------------------------------------------------- | | `title` | string \| null | Job title | | `description` | string \| null | Role description | | `contractType` | string \| null | Contract type (e.g. `"Permanent"`, `"Internship"`) | | `companyId` | string \| null | Reverse Contact company id (`com_...`). Pass it as `id` to the [Company Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-company) | | `companyName` | string \| null | Company name | | `companyLinkedinId` | string \| null | Company Social internal identifier | | `companyUrl` | string \| null | Company Social profile URL | | `companyLocation` | string \| null | Company location | | `companyLogoUrl` | string \| null | Company logo URL | | `startEndDate` | DateRange \| null | Position duration | **startEndDate** (DateRange): Period boundaries | Field | Type | Description | | ------- | -------------- | -------------------------------------- | | `start` | string \| null | Start date (ISO 8601) | | `end` | string \| null | End date (ISO 8601, `null` if ongoing) | **alternativePersons** (EnrichedPersonFull[]): Other candidates that matched your input but scored lower than `person`, ordered best first (up to 9). Empty when the match is unique. Alternatives mostly show up when resolving by name, where several people can match. Each one is returned with its full profile, the same shape as `person`; the fields below are always present, including on the rare item that falls back to the light shape. | Field | Type | Description | | --------------------------- | -------------- | ---------------------------------------------------------------------------- | | `id` | string | Internal identifier for the person | | `publicId` | string | Public profile slug (the `/in/xxx` part of the URL) | | `linkedinUrl` | string | Full profile URL | | `firstName` | string | First name | | `lastName` | string | Last name | | `headline` | string | Profile headline | | `currentPositionTitle` | string | Current job title ([how it is chosen](https://docs.reversecontact.com/guides/current-position-resolution)) | | `currentCompanyId` | null | Always `null` on alternative candidates | | `currentCompanyName` | string | Current company name | | `currentCompanyLinkedinId` | string | Social identifier of the current company (useful to chain into a company fetch) | | `updateDate` | string | When the record was last refreshed (ISO 8601) | **location** (EnrichedPersonLocation): Geographic location (`null` if unknown) | Field | Type | Description | | ------------- | -------------- | ------------------------------ | | `city` | string | City | | `state` | string | State or region | | `country` | string | Country name | | `countryCode` | string | ISO country code (e.g. `"US"`) | | `rawLocation` | string | Raw location string as displayed on the profile (`null` if unavailable) | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | -------------------------------- | | `creditsConsumed` | number | Credits consumed by this request | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Code examples **Code Examples** ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.enrich.person({ email: "jane.doe@acme.com" }); if (result === null) { console.log("No confident match: nothing was charged"); } else { const { person } = result.data; console.log(person?.firstName, person?.lastName, person?.currentCompanyName); } ``` ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/enrich/persons \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "jane.doe@acme.com"}' ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.enrich.person(email="jane.doe@acme.com") if result is None: print("No confident match: nothing was charged") elif result.data is not None and result.data.person is not None: person = result.data.person print(person.first_name, person.last_name, person.current_company_name) ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "person": { "id": "prs_9f8e7d6c5b4a", "firstName": "Jane", "lastName": "Doe", "publicId": "jane-doe", "memberId": "ACoAAB12345", "linkedinUrl": "https://www.social.com/in/jane-doe", "headline": "VP of Marketing @ Acme Corp | Demand Gen & Brand", "summary": "Experienced marketing leader with 15+ years driving growth for B2B SaaS companies. Passionate about data-driven strategies and building high-performing teams.", "pronoun": "she/her", "photoUrl": "https://media.licdn.com/dms/image/v2/D5603AQ.../profile-photo.jpg", "backgroundUrl": "https://media.licdn.com/dms/image/v2/D5616AQ.../background.jpg", "isOpenToWork": false, "hasPremium": true, "hasVerificationBadge": true, "isInRemembrance": false, "creationDate": "2009-03-15T00:00:00.000Z", "followersCount": 12480, "connectionsCount": 500, "location": { "city": "San Francisco", "state": "California", "country": "United States", "countryCode": "US", "rawLocation": "San Francisco Bay Area" }, "currentPosition": { "title": "VP of Marketing", "description": "Leading a team of 25 marketers across demand gen, product marketing, and brand.", "contractType": "Full-time", "companyId": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Acme Corp", "companyLinkedinId": "12345678", "companyUrl": "https://www.social.com/company/acme-corp", "companyLocation": "San Francisco, CA", "companyLogoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "startEndDate": { "start": "2021-06-01T00:00:00.000Z", "end": null } }, "experience": [ { "title": "VP of Marketing", "description": "Leading a team of 25 marketers across demand gen, product marketing, and brand.", "contractType": "Full-time", "companyId": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Acme Corp", "companyLinkedinId": "12345678", "companyUrl": "https://www.social.com/company/acme-corp", "companyLocation": "San Francisco, CA", "companyLogoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "startEndDate": { "start": "2021-06-01T00:00:00.000Z", "end": null } }, { "title": "Director of Marketing", "description": "Built the marketing function from scratch. Scaled from 0 to $5M ARR.", "contractType": "Full-time", "companyId": "com_01jbd7m3n8p2q5r9s4t6v1w0xy", "companyName": "Startup Inc", "companyLinkedinId": "98765432", "companyUrl": "https://www.social.com/company/startup-inc", "companyLocation": "Remote", "companyLogoUrl": null, "startEndDate": { "start": "2018-01-01T00:00:00.000Z", "end": "2021-05-31T00:00:00.000Z" } } ], "education": [ { "degreeName": "MBA", "fieldOfStudy": "Marketing", "description": null, "grade": null, "schoolName": "Stanford Graduate School of Business", "schoolUrl": "https://www.social.com/school/stanford-gsb", "schoolLogoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "startEndDate": { "start": "2012-09-01T00:00:00.000Z", "end": "2014-06-15T00:00:00.000Z" } } ], "skills": ["Marketing Strategy", "B2B SaaS", "Demand Generation", "Product Marketing"], "languages": [{ "language": "English", "proficiency": "Native or bilingual" }], "certifications": [], "recommendations": [], "testScores": [], "currentPositionTitle": "VP of Marketing", "currentCompanyId": "com_01jbd5vewyenebxgaz869ffe5t", "currentCompanyName": "Acme Corp", "currentCompanyLinkedinId": "12345678", "updateDate": "2026-04-07T14:22:17.271Z" }, "alternativePersons": [ { "id": "prs_1a2b3c4d5e6f", "publicId": "jane-doe-marketing", "linkedinUrl": "https://www.social.com/in/jane-doe-marketing", "firstName": "Jane", "lastName": "Doe", "headline": "Marketing Manager @ Acme Corp Europe", "summary": "Brand and demand generation for Acme Corp in Europe.", "location": { "city": "London", "state": null, "country": "United Kingdom", "countryCode": "GB", "rawLocation": "London, England, United Kingdom" }, "currentPosition": { "title": "Marketing Manager", "companyId": "com_01jbd9f2h4k6m8n0p3q5r7s9tv", "companyName": "Acme Corp Europe", "companyLinkedinId": "87654321" }, "experience": [ { "title": "Marketing Manager", "companyId": "com_01jbd9f2h4k6m8n0p3q5r7s9tv", "companyName": "Acme Corp Europe", "companyLinkedinId": "87654321" } ], "education": [], "skills": ["Demand Generation", "Brand Marketing"], "currentPositionTitle": "Marketing Manager", "currentCompanyId": null, "currentCompanyName": "Acme Corp Europe", "currentCompanyLinkedinId": "87654321", "updateDate": "2026-03-11T09:04:52.118Z" } ], "isFullProfile": true }, "error": null, "metadata": { "requestId": "fdedcabf-29d3-4aae-bede-89bd93cdc575", "executionTimeMs": 1240 }, "quotas": { "creditsConsumed": 2, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4535, "left": 5465 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 8, "left": 52, "nextReset": "2025-01-20T14:16:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 8, "left": 52, "nextReset": "2025-01-20T14:16:00.000Z" } } } } ``` --- # Enrich company > Get a company's full profile from its domain with POST /v2/enrich/companies. It costs 2 credits, charged only when a company is found. - Page: https://docs.reversecontact.com/endpoints/enrich-company - Endpoint: `POST https://api.reversecontact.com/v2/enrich/companies` - Credits: 2 credits when a company is found, full profile included. A 404 is free. - Answer: synchronous. - Available in: REST API, MCP tool `enrich_company`, TypeScript and Python SDKs, CLI command `rc enrich company`. - Last verified: 2026-10-15 ## Introduction The Enrich Company endpoint returns a company's full profile from a domain. This endpoint costs **2 credits**, charged only when a company is found. The full profile is included at no extra cost. A not-found response (`404`) is always free. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it in the `Authorization: Bearer YOUR_API_KEY` header. | ## Request body | Name | Type | Required | Description | | ------------- | ------- | -------- | -------------------------------------------------------- | | `domain` | string | Yes | Company domain to look up. A bare domain or a full URL works (e.g. `acme.com` or `https://acme.com`). | | `fullProfile` | boolean | No | Deprecated and ignored. Enrich always returns the full profile, so this field no longer changes the response or the cost. It is still accepted so existing requests keep working, and you can remove it. | ## Response structure The company is returned nested under `data.company`, and `data.isFullProfile` tells you which shape it has. ### Full profile `data.company` is the full company profile: the same company shape as `/v2/fetch/companies`, with full-profile fields such as `description`, `specialities`, `headquarter`, `locations`, `foundedOn`, `logoUrl` and `phone`. The full shape uses the fetch naming, so the company name is returned as `companyName`. It also keeps the enrich fields `name` (the same company name), `companyUpdateDate` and `location`. The full profile is a superset of the light enrich shape: every light field keeps its name, its place and its type, and the full-profile fields are added next to them. An integration built on the light shape keeps working unchanged. The [Company Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-company) documents every full-profile field. ### Light fallback In rare cases the full profile cannot be retrieved in time. The response then has `data.isFullProfile` set to `false`, and `data.company` contains only the light shape: `id`, `name`, `publicId`, `linkedinId`, `linkedinUrl`, `industry`, `tagline`, `websiteUrl`, `employeesCount`, `followersCount`, `employeeCountRange`, `companyUpdateDate` and `location`. The request still costs 2 credits. To complete a light result, call the [Company Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-company) (`/v2/fetch/companies`, 1 credit for a cached profile) with the `id` or the `linkedinUrl` returned in `data.company`: ```json { "id": "com_01jbd5vewyenebxgaz869ffe5t" } ``` | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (EnrichCompanyResult): The response payload (`null` on error responses) | Field | Type | Description | | --------------- | ------- | ---------------------------------------------------------------------------------------------------- | | `isFullProfile` | boolean | `true` when `company` is the full profile. `false` when only the light shape could be returned in time (see [Light fallback](#light-fallback)) | **company** (EnrichedCompanyFull): The matched company: the full profile, or the light shape when `isFullProfile` is `false` | Field | Type | Description | | -------------------- | -------------- | ---------------------------------------------------------------- | | `id` | string | Internal identifier for the company (`com_...`). Pass it as `id` to the [Company Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-company) to fetch this company again | | `companyName` | string | Company name | | `name` | string | Company name, same value as `companyName` | | `publicId` | string | Universal name / slug (the `/company/xxx` part of the URL) | | `linkedinId` | string \| null | Social identifier of the company | | `linkedinUrl` | string \| null | Full Social company URL | | `websiteUrl` | string \| null | Official company website URL | | `industry` | string \| null | Industry (e.g. `"Software Development"`) | | `description` | string \| null | Full company description | | `tagline` | string \| null | Short company tagline | | `logoUrl` | string \| null | Logo image URL | | `backgroundUrl` | string \| null | Background / banner image URL | | `employeesCount` | number \| null | Exact number of employees | | `followersCount` | number \| null | Number of followers on the company profile | | `specialities` | string[] | List of company specialties | | `phone` | string \| null | Phone number | | `companyUpdateDate` | string \| null | When the record was last refreshed (ISO 8601) | **headquarter** (CompanyHeadquarter): Headquarter location (`null` if unknown) | Field | Type | Description | | ---------------- | -------------- | ----------------------------- | | `street1` | string \| null | Primary street address line | | `street2` | string \| null | Secondary street address line | | `postalCode` | string \| null | Postal code | | `city` | string \| null | City | | `geographicArea` | string \| null | State or region | | `country` | string \| null | Country (ISO code or name) | **locations** (Location[]): All office locations registered on the profile (can be empty) | Field | Type | Description | | ------------ | -------------- | ------------------------------------------------------------------------------ | | `street1` | string \| null | Street address line 1 | | `street2` | string \| null | Street address line 2 | | `postalCode` | string \| null | Postal / ZIP code | | `city` | string \| null | City | | `state` | string \| null | State or region (named `state` here, not `geographicArea` as in `headquarter`) | | `country` | string \| null | Country ISO 3166 code (e.g. `"US"`, `"FR"`) | **foundedOn** (FoundedOn): Founding year (`null` if unknown) | Field | Type | Description | | ------ | ------ | --------------- | | `year` | number | Four-digit year | **employeeCountRange** (EnrichedCompanyEmployeeCountRange): Estimated employee count range (`null` if unknown) | Field | Type | Description | | ------- | ------ | --------------------------------- | | `start` | number | Lower bound of the employee range | | `end` | number | Upper bound of the employee range | **location** (EnrichedCompanyLocation): Geographic location (`null` if unknown) | Field | Type | Description | | --------- | -------------- | ------------------------------ | | `city` | string | City | | `country` | string | ISO country code (e.g. `"FR"`) | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | -------------------------------- | | `creditsConsumed` | number | Credits consumed by this request | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Code examples **Code Examples** ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.enrich.company({ domain: "reversecontact.com" }); if (result === null) { console.log("No company found: nothing was charged"); } else { console.log(result.data.company?.name, result.data.company?.industry); } ``` ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/enrich/companies \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "reversecontact.com"}' ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.enrich.company(domain="reversecontact.com") if result is None: print("No company found: nothing was charged") elif result.data is not None and result.data.company is not None: print(result.data.company.name, result.data.company.industry) ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "company": { "id": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Reverse Contact", "linkedinId": "89181966", "publicId": "reverse-contact", "linkedinUrl": "https://www.social.com/company/reverse-contact", "employeesCount": 19, "followersCount": 2075, "employeeCountRange": { "start": 11, "end": 50 }, "websiteUrl": "https://www.reversecontact.com", "tagline": "The first reverse email and phone lookup powered by LinkedIn data", "description": "We are pioneers in live web scraping and data solutions, empowering AI applications to harness the power of real-time information.", "industry": "Software Development", "phone": null, "specialities": [ "Lead Generation", "Lead Qualification", "Market Search", "Competitor analysis", "CRM enrichment" ], "headquarter": { "street1": "25, Rue de Tolbiac", "street2": null, "postalCode": "75013", "city": "Paris", "geographicArea": "Île-de-France", "country": "FR" }, "logoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "foundedOn": { "year": 2020 }, "locations": [ { "street1": "25, Rue de Tolbiac", "street2": null, "postalCode": "75013", "city": "Paris", "state": "Île-de-France", "country": "FR" } ], "backgroundUrl": "https://media.licdn.com/dms/image/v2/D5616AQ.../background.jpg", "name": "Reverse Contact", "companyUpdateDate": "2026-04-07T14:22:17.271Z", "location": { "city": "Paris", "country": "FR" } }, "isFullProfile": true }, "error": null, "metadata": { "requestId": "9742bdeb-cc91-4844-afb3-a63e3896341b", "executionTimeMs": 1120 }, "quotas": { "creditsConsumed": 2, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4537, "left": 5463 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 9, "left": 51, "nextReset": "2025-01-20T14:16:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 9, "left": 51, "nextReset": "2025-01-20T14:16:00.000Z" } } } } ``` --- # People search > Search people by job title, company, skills, seniority or location and retrieve full profiles with cursor pagination and per-profile pricing. - Page: https://docs.reversecontact.com/endpoints/search-people - Endpoint: `POST https://api.reversecontact.com/v2/search/people` - Credits: 1 credit per profile delivered. A page with no profile is free. - Answer: synchronous. - Available in: REST API, MCP tool `search_persons`, TypeScript and Python SDKs, CLI command `rc search people`. - Last verified: 2026-11-03 ## Introduction The People Search endpoint returns full person profiles matching a combination of filters. All filters are optional and combined with AND logic. Results come from our cache-based search index: this endpoint never triggers a live fetch. Every result is a complete profile, the same shape the [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) endpoint returns, so you do not need a second call to get the full data of a hit. Use the free [People search count](https://docs.reversecontact.com/endpoints/search-people-count) endpoint to check how many profiles match before you spend credits. > Coming from `POST /v2/search/persons`? See [Migrating people search from persons to people](https://docs.reversecontact.com/guides/migrating-search-persons-to-people). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body All filters are optional and combined with AND logic. The table lists the scalar filters and ends with the pagination parameters (`cursor`, `perPage`). The nested-object filters (`followersCount`, `connectionsCount`, `currentCompanyEmployeeCountRange`, `totalExperienceMonths`, `location`, `experience`) follow it as expandable objects. | Name | Type | Required | Description | | -------------------------- | ------------------ | -------- | -------------------------------------------------------------------- | | `firstName` | string | No | Filter by first name | | `lastName` | string | No | Filter by last name | | `headline` | string | No | Match keywords found in the profile headline | | `isOpenToWork` | boolean | No | Restrict to profiles flagged open-to-work | | `isCurrentlyEmployed` | boolean | No | Restrict to profiles that have a current employer | | `isPremium` | boolean | No | Restrict to Social Premium members | | `isVerified` | boolean | No | Restrict to profiles with a verification badge | | `currentPositionTitle` | string \| string[] | No | Current position title, or array of titles: a person matches if their current title matches any of them | | `excludeCurrentPositionTitle` | string \| string[] | No | Current position title, or array of titles, to exclude from results | | `positionTitle` | string \| string[] | No | Current or past position title, or array of titles (OR-matched) | | `excludePositionTitle` | string \| string[] | No | Current or past position title, or array of titles, to exclude | | `currentCompanyName` | string | No | Match the current employer name | | `excludeCurrentCompanyName` | string | No | Exclude the current employer name | | `companyName` | string \| string[] | No | Current or past company name, or array of names (OR-matched) | | `excludeCompanyName` | string \| string[] | No | Current or past company name, or array of names, to exclude | | `currentCompanyId` | string \| string[] | No | Reverse Contact internal id (`com_...`) of the current employer | | `excludeCurrentCompanyId` | string \| string[] | No | Current employer `com_...` id, or array of ids, to exclude | | `companyId` | string \| string[] | No | Current or past company `com_...` id, or array of ids | | `excludeCompanyId` | string \| string[] | No | Current or past company `com_...` id, or array of ids, to exclude | | `currentCompanyLinkedinId` | string \| string[] | No | Current employer Social identifier, or array of identifiers | | `excludeCurrentCompanyLinkedinId` | string \| string[] | No | Current employer Social identifier, or array, to exclude | | `companyLinkedinId` | string \| string[] | No | Current or past company Social identifier, or array of identifiers | | `excludeCompanyLinkedinId` | string \| string[] | No | Current or past company Social identifier, or array, to exclude | | `currentCompanyPublicId` | string \| string[] | No | Current employer public ID or slug, or array | | `excludeCurrentCompanyPublicId` | string \| string[] | No | Current employer public ID or slug, or array, to exclude | | `companyPublicId` | string \| string[] | No | Current or past company public ID or slug, or array | | `excludeCompanyPublicId` | string \| string[] | No | Current or past company public ID or slug, or array, to exclude | | `currentCompanyIndustry` | string \| string[] | No | Current employer industry. See [Industry values](https://docs.reversecontact.com/reference/industries) for the full list | | `companyIndustry` | string \| string[] | No | Current or past company industry, from the same industry list | | `excludeCompanyIndustry` | string \| string[] | No | Current or past company industry, or array, to exclude | | `currentFunction` | string \| string[] | No | Current function from the [position taxonomy](#position-taxonomy) | | `excludeCurrentFunction` | string \| string[] | No | Current function, or array of functions, to exclude | | `currentSubFunction` | string \| string[] | No | Current sub-function from the [position taxonomy](#position-taxonomy) | | `excludeCurrentSubFunction` | string \| string[] | No | Current sub-function, or array of sub-functions, to exclude | | `currentSeniority` | string \| string[] | No | Current seniority from the [position taxonomy](#position-taxonomy) | | `excludeCurrentSeniority` | string \| string[] | No | Current seniority, or array of seniorities, to exclude | | `currentContractType` | string \| string[] | No | Contract type of the current position, or array (OR-matched). See [Contract types](#contract-types) | | `excludeCurrentContractType` | string \| string[] | No | Contract type of the current position, or array, to exclude. Needs a positive filter next to it | | `contractType` | string \| string[] | No | Contract type of any position, current or past, or array (OR-matched) | | `excludeContractType` | string \| string[] | No | Contract type of any position, current or past, or array, to exclude. Needs a positive filter next to it | | `schoolName` | string \| string[] | No | School name, or array of names: a person matches if they attended any of them. See [Schools](#schools) | | `schoolId` | string \| string[] | No | Reverse Contact school id (`sch_...`), or array. See [Schools](#schools) | | `schoolLinkedinId` | string \| string[] | No | School Social identifier, or array. See [Schools](#schools) | | `skills` | string \| string[] | No | Skill name, or array of skill names: a person matches if they list any of them. See [Skills](#skills) | | `languages` | string \| string[] | No | Language name in English, or array: a person matches if they speak any of them. See [Languages](#languages) | | `excludeLanguages` | string \| string[] | No | Language name in English, or array, to exclude. Needs a positive filter next to it | | `keywords` | string | No | Words searched across the whole profile: every word must appear, in any field. See [Keywords](#keywords) | | `changedJobsWithinDays` | number | No | `30`, `60`, `90` or `120`: the current position started within that many days. See [Recent job change](#recent-job-change) | | `maxDataAgeDate` | string | No | ISO 8601 datetime. Only returns profiles refreshed after this date | | `cursor` | string | No | Opaque cursor returned as `metadata.nextCursor`; request the next page | | `perPage` | number | No | Profiles per page (1 to 100, default `25`) | > Filters are validated before the query runs. Unknown field names, blank values, values outside a closed list (`currentCompanyIndustry`, `currentFunction`, `currentSubFunction`, `currentSeniority`, the [contract types](#contract-types), `changedJobsWithinDays`, `location.countryCode`, `location.area`), `keywords` or `schoolName` without a single word and inconsistent ranges return HTTP `422` with the offending field, the reason and suggested values, and cost 0 credits. Unknown fields are rejected rather than ignored, because an ignored filter would return unfiltered results. Each list filter accepts at most 20 values. See [Search filter validation](https://docs.reversecontact.com/guides/errors-retries#search-filter-validation). > Title matching is token-based, not exact: `"CTO"` also matches `"Deputy CTO"`. Multiple titles are OR-matched, and exclusions always win over matches. The `positionTitle`, `companyName`, `companyId`, `companyLinkedinId`, `companyPublicId`, `companyIndustry` and `contractType` filters match current and past positions, while their `current...` counterparts match the current position only. **followersCount** (RangeFilter): Filter by follower count range. Both bounds are optional and inclusive. | Field | Type | Description | | ----- | ------ | ------------------------ | | `min` | number | Minimum value, inclusive | | `max` | number | Maximum value, inclusive | **connectionsCount** (RangeFilter): Filter by the number of connections of the profile. Both bounds are optional and inclusive, and one is enough: `{ "min": 501 }` keeps profiles with more than 500 connections. | Field | Type | Description | | ----- | ------ | ------------------------ | | `min` | number | Minimum value, inclusive | | `max` | number | Maximum value, inclusive | **currentCompanyEmployeeCountRange** (RangeFilter): Filter by the current company employee count range. Both bounds are optional and inclusive. Headcount is stored as fixed size brackets (`1`, `2-10`, `11-50`, `51-200`, `201-500`, `501-1000`, `1001-5000`, `5001-10000`, `10001+`), not as an exact number, and a range only matches a bracket it fully contains. Send bracket boundaries, otherwise the range contains no bracket, matches nothing and returns an empty `data.data` array. | Field | Type | Description | | ----- | ------ | ------------------------ | | `min` | number | Lower bound, inclusive. Must be a bracket start: `1`, `2`, `11`, `51`, `201`, `501`, `1001`, `5001`, `10001` | | `max` | number | Upper bound, inclusive. Must be a bracket end: `1`, `10`, `50`, `200`, `500`, `1000`, `5000`, `10000`. Omit it to include `10001+` | > For example, `{ "min": 11, "max": 50 }` matches the `11-50` bracket. `{ "min": 11, "max": 200 }` spans `11-50` and `51-200`. `{ "min": 10001 }` matches `10001+`. `{ "min": 11, "max": 20 }` returns an empty array, because no whole bracket fits between 11 and 20. **totalExperienceMonths** (RangeFilter): Filter by total career length in months. Both bounds are optional and inclusive, from 0 to 1200. | Field | Type | Description | | ----- | ------ | ------------------------ | | `min` | number | Minimum value, inclusive | | `max` | number | Maximum value, inclusive | **location** (LocationFilter): Multi-field location filter. Each sub-field is optional and accepts a single string or an array of strings (OR matching), with a maximum of 20 values per sub-field (for example 20 `region` values). Beyond that, the request returns HTTP `422` with `FILTER_VALUES_LIMIT_EXCEEDED` and costs 0 credits: split it into several searches. | Field | Type | Description | | ------------- | ------------------ | ----------------------------------- | | `country` | string \| string[] | Country name (e.g. `"France"`) | | `countryCode` | string \| string[] | ISO 3166 country code (e.g. `"FR"`) | | `area` | string \| string[] | Zone id from the [area values](https://docs.reversecontact.com/reference/areas) (e.g. `"MR:EMEA"`, `"MC:DACH"`, `"IG:EU"`): matches people whose country is a member of the zone | | `region` | string \| string[] | State or region | | `city` | string \| string[] | City | > Zones in `area` add up with each other and with `country` and `countryCode`, while `region` and `city` narrow the result. `{ "area": "MC:DACH", "countryCode": "FR" }` covers Germany, Austria, Switzerland and France. A zone only adds countries: there is no exclusion. Only full zone ids are accepted, so `"DACH"` returns HTTP `422` with `"MC:DACH"` in `details.suggestions`. See [Area values](https://docs.reversecontact.com/reference/areas). **experience** (ExperienceFilter[]): Required past experience. Each entry demands a position taxonomy value, with an optional minimum tenure. Entries are AND-combined, up to 10 per request. | Field | Type | Description | | ----------- | ------ | ---------------------------------------------------------------------- | | `dimension` | string | Taxonomy dimension: `function`, `subFunction` or `seniority` | | `value` | string | Value from the selected dimension of the [position taxonomy](#position-taxonomy) | | `minMonths` | number | Optional minimum tenure in that dimension, in months (0 to 720) | ## Position taxonomy `currentFunction`, `currentSubFunction`, `currentSeniority` and the `experience` filter match a closed taxonomy of professional functions. A value must come from the taxonomy: case and surrounding spaces are ignored, and any other value is rejected with HTTP `422` and the closest valid values. There are 32 functions, 386 sub-functions and 10 seniorities. The functions and seniorities are listed below. The sub-function list is long: read it from the [available fields](https://docs.reversecontact.com/endpoints/search-people-available-fields) endpoint, which returns every accepted value for each filter and the function each sub-function belongs to. ### Functions (32) - Administrative - Agriculture and Environment - Construction and Trades - Consulting and Advisory - Customer Service - Design - Education - Energy and Utilities - Entertainment and Gaming - Executive and Leadership - Finance - Hospitality and Tourism - Human Resources - Legal - Marketing - Media and Communications - Medical and Health - Nonprofit and Government - Not Employed - Operations - Other - Personal and Home Services - Product - Project and Program Management - Public Safety and Security - Real Estate - Research and Science - Retail and Consumer - Sales - Software - Traditional Engineering - Transportation and Logistics ### Seniorities (10) - CXO - VP - Director - Head - Manager - Partner - Owner / Founder - Individual Contributor - Intern - Other ## Contract types and schools ### Contract types `currentContractType` matches the contract type of the current position, and `contractType` the contract type of any position, current or past. Both take one value or a list, OR-matched, from these eight: - `full-time` - `part-time` - `permanent` - `contract` - `internship` - `self-employed` - `freelance` - `apprenticeship` Case, spaces and underscores are ignored: `"Self employed"` is read as `self-employed`. Any other value returns HTTP `422` with the closest valid values. A position with no contract type on record never matches. `excludeContractType` removes people who held one of the listed contract types in any position, current or past, and keeps people with no contract type on record. `excludeCurrentContractType` removes people whose current position has one of the listed types, and also leaves out people whose current position has no contract type on record. Both only refine a search: sent alone, they are rejected. The same value in an include and its exclude returns HTTP `422` with `CONTRADICTORY_FILTERS`. ### Schools `schoolName` matches the schools of the education history. The value is split into words, case and accents are ignored, and every word must appear in the name of a single school, in any order: `"HEC Paris"` matches a person who studied at HEC Paris, not a person whose words are spread across two schools. Words are matched whole. A list is OR-matched: a person matches when they attended at least one of the schools. There is no exclude variant, and a value with no word at all returns HTTP `422`. `schoolLinkedinId` and `schoolId` target one school exactly, across the whole education history, and a list is OR-matched. `schoolLinkedinId` is the number at the end of the `schoolUrl` of an education entry: `235785` for `https://www.social.com/school/235785`. Some education entries do not carry one. `schoolId` takes a Reverse Contact school id, `sch_` followed by 26 characters, and any other shape returns HTTP `422`. Education entries do not return this id yet, so `schoolLinkedinId` is the one to use today. **Example** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/search/people \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "currentFunction": "Design", "currentContractType": ["freelance", "self-employed"], "schoolName": ["HEC Paris", "ESSEC"], "location": { "countryCode": "FR" }, "perPage": 25 }' ``` ## Skills, languages, keywords and job changes These four filters read the whole profile rather than one position. They combine with every other filter, and each of them except `excludeLanguages` is a valid search on its own. ### Skills `skills` matches the skills listed on the profile, whatever position or school they come from. Matching ignores case and accents and compares whole skill names, never part of one: `"Java"` does not match a profile that only lists `"JavaScript"`, and `"Machine Learning"` is matched as one value. A list is OR-matched: a person matches when they list at least one of the requested skills. ### Languages `languages` keeps people who speak at least one of the requested languages, and `excludeLanguages` removes people who speak any of the listed ones. Profiles that list no language are kept by `excludeLanguages`, which only refines a search: sent alone, it is rejected. Send language names in English. Profiles store their languages in their own spelling (`Français`, `Inglese`, `Deutsch`), and the following names also match those spellings: Arabic, Bengali, Catalan, Chinese, Czech, Danish, Dutch, English, Finnish, French, German, Greek, Hebrew, Hindi, Hungarian, Indonesian, Italian, Japanese, Korean, Mandarin, Norwegian, Polish, Portuguese, Punjabi, Romanian, Russian, Spanish, Swedish, Tagalog, Thai, Turkish, Ukrainian, Urdu, Vietnamese. Any other value is accepted and matches its exact spelling only. Matching compares whole names: `"French"` does not match `"French Creole"`. There is no proficiency level: any level matches. > About one profile in six lists its languages, so a `languages` filter leaves out every profile that lists none. ### Keywords `keywords` is one string searched across the whole profile: names, headline, summary, current and past job titles, companies, industries, skills, schools, degrees, certifications, languages and location. The value is split into words, case and accents are ignored, and every word must appear somewhere in the profile, in any field and in any order: `"react paris"` requires both react and paris. Words are matched whole: `"react"` does not match `"reactive"`. Because the words may come from different fields, there is no exact-phrase match. A value with no word at all, such as `"!!!"`, returns HTTP `422`. ### Recent job change `changedJobsWithinDays` keeps people whose current position started within the last `30`, `60`, `90` or `120` days, boundary included, counted in UTC days. A profile with no current position, or whose current position has no start date, does not match, and a start date in the future never matches. Any other value returns HTTP `422` with the accepted ones. **Example** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/search/people \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "currentFunction": "Software", "skills": ["Python", "Machine Learning"], "languages": "German", "keywords": "fintech", "changedJobsWithinDays": 90, "location": { "countryCode": "DE" }, "perPage": 25 }' ``` > On the largest countries, a `skills` or `languages` filter used alone scans a very large set of profiles and can be slow. Pair it with a title, a function or a company to keep the search fast. ## Pagination and credits People search uses cursor pagination. Each response returns up to `perPage` profiles (1 to 100, default `25`) and a `metadata.nextCursor` value. Pass that value back as `cursor` in the next request to get the following page; `nextCursor` is `null` on the last page. The cursor is opaque: send it back unchanged, without parsing or altering it. There is no `page` parameter. A page can hold fewer profiles than `perPage`, or none at all, while `nextCursor` is still set, because some profiles are removed after the page is built. Keep paging until `nextCursor` is `null`: that is the only end-of-results signal. A `nextCursor` has no hard expiry. The result list behind it is cached for 10 minutes. If you use a cursor after that, the search is re-run with the same filters and you get the next page from the refreshed list. If the data changed in the meantime, that page can contain a profile you already received or skip one. For a consistent result set, fetch each page within 10 minutes of the previous one. An invalid or expired cursor returns HTTP `400` with the message `Invalid or expired cursor`. This happens when a filter is changed between two pages (keep the request body identical and only add the `cursor`), or when the cursor is malformed (truncated or altered). `perPage` above `100` returns HTTP `422` with `error.code` `PAGINATION_LIMIT_EXCEEDED`, and the `error.details` object identifies the field and maximum, for example `{ "field": "perPage", "max": 100 }`. People search costs **1 credit per profile delivered**. Compliance-blocked profiles are removed before the response and are not billed. A page that returns 25 profiles costs 25 credits, and a page that returns no profile costs 0 credits. Check the size of a query first with the free [People search count](https://docs.reversecontact.com/endpoints/search-people-count) endpoint, and lower `perPage` to control how many credits a single request can spend. ## Response structure > This endpoint uses a nested envelope: the top-level V2 `data` field wraps a search payload that holds the results array (`data.data`) and the pagination info (`data.metadata`). See the example response for the exact shape. | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data.data** (PersonFull[]): Full profiles returned for this page, wrapped under `data.data` in the envelope. Empty array if no match. Each item is the same full profile shape as the [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) endpoint; the table below lists its main fields. | Field | Type | Description | | -------------------- | -------------- | -------------------------------------------------------------------- | | `id` | string | Reverse Contact internal person identifier (`prs_...`) | | `publicId` | string | Social public identifier (the `/in/xxx` slug) | | `memberId` | string \| null | Upstream member identifier when available | | `linkedinUrl` | string | Full Social profile URL | | `firstName` | string \| null | First name | | `lastName` | string \| null | Last name | | `headline` | string \| null | Profile headline | | `summary` | string \| null | Profile summary | | `isOpenToWork` | boolean | Whether the profile is flagged open to work | | `hasPremium` | boolean | Whether the profile has Social Premium | | `hasVerificationBadge` | boolean | Whether the profile has a verification badge | | `photoUrl` | string \| null | Profile photo URL | | `creationDate` | string \| null | When the profile was created (ISO 8601) | | `followersCount` | number \| null | Follower count | | `connectionsCount` | number \| null | Connection count | | `currentPosition` | object \| null | Current job position ([how it is chosen](https://docs.reversecontact.com/guides/current-position-resolution)) | | `experience` | array | Professional experiences, same shape as `currentPosition` | | `education` | array | Education history | | `skills` | string[] | Skills listed on the profile | | `languages` | array | Spoken languages | | `certifications` | array | Professional certifications | | `recommendations` | array | Received recommendations | **location** (PersonLocation): Geographic location (`null` if unknown) | Field | Type | Description | | ------------- | -------------- | ------------------------------ | | `city` | string \| null | City | | `state` | string \| null | State or region | | `country` | string \| null | Country name | | `countryCode` | string \| null | ISO country code (e.g. `"US"`) | | `rawLocation` | string \| null | Raw location string as displayed on the profile | **currentPosition** (CurrentPosition): Current job position (`null` if no current position) | Field | Type | Description | | ------------------ | -------------- | ------------------------------------------ | | `title` | string \| null | Job title | | `description` | string \| null | Position description | | `contractType` | string \| null | Contract type | | `companyId` | string \| null | Reverse Contact company id (`com_...`). Pass it as `id` to the [Company Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-company) | | `companyName` | string \| null | Company name | | `companyLinkedinId` | string \| null | Company Social identifier | | `companyUrl` | string \| null | Company Social URL | | `companyLocation` | string \| null | Company location | | `companyLogoUrl` | string \| null | Company logo URL | | `startEndDate` | object \| null | Period boundaries ([DateRange](https://docs.reversecontact.com/api-reference)) | **data.metadata** (SearchPagination): Pagination metadata wrapped under `data.metadata` in the envelope. | Field | Type | Description | | ------------ | -------------- | ---------------------------------------------------------------------- | | `perPage` | number | Profiles requested per page | | `count` | number | Profiles returned in this page. Use [People search count](https://docs.reversecontact.com/endpoints/search-people-count) for the total matching set | | `nextCursor` | string \| null | Opaque cursor for the next page, `null` on the last page | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | -------------------------------- | | `creditsConsumed` | number | Credits consumed by this request | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information (not to be confused with `data.metadata` which is pagination) | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/search/people \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "currentFunction": "Marketing", "currentSeniority": ["Director", "VP"], "excludeCurrentSubFunction": "Marketing Operations", "currentCompanyIndustry": ["Software Development", "Technology, Information and Internet"], "location": { "country": "United States", "city": ["San Francisco", "New York"] }, "followersCount": { "min": 1000 }, "experience": [ { "dimension": "function", "value": "Sales", "minMonths": 24 } ], "perPage": 25 }' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); // 1 credit per profile delivered: maxResults caps how many profiles the iterator requests. const people = client.search.iterPeople( { currentFunction: "Marketing", currentSeniority: ["Director", "VP"], excludeCurrentSubFunction: "Marketing Operations", currentCompanyIndustry: ["Software Development", "Technology, Information and Internet"], location: { country: "United States", city: ["San Francisco", "New York"] }, followersCount: { min: 1000 }, experience: [{ dimension: "function", value: "Sales", minMonths: 24 }], perPage: 25 }, { maxResults: 100 } ); for await (const person of people) { console.log(person.firstName, person.lastName); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: # 1 credit per profile delivered: max_results caps how many profiles the iterator requests. people = client.search.iter_people( current_function="Marketing", current_seniority=["Director", "VP"], exclude_current_sub_function="Marketing Operations", current_company_industry=["Software Development", "Technology, Information and Internet"], location={"country": "United States", "city": ["San Francisco", "New York"]}, followers_count={"min": 1000}, experience=[{"dimension": "function", "value": "Sales", "minMonths": 24}], per_page=25, max_results=100, ) for person in people: if person is not None: print(person.first_name, person.last_name) ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "data": [ { "id": "prs_01h2x3y4z5a6b7c8d9e0f1g2h3", "publicId": "janedoe", "memberId": "ACoAAB12345", "linkedinUrl": "https://www.social.com/in/janedoe", "firstName": "Jane", "lastName": "Doe", "headline": "VP of Marketing at Acme Corp | Demand Gen & Brand", "summary": "Experienced marketing leader with 15+ years driving growth for B2B SaaS companies.", "pronoun": "she/her", "isOpenToWork": false, "hasPremium": true, "hasVerificationBadge": true, "isInRemembrance": false, "photoUrl": "https://media.licdn.com/dms/image/v2/D5603AQ.../profile-photo.jpg", "backgroundUrl": null, "creationDate": "2009-03-15T00:00:00.000Z", "followersCount": 12480, "connectionsCount": 500, "location": { "city": "San Francisco", "state": "California", "country": "United States of America", "countryCode": "US", "rawLocation": "San Francisco Bay Area" }, "currentPosition": { "title": "VP of Marketing", "description": "Leading a team of 25 marketers across demand gen, product marketing, and brand.", "contractType": "Full-time", "companyId": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Acme Corp", "companyLinkedinId": "12345678", "companyUrl": "https://www.social.com/company/acme-corp", "companyLocation": "San Francisco, CA", "companyLogoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "startEndDate": { "start": "2021-06-01T00:00:00.000Z", "end": null } }, "experience": [ { "title": "VP of Marketing", "description": "Leading a team of 25 marketers across demand gen, product marketing, and brand.", "contractType": "Full-time", "companyId": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Acme Corp", "companyLinkedinId": "12345678", "companyUrl": "https://www.social.com/company/acme-corp", "companyLocation": "San Francisco, CA", "companyLogoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "startEndDate": { "start": "2021-06-01T00:00:00.000Z", "end": null } }, { "title": "Director of Marketing", "description": "Built the marketing function from scratch. Scaled from 0 to $5M ARR.", "contractType": "Full-time", "companyId": "com_01jbd7m3n8p2q5r9s4t6v1w0xy", "companyName": "Startup Inc", "companyLinkedinId": "98765432", "companyUrl": "https://www.social.com/company/startup-inc", "companyLocation": "Remote", "companyLogoUrl": null, "startEndDate": { "start": "2018-01-01T00:00:00.000Z", "end": "2021-05-31T00:00:00.000Z" } } ], "education": [ { "degreeName": "MBA", "fieldOfStudy": "Marketing", "description": null, "grade": null, "schoolName": "Stanford Graduate School of Business", "schoolUrl": "https://www.social.com/school/stanford-gsb", "schoolLogoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "startEndDate": { "start": "2012-09-01T00:00:00.000Z", "end": "2014-06-15T00:00:00.000Z" } } ], "skills": ["Marketing Strategy", "B2B SaaS", "Demand Generation", "Product Marketing"], "languages": [{ "language": "English", "proficiency": "Native or bilingual" }], "certifications": [], "recommendations": [], "testScores": [] } ], "metadata": { "perPage": 25, "count": 1, "nextCursor": "cur_01j9x7q4m2z8b6d4f0a2c8e6" } }, "error": null, "metadata": { "requestId": "d87f3d57-c761-41cd-a11a-c70c2dda1870", "executionTimeMs": 410 }, "quotas": { "creditsConsumed": 1, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4522, "left": 5478 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2026-09-14T14:16:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2026-09-14T14:16:00.000Z" } } } } ``` --- # People search count > Count matching people with the same filters as People search, for free, to size a query before paying 1 credit per profile delivered. - Page: https://docs.reversecontact.com/endpoints/search-people-count - Endpoint: `POST https://api.reversecontact.com/v2/search/people/count` - Credits: Free. - Answer: synchronous. - Available in: REST API, MCP tool `count_persons`, TypeScript and Python SDKs. - Last verified: 2026-10-12 ## Introduction The People Search Count endpoint returns the total number of profiles matching the same filters as [People search](https://docs.reversecontact.com/endpoints/search-people), without returning any profile. Use it to size a query before spending credits: [People search](https://docs.reversecontact.com/endpoints/search-people) costs 1 credit per profile delivered, while this endpoint is free. The endpoint accepts exactly the filters of People search, without the pagination parameters: a count is a single number, so there is no page to request. Like [Usage](https://docs.reversecontact.com/endpoints/usage), this endpoint **costs 0 credits**, but each call still counts toward your rate limits. A free trial with no credit left gets `402 NO_CREDITS`. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body All filters are optional and combined with AND logic, exactly as on [People search](https://docs.reversecontact.com/endpoints/search-people#request-body): scalar filters, the nested-object filters (`followersCount`, `connectionsCount`, `currentCompanyEmployeeCountRange`, `totalExperienceMonths`, `location`, `experience`), the [position taxonomy](https://docs.reversecontact.com/endpoints/search-people#position-taxonomy) filters, the [contract type and school](https://docs.reversecontact.com/endpoints/search-people#contract-types-and-schools) filters, and the [skills, languages, keywords and job change](https://docs.reversecontact.com/endpoints/search-people#skills-languages-keywords-and-job-changes) filters. Each filter is documented on the People search page. Filters are validated before the query runs: unknown field names, blank values and values outside a closed list return HTTP `422` with the offending field and suggested values. See [Search filter validation](https://docs.reversecontact.com/guides/errors-retries#search-filter-validation). ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (SearchCount): Total number of matching profiles | Field | Type | Description | | ------- | ------ | ------------------------------------------------- | | `count` | number | Total profiles matching the supplied filters, across all pages | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/search/people/count \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "currentFunction": "Marketing", "currentSeniority": ["Director", "VP"], "location": { "countryCode": "FR" } }' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.search.countPeople({ currentFunction: "Marketing", currentSeniority: ["Director", "VP"], location: { countryCode: "FR" } }); console.log(`${result.data.count} matching people`); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.search.count_people( current_function="Marketing", current_seniority=["Director", "VP"], location={"countryCode": "FR"}, ) if result.data is not None and result.data.count is not None: print(f"{result.data.count:.0f} matching people") ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "count": 1842 }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "da5a072c-5ea9-4c20-9215-e03df33085e1", "executionTimeMs": 95 } } ``` --- # Company search > Search companies by industry, employee count, founding year or location and retrieve full company profiles with cursor pagination. - Page: https://docs.reversecontact.com/endpoints/search-companies - Endpoint: `POST https://api.reversecontact.com/v2/search/organizations` - Credits: 1 credit per company delivered. A page with no company is free. - Answer: synchronous. - Available in: REST API, MCP tool `search_companies`, TypeScript and Python SDKs, CLI command `rc search organizations`. - Last verified: 2026-11-03 ## Introduction The Company Search endpoint returns full company profiles matching a combination of filters, combined with AND logic. Results are drawn from our cache-based search index: this endpoint never triggers a live fetch. Every result is a complete profile, the same shape the [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) endpoint returns, so you do not need a second call to get the full data of a hit. Use the free [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count) endpoint to check how many companies match before you spend credits. > Coming from `POST /v2/search/companies`? See [Migrating company search to the new endpoint](https://docs.reversecontact.com/guides/migrating-search-companies-to-organizations). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body Every filter is optional, but a request needs at least one, and filters are combined with AND logic. The table below lists the scalar filters, then the pagination parameters `cursor` and `perPage`. The nested-object filters (`employeeCountRange`, `foundedYear`, `followersCount`, `location`) follow it as expandable fields. | Name | Type | Required | Description | | ---------------- | ------------------ | -------- | -------------------------------------------------------------------------- | | `companyName` | string | No | Filter by company name | | `companyDomain` | string | No | Filter by website domain (e.g. `acme.com`). Disposable domains are rejected with `422` | | `industry` | string \| string[] | No | Industry, or array of industries: a company matches if its industry is any of them. See [Industry values](https://docs.reversecontact.com/reference/industries) for the full list | | `specialities` | string \| string[] | No | Speciality, or array of specialities: a company matches if it lists any of them. See [Specialities](#specialities) | | `maxDataAgeDate` | string | No | ISO 8601 datetime. Only returns companies refreshed after this date. Needs at least one other filter | | `cursor` | string | No | Opaque cursor returned as `metadata.nextCursor`; request the next page | | `perPage` | number | No | Companies per page (1 to 100, default `25`) | > Filters are validated before the query runs. Unknown field names, blank values (`""`, `[]`), values outside a closed list (`industry`, `location.countryCode`, `location.area`) and inconsistent ranges return HTTP `422` with the offending field, the reason and suggested values, and cost 0 credits. Unknown fields are rejected rather than ignored, because an ignored filter would return unfiltered results. Each list filter accepts at most 20 values. See [Search filter validation](https://docs.reversecontact.com/guides/errors-retries#search-filter-validation). > A request needs at least one filter: a body with only `cursor` and `perPage` returns HTTP `422` with `NO_FILTER_PROVIDED`. `maxDataAgeDate` narrows a search but selects nothing on its own, so a request whose only filter is `maxDataAgeDate` returns `NO_FILTER_PROVIDED` too. `followersCount` sent as the only filter needs both `min` and `max`, otherwise the request returns `INCOMPLETE_RANGE_FILTER`. None of these requests costs credits. > `industry` is matched exactly against the [industry list](https://docs.reversecontact.com/reference/industries). Case, surrounding spaces, accents and `&` versus `and` are normalized for you, and any other unlisted value returns HTTP `422` with `INVALID_FILTER_VALUE` and up to three suggested values, never an empty page. **employeeCountRange** (RangeFilter): Filter by employee count. Both bounds are optional and inclusive, from 0 to 1,000,000,000. Headcount is stored as fixed size brackets (`1`, `2-10`, `11-50`, `51-200`, `201-500`, `501-1000`, `1001-5000`, `5001-10000`, `10001+`), not as an exact number, and a range only matches a bracket it fully contains. Send bracket boundaries, otherwise the range contains no bracket, matches nothing and returns an empty `data.data` array. | Field | Type | Description | | ----- | ------ | ------------------------ | | `min` | number | Lower bound, inclusive. Must be a bracket start: `1`, `2`, `11`, `51`, `201`, `501`, `1001`, `5001`, `10001` | | `max` | number | Upper bound, inclusive. Must be a bracket end: `1`, `10`, `50`, `200`, `500`, `1000`, `5000`, `10000`. Omit it to include `10001+` | > `{ "min": 11, "max": 50 }` matches the `11-50` bracket. `{ "min": 11, "max": 200 }` spans `11-50` and `51-200`. `{ "min": 10001 }` matches `10001+`. `{ "min": 11, "max": 20 }` returns an empty array, because no whole bracket fits between 11 and 20. **foundedYear** (RangeFilter): Filter by founding year. Both bounds are optional and inclusive, from 0 to 2100. | Field | Type | Description | | ----- | ------ | ------------------------------------ | | `min` | number | Earliest founding year, inclusive | | `max` | number | Latest founding year, inclusive | **followersCount** (RangeFilter): Filter by follower count. Both bounds are optional and inclusive, from 0 to 1,000,000,000. | Field | Type | Description | | ----- | ------ | ------------------------ | | `min` | number | Minimum value, inclusive | | `max` | number | Maximum value, inclusive | > Range bounds are whole numbers, `min` must not be greater than `max`, and a bound outside the accepted interval (for example a `foundedYear.max` of `2200`) returns HTTP `422` with `INVALID_RANGE` and the accepted maximum in `error.details`. The request costs 0 credits. **location** (LocationFilter): Headquarters location filter. Each sub-field is optional and accepts a single string or an array of strings (OR matching), with a maximum of 20 values per sub-field. Beyond that, the request returns HTTP `422` with `FILTER_VALUES_LIMIT_EXCEEDED` and costs 0 credits: split it into several searches. | Field | Type | Description | | ------------- | ------------------ | ----------------------------------------------------------------- | | `country` | string \| string[] | Country name in English (e.g. `"Germany"`). An ISO code sent here is rejected with `422`: put codes in `countryCode` | | `countryCode` | string \| string[] | ISO 3166-1 alpha-2 country code (e.g. `"DE"`). Values outside the list are rejected with `422` | | `area` | string \| string[] | Zone id from the [area values](https://docs.reversecontact.com/reference/areas) (e.g. `"MR:EMEA"`, `"MC:DACH"`, `"IG:EU"`): matches companies whose headquarters country is a member of the zone | | `region` | string \| string[] | State or region | | `city` | string \| string[] | City, in its local spelling (e.g. `"München"`) | > Zones in `area` add up with each other and with `country` and `countryCode`, while `region` and `city` narrow the result. `{ "area": "IG:GCC", "countryCode": "EG" }` covers the six Gulf Cooperation Council countries and Egypt. A zone only adds countries: there is no exclusion. Only full zone ids are accepted, so `"GCC"` returns HTTP `422` with `"IG:GCC"` in `details.suggestions`. See [Area values](https://docs.reversecontact.com/reference/areas). ## Specialities `specialities` matches the specialities a company lists on its profile, such as `"SaaS"` or `"Machine Learning"`. Matching is word-based, not exact: case and accents are ignored, and every word of a value must appear among the company's specialities, so `"SaaS"` also matches a company that lists `"B2B SaaS"`. A list is OR-matched: a company matches when it lists at least one of the requested specialities. **Example** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/search/organizations \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "specialities": ["SaaS", "Machine Learning"], "foundedYear": { "min": 2015 }, "location": { "countryCode": "DE", "city": "München" }, "perPage": 25 }' ``` > A very broad query, such as one common industry on its own, has to rank a very large set of companies and can be slow or time out with HTTP `504`, which costs 0 credits. Pair it with an employee count range, a founding year or a precise location to keep the search fast. ## Pagination and credits Company search uses cursor pagination. Each response returns up to `perPage` companies (1 to 100, default `25`) and a `metadata.nextCursor` value. Pass that value back as `cursor` in the next request, with the same filters, to get the following page; `nextCursor` is `null` on the last page. The cursor is opaque: send it back unchanged, without parsing or altering it. `perPage` may change from one page to the next. A page can hold fewer companies than `perPage`, or none at all, while `nextCursor` is still set, because some companies are removed after the page is built. Keep paging until `nextCursor` is `null`: that is the only end-of-results signal. There is no `page` parameter. A request that sends `page` returns HTTP `422` with `error.code` `UNKNOWN_FIELD`, and the error names `cursor` as its replacement. The cursor does not expire, and a long pause between two pages is fine: the next page picks up where the previous one stopped. If companies were refreshed in the meantime, one of them can occasionally repeat or be skipped at the page boundary. When the last page is exactly full, its `nextCursor` is not `null` yet, and the request it leads to returns an empty page, which costs 0 credits. An invalid cursor returns HTTP `400` with the message `Invalid or expired cursor`. This happens when a filter is changed between two pages (keep the filters identical and only add the `cursor`), or when the cursor is malformed (truncated or altered). `perPage` above `100` returns HTTP `422` with `error.code` `PAGINATION_LIMIT_EXCEEDED`, and the `error.details` object identifies the field and maximum, for example `{ "field": "perPage", "max": 100 }`. A `perPage` that is not a whole number returns HTTP `422` with `error.code` `INVALID_PAGINATION`. Company search costs **1 credit per company delivered**. Companies removed for compliance reasons before the response are not billed. A page that returns 25 companies costs 25 credits, and a page that returns no company costs 0 credits. Before it runs, a request needs as many credits available as its `perPage`: with fewer, it returns HTTP `402` with `INSUFFICIENT_CREDITS` and costs nothing. Check the size of a query first with the free [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count) endpoint, and lower `perPage` to control how many credits a single request can spend. ## Response structure > This endpoint uses a nested envelope: the top-level V2 `data` field wraps an inner search payload that contains the results array (`data.data`) and the pagination info (`data.metadata`). See the example response for the exact shape. | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data.data** (CompanyFull[]): Full company profiles returned for this page, wrapped under `data.data` in the envelope. Empty array if no match. Each item is the same full profile shape as the [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) endpoint; the table below lists its fields. | Field | Type | Description | | -------------------- | -------------------------- | -------------------------------------------------------------------------- | | `id` | string | Reverse Contact company id (`com_...`). Use it as the `id` parameter of the [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) endpoint | | `companyName` | string | Company name | | `linkedinId` | string | Social company identifier | | `publicId` | string | Universal name / slug (the `/company/xxx` part of the URL) | | `linkedinUrl` | string \| null | Full Social company URL | | `employeesCount` | number \| null | Exact number of employees | | `followersCount` | number \| null | Number of Social followers | | `employeeCountRange` | EmployeeCountRange \| null | Employee count bracket | | `websiteUrl` | string \| null | Official website URL | | `tagline` | string \| null | Tagline / slogan | | `description` | string \| null | Full description | | `industry` | string \| null | Industry (e.g. `"Software Development"`) | | `phone` | string \| null | Phone number | | `specialities` | string[] | Specialities listed on the profile | | `logoUrl` | string \| null | Logo image URL | | `foundedOn` | FoundedOn \| null | Founding year wrapper | | `locations` | Location[] | All office locations registered on the profile (can be empty) | | `backgroundUrl` | string \| null | Background / banner image URL | **headquarter** (CompanyHeadquarter): Headquarters location (`null` if unknown). The `location` filter matches these fields. | Field | Type | Description | | ---------------- | -------------- | -------------------------------------------- | | `street1` | string \| null | Primary street address line | | `street2` | string \| null | Secondary street address line | | `postalCode` | string \| null | Postal code | | `city` | string \| null | City | | `geographicArea` | string \| null | State or region | | `country` | string \| null | Country ISO 3166 code (e.g. `"US"`, `"FR"`) | **employeeCountRange** (EmployeeCountRange): Employee count bracket (`null` if unknown). Its fields are `start` / `end`, unlike the request `RangeFilter`, which uses `min` / `max`. For the top bracket (`10,001+`) there is no upper bound, and `end` can be `10001` or `1`: read any range whose `start` is `10001` as `10,001+`. Use `employeesCount` for the exact headcount when available. | Field | Type | Description | | ------- | ------ | ---------------------------------------------------------------------------------------- | | `start` | number | Lower bound of the range | | `end` | number | Upper bound of the range. Ignore it for the top bracket, where `start` is `10001` | **foundedOn** (FoundedOn): Founding year (`null` if unknown) | Field | Type | Description | | ------ | ------ | --------------- | | `year` | number | Four-digit year | **locations** (Location[]): All office locations registered on the profile (can be empty) | Field | Type | Description | | ------------ | -------------- | ------------------------------------------------------------------------------ | | `street1` | string \| null | Street address line 1 | | `street2` | string \| null | Street address line 2 | | `postalCode` | string \| null | Postal / ZIP code | | `city` | string \| null | City | | `state` | string \| null | State or region (named `state` here, not `geographicArea` as in `headquarter`) | | `country` | string \| null | Country ISO 3166 code (e.g. `"US"`, `"FR"`) | **data.metadata** (SearchPagination): Pagination metadata wrapped under `data.metadata` in the envelope. | Field | Type | Description | | ------------ | -------------- | ---------------------------------------------------------------------- | | `perPage` | number | Companies requested per page | | `count` | number | Companies returned in this page. Use [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count) for the total matching set | | `nextCursor` | string \| null | Opaque cursor for the next page, `null` on the last page | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | -------------------------------- | | `creditsConsumed` | number | Credits consumed by this request | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information (not to be confused with `data.metadata` which is pagination) | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/search/organizations \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "industry": ["Software Development", "Technology, Information and Internet"], "employeeCountRange": { "min": 51, "max": 500 }, "foundedYear": { "min": 2015 }, "location": { "country": "United States", "city": ["San Francisco", "Austin"] }, "perPage": 25 }' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); // 1 credit per company delivered: maxResults caps how many companies the iterator requests. const companies = client.search.iterOrganizations( { industry: ["Software Development", "Technology, Information and Internet"], employeeCountRange: { min: 51, max: 500 }, foundedYear: { min: 2015 }, location: { country: "United States", city: ["San Francisco", "Austin"] }, perPage: 25 }, { maxResults: 100 } ); for await (const company of companies) { console.log(company.companyName, company.websiteUrl); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: # 1 credit per company delivered: max_results caps how many companies the iterator requests. companies = client.search.iter_organizations( industry=["Software Development", "Technology, Information and Internet"], employee_count_range={"min": 51, "max": 500}, founded_year={"min": 2015}, location={"country": "United States", "city": ["San Francisco", "Austin"]}, per_page=25, max_results=100, ) for company in companies: if company is not None: print(company.company_name, company.website_url) ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "data": [ { "id": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Acme Corp", "linkedinId": "12345678", "publicId": "acme", "linkedinUrl": "https://www.social.com/company/acme", "employeesCount": 452, "followersCount": 28512, "employeeCountRange": { "start": 201, "end": 500 }, "websiteUrl": "https://www.acmecorp.com", "tagline": "Building the future of work", "description": "Acme Corp builds collaboration software for enterprise teams. Founded in 2016, it serves over 10,000 customers worldwide.", "industry": "Software Development", "phone": "+1 (415) 555-0123", "specialities": ["SaaS", "Enterprise Software", "Collaboration", "API Integration"], "headquarter": { "street1": "1 Market Street", "street2": "Suite 400", "postalCode": "94105", "city": "San Francisco", "geographicArea": "California", "country": "US" }, "logoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "foundedOn": { "year": 2016 }, "locations": [ { "street1": "1 Market Street", "street2": "Suite 400", "postalCode": "94105", "city": "San Francisco", "state": "California", "country": "US" }, { "street1": "500 Congress Avenue", "street2": null, "postalCode": "78701", "city": "Austin", "state": "Texas", "country": "US" } ], "backgroundUrl": "https://media.licdn.com/dms/image/v2/D5616AQ.../background.jpg" } ], "metadata": { "perPage": 25, "count": 1, "nextCursor": null } }, "error": null, "metadata": { "requestId": "85612342-68ff-4d7a-8385-bd4ca546355d", "executionTimeMs": 320 }, "quotas": { "creditsConsumed": 1, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4522, "left": 5478 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2026-10-16T14:16:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2026-10-16T14:16:00.000Z" } } } } ``` --- # Company search count > Count matching companies with the same filters as Company search, for free, to size a query before paying 1 credit per company delivered. - Page: https://docs.reversecontact.com/endpoints/search-companies-count - Endpoint: `POST https://api.reversecontact.com/v2/search/organizations/count` - Credits: Free. - Answer: synchronous. - Available in: REST API, MCP tool `count_companies`, TypeScript and Python SDKs. - Last verified: 2026-10-16 ## Introduction The Company Search Count endpoint returns the total number of companies matching the same filters as [Company search](https://docs.reversecontact.com/endpoints/search-companies), without returning any profile. Use it to size a query before spending credits: [Company search](https://docs.reversecontact.com/endpoints/search-companies) costs 1 credit per company delivered, while this endpoint is free. The endpoint accepts exactly the filters of Company search, without the pagination parameters. A count is a single number, so there is no page to request: sending `cursor` or `perPage` returns HTTP `422` with `UNKNOWN_FIELD`. Like [Usage](https://docs.reversecontact.com/endpoints/usage), this endpoint **costs 0 credits**, but it still counts toward your rate limits. A free trial with no credit left gets `402 NO_CREDITS`. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body Filters are combined with AND logic, exactly as on [Company search](https://docs.reversecontact.com/endpoints/search-companies#request-body): the scalar filters (`companyName`, `companyDomain`, `industry`, `specialities`, `maxDataAgeDate`), the nested-object filters (`employeeCountRange`, `foundedYear`, `followersCount`, `location`) and the [specialities](https://docs.reversecontact.com/endpoints/search-companies#specialities) matching rules. The Company search page documents each of them. At least one filter is required, and `maxDataAgeDate` cannot be the only one: both cases return HTTP `422` with `NO_FILTER_PROVIDED`. Filters are validated before the query runs: unknown field names, blank values and values outside a closed list return HTTP `422` with the offending field and suggested values. See [Search filter validation](https://docs.reversecontact.com/guides/errors-retries#search-filter-validation). ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (SearchCount): Total number of matching companies | Field | Type | Description | | ------- | ------ | ------------------------------------------------- | | `count` | number | Total companies matching the supplied filters, across all pages | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/search/organizations/count \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "industry": "Software Development", "employeeCountRange": { "min": 51, "max": 200 }, "location": { "countryCode": "FR" } }' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.search.countOrganizations({ industry: "Software Development", employeeCountRange: { min: 51, max: 200 }, location: { countryCode: "FR" } }); console.log(`${result.data.count} matching companies`); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.search.count_organizations( industry="Software Development", employee_count_range={"min": 51, "max": 200}, location={"countryCode": "FR"}, ) if result.data is not None and result.data.count is not None: print(f"{result.data.count:.0f} matching companies") ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "count": 2317 }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "29ace142-a14e-4278-a4be-806ec25c77ee", "executionTimeMs": 88 } } ``` --- # People lookalike > Find people similar to a seed profile by Social URL or person id, using its function, seniority and location, and retrieve full profiles. - Page: https://docs.reversecontact.com/endpoints/search-people-lookalike - Endpoint: `POST https://api.reversecontact.com/v2/search/people/lookalike` - Credits: 1 credit per profile delivered. A page with no profile is free. - Answer: synchronous. - Available in: REST API, MCP tool `search_persons_lookalike`, TypeScript and Python SDKs. - Last verified: 2026-11-03 ## Introduction The People Lookalike endpoint returns the people who look like one seed profile. You name the seed by its Social profile URL or by its Reverse Contact person id (`prs_...`), and the search is built from what the seed does today: its current function and sub-function, with the time it has spent in them when it is known, its current seniority and its location (country and city). The seed itself is never part of the results. Every result is a complete profile, the same shape [People search](https://docs.reversecontact.com/endpoints/search-people) returns, with the same cursor pagination and the same price: 1 credit per profile delivered. Results come from our cache-based search index: this endpoint never triggers a live fetch. Use it when you know one good profile and want more people like it. To choose the criteria yourself, use [People search](https://docs.reversecontact.com/endpoints/search-people). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body Provide exactly one seed: `url` or `id`. Sending both returns HTTP `422`, because a lookalike is built from a single profile. | Name | Type | Required | Description | | --------- | ------ | ----------- | ---------------------------------------------------------------------------------------------------- | | `url` | string | Conditional | Public Social profile URL of the seed (e.g. `https://social.com/in/janedoe`). Required unless `id` is given. | | `id` | string | Conditional | Reverse Contact person id of the seed (e.g. `prs_01jbd5vewyenebxgaz869ffe5t`), as returned in the `id` field of any person response. Required unless `url` is given. | | `cursor` | string | No | Opaque cursor returned as `metadata.nextCursor`; request the next page | | `perPage` | number | No | Profiles per page (1 to 100, default `25`) | > The search criteria come from the seed, so the request takes no search filter. Any other field, such as `location` or `currentSeniority`, returns HTTP `422` with `error.code` `LOOKALIKE_FILTERS_NOT_SUPPORTED` and the offending fields in `error.details.fields`, and costs 0 credits. A filter is refused rather than ignored, because an ignored filter would return results you did not ask for. > The seed must already be in our search index. A profile that is not indexed yet returns HTTP `404` and costs 0 credits: no lookalike can be built from it, so try another seed. An `id` that matches no profile returns HTTP `422` with `error.code` `VALIDATION_ERROR`, and a seed under an active data subject request (GDPR/CCPA) returns HTTP `451` with `error.code` `DATA_SUBJECT_BLOCKED`. Neither costs credits. ## Pagination and credits People lookalike uses cursor pagination, like People search. Each response returns up to `perPage` profiles (1 to 100, default `25`) and a `metadata.nextCursor` value. To get the following page, send the same seed again with that value as `cursor`; `nextCursor` is `null` on the last page. The cursor is opaque: send it back unchanged, without parsing or altering it. There is no `page` parameter. A page can hold fewer profiles than `perPage`, or none at all, while `nextCursor` is still set, because some profiles are removed after the page is built. Keep paging until `nextCursor` is `null`: that is the only end-of-results signal. `perPage` above `100` returns HTTP `422` with `error.code` `PAGINATION_LIMIT_EXCEEDED`, and a `perPage` that is not a whole number returns HTTP `422` with `error.code` `INVALID_PAGINATION`. People lookalike costs **1 credit per profile delivered**. Compliance-blocked profiles are removed before the response and are not billed. A page that returns 25 profiles costs 25 credits, and a page that returns no profile costs 0 credits. Lower `perPage` to control how many credits a single request can spend. ## Response structure > This endpoint uses a nested envelope: the top-level V2 `data` field wraps a search payload that holds the results array (`data.data`) and the pagination info (`data.metadata`). See the example response for the exact shape. | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data.data** (PersonFull[]): Full profiles of the people who look like the seed, wrapped under `data.data` in the envelope. The seed is never among them. Empty array if no one matches. Each item is the same full profile shape as the [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) endpoint; the table below lists its main fields. | Field | Type | Description | | -------------------- | -------------- | -------------------------------------------------------------------- | | `id` | string | Reverse Contact person id (`prs_...`). Send it back as `id` to use this profile as the next seed | | `publicId` | string | Social public identifier (the `/in/xxx` slug) | | `memberId` | string \| null | Upstream member identifier when available | | `linkedinUrl` | string | Full Social profile URL | | `firstName` | string \| null | First name | | `lastName` | string \| null | Last name | | `headline` | string \| null | Profile headline | | `summary` | string \| null | Profile summary | | `isOpenToWork` | boolean | Whether the profile is flagged open to work | | `hasPremium` | boolean | Whether the profile has Social Premium | | `hasVerificationBadge` | boolean | Whether the profile has a verification badge | | `photoUrl` | string \| null | Profile photo URL | | `creationDate` | string \| null | When the profile was created (ISO 8601) | | `followersCount` | number \| null | Follower count | | `connectionsCount` | number \| null | Connection count | | `currentPosition` | object \| null | Current job position ([how it is chosen](https://docs.reversecontact.com/guides/current-position-resolution)) | | `experience` | array | Professional experiences, same shape as `currentPosition` | | `education` | array | Education history | | `skills` | string[] | Skills listed on the profile | | `languages` | array | Spoken languages | | `certifications` | array | Professional certifications | | `recommendations` | array | Received recommendations | **location** (PersonLocation): Geographic location (`null` if unknown) | Field | Type | Description | | ------------- | -------------- | ------------------------------ | | `city` | string \| null | City | | `state` | string \| null | State or region | | `country` | string \| null | Country name | | `countryCode` | string \| null | ISO country code (e.g. `"US"`) | | `rawLocation` | string \| null | Raw location string as displayed on the profile | **currentPosition** (CurrentPosition): Current job position (`null` if no current position) | Field | Type | Description | | ------------------ | -------------- | ------------------------------------------ | | `title` | string \| null | Job title | | `description` | string \| null | Position description | | `contractType` | string \| null | Contract type | | `companyId` | string \| null | Reverse Contact company id (`com_...`). Pass it as `id` to the [Company Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-company) | | `companyName` | string \| null | Company name | | `companyLinkedinId` | string \| null | Company Social identifier | | `companyUrl` | string \| null | Company Social URL | | `companyLocation` | string \| null | Company location | | `companyLogoUrl` | string \| null | Company logo URL | | `startEndDate` | object \| null | Period boundaries ([DateRange](https://docs.reversecontact.com/api-reference)) | **data.metadata** (SearchPagination): Pagination metadata wrapped under `data.metadata` in the envelope. | Field | Type | Description | | ------------ | -------------- | ---------------------------------------------------------------------- | | `perPage` | number | Profiles requested per page | | `count` | number | Profiles returned in this page | | `nextCursor` | string \| null | Opaque cursor for the next page, `null` on the last page | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | -------------------------------- | | `creditsConsumed` | number | Credits consumed by this request | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information (not to be confused with `data.metadata` which is pagination) | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/search/people/lookalike \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://social.com/in/janedoe", "perPage": 25 }' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); // 1 credit per profile delivered: maxResults caps how many lookalikes the iterator requests. const lookalikes = client.search.iterPeopleLookalike( { id: "prs_01jbd5vewyenebxgaz869ffe5t", perPage: 25 }, { maxResults: 50 } ); for await (const person of lookalikes) { console.log(person.firstName, person.lastName); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: # 1 credit per profile delivered: max_results caps how many lookalikes the iterator requests. lookalikes = client.search.iter_people_lookalike( id="prs_01jbd5vewyenebxgaz869ffe5t", per_page=25, max_results=50 ) for person in lookalikes: if person is not None: print(person.first_name, person.last_name) ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "data": [ { "id": "prs_01h2x3y4z5a6b7c8d9e0f1g2h3", "publicId": "janedoe", "memberId": "ACoAAB12345", "linkedinUrl": "https://www.social.com/in/janedoe", "firstName": "Jane", "lastName": "Doe", "headline": "VP of Marketing at Acme Corp | Demand Gen & Brand", "summary": "Experienced marketing leader with 15+ years driving growth for B2B SaaS companies.", "pronoun": "she/her", "isOpenToWork": false, "hasPremium": true, "hasVerificationBadge": true, "isInRemembrance": false, "photoUrl": "https://media.licdn.com/dms/image/v2/D5603AQ.../profile-photo.jpg", "backgroundUrl": null, "creationDate": "2009-03-15T00:00:00.000Z", "followersCount": 12480, "connectionsCount": 500, "location": { "city": "San Francisco", "state": "California", "country": "United States of America", "countryCode": "US", "rawLocation": "San Francisco Bay Area" }, "currentPosition": { "title": "VP of Marketing", "description": "Leading a team of 25 marketers across demand gen, product marketing, and brand.", "contractType": "Full-time", "companyId": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Acme Corp", "companyLinkedinId": "12345678", "companyUrl": "https://www.social.com/company/acme-corp", "companyLocation": "San Francisco, CA", "companyLogoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "startEndDate": { "start": "2021-06-01T00:00:00.000Z", "end": null } }, "experience": [ { "title": "VP of Marketing", "description": "Leading a team of 25 marketers across demand gen, product marketing, and brand.", "contractType": "Full-time", "companyId": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Acme Corp", "companyLinkedinId": "12345678", "companyUrl": "https://www.social.com/company/acme-corp", "companyLocation": "San Francisco, CA", "companyLogoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "startEndDate": { "start": "2021-06-01T00:00:00.000Z", "end": null } }, { "title": "Director of Marketing", "description": "Built the marketing function from scratch. Scaled from 0 to $5M ARR.", "contractType": "Full-time", "companyId": "com_01jbd7m3n8p2q5r9s4t6v1w0xy", "companyName": "Startup Inc", "companyLinkedinId": "98765432", "companyUrl": "https://www.social.com/company/startup-inc", "companyLocation": "Remote", "companyLogoUrl": null, "startEndDate": { "start": "2018-01-01T00:00:00.000Z", "end": "2021-05-31T00:00:00.000Z" } } ], "education": [ { "degreeName": "MBA", "fieldOfStudy": "Marketing", "description": null, "grade": null, "schoolName": "Stanford Graduate School of Business", "schoolUrl": "https://www.social.com/school/stanford-gsb", "schoolLogoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "startEndDate": { "start": "2012-09-01T00:00:00.000Z", "end": "2014-06-15T00:00:00.000Z" } } ], "skills": ["Marketing Strategy", "B2B SaaS", "Demand Generation", "Product Marketing"], "languages": [{ "language": "English", "proficiency": "Native or bilingual" }], "certifications": [], "recommendations": [], "testScores": [] } ], "metadata": { "perPage": 25, "count": 1, "nextCursor": "cur_01j9x8r5n3a9c7e5g1b3d9f7" } }, "error": null, "metadata": { "requestId": "4d88c278-174f-4c0a-b72c-248e1e5e050c", "executionTimeMs": 520 }, "quotas": { "creditsConsumed": 1, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4522, "left": 5478 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2026-09-14T14:16:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2026-09-14T14:16:00.000Z" } } } } ``` --- # Agentic Search > Describe the people you want in plain language and get full profiles back, with the filters we read from your prompt. - Page: https://docs.reversecontact.com/endpoints/search-people-prompt - Endpoint: `POST https://api.reversecontact.com/v2/search/people/prompt` - Credits: 1 credit per page for the search, plus 1 credit per profile delivered. - Answer: synchronous. - Available in: REST API, TypeScript and Python SDKs. - Last verified: 2026-11-03 ## Introduction The Agentic Search endpoint takes one sentence, such as `VP Sales at software companies with 51 to 200 employees in Paris`, reads it into [People search](https://docs.reversecontact.com/endpoints/search-people) filters, runs that search and returns the matching full profiles. You do not build the filters yourself: the response tells you which ones were applied, in `data.interpretation`. Every result is a complete profile, the same shape People search returns, with the same cursor pagination. Results come from our cache-based search index: this endpoint never triggers a live fetch. Use it when the request comes from a person typing what they want, or when you prefer not to map criteria to filter names. To control every filter yourself, use [People search](https://docs.reversecontact.com/endpoints/search-people). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body | Name | Type | Required | Description | | --------- | ------ | -------- | ---------------------------------------------------------------------------------------------------- | | `prompt` | string | Yes | Plain-language description of the people to find, in any language. At least 3 characters and at most 1000 tokens | | `cursor` | string | No | Opaque cursor returned as `metadata.nextCursor`; request the next page with the same `prompt` | | `perPage` | number | No | Profiles per page (1 to 100, default `25`) | > The 1000 token limit on `prompt` is about 4000 characters of English text. It is counted conservatively: four plain ASCII characters count as one token, and every other character (an accented letter, a non-Latin script) counts as one token on its own. A longer prompt returns HTTP `422` with `error.code` `INVALID_PROMPT` and costs 0 credits. > The filters come from the prompt, so the request takes no search filter. Any other field, such as `location` or `currentSeniority`, returns HTTP `422` with `error.code` `PROMPT_FILTERS_NOT_SUPPORTED` and the offending fields in `error.details.fields`, and costs 0 credits. A filter is refused rather than ignored, because an ignored filter would return results you did not ask for. Describe it in the prompt instead, or send explicit filters to People search. ## What a prompt can express A prompt is read into these People search filters: | What you describe | Filters it becomes | | ----------------- | ------------------ | | A current or past job title, to include or exclude | `currentPositionTitle`, `positionTitle` and their `exclude...` counterparts | | A current or past company, to include or exclude | `currentCompanyName`, `companyName` and their `exclude...` counterparts | | An industry | `currentCompanyIndustry`, `companyIndustry`, `excludeCompanyIndustry`, from the [industry list](https://docs.reversecontact.com/reference/industries) | | A company size | `currentCompanyEmployeeCountRange` | | A place | `location` (country, region or city) | | A career length or an audience size | `totalExperienceMonths`, `followersCount` | | A person's name or headline keywords | `firstName`, `lastName`, `headline` | | A skill or a technology the person masters | `skills` | | A spoken language, to include or exclude | `languages`, `excludeLanguages`, from the [language names](https://docs.reversecontact.com/endpoints/search-people#languages) | | Words to find anywhere in the profile | `keywords` | | A recent job change | `changedJobsWithinDays` (30, 60, 90 or 120 days) | | How people are employed, such as freelancers or interns, to include or exclude | `currentContractType`, `contractType` and their `exclude...` counterparts, from the [contract types](https://docs.reversecontact.com/endpoints/search-people#contract-types) | | A school or university the person attended | `schoolName` | | Open to work, currently employed, Premium, verified | `isOpenToWork`, `isCurrentlyEmployed`, `isPremium`, `isVerified` | | How recent the data must be | `maxDataAgeDate` | Job titles and company names are matched as written, with the same token-based matching as People search, so write a title the way it appears on profiles. A seniority word such as `VP` or `Director` is matched inside the job title: the [position taxonomy](https://docs.reversecontact.com/endpoints/search-people#position-taxonomy) filters (`currentFunction`, `currentSubFunction`, `currentSeniority` and `experience`) are not read from a prompt yet. To filter on them, use People search. Anything the prompt asks for that no filter covers, such as a salary, is left out of the search and reported in `data.interpretation.warnings`, so you can see what the results do not take into account. ## Pagination and credits Agentic Search uses cursor pagination, like People search. Each response returns up to `perPage` profiles (1 to 100, default `25`) and a `metadata.nextCursor` value. To get the following page, send the same `prompt` again with that value as `cursor`; `nextCursor` is `null` on the last page. The cursor is opaque: send it back unchanged, without parsing or altering it. There is no `page` parameter, and `perPage` may change from one page to the next. A page can hold fewer profiles than `perPage`, or none at all, while `nextCursor` is still set, because some profiles are removed after the page is built. Keep paging until `nextCursor` is `null`: that is the only end-of-results signal. The cursor does not expire, and a long pause between two pages is fine: the next page picks up where the previous one stopped. If profiles were refreshed in the meantime, one of them can occasionally repeat or be skipped at the page boundary. A cursor only works with the filters that produced it. A prompt keeps the filters it was first read into for 24 hours in your workspace, so resending it unchanged, from any of your API keys, always runs the same search. Spaces and line breaks do not count as a change. If you edit the prompt, or come back after 24 hours, the prompt is read again, and a cursor sent with filters other than the ones that produced it returns HTTP `400` and costs 0 credits: start again from the first page, without `cursor`. `perPage` above `100` returns HTTP `422` with `error.code` `PAGINATION_LIMIT_EXCEEDED`, and a `perPage` that is not a whole number returns HTTP `422` with `error.code` `INVALID_PAGINATION`. Agentic Search costs **1 credit per page for the search itself, plus 1 credit per profile delivered**. The search credit is charged on every page, including when no profile matches. Compliance-blocked profiles are removed before the response and are not billed. A page that returns 25 profiles costs 26 credits, and a page that returns no profile costs 1 credit. A request refused before the search runs, for any of the reasons listed under [Errors](#errors), costs 0 credits. Lower `perPage` to control how many credits a single request can spend. > `data.interpretation.filters` is a valid People search body. Send it to the free [People search count](https://docs.reversecontact.com/endpoints/search-people-count) endpoint to get the total number of matches, or to People search to adjust a filter by hand. ## Errors Every error below is returned before the search runs and costs 0 credits. When the prompt was read, `error.details.interpretation` carries the filters and warnings it produced, so you can see why it could not run. | HTTP | `error.code` | What happened | What to do | | ---- | ------------ | ------------- | ---------- | | `422` | `INVALID_PROMPT` | `prompt` is missing, is not a string, is shorter than 3 characters or longer than 1000 tokens | Send a prompt within the limits | | `422` | `PROMPT_FILTERS_NOT_SUPPORTED` | The body carries a field other than `prompt`, `cursor` and `perPage`. `error.details.fields` lists them | Describe those criteria in the prompt, or use People search | | `422` | `NO_FILTERS_INTERPRETED` | No filter could be read from the prompt, and a search without filters would scan the whole database. `error.details.interpretation.warnings` says what was not understood | Name a job title, a company, a place or an industry | | `422` | `PROMPT_FILTERS_REJECTED` | The filters read from the prompt cannot run together, for example the same title both included and excluded. `error.details.errors` lists each problem with its field, as in [Search filter validation](https://docs.reversecontact.com/guides/errors-retries#search-filter-validation) | Rephrase the prompt, or use People search | | `422` | `INVALID_PAGINATION`, `PAGINATION_LIMIT_EXCEEDED` | `perPage` is not a whole number, or is above 100 | Send a `perPage` from 1 to 100 | | `400` | `REQUEST_ERROR` | The cursor does not belong to the filters the prompt was read into (`Invalid or expired cursor`) | Start again from the first page, without `cursor` | | `502` | `PROMPT_INTERPRETATION_FAILED` | The prompt could not be read this time | Retry, or rephrase the prompt | | `504` | `PROMPT_INTERPRETATION_TIMEOUT` | Reading the prompt took too long | Retry with backoff | | `503` | `PROMPT_SEARCH_UNAVAILABLE` | Agentic Search is temporarily unavailable | Retry later, or use People search with explicit filters | ## Response structure > This endpoint uses a nested envelope: the top-level V2 `data` field wraps a search payload that holds the results array (`data.data`), the pagination info (`data.metadata`) and the filters read from the prompt (`data.interpretation`). See the example response for the exact shape. | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data.data** (PersonFull[]): Full profiles returned for this page, wrapped under `data.data` in the envelope. Empty array if no match. Each item is the same full profile shape as the [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) endpoint; the table below lists its main fields. | Field | Type | Description | | -------------------- | -------------- | -------------------------------------------------------------------- | | `id` | string | Reverse Contact person id (`prs_...`) | | `publicId` | string | Social public identifier (the `/in/xxx` slug) | | `memberId` | string \| null | Upstream member identifier when available | | `linkedinUrl` | string | Full Social profile URL | | `firstName` | string \| null | First name | | `lastName` | string \| null | Last name | | `headline` | string \| null | Profile headline | | `summary` | string \| null | Profile summary | | `isOpenToWork` | boolean | Whether the profile is flagged open to work | | `hasPremium` | boolean | Whether the profile has Social Premium | | `hasVerificationBadge` | boolean | Whether the profile has a verification badge | | `photoUrl` | string \| null | Profile photo URL | | `creationDate` | string \| null | When the profile was created (ISO 8601) | | `followersCount` | number \| null | Follower count | | `connectionsCount` | number \| null | Connection count | | `currentPosition` | object \| null | Current job position ([how it is chosen](https://docs.reversecontact.com/guides/current-position-resolution)) | | `experience` | array | Professional experiences, same shape as `currentPosition` | | `education` | array | Education history | | `skills` | string[] | Skills listed on the profile | | `languages` | array | Spoken languages | | `certifications` | array | Professional certifications | | `recommendations` | array | Received recommendations | **location** (PersonLocation): Geographic location (`null` if unknown) | Field | Type | Description | | ------------- | -------------- | ------------------------------ | | `city` | string \| null | City | | `state` | string \| null | State or region | | `country` | string \| null | Country name | | `countryCode` | string \| null | ISO country code (e.g. `"US"`) | | `rawLocation` | string \| null | Raw location string as displayed on the profile | **currentPosition** (CurrentPosition): Current job position (`null` if no current position) | Field | Type | Description | | ------------------ | -------------- | ------------------------------------------ | | `title` | string \| null | Job title | | `description` | string \| null | Position description | | `contractType` | string \| null | Contract type | | `companyId` | string \| null | Reverse Contact company id (`com_...`). Pass it as `id` to the [Company Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-company) | | `companyName` | string \| null | Company name | | `companyLinkedinId` | string \| null | Company Social identifier | | `companyUrl` | string \| null | Company Social URL | | `companyLocation` | string \| null | Company location | | `companyLogoUrl` | string \| null | Company logo URL | | `startEndDate` | object \| null | Period boundaries ([DateRange](https://docs.reversecontact.com/api-reference)) | **data.metadata** (SearchPagination): Pagination metadata wrapped under `data.metadata` in the envelope. | Field | Type | Description | | ------------ | -------------- | ---------------------------------------------------------------------- | | `perPage` | number | Profiles requested per page | | `count` | number | Profiles returned in this page. Use [People search count](https://docs.reversecontact.com/endpoints/search-people-count) with `data.interpretation.filters` for the total matching set | | `nextCursor` | string \| null | Opaque cursor for the next page, `null` on the last page. Send it with the same `prompt` | **data.interpretation** (PromptInterpretation): What the prompt was read into, the same on every page of a prompt. | Field | Type | Description | | ---------- | -------- | --------------------------------------------------------------------------- | | `filters` | object | The People search filters applied, keyed like the [People search](https://docs.reversecontact.com/endpoints/search-people) request body | | `warnings` | string[] | What the prompt asked for that the search does not apply, such as a salary. Empty when everything was applied | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | -------------------------------- | | `creditsConsumed` | number | Credits consumed by this request: 1 for the search plus 1 per profile delivered | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information (not to be confused with `data.metadata` which is pagination) | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/search/people/prompt \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "VP Sales at software companies with 51 to 200 employees in Paris who speak German and studied at HEC", "perPage": 25 }' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const page = await client.search.peopleByPrompt({ prompt: "VP Sales at software companies with 51 to 200 employees in Paris who speak German and studied at HEC", perPage: 25 }); console.log("Filters applied:", page.interpretation?.filters); console.log("Not applied:", page.interpretation?.warnings); console.log(`Got ${page.data.length} profiles, credits used: ${page.quotas?.creditsConsumed}`); // The same prompt with the next cursor, billed like the first page; null after the last page. const nextPage = await page.getNextPage(); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: page = client.search.people_by_prompt( prompt="VP Sales at software companies with 51 to 200 employees in Paris who speak German and studied at HEC", per_page=25, ) if page.interpretation is not None: print("Filters applied:", page.interpretation.filters) print("Not applied:", page.interpretation.warnings) print(f"Got {len(page.data)} profiles, next cursor: {page.next_cursor}") # The same prompt with the next cursor, billed like the first page; None after the last page. next_page = page.get_next_page() ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "data": [ { "id": "prs_01h2x3y4z5a6b7c8d9e0f1g2h3", "publicId": "claire-martin", "memberId": "ACoAAB67890", "linkedinUrl": "https://www.social.com/in/claire-martin", "firstName": "Claire", "lastName": "Martin", "headline": "VP Sales at Northwind | B2B SaaS", "summary": "Sales leader building revenue teams for European software companies.", "pronoun": null, "isOpenToWork": false, "hasPremium": true, "hasVerificationBadge": false, "isInRemembrance": false, "photoUrl": "https://media.licdn.com/dms/image/v2/D4E03AQ.../profile-photo.jpg", "backgroundUrl": null, "creationDate": "2011-05-02T00:00:00.000Z", "followersCount": 3120, "connectionsCount": 500, "location": { "city": "Paris", "state": "Île-de-France", "country": "France", "countryCode": "FR", "rawLocation": "Paris, Île-de-France, France" }, "currentPosition": { "title": "VP Sales", "description": "Leading a sales team of 18 across France and the DACH region.", "contractType": "Full-time", "companyId": "com_01jbd8a4c6e2g9h3k5m7n1p0qr", "companyName": "Northwind", "companyLinkedinId": "23456789", "companyUrl": "https://www.social.com/company/northwind", "companyLocation": "Paris, France", "companyLogoUrl": "https://media.licdn.com/dms/image/v2/C4E0BAQ.../logo.jpg", "startEndDate": { "start": "2022-03-01T00:00:00.000Z", "end": null } }, "experience": [ { "title": "VP Sales", "description": "Leading a sales team of 18 across France and the DACH region.", "contractType": "Full-time", "companyId": "com_01jbd8a4c6e2g9h3k5m7n1p0qr", "companyName": "Northwind", "companyLinkedinId": "23456789", "companyUrl": "https://www.social.com/company/northwind", "companyLocation": "Paris, France", "companyLogoUrl": "https://media.licdn.com/dms/image/v2/C4E0BAQ.../logo.jpg", "startEndDate": { "start": "2022-03-01T00:00:00.000Z", "end": null } } ], "education": [ { "degreeName": "Master in Management", "fieldOfStudy": "Business", "description": null, "grade": null, "schoolName": "HEC Paris", "schoolUrl": "https://www.social.com/school/hec-paris", "schoolLogoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "startEndDate": { "start": "2010-09-01T00:00:00.000Z", "end": "2013-06-30T00:00:00.000Z" } } ], "skills": ["SaaS Sales", "Team Leadership", "Negotiation"], "languages": [ { "language": "French", "proficiency": "Native or bilingual" }, { "language": "German", "proficiency": "Professional working proficiency" } ], "certifications": [], "recommendations": [], "testScores": [] } ], "metadata": { "perPage": 25, "count": 1, "nextCursor": "cur_01j9x9s6p4b0d8f6h2c4e0g8" }, "interpretation": { "filters": { "currentPositionTitle": "VP Sales", "currentCompanyIndustry": "Software Development", "currentCompanyEmployeeCountRange": { "min": 51, "max": 200 }, "location": { "countryCode": "FR", "city": "Paris" }, "languages": "German", "schoolName": "HEC" }, "warnings": [] } }, "error": null, "metadata": { "requestId": "ef1ae050-a957-46c4-b4fe-55f4ebf13d3e", "executionTimeMs": 5210 }, "quotas": { "creditsConsumed": 2, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4523, "left": 5477 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2026-09-29T14:16:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2026-09-29T14:16:00.000Z" } } } } ``` --- # Agentic Company Search > Describe the companies you want in plain language and get full company profiles back, with the filters we read from your prompt. - Page: https://docs.reversecontact.com/endpoints/search-companies-prompt - Endpoint: `POST https://api.reversecontact.com/v2/search/organizations/prompt` - Credits: 1 credit per page for the search, plus 1 credit per company delivered. - Answer: synchronous. - Available in: REST API, TypeScript and Python SDKs. - Last verified: 2026-11-03 ## Introduction The Agentic Company Search endpoint takes one sentence, such as `Fintech scale-ups headquartered in Berlin, 200 to 500 people`, reads it into [Company search](https://docs.reversecontact.com/endpoints/search-companies) filters, runs that search and returns the matching full company profiles. You do not build the filters yourself: the response tells you which ones were applied, in `data.interpretation`. Every result is a complete profile, the same shape Company search returns, with the same cursor pagination. Results are drawn from our cache-based search index: this endpoint never triggers a live fetch. Use it when the request comes from a person typing what they want, or when you prefer not to map criteria to filter names. To control every filter yourself, use [Company search](https://docs.reversecontact.com/endpoints/search-companies). To find people rather than companies, use [Agentic Search](https://docs.reversecontact.com/endpoints/search-people-prompt). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body | Name | Type | Required | Description | | --------- | ------ | -------- | ---------------------------------------------------------------------------------------------------- | | `prompt` | string | Yes | Plain-language description of the companies to find, in any language. At least 3 characters and at most 1000 tokens | | `cursor` | string | No | Opaque cursor returned as `metadata.nextCursor`; request the next page with the same `prompt` | | `perPage` | number | No | Companies per page (1 to 100, default `25`) | > The 1000 token limit of `prompt` is about 4000 characters of English text. It is counted conservatively: four plain ASCII characters count as one token, and every other character (an accented letter, a non-Latin script) counts as one token on its own. A longer prompt returns HTTP `422` with `error.code` `INVALID_PROMPT` and costs 0 credits. > The filters come from the prompt, so the request takes no search filter. Any other field, such as `industry` or `location`, returns HTTP `422` with `error.code` `PROMPT_FILTERS_NOT_SUPPORTED` and the offending fields in `error.details.fields`, and costs 0 credits. A filter is refused rather than ignored, because an ignored filter would return results you did not ask for. Describe it in the prompt instead, or send explicit filters to Company search. ## What a prompt can express A prompt is read into these Company search filters: | What you describe | Filters it becomes | | ----------------- | ------------------ | | A company name, or part of it | `companyName` | | A website or a domain | `companyDomain` | | An industry | `industry`, from the [industry list](https://docs.reversecontact.com/reference/industries) | | What the company specializes in | `specialities` | | A company size, in numbers or in words | `employeeCountRange` | | A founding year or period | `foundedYear` | | An audience size | `followersCount` | | A headquarters place | `location` (country, region or city) | | How recent the data must be | `maxDataAgeDate`, next to at least one other filter | A size word becomes a headcount range: a startup is 1 to 50 employees, a scale-up 51 to 500, a mid-market company 501 to 5000 and an enterprise 5001 and more. Headcount is stored as fixed brackets, and a range only matches the brackets it fully contains (see [Company search](https://docs.reversecontact.com/endpoints/search-companies)), so a size word or round bounds such as `200 to 500 people` work best. A founding period such as `founded after 2015` becomes `foundedYear` from 2016. Company names and places are kept as you write them, in any language. Companies store their headquarters city in its local spelling, so `München` matches more companies than `Munich`. Industries are read into the closest values of the industry list. Anything the prompt asks for that no filter covers, such as funding, revenue or headcount growth, is left out of the search and reported in `data.interpretation.warnings`, so you can see what the results do not take into account. ## Pagination and credits Agentic Company Search uses cursor pagination, like Company search. Each response returns up to `perPage` companies (1 to 100, default `25`) and a `metadata.nextCursor` value. To get the following page, send the same `prompt` again with that value as `cursor`; `nextCursor` is `null` on the last page. The cursor is opaque: send it back unchanged, without parsing or altering it. There is no `page` parameter, and `perPage` may change from one page to the next. A page can hold fewer companies than `perPage`, or none at all, while `nextCursor` is still set, because some companies are removed after the page is built. Keep paging until `nextCursor` is `null`: that is the only end-of-results signal. The cursor does not expire, and a long pause between two pages is fine: the next page picks up where the previous one stopped. If companies were refreshed in the meantime, one of them can occasionally repeat or be skipped at the page boundary. A cursor only works with the filters that produced it. A prompt keeps the filters it was first read into for 24 hours in your workspace, so resending it unchanged, from any of your API keys, always runs the same search. Spaces and line breaks do not count as a change. If you edit the prompt, or come back after 24 hours, the prompt is read again, and a cursor sent with filters other than the ones that produced it returns HTTP `400` and costs 0 credits: start again from the first page, without `cursor`. `perPage` above `100` returns HTTP `422` with `error.code` `PAGINATION_LIMIT_EXCEEDED`, and a `perPage` that is not a whole number returns HTTP `422` with `error.code` `INVALID_PAGINATION`. Agentic Company Search costs **1 credit per page for the search itself, plus 1 credit per company delivered**. The search credit is charged on every page, including when no company matches. Companies removed for compliance reasons before the response are not billed. A page that returns 25 companies costs 26 credits, and a page that returns no company costs 1 credit. Before it runs, a request needs `perPage` plus 1 credits available: with fewer, it returns HTTP `402` with `INSUFFICIENT_CREDITS` and costs nothing. A request refused before the search runs, for any of the reasons listed under [Errors](#errors), costs 0 credits. Lower `perPage` to control how many credits a single request can spend. > `data.interpretation.filters` is a valid Company search body. Send it to the free [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count) endpoint to know how many companies match in total, or to Company search to adjust a filter by hand. ## Errors Every error below is returned before the search runs and costs 0 credits. When the prompt was read, `error.details.interpretation` carries the filters and warnings it produced, so you can see why it could not run. | HTTP | `error.code` | What happened | What to do | | ---- | ------------ | ------------- | ---------- | | `422` | `INVALID_PROMPT` | `prompt` is missing, is not a string, is shorter than 3 characters or longer than 1000 tokens | Send a prompt within the limits | | `422` | `PROMPT_FILTERS_NOT_SUPPORTED` | The body carries a field other than `prompt`, `cursor` and `perPage`. `error.details.fields` lists them | Describe those criteria in the prompt, or use Company search | | `422` | `NO_FILTERS_INTERPRETED` | No filter could be read from the prompt, and a search without filters would scan the whole database. A data freshness on its own does not count as a filter. `error.details.interpretation.warnings` says what was not understood | Name an industry, a place, a company size or a company name | | `422` | `PROMPT_FILTERS_REJECTED` | The filters read from the prompt break a search rule. `error.details.errors` lists each problem with its field, as in [Search filter validation](https://docs.reversecontact.com/guides/errors-retries#search-filter-validation) | Rephrase the prompt, or use Company search | | `422` | `DISPOSABLE_EMAIL_DOMAIN` | The website read from the prompt is a disposable email provider | Name the company's own website | | `422` | `INVALID_PAGINATION`, `PAGINATION_LIMIT_EXCEEDED` | `perPage` is not a whole number, or is above 100 | Send a `perPage` from 1 to 100 | | `400` | `REQUEST_ERROR` | The cursor does not belong to the filters the prompt was read into (`Invalid or expired cursor`) | Start again from the first page, without `cursor` | | `502` | `PROMPT_INTERPRETATION_FAILED` | The prompt could not be read this time | Retry, or rephrase the prompt | | `504` | `PROMPT_INTERPRETATION_TIMEOUT` | Reading the prompt took too long | Retry with backoff | | `503` | `PROMPT_SEARCH_UNAVAILABLE` | Agentic Search is temporarily unavailable | Retry later, or use Company search with explicit filters | ## Response structure > This endpoint uses a nested envelope: the top-level V2 `data` field wraps an inner search payload that contains the results array (`data.data`), the pagination info (`data.metadata`) and the filters read from the prompt (`data.interpretation`). See the example response for the exact shape. | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data.data** (CompanyFull[]): Full company profiles returned for this page, wrapped under `data.data` in the envelope. Empty array if no match. Each item is the same full profile shape as the [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) endpoint; the table below lists its main fields. | Field | Type | Description | | -------------------- | -------------------------- | -------------------------------------------------------------------------- | | `id` | string | Reverse Contact company id (`com_...`). Use it as the `id` parameter of the [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) endpoint | | `companyName` | string | Company name | | `linkedinId` | string | Social company identifier | | `publicId` | string | Universal name / slug (the `/company/xxx` part of the URL) | | `linkedinUrl` | string \| null | Full Social company URL | | `employeesCount` | number \| null | Exact number of employees | | `followersCount` | number \| null | Number of Social followers | | `employeeCountRange` | EmployeeCountRange \| null | Employee count bracket, with `start` and `end` fields | | `websiteUrl` | string \| null | Official website URL | | `tagline` | string \| null | Tagline / slogan | | `description` | string \| null | Full description | | `industry` | string \| null | Industry (e.g. `"Software Development"`) | | `phone` | string \| null | Phone number | | `specialities` | string[] | Specialities listed on the profile | | `logoUrl` | string \| null | Logo image URL | | `foundedOn` | FoundedOn \| null | Founding year wrapper, with a `year` field | | `locations` | Location[] | All office locations registered on the profile (can be empty) | | `backgroundUrl` | string \| null | Background / banner image URL | **headquarter** (CompanyHeadquarter): Headquarters location (`null` if unknown). The `location` filter matches these fields. | Field | Type | Description | | ---------------- | -------------- | -------------------------------------------- | | `street1` | string \| null | Primary street address line | | `street2` | string \| null | Secondary street address line | | `postalCode` | string \| null | Postal code | | `city` | string \| null | City | | `geographicArea` | string \| null | State or region | | `country` | string \| null | Country ISO 3166 code (e.g. `"US"`, `"FR"`) | **data.metadata** (SearchPagination): Pagination metadata wrapped under `data.metadata` in the envelope. | Field | Type | Description | | ------------ | -------------- | ---------------------------------------------------------------------- | | `perPage` | number | Companies requested per page | | `count` | number | Companies returned in this page. Use [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count) with `data.interpretation.filters` for the total matching set | | `nextCursor` | string \| null | Opaque cursor for the next page, `null` on the last page. Send it with the same `prompt` | **data.interpretation** (PromptInterpretation): What the prompt was read into, the same on every page of a prompt. | Field | Type | Description | | ---------- | -------- | --------------------------------------------------------------------------- | | `filters` | object | The Company search filters applied, keyed like the [Company search](https://docs.reversecontact.com/endpoints/search-companies) request body | | `warnings` | string[] | What the prompt asked for that the search does not apply, such as funding. Empty when everything was applied | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | -------------------------------- | | `creditsConsumed` | number | Credits consumed by this request: 1 for the search plus 1 per company delivered | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information (not to be confused with `data.metadata` which is pagination) | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/search/organizations/prompt \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "Fintech scale-ups headquartered in Berlin, 200 to 500 people, that raised a Series B", "perPage": 25 }' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const page = await client.search.organizationsByPrompt({ prompt: "Fintech scale-ups headquartered in Berlin, 200 to 500 people, that raised a Series B", perPage: 25 }); console.log("Filters applied:", page.interpretation?.filters); console.log("Not applied:", page.interpretation?.warnings); console.log(`Got ${page.data.length} companies, credits used: ${page.quotas?.creditsConsumed}`); // The same prompt with the next cursor, billed like the first page; null after the last page. const nextPage = await page.getNextPage(); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: page = client.search.organizations_by_prompt( prompt="Fintech scale-ups headquartered in Berlin, 200 to 500 people, that raised a Series B", per_page=25, ) if page.interpretation is not None: print("Filters applied:", page.interpretation.filters) print("Not applied:", page.interpretation.warnings) print(f"Got {len(page.data)} companies, next cursor: {page.next_cursor}") # The same prompt with the next cursor, billed like the first page; None after the last page. next_page = page.get_next_page() ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "data": [ { "id": "com_01jbd6k2q9x4r7t1v3w5y8z0ab", "companyName": "Ledgerline", "linkedinId": "34567890", "publicId": "ledgerline", "linkedinUrl": "https://www.social.com/company/ledgerline", "employeesCount": 318, "followersCount": 14250, "employeeCountRange": { "start": 201, "end": 500 }, "websiteUrl": "https://www.ledgerline.example", "tagline": "Payments infrastructure for European marketplaces", "description": "Ledgerline builds payment and payout infrastructure for marketplaces across Europe. Founded in 2018 in Berlin.", "industry": "Financial Services", "phone": null, "specialities": ["Payments", "Fintech", "Marketplaces"], "headquarter": { "street1": "Friedrichstraße 68", "street2": null, "postalCode": "10117", "city": "Berlin", "geographicArea": "Berlin", "country": "DE" }, "logoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "foundedOn": { "year": 2018 }, "locations": [ { "street1": "Friedrichstraße 68", "street2": null, "postalCode": "10117", "city": "Berlin", "state": "Berlin", "country": "DE" } ], "backgroundUrl": null } ], "metadata": { "perPage": 25, "count": 1, "nextCursor": "cur_01j9y2b7c4d8f0g3h6k9m2n5" }, "interpretation": { "filters": { "industry": "Financial Services", "employeeCountRange": { "min": 200, "max": 500 }, "location": { "countryCode": "DE", "city": "Berlin" } }, "warnings": ["Not available as filters yet: Series B."] } }, "error": null, "metadata": { "requestId": "3c221488-561c-4e76-aef9-73fe29a19be0", "executionTimeMs": 2690 }, "quotas": { "creditsConsumed": 2, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4524, "left": 5476 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2026-10-16T14:16:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2026-10-16T14:16:00.000Z" } } } } ``` --- # Person profile > Retrieve a person's professional profile from a Social URL, with work experience, education, skills and certifications. - Page: https://docs.reversecontact.com/endpoints/fetch-profile - Endpoint: `POST https://api.reversecontact.com/v2/fetch/persons` - Credits: 1 credit when the profile is found in the cache. A 404 is free. - Answer: synchronous. - Available in: REST API, MCP tool `enrich_person`, TypeScript and Python SDKs, CLI command `rc fetch person`. - Last verified: 2026-09-14 ## Introduction The Person Profile endpoint retrieves a professional profile from a Social URL. It returns structured data including work experience, education, skills, certifications, and more. This endpoint costs **1 credit** when the profile is found in the cache. A not-found response (`404`) is always free: no credits are consumed. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body Provide `url` or `id`. If you send both, `id` takes precedence. | Name | Type | Required | Description | | ----- | ------ | ----------- | ------------------------------------------------------------------------------------------------- | | `url` | string | Conditional | Public Social profile URL (e.g. `https://social.com/in/janedoe`). Required unless `id` is given. | | `id` | string | Conditional | Reverse Contact person id from a previous response (e.g. `prs_01jbd5vewyenebxgaz869ffe5t`). Required unless `url` is given. | ## Response structure | Field | Type | Description | | ---------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (Person): The enriched person profile (`null` on error responses) | Field | Type | Description | | ---------------------- | -------------- | ------------------------------------------------------ | | `id` | string | Reverse Contact person id (`prs_...`). Pass it back as the `id` request parameter to re-fetch this profile | | `publicId` | string | Social public identifier (the `/in/xxx` slug) | | `memberId` | string | Social internal member ID | | `linkedinUrl` | string | Full Social profile URL | | `firstName` | string \| null | First name | | `lastName` | string \| null | Last name | | `headline` | string \| null | Profile headline | | `summary` | string \| null | About / summary section | | `pronoun` | string \| null | Preferred pronoun (e.g. `"she/her"`, `"he/him"`) | | `isOpenToWork` | boolean | Whether the person is open to work | | `hasPremium` | boolean | Whether the person has Social Premium | | `hasVerificationBadge` | boolean | Whether the profile is verified | | `isInRemembrance` | boolean | Whether the profile is memorialized | | `photoUrl` | string \| null | Profile photo URL | | `backgroundUrl` | string \| null | Background / banner image URL | | `creationDate` | string \| null | Account creation date (ISO 8601) | | `followersCount` | number \| null | Number of followers | | `connectionsCount` | number \| null | Number of connections | | `skills` | string[] | List of skill names | **location** (PersonLocation): Geographic location | Field | Type | Description | | ------------- | -------------- | ---------------------------------- | | `city` | string \| null | City name | | `state` | string \| null | State or region | | `country` | string \| null | Country name | | `countryCode` | string \| null | ISO country code (e.g. `"US"`) | | `rawLocation` | string \| null | Raw, unparsed location label | > [!NOTE] > When a profile lists several jobs, the current position is selected (and the experience and education lists are ordered) by fixed rules. See [Current position and work history ordering](https://docs.reversecontact.com/guides/current-position-resolution). **currentPosition** (CurrentPosition): Current job position (`null` if no current position) | Field | Type | Description | | ------------------- | ----------------- | ---------------------------------------------------- | | `title` | string \| null | Job title | | `description` | string \| null | Role description | | `contractType` | string \| null | Contract type (e.g. `"Permanent"`, `"Internship"`) | | `companyId` | string \| null | Reverse Contact company id (`com_...`). Pass it as `id` to the [Company Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-company) | | `companyName` | string \| null | Company name | | `companyLinkedinId` | string \| null | Company Social internal identifier | | `companyUrl` | string \| null | Company Social profile URL | | `companyLocation` | string \| null | Company location | | `companyLogoUrl` | string \| null | Company logo URL | | `startEndDate` | DateRange \| null | Position duration | **startEndDate** (DateRange): Period boundaries | Field | Type | Description | | ------- | -------------- | -------------------------------------- | | `start` | string \| null | Start date (ISO 8601) | | `end` | string \| null | End date (ISO 8601, `null` if ongoing) | **experience** (PersonExperience[]): Professional experiences | Field | Type | Description | | ------------------- | ----------------- | ---------------------------------------------------- | | `title` | string \| null | Job title | | `description` | string \| null | Role description | | `contractType` | string \| null | Contract type (e.g. `"Permanent"`, `"Internship"`) | | `companyId` | string \| null | Reverse Contact company id (`com_...`). Pass it as `id` to the [Company Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-company) | | `companyName` | string \| null | Company name | | `companyLinkedinId` | string \| null | Company Social internal identifier | | `companyUrl` | string \| null | Company Social profile URL | | `companyLocation` | string \| null | Company location | | `companyLogoUrl` | string \| null | Company logo URL | | `startEndDate` | DateRange \| null | Position duration | **startEndDate** (DateRange): Period boundaries | Field | Type | Description | | ------- | -------------- | -------------------------------------- | | `start` | string \| null | Start date (ISO 8601) | | `end` | string \| null | End date (ISO 8601, `null` if ongoing) | **education** (PersonEducation[]): Education history | Field | Type | Description | | --------------- | ----------------- | ---------------------------- | | `degreeName` | string \| null | Degree name (e.g. `"B.S."`) | | `fieldOfStudy` | string \| null | Field of study | | `description` | string \| null | Additional description | | `grade` | string \| null | Reported grade | | `schoolName` | string \| null | School name | | `schoolUrl` | string \| null | School Social profile URL | | `schoolLogoUrl` | string \| null | School logo URL | | `startEndDate` | DateRange \| null | Education period | **startEndDate** (DateRange): Period boundaries | Field | Type | Description | | ------- | -------------- | -------------------------------------- | | `start` | string \| null | Start date (ISO 8601) | | `end` | string \| null | End date (ISO 8601, `null` if ongoing) | **languages** (PersonLanguage[]): Spoken languages | Field | Type | Description | | ------------- | -------------- | ------------------------------------------------------------- | | `language` | string \| null | Language name | | `proficiency` | string \| null | Proficiency level (e.g. `"Native"`, `"Professional working"`) | **certifications** (PersonCertification[]): Professional certifications | Field | Type | Description | | ------------------ | ---------------- | ------------------------------ | | `name` | string \| null | Certification name | | `organizationName` | string \| null | Issuing organization | | `organizationUrl` | string \| null | Organization Social profile URL| | `issuedDate` | string \| null | Issue date (ISO 8601) | **recommendations** (PersonRecommendation[]): Received recommendations | Field | Type | Description | | ---------------- | -------------- | ------------------------------------------ | | `caption` | string \| null | Short caption / role of the recommender | | `description` | string \| null | Recommendation text | | `authorFullname` | string \| null | Full name of the recommender | | `authorUrl` | string \| null | Recommender Social profile URL | **testScores** (PersonTestScore[]): Standardized test scores | Field | Type | Description | | ----------- | -------------- | ------------------------ | | `testTitle` | string \| null | Test name | | `score` | string \| null | Score value | | `date` | string \| null | Date taken (ISO 8601) | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ------------------ | ------ | ------------------------------ | | `creditsConsumed` | number | Credits consumed by this request | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | -------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits`| boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | ------------------------ | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | ----------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ------------------------------------------ | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ------- | ------ | -------------------- | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | ----------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ------------------------------------------ | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information | Field | Type | Description | | ---------------- | ------ | -------------------------------------------------------------------------------- | | `requestId` | string | Unique identifier for this request | | `executionTimeMs`| number | Total execution time in milliseconds | | `updatedAt` | string | Last upstream update timestamp (ISO 8601). Only present when data is from cache. | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/fetch/persons \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://social.com/in/janedoe"}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.fetch.person({ url: "https://social.com/in/janedoe" }); if (result === null) { console.log("Profile not found: nothing was charged"); } else { console.log(result.data.firstName, result.data.lastName); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.fetch.person(url="https://social.com/in/janedoe") if result is None: print("Profile not found: nothing was charged") elif result.data is not None: print(result.data.first_name, result.data.last_name) ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "id": "prs_01jbd5vewyenebxgaz869ffe5t", "publicId": "janedoe", "memberId": "ACoAAB12345", "linkedinUrl": "https://www.social.com/in/janedoe", "firstName": "Jane", "lastName": "Doe", "headline": "VP of Marketing at Acme Corp", "summary": "Experienced marketing leader with 15+ years driving growth for B2B SaaS companies. Passionate about data-driven strategies and building high-performing teams.", "pronoun": "she/her", "isOpenToWork": false, "hasPremium": true, "hasVerificationBadge": true, "isInRemembrance": false, "photoUrl": "https://media.licdn.com/dms/image/v2/D5603AQ.../profile-photo.jpg", "backgroundUrl": "https://media.licdn.com/dms/image/v2/D5616AQ.../background.jpg", "creationDate": "2009-03-15T00:00:00.000Z", "followersCount": 12480, "connectionsCount": 500, "location": { "city": "San Francisco", "state": "California", "country": "United States", "countryCode": "US", "rawLocation": "San Francisco Bay Area" }, "currentPosition": { "title": "VP of Marketing", "description": "Leading a team of 25 marketers across demand gen, product marketing, and brand. Drove 140% pipeline growth YoY.", "contractType": "Full-time", "companyId": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Acme Corp", "companyLinkedinId": "12345678", "companyUrl": "https://www.social.com/company/acme-corp", "companyLocation": "San Francisco, CA", "companyLogoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "startEndDate": { "start": "2021-06-01T00:00:00.000Z", "end": null } }, "experience": [ { "title": "VP of Marketing", "description": "Leading a team of 25 marketers across demand gen, product marketing, and brand. Drove 140% pipeline growth YoY.", "contractType": "Full-time", "companyId": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Acme Corp", "companyLinkedinId": "12345678", "companyUrl": "https://www.social.com/company/acme-corp", "companyLocation": "San Francisco, CA", "companyLogoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "startEndDate": { "start": "2021-06-01T00:00:00.000Z", "end": null } }, { "title": "Director of Marketing", "description": "Built the marketing function from scratch. Scaled from 0 to $5M ARR.", "contractType": "Full-time", "companyId": "com_01jbd7m3n8p2q5r9s4t6v1w0xy", "companyName": "Startup Inc", "companyLinkedinId": "98765432", "companyUrl": "https://www.social.com/company/startup-inc", "companyLocation": "Remote", "companyLogoUrl": "https://media.licdn.com/dms/image/v2/C560BAQ.../logo.jpg", "startEndDate": { "start": "2018-01-01T00:00:00.000Z", "end": "2021-05-31T00:00:00.000Z" } } ], "education": [ { "degreeName": "MBA", "fieldOfStudy": "Marketing", "description": null, "grade": null, "schoolName": "Stanford Graduate School of Business", "schoolUrl": "https://www.social.com/school/stanford-gsb", "schoolLogoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "startEndDate": { "start": "2012-09-01T00:00:00.000Z", "end": "2014-06-15T00:00:00.000Z" } } ], "skills": [ "Marketing Strategy", "B2B SaaS", "Demand Generation", "Product Marketing", "Team Leadership", "Data Analytics", "Content Marketing", "SEO", "Account-Based Marketing" ], "languages": [ { "language": "English", "proficiency": "Native or bilingual" }, { "language": "French", "proficiency": "Professional working" } ], "certifications": [ { "name": "Google Analytics Certified", "organizationName": "Google", "organizationUrl": "https://www.social.com/company/google", "issuedDate": "2023-02-01T00:00:00.000Z" } ], "recommendations": [ { "caption": "CEO at Startup Inc", "description": "Jane is one of the most strategic marketers I've worked with. She has an incredible ability to turn data into actionable growth plans.", "authorFullname": "John Smith", "authorUrl": "https://www.social.com/in/johnsmith" } ], "testScores": [] }, "error": null, "metadata": { "requestId": "40001c4e-61b3-4025-a5fa-f1b4542d7c47", "executionTimeMs": 2340, "updatedAt": "2025-01-15T09:30:00.000Z" }, "quotas": { "creditsConsumed": 1, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 12, "left": 48, "nextReset": "2025-01-15T09:31:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 12, "left": 48, "nextReset": "2025-01-15T09:31:00.000Z" } } } } ``` --- # Person profile live > Fetch a fresh Social person profile from a URL or person id, then retrieve the asynchronous result through polling or a webhook callback. - Page: https://docs.reversecontact.com/endpoints/fetch-profile-live - Endpoint: `POST https://api.reversecontact.com/v2/fetch/persons/live` - Credits: 2 credits per request, consumed when the job is created. - Answer: asynchronous. The request returns a job (`data.webhookId`): poll `GET /v2/webhooks/{webhookId}?wait=20` until `status` is `succeeded` or `errored`, or receive the result on a webhook. Each poll answers as soon as the job ends, or after at most 20 seconds with the job still running; while it runs, start two polls at least `Retry-After` seconds apart (2 if the header is missing). - Available in: REST API, MCP tool `enrich_person_live`, TypeScript and Python SDKs, CLI command `rc fetch person --live`. - Last verified: 2026-09-14 ## Introduction The Person Profile Live endpoint triggers a real-time fetch of a Social profile. Instead of relying on cached data, it pulls a fresh copy and returns the result asynchronously. Use this endpoint when you need the most up-to-date profile data and can wait for the async result. Retrieve the result via [polling](https://docs.reversecontact.com/guides/polling) (recommended, no public endpoint required) or via a [webhook callback](https://docs.reversecontact.com/guides/webhooks) (advanced, push delivery). This endpoint costs **2 credits** per request, consumed when the job is created. When a job fails, its `errorCode` says whether the credits are kept or refunded: see [Webhook callback errors](#webhook-callback-errors-advanced). > [!NOTE] > This endpoint is asynchronous. We aim to deliver each result in under 15 seconds, but it can take tens of minutes depending on conditions. It is built to update databases and feed async pipelines, not to back a synchronous request a user is waiting on. See [Polling](https://docs.reversecontact.com/guides/polling) to retrieve the result. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body | Name | Type | Required | Description | | ------------ | ------ | ----------- | --------------------------------------------------------------------------------------------------- | | `url` | string | Conditional | Public Social profile URL (e.g. `https://social.com/in/janedoe`). Provide exactly one of `url` or `id`. | | `id` | string | Conditional | Reverse Contact person id (`prs_…`). The gateway resolves it to the current Social public slug before starting the scrape. | | `webhookUrl` | string | Optional | HTTPS push URL for webhook delivery. Omit this field to retrieve the result via [polling](https://docs.reversecontact.com/guides/polling) (recommended). See the [Webhooks guide](https://docs.reversecontact.com/guides/webhooks) when you need push delivery. | ## Response structure This endpoint replies immediately with a `webhookId`. You then retrieve the result in one of two ways: 1. Polling (recommended): call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. With `?wait=20`, each call waits up to 20 seconds and answers as soon as the result is ready. You need no webhook URL. See the [Polling guide](https://docs.reversecontact.com/guides/polling). 2. Webhook callback (advanced): if you provided a `webhookUrl` (or a workspace default is configured), the gateway POSTs the person data to that URL once the fetch completes. See [Webhook callback payload (advanced)](#webhook-callback-payload-advanced) below. | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the job was created successfully | **data** (JobAck): Initial job acknowledgement. Save the `webhookId` to retrieve the result via polling (`GET /v2/webhooks/:webhookId`) or to correlate an upcoming webhook callback (`null` on error responses). | Field | Type | Description | | ----------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- | | `status` | string | Job status, always `"created"` for the initial response | | `webhookId` | string | Unique identifier (UUID). Save it to poll the result at `GET /v2/webhooks/:webhookId` or to correlate an incoming webhook callback. | | `pollUrl` | string | Path of the polling endpoint for this job (e.g. `/v2/webhooks/{webhookId}`). | | Field | Type | Description | | ------- | ------ | ------------------------ | | `error` | `null` | Always `null` on success | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | -------------------------------- | | `creditsConsumed` | number | Credits consumed by this request | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Webhook callback payload (advanced) Once the live fetch completes, the gateway POSTs a JSON payload to your `webhookUrl`. Use the `webhookId` from the initial response to correlate the callback with your original request. The payload has no envelope: unlike the synchronous response, it has no `success`, `error`, `metadata` or `quotas` keys at the top level. | Field | Type | Description | | ----------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- | | `webhookId` | string | Always present. The same identifier returned in the initial response. Use it to correlate the callback with the original request. | **data** (Profile): Always present. On success: the enriched person profile (fields below). On failure: see [Webhook callback errors (advanced)](#webhook-callback-errors-advanced). | Field | Type | Description | | ---------------------- | -------------- | ------------------------------------------------------ | | `id` | string | Reverse Contact person id (`prs_...`). Use it as the `id` parameter of the [Person Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-profile) to re-fetch this profile | | `publicId` | string | Social public identifier (the `/in/xxx` slug) | | `memberId` | string | Social internal member ID | | `linkedinUrl` | string | Full Social profile URL | | `firstName` | string \| null | First name | | `lastName` | string \| null | Last name | | `headline` | string \| null | Profile headline | | `summary` | string \| null | About / summary section | | `pronoun` | string \| null | Preferred pronoun (e.g. `"she/her"`, `"he/him"`) | | `isOpenToWork` | boolean | Whether the person is open to work | | `hasPremium` | boolean | Whether the person has Social Premium | | `hasVerificationBadge` | boolean | Whether the profile is verified | | `isInRemembrance` | boolean | Whether the profile is memorialized | | `photoUrl` | string \| null | Profile photo URL | | `backgroundUrl` | string \| null | Background / banner image URL | | `creationDate` | string \| null | Account creation date (ISO 8601) | | `followersCount` | number \| null | Number of followers | | `connectionsCount` | number \| null | Number of connections | | `skills` | string[] | List of skill names | **location** (ProfileLocation): Geographic location | Field | Type | Description | | ------------- | -------------- | ------------------------------ | | `city` | string \| null | City name | | `state` | string \| null | State or region | | `country` | string \| null | Country name | | `countryCode` | string \| null | ISO country code (e.g. `"US"`) | | `rawLocation` | string \| null | Raw, unparsed location label | > [!NOTE] > When a profile lists several jobs, the current position is selected (and the experience and education lists are ordered) by fixed rules. See [Current position and work history ordering](https://docs.reversecontact.com/guides/current-position-resolution). **currentPosition** (ProfilePosition): Current job position | Field | Type | Description | | ------------------- | ----------------- | ------------------------------------------------ | | `title` | string \| null | Job title | | `description` | string \| null | Role description | | `contractType` | string \| null | Contract type (e.g. `"Permanent"`, `"Contract"`) | | `companyId` | string \| null | Reverse Contact company id (`com_...`). Pass it as `id` to the [Company Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-company) | | `companyName` | string \| null | Company name | | `companyLinkedinId` | string \| null | Company Social identifier | | `companyUrl` | string \| null | Company Social profile URL | | `companyLocation` | string \| null | Company location (city, region, country) | | `companyLogoUrl` | string \| null | Company logo URL | | `startEndDate` | DateRange \| null | Position duration | **startEndDate** (DateRange): Period boundaries | Field | Type | Description | | ------- | -------------- | -------------------------------------- | | `start` | string \| null | Start date (ISO 8601) | | `end` | string \| null | End date (ISO 8601, `null` if ongoing) | **experience** (ProfileExperience[]): Professional experiences | Field | Type | Description | | ------------------- | ----------------- | ------------------------------------------------ | | `title` | string \| null | Job title | | `description` | string \| null | Role description | | `contractType` | string \| null | Contract type (e.g. `"Permanent"`, `"Contract"`) | | `companyId` | string \| null | Reverse Contact company id (`com_...`). Pass it as `id` to the [Company Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-company) | | `companyName` | string \| null | Company name | | `companyLinkedinId` | string \| null | Company Social identifier | | `companyUrl` | string \| null | Company Social profile URL | | `companyLocation` | string \| null | Company location (city, region, country) | | `companyLogoUrl` | string \| null | Company logo URL | | `startEndDate` | DateRange \| null | Position duration | **startEndDate** (DateRange): Period boundaries | Field | Type | Description | | ------- | -------------- | -------------------------------------- | | `start` | string \| null | Start date (ISO 8601) | | `end` | string \| null | End date (ISO 8601, `null` if ongoing) | **education** (ProfileEducation[]): Education history | Field | Type | Description | | --------------- | ----------------- | ------------------------------------------ | | `degreeName` | string \| null | Degree name (e.g. `"B.S."`, `"MBA"`) | | `fieldOfStudy` | string \| null | Field of study | | `description` | string \| null | Additional description | | `grade` | string \| null | Grade or honors | | `schoolName` | string \| null | School name | | `schoolUrl` | string \| null | School Social profile URL | | `schoolLogoUrl` | string \| null | School logo URL | | `startEndDate` | DateRange \| null | Education period | **startEndDate** (DateRange): Period boundaries | Field | Type | Description | | ------- | -------------- | -------------------------------------- | | `start` | string \| null | Start date (ISO 8601) | | `end` | string \| null | End date (ISO 8601, `null` if ongoing) | **languages** (ProfileLanguage[]): Spoken languages | Field | Type | Description | | ------------- | -------------- | ---------------------------------------------------------------------- | | `language` | string \| null | Language name | | `proficiency` | string \| null | Proficiency level (e.g. `"Native or bilingual"`, `"Full professional"`) | **recommendations** (ProfileRecommendation[]): Received recommendations | Field | Type | Description | | ---------------- | -------------- | --------------------------------------- | | `caption` | string \| null | Short caption / role of the recommender | | `description` | string \| null | Recommendation text | | `authorFullname` | string \| null | Full name of the recommender | | `authorUrl` | string \| null | Recommender Social profile URL | **certifications** (ProfileCertification[]): Professional certifications | Field | Type | Description | | ------------------ | -------------- | ------------------------------------------------- | | `name` | string \| null | Certification name | | `organizationName` | string \| null | Issuing organization name | | `organizationUrl` | string \| null | Issuing organization profile URL | | `issuedDate` | string \| null | Issue date (ISO 8601, may be `null` if unknown) | **testScores** (ProfileTestScore[]): Standardized test scores | Field | Type | Description | | ----------- | -------------- | --------------------- | | `testTitle` | string \| null | Test name | | `score` | string \| null | Score value | | `date` | string \| null | Date taken (ISO 8601) | ## Webhook callback errors (advanced) When the live fetch fails, `data` has no profile fields. It echoes the original input back, with an `errorCode` that identifies the failure: **Error payload** ```json [Webhook callback (error)] { "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "data": { "url": "https://www.social.com/in/janedoe", "errorCode": "data-not-found" } } ``` `data.errorCode` takes one of these values: | Code | Billed | Description | | ------------------ | ------ | ------------------------------------------------------------------- | | `person-not-found` | Yes | The person profile doesn't exist or could not be scraped | | `invalid-data` | No | The fetched data failed quality validation | | `fetch-data-error` | No | The API failed while fetching data. Safe to retry | | `result-unavailable` | No | The enrichment completed but the result was lost on our side. Retry the job. If the request was charged, contact support to get the credit back. | | `data-subject-blocked` | No | This person asked not to be processed under GDPR or CCPA. Do not retry. | > [!NOTE] > In the Billed column, Yes means the credits consumed at request time are kept, and No means they are fully refunded to your workspace balance. ## Code examples **Code Examples** ```typescript [TypeScript SDK - Polling (recommended)] import ReverseContact, { JobFailedError } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.fetch.personLive({ url: "https://social.com/in/janedoe" }); console.log("Save this job id:", job.id); // resume after a restart with client.jobs.wait(id) try { // Long polls until the job ends, 30 minutes at most by default. const done = await job.wait(); console.log(done.data.result?.firstName, done.data.result?.lastName); } catch (error) { if (error instanceof JobFailedError) console.log("Job failed:", error.errorCode, "credits:", error.creditsConsumed); else throw error; } ``` ```typescript [TypeScript SDK - Webhook] import ReverseContact, { parseWebhookDelivery } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.fetch.personLive({ url: "https://social.com/in/janedoe", webhookUrl: "https://your-app.com/webhooks/reversecontact" }); console.log(job.status, job.id); // The route behind webhookUrl receives { webhookId, data }: answer within 10 seconds. async function handleWebhook(request: Request): Promise { const delivery = parseWebhookDelivery(await request.json()); console.log(delivery.webhookId, delivery.isError ? "failed" : "succeeded", delivery.data); return new Response(null, { status: 200 }); } ``` ```bash [cURL - Polling (recommended)] KEY="YOUR_API_KEY" ID=$(curl -sX POST https://api.reversecontact.com/v2/fetch/persons/live \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"url":"https://social.com/in/janedoe"}' | jq -r .data.webhookId) while :; do S=$(curl -s https://api.reversecontact.com/v2/webhooks/$ID \ -H "Authorization: Bearer $KEY" | jq -r .data.status) [ "$S" = "succeeded" ] && break [ "$S" = "errored" ] && echo "job failed" && exit 1 sleep 2 done curl -s https://api.reversecontact.com/v2/webhooks/$ID \ -H "Authorization: Bearer $KEY" | jq .data.result ``` ```bash [cURL - Webhook] curl -X POST https://api.reversecontact.com/v2/fetch/persons/live \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://social.com/in/janedoe", "webhookUrl": "https://your-app.com/webhooks/reversecontact" }' ``` ```python [Python SDK - Polling (recommended)] import os from reversecontact import JobFailedError, ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.fetch.person_live(url="https://social.com/in/janedoe") print("Save this job id:", job.id) # resume after a restart with client.jobs.wait(id) try: # Long polls until the job ends, 30 minutes at most by default. done = job.wait() person = done.data.result if done.data else None if person is not None: print(person.first_name, person.last_name) except JobFailedError as error: print("Job failed:", error.error_code) ``` ```python [Python SDK - Webhook] import os from reversecontact import ReverseContact, parse_webhook_delivery with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.fetch.person_live( url="https://social.com/in/janedoe", webhook_url="https://your-app.com/webhooks/reversecontact", ) print(job.status, job.id) # The route behind webhook_url receives {webhookId, data}: answer within 10 seconds. def handle_webhook(body: bytes) -> None: delivery = parse_webhook_delivery(body) print(delivery.webhook_id, "failed" if delivery.is_error else "succeeded", delivery.data) ``` ## Example response **Example Response** ```json [Initial response] { "success": true, "data": { "status": "created", "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "pollUrl": "/v2/webhooks/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" }, "error": null, "metadata": { "requestId": "40001c4e-61b3-4025-a5fa-f1b4542d7c47", "executionTimeMs": 120 }, "quotas": { "creditsConsumed": 2, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4523, "left": 5477 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 12, "left": 48, "nextReset": "2025-01-15T09:31:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 12, "left": 48, "nextReset": "2025-01-15T09:31:00.000Z" } } } } ``` ```json [Webhook callback (success)] { "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "data": { "id": "prs_01jbd5vewyenebxgaz869ffe5t", "publicId": "janedoe", "memberId": "286401114", "linkedinUrl": "https://www.social.com/in/janedoe", "firstName": "Jane", "lastName": "Doe", "headline": "VP of Engineering @ Acme Corp", "summary": "Backend leader specialized in TypeScript, DDD and Clean Architecture...", "pronoun": null, "isOpenToWork": false, "hasPremium": false, "hasVerificationBadge": false, "isInRemembrance": false, "photoUrl": "https://media.licdn.com/dms/image/.../profile.jpg", "backgroundUrl": "https://media.licdn.com/dms/image/.../background.jpg", "creationDate": "2013-09-01T12:00:00.000Z", "followersCount": 666, "connectionsCount": 661, "skills": [ "TypeScript", "Domain-Driven Design (DDD)", "Node.js", "PostgreSQL" ], "location": { "city": "Paris", "state": "Île-de-France", "country": "France", "countryCode": "FR", "rawLocation": "Paris, Île-de-France, France" }, "currentPosition": { "title": "VP of Engineering", "description": "- Leading engineering during a growth phase...", "contractType": null, "companyId": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Acme Corp", "companyLinkedinId": "89181966", "companyUrl": "https://www.social.com/company/acme-corp", "companyLocation": "Paris", "companyLogoUrl": "https://media.licdn.com/dms/image/.../logo.jpg", "startEndDate": { "start": "2026-01-01T00:00:00.000Z", "end": null } }, "experience": [ { "title": "VP of Engineering", "description": "- Leading engineering during a growth phase...", "contractType": "Permanent", "companyId": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Acme Corp", "companyLinkedinId": "89181966", "companyUrl": "https://www.social.com/company/acme-corp", "companyLocation": "Paris", "companyLogoUrl": "https://media.licdn.com/dms/image/.../logo.jpg", "startEndDate": { "start": "2026-01-01T00:00:00.000Z", "end": null } } ], "education": [ { "degreeName": "Engineer's Degree in Computer Engineering", "fieldOfStudy": null, "description": "Generalist engineering degree focused on IT systems...", "grade": null, "schoolName": "ESILV", "schoolUrl": "https://www.social.com/school/esilvparis", "schoolLogoUrl": "https://media.licdn.com/dms/image/.../logo.jpg", "startEndDate": { "start": "2016-01-01T00:00:00.000Z", "end": "2019-12-31T00:00:00.000Z" } } ], "languages": [ { "language": "English", "proficiency": "Full professional proficiency" }, { "language": "Chinese", "proficiency": "Native or bilingual proficiency" } ], "recommendations": [ { "authorFullname": "Karim Baali", "authorUrl": "https://www.social.com/in/karimbaali", "caption": "Co-founder @Fragments Studio", "description": "I had the opportunity to work with Jane, both at school and at work..." } ], "certifications": [ { "name": "Test of English for International Communication", "organizationName": "ETS", "organizationUrl": "https://www.social.com/company/163705", "issuedDate": null } ], "testScores": [] } } ``` ```json [Webhook callback (error)] { "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "data": { "url": "https://www.social.com/in/janedoe", "errorCode": "data-not-found" } } ``` --- # Person profile status > Check if a person profile exists in the database, and when it was last updated, before requesting enrichment. The check costs 0 credits. - Page: https://docs.reversecontact.com/endpoints/check-person - Endpoint: `POST https://api.reversecontact.com/v2/fetch/persons/check` - Credits: Free. Needs at least 2 credits in the workspace: below that, 402 NO_CREDITS. - Answer: synchronous. - Available in: REST API, MCP tool `check_person`, TypeScript and Python SDKs, CLI command `rc check person`. - Last verified: 2026-10-15 ## Introduction The Person Profile Status endpoint verifies whether a person profile exists in our database and returns freshness information. Use it to check data availability before spending credits on a full fetch or a live pull. ## When to use A typical cost-optimized workflow looks like this: 1. Call Profile Status to get the existence status, the last update date and the content counts (experience, skills, education). 2. If the profile exists and is recent, use [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) (cached, 1 credit) to retrieve the full enriched data. 3. If the profile is stale or missing, use [Person profile live](https://docs.reversecontact.com/endpoints/fetch-profile-live) (fresh from Social, 2 credits) for an up-to-date result. > [!NOTE] > Profile Status is free, but it needs at least 2 credits in your workspace. Below that, it returns `402 NO_CREDITS` until you top up: the fetch or live pull that follows can cost 2 credits and could not run anyway. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body Provide either `url` or `id`. If both are supplied, `id` takes precedence. | Name | Type | Required | Description | | ----- | ------ | ----------- | ----------------------------------------------------------------------------------------------------------------------- | | `url` | string | Conditional | Public Social profile URL (e.g. `https://social.com/in/janedoe`). Required unless `id` is given. | | `id` | string | Conditional | Reverse Contact person id from a previous response (e.g. `prs_01jbd5vewyenebxgaz869ffe5t`). Required unless `url` is given. | ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (CheckData): Availability and freshness information (`null` on error responses) | Field | Type | Description | | ----------------- | -------------- | -------------------------------------------------------------------------------------------------------- | | `exists` | boolean | `true` if the profile exists in the database | | `lastUpdate` | string \| null | When Reverse Contact last fetched this profile from the source (ISO 8601). `null` when `exists` is `false` | | `experienceCount` | number | Number of experience entries. Only present when `exists` is `true` | | `skillCount` | number | Number of skills. Only present when `exists` is `true` | | `schoolCount` | number | Number of education entries. Only present when `exists` is `true` | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | ---------------------------------------------------------------------------------------------- | | `creditsConsumed` | number | Credits consumed by this request. Always `0` for Profile Status | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/fetch/persons/check \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://social.com/in/janedoe"}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.fetch.checkPerson({ url: "https://social.com/in/janedoe" }); if (result.data.exists) { console.log(`Profile exists, last updated ${result.data.lastUpdate}`); } else { console.log("Profile not in database yet"); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.fetch.check_person(url="https://social.com/in/janedoe") if result.data is not None and result.data.exists: print(f"Profile exists, last updated {result.data.last_update}") else: print("Profile not in database yet") ``` ## Example response **Example Response** ```json [200 - Found] { "success": true, "data": { "exists": true, "lastUpdate": "2025-01-15T09:30:00.000Z", "experienceCount": 5, "skillCount": 12, "schoolCount": 2 }, "error": null, "metadata": { "requestId": "4e740daa-976d-43b1-bf01-16425e0aa278", "executionTimeMs": 120 }, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 12, "left": 48, "nextReset": "2025-01-15T09:31:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 12, "left": 48, "nextReset": "2025-01-15T09:31:00.000Z" } } } } ``` ```json [200 - Not Found] { "success": true, "data": { "exists": false, "lastUpdate": null }, "error": null, "metadata": { "requestId": "f618efad-67bb-46af-a216-165bd0cbbcda", "executionTimeMs": 85 }, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 13, "left": 47, "nextReset": "2025-01-15T09:31:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 13, "left": 47, "nextReset": "2025-01-15T09:31:00.000Z" } } } } ``` --- # Company profile > Retrieve a company profile from a Social company URL, with industry, employee count, headquarters, office locations and specialties. - Page: https://docs.reversecontact.com/endpoints/fetch-company - Endpoint: `POST https://api.reversecontact.com/v2/fetch/companies` - Credits: 1 credit when the company is found in the cache. A 404 is free. - Answer: synchronous. - Available in: REST API, MCP tool `enrich_company`, TypeScript and Python SDKs, CLI command `rc fetch company`. - Last verified: 2026-11-03 ## Introduction The Company Profile endpoint retrieves a company profile from a Social company URL. It returns structured data including the industry, employee count, headquarters, specialties, and more. This endpoint costs **1 credit** when the company is found in the cache. A not-found response (`404`) is always free: no credits are consumed. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body Provide `url` or `id`. If you send both, `id` takes precedence. | Name | Type | Required | Description | | ----- | ------ | ----------- | ------------------------------------------------------------------------------------------------------ | | `url` | string | Conditional | Public Social company URL (e.g. `https://social.com/company/acme-corp`). Required unless `id` is given. | | `id` | string | Conditional | Reverse Contact company id from a previous response (e.g. `com_01jbd5vewyenebxgaz869ffe5t`). Required unless `url` is given. | ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (Company): The enriched company profile (`null` on error responses) | Field | Type | Description | | -------------------- | ---------------------------- | --------------------------------------------------------- | | `id` | string | Reverse Contact company id (`com_...`). Pass it back as the `id` request parameter to re-fetch this company | | `companyName` | string | Company name | | `linkedinId` | string | Social company identifier | | `publicId` | string | Universal name / slug (the `/company/xxx` part of the URL) | | `linkedinUrl` | string \| null | Full Social company URL | | `websiteUrl` | string \| null | Official company website URL | | `industry` | string \| null | Industry (e.g. `"Software Development"`) | | `description` | string \| null | Full company description | | `tagline` | string \| null | Tagline / slogan | | `logoUrl` | string \| null | Logo image URL | | `backgroundUrl` | string \| null | Background / banner image URL | | `employeesCount` | number \| null | Exact number of employees | | `employeeCountRange` | EmployeeCountRange \| null | Employee count range bracket | | `followersCount` | number \| null | Number of Social followers | | `specialities` | string[] | List of company specialties | | `locations` | Location[] | All office locations registered on the profile (can be empty) | | `foundedOn` | FoundedOn \| null | Founding year wrapper | | `phone` | string \| null | Phone number | **headquarter** (CompanyHeadquarter): Headquarter location (`null` if unknown) | Field | Type | Description | | ---------------- | -------------- | ---------------------------- | | `street1` | string \| null | Primary street address line | | `street2` | string \| null | Secondary street address line | | `city` | string \| null | City | | `country` | string \| null | Country (ISO code or name) | | `geographicArea` | string \| null | State or region | | `postalCode` | string \| null | Postal code | **locations** (Location[]): All office locations registered on the profile (can be empty) | Field | Type | Description | | ------------ | -------------- | ------------------------------------------------------------------------------ | | `street1` | string \| null | Street address line 1 | | `street2` | string \| null | Street address line 2 | | `postalCode` | string \| null | Postal / ZIP code | | `city` | string \| null | City | | `state` | string \| null | State or region (named `state` here, not `geographicArea` as in `headquarter`) | | `country` | string \| null | Country ISO 3166 code (e.g. `"US"`, `"FR"`) | **employeeCountRange** (EmployeeCountRange): Employee count bracket (`null` if unknown). Buckets mirror the source's native `staffCountRange`: `1`, `2–10`, `11–50`, `51–200`, `201–500`, `501–1,000`, `1,001–5,000`, `5,001–10,000`, `10,001+`. For the top bucket (`10,001+`), the source has no upper bound and returns `end: 1` as a sentinel: interpret any range where `end < start` as `start+` (unbounded). Use `employeesCount` for the exact headcount when available. | Field | Type | Description | | ------- | ------ | ---------------------------------------------------------------------------------------- | | `start` | number | Lower bound of the range | | `end` | number | Upper bound of the range. Ignore it for the top bucket, where `start` is `10001` | > Example for the 10,001+ bucket: `{ "start": 10001, "end": 10001 }`, or `{ "start": 10001, "end": 1 }` on some companies. In both cases the company has 10,001 or more employees, with no upper bound. **foundedOn** (FoundedOn): Founding year (`null` if unknown) | Field | Type | Description | | ------ | ------ | -------------------- | | `year` | number | Four-digit year | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | -------------------------------- | | `creditsConsumed` | number | Credits consumed by this request | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information | Field | Type | Description | | ----------------- | ------ | -------------------------------------------------------------------------------- | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | | `updatedAt` | string | Last upstream update timestamp (ISO 8601). Only present when data is from cache. | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/fetch/companies \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://social.com/company/acme-corp"}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.fetch.company({ url: "https://social.com/company/acme-corp" }); if (result === null) { console.log("Company not found: nothing was charged"); } else { console.log(result.data.companyName, result.data.industry); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.fetch.company(url="https://social.com/company/acme-corp") if result is None: print("Company not found: nothing was charged") elif result.data is not None: print(result.data.company_name, result.data.industry) ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "id": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Acme Corp", "linkedinId": "12345678", "publicId": "acme-corp", "linkedinUrl": "https://www.social.com/company/acme-corp", "websiteUrl": "https://www.acmecorp.com", "industry": "Software Development", "description": "Acme Corp is a leading provider of innovative SaaS solutions for enterprise teams. Founded in 2015, we serve over 10,000 customers worldwide.", "tagline": "Building the future of work", "logoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../logo.jpg", "backgroundUrl": "https://media.licdn.com/dms/image/v2/D5616AQ.../background.jpg", "employeesCount": 450, "employeeCountRange": { "start": 201, "end": 500 }, "followersCount": 28500, "headquarter": { "street1": "1 Market Street", "street2": "Suite 400", "city": "San Francisco", "country": "US", "geographicArea": "California", "postalCode": "94105" }, "specialities": [ "SaaS", "Enterprise Software", "Cloud Computing", "Data Analytics", "API Integration" ], "locations": [ { "street1": "1 Market Street", "street2": "Suite 400", "postalCode": "94105", "city": "San Francisco", "state": "California", "country": "US" }, { "street1": "10 Queen Street", "street2": null, "postalCode": "EC4N 1TX", "city": "London", "state": null, "country": "GB" } ], "foundedOn": { "year": 2015 }, "phone": "+1 (415) 555-0123" }, "error": null, "metadata": { "requestId": "5168be92-42f6-4cb0-a1e3-a672a90926c7", "executionTimeMs": 1850, "updatedAt": "2025-01-20T14:15:00.000Z" }, "quotas": { "creditsConsumed": 1, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2025-01-20T14:16:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2025-01-20T14:16:00.000Z" } } } } ``` --- # Company profile live > Fetch a fresh Social company profile from a URL or company id, then retrieve the asynchronous result through polling or a webhook callback. - Page: https://docs.reversecontact.com/endpoints/fetch-company-live - Endpoint: `POST https://api.reversecontact.com/v2/fetch/companies/live` - Credits: 2 credits per request, consumed when the job is created. - Answer: asynchronous. The request returns a job (`data.webhookId`): poll `GET /v2/webhooks/{webhookId}?wait=20` until `status` is `succeeded` or `errored`, or receive the result on a webhook. Each poll answers as soon as the job ends, or after at most 20 seconds with the job still running; while it runs, start two polls at least `Retry-After` seconds apart (2 if the header is missing). - Available in: REST API, MCP tool `enrich_company_live`, TypeScript and Python SDKs, CLI command `rc fetch company --live`. - Last verified: 2026-11-03 ## Introduction The Company Profile Live endpoint triggers a real-time fetch of a Social company profile. Instead of relying on cached data, it pulls a fresh copy and returns the result asynchronously. Use this endpoint when you need the most up-to-date company data and can wait for the async result. Retrieve the result via [polling](https://docs.reversecontact.com/guides/polling) (recommended, no public endpoint required) or via a [webhook callback](https://docs.reversecontact.com/guides/webhooks) (advanced, push delivery). This endpoint costs **2 credits** per request, consumed when the job is created. When a job fails, its `errorCode` says whether the credits are kept or refunded: see [Webhook callback errors](#webhook-callback-errors-advanced). > [!NOTE] > This endpoint is asynchronous. We aim to deliver each result in under 15 seconds, but it can take tens of minutes depending on conditions. It is built to update databases and feed async pipelines, not to back a synchronous request a user is waiting on. See [Polling](https://docs.reversecontact.com/guides/polling) to retrieve the result. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body | Name | Type | Required | Description | | ------------ | ------ | ----------- | --------------------------------------------------------------------------------------------------- | | `url` | string | Conditional | Public Social company URL (e.g. `https://social.com/company/acme-corp`). Provide exactly one of `url` or `id`. | | `id` | string | Conditional | Reverse Contact company id (`com_…`). The gateway resolves it to the current Social public slug before starting the scrape. | | `webhookUrl` | string | Optional | HTTPS push URL for webhook delivery. Omit this field to retrieve the result via [polling](https://docs.reversecontact.com/guides/polling) (recommended). See the [Webhooks guide](https://docs.reversecontact.com/guides/webhooks) when you need push delivery. | ## Response structure This endpoint replies immediately with a `webhookId`. You then retrieve the result in one of two ways: 1. Polling (recommended): call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. With `?wait=20`, each call waits up to 20 seconds and answers as soon as the result is ready. You need no webhook URL. See the [Polling guide](https://docs.reversecontact.com/guides/polling). 2. Webhook callback (advanced): if you provided a `webhookUrl` (or a workspace default is configured), the gateway POSTs the company data to that URL once the fetch completes. See [Webhook callback payload (advanced)](#webhook-callback-payload-advanced) below. | Field | Type | Description | | --------- | ------- | ------------------------------------------ | | `success` | boolean | `true` if the job was created successfully | **data** (JobAck): Initial job acknowledgement. Save the `webhookId` to retrieve the result via polling (`GET /v2/webhooks/:webhookId`) or to correlate an upcoming webhook callback (`null` on error responses). | Field | Type | Description | | ----------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- | | `status` | string | Job status, always `"created"` for the initial response | | `webhookId` | string | Unique identifier (UUID). Save it to poll the result at `GET /v2/webhooks/:webhookId` or to correlate an incoming webhook callback. | | `pollUrl` | string | Path of the polling endpoint for this job (e.g. `/v2/webhooks/{webhookId}`). | | Field | Type | Description | | ------- | ------ | ------------------------ | | `error` | `null` | Always `null` on success | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | -------------------------------- | | `creditsConsumed` | number | Credits consumed by this request | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Webhook callback payload (advanced) Once the live fetch completes, the gateway POSTs a JSON payload to your `webhookUrl`. Use the `webhookId` from the initial response to correlate the callback with your original request. The payload has no envelope: unlike the synchronous response, it has no `success`, `error`, `metadata` or `quotas` keys at the top level. | Field | Type | Description | | ----------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- | | `webhookId` | string | Always present. The same identifier returned in the initial response. Use it to correlate the callback with the original request. | **data** (Company): Always present. On success: the enriched company profile (fields below). On failure: see [Webhook callback errors (advanced)](#webhook-callback-errors-advanced). | Field | Type | Description | | -------------------- | -------------------------- | ---------------------------------------------------------- | | `id` | string | Reverse Contact company id (`com_...`). Use it as the `id` parameter of the [Company Profile endpoint](https://docs.reversecontact.com/endpoints/fetch-company) to re-fetch this company | | `companyName` | string | Company name | | `linkedinId` | string | Social company identifier | | `publicId` | string | Universal name / slug (the `/company/xxx` part of the URL) | | `linkedinUrl` | string \| null | Full Social company URL | | `websiteUrl` | string \| null | Official company website URL | | `industry` | string \| null | Industry (e.g. `"Software Development"`) | | `description` | string \| null | Full company description | | `tagline` | string \| null | Tagline / slogan | | `logoUrl` | string \| null | Logo image URL | | `backgroundUrl` | string \| null | Background / banner image URL | | `employeesCount` | number \| null | Exact number of employees | | `employeeCountRange` | EmployeeCountRange \| null | Employee count range bracket | | `followersCount` | number \| null | Number of Social followers | | `specialities` | string[] | List of company specialties. Empty array if none | | `foundedOn` | FoundedOn \| null | Founding year wrapper | | `phone` | string \| null | Phone number | **headquarter** (Headquarter): Primary headquarter location (`null` if unknown) | Field | Type | Description | | ---------------- | -------------- | ----------------------------------------------- | | `street1` | string \| null | Street address line 1 | | `street2` | string \| null | Street address line 2 | | `postalCode` | string \| null | Postal / ZIP code | | `city` | string \| null | City | | `geographicArea` | string \| null | State or region | | `country` | string \| null | Country ISO 3166 code (e.g. `"US"`, `"FR"`) | **locations** (Location[]): All office locations registered on the profile (can be empty) | Field | Type | Description | | ------------ | -------------- | ----------------------------------------------- | | `street1` | string \| null | Street address line 1 | | `street2` | string \| null | Street address line 2 | | `postalCode` | string \| null | Postal / ZIP code | | `city` | string \| null | City | | `state` | string \| null | State or region (named `state` here, not `geographicArea` as in `headquarter`) | | `country` | string \| null | Country ISO 3166 code (e.g. `"US"`, `"FR"`) | **employeeCountRange** (EmployeeCountRange): Employee count bracket (`null` if unknown). Buckets mirror the source's native `staffCountRange`: `1`, `2–10`, `11–50`, `51–200`, `201–500`, `501–1,000`, `1,001–5,000`, `5,001–10,000`, `10,001+`. For the top bucket (`10,001+`), the source has no upper bound and returns `end: 1` as a sentinel: interpret any range where `end < start` as `start+` (unbounded). Use `employeesCount` for the exact headcount when available. | Field | Type | Description | | ------- | ------ | ---------------------------------------------------------------------------------------- | | `start` | number | Lower bound of the range | | `end` | number | Upper bound of the range. Ignore it for the top bucket, where `start` is `10001` | > Example for the 10,001+ bucket: `{ "start": 10001, "end": 10001 }`, or `{ "start": 10001, "end": 1 }` on some companies. In both cases the company has 10,001 or more employees, with no upper bound. **foundedOn** (FoundedOn): Founding year (`null` if unknown) | Field | Type | Description | | ------ | ------ | --------------- | | `year` | number | Four-digit year | ## Webhook callback errors (advanced) When the live fetch fails, `data` has no company fields. It echoes the original input back, with an `errorCode` that identifies the failure: **Error payload** ```json [Webhook callback (error)] { "webhookId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "data": { "url": "https://www.social.com/company/acme-corp", "errorCode": "company-not-found" } } ``` `data.errorCode` takes one of these values: | Code | Billed | Description | | ------------------- | ------ | ------------------------------------------------------------------- | | `company-not-found` | Yes | The company profile doesn't exist or could not be scraped | | `invalid-data` | No | The fetched data failed quality validation | | `fetch-data-error` | No | The API failed while fetching data. Safe to retry | | `result-unavailable` | No | The enrichment completed but the result was lost on our side. Retry the job. If the request was charged, contact support to get the credit back. | | `data-subject-blocked` | No | This company asked not to be processed under GDPR or CCPA. Do not retry. | > [!NOTE] > In the Billed column, Yes means the credits consumed at request time are kept, and No means they are fully refunded to your workspace balance. ## Code examples **Code Examples** ```typescript [TypeScript SDK - Polling (recommended)] import ReverseContact, { JobFailedError } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.fetch.companyLive({ url: "https://social.com/company/acme-corp" }); console.log("Save this job id:", job.id); // resume after a restart with client.jobs.wait(id) try { // Long polls until the job ends, 30 minutes at most by default. const done = await job.wait(); console.log(done.data.result?.companyName, done.data.result?.industry); } catch (error) { if (error instanceof JobFailedError) console.log("Job failed:", error.errorCode, "credits:", error.creditsConsumed); else throw error; } ``` ```typescript [TypeScript SDK - Webhook] import ReverseContact, { parseWebhookDelivery } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.fetch.companyLive({ url: "https://social.com/company/acme-corp", webhookUrl: "https://your-app.com/webhooks/reversecontact" }); console.log(job.status, job.id); // The route behind webhookUrl receives { webhookId, data }: answer within 10 seconds. async function handleWebhook(request: Request): Promise { const delivery = parseWebhookDelivery(await request.json()); console.log(delivery.webhookId, delivery.isError ? "failed" : "succeeded", delivery.data); return new Response(null, { status: 200 }); } ``` ```bash [cURL - Polling (recommended)] KEY="YOUR_API_KEY" ID=$(curl -sX POST https://api.reversecontact.com/v2/fetch/companies/live \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"url":"https://social.com/company/acme-corp"}' | jq -r .data.webhookId) while :; do S=$(curl -s https://api.reversecontact.com/v2/webhooks/$ID \ -H "Authorization: Bearer $KEY" | jq -r .data.status) [ "$S" = "succeeded" ] && break [ "$S" = "errored" ] && echo "job failed" && exit 1 sleep 2 done curl -s https://api.reversecontact.com/v2/webhooks/$ID \ -H "Authorization: Bearer $KEY" | jq .data.result ``` ```bash [cURL - Webhook] curl -X POST https://api.reversecontact.com/v2/fetch/companies/live \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://social.com/company/acme-corp", "webhookUrl": "https://your-app.com/webhooks/reversecontact" }' ``` ```python [Python SDK - Polling (recommended)] import os from reversecontact import JobFailedError, ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.fetch.company_live(url="https://social.com/company/acme-corp") print("Save this job id:", job.id) # resume after a restart with client.jobs.wait(id) try: # Long polls until the job ends, 30 minutes at most by default. done = job.wait() company = done.data.result if done.data else None if company is not None: print(company.company_name, company.industry) except JobFailedError as error: print("Job failed:", error.error_code) ``` ```python [Python SDK - Webhook] import os from reversecontact import ReverseContact, parse_webhook_delivery with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.fetch.company_live( url="https://social.com/company/acme-corp", webhook_url="https://your-app.com/webhooks/reversecontact", ) print(job.status, job.id) # The route behind webhook_url receives {webhookId, data}: answer within 10 seconds. def handle_webhook(body: bytes) -> None: delivery = parse_webhook_delivery(body) print(delivery.webhook_id, "failed" if delivery.is_error else "succeeded", delivery.data) ``` ## Example response **Example Response** ```json [Initial response] { "success": true, "data": { "status": "created", "webhookId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "pollUrl": "/v2/webhooks/b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e" }, "error": null, "metadata": { "requestId": "3740a067-a38e-4861-9c8e-e3467cbf9d0e", "executionTimeMs": 95 }, "quotas": { "creditsConsumed": 2, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4524, "left": 5476 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2025-01-20T14:16:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 5, "left": 55, "nextReset": "2025-01-20T14:16:00.000Z" } } } } ``` ```json [Webhook callback (success)] { "webhookId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "data": { "id": "com_01jbd5vewyenebxgaz869ffe5t", "companyName": "Acme Corp", "linkedinId": "12345678", "publicId": "acme-corp", "linkedinUrl": "https://www.social.com/company/acme-corp", "employeesCount": 452, "followersCount": 28512, "employeeCountRange": { "start": 201, "end": 500 }, "websiteUrl": "https://www.acmecorp.com", "tagline": "Building the future of work", "description": "Acme Corp is a leading provider of innovative SaaS solutions for enterprise teams. Founded in 2015, we serve over 10,000 customers worldwide with a focus on data-driven productivity tools.", "industry": "Software Development", "phone": "+1 (415) 555-0123", "specialities": [ "SaaS", "Enterprise Software", "Cloud Computing", "Data Analytics", "API Integration", "Workflow Automation", "Team Collaboration" ], "headquarter": { "street1": "600 Market Street", "street2": "Floor 12", "postalCode": "94105", "city": "San Francisco", "geographicArea": "California", "country": "US" }, "logoUrl": "https://media.licdn.com/dms/image/v2/C4D0BAQ.../company-logo_400_400/logo.jpg", "foundedOn": { "year": 2015 }, "locations": [ { "street1": "600 Market Street", "street2": "Floor 12", "postalCode": "94105", "city": "San Francisco", "state": "California", "country": "US" }, { "street1": "10 rue de la Paix", "street2": null, "postalCode": "75002", "city": "Paris", "state": null, "country": "FR" }, { "street1": "45 King William Street", "street2": "3rd Floor", "postalCode": "EC4N 7DG", "city": "London", "state": null, "country": "GB" } ], "backgroundUrl": "https://media.licdn.com/dms/image/v2/D5616AQ.../company-background/banner.jpg" } } ``` ```json [Webhook callback (error)] { "webhookId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "data": { "url": "https://www.social.com/company/acme-corp", "errorCode": "company-not-found" } } ``` --- # Company profile status > Check if a company profile exists in the database, and when it was last updated, before requesting enrichment. The check costs 0 credits. - Page: https://docs.reversecontact.com/endpoints/check-company - Endpoint: `POST https://api.reversecontact.com/v2/fetch/companies/check` - Credits: Free. Needs at least 2 credits in the workspace: below that, 402 NO_CREDITS. - Answer: synchronous. - Available in: REST API, MCP tool `check_company`, TypeScript and Python SDKs, CLI command `rc check company`. - Last verified: 2026-10-15 ## Introduction The Company Profile Status endpoint verifies whether a company profile exists in our database and returns freshness information. Use it to check data availability before spending credits on a full fetch or a live pull. ## When to use A typical cost-optimized workflow looks like this: 1. Call Profile Status to get the existence status, the last update date and the speciality count. 2. If the company exists and is recent, use [Company profile](https://docs.reversecontact.com/endpoints/fetch-company) (cached, 1 credit) to retrieve the full company profile. 3. If the company is stale or missing, use [Company profile live](https://docs.reversecontact.com/endpoints/fetch-company-live) (fresh from Social, 2 credits) for an up-to-date result. > [!NOTE] > Profile Status is free, but it needs at least 2 credits in your workspace. Below that, it returns `402 NO_CREDITS` until you top up: the fetch or live pull that follows can cost 2 credits and could not run anyway. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body Provide either `url` or `id`. If both are supplied, `id` takes precedence. | Name | Type | Required | Description | | ----- | ------ | ----------- | ------------------------------------------------------------------------------------------------------------------------- | | `url` | string | Conditional | Public Social company URL (e.g. `https://social.com/company/acme-corp`). Required unless `id` is given. | | `id` | string | Conditional | Reverse Contact company id from a previous response (e.g. `com_01jbd5vewyenebxgaz869ffe5t`). Required unless `url` is given. | ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (CheckData): Availability and freshness information (`null` on error responses) | Field | Type | Description | | ----------------- | -------------- | --------------------------------------------------------------------------------------------------------- | | `exists` | boolean | `true` if the company exists in the database | | `lastUpdate` | string \| null | When Reverse Contact last fetched this company from the source (ISO 8601). `null` when `exists` is `false` | | `specialityCount` | number | Number of company specialities. Only present when `exists` is `true` | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | --------------------------------------------------------------- | | `creditsConsumed` | number | Credits consumed by this request. Always `0` for Profile Status | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/fetch/companies/check \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://social.com/company/acme-corp"}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.fetch.checkCompany({ url: "https://social.com/company/acme-corp" }); if (result.data.exists) { console.log(`Company exists, last updated ${result.data.lastUpdate}`); } else { console.log("Company not in database yet"); } ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.fetch.check_company(url="https://social.com/company/acme-corp") if result.data is not None and result.data.exists: print(f"Company exists, last updated {result.data.last_update}") else: print("Company not in database yet") ``` ## Example response **Example Response** ```json [200 - Found] { "success": true, "data": { "exists": true, "lastUpdate": "2025-01-10T14:22:00.000Z", "specialityCount": 8 }, "error": null, "metadata": { "requestId": "a17f2c00-b340-47ab-af74-f59c9afdfd41", "executionTimeMs": 95 }, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 14, "left": 46, "nextReset": "2025-01-15T09:31:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 14, "left": 46, "nextReset": "2025-01-15T09:31:00.000Z" } } } } ``` ```json [200 - Not Found] { "success": true, "data": { "exists": false, "lastUpdate": null }, "error": null, "metadata": { "requestId": "131edc34-c504-4a23-a1ac-e0405013131f", "executionTimeMs": 78 }, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 15, "left": 45, "nextReset": "2025-01-15T09:31:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 15, "left": 45, "nextReset": "2025-01-15T09:31:00.000Z" } } } } ``` --- # Find Email > Find a professional or personal email address from a Social URL, a Reverse Contact person id, or full name and company domain, then receive the result by webhook. - Page: https://docs.reversecontact.com/endpoints/contact-email - Endpoint: `POST https://api.reversecontact.com/v2/contact/email` - Credits: 3 credits when a professional email is found, 5 for a personal one, whatever the emailType: an any lookup costs 3, 5 or 8 (8 when it finds both). A lookup that finds none is free. - Answer: asynchronous. The request returns a job (`data.webhookId`): poll `GET /v2/webhooks/{webhookId}?wait=20` until `status` is `succeeded` or `errored`, or receive the result on a webhook. Each poll answers as soon as the job ends, or after at most 20 seconds with the job still running; while it runs, start two polls at least `Retry-After` seconds apart (2 if the header is missing). - Available in: REST API, MCP tool `find_email`, TypeScript and Python SDKs, CLI command `rc contact email`. - Last verified: 2026-10-29 ## Introduction The Find Email endpoint discovers email addresses for a person: professional ones by default, personal ones on request with `emailType`. You can identify the person with a Social profile URL, with a Reverse Contact person id (`prs_…`) from a Search, Fetch or Enrich result, or with their full name and company domain. Processing is asynchronous: the API acknowledges the request with a `webhookId`, then you retrieve the result by [polling](https://docs.reversecontact.com/guides/polling) (recommended, no public endpoint required) or through a [webhook callback](https://docs.reversecontact.com/guides/webhooks) (advanced, push delivery). Use this endpoint when you need an email address for outreach or CRM enrichment. For push delivery, configure a workspace webhook URL in Settings > Webhooks, which receives found emails only, or provide `webhookUrl` in the request, which also receives not-found and error outcomes. The price depends on the kind of email found, not on the `emailType` you send: a professional email costs **3 credits** and a personal email costs **5 credits**, so an `any` lookup costs 3, 5 or 8 credits depending on what it finds. A lookup that finds no email is free. See [Credits by email type](#credits-by-email-type) for what each `emailType` debits. > [!NOTE] > Find Email is asynchronous. We aim to deliver each result in under 15 seconds, but it can take tens of minutes depending on conditions. Use it to update databases and feed async pipelines, not to answer a request a user is waiting on. See [Polling](https://docs.reversecontact.com/guides/polling) for delivery details. > [!NOTE] > Every email we return is verified. Each candidate address goes through our own validation process, then through a waterfall of specialist verification providers, and we return only the addresses that pass both. We monitor these providers and switch to another one when needed, and we keep evaluating new vendors to add to the waterfall. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it in the `Authorization: Bearer YOUR_API_KEY` header. | ## Request body Choose one lookup strategy: a Social profile URL, a person id, or a full name with a company domain. The split `firstName` + `lastName` form is also accepted. | Name | Type | Required | Description | | --------------- | ------ | ----------- | -------------------------------------------------------------------------------------------------------- | | `url` | string | Conditional | Public Social profile URL, such as `https://www.social.com/in/janedoe`. Use this for the URL strategy. | | `id` | string | Conditional | Reverse Contact person id from a previous response, such as `prs_01jbd5vewyenebxgaz869ffe5t`. Use this for the id strategy. | | `fullName` | string | Conditional | Person's full name, such as `Jane Doe`. Use this with `companyDomain`. | | `firstName` | string | Conditional | Person's first name. Use this with `lastName` and `companyDomain` instead of `fullName`. | | `lastName` | string | Conditional | Person's last name. Use this with `firstName` and `companyDomain` instead of `fullName`. | | `companyDomain` | string | Conditional | Company domain, such as `acme.com`. Required for the name-based strategies. | | `emailType` | string | Optional | Kind of email to find: `professional` (default), `personal`, or `any` to accept either. Omit it to find professional emails. | | `webhookUrl` | string | Optional | HTTPS URL that receives the callback. Omit it to retrieve the result by polling instead. | > [!NOTE] > The request must contain `url`, or `id`, or `fullName` + `companyDomain`, or `firstName` + `lastName` + `companyDomain`. > [!NOTE] > `id` is the `id` field of any person returned by Search, Fetch or Enrich. If both `url` and `id` are supplied, `id` takes precedence. An `id` that matches no person returns `422 VALIDATION_ERROR` before any lookup starts, and costs 0 credits. **Person id** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/contact/email \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"id":"prs_01jbd5vewyenebxgaz869ffe5t"}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.contact.findEmail({ id: "prs_01jbd5vewyenebxgaz869ffe5t" }); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.contact.find_email(id="prs_01jbd5vewyenebxgaz869ffe5t") ``` > [!NOTE] > `emailType` sets the kind of email to find. Without it, the lookup finds professional emails only. `personal` finds personal emails only. `any` accepts both kinds: the lookup tries its sources in turn and stops at the first one that finds an email, and that source can return a professional and a personal email at once. Each found email carries its `type`. An unknown value returns `422`. The price depends on the kind of email found: see [Credits by email type](#credits-by-email-type). ## Idempotency-Key header A lookup is debited when its job is created. If the response gets lost (a timeout, a dropped connection), sending the request again would start and bill a second lookup. To retry safely, send an `Idempotency-Key` header with a key you choose for this lookup, and send the same key again when you retry. | Name | Type | Required | Description | | ----------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- | | `Idempotency-Key` | string | Optional | A key you choose for this lookup: 1 to 255 printable ASCII characters without spaces, such as a UUID. Use a new key for each new lookup. | For 24 hours, a request with the same key and the same body returns the response of the first one, unchanged, with the same `webhookId` and the header `Idempotent-Replayed: true`. No new lookup starts and no credits are debited: the replayed `quotas` describe the first request. Keys are scoped to your workspace and to this endpoint. A replayed request still counts toward your per-minute rate limit. | Status | Code | When | | ------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `409` | `IDEMPOTENCY_KEY_IN_USE` | The first request with this key is still being processed. Retry after the `Retry-After` seconds to receive its response. | | `422` | `IDEMPOTENCY_KEY_REUSED` | The key was already used with a different body. Use a new key for a new lookup. | | `400` | `INVALID_IDEMPOTENCY_KEY` | The key is empty, longer than 255 characters, or contains a space or a non-ASCII character. | A first request that ends in an error is not kept: it started nothing and cost nothing, so a retry with the same key runs as a new request. **Idempotency-Key** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/contact/email \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d" \ -d '{"url":"https://www.social.com/in/janedoe"}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); // The SDK sends a new Idempotency-Key on each call and reuses it on its own retries. // To retry after a restart, store your own key with the request and pass it again. const job = await client.contact.findEmail( { url: "https://www.social.com/in/janedoe" }, { idempotencyKey: "4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d" } ); console.log(job.id, job.replayed); // replayed: true when the first response came back ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: # The SDK sends a new Idempotency-Key on each call and reuses it on its own retries. # To retry after a restart, store your own key with the request and pass it again. job = client.contact.find_email( url="https://www.social.com/in/janedoe", idempotency_key="4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d", ) print(job.id, job.replayed) # replayed: True when the first response came back ``` ## Credits by email type Each kind of email is billed once, however many addresses of that kind the lookup returns: a professional email costs 3 credits (two professional addresses still cost 3) and a personal email costs 5 credits. When a lookup finds both kinds, which only `any` can do, the prices add up to 8 credits. An `any` lookup therefore has no fixed price: it costs 3, 5 or 8 credits depending on what it finds. A lookup that finds no email costs 0 credits. The credits are debited when the job is created, then adjusted to what the lookup found when the result arrives: | `emailType` | Debited at creation | Final cost by result | | --------------------------- | :-----------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `professional` (or omitted) | 3 | Email found: 3. No email found: the 3 credits are refunded. | | `personal` | 5 | Email found: 5. No email found: the 5 credits are refunded. | | `any` | 5 | Professional only: 3 (2 credits refunded). Personal only: 5 (no change). Both kinds: 8 (3 more credits debited). No email found: the 5 credits are refunded. | Before it accepts the request, the API checks that your balance could pay for the most expensive result: 3 credits for a `professional` lookup, 5 for `personal`, and 8 for `any` in case it finds both kinds. This is a balance check, not the price: an `any` lookup is still billed 3, 5 or 8 credits depending on what it finds. Below that balance, the request returns `402 INSUFFICIENT_CREDITS` and nothing is debited (see [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries)). With 6 credits left, for example, a `personal` lookup is accepted and an `any` lookup is refused. The initial response's `quotas.creditsConsumed` shows what was debited at creation: 3, or 5 for `personal` and `any`. Any refund or extra debit is applied to your balance when the result arrives. ## Response structure This endpoint replies immediately with a `webhookId`. You then retrieve the result in one of two ways: 1. Polling (recommended): call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. With `?wait=20`, each call waits up to 20 seconds and answers as soon as the result is ready. No webhook URL is needed. See the [Polling guide](https://docs.reversecontact.com/guides/polling). 2. Webhook callback (advanced): if you provided a `webhookUrl` (or a workspace default is configured), the gateway POSTs the emails to that URL once the lookup completes. See [Webhook callback payload (advanced)](#webhook-callback-payload-advanced) below. | Field | Type | Description | | --------- | ------- | ------------------------------------------ | | `success` | boolean | `true` if the job was created successfully | **data** (JobAck): Initial job acknowledgement. Save the `webhookId` to retrieve the result via polling (`GET /v2/webhooks/:webhookId`) or to correlate an upcoming webhook callback (`null` on error responses). | Field | Type | Description | | ----------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- | | `status` | string | Job status, always `"created"` for the initial response | | `webhookId` | string | Unique identifier (UUID). Save it to poll the result at `GET /v2/webhooks/:webhookId`, or to correlate an incoming webhook callback. | | `pollUrl` | string | Convenience hint pointing to the polling endpoint for this job (e.g. `/v2/webhooks/{webhookId}`). | | Field | Type | Description | | ------- | ------ | ------------------------ | | `error` | `null` | Always `null` on success | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | -------------------------------- | | `creditsConsumed` | number | Credits debited when the job is created: 3, or 5 for `personal` and `any` | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Webhook callback payload (advanced) When the lookup succeeds, Reverse Contact sends a JSON `POST` request to your `webhookUrl`. The callback is not wrapped in the V2 response envelope. It always includes the `webhookId` from the initial acknowledgement and a `data` value. **Success payload** ```json [Webhook callback (success)] { "webhookId": "b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4", "data": [ { "value": "jane.doe@acme.com", "type": "professional" } ] } ``` **data** (FoundEmail): One entry per email address discovered for the person. | Field | Type | Description | | ------- | ------ | ---------------------------------------- | | `value` | string | The discovered email address | | `type` | string | Email category: `professional` or `personal` | ## Webhook callback errors (advanced) When the lookup cannot return an email, `data` is an object instead of an array, with an `errorCode` that gives the reason: **Error payload** ```json [Webhook callback (error)] { "webhookId": "b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4", "data": { "errorCode": "email-not-found" } } ``` `data.errorCode` takes one of these values: | Code | Billed | Description | | ------------------------- | ------ | ----------------------------------------------------------------- | | `email-not-found` | No | No email address was discovered for the person | | `person-not-found` | No | No matching person was found for the supplied identifiers | | `invalid-request` | No | The lookup input is malformed or does not match an accepted form | | `fetch-data-error` | No | The lookup failed while fetching data; safe to retry | | `webhook-url-invalid` | Yes | The callback URL returned a 4xx response | | `webhook-url-errored` | Yes | The callback URL returned a 5xx response or a network error | | `webhook-url-timeout` | Yes | The callback URL did not respond in time | | `webhook-url-unreachable` | Yes | The callback URL could not be reached | > [!NOTE] > The lookup is billed only when an email is found: 3 credits for a professional email, 5 for a personal one, so an `any` lookup costs 3, 5 or 8, the 8 only when it finds both (see [Credits by email type](#credits-by-email-type)). A lookup that returns `email-not-found`, `person-not-found`, `invalid-request`, or `fetch-data-error` costs 0 credits: the credits debited at creation are refunded. A delivery failure remains billed when an email was found. ## Code examples **Code Examples** ```typescript [TypeScript SDK - Polling (recommended)] import ReverseContact, { JobFailedError } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.contact.findEmail({ url: "https://www.social.com/in/janedoe" }); console.log("Save this job id:", job.id); // resume after a restart with client.jobs.wait(id) try { // Long polls until the job ends, 30 minutes at most by default. const done = await job.wait(); for (const email of done.data.result ?? []) console.log(email.value, email.type); } catch (error) { if (error instanceof JobFailedError) console.log("Ended without an email:", error.errorCode, "credits:", error.creditsConsumed); else throw error; } ``` ```typescript [TypeScript SDK - Webhook] import ReverseContact, { parseWebhookDelivery } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.contact.findEmail({ fullName: "Jane Doe", companyDomain: "acme.com", emailType: "any", webhookUrl: "https://your-app.com/webhooks/reversecontact" }); console.log(job.status, job.id); // The route behind webhookUrl receives { webhookId, data }: answer within 10 seconds. async function handleWebhook(request: Request): Promise { const delivery = parseWebhookDelivery(await request.json()); console.log(delivery.webhookId, delivery.isError ? "failed" : "succeeded", delivery.data); return new Response(null, { status: 200 }); } ``` ```bash [cURL - Polling (recommended)] KEY="YOUR_API_KEY" ID=$(curl -sX POST https://api.reversecontact.com/v2/contact/email \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"url":"https://www.social.com/in/janedoe"}' | jq -r .data.webhookId) while :; do S=$(curl -s https://api.reversecontact.com/v2/webhooks/$ID \ -H "Authorization: Bearer $KEY" | jq -r .data.status) [ "$S" = "succeeded" ] && break [ "$S" = "errored" ] && echo "job failed" && exit 1 sleep 2 done curl -s https://api.reversecontact.com/v2/webhooks/$ID \ -H "Authorization: Bearer $KEY" | jq .data.result ``` ```bash [cURL - Webhook] curl -X POST https://api.reversecontact.com/v2/contact/email \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fullName": "Jane Doe", "companyDomain": "acme.com", "emailType": "any", "webhookUrl": "https://your-app.com/webhooks/reversecontact" }' ``` ```python [Python SDK - Polling (recommended)] import os from reversecontact import JobFailedError, ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.contact.find_email(url="https://www.social.com/in/janedoe") print("Save this job id:", job.id) # resume after a restart with client.jobs.wait(id) try: # Long polls until the job ends, 30 minutes at most by default. done = job.wait() found = done.data.result if done.data and done.data.result else [] for email in found: if email is not None: print(email.value, email.type) except JobFailedError as error: print("Ended without an email:", error.error_code) ``` ```python [Python SDK - Webhook] import os from reversecontact import ReverseContact, parse_webhook_delivery with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.contact.find_email( full_name="Jane Doe", company_domain="acme.com", email_type="any", webhook_url="https://your-app.com/webhooks/reversecontact", ) print(job.status, job.id) # The route behind webhook_url receives {webhookId, data}: answer within 10 seconds. def handle_webhook(body: bytes) -> None: delivery = parse_webhook_delivery(body) print(delivery.webhook_id, "failed" if delivery.is_error else "succeeded", delivery.data) ``` ## Example response **Example Response** ```json [Initial response] { "success": true, "data": { "status": "created", "webhookId": "b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4", "pollUrl": "/v2/webhooks/b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4" }, "error": null, "metadata": { "requestId": "a9a842f2-ac02-4f37-aa4f-b8c738dd74af", "executionTimeMs": 95 }, "quotas": { "creditsConsumed": 3, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4539, "left": 5461 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 10, "left": 50, "nextReset": "2025-01-20T14:16:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 10, "left": 50, "nextReset": "2025-01-20T14:16:00.000Z" } } } } ``` ```json [Webhook callback] { "webhookId": "b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4", "data": [ { "value": "jane.doe@acme.com", "type": "professional" } ] } ``` --- # Verify Email > Check whether an email address can receive mail and get a verdict (valid, risky, invalid or unknown) by polling or webhook. - Page: https://docs.reversecontact.com/endpoints/contact-email-verify - Endpoint: `POST https://api.reversecontact.com/v2/contact/email/verify` - Credits: 1 credit per verdict, whatever it says. A check that fails is refunded. - Answer: asynchronous. The request returns a job (`data.webhookId`): poll `GET /v2/webhooks/{webhookId}?wait=20` until `status` is `succeeded` or `errored`, or receive the result on a webhook. Each poll answers as soon as the job ends, or after at most 20 seconds with the job still running; while it runs, start two polls at least `Retry-After` seconds apart (2 if the header is missing). - Available in: REST API, TypeScript and Python SDKs. - Last verified: 2026-11-03 ## Introduction The Verify Email endpoint checks whether an email address can receive mail, and returns one verdict: `valid`, `risky`, `invalid` or `unknown`. Processing is asynchronous: the API acknowledges the request with a `webhookId`, then you retrieve the verdict by [polling](https://docs.reversecontact.com/guides/polling) (recommended, no public endpoint required) or through a [webhook callback](https://docs.reversecontact.com/guides/webhooks) (advanced, push delivery). Use this endpoint to clean a list before a send, or to check an address that comes from a form, a CRM or another source. Addresses returned by [Find Email](https://docs.reversecontact.com/endpoints/contact-email) are already verified, so you do not need to check them again. This endpoint costs **1 credit** per verdict, whatever the verdict says, `unknown` included. A check that fails without a verdict is refunded. See [Verdicts](#verdicts). > [!NOTE] > Verify Email is asynchronous. We aim to deliver each verdict in under 15 seconds, but it can take tens of minutes depending on conditions. Use it to update databases and feed async pipelines, not to answer a request a user is waiting on. See [Polling](https://docs.reversecontact.com/guides/polling) for delivery details. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it in the `Authorization: Bearer YOUR_API_KEY` header. | ## Request body | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `email` | string | Yes | The email address to check, such as `jane@acme.com`. | | `webhookUrl` | string | Optional | HTTPS URL that receives the result, including error outcomes. Omit it to poll for the verdict instead. | A request with a missing or malformed `email` is refused with a `422` and costs nothing: no job starts. ## Idempotency-Key header A check is debited when its job is created. If the response gets lost (a timeout, a dropped connection), sending the request again would start and bill a second check. To retry safely, send an `Idempotency-Key` header with a key you choose for this check, and send the same key again when you retry. | Name | Type | Required | Description | | ----------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- | | `Idempotency-Key` | string | Optional | A key you choose for this check: 1 to 255 printable ASCII characters without spaces, such as a UUID. Use a new key for each new check. | For 24 hours, a request with the same key and the same body returns the response of the first one, unchanged, with the same `webhookId` and the header `Idempotent-Replayed: true`. No new check starts and no credits are debited: the replayed `quotas` describe the first request. Keys are scoped to your workspace and to this endpoint. A replayed request still counts toward your per-minute rate limit. | Status | Code | When | | ------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `409` | `IDEMPOTENCY_KEY_IN_USE` | The first request with this key is still being processed. Retry after the `Retry-After` seconds to receive its response. | | `422` | `IDEMPOTENCY_KEY_REUSED` | The key was already used with a different body. Use a new key for a new check. | | `400` | `INVALID_IDEMPOTENCY_KEY` | The key is empty, longer than 255 characters, or contains a space or a non-ASCII character. | A first request that ends in an error is not kept: it started nothing and cost nothing, so a retry with the same key runs as a new request. **Idempotency-Key** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/contact/email/verify \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d" \ -d '{"email":"jane@acme.com"}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); // The SDK sends a new Idempotency-Key on each call and reuses it on its own retries. // To retry after a restart, store your own key with the request and pass it again. const job = await client.contact.verifyEmail( { email: "jane@acme.com" }, { idempotencyKey: "4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d" } ); console.log(job.id, job.replayed); // replayed: true when the first response came back ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: # The SDK sends a new Idempotency-Key on each call and reuses it on its own retries. # To retry after a restart, store your own key with the request and pass it again. job = client.contact.verify_email( email="jane@acme.com", idempotency_key="4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d", ) print(job.id, job.replayed) # replayed: True when the first response came back ``` ## Verdicts The verdict is the `status` field of the result. It is not the status of the job: when you poll, `data.status` is the job (`succeeded`) and `data.result.status` is the verdict. | Verdict | Meaning | What to do | | --------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------- | | `valid` | The address can receive mail. | Send. | | `risky` | The address may receive mail, but delivery is not guaranteed. A catch-all domain is a common cause. | Send with care, or keep it out of a cold send. | | `invalid` | The address cannot receive mail. | Do not send. | | `unknown` | The check was inconclusive. | Treat it as unverified. You can check it again later. | Every verdict costs 1 credit. ## Response structure This endpoint replies immediately with a `webhookId`. You then retrieve the result in one of two ways: 1. Polling (recommended): call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. With `?wait=20`, each call waits up to 20 seconds and answers as soon as the result is ready. No webhook URL is needed. See the [Polling guide](https://docs.reversecontact.com/guides/polling). 2. Webhook callback (advanced): if you provided a `webhookUrl` (or a workspace default is configured), Reverse Contact POSTs the verdict to that URL once the check completes. See [Webhook callback payload (advanced)](#webhook-callback-payload-advanced) below. | Field | Type | Description | | --------- | ------- | ------------------------------------------ | | `success` | boolean | `true` if the job was created successfully | **data** (JobAck): Initial job acknowledgement. Save the `webhookId` to retrieve the result via polling (`GET /v2/webhooks/:webhookId`) or to correlate an upcoming webhook callback (`null` on error responses). | Field | Type | Description | | ----------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- | | `status` | string | Job status, always `"created"` for the initial response | | `webhookId` | string | Unique identifier (UUID). Save it to poll the result at `GET /v2/webhooks/:webhookId`, or to correlate an incoming webhook callback. | | `pollUrl` | string | Convenience hint pointing to the polling endpoint for this job (e.g. `/v2/webhooks/{webhookId}`). | | Field | Type | Description | | ------- | ------ | ------------------------ | | `error` | `null` | Always `null` on success | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | -------------------------------- | | `creditsConsumed` | number | Credits debited when the job is created: 1 | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Webhook callback payload (advanced) When the check returns a verdict, Reverse Contact sends a JSON `POST` request to your `webhookUrl`. The callback is not wrapped in the V2 response envelope. It always includes the `webhookId` from the initial acknowledgement and a `data` value. **Success payload** ```json [Webhook callback (success)] { "webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "data": { "email": "jane@acme.com", "status": "valid" } } ``` **data** (EmailVerification): The verdict for the address you sent. | Field | Type | Description | | -------- | ------ | -------------------------------------------------------------------- | | `email` | string | The address that was checked | | `status` | string | The verdict: `valid`, `risky`, `invalid` or `unknown`. See [Verdicts](#verdicts). | ## Webhook callback errors (advanced) When the check cannot return a verdict, `data` holds an `errorCode` that gives the reason: **Error payload** ```json [Webhook callback (error)] { "webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "data": { "errorCode": "fetch-data-error" } } ``` `data.errorCode` takes one of these values: | Code | Billed | Description | | ------------------------- | ------ | ------------------------------------------------------------ | | `invalid-request` | No | The address has a form the check cannot read | | `fetch-data-error` | No | The check failed before a verdict; safe to retry | | `webhook-url-invalid` | Yes | The callback URL returned a 4xx response | | `webhook-url-errored` | Yes | The callback URL returned a 5xx response or a network error | | `webhook-url-timeout` | Yes | The callback URL did not respond in time | | `webhook-url-unreachable` | Yes | The callback URL could not be reached | > [!NOTE] > A check that returns `invalid-request` or `fetch-data-error` costs 0 credits: the credit debited at creation is refunded. A delivery failure remains billed, because the verdict was reached: you can still read it by polling. ## Code examples **Code Examples** ```typescript [TypeScript SDK - Polling (recommended)] import ReverseContact, { JobFailedError } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.contact.verifyEmail({ email: "jane@acme.com" }); console.log("Save this job id:", job.id); // resume after a restart with client.jobs.wait(id) try { // Long polls until the job ends, 30 minutes at most by default. const done = await job.wait(); console.log(done.data.result?.email, done.data.result?.status); } catch (error) { if (error instanceof JobFailedError) console.log("Ended without a verdict:", error.errorCode, "credits:", error.creditsConsumed); else throw error; } ``` ```typescript [TypeScript SDK - Webhook] import ReverseContact, { parseWebhookDelivery } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.contact.verifyEmail({ email: "jane@acme.com", webhookUrl: "https://your-app.com/webhooks/reversecontact" }); console.log(job.status, job.id); // The route behind webhookUrl receives { webhookId, data }: answer within 10 seconds. async function handleWebhook(request: Request): Promise { const delivery = parseWebhookDelivery(await request.json()); console.log(delivery.webhookId, delivery.isError ? "failed" : "succeeded", delivery.data); return new Response(null, { status: 200 }); } ``` ```bash [cURL - Polling (recommended)] KEY="YOUR_API_KEY" ID=$(curl -sX POST https://api.reversecontact.com/v2/contact/email/verify \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"email":"jane@acme.com"}' | jq -r .data.webhookId) while :; do S=$(curl -s https://api.reversecontact.com/v2/webhooks/$ID \ -H "Authorization: Bearer $KEY" | jq -r .data.status) [ "$S" = "succeeded" ] && break [ "$S" = "errored" ] && echo "job failed" && exit 1 sleep 2 done curl -s https://api.reversecontact.com/v2/webhooks/$ID \ -H "Authorization: Bearer $KEY" | jq .data.result ``` ```bash [cURL - Webhook] curl -X POST https://api.reversecontact.com/v2/contact/email/verify \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "jane@acme.com", "webhookUrl": "https://your-app.com/webhooks/reversecontact" }' ``` ```python [Python SDK - Polling (recommended)] import os from reversecontact import JobFailedError, ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.contact.verify_email(email="jane@acme.com") print("Save this job id:", job.id) # resume after a restart with client.jobs.wait(id) try: # Long polls until the job ends, 30 minutes at most by default. done = job.wait() if done.data and done.data.result: print(done.data.result.email, done.data.result.status) except JobFailedError as error: print("Ended without a verdict:", error.error_code) ``` ```python [Python SDK - Webhook] import os from reversecontact import ReverseContact, parse_webhook_delivery with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.contact.verify_email( email="jane@acme.com", webhook_url="https://your-app.com/webhooks/reversecontact", ) print(job.status, job.id) # The route behind webhook_url receives {webhookId, data}: answer within 10 seconds. def handle_webhook(body: bytes) -> None: delivery = parse_webhook_delivery(body) print(delivery.webhook_id, "failed" if delivery.is_error else "succeeded", delivery.data) ``` ## Example response **Example Response** ```json [Initial response] { "success": true, "data": { "status": "created", "webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "pollUrl": "/v2/webhooks/c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f" }, "error": null, "metadata": { "requestId": "2f25aff4-f3df-45ab-be00-be477ba984e5", "executionTimeMs": 95 }, "quotas": { "creditsConsumed": 1, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4537, "left": 5463 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 10, "left": 50, "nextReset": "2025-01-20T14:16:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 10, "left": 50, "nextReset": "2025-01-20T14:16:00.000Z" } } } } ``` ```json [Polling result] { "success": true, "data": { "webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "status": "succeeded", "eventType": "FETCH_CONTACT_DATA", "errorCode": null, "createdAt": "2026-11-03T12:43:02.017Z", "result": { "email": "jane@acme.com", "status": "valid" }, "creditsConsumed": 1 } } ``` ```json [Webhook callback] { "webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "data": { "email": "jane@acme.com", "status": "valid" } } ``` --- # Find Phone > Find a professional phone number from a Social URL, a Reverse Contact person id, or full name and company domain, then receive the result by webhook. - Page: https://docs.reversecontact.com/endpoints/contact-phone - Endpoint: `POST https://api.reversecontact.com/v2/contact/phone` - Credits: 20 credits when a phone number is found. A lookup that finds none is free. - Answer: asynchronous. The request returns a job (`data.webhookId`): poll `GET /v2/webhooks/{webhookId}?wait=20` until `status` is `succeeded` or `errored`, or receive the result on a webhook. Each poll answers as soon as the job ends, or after at most 20 seconds with the job still running; while it runs, start two polls at least `Retry-After` seconds apart (2 if the header is missing). - Available in: REST API, MCP tool `find_phone`, TypeScript and Python SDKs, CLI command `rc contact phone`. - Last verified: 2026-10-29 ## Introduction The Find Phone endpoint discovers professional phone numbers for a person. You can identify the person with a Social profile URL, with a Reverse Contact person id (`prs_…`) from a Search, Fetch or Enrich result, or with their full name and company domain. Processing is asynchronous: the API acknowledges the request with a `webhookId`, then you retrieve the result by [polling](https://docs.reversecontact.com/guides/polling) (recommended, no public endpoint required) or through a [webhook callback](https://docs.reversecontact.com/guides/webhooks) (advanced, push delivery). Use this endpoint when you need a phone number for outreach or CRM enrichment. For push delivery, configure a workspace webhook URL in Settings > Webhooks, which receives found numbers only, or provide `webhookUrl` in the request, which also receives not-found and error outcomes. > [!NOTE] > Find Phone is asynchronous. We aim to deliver each result in under 15 seconds, but it can take tens of minutes depending on conditions. Use it to update databases and feed async pipelines, not to answer a request a user is waiting on. See [Polling](https://docs.reversecontact.com/guides/polling) for delivery details. > [!NOTE] > Phone numbers are harder to source than email addresses, so expect fewer matches than with the [Find Email endpoint](https://docs.reversecontact.com/endpoints/contact-email). This endpoint costs **20 credits**, charged only when at least one phone number is found. A lookup that finds nothing costs 0 credits. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it in the `Authorization: Bearer YOUR_API_KEY` header. | ## Request body Choose one lookup strategy: a Social profile URL, a person id, or a full name with a company domain. The split `firstName` + `lastName` form is also accepted. | Name | Type | Required | Description | | --------------- | ------ | ----------- | -------------------------------------------------------------------------------------------------------- | | `url` | string | Conditional | Public Social profile URL, such as `https://www.social.com/in/janedoe`. Use this for the URL strategy. | | `id` | string | Conditional | Reverse Contact person id from a previous response, such as `prs_01jbd5vewyenebxgaz869ffe5t`. Use this for the id strategy. | | `fullName` | string | Conditional | Person's full name, such as `Jane Doe`. Use this with `companyDomain`. | | `firstName` | string | Conditional | Person's first name. Use this with `lastName` and `companyDomain` instead of `fullName`. | | `lastName` | string | Conditional | Person's last name. Use this with `firstName` and `companyDomain` instead of `fullName`. | | `companyDomain` | string | Conditional | Company domain, such as `acme.com`. Required for the name-based strategies. | | `webhookUrl` | string | Optional | HTTPS URL that receives the callback. Omit it to retrieve the result by polling instead. | > [!NOTE] > The request must contain `url`, or `id`, or `fullName` + `companyDomain`, or `firstName` + `lastName` + `companyDomain`. > [!NOTE] > `id` is the `id` field of any person returned by Search, Fetch or Enrich. If both `url` and `id` are supplied, `id` takes precedence. An `id` that matches no person returns `422 VALIDATION_ERROR` before any lookup starts, and costs 0 credits. **Person id** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/contact/phone \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"id":"prs_01jbd5vewyenebxgaz869ffe5t"}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.contact.findPhone({ id: "prs_01jbd5vewyenebxgaz869ffe5t" }); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.contact.find_phone(id="prs_01jbd5vewyenebxgaz869ffe5t") ``` ## Idempotency-Key header A lookup starts as soon as the request is accepted. If the response gets lost (a timeout, a dropped connection), sending the request again would start and bill a second lookup. To retry safely, send an `Idempotency-Key` header with a key you choose for this lookup, and send the same key again when you retry. | Name | Type | Required | Description | | ----------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- | | `Idempotency-Key` | string | Optional | A key you choose for this lookup: 1 to 255 printable ASCII characters without spaces, such as a UUID. Use a new key for each new lookup. | For 24 hours, a request with the same key and the same body returns the response of the first one, unchanged, with the same `webhookId` and the header `Idempotent-Replayed: true`. No new lookup starts and no credits are debited: the replayed `quotas` describe the first request. Keys are scoped to your workspace and to this endpoint. A replayed request still counts toward your per-minute rate limit. | Status | Code | When | | ------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `409` | `IDEMPOTENCY_KEY_IN_USE` | The first request with this key is still being processed. Retry after the `Retry-After` seconds to receive its response. | | `422` | `IDEMPOTENCY_KEY_REUSED` | The key was already used with a different body. Use a new key for a new lookup. | | `400` | `INVALID_IDEMPOTENCY_KEY` | The key is empty, longer than 255 characters, or contains a space or a non-ASCII character. | A first request that ends in an error is not kept: it started nothing and cost nothing, so a retry with the same key runs as a new request. **Idempotency-Key** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/contact/phone \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d" \ -d '{"url":"https://www.social.com/in/janedoe"}' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); // The SDK sends a new Idempotency-Key on each call and reuses it on its own retries. // To retry after a restart, store your own key with the request and pass it again. const job = await client.contact.findPhone( { url: "https://www.social.com/in/janedoe" }, { idempotencyKey: "4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d" } ); console.log(job.id, job.replayed); // replayed: true when the first response came back ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: # The SDK sends a new Idempotency-Key on each call and reuses it on its own retries. # To retry after a restart, store your own key with the request and pass it again. job = client.contact.find_phone( url="https://www.social.com/in/janedoe", idempotency_key="4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d", ) print(job.id, job.replayed) # replayed: True when the first response came back ``` ## Response structure This endpoint replies immediately with a `webhookId`. You then retrieve the result in one of two ways: 1. Polling (recommended): call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. With `?wait=20`, each call waits up to 20 seconds and answers as soon as the result is ready. No webhook URL is needed. See the [Polling guide](https://docs.reversecontact.com/guides/polling). 2. Webhook callback (advanced): if you provided a `webhookUrl` (or a workspace default is configured), the gateway POSTs the phone numbers to that URL once the lookup completes. See [Webhook callback payload (advanced)](#webhook-callback-payload-advanced) below. | Field | Type | Description | | --------- | ------- | ------------------------------------------ | | `success` | boolean | `true` if the job was created successfully | **data** (JobAck): Initial job acknowledgement. Save the `webhookId` to retrieve the result via polling (`GET /v2/webhooks/:webhookId`) or to correlate an upcoming webhook callback (`null` on error responses). | Field | Type | Description | | ----------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- | | `status` | string | Job status, always `"created"` for the initial response | | `webhookId` | string | Unique identifier (UUID). Save it to poll the result at `GET /v2/webhooks/:webhookId`, or to correlate an incoming webhook callback. | | `pollUrl` | string | Convenience hint pointing to the polling endpoint for this job (e.g. `/v2/webhooks/{webhookId}`). | | Field | Type | Description | | ------- | ------ | ------------------------ | | `error` | `null` | Always `null` on success | **quotas** (V2QuotaInfo): Credits and rate limit usage after this request | Field | Type | Description | | ----------------- | ------ | -------------------------------- | | `creditsConsumed` | number | Credits consumed by this request | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **metadata** (V2ResponseMetadata): Request tracking information | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Webhook callback payload (advanced) When the lookup succeeds, Reverse Contact sends a JSON `POST` request to your `webhookUrl`. The callback is not wrapped in the V2 response envelope. It always includes the `webhookId` from the initial acknowledgement and a `data` value. **Success payload** ```json [Webhook callback (success)] { "webhookId": "b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4", "data": [ { "value": "+33612345678", "type": "mobile" } ] } ``` **data** (FoundPhone): One entry per phone number discovered for the person. | Field | Type | Description | | ------- | -------------- | --------------------------------------------------------------------------------------------- | | `value` | string | The discovered phone number, in the format reported by the provider (usually E.164) | | `type` | string \| null | Phone kind reported by the provider, such as `mobile`; `null` when the provider gives no kind | ## Webhook callback errors (advanced) When the lookup cannot return a phone number, `data` is an object instead of an array, with an `errorCode` that gives the reason: **Error payload** ```json [Webhook callback (error)] { "webhookId": "b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4", "data": { "errorCode": "phone-not-found" } } ``` `data.errorCode` takes one of these values: | Code | Billed | Description | | ------------------------- | ------ | ----------------------------------------------------------------- | | `phone-not-found` | No | No phone number was discovered for the person | | `person-not-found` | No | No matching person was found for the supplied identifiers | | `invalid-request` | No | The lookup input is malformed or does not match an accepted form | | `fetch-data-error` | No | The lookup failed while fetching data; safe to retry | | `webhook-url-invalid` | Yes | The callback URL returned a 4xx response | | `webhook-url-errored` | Yes | The callback URL returned a 5xx response or a network error | | `webhook-url-timeout` | Yes | The callback URL did not respond in time | | `webhook-url-unreachable` | Yes | The callback URL could not be reached | > [!NOTE] > The lookup is billed 20 credits only when at least one phone number is found. A lookup that returns `phone-not-found`, `person-not-found`, `invalid-request`, or `fetch-data-error` costs 0 credits. A delivery failure remains billed when a phone number was found. ## Code examples **Code Examples** ```typescript [TypeScript SDK - Polling (recommended)] import ReverseContact, { JobFailedError } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.contact.findPhone({ url: "https://www.social.com/in/janedoe" }); console.log("Save this job id:", job.id); // resume after a restart with client.jobs.wait(id) try { // Long polls until the job ends, 30 minutes at most by default. const done = await job.wait(); for (const phone of done.data.result ?? []) console.log(phone.value, phone.type); } catch (error) { if (error instanceof JobFailedError) console.log("Ended without a phone number:", error.errorCode, "credits:", error.creditsConsumed); else throw error; } ``` ```typescript [TypeScript SDK - Webhook] import ReverseContact, { parseWebhookDelivery } from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const job = await client.contact.findPhone({ fullName: "Jane Doe", companyDomain: "acme.com", webhookUrl: "https://your-app.com/webhooks/reversecontact" }); console.log(job.status, job.id); // The route behind webhookUrl receives { webhookId, data }: answer within 10 seconds. async function handleWebhook(request: Request): Promise { const delivery = parseWebhookDelivery(await request.json()); console.log(delivery.webhookId, delivery.isError ? "failed" : "succeeded", delivery.data); return new Response(null, { status: 200 }); } ``` ```bash [cURL - Polling (recommended)] KEY="YOUR_API_KEY" ID=$(curl -sX POST https://api.reversecontact.com/v2/contact/phone \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"url":"https://www.social.com/in/janedoe"}' | jq -r .data.webhookId) while :; do S=$(curl -s https://api.reversecontact.com/v2/webhooks/$ID \ -H "Authorization: Bearer $KEY" | jq -r .data.status) [ "$S" = "succeeded" ] && break [ "$S" = "errored" ] && echo "job failed" && exit 1 sleep 2 done curl -s https://api.reversecontact.com/v2/webhooks/$ID \ -H "Authorization: Bearer $KEY" | jq .data.result ``` ```bash [cURL - Webhook] curl -X POST https://api.reversecontact.com/v2/contact/phone \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fullName": "Jane Doe", "companyDomain": "acme.com", "webhookUrl": "https://your-app.com/webhooks/reversecontact" }' ``` ```python [Python SDK - Polling (recommended)] import os from reversecontact import JobFailedError, ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.contact.find_phone(url="https://www.social.com/in/janedoe") print("Save this job id:", job.id) # resume after a restart with client.jobs.wait(id) try: # Long polls until the job ends, 30 minutes at most by default. done = job.wait() found = done.data.result if done.data and done.data.result else [] for phone in found: if phone is not None: print(phone.value, phone.type) except JobFailedError as error: print("Ended without a phone number:", error.error_code) ``` ```python [Python SDK - Webhook] import os from reversecontact import ReverseContact, parse_webhook_delivery with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: job = client.contact.find_phone( full_name="Jane Doe", company_domain="acme.com", webhook_url="https://your-app.com/webhooks/reversecontact", ) print(job.status, job.id) # The route behind webhook_url receives {webhookId, data}: answer within 10 seconds. def handle_webhook(body: bytes) -> None: delivery = parse_webhook_delivery(body) print(delivery.webhook_id, "failed" if delivery.is_error else "succeeded", delivery.data) ``` ## Example response **Example Response** ```json [Initial response] { "success": true, "data": { "status": "created", "webhookId": "b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4", "pollUrl": "/v2/webhooks/b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4" }, "error": null, "metadata": { "requestId": "0882d345-1902-47b3-b19c-98482adf9929", "executionTimeMs": 95 }, "quotas": { "creditsConsumed": 20, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4539, "left": 5461 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 10, "left": 50, "nextReset": "2025-01-20T14:16:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 10, "left": 50, "nextReset": "2025-01-20T14:16:00.000Z" } } } } ``` ```json [Webhook callback] { "webhookId": "b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4", "data": [ { "value": "+33612345678", "type": "mobile" } ] } ``` --- # All Lists > Retrieve all Lists in your workspace with their ids, member counts and Pending Intake counts, then use a List id to read, fill or export it. - Page: https://docs.reversecontact.com/endpoints/lists - Endpoint: `GET https://api.reversecontact.com/v2/lists` - Credits: Free. - Answer: synchronous. - Available in: REST API, MCP tool `list_lists`, TypeScript and Python SDKs. - Last verified: 2026-11-04 ## Introduction This endpoint returns all the Lists of your workspace, newest first, with how many members and how many [Pending Intake](https://docs.reversecontact.com/guides/lists#pending-intake) lines each one holds. Use it to find the `id` of a List before you read, fill or export it. This endpoint is **free** (0 credits). It counts toward your per-minute rate limit like any other call. A workspace holds at most 200 Lists (1 on the free trial), so one page of the default size returns them all. A workspace that has never had a List gets an empty page. The API key needs the `lists` permission (see [API key management](https://docs.reversecontact.com/guides/api-key-management)). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Query Parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------- | | `perPage` | integer | No | Lists per page, from 1 to 200. Defaults to 200. | | `cursor` | string | No | The `nextCursor` of the previous page. A malformed cursor, or one sent with other parameters, returns `400`. | ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (ListPage): One page of Lists | Field | Type | Description | | ------------ | -------------- | --------------------------------------------------------- | | `nextCursor` | string \| null | Send it back as `cursor` for the next page, `null` on the last page | **items** (List[]): The Lists of this page, newest first | Field | Type | Description | | -------------------- | -------------- | --------------------------------------------------------------------------- | | `id` | string | List id (UUID) | | `name` | string | List name | | `entity` | string | `persons` or `companies`: a List never mixes both | | `memberCount` | number | List Members | | `pendingIntakeCount` | number | Lines no one matched, kept as Pending Intake | | `revision` | number | Raised by every change to the members | | `archivedAt` | string \| null | Archive date (ISO 8601), `null` for an active List | | `createdAt` | string | Creation date (ISO 8601) | | `updatedAt` | string | Last update date (ISO 8601) | | `originQuery` | object \| null | The Search filters the List was built from in the app, `null` otherwise | | `originLastFind` | object \| null | The last run of those filters, `null` otherwise | `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). ## Code examples **Code Examples** ```bash [cURL] curl https://api.reversecontact.com/v2/lists \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript [JavaScript] const response = await fetch("https://api.reversecontact.com/v2/lists", { headers: { "Authorization": "Bearer YOUR_API_KEY" } }); const payload = await response.json(); for (const list of payload.data.items) { console.log(list.id, list.name, `${list.memberCount} members`); } ``` ```python [Python] import requests response = requests.get( "https://api.reversecontact.com/v2/lists", headers={"Authorization": "Bearer YOUR_API_KEY"} ) for item in response.json()["data"]["items"]: print(item["id"], item["name"], f"{item['memberCount']} members") ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "items": [ { "id": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40", "name": "Q4 sales leaders", "entity": "persons", "memberCount": 1240, "pendingIntakeCount": 18, "revision": 7, "archivedAt": null, "createdAt": "2026-10-20T09:12:44.000Z", "updatedAt": "2026-10-27T16:03:10.000Z", "originQuery": null, "originLastFind": null } ], "nextCursor": null }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "014e621a-ecb1-4849-8afe-3f4f7cd7514d", "executionTimeMs": 38 } } ``` --- # Create a List > Create an empty List of people or companies in your workspace for free, and use an Idempotency-Key header to retry without creating duplicates. - Page: https://docs.reversecontact.com/endpoints/list-create - Endpoint: `POST https://api.reversecontact.com/v2/lists` - Credits: Free. - Answer: synchronous. - Available in: REST API, MCP tool `create_list`, TypeScript and Python SDKs. - Last verified: 2026-11-04 ## Introduction The Create a List endpoint creates an empty List of people or of companies. A List never mixes both: its `entity` decides which Signals it accepts and which data families it can hold. Fill it afterwards with [Add members](https://docs.reversecontact.com/endpoints/list-members-add) or [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals). Creating a List is **free** (0 credits). A workspace holds at most 200 Lists, and 1 on the free trial: past that, the request returns `409 LIST_LIMIT_REACHED`. When the name is already taken, the List is created as `Name (2)`. The first List of a workspace also sets up where its Lists are stored. While that runs, the request returns `503 LISTS_PREPARING`, with `error.details.retryAfterSeconds`: send the same request again a few seconds later, with the same `Idempotency-Key`. The API key needs the `lists` permission (see [API key management](https://docs.reversecontact.com/guides/api-key-management)). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body | Name | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------ | | `name` | string | Yes | List name, 1 to 200 characters. Spaces around it are removed. | | `entity` | string | Yes | `persons` for people, `companies` for companies | ### Retry safely with Idempotency-Key Send an `Idempotency-Key` header (1 to 200 printable ASCII characters, no spaces) to make the creation safe to retry: a second request with the same key returns the List the first one created, with `201`, instead of creating `Name (2)`. Reusing the key for the other `entity` returns `409 IDEMPOTENCY_KEY_REUSED`. ## Response structure The response has status `201`. | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the List was created | | `error` | `null` | Always `null` on success | **data** (List): The List created, or the List this Idempotency-Key created | Field | Type | Description | | -------------------- | -------------- | ---------------------------------------------------- | | `id` | string | List id (UUID): keep it to fill and read the List | | `name` | string | List name, suffixed when the name was already taken | | `entity` | string | `persons` or `companies` | | `memberCount` | number | List Members, `0` for a new List | | `pendingIntakeCount` | number | Pending Intake lines, `0` for a new List | | `revision` | number | Raised by every change to the members | | `archivedAt` | `null` | `null` for an active List | | `createdAt` | string | Creation date (ISO 8601) | | `updatedAt` | string | Last update date (ISO 8601) | | `originQuery` | `null` | `null` for a List created through the API | | `originLastFind` | `null` | `null` for a List created through the API | `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). ## Errors | Status | Code | Meaning | | ------ | ------------------------ | ---------------------------------------------------------------- | | `400` | `LIST_INVALID_INPUT` | The `Idempotency-Key` header is malformed, or `name` holds only spaces | | `409` | `LIST_LIMIT_REACHED` | The workspace already has 200 Lists (1 on the free trial) | | `409` | `IDEMPOTENCY_KEY_REUSED` | The key already created a List of the other `entity` | | `422` | `VALIDATION_ERROR` | `name` is missing or too long, or `entity` is not a known value | | `503` | `LISTS_PREPARING` | The first List of the workspace is being set up: retry in a few seconds | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/lists \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: crm-sync-2026-10-28" \ -d '{ "name": "Q4 sales leaders", "entity": "persons" }' ``` ```javascript [JavaScript] const response = await fetch("https://api.reversecontact.com/v2/lists", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json", "Idempotency-Key": "crm-sync-2026-10-28" }, body: JSON.stringify({ name: "Q4 sales leaders", entity: "persons" }) }); const { data: list } = await response.json(); console.log("List created:", list.id); ``` ```python [Python] import requests response = requests.post( "https://api.reversecontact.com/v2/lists", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json", "Idempotency-Key": "crm-sync-2026-10-28" }, json={"name": "Q4 sales leaders", "entity": "persons"} ) print("List created:", response.json()["data"]["id"]) ``` ## Example response **Example Response** ```json [201 - Created] { "success": true, "data": { "id": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40", "name": "Q4 sales leaders", "entity": "persons", "memberCount": 0, "pendingIntakeCount": 0, "revision": 1, "archivedAt": null, "createdAt": "2026-10-28T09:12:44.000Z", "updatedAt": "2026-10-28T09:12:44.000Z", "originQuery": null, "originLastFind": null }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "b4777f52-42fd-44e7-abc5-19955b65c753", "executionTimeMs": 52 } } ``` ```json [409 - Limit reached] { "success": false, "data": null, "error": { "code": "LIST_LIMIT_REACHED", "message": "This workspace already has 200 lists" }, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "3f594ff6-5425-4f46-b2b9-8c65acebd9f0", "executionTimeMs": 21 } } ``` --- # Get a List > Retrieve a List with its member and Pending Intake counts and a page of stored rows, filtered by kind, with cursor pagination at no credit cost. - Page: https://docs.reversecontact.com/endpoints/list - Endpoint: `GET https://api.reversecontact.com/v2/lists/:listId` - Credits: Free. - Answer: synchronous. - Available in: REST API, MCP tool `get_list`, TypeScript and Python SDKs. - Last verified: 2026-11-04 ## Introduction The Get a List endpoint returns one List and one page of its rows, ordered by the date they were added: the List Members (`kind: "member"`) and the lines no one matched, kept as [Pending Intake](https://docs.reversecontact.com/guides/lists#pending-intake) (`kind: "pending"`). It reads what the List already holds and never looks anyone up. This endpoint is **free** (0 credits). It counts toward your per-minute rate limit like any other call. To page through the rows without the List header, or to find members by your own identifier, use [List members](https://docs.reversecontact.com/endpoints/list-members). The API key needs the `lists` permission (see [API key management](https://docs.reversecontact.com/guides/api-key-management)). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request The route is `GET /v2/lists/:listId`. ### Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | -------------- | | `listId` | string | Yes | List id (UUID) | ### Query Parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------- | | `kind` | string | No | `all` (default) mixes members and Pending Intake by added date, `members` or `pending` returns one kind only | | `perPage` | integer | No | Rows per page, from 1 to 1,000. Defaults to 100. | | `cursor` | string | No | The `nextCursor` of the previous page, sent with the same `kind`. A malformed cursor, or one sent with other parameters, returns `400`. | ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (ListDetail): The List and one page of its rows | Field | Type | Description | | ------------ | -------------- | -------------------------------------------------------------------- | | `list` | object | The List, with the fields of [All Lists](https://docs.reversecontact.com/endpoints/lists#response-structure) | | `members` | array | One page of rows: members (`kind: "member"`) and Pending Intake (`kind: "pending"`). Each row is described in [List members](https://docs.reversecontact.com/endpoints/list-members#response-structure). | | `nextCursor` | string \| null | Send it back as `cursor` for the next page, `null` on the last page | `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). A List that does not exist in your workspace returns `404 LIST_NOT_FOUND`, or `404 LISTS_NOT_ACTIVATED` when the workspace has never had a List. ## Code examples **Code Examples** ```bash [cURL] curl "https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40?perPage=50" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript [JavaScript] const listId = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40"; const response = await fetch( `https://api.reversecontact.com/v2/lists/${listId}?perPage=50`, { headers: { "Authorization": "Bearer YOUR_API_KEY" } } ); const { data } = await response.json(); console.log(data.list.name, `${data.list.memberCount} members`); console.log("Rows on this page:", data.members.length); ``` ```python [Python] import requests list_id = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40" response = requests.get( f"https://api.reversecontact.com/v2/lists/{list_id}", headers={"Authorization": "Bearer YOUR_API_KEY"}, params={"perPage": 50} ) data = response.json()["data"] print(data["list"]["name"], f"{data['list']['memberCount']} members") ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "list": { "id": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40", "name": "Q4 sales leaders", "entity": "persons", "memberCount": 2, "pendingIntakeCount": 1, "revision": 3, "archivedAt": null, "createdAt": "2026-10-28T09:12:44.000Z", "updatedAt": "2026-10-28T09:20:02.000Z", "originQuery": null, "originLastFind": null }, "members": [ { "kind": "member", "id": "9b1e4c7a-2d3f-4e8a-b6c5-0a1f2e3d4c5b", "entityId": "prs_01jbd5vewyenebxgaz869ffe5t", "linkedinUrl": "https://www.social.com/in/janedoe", "status": "enriched", "fields": { "name": "Jane Doe", "headline": "VP Sales at Acme Corp", "company": "Acme Corp", "location": { "country": "France" }, "url": "https://www.social.com/in/janedoe", "status": "enriched" }, "addedAt": "2026-10-28T09:15:31.000Z", "families": { "profile": { "enrichedAt": "2026-10-28T09:15:31.000Z" } }, "externalId": "crm-001" }, { "kind": "pending", "id": "7e8f9a0b-1c2d-4e3f-8a4b-5c6d7e8f9a0b", "signal": { "email": "unknown.person@example.com" }, "source": "paste", "addedAt": "2026-10-28T09:15:32.000Z", "fields": {} } ], "nextCursor": null }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "2313f06e-20b2-4452-8f83-5987f4c420c8", "executionTimeMs": 44 } } ``` --- # Delete a List > Permanently delete a List, its members, stored data and Pending Intake for free, and handle LIST_BUSY while imports or searches still run. - Page: https://docs.reversecontact.com/endpoints/list-delete - Endpoint: `DELETE https://api.reversecontact.com/v2/lists/:listId` - Credits: Free. - Answer: synchronous. - Available in: REST API, MCP tool `delete_list`, TypeScript and Python SDKs. - Last verified: 2026-11-04 ## Introduction The Delete a List endpoint deletes a List for good: its members, the data the List stored for them and its [Pending Intake](https://docs.reversecontact.com/guides/lists#pending-intake). Export the List first with [Export a List](https://docs.reversecontact.com/endpoints/list-export) if you need to keep a copy. To take some members out and keep the List, use [Remove members](https://docs.reversecontact.com/endpoints/list-members-remove). Deleting a List is **free** (0 credits). While an import is queued, running or paused on the List, while the lines of a cancelled import still wait for their result, or while a Search of the app is still adding people to it, the request returns `409 LIST_BUSY`: wait for the import to end, or [cancel it](https://docs.reversecontact.com/endpoints/list-operation-cancel) and wait for its last results, then delete again. The API key needs the `lists` permission (see [API key management](https://docs.reversecontact.com/guides/api-key-management)). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request The route is `DELETE /v2/lists/:listId`. ### Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------------------- | | `listId` | string | Yes | Id (UUID) of the List to delete | The request has no body. ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the List was deleted | | `error` | `null` | Always `null` on success | **data** (ListDeleted): Deletion acknowledgement | Field | Type | Description | | --------- | ------- | ------------ | | `deleted` | boolean | Always `true` | `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). ## Errors | Status | Code | Meaning | | ------ | ---------------- | -------------------------------------------------------- | | `404` | `LIST_NOT_FOUND` | No List with this id in your workspace | | `404` | `LISTS_NOT_ACTIVATED` | The workspace has never had a List | | `409` | `LIST_BUSY` | An operation is queued, running or paused on the List, or its last results are still on their way | ## Code examples **Code Examples** ```bash [cURL] curl -X DELETE https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40 \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript [JavaScript] const listId = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40"; const response = await fetch(`https://api.reversecontact.com/v2/lists/${listId}`, { method: "DELETE", headers: { "Authorization": "Bearer YOUR_API_KEY" } }); console.log("Deleted:", (await response.json()).data.deleted); ``` ```python [Python] import requests list_id = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40" response = requests.delete( f"https://api.reversecontact.com/v2/lists/{list_id}", headers={"Authorization": "Bearer YOUR_API_KEY"} ) print("Deleted:", response.json()["data"]["deleted"]) ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "deleted": true }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "929ada6c-7ea9-4808-a45f-71480493965e", "executionTimeMs": 61 } } ``` --- # Export a List > Export List members and Pending Intake as CSV for free, including stored profile, company, email and phone data without new lookups. - Page: https://docs.reversecontact.com/endpoints/list-export - Endpoint: `GET https://api.reversecontact.com/v2/lists/:listId/export` - Credits: Free. - Answer: synchronous. - Available in: REST API, MCP tool `export_list`, TypeScript and Python SDKs, CLI command `rc lists export`. - Last verified: 2026-11-04 ## Introduction The Export a List endpoint downloads a List as a CSV file: the List Members, with the data the List already holds for each of them (profile fields, company, and the email and phone found by an [import](https://docs.reversecontact.com/endpoints/list-intake-signals) with the `contact_email` or `contact_phone` family), and the [Pending Intake](https://docs.reversecontact.com/guides/lists#pending-intake) lines, with what was sent for them. Exporting is **free** (0 credits): the file holds what the List stored, and nothing is looked up again. It counts toward your per-minute rate limit like any other call. The API key needs the `lists` permission (see [API key management](https://docs.reversecontact.com/guides/api-key-management)). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request The route is `GET /v2/lists/:listId/export`. ### Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | -------------- | | `listId` | string | Yes | List id (UUID) | ## Response structure A successful response is the CSV file itself (`Content-Type: text/csv; charset=utf-8`), not the JSON envelope: `Content-Disposition` names it `list--.csv`, where `` is the List name in lowercase, without accents, with a dash in place of spaces and signs (`list-q4-sales-leaders-persons.csv`). The file has one header row, then one row per line of the List, members and Pending Intake mixed, in the order they were added. A value your team typed over a cell in the app replaces the enriched one, and an email or phone number typed in the app reads `found` with the source `manual`. A value that starts with `=`, `+`, `-`, `@`, a tab or a carriage return is prefixed with `'`, so a spreadsheet never reads it as a formula. Columns you added to the List in the app follow the default columns, headed by their name. A Pending Intake row holds what was sent for it: `linkedin_url` is the URL of the Signal, `name` its first and last name (or else its company name, email or domain), `currentCompanyName` its company name. Its contact columns read `not_attempted`, unless your team typed a contact over the line in the app. A people List has these columns: | Column | Content | | ------------------------------ | ----------------------------------------------------------------------- | | `linkedin_url` | Profile URL of the member | | `name` | Full name | | `headline` | Profile headline | | `location` | Location, as text | | `currentCompanyName` | Current company | | `currentCompanyLinkedInUrl` | Company page URL of the current company | | `contact_email_status` | `found`, `not_found`, `error` or `not_attempted` | | `contact_email` | Email found, empty otherwise | | `contact_email_source` | Where the email came from, when known | | `contact_email_attempted_at` | When the email was looked for (ISO 8601) | | `contact_email_freshness_at` | Date of the email data (ISO 8601) | | `contact_phone_status` | `found`, `not_found`, `error` or `not_attempted` | | `contact_phone` | Phone number found, empty otherwise | | `contact_phone_type` | Kind of number when the provider gives one, such as `mobile` | | `contact_phone_source` | Where the number came from, when known | | `contact_phone_attempted_at` | When the number was looked for (ISO 8601) | | `contact_phone_freshness_at` | Date of the phone data (ISO 8601) | | `currentCompanyWebsite` | Website of the current company | A companies List has these columns: `linkedin_url`, `name`, `industry`, `location`, `website`, `employeeCount`, `foundedOn`, `phone` and `description`. An error returns the usual JSON envelope, with `quotas` and `metadata` (see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure)): `404 LIST_NOT_FOUND` when no List has this id in your workspace, or `404 LISTS_NOT_ACTIVATED` when the workspace has never had a List. The `200` status is sent with the first bytes of the file. An error during the download, or the 30-minute limit, ends the file early with that same `200`: check that it holds one row per line of the List, the `memberCount` plus the `pendingIntakeCount` of [Get a List](https://docs.reversecontact.com/endpoints/list), and export it again when rows are missing. A workspace exports one List at a time. While a file is downloading, a second export returns `429 LIST_EXPORT_IN_PROGRESS`, whichever API key asks: wait for the first to finish, then retry. `429 LIST_EXPORT_BUSY` means the server is serving another export, and a retry usually gets through within seconds. Neither costs a credit. `error.details.retryAfter` and the `Retry-After` header give the seconds to wait. A download that stops reading is closed after 75 seconds, and one export runs for 30 minutes at most. ## Code examples **Code Examples** ```bash [cURL] curl https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/export \ -H "Authorization: Bearer YOUR_API_KEY" \ -o q4-sales-leaders.csv ``` ```javascript [JavaScript] import { writeFile } from "node:fs/promises"; const listId = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40"; const response = await fetch( `https://api.reversecontact.com/v2/lists/${listId}/export`, { headers: { "Authorization": "Bearer YOUR_API_KEY" } } ); if (!response.ok) throw new Error((await response.json()).error.message); await writeFile("q4-sales-leaders.csv", await response.text()); ``` ```python [Python] import requests list_id = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40" response = requests.get( f"https://api.reversecontact.com/v2/lists/{list_id}/export", headers={"Authorization": "Bearer YOUR_API_KEY"} ) response.raise_for_status() with open("q4-sales-leaders.csv", "wb") as file: file.write(response.content) ``` ## Example response The second row of this file is a Pending Intake line: an email no one matched. **Example Response** ```text [200 - CSV file] linkedin_url,name,headline,location,currentCompanyName,currentCompanyLinkedInUrl,contact_email_status,contact_email,contact_email_source,contact_email_attempted_at,contact_email_freshness_at,contact_phone_status,contact_phone,contact_phone_type,contact_phone_source,contact_phone_attempted_at,contact_phone_freshness_at,currentCompanyWebsite https://www.social.com/in/janedoe,Jane Doe,VP Sales at Acme Corp,"Paris, France",Acme Corp,https://www.social.com/company/acme-corp,found,jane.doe@acme.com,,2026-10-28T09:16:02.000Z,2026-10-28T09:16:02.000Z,not_attempted,,,,,,https://acme.com ,unknown.person@example.com,,,,,not_attempted,,,,,not_attempted,,,,,, ``` ```json [429 - Export already running] { "success": false, "data": null, "error": { "code": "LIST_EXPORT_IN_PROGRESS", "message": "This workspace is already exporting a List. Wait for it to finish, then retry.", "details": { "retryAfter": 30 } }, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "d86ecf64-fcc2-489b-9747-1ad0fa9630b2", "executionTimeMs": 4 } } ``` ```json [404 - List not found] { "success": false, "data": null, "error": { "code": "LIST_NOT_FOUND", "message": "List not found" }, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "bd792a47-a27b-40b1-b587-5ed509ce36bd", "executionTimeMs": 18 } } ``` --- # List members > Read List members and Pending Intake with cursor pagination, or find members by externalId to match CRM records, without spending credits. - Page: https://docs.reversecontact.com/endpoints/list-members - Endpoint: `GET https://api.reversecontact.com/v2/lists/:listId/members` - Credits: Free. - Answer: synchronous. - Available in: REST API, MCP tool `get_list`, TypeScript and Python SDKs, CLI command `rc lists read`. - Last verified: 2026-11-04 ## Introduction The List members endpoint returns one page of the rows of a List: the List Members (`kind: "member"`), and the lines no one matched, kept as [Pending Intake](https://docs.reversecontact.com/guides/lists#pending-intake) (`kind: "pending"`). With `externalId`, it returns the members that carry that identifier of yours, so you can find the member behind a line of your CRM without keeping our ids. This endpoint is **free** (0 credits). It reads what the List holds and never looks anyone up. It counts toward your per-minute rate limit like any other call. The API key needs the `lists` permission (see [API key management](https://docs.reversecontact.com/guides/api-key-management)). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request The route is `GET /v2/lists/:listId/members`. ### Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | -------------- | | `listId` | string | Yes | List id (UUID) | ### Query Parameters | Name | Type | Required | Description | | ------------ | ------- | -------- | --------------------------------------------------------------------------------------------------------------- | | `kind` | string | No | `all` (default) mixes members and Pending Intake by added date, `members` or `pending` returns one kind only | | `externalId` | string | No | Returns the members carrying this identifier of yours (1 to 200 characters) | | `perPage` | integer | No | Rows per page, from 1 to 1,000. Defaults to 100. | | `cursor` | string | No | The `nextCursor` of the previous page, sent with the same filters. A malformed cursor, or one sent with other filters, returns `400`. | ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (ListRows): One page of rows | Field | Type | Description | | -------------------- | -------------- | ------------------------------------------------------------------- | | `memberCount` | number | List Members in the whole List | | `pendingIntakeCount` | number | Pending Intake lines in the whole List | | `nextCursor` | string \| null | Send it back as `cursor` for the next page, `null` on the last page | **items** (Member): A row with `kind: "member"`: a person or company the List holds | Field | Type | Description | | ------------- | -------------- | --------------------------------------------------------------------------------------------------- | | `kind` | string | `member` | | `id` | string | Member id (UUID), the one [Remove members](https://docs.reversecontact.com/endpoints/list-members-remove) takes | | `entityId` | string | Person (`prs_...`) or company (`com_...`) id, the one the other endpoints return | | `linkedinUrl` | string \| null | Profile or company page URL | | `status` | string | `enriched` when the List holds the member's profile or company details, which is the case of every member added from a Signal; `profiled` or `identified` for a member added from a Search in the app, with or without a stored profile | | `fields` | object | The fields the List shows for the member: `name`, `headline`, `company`, `location`, `url`, `photo` for a person; `name`, `industry`, `location`, `url`, `websiteUrl`, `logo` for a company. `enrichment` holds the enriched fields once a family was bought. A value your team typed over a cell in the app replaces the enriched one. | | `addedAt` | string | Date the member was added (ISO 8601) | | `families` | object | What the List holds for each data family, see below | | `externalId` | string \| null | Your identifier for this member, `null` when none was sent. A member keeps the first one it received. | **families** (Families): Data families, absent until a family was bought or looked for | Field | Type | Description | | -------------- | ------ | ----------------------------------------------------------------------------------------------------- | | `profile` | object | `enrichedAt`: when the profile was obtained, by the lookup that added the member or by the `profile` family (ISO 8601). `dataAt`, when present: the date of the data itself. | | `company` | object | `enrichedAt`: when the company details were obtained. `dataAt`, when present: the date of the data itself. | | `contactEmail` | object | `status`: `found`, `not_found`, `error` or `not_attempted`. With `found`, `value` is the email. `source`, when known: where it came from, `signal:paste` or `signal:file` for the email of an email Signal, `manual` for an email typed in the app. `attemptedAt`: when it was looked for. `freshnessAt`, when present: the date of the data. | | `contactPhone` | object | `status`: `found`, `not_found`, `error` or `not_attempted`. With `found`, `value` is the number and `type` its kind when the provider gives one. `source`, `attemptedAt` and `freshnessAt` as for `contactEmail`. | **items** (PendingIntake): A row with `kind: "pending"`: a line no one matched | Field | Type | Description | | --------- | ------ | -------------------------------------------------------- | | `kind` | string | `pending` | | `id` | string | Pending Intake id (UUID), the one `pendingIds` takes | | `signal` | object | The Signal as you sent it, with its `externalId` when you sent one | | `source` | string | `paste` or `file` | | `addedAt` | string | Date the line was kept (ISO 8601) | | `fields` | object | Values your team typed over the line in the app, by column (`name`, `company`, `url`, `contactEmail`...), `{}` otherwise | `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). A List that does not exist in your workspace returns `404 LIST_NOT_FOUND`, or `404 LISTS_NOT_ACTIVATED` when the workspace has never had a List. ## Code examples **Code Examples** ```bash [cURL] curl "https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/members?kind=members&perPage=500" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript [JavaScript] // Read every member of a List, one page at a time. const listId = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40"; const members = []; let cursor = null; do { const url = new URL(`https://api.reversecontact.com/v2/lists/${listId}/members`); url.searchParams.set("kind", "members"); url.searchParams.set("perPage", "500"); if (cursor) url.searchParams.set("cursor", cursor); const response = await fetch(url, { headers: { "Authorization": "Bearer YOUR_API_KEY" } }); const { data } = await response.json(); members.push(...data.items); cursor = data.nextCursor; } while (cursor); console.log(`${members.length} members read`); ``` ```python [Python] import requests # Find the member behind a line of your CRM. list_id = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40" response = requests.get( f"https://api.reversecontact.com/v2/lists/{list_id}/members", headers={"Authorization": "Bearer YOUR_API_KEY"}, params={"externalId": "crm-001"} ) for member in response.json()["data"]["items"]: print(member["id"], member["fields"].get("name")) ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "items": [ { "kind": "member", "id": "9b1e4c7a-2d3f-4e8a-b6c5-0a1f2e3d4c5b", "entityId": "prs_01jbd5vewyenebxgaz869ffe5t", "linkedinUrl": "https://www.social.com/in/janedoe", "status": "enriched", "fields": { "name": "Jane Doe", "headline": "VP Sales at Acme Corp", "company": "Acme Corp", "location": { "country": "France" }, "url": "https://www.social.com/in/janedoe", "status": "enriched" }, "addedAt": "2026-10-28T09:15:31.000Z", "families": { "profile": { "enrichedAt": "2026-10-28T09:15:31.000Z" }, "contactEmail": { "status": "found", "value": "jane.doe@acme.com", "attemptedAt": "2026-10-28T09:16:02.000Z" } }, "externalId": "crm-001" } ], "memberCount": 1240, "pendingIntakeCount": 18, "nextCursor": "eyJ2IjoxLCJhZnRlciI6IjIwMjYtMTAtMjhUMDk6MTU6MzEifQ" }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "74ee948b-32cf-43b5-a795-411c6f4fada0", "executionTimeMs": 47 } } ``` --- # Add members > Add up to 1,000 people or companies to a List from Social URLs, emails, names or domains, with one result per Signal. - Page: https://docs.reversecontact.com/endpoints/list-members-add - Endpoint: `POST https://api.reversecontact.com/v2/lists/:listId/members` - Credits: Free to call. Each new person costs 1 credit from a Social profile URL (2 with live), 2 from an email or a name. Each new company costs 1 credit from a company page URL or a company id (2 when the page is read live), 2 from a domain. A member already on the List, or a Signal no one matches, is free. - Answer: synchronous. - Available in: REST API, MCP tool `add_list_members`, TypeScript and Python SDKs, CLI command `rc lists add`. - Last verified: 2026-11-04 ## Introduction The Add members endpoint turns up to 1,000 Signals into List Members and answers once they are all done. A Signal is what you know about someone: a Social profile URL, an email, or a first and last name with a company for a person; a company page URL, a domain or a company id for a company. The response holds one result per Signal, in the order you sent them, so you can match each line of your file to the member it became. A Signal no one matches is kept on the List as [Pending Intake](https://docs.reversecontact.com/guides/lists#pending-intake), free. When a Signal sent again finds someone, the new member replaces its Pending Intake line. A person or company already on the List is left as it is, free, with one exception: the email of an email Signal becomes the member's work email (`families.contactEmail`, `source: "signal:paste"` or `"signal:file"`) when the List has not found one yet, for free. This holds for a new member too, so a later email lookup never buys back an address you gave. To add more than 1,000 Signals, to buy data families (profile, company, email, phone) for each member, to set a credit ceiling or to be told by webhook, use [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals) instead. ## Credits The call itself is **free**: only the new members are charged, at the price of the Signal that found them. | Signal | People List | Companies List | | ----------------------------------------- | ------------------------------ | -------------- | | Social URL, from saved data | 1 credit | 1 credit, 2 when the company page is read live | | Social URL with `live: true` | 2 credits | As without `live`: `live` has no effect on companies | | Email, or first and last name with a company | 2 credits | Refused | | Company id (`com_...`) | Refused | 1 credit, 2 when the company page is read live | | Domain alone | Refused | 2 credits | | Already on the List, or matched by no one | Free | Free | A Signal of the other kind (a company on a people List, a person on a companies List) refuses the request with `400 LIST_ENTITY_MISMATCH`. Before anything is added, the API checks that your balance covers what the batch could charge: every Signal at its price, except a Social URL already on the List or repeated in the batch (an email, a name, a domain or a company id is always counted, and a company page URL or id at 1 credit). Below that, the request returns `402` and nothing is charged. Each new member is then charged before it is written. A person or company that asked us to stop processing their data is refused (`data_subject_blocked`): nothing is kept or charged for it. `data.creditsConsumed` and `quotas.creditsConsumed` both give what the call charged. The API key needs the `lists` permission, plus `persons:enrich` for a people List or `companies:enrich` for a companies List (see [API key management](https://docs.reversecontact.com/guides/api-key-management)). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body The List is named in the path: `POST /v2/lists/:listId/members`. | Name | Type | Required | Description | | --------- | ------- | -------- | ------------------------------------------------------------------------------------------------- | | `signals` | array | Yes | 1 to 1,000 Signals, described below | | `live` | boolean | No | `true` fetches the profile of a Social profile URL live (2 credits). Omitted or `false`, saved data is used (1 credit). No effect on a companies List. | | `source` | string | No | `paste` (default) or `file`: how a Pending Intake line says it arrived | Each Signal is an object with the fields that identify one person or one company: | Field | Type | Description | | --------------- | ------ | ------------------------------------------------------------------------------------------------- | | `linkedinUrl` | string | Profile URL (people List) or company page URL (companies List). `url` is accepted as an alias. | | `email` | string | Email of the person | | `firstName` | string | First name, with `lastName` and `companyDomain` or `companyName` | | `lastName` | string | Last name, with `firstName` and `companyDomain` or `companyName` | | `companyDomain` | string | For a person, the domain of their company. On a companies List, the company itself. | | `companyName` | string | Company name, a hint for a person found by name | | `domain` | string | Company domain, on a companies List | | `companyId` | string | Company id (`com_...`) returned by our API, on a companies List | | `externalId` | string | Your own identifier for the line, 1 to 200 characters. It is echoed in the line result and kept on the member, so [List members](https://docs.reversecontact.com/endpoints/list-members) can find it. A member keeps the first one it receives. It is never used to find anyone. | One invalid Signal refuses the whole request before anything is charged, and `error.details.signalIndex` gives its position. ## Response structure The request answers once every Signal has been handled. It runs within a 62-second time limit: past it, it returns `504`, and the members already added stay added and charged. Read the List to see where it stopped, or send large batches through the [import](https://docs.reversecontact.com/endpoints/list-intake-signals). | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed | | `error` | `null` | Always `null` on success | **data** (AddMembers): What became of the Signals | Field | Type | Description | | ----------------- | ------ | --------------------------------------------------------------------------------------------------- | | `inserted` | number | Members added | | `skipped` | number | Signals naming a member already on the List | | `refused` | number | Signals refused for the member limit, an ambiguous identity or `data_subject_blocked`. A line stopped by credits or by the Pending Intake limit reads `refused` in `results` but is not counted here. | | `unresolvedCount` | number | Signals no one matched, kept as Pending Intake | | `creditsConsumed` | number | Credits charged by this call | | `stoppedReason` | string | Present when a stop left later Signals unread: `quota` (credits), `member_cap` or `pending_cap` (List limits) | **results** (SignalResult[]): One entry per Signal, in request order | Field | Type | Description | | ------------ | ------ | ------------------------------------------------------------------------------------------------- | | `index` | number | Position of the Signal in the request, from 0 | | `outcome` | string | `added`, `already_present`, `not_found`, `refused`, `failed` or `not_reached` | | `externalId` | string | Your identifier, as sent | | `memberId` | string | The member the line names, for `added` and `already_present` | | `entityId` | string | Person (`prs_...`) or company (`com_...`) id | | `pendingId` | string | The Pending Intake line kept for a `not_found` Signal | | `reason` | string | Why a line was refused, failed or not reached: `member_limit`, `pending_intake_limit`, `insufficient_credits`, `ambiguous_identity`, `data_subject_blocked`, `identity_unavailable`, `quota_unavailable` or `stopped` | `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). ## Errors | Status | Code | Meaning | | ------ | ----------------------------------- | -------------------------------------------------------------------------- | | `400` | `LIST_INVALID_INPUT` | A Signal cannot identify anyone, see `error.details.signalIndex` | | `400` | `LIST_ENTITY_MISMATCH` | A Signal names a company on a people List, or a person on a companies List | | `402` | `INSUFFICIENT_CREDITS` | The balance cannot pay for what the batch could charge | | `403` | `INSUFFICIENT_PERMISSIONS` | The key lacks `lists`, or the enrich permission of the List's entity | | `404` | `LIST_NOT_FOUND` | No List with this id in your workspace | | `404` | `LISTS_NOT_ACTIVATED` | The workspace has never had a List: create one first | | `409` | `LIST_MEMBER_LIMIT_REACHED` | A request with a single Signal, on a List that already holds 25,000 members (50 on the free trial) | | `409` | `LIST_PENDING_INTAKE_LIMIT_REACHED` | A request with a single Signal no one matches, on a List that already holds 25,000 Pending Intake lines (50 on the free trial) | | `504` | `GATEWAY_TIMEOUT` | The request ran out of time after 62 seconds: the Signals already added stay added | With several Signals, a full List does not refuse the request: it answers `200`, the line that met the limit reads `refused` (`member_limit` or `pending_intake_limit`), the next ones read `not_reached`, and `stoppedReason` is `member_cap` or `pending_cap`. ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/members \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "signals": [ { "linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001" }, { "email": "john.smith@acme.com", "externalId": "crm-002" }, { "firstName": "Ana", "lastName": "Lopez", "companyDomain": "northwind.com", "externalId": "crm-003" } ] }' ``` ```javascript [JavaScript] const listId = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40"; const response = await fetch(`https://api.reversecontact.com/v2/lists/${listId}/members`, { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ signals: [ { linkedinUrl: "https://www.social.com/in/janedoe", externalId: "crm-001" }, { email: "john.smith@acme.com", externalId: "crm-002" }, { firstName: "Ana", lastName: "Lopez", companyDomain: "northwind.com", externalId: "crm-003" } ] }) }); const { data } = await response.json(); for (const result of data.results) { console.log(result.externalId, result.outcome, result.memberId ?? result.pendingId); } console.log("Credits charged:", data.creditsConsumed); ``` ```python [Python] import requests list_id = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40" response = requests.post( f"https://api.reversecontact.com/v2/lists/{list_id}/members", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "signals": [ {"linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001"}, {"email": "john.smith@acme.com", "externalId": "crm-002"}, {"firstName": "Ana", "lastName": "Lopez", "companyDomain": "northwind.com", "externalId": "crm-003"} ] } ) data = response.json()["data"] for result in data["results"]: print(result.get("externalId"), result["outcome"]) print("Credits charged:", data["creditsConsumed"]) ``` ## Example response In this example, the Social profile URL adds a new member (1 credit), the email names a person already on the List (free), and no one matches the name (free, kept as Pending Intake). **Example Response** ```json [200 - Success] { "success": true, "data": { "inserted": 1, "skipped": 1, "refused": 0, "unresolvedCount": 1, "creditsConsumed": 1, "results": [ { "index": 0, "outcome": "added", "externalId": "crm-001", "memberId": "9b1e4c7a-2d3f-4e8a-b6c5-0a1f2e3d4c5b", "entityId": "prs_01jbd5vewyenebxgaz869ffe5t" }, { "index": 1, "outcome": "already_present", "externalId": "crm-002", "memberId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "entityId": "prs_01h2x3y4z5a6b7c8d9e0f1g2h3" }, { "index": 2, "outcome": "not_found", "externalId": "crm-003", "pendingId": "7e8f9a0b-1c2d-4e3f-8a4b-5c6d7e8f9a0b" } ] }, "error": null, "quotas": { "creditsConsumed": 1, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4522, "left": 5478 } } }, "metadata": { "requestId": "73e35798-7fd6-4c5f-8545-5f351b795b76", "executionTimeMs": 2140 } } ``` --- # Remove members > Remove List members and their stored data, or dismiss Pending Intake lines by id, at no credit cost. Previous charges are not refunded. - Page: https://docs.reversecontact.com/endpoints/list-members-remove - Endpoint: `DELETE https://api.reversecontact.com/v2/lists/:listId/members` - Credits: Free. - Answer: synchronous. - Available in: REST API, MCP tool `remove_list_members`, TypeScript and Python SDKs. - Last verified: 2026-11-04 ## Introduction The Remove members endpoint takes List Members out of a List, with the data the List stored for them, and dismisses [Pending Intake](https://docs.reversecontact.com/guides/lists#pending-intake) lines. Removing members never dismisses Pending Intake: send the `pendingIds` of the lines you want gone. Removing is **free** (0 credits), and nothing is refunded: credits spent on a member stay spent. A person removed then added again is charged again at the price of its Signal. The API key needs the `lists` permission (see [API key management](https://docs.reversecontact.com/guides/api-key-management)). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body The List is named in the path: `DELETE /v2/lists/:listId/members`. Send at least one id in either array, and at most 25,000 ids in both together. | Name | Type | Required | Description | | ------------ | -------- | ----------- | ----------------------------------------------------------------------------------- | | `memberIds` | string[] | Conditional | Member ids (UUID) to remove, the `id` of rows with `kind: "member"` | | `pendingIds` | string[] | Conditional | Pending Intake ids (UUID) to dismiss, the `id` of rows with `kind: "pending"` | ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed | | `error` | `null` | Always `null` on success | **data** (RemoveMembers): What was removed | Field | Type | Description | | ----------- | ------ | ------------------------------- | | `removed` | number | Members removed | | `dismissed` | number | Pending Intake lines dismissed | An id that is not on the List is left out of the counts. `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). ## Errors | Status | Code | Meaning | | ------ | -------------------- | --------------------------------------------- | | `400` | `LIST_INVALID_INPUT` | Neither `memberIds` nor `pendingIds` holds an id, or they hold more than 25,000 ids together | | `404` | `LIST_NOT_FOUND` | No List with this id in your workspace | | `404` | `LISTS_NOT_ACTIVATED` | The workspace has never had a List | | `422` | `VALIDATION_ERROR` | An id is not a UUID | ## Code examples **Code Examples** ```bash [cURL] curl -X DELETE https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/members \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "memberIds": ["9b1e4c7a-2d3f-4e8a-b6c5-0a1f2e3d4c5b"], "pendingIds": ["7e8f9a0b-1c2d-4e3f-8a4b-5c6d7e8f9a0b"] }' ``` ```javascript [JavaScript] const listId = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40"; const response = await fetch(`https://api.reversecontact.com/v2/lists/${listId}/members`, { method: "DELETE", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ memberIds: ["9b1e4c7a-2d3f-4e8a-b6c5-0a1f2e3d4c5b"], pendingIds: ["7e8f9a0b-1c2d-4e3f-8a4b-5c6d7e8f9a0b"] }) }); const { data } = await response.json(); console.log(`${data.removed} removed, ${data.dismissed} dismissed`); ``` ```python [Python] import requests list_id = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40" response = requests.delete( f"https://api.reversecontact.com/v2/lists/{list_id}/members", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "memberIds": ["9b1e4c7a-2d3f-4e8a-b6c5-0a1f2e3d4c5b"], "pendingIds": ["7e8f9a0b-1c2d-4e3f-8a4b-5c6d7e8f9a0b"] } ) data = response.json()["data"] print(f"{data['removed']} removed, {data['dismissed']} dismissed") ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "removed": 1, "dismissed": 1 }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4522, "left": 5478 } } }, "metadata": { "requestId": "546b4889-3390-4f29-86b2-561608af3373", "executionTimeMs": 33 } } ``` --- # Import members and data > Import up to 25,000 Signals or CSV rows into a List, enrich members within a credit budget, and track progress by polling or webhook. - Page: https://docs.reversecontact.com/endpoints/list-intake-signals - Endpoint: `POST https://api.reversecontact.com/v2/lists/:listId/intake/signals` - Credits: Free to queue. Each new member costs its identity price, and each data family you ask for costs what its endpoint costs, up to your maxCredits. - Answer: asynchronous. The request returns an operation (`data.operationId`, `data.pollUrl`): read `GET {pollUrl}?wait=20` until `clientAction` is no longer `poll`, or receive its state on the `webhookUrl` of the import. Reads are free and never rate limited. - Available in: REST API, MCP tool `import_to_list`, TypeScript and Python SDKs. - Last verified: 2026-11-04 ## Introduction The Import members and data endpoint queues up to 25,000 Signals, or a public CSV file or Google Sheet, as one operation on a List. It answers at once with an `operationId` and a `pollUrl`, then works in the background: each line is identified like an [add](https://docs.reversecontact.com/endpoints/list-members-add), and with `families`, the data you ask for is bought for each member, new or already on the List. Use it instead of [Add members](https://docs.reversecontact.com/endpoints/list-members-add) for more than 1,000 Signals, to fill a List with emails, phone numbers, profiles or company details, to set a credit ceiling, or to be told by webhook when the import stops moving. Follow it with [Read a List operation](https://docs.reversecontact.com/endpoints/list-operation) and stop it with [Cancel a List operation](https://docs.reversecontact.com/endpoints/list-operation-cancel). ## Credits Queuing the import is **free**: the operation is charged line by line as it runs, and never more than `maxCredits`. | What the line does | Credits | | ------------------------------------------ | ------------------------------------------------------------------------- | | Adds a new person | 1 from a Social profile URL (2 with `live: true`), 2 from an email or a name | | Adds a new company | 1 from a company page URL or id (2 when the page is read live), 2 from a domain | | Names a member already on the List, or no one | Free | | `profile` family | Per member that lacks it, 1 when read from its Social profile URL, 2 when found by name (2 in the worst case). A person the import adds comes with their profile, free. | | `company` family | Per member, 1 from a company page URL, 2 from a domain (2 in the worst case). On a people List, it is the person's current company. On a companies List, a company the import adds comes with its details, free. | | `contact_email` family | 3 per email found, free when none is found. The email of an email Signal is kept as the member's email, free. | | `contact_phone` family | 20 per phone number found, free when none is found | Data a member already holds is never bought again, and an email or a phone number already looked for, found or not, is not looked for again: an import replayed on the same List charges only what is missing. A line no one matches is kept as [Pending Intake](https://docs.reversecontact.com/guides/lists#pending-intake), free. When a Signal sent again finds someone, the new member replaces its Pending Intake line. The `contact_phone` family needs phone finding on your workspace, as [Find Phone](https://docs.reversecontact.com/endpoints/contact-phone) does. Without it, the member of each line that asks for a phone number is still added, the line reads `resolved` with `errorCode: "CONTACT_PHONE_FINDER_DISABLED"`, and no phone number is charged. The API key needs the `lists` permission, plus the permission of each thing the import buys: `persons:enrich` for people and their profiles, `companies:enrich` for companies and company details, `contact` for the email and phone families (see [API key management](https://docs.reversecontact.com/guides/api-key-management)). The request that starts an import counts as one request on your daily limit and is not counted against your per-minute limit, whatever its number of lines. Its lines then take their share of your per-minute limit as they run, at most 80% of it, and wait when that share is taken. Reading the operation is never counted. See [Rate limits](https://docs.reversecontact.com/guides/rate-limits#how-requests-are-counted). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body The List is named in the path: `POST /v2/lists/:listId/intake/signals`. Send `signals` or `sourceUrl`, not both. | Name | Type | Required | Description | | ------------- | -------- | ----------- | --------------------------------------------------------------------------------------------------- | | `signals` | array | Conditional | 1 to 25,000 Signals, with the fields of [Add members](https://docs.reversecontact.com/endpoints/list-members-add#request-body), `externalId` included | | `sourceUrl` | string | Conditional | A public HTTPS CSV file or Google Sheet URL instead of `signals`: up to 25,000 rows and 25 MiB. Columns are matched from their headers (Social URL, email, first name, last name, company, domain). | | `families` | string[] | No | Data to buy for each member: `profile`, `company`, `contact_email`, `contact_phone`. A companies List takes `company` only. | | `maxCredits` | integer | Conditional | The most this operation may spend. Required with `families` when the worst case is above 25 credits. Omitted, the ceiling is the worst case of the import. | | `live` | boolean | No | `true` fetches the profile of each Social profile URL live. Omitted or `false`, saved data is used. No effect on a companies List. | | `operationId` | string | No | Your UUID for this operation. Sending the same request again with it returns the same operation and never charges twice. | | `webhookUrl` | string | No | Public HTTPS URL told each time the operation stops moving, see [Webhook callback](#webhook-callback) | | `source` | string | No | `paste` (default) or `file`: how a Pending Intake line says it arrived | ### The credit ceiling When `families` makes the worst case of the import (every line a new member, every family bought, every email and phone found) higher than 25 credits, the request must carry `maxCredits`: without it, it returns `400 LIST_INVALID_INPUT` and nothing is queued. `maxCredits` is your consent to spend up to that amount, and the operation stops before a step that would cross it. An import without `families` needs no ceiling: it costs at most the identity of its lines. An import sent without `maxCredits` gets its worst case as ceiling, so it may spend up to that amount. ### Deduplication and replays Lines naming the same identity with the same `externalId` are deduplicated: `duplicateCount` gives how many were dropped. Two lines naming the same person under two `externalId` values stay two lines, and the second one reads `already_present`. With `operationId`, sending the same request again returns the operation it created, whatever its state, and charges nothing twice. The same `operationId` with other Signals, families, `live` or `source` returns `400 LIST_INVALID_INPUT`. A cancelled operation stays cancelled: a new run needs a new `operationId`. ### Pauses and how to resume The operation pauses instead of failing when it cannot go on, and keeps the work done: - **`maxCredits` reached** (`stopReason: "max_credits"`): send the same request again, same `operationId`, with a higher `maxCredits`. `nextStep.minimumMaxCredits` gives the smallest ceiling that lets it continue. - **Balance too low** (`stopReason: "balance"`): add credits, then send the same request again with the same `operationId`. - **Paused from the app** (`stopReason: "paused_by_user"`): send the same request again with the same `operationId`. A paused operation holds the imports queued after it on the same List (`stopReason: "blocked_by_paused_operation"`, with `nextStep.blockingOperationId`): resume or cancel it to let them start. An operation also stops for good when the List reaches its member or Pending Intake limit (`list_member_limit`, `list_pending_intake_limit`); the lines it did not reach are not charged. ### Lines that name no one, or the wrong kind A line never refuses the whole import. A line naming a company on a people List, or a person on a companies List, reads `refused` with `errorCode: "ENTITY_MISMATCH"`. A Signal that identifies no one reads `failed` with `errorCode: "LIST_INVALID_INPUT"`, and a row of a `sourceUrl` file with no usable column reads `refused` with `errorCode: "NO_INPUT"`. These lines are free, and [Read a List operation](https://docs.reversecontact.com/endpoints/list-operation#response-structure) gives them line by line. ## Response structure The request answers `202` at once, with a `Retry-After: 5` header. It runs within a 15-second time limit, which a slow `sourceUrl` download can cross: past it, it returns `504`, and the import may have been queued all the same. Always send an `operationId` for this reason: the same request sent again with it returns the operation if it exists, or queues it, and never charges twice. | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the operation was queued | | `error` | `null` | Always `null` on success | **data** (OperationAck): The operation queued | Field | Type | Description | | ---------------- | ------ | --------------------------------------------------------------------------------------------- | | `operationId` | string | Operation id (UUID). **Save it**: it reads, resumes and cancels the operation. | | `pollUrl` | string | Path that reads the operation: `GET` it, with `?wait=20` to hold the request until it settles | | `totalRows` | number | Lines queued after deduplication | | `duplicateCount` | number | Lines dropped as duplicates of an earlier line | | `sourceRows` | number | With `sourceUrl` only: the non-empty data rows read from the file | | `mapping` | object | With `sourceUrl` only: the header each Signal field was read from, for example `{ "linkedinUrl": "Profile", "email": "Work email" }` | | `job` | object | The internal record behind the operation, left out of the example below. Its fields may change: read the operation at `pollUrl` instead. | `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). `quotas.creditsConsumed` is `0`: the credits of the operation appear in its `creditsConsumed` as it runs. Then read the operation at its `pollUrl` until `clientAction` is `done`: see [Read a List operation](https://docs.reversecontact.com/endpoints/list-operation). ## Webhook callback With `webhookUrl`, that URL receives one `POST` each time the operation stops moving: it completed, paused at a ceiling or a low balance, was cancelled, or its outcome became unknown. It receives nothing while the operation runs. The event says which operation changed and carries no member data: read the operation, and its per-line results, at `pollUrl` with your API key. This event is not the lookup callback of the [Webhooks guide](https://docs.reversecontact.com/guides/webhooks): it has its own payload, below, and is sent to `webhookUrl` only, never to the workspace webhook URL of **Settings > Webhooks**. | Field | Type | Description | | -------------- | -------------- | ---------------------------------------------------------------------- | | `event` | string | Always `list.operation.updated` | | `operationId` | string | The operation that changed | | `listId` | string | Its List | | `status` | string | Status of the operation, as [Read a List operation](https://docs.reversecontact.com/endpoints/list-operation#response-structure) gives it | | `clientAction` | string | `done`, `resume` or `reconcile` | | `stopReason` | string \| null | Why it stopped, `null` once completed | | `pollUrl` | string | Path that reads the operation | | `occurredAt` | string | When the event was sent (ISO 8601) | The request is JSON and its `x-reversecontact-event` header names the event, `list.operation.updated`. Answer with a `2xx` within 10 seconds. A timeout, a `408`, a `429` or a `5xx` is tried again twice, after 2 then 5 seconds; a redirect is never followed. Each state is sent once, so a receiver that missed it can always read the operation at `pollUrl`. The URL must be public HTTPS: a private, local or unresolvable host returns `400 WEBHOOK_URL_UNREACHABLE` and nothing is queued. It is checked again before each delivery. ## Errors | Status | Code | Meaning | | ------ | -------------------------- | ---------------------------------------------------------------------------------------- | | `400` | `LIST_INVALID_INPUT` | Neither or both of `signals` and `sourceUrl`, `maxCredits` missing above 25 credits, a family the List cannot hold, or an `operationId` sent again with another request | | `400` | `WEBHOOK_URL_UNREACHABLE` | `webhookUrl` is not a public HTTPS URL | | `403` | `INSUFFICIENT_PERMISSIONS` | The key lacks `lists`, or the permission of what the import buys | | `404` | `LIST_NOT_FOUND` | No List with this id in your workspace | | `404` | `LISTS_NOT_ACTIVATED` | The workspace has never had a List: create one first | | `422` | `BULK_JOB_CSV_PARSE_ERROR` | The `sourceUrl` file could not be read: not a public HTTPS URL, no header row, no recognized column, too many rows, or over 25 MiB | | `502` | `LIST_JOB_ENGINE_ERROR` | The List service is briefly unavailable: send the same request again, with the same `operationId` | | `503` | `BULK_QUEUE_UNAVAILABLE` | The import queue is busy: retry shortly | | `504` | `GATEWAY_TIMEOUT` | The request ran out of time after 15 seconds: send it again with the same `operationId` | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/intake/signals \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f", "signals": [ { "linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001" }, { "email": "john.smith@acme.com", "externalId": "crm-002" } ], "families": ["contact_email"], "maxCredits": 100, "webhookUrl": "https://example.com/hooks/reversecontact" }' ``` ```javascript [JavaScript] const KEY = "YOUR_API_KEY"; const BASE = "https://api.reversecontact.com"; const listId = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40"; // Queue the import, then follow it until it no longer moves. const queued = await fetch(`${BASE}/v2/lists/${listId}/intake/signals`, { method: "POST", headers: { "Authorization": `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ operationId: crypto.randomUUID(), signals: [ { linkedinUrl: "https://www.social.com/in/janedoe", externalId: "crm-001" }, { email: "john.smith@acme.com", externalId: "crm-002" } ], families: ["contact_email"], maxCredits: 100 }) }); const { data: ack } = await queued.json(); let operation; do { const response = await fetch(`${BASE}${ack.pollUrl}?wait=20`, { headers: { "Authorization": `Bearer ${KEY}` } }); operation = (await response.json()).data; } while (operation.clientAction === "poll"); console.log(operation.status, `${operation.creditsConsumed} credits`, operation.nextStep?.message ?? ""); ``` ```python [Python] import uuid import requests KEY = "YOUR_API_KEY" BASE = "https://api.reversecontact.com" list_id = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40" headers = {"Authorization": f"Bearer {KEY}"} ack = requests.post( f"{BASE}/v2/lists/{list_id}/intake/signals", headers=headers, json={ "operationId": str(uuid.uuid4()), "signals": [ {"linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001"}, {"email": "john.smith@acme.com", "externalId": "crm-002"} ], "families": ["contact_email"], "maxCredits": 100 } ).json()["data"] while True: operation = requests.get(f"{BASE}{ack['pollUrl']}", headers=headers, params={"wait": 20}, timeout=30).json()["data"] if operation["clientAction"] != "poll": break print(operation["status"], operation["creditsConsumed"], "credits") ``` ## Example response **Example Response** ```json [202 - Queued] { "success": true, "data": { "operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f", "pollUrl": "/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress", "totalRows": 2, "duplicateCount": 0 }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4522, "left": 5478 } } }, "metadata": { "requestId": "b1d8124d-e074-4d7b-9a14-fba9bfe74625", "executionTimeMs": 88 } } ``` ```json [Webhook event] { "event": "list.operation.updated", "operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f", "listId": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40", "status": "completed", "clientAction": "done", "stopReason": null, "pollUrl": "/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress", "occurredAt": "2026-10-28T09:21:40.000Z" } ``` --- # Read a List operation > Follow an import with long polling, read why it paused and what to do, and page through its per-line results, for free. - Page: https://docs.reversecontact.com/endpoints/list-operation - Endpoint: `GET https://api.reversecontact.com/v2/lists/:listId/operations/:operationId/progress` - Credits: Free, also at a zero balance, and never counted against the rate limits. - Answer: synchronous. - Available in: REST API, MCP tool `get_list_operation`, TypeScript and Python SDKs. - Last verified: 2026-11-04 ## Introduction The Read a List operation endpoint reads an operation started by an [import](https://docs.reversecontact.com/endpoints/list-intake-signals): it is the `pollUrl` of the import. It says how far the operation is, what it has charged, and what to do next with `clientAction`. With `rows=true`, it also returns the result of each line you sent, with your `externalId` and the member it became. Reading an operation is **free**, also at a zero balance, and never counted against your rate limits, so you can follow a long import at the pace it asks for. Any key of the workspace with the `lists` permission can read it. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request The route is `GET /v2/lists/:listId/operations/:operationId/progress`. ### Path parameters | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------- | | `listId` | string | Yes | List id (UUID) | | `operationId` | string | Yes | The `operationId` of the import acknowledgement | ### Query Parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------ | | `wait` | integer | No | Hold the request up to this many seconds, 0 to 20, while the operation runs. It answers as soon as the operation settles, pauses or stops, or with the operation still running when the wait ends. Defaults to 0. | | `rows` | string | No | `true` adds one page of per-line results | | `perPage` | integer | No | With `rows=true`: lines per page, from 1 to 1,000. Defaults to 200. | | `cursor` | string | No | With `rows=true`: the `rows.nextCursor` of the previous page | `cursor` and `perPage` without `rows=true` return `400 LIST_INVALID_INPUT`. ### How to follow an operation Read the operation while `clientAction` is `poll`, with `wait=20` and an HTTP client timeout above it, for example 30 seconds. Start two reads at least `pollAfterSeconds` apart (the `Retry-After` header carries the same value): a held read already lasts longer, so the next one starts right away. Then act on `clientAction`: | `clientAction` | What to do | | -------------- | ------------------------------------------------------------------------------------------------------------- | | `poll` | The operation runs: read it again | | `done` | It ended. With `completed`, the data can be read in the List; read `rows` for the line results | | `resume` | It paused: `stopReason` says why and `nextStep` what to do, see [Pauses and how to resume](https://docs.reversecontact.com/endpoints/list-intake-signals#pauses-and-how-to-resume) | | `reconcile` | The outcome is unknown: read the List before doing anything, and never send the import again blindly | ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (ListOperation): The operation | Field | Type | Description | | ---------------------- | -------------- | --------------------------------------------------------------------------------------------------- | | `operationId` | string | Operation id | | `listId` | string | List id | | `kind` | string | `list_intake_persons` or `list_intake_companies` | | `status` | string | `queued`, `running`, `paused`, `stopped_budget`, `completed`, `partial`, `failed`, `cancelled`, `refused` or `unknown` | | `clientAction` | string | `poll`, `resume`, `done` or `reconcile`: what to do next | | `creditsConsumed` | number | Credits charged so far | | `creditsPending` | number | Credits reserved by steps in flight | | `creditsEstimated` | number | Credits reserved when the operation was created: `0` for an import, which spends up to `maxCredits` | | `maxCredits` | number \| null | The ceiling of the operation | | `estimatedSeconds` | number \| null | Seconds left while it runs, `0` once done, `null` when not known yet | | `estimateBlockedUntil` | string \| null | When the daily extraction limit holds the operation: the next 00:00 UTC, when it resumes | | `pollAfterSeconds` | number | Seconds to wait before reading again, `0` once you can stop | | `pollUrl` | string | Path of this endpoint for the operation | | `followUp` | object | The same follow-up for an agent: `tool` (`get_list_operation`, the [MCP](https://docs.reversecontact.com/guides/mcp) tool), its `arguments` (`listId`, `operationId`) and `pollAfterSeconds` | | `replay` | object | Always `{ "reuseOperationId": true, "startNewWork": false, "debitAgain": false }`: sending the import again with the same `operationId` starts no new work and charges nothing twice | | `stopReason` | string \| null | Why it is not running: `max_credits`, `balance`, `blocked_by_paused_operation`, `paused_by_user` (paused from the app), `list_member_limit`, `list_pending_intake_limit`, `insufficient_credits` or `cancelled` | | `errorMessage` | string \| null | What failed, when it failed | | `startedAt` | string \| null | Start date (ISO 8601) | | `completedAt` | string \| null | End date (ISO 8601), once the data can be read | | `createdAt` | string \| null | Date the operation was created (ISO 8601) | | `updatedAt` | string \| null | Date of its last change (ISO 8601) | **progress** (Progress): Lines of the operation | Field | Type | Description | | -------------- | ------ | --------------------------------------- | | `total` | number | Lines in the operation | | `processed` | number | Lines processed | | `succeeded` | number | Lines that found their member and every family asked for | | `failed` | number | Lines that failed, matched no one, or found their member but not a family asked for (an email not found, for example) | | `skipped` | number | Lines skipped | | `remaining` | number | Lines left | | `unknownCount` | number | Lines whose outcome is unknown | **nextStep** (NextStep): What to do after a stop, `null` otherwise | Field | Type | Description | | --------------------- | ------ | ------------------------------------------------------------------------------------ | | `action` | string | `raise_max_credits_and_resume`, `top_up_and_resume`, `resume` (after a pause from the app), `resume_or_cancel_blocking_operation` or `none` | | `message` | string | The next step, in a sentence you can show | | `requiredCredits` | number | Worst-case credits of the step that stopped | | `minimumMaxCredits` | number | After a `max_credits` stop: the smallest `maxCredits` that lets it continue | | `blockingOperationId` | string | The paused operation this one waits for | **rows** (LineResults): With `rows=true`, one page of line results; absent without `rows=true` | Field | Type | Description | | ------------ | -------------- | ------------------------------------------------------------------- | | `nextCursor` | string \| null | Send it back as `cursor` for the next page, `null` on the last page | **items** (LineResult[]): One entry per line, in request order | Field | Type | Description | | ------------ | -------------- | ----------------------------------------------------------------------------------------------------- | | `index` | number \| null | Position of the line in `signals`, or among the non-empty data rows of a `sourceUrl` file, from 0; `null` for a file sent from the app | | `rowNumber` | number | Line number in the operation | | `outcome` | string | `pending` (not processed yet), `added`, `already_present`, `resolved` (the member was found, but the line does not say whether it added it: after a restart, or when a requested family could not be bought), `not_found`, `refused`, `failed`, `not_reached` (not started before a cancel) or `unknown` | | `externalId` | string \| null | Your identifier, as sent | | `memberId` | string \| null | The member the line names | | `errorCode` | string \| null | What went wrong: for example `NOT_FOUND`, `ENTITY_MISMATCH` (a company on a people List, or the reverse), `DATA_SUBJECT_BLOCKED` (the person asked us to stop processing their data), or `CONTACT_EMAIL_NOT_FOUND` when the member was found but a family could not be bought | | `reason` | string \| null | How the line ended, once it ended: `completed` when every requested family was bought, `not_found`, `data_subject_blocked`, `unknown`, or what the family lookup returned. `null` for a line without families that found its member. | | `signal` | object | The identifiers the line was sent with | `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). `quotas.creditsConsumed` is `0` for the read itself. An operation that does not belong to this List and workspace returns `404 LIST_OPERATION_NOT_FOUND`. ## Code examples **Code Examples** ```bash [cURL] curl "https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress?wait=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript [JavaScript] // Once the import is done, read the result of every line. const KEY = "YOUR_API_KEY"; const pollUrl = "/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress"; const lines = []; let cursor = null; do { const url = new URL(`https://api.reversecontact.com${pollUrl}`); url.searchParams.set("rows", "true"); url.searchParams.set("perPage", "1000"); if (cursor) url.searchParams.set("cursor", cursor); const response = await fetch(url, { headers: { "Authorization": `Bearer ${KEY}` } }); const { data } = await response.json(); lines.push(...data.rows.items); cursor = data.rows.nextCursor; } while (cursor); for (const line of lines) console.log(line.externalId, line.outcome, line.memberId); ``` ```python [Python] import requests url = "https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress" operation = requests.get( url, headers={"Authorization": "Bearer YOUR_API_KEY"}, params={"wait": 20}, timeout=30 ).json()["data"] print(operation["status"], operation["clientAction"]) if operation["clientAction"] == "resume": print(operation["nextStep"]["message"]) ``` ## Example response The second example reads the import of [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals#code-examples) once done: Jane Doe is added (1 credit) and her email is found (3 credits). John Smith is already on the List, and the email of his Signal becomes his work email, free. **Example Response** ```json [200 - Paused at maxCredits] { "success": true, "data": { "operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f", "listId": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40", "kind": "list_intake_persons", "status": "stopped_budget", "clientAction": "resume", "progress": { "processed": 312, "total": 500, "succeeded": 298, "failed": 14, "skipped": 0, "remaining": 188, "unknownCount": 0 }, "creditsConsumed": 998, "creditsPending": 0, "creditsEstimated": 0, "maxCredits": 1000, "estimatedSeconds": null, "estimateBlockedUntil": null, "pollAfterSeconds": 0, "pollUrl": "/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress", "followUp": { "tool": "get_list_operation", "arguments": { "listId": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40", "operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f" }, "pollAfterSeconds": 0 }, "replay": { "reuseOperationId": true, "startNewWork": false, "debitAgain": false }, "stopReason": "max_credits", "nextStep": { "action": "raise_max_credits_and_resume", "message": "Paused at the operation's maxCredits ceiling. Resume it with a maxCredits of at least 1001 to continue. Work already done is kept and is not charged again.", "minimumMaxCredits": 1001, "requiredCredits": 3 }, "errorMessage": null, "startedAt": "2026-10-28T09:16:00.000Z", "completedAt": null, "createdAt": "2026-10-28T09:15:58.000Z", "updatedAt": "2026-10-28T09:19:12.000Z" }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 5520, "left": 4480 } } }, "metadata": { "requestId": "60a16561-060b-4daa-9838-56deafe39c12", "executionTimeMs": 26 } } ``` ```json [200 - Completed, with rows] { "success": true, "data": { "operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f", "listId": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40", "kind": "list_intake_persons", "status": "completed", "clientAction": "done", "progress": { "processed": 2, "total": 2, "succeeded": 2, "failed": 0, "skipped": 0, "remaining": 0, "unknownCount": 0 }, "creditsConsumed": 4, "creditsPending": 0, "creditsEstimated": 0, "maxCredits": 100, "estimatedSeconds": 0, "estimateBlockedUntil": null, "pollAfterSeconds": 0, "pollUrl": "/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress", "followUp": { "tool": "get_list_operation", "arguments": { "listId": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40", "operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f" }, "pollAfterSeconds": 0 }, "replay": { "reuseOperationId": true, "startNewWork": false, "debitAgain": false }, "stopReason": null, "nextStep": null, "errorMessage": null, "startedAt": "2026-10-28T09:16:00.000Z", "completedAt": "2026-10-28T09:21:38.000Z", "createdAt": "2026-10-28T09:15:58.000Z", "updatedAt": "2026-10-28T09:21:38.000Z", "rows": { "items": [ { "index": 0, "rowNumber": 1, "outcome": "added", "externalId": "crm-001", "memberId": "9b1e4c7a-2d3f-4e8a-b6c5-0a1f2e3d4c5b", "errorCode": null, "reason": "completed", "signal": { "linkedinUrl": "https://www.social.com/in/janedoe" } }, { "index": 1, "rowNumber": 2, "outcome": "already_present", "externalId": "crm-002", "memberId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "errorCode": null, "reason": "completed", "signal": { "email": "john.smith@acme.com" } } ], "nextCursor": null } }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4526, "left": 5474 } } }, "metadata": { "requestId": "2d6a1a35-188a-4d9a-868f-2257ee4554c1", "executionTimeMs": 31 } } ``` --- # Cancel a List operation > Cancel a List import for free and keep members already added. Lines in progress finish as usual; lines not started are never charged. - Page: https://docs.reversecontact.com/endpoints/list-operation-cancel - Endpoint: `POST https://api.reversecontact.com/v2/lists/:listId/operations/:operationId/cancel` - Credits: Free. Lines that had not started are never charged. - Answer: synchronous. - Available in: REST API, MCP tool `cancel_list_operation`, TypeScript and Python SDKs. - Last verified: 2026-11-04 ## Introduction The Cancel a List operation endpoint stops an [import](https://docs.reversecontact.com/endpoints/list-intake-signals) for good. Lines already in flight finish and are charged as usual; lines that had not started are never charged and read `not_reached` in the [per-line results](https://docs.reversecontact.com/endpoints/list-operation#response-structure). Members already added stay on the List. Any key of the workspace with the `lists` permission can cancel an import, whichever key started it: a rotated key can still stop an import its predecessor queued. Cancelling is **free** (0 credits) and final: sending the import again with the same `operationId` returns the cancelled operation, and a new run needs a new `operationId`. Cancelling an operation already cancelled answers it again, and an operation already stopped by a List limit or a lack of credits is answered with its own `stopReason` (`list_member_limit`, `list_pending_intake_limit` or `insufficient_credits`). When the import has a `webhookUrl`, that URL receives the `cancelled` state. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request The route is `POST /v2/lists/:listId/operations/:operationId/cancel`. ### Path parameters | Name | Type | Required | Description | | ------------- | ------ | -------- | ----------------------------------------------- | | `listId` | string | Yes | List id (UUID) | | `operationId` | string | Yes | The `operationId` of the import acknowledgement | The request has no body. ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the operation was cancelled | | `error` | `null` | Always `null` on success | `data` is the cancelled operation, with the fields of [Read a List operation](https://docs.reversecontact.com/endpoints/list-operation#response-structure): `status` is `cancelled`, `clientAction` is `done` and `stopReason` is `cancelled`, or the reason of an earlier stop. Its `progress` and `creditsConsumed` can still move while the lines in flight finish: read the operation again to see the final counts. `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). ## Errors | Status | Code | Meaning | | ------ | -------------------------------- | --------------------------------------------------------------- | | `404` | `LIST_OPERATION_NOT_FOUND` | No operation with this id on this List of your workspace | | `409` | `LIST_OPERATION_NOT_CANCELLABLE` | The operation already ended: there is nothing left to cancel | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/cancel \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript [JavaScript] const listId = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40"; const operationId = "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f"; const response = await fetch( `https://api.reversecontact.com/v2/lists/${listId}/operations/${operationId}/cancel`, { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY" } } ); const { data, error } = await response.json(); console.log(error ? error.code : `${data.status}, ${data.progress.remaining} lines not reached`); ``` ```python [Python] import requests list_id = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40" operation_id = "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f" response = requests.post( f"https://api.reversecontact.com/v2/lists/{list_id}/operations/{operation_id}/cancel", headers={"Authorization": "Bearer YOUR_API_KEY"} ) payload = response.json() print(payload["error"]["code"] if payload["error"] else payload["data"]["status"]) ``` ## Example response **Example Response** ```json [200 - Cancelled] { "success": true, "data": { "operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f", "listId": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40", "kind": "list_intake_persons", "status": "cancelled", "clientAction": "done", "progress": { "processed": 120, "total": 500, "succeeded": 114, "failed": 6, "skipped": 0, "remaining": 380, "unknownCount": 0 }, "creditsConsumed": 456, "creditsPending": 0, "creditsEstimated": 0, "maxCredits": 2000, "estimatedSeconds": 0, "estimateBlockedUntil": null, "pollAfterSeconds": 0, "pollUrl": "/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress", "followUp": { "tool": "get_list_operation", "arguments": { "listId": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40", "operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f" }, "pollAfterSeconds": 0 }, "replay": { "reuseOperationId": true, "startNewWork": false, "debitAgain": false }, "stopReason": "cancelled", "nextStep": { "action": "none", "message": "Cancelled. Rows that had not started were not charged." }, "errorMessage": null, "startedAt": "2026-10-28T09:16:00.000Z", "completedAt": "2026-10-28T09:18:12.000Z", "createdAt": "2026-10-28T09:15:58.000Z", "updatedAt": "2026-10-28T09:18:12.000Z" }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4978, "left": 5022 } } }, "metadata": { "requestId": "2373b182-0821-45fb-b10f-37428d3b8087", "executionTimeMs": 40 } } ``` ```json [409 - Already ended] { "success": false, "data": null, "error": { "code": "LIST_OPERATION_NOT_CANCELLABLE", "message": "This List operation already ended (completed): there is nothing left to cancel" }, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4978, "left": 5022 } } }, "metadata": { "requestId": "d5f5c787-9f5f-42c4-9de8-f94144955cec", "executionTimeMs": 22 } } ``` --- # Bulk enrichment > Enrich up to 25,000 people in one request, priced like the unit calls and refunded for what is not found. Read results as they come, or as CSV or NDJSON. - Page: https://docs.reversecontact.com/endpoints/bulk-enrich-persons - Endpoint: `POST https://api.reversecontact.com/v2/enrich/persons/bulk` - Credits: Free to send. Each item costs what its unit call costs, reserved when the job is created and refunded for what is not found. - Answer: asynchronous. The request returns the job (`data.job`): read `GET {data.job.links.self}?wait=20` while `status` is `queued` or `running`, or receive `job.updated` on its `webhookUrl`, then download the CSV or NDJSON at `links.results`, or read the results while it runs at `GET /v2/jobs/{jobId}/rows`. Reading the job is free and never rate limited; the download and each page of results are free and count as one request. - Available in: REST API, MCP tool `bulk_enrich_persons`, TypeScript and Python SDKs, CLI command `rc bulk create`. - Last verified: 2026-11-04 ## Introduction The Bulk enrichment endpoint enriches up to 25,000 people in one request. Send them as JSON items (a Social profile URL, an email, or a name with its company), choose what to look up for each one (the profile, an email, a phone number), and it runs in the background as a job. It answers at once with the job: follow it with [Get a job](https://docs.reversecontact.com/endpoints/job), or be told on your `webhookUrl` when it settles. Read its results a page at a time while it runs with [Read job results by page](https://docs.reversecontact.com/endpoints/job-rows), or download one row per item, as CSV or NDJSON, with [Download job results](https://docs.reversecontact.com/endpoints/job-results). Bulk enrichment fits a one-off need, such as a CRM export or a file of leads to complete: you get a file back and nothing is kept. To keep people over time, add data to them later and never pay twice for the same person, use a [List](https://docs.reversecontact.com/guides/lists) instead. To look up one person while a user waits, call [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person) or [Find Email](https://docs.reversecontact.com/endpoints/contact-email). ## Credits Sending the request is **free**. Each item costs what its unit call costs, and only for what is found: | What the item looks up | Credits | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | Profile, from a Social profile URL | 1, or 2 with `live: true`, as [Person profile](https://docs.reversecontact.com/endpoints/fetch-profile) | | Profile, from an email or a name | 2, as [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person) | | `contact_email` | 3 for a professional email, 5 for a personal one, 8 for both with `emailType: "any"`, as [Find Email](https://docs.reversecontact.com/endpoints/contact-email) | | `contact_phone` | 20 per phone number found, as [Find Phone](https://docs.reversecontact.com/endpoints/contact-phone) | When the job is created, it reserves the most each item can cost, in the order you sent them, as long as your balance covers it. An item the balance no longer covers is not run: it reads `insufficient_credits` in the results, costs nothing, and `notRunForCredits` counts it, while a later item that costs less can still run. When the job settles, what an item did not find is given back: you pay only for what was found. On a Go, Plus or Max plan, a profile your plan covers costs no credit in a job, as it does in its unit call, and once the workspace has delivered its [daily profile cap](https://docs.reversecontact.com/guides/rate-limits#daily-profile-cap-of-the-go-plus-and-max-plans), the items of the job wait until 00:00 UTC instead of being refused. The email and phone finders start from a Social profile URL, or from a first name, a last name and a company domain. For any other item, the job first matches the person's profile, charged like a profile lookup when it is found: an item with only an email, sent for a phone number, costs 2 credits for its profile, plus 20 if a number is found. An item with only an email is never sent to the email finder, since it already holds one. An item that names the same person as an earlier item of the job is looked up once: its row reads `duplicate`, costs nothing, and `duplicateCount` counts it. The API key needs the permission of what the job asks for: Person Enrichment when `families` includes `profile` (the default), Contact for `contact_email` and `contact_phone`. A key with [credit limits](https://docs.reversecontact.com/guides/api-key-management#_4-set-credit-limits-optional) must hold the whole reservation: a job that would take it over its daily, monthly or total limit is refused with `KEY_CREDIT_LIMIT_REACHED` before anything is reserved. Once the job settles, the key counts only what it spent. Creating a job counts as one request on your rate limits, whatever its number of items. Its items then take their share of your per-minute limit as they run, at most 80% of it, and wait when that share is taken: see [Rate limits](https://docs.reversecontact.com/guides/rate-limits#how-requests-are-counted). Reading the job is never counted. Following it, downloading its results and cancelling it still work at a zero balance. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body The route is `POST /v2/enrich/persons/bulk`. | Name | Type | Required | Description | | ------------ | -------- | -------- | -------------------------------------------------------------------------------------------------------------- | | `items` | array | Yes | 1 to 25,000 people, see [Items](#items) | | `families` | string[] | No | What to look up for each item: `profile`, `contact_email`, `contact_phone`. Defaults to `["profile"]`. | | `emailType` | string | No | With `contact_email`, the kind of email to find: `professional` (default), `personal` or `any` | | `live` | boolean | No | `true` fetches the profile of a Social profile URL item live. Omitted or `false`, saved data is used. | | `name` | string | No | A name for the job, up to 255 characters, shown in the job history and in the app. Defaults to `Sent via API`. | | `webhookUrl` | string | No | Public HTTPS URL that receives one `POST` when the job settles, see [Webhook callback](#webhook-callback) | ### Items | Name | Type | Required | Description | | --------------- | ------ | ----------- | ------------------------------------------------------------------------------------ | | `linkedinUrl` | string | Conditional | Social profile URL of the person | | `email` | string | Conditional | Email address of the person | | `firstName` | string | Conditional | First name | | `lastName` | string | Conditional | Last name | | `companyDomain` | string | No | Domain of the person's company, such as `acme.com` | | `companyName` | string | No | Name of the person's company, when you have no domain | | `externalId` | string | No | Your own id for the item, up to 255 characters, given back in its row of the results | Give each item a Social profile URL, an email, or a first and last name with its company: these find the right person. An item that holds only a company is still looked up, rarely matches anyone, and costs nothing when no profile is found. An item with no field at all reads `skipped` in the results, and an item whose only field is an invalid profile URL reads `invalid_input`: neither costs anything. With only `contact_email` or `contact_phone`, an item the finders cannot reach, such as an email alone for `contact_email`, reads `skipped` too. A request in which no item can be looked up returns `422 BULK_JOB_CSV_PARSE_ERROR` and nothing is created. ### Unknown fields A field that the body or an item does not define is refused with `422 UNKNOWN_FIELD`, never ignored: the job would otherwise run without it, for example a profile-only job for a misspelled `families`. `error.details` names the field, such as `items[3].linkedin_url`, the field it most likely stands for, and the accepted fields. Nothing is created and nothing is charged. ### Idempotency-Key header Creating a job reserves its credits. If the response gets lost (a timeout, a dropped connection), sending the request again would create and charge a second job. To retry safely, send an `Idempotency-Key` header with a key you choose for this job, such as a UUID, and send the same key when you retry. For 24 hours, the same key with the same body returns the first response with the header `Idempotent-Replayed: true`, without creating another job or charging anything. The same key with another body returns `422 IDEMPOTENCY_KEY_REUSED`, and a key whose first request is still being processed returns `409 IDEMPOTENCY_KEY_IN_USE`: retry after the `Retry-After` seconds. A key that is empty, longer than 255 characters, or holds a space or a non-ASCII character returns `400 INVALID_IDEMPOTENCY_KEY`. ## Response structure The request answers `202` at once, with a `Retry-After: 2` header. | Field | Type | Description | | --------- | ------- | ----------------------------- | | `success` | boolean | `true` if the job was created | | `error` | `null` | Always `null` on success | **data** (BulkEnrichAck): The job created | Field | Type | Description | | ------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `job` | object | The job, with the fields of [Get a job](https://docs.reversecontact.com/endpoints/job#response-structure). **Save its `id`**: it reads, downloads and cancels the job. | | `duplicateCount` | number | Items that name the same person as an earlier item: looked up once, free | | `creditsEstimated` | number | Credits reserved for the job: the most its items can cost | | `notRunForCredits` | number | Items not run because your balance did not cover them | `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). `quotas.creditsConsumed` is `0`, and `quotas.workspace.credits` is read before the job reserves its credits: `creditsEstimated` is what the job took, and [Usage](https://docs.reversecontact.com/endpoints/usage) gives the balance after it. Then read the job at `job.links.self` while its `status` is `queued` or `running`: see [Get a job](https://docs.reversecontact.com/endpoints/job). ## Webhook callback With `webhookUrl`, that URL receives one `POST` when the job settles: it completed, was cancelled, or failed. It receives nothing while the job runs. The event says which job settled and carries no results: download them at `resultsUrl` with your API key. This event is not the lookup callback of the [Webhooks guide](https://docs.reversecontact.com/guides/webhooks): it has its own payload, below, and is sent to `webhookUrl` only, never to the workspace webhook URL of **Settings > Webhooks**. | Field | Type | Description | | ------------ | -------------- | ------------------------------------------------ | | `event` | string | Always `job.updated` | | `jobId` | string | The job that settled | | `type` | string | Always `enrich_persons` | | `status` | string | `completed`, `cancelled` or `failed` | | `pollUrl` | string | Path that reads the job | | `resultsUrl` | string \| null | Path of the results file, `null` for a failed job | | `occurredAt` | string | When the event was sent (ISO 8601) | The request is JSON and its `x-reversecontact-event` header names the event, `job.updated`. Answer with a `2xx` within 10 seconds. A timeout, a network error, a `408`, a `429` or a `5xx` is tried again after 2 then 5 seconds. If these three tries fail, the delivery is tried again 1, 2, 4, 8, 16 and 32 minutes later, then twice an hour apart: about 3 hours in all. A redirect is never followed, and a redirect or another `4xx` is not tried again. A retried delivery can reach you after your endpoint already handled it, so the same event can arrive twice: key it on `jobId` and `status`. If the event has not arrived, read the job at `pollUrl`. The URL must be public HTTPS: a private, local or unresolvable host returns `400 WEBHOOK_URL_UNREACHABLE` and nothing is created. It is checked again before each delivery. ## Errors | Status | Code | Meaning | | ------ | ------------------------------- | -------------------------------------------------------------------------------------- | | `400` | `WEBHOOK_URL_UNREACHABLE` | `webhookUrl` is not a public HTTPS URL | | `402` | `BULK_JOB_INSUFFICIENT_CREDITS` | Your balance does not cover a single item | | `403` | `INSUFFICIENT_PERMISSIONS` | The key lacks the permission of a lookup the job runs | | `403` | `BULK_JOB_PHONE_FINDER_DISABLED` | `contact_phone` is asked on a workspace where phone finding is not enabled | | `403` | `KEY_CREDIT_LIMIT_REACHED` | The job would take the key over its total credit limit | | `413` | `REQUEST_BODY_TOO_LARGE` | The JSON body is over 25 MB | | `422` | `UNKNOWN_FIELD` | The body or an item holds a field the job does not define | | `422` | `VALIDATION_ERROR` | A required field is missing, a value is not accepted, or more than 25,000 items | | `422` | `BULK_JOB_CSV_PARSE_ERROR` | No item has a Social profile URL, an email or a name the selected families can look up | | `429` | `KEY_CREDIT_LIMIT_REACHED` | The job would take the key over its daily or monthly credit limit | | `503` | `BULK_QUEUE_UNAVAILABLE` | The job queue is busy: retry shortly, with the same `Idempotency-Key` | The `Idempotency-Key` errors are listed in [Idempotency-Key header](#idempotency-key-header). ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/enrich/persons/bulk \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 9e2f4a1c-6b3d-4e7f-8a5c-2d1b0e9f8a7c" \ -d '{ "name": "CRM export, November", "items": [ { "linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001" }, { "firstName": "John", "lastName": "Smith", "companyDomain": "acme.com", "externalId": "crm-002" } ], "families": ["profile", "contact_email"], "webhookUrl": "https://example.com/hooks/reversecontact" }' ``` ```javascript [JavaScript] const KEY = "YOUR_API_KEY"; const BASE = "https://api.reversecontact.com"; const headers = { "Authorization": `Bearer ${KEY}` }; // Start the job, then follow it until it settles. const created = await fetch(`${BASE}/v2/enrich/persons/bulk`, { method: "POST", headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() }, body: JSON.stringify({ name: "CRM export, November", items: [ { linkedinUrl: "https://www.social.com/in/janedoe", externalId: "crm-001" }, { firstName: "John", lastName: "Smith", companyDomain: "acme.com", externalId: "crm-002" } ], families: ["profile", "contact_email"] }) }); let { data: { job } } = await created.json(); while (job.status === "queued" || job.status === "running") { const response = await fetch(`${BASE}${job.links.self}?wait=20`, { headers }); job = (await response.json()).data; } if (job.links.results) { const csv = await (await fetch(`${BASE}${job.links.results}`, { headers })).text(); console.log(job.status, `${job.credits.used} credits`, csv.split("\n").length - 1, "lines"); } ``` ```python [Python] import uuid import requests KEY = "YOUR_API_KEY" BASE = "https://api.reversecontact.com" headers = {"Authorization": f"Bearer {KEY}"} job = requests.post( f"{BASE}/v2/enrich/persons/bulk", headers={**headers, "Idempotency-Key": str(uuid.uuid4())}, json={ "name": "CRM export, November", "items": [ {"linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001"}, {"firstName": "John", "lastName": "Smith", "companyDomain": "acme.com", "externalId": "crm-002"} ], "families": ["profile", "contact_email"] } ).json()["data"]["job"] while job["status"] in ("queued", "running"): job = requests.get(f"{BASE}{job['links']['self']}", headers=headers, params={"wait": 20}, timeout=30).json()["data"] if job["links"]["results"]: with open("results.csv", "wb") as file: file.write(requests.get(f"{BASE}{job['links']['results']}", headers=headers).content) print(job["status"], job["credits"]["used"], "credits") ``` ## Example response **Example Response** ```json [202 - Created] { "success": true, "data": { "job": { "id": "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60", "type": "enrich_persons", "entity": "persons", "status": "queued", "source": "api", "name": "CRM export, November", "listId": null, "progress": { "total": 2, "processed": 0, "succeeded": 0, "failed": 0, "skipped": 0, "remaining": 2 }, "credits": { "estimated": 9, "used": null }, "estimatedSecondsRemaining": null, "error": null, "createdAt": "2026-11-04T09:30:00.000Z", "startedAt": null, "completedAt": null, "links": { "self": "/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60", "results": null, "operation": null } }, "duplicateCount": 0, "creditsEstimated": 9, "notRunForCredits": 0 }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4522, "left": 5478 } } }, "metadata": { "requestId": "91f305d7-392b-4af2-9663-b5dd6e943ab8", "executionTimeMs": 120 } } ``` ```json [Webhook event] { "event": "job.updated", "jobId": "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60", "type": "enrich_persons", "status": "completed", "pollUrl": "/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60", "resultsUrl": "/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60/results", "occurredAt": "2026-11-04T09:31:12.000Z" } ``` --- # Job history > Read every job of your workspace, newest first, whoever started it, from the API, an AI assistant or the app, with filters by type, status and List, for free. - Page: https://docs.reversecontact.com/endpoints/jobs - Endpoint: `GET https://api.reversecontact.com/v2/jobs` - Credits: Free. - Answer: synchronous. - Available in: REST API, MCP tool `list_jobs`, TypeScript and Python SDKs, CLI command `rc bulk list`. - Last verified: 2026-11-04 ## Introduction The Job history endpoint returns the background jobs of your workspace, newest first: the jobs of [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons), and the jobs that run the same way in the app or through a [List](https://docs.reversecontact.com/guides/lists), such as a List import or a Search exported as a file. It covers every job, whoever started it: an API key, an AI assistant through the [MCP server](https://docs.reversecontact.com/guides/mcp), Ask or a member of your team in the app. Each item has the fields of [Get a job](https://docs.reversecontact.com/endpoints/job#response-structure). This endpoint is **free** (0 credits), also at a zero balance. It counts toward your per-minute rate limit like any other call. A key sees the jobs its permissions allow: Person Enrichment or Contact for the `enrich_persons` jobs, Lists for the List jobs, Search for the Search exports. A key with no matching permission gets an empty page. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Query Parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `type` | string | No | Only these types, comma-separated, see [Job types](#job-types) | | `status` | string | No | Only these statuses, comma-separated: `queued`, `running`, `paused`, `completed`, `cancelled`, `failed` | | `listId` | string | No | Only the jobs of this List (UUID) | | `perPage` | integer | No | Jobs per page, from 1 to 100. Defaults to 50. | | `cursor` | string | No | The `nextCursor` of the previous page, sent with the same filters. A malformed cursor, or one sent with other filters, returns `400`. | The route is `GET /v2/jobs`. A `type` or `status` that is not in its list returns `400 JOB_INVALID_INPUT` with the accepted values. ### Job types | `type` | What the job does | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `enrich_persons` | Looks up people and returns a CSV: [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons), or a file enriched with Bulk Import in the app | | `list_import` | Imports Signals or a file into a List: [Import members and data](https://docs.reversecontact.com/endpoints/list-intake-signals) | | `list_enrich` | Buys data for the members of a List | | `list_search_import` | Adds the results of a Search to a List | | `search_export` | Exports the results of a Search as a file | Only an `enrich_persons` job has a results CSV at [Download job results](https://docs.reversecontact.com/endpoints/job-results). The members of a List job are read with [List members](https://docs.reversecontact.com/endpoints/list-members), and its `links.operation` is the [List operation](https://docs.reversecontact.com/endpoints/list-operation) it runs as. ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (JobPage): One page of jobs | Field | Type | Description | | ------------ | -------------- | ----------------------------------------------------------------------------------------------------------- | | `items` | Job[] | The jobs of this page, newest first, with the fields of [Get a job](https://docs.reversecontact.com/endpoints/job#response-structure) | | `nextCursor` | string \| null | Send it back as `cursor` for the next page, `null` on the last page | `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). ## Errors | Status | Code | Meaning | | ------ | ------------------- | ----------------------------------------------------------------------------------------------------------------- | | `400` | `JOB_INVALID_INPUT` | An unknown `type` or `status`, a `perPage` out of range, or a cursor that is malformed or sent with other filters | | `422` | `VALIDATION_ERROR` | `listId` is not a UUID | ## Code examples **Code Examples** ```bash [cURL] curl "https://api.reversecontact.com/v2/jobs?type=enrich_persons&status=completed,cancelled" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript [JavaScript] const headers = { "Authorization": "Bearer YOUR_API_KEY" }; const jobs = []; let cursor = null; // Read every completed or cancelled job, page by page. do { const query = new URLSearchParams({ type: "enrich_persons", status: "completed,cancelled", perPage: "100" }); if (cursor) query.set("cursor", cursor); const response = await fetch(`https://api.reversecontact.com/v2/jobs?${query}`, { headers }); const { data } = await response.json(); jobs.push(...data.items); cursor = data.nextCursor; } while (cursor); for (const job of jobs) console.log(job.name, job.status, job.credits.used); ``` ```python [Python] import requests headers = {"Authorization": "Bearer YOUR_API_KEY"} params = {"type": "enrich_persons", "status": "completed,cancelled", "perPage": 100} jobs = [] while True: data = requests.get("https://api.reversecontact.com/v2/jobs", headers=headers, params=params).json()["data"] jobs.extend(data["items"]) if not data["nextCursor"]: break params["cursor"] = data["nextCursor"] for job in jobs: print(job["name"], job["status"], job["credits"]["used"]) ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "items": [ { "id": "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60", "type": "enrich_persons", "entity": "persons", "status": "completed", "source": "api", "name": "CRM export, November", "listId": null, "progress": { "total": 2, "processed": 2, "succeeded": 2, "failed": 0, "skipped": 0, "remaining": 0 }, "credits": { "estimated": 9, "used": 6 }, "estimatedSecondsRemaining": null, "error": null, "createdAt": "2026-11-04T09:30:00.000Z", "startedAt": "2026-11-04T09:30:01.000Z", "completedAt": "2026-11-04T09:31:12.000Z", "links": { "self": "/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60", "results": "/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60/results", "operation": null } }, { "id": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f", "type": "list_import", "entity": "persons", "status": "running", "source": "dashboard", "name": "Trade show leads.csv", "listId": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40", "progress": { "total": 500, "processed": 120, "succeeded": 114, "failed": 6, "skipped": 0, "remaining": 380 }, "credits": { "estimated": 0, "used": 456 }, "estimatedSecondsRemaining": 240, "error": null, "createdAt": "2026-11-04T09:12:00.000Z", "startedAt": "2026-11-04T09:12:02.000Z", "completedAt": null, "links": { "self": "/v2/jobs/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f", "results": null, "operation": "/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress" } } ], "nextCursor": null }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4528, "left": 5472 } } }, "metadata": { "requestId": "527e1f57-3514-4080-a649-adb2983ffb0f", "executionTimeMs": 44 } } ``` --- # Get a job > Follow a job with long polling until it settles, with its progress, the credits it reserved and spent, and the link to its results, for free. - Page: https://docs.reversecontact.com/endpoints/job - Endpoint: `GET https://api.reversecontact.com/v2/jobs/:jobId` - Credits: Free, also at a zero balance, and never counted against the rate limits. - Answer: synchronous. - Available in: REST API, MCP tool `get_job`, TypeScript and Python SDKs, CLI command `rc bulk get`. - Last verified: 2026-11-04 ## Introduction The Get a job endpoint reads one job: it is the `links.self` path of a job you [created](https://docs.reversecontact.com/endpoints/bulk-enrich-persons). It says how far the job is, what it reserved and spent, and, once it is completed or cancelled, where to download its results. It reads any job of the [job history](https://docs.reversecontact.com/endpoints/jobs) too, such as a List import or a Search export started in the app. Reading a job is **free**, also at a zero balance, and never counted against your rate limits, so you can follow a long job at the pace it asks for. A key reads the jobs its permissions allow: Person Enrichment or Contact for a [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) job, Lists for a List job, Search for a Search export. A job the key may not read returns `404`, as a job that does not exist. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request The route is `GET /v2/jobs/:jobId`. ### Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | -------------------------- | | `jobId` | string | Yes | The `id` of the job (UUID) | ### Query Parameters | Name | Type | Required | Description | | ------ | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `wait` | integer | No | Hold the request up to this many seconds, 0 to 20, while the job is queued or running. It answers as soon as the job settles or pauses, or with the job still running when the wait ends. Defaults to 0. | ### How to follow a job Read the job while its `status` is `queued` or `running`, with `wait=20` and an HTTP client timeout above it, for example 30 seconds. While the job moves, the response carries a `Retry-After: 2` header: start two reads at least that far apart. A held read already lasts longer, so the next one starts right away. | `status` | What it means | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `queued` | Waiting for its turn: read it again | | `running` | Items are being looked up: read it again | | `paused` | It waits to be resumed: a member of your workspace paused it in the app, or a List import reached its `maxCredits` or your balance | | `completed` | Every item was handled: download the results at `links.results` | | `cancelled` | Stopped: the items not started were never charged. The results of the items done are at `links.results`, where an item still in flight reads `running` until it ends, within the hour | | `failed` | The job could not run: `error.code` says why, and it has no results file | `completed`, `cancelled` and `failed` are final. The [webhook](https://docs.reversecontact.com/endpoints/bulk-enrich-persons#webhook-callback) of a job is sent when it reaches one of them. ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (Job): The job | Field | Type | Description | | --------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Job id (UUID) | | `type` | string | `enrich_persons` (Bulk enrichment, or a file enriched in the app), `list_import`, `list_enrich`, `list_search_import` or `search_export`, see [the job history](https://docs.reversecontact.com/endpoints/jobs#job-types) | | `entity` | string | `persons` or `companies` | | `status` | string | `queued`, `running`, `paused`, `completed`, `cancelled` or `failed` | | `source` | string \| null | Who started it: `api` (an API key), `mcp` (an AI assistant), `ask` or `dashboard` (a person in the app). `null` for a job older than this field. | | `name` | string | The `name` given at creation, or the file name of a job started in the app | | `listId` | string \| null | The List a List job works on, `null` otherwise | | `estimatedSecondsRemaining` | number \| null | Seconds left at the recent pace while the job runs, `null` when unknown or settled | | `error` | object \| null | `{ "code": "..." }` when the job failed, `null` otherwise | | `createdAt` | string | Creation date (ISO 8601) | | `startedAt` | string \| null | When it started running, `null` while queued | | `completedAt` | string \| null | When it settled, `null` while it moves | **progress** (JobProgress): How far the job is | Field | Type | Description | | ----------- | ------ | ----------------------------------------------------------------------------- | | `total` | number | Items in the job, duplicates included | | `processed` | number | Items done | | `succeeded` | number | Items that found at least part of what was asked | | `failed` | number | Items that found nothing, or could not be looked up | | `skipped` | number | Items not run: a duplicate, no usable field, an invalid profile URL, not enough credits, or cancelled | | `remaining` | number | Items left to do | **credits** (JobCredits): What the job reserved and spent | Field | Type | Description | | ----------- | -------------- | -------------------------------------------------------------------------------------------- | | `estimated` | number | Credits reserved when the job was created, `0` for a job that reserves nothing up front, such as a List import | | `used` | number \| null | Credits spent. A List import counts them as it goes; any other job reads `null` until it has settled them, then gives back the rest of `estimated`. | **links** (JobLinks): Where to go next | Field | Type | Description | | ----------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `self` | string | Path that reads this job | | `results` | string \| null | Path of the results file, CSV or NDJSON, set once an `enrich_persons` job is completed or cancelled. See [Download job results](https://docs.reversecontact.com/endpoints/job-results). To read the results while the job runs, see [Read job results by page](https://docs.reversecontact.com/endpoints/job-rows). | | `operation` | string \| null | The [List operation](https://docs.reversecontact.com/endpoints/list-operation) a List job runs as, `null` for other jobs | `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). ## Errors | Status | Code | Meaning | | ------ | ------------------ | ------------------------------------------------------------------- | | `404` | `JOB_NOT_FOUND` | No job with this id in your workspace, or one your key may not read | | `422` | `VALIDATION_ERROR` | `jobId` is not a UUID, or `wait` is not an integer from 0 to 20 | ## Code examples **Code Examples** ```bash [cURL] curl "https://api.reversecontact.com/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60?wait=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript [JavaScript] const jobId = "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60"; const headers = { "Authorization": "Bearer YOUR_API_KEY" }; let job; do { const response = await fetch(`https://api.reversecontact.com/v2/jobs/${jobId}?wait=20`, { headers }); job = (await response.json()).data; } while (job.status === "queued" || job.status === "running"); console.log(job.status, `${job.credits.used} of ${job.credits.estimated} credits`, job.links.results); ``` ```python [Python] import requests job_id = "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60" headers = {"Authorization": "Bearer YOUR_API_KEY"} while True: job = requests.get( f"https://api.reversecontact.com/v2/jobs/{job_id}", headers=headers, params={"wait": 20}, timeout=30 ).json()["data"] if job["status"] not in ("queued", "running"): break print(job["status"], job["credits"]["used"], job["links"]["results"]) ``` ## Example response **Example Response** ```json [200 - Completed] { "success": true, "data": { "id": "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60", "type": "enrich_persons", "entity": "persons", "status": "completed", "source": "api", "name": "CRM export, November", "listId": null, "progress": { "total": 2, "processed": 2, "succeeded": 2, "failed": 0, "skipped": 0, "remaining": 0 }, "credits": { "estimated": 9, "used": 6 }, "estimatedSecondsRemaining": null, "error": null, "createdAt": "2026-11-04T09:30:00.000Z", "startedAt": "2026-11-04T09:30:01.000Z", "completedAt": "2026-11-04T09:31:12.000Z", "links": { "self": "/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60", "results": "/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60/results", "operation": null } }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4528, "left": 5472 } } }, "metadata": { "requestId": "e5e6c816-df62-43a7-8fbe-1d3a94b6909c", "executionTimeMs": 31 } } ``` ```json [404 - Not found] { "success": false, "data": null, "error": { "code": "JOB_NOT_FOUND", "message": "No job found with that identifier" }, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4528, "left": 5472 } } }, "metadata": { "requestId": "6408b12a-8668-464f-97f3-3a1497efb4f5", "executionTimeMs": 12 } } ``` --- # Download job results > Download the results of a completed or cancelled job as one CSV or NDJSON file, one row per item in the order sent, with your externalId, what was found and what it cost, for free. - Page: https://docs.reversecontact.com/endpoints/job-results - Endpoint: `GET https://api.reversecontact.com/v2/jobs/:jobId/results` - Credits: Free, also at a zero balance. - Answer: synchronous. - Available in: REST API, MCP tool `get_job_results`, TypeScript and Python SDKs, CLI command `rc bulk results`. - Last verified: 2026-11-04 ## Introduction The Download job results endpoint returns the results of a job you [created](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) as one file: one row per item, in the order you sent them, duplicates included, with your own fields and `externalId` first, then what was found for the item and what it cost. It is the `links.results` path of the job. By default the file is a CSV, the same file the app downloads; with `format=ndjson`, it holds one JSON object per item, for a program to read. The file is ready once the job is `completed` or `cancelled`. Before that, and for a failed job, the request returns `409 JOB_RESULTS_NOT_READY`. Follow the job with [Get a job](https://docs.reversecontact.com/endpoints/job) and download the file when `links.results` is set, or read the results as they come with [Read job results by page](https://docs.reversecontact.com/endpoints/job-rows). A cancelled job gives the results of the items done, and the items it never started read `skipped`. It still finishes the items it had started, within the hour: until each one ends, its row reads `running`, and a file downloaded after that gives its outcome. Downloading the results is **free** (0 credits), also at a zero balance, and as often as you need. It counts toward your per-minute rate limit like any other call. The key needs Person Enrichment or Contact, the permissions that read an `enrich_persons` job. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request The route is `GET /v2/jobs/:jobId/results`. ### Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | -------------------------- | | `jobId` | string | Yes | The `id` of the job (UUID) | ### Query Parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------------------------- | | `format` | string | No | `csv` (default), or `ndjson` for one JSON object per line | ## Response structure The response is the file itself, not a JSON envelope, and a large file is streamed: read the body to its end. A `Content-Disposition` header names the file after the job, ending in `_enriched.csv` or `_enriched.ndjson`. ### CSV With `format=csv`, the default, the file is `Content-Type: text/csv; charset=utf-8`. It is UTF-8 and starts with a byte order mark, so a spreadsheet opens accented names correctly. The columns depend on the `families` of the job, never on its rows, so every row has the same header: | Columns | What they hold | | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `linkedinUrl`, `email`, `firstName`, `lastName`, `companyDomain`, `companyName`, `externalId` | The item as you sent it | | `rc_status` | What happened to the item, see the table below | | `rc_message` | The same in a sentence, with the credits the item cost | | `rc_email`, `rc_email_status` | With `contact_email`: the email found, and `found`, `not_found`, `error` or `skipped`. With `emailType: "any"`, `rc_personal_email` holds the personal email. | | `rc_phone`, `rc_phone_type`, `rc_phone_status` | With `contact_phone`: the number found, its type, and its status | | `rc_id`, `rc_first_name`, `rc_headline` and the other `rc_` profile columns | With `profile`: one column per field of the [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person#response-structure) response, nested fields joined with `_`, lists such as `rc_experience` as JSON. Without `profile`, only `rc_linkedin_url`, the profile the finders searched from. | | `rc_status` | Meaning | | ----------------------- | ---------------------------------------------------------------------------------------------------------------- | | `enriched` | Everything asked was found | | `partial` | Part of what was asked was found, such as the profile but no email | | `not_found` | Nothing was found; only a profile matched on the way to a finder is charged | | `duplicate` | The same person as an earlier item, looked up once on that row: free | | `skipped` | Not run: the item had no field, nothing the families can look up, or the job was cancelled before it. Free. | | `insufficient_credits` | Not run because your balance did not cover it when the job was created. Free. | | `invalid_input` | The item was rejected, such as an invalid Social profile URL. Free. | | `data_subject_blocked` | This person asked not to be processed under GDPR or CCPA. Free. | | `rate_limited`, `error` | The lookup could not be done after retries. Free: send the item again later. | | `unknown` | The lookup may have run but its result was not saved. It is not run again on its own. | | `running` | A cancelled job had started the item and it has not ended yet. It ends within the hour: download the file again for its outcome. | ### NDJSON With `format=ndjson`, the file is `Content-Type: application/x-ndjson; charset=utf-8`: one JSON object per item, one per line, in the order sent. Each object says what the CSV row says, typed, with the person in the shape of [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person#response-structure). [Read job results by page](https://docs.reversecontact.com/endpoints/job-rows) returns the same objects, a page at a time. | Field | Type | Description | | ----------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `row` | number | Position of the item in the job, from 1 | | `input` | object | The item as you sent it, `externalId` included, empty fields left out | | `status` | string | The `rc_status` of the item, see the table above | | `message` | string | The `rc_message` of the item, with the credits it cost | | `creditsConsumed` | number | Credits this item cost | | `person` | object \| null | The person found: the full profile when `isFullProfile` is `true`. `null` when no one was found or the job did not ask for the profile. | | `isFullProfile` | boolean | `true` when `person` is the full profile | | `linkedinUrl` | string | Without `profile` in the job: the Social profile URL the finders searched from, as `rc_linkedin_url` | | `contactEmail` | object | With `contact_email`: `status` (`found`, `not_found`, `error` or `skipped`, `null` when the finder did not run), `value`, and `personalValue` with `emailType: "any"` | | `contactPhone` | object | With `contact_phone`: `status` (`found`, `not_found`, `error` or `skipped`, `null` when the finder did not run), `value` and `type` | A job whose results were stored before NDJSON existed has its CSV only: `format=ndjson` returns `409 JOB_RESULTS_FORMAT_UNAVAILABLE`. ## Errors The errors are JSON, in the envelope of every V2 response. | Status | Code | Meaning | | ------ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `404` | `JOB_NOT_FOUND` | No `enrich_persons` job with this id in your workspace, or one your key may not read. A List job or a Search export has no results file here. | | `409` | `JOB_RESULTS_NOT_READY` | The job is still `queued`, `running` or `paused`, or it `failed` and has no file | | `409` | `JOB_RESULTS_FORMAT_UNAVAILABLE` | `format=ndjson` on a job whose results were stored before NDJSON existed: download its CSV | | `422` | `VALIDATION_ERROR` | `jobId` is not a UUID, or `format` is not `csv` or `ndjson` | ## Code examples **Code Examples** ```bash [cURL] curl https://api.reversecontact.com/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60/results \ -H "Authorization: Bearer YOUR_API_KEY" \ -o results.csv ``` ```bash [cURL (NDJSON)] curl "https://api.reversecontact.com/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60/results?format=ndjson" \ -H "Authorization: Bearer YOUR_API_KEY" \ -o results.ndjson ``` ```javascript [JavaScript] import { writeFile } from "node:fs/promises"; const jobId = "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60"; const response = await fetch(`https://api.reversecontact.com/v2/jobs/${jobId}/results`, { headers: { "Authorization": "Bearer YOUR_API_KEY" } }); if (response.ok) { await writeFile("results.csv", Buffer.from(await response.arrayBuffer())); } else { const { error } = await response.json(); console.log(error.code, error.message); } ``` ```python [Python] import requests job_id = "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60" response = requests.get( f"https://api.reversecontact.com/v2/jobs/{job_id}/results", headers={"Authorization": "Bearer YOUR_API_KEY"}, stream=True ) if response.ok: with open("results.csv", "wb") as file: for chunk in response.iter_content(chunk_size=65536): file.write(chunk) else: print(response.json()["error"]["code"]) ``` ```python [Python (NDJSON)] import json import requests job_id = "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60" response = requests.get( f"https://api.reversecontact.com/v2/jobs/{job_id}/results", headers={"Authorization": "Bearer YOUR_API_KEY"}, params={"format": "ndjson"}, stream=True ) response.raise_for_status() for line in response.iter_lines(): if line: item = json.loads(line) print(item["row"], item["input"].get("externalId"), item["status"]) ``` ## Example response The CSV below is cut after the first profile columns: the file carries every `rc_` column of the profile. The NDJSON example cuts `person` after its first fields. **Example Response** ```text [200 - CSV file] linkedinUrl,email,firstName,lastName,companyDomain,companyName,externalId,rc_status,rc_message,rc_email,rc_email_status,rc_id,rc_public_id,rc_linkedin_url,rc_first_name,rc_last_name,rc_headline https://www.social.com/in/janedoe,,,,,,crm-001,enriched,Profile and email found (4 credits),jane.doe@acme.com,found,prs_9f8e7d6c5b4a,janedoe,https://www.social.com/in/janedoe,Jane,Doe,VP of Marketing @ Acme Corp ,,John,Smith,acme.com,,crm-002,partial,"Profile found, no email (2 credits)",,not_found,prs_1a2b3c4d5e6f,john-smith,https://www.social.com/in/john-smith,John,Smith,Head of Sales at Acme ``` ```text [200 - NDJSON file] {"row":1,"input":{"linkedinUrl":"https://www.social.com/in/janedoe","externalId":"crm-001"},"status":"enriched","message":"Profile and email found (4 credits)","creditsConsumed":4,"person":{"id":"prs_9f8e7d6c5b4a","firstName":"Jane","lastName":"Doe","linkedinUrl":"https://www.social.com/in/janedoe","headline":"VP of Marketing @ Acme Corp"},"isFullProfile":true,"contactEmail":{"status":"found","value":"jane.doe@acme.com","personalValue":null}} {"row":2,"input":{"firstName":"John","lastName":"Smith","companyDomain":"acme.com","externalId":"crm-002"},"status":"partial","message":"Profile found, no email (2 credits)","creditsConsumed":2,"person":{"id":"prs_1a2b3c4d5e6f","firstName":"John","lastName":"Smith","linkedinUrl":"https://www.social.com/in/john-smith","headline":"Head of Sales at Acme"},"isFullProfile":true,"contactEmail":{"status":"not_found","value":null,"personalValue":null}} ``` ```json [409 - Not ready] { "success": false, "data": null, "error": { "code": "JOB_RESULTS_NOT_READY", "message": "The job is running: its results file is ready once it is completed or cancelled" }, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4531, "left": 5469 } } }, "metadata": { "requestId": "91574469-c62b-46b7-973a-16bc4211dfc8", "executionTimeMs": 15 } } ``` --- # Read job results by page > Read the results of a Bulk enrichment job a page at a time, in the order sent, from the start of the run, with a cursor that never skips an item, for free. - Page: https://docs.reversecontact.com/endpoints/job-rows - Endpoint: `GET https://api.reversecontact.com/v2/jobs/:jobId/rows` - Credits: Free, also at a zero balance. Each page counts toward the rate limits. - Answer: synchronous. - Available in: REST API, MCP tool `get_job_rows`, TypeScript and Python SDKs, CLI command `rc bulk rows`. - Last verified: 2026-11-04 ## Introduction The Read job results by page endpoint returns the results of a job you [created](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) as JSON, a page at a time, in the order you sent the items. Each item has the same fields as a line of the [NDJSON results file](https://docs.reversecontact.com/endpoints/job-results#ndjson): your own fields and `externalId`, what happened, what it cost, and the person found. You can read it from the start of the run: there is no need to wait for the job to settle. While the job runs, a page stops at the first item not finished yet, so a cursor never skips an item and every item you read is final. When the next item is still running, the page is empty, `nextCursor` stays set and the response carries a `Retry-After` header: read the same cursor again after that many seconds. `nextCursor` is `null` once nothing more will come. A cursor keeps working after the job settles, so you can stop reading and pick up where you left off. A cancelled job still finishes the items it had started, within the hour: a page waits for them the same way. While the job is paused, the page stays empty until it runs again. This endpoint is **free** (0 credits), also at a zero balance. Each page counts toward your rate limits like any other call, and while the job runs, its items take up to 80% of your per-minute limit: read with `limit=100`, at the `Retry-After` pace. The TypeScript and Python SDKs and `rc bulk rows --all` wait out a `429` on their own. The key needs Person Enrichment or Contact, the permissions that read an `enrich_persons` job. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request The route is `GET /v2/jobs/:jobId/rows`. ### Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | -------------------------- | | `jobId` | string | Yes | The `id` of the job (UUID) | ### Query Parameters | Name | Type | Required | Description | | -------- | ------- | -------- | -------------------------------------------------------------------------------------------- | | `cursor` | string | No | `nextCursor` of the previous page, up to 512 characters. Without it, the page starts at the first item. | | `limit` | integer | No | Items per page, 1 to 100. Defaults to 100. | A page can hold fewer items than `limit`: it stops at the first item not finished, and before 2 MB of JSON. ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (JobRowsPage): One page of results | Field | Type | Description | | ------------ | -------------- | --------------------------------------------------------------------------- | | `items` | array | The items of this page, in the order sent, see below | | `nextCursor` | string \| null | Pass it as `cursor` to read on. `null` once nothing more will come. | | `status` | string | The job status when the page was read, as in [Get a job](https://docs.reversecontact.com/endpoints/job) | **items** (JobResultLine): One item of the job | Field | Type | Description | | ----------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `row` | number | Position of the item in the job, from 1 | | `input` | object | The item as you sent it, `externalId` included, empty fields left out | | `status` | string | What happened to the item, as the `rc_status` column: see [Download job results](https://docs.reversecontact.com/endpoints/job-results#response-structure) | | `message` | string | The same in a sentence, with the credits the item cost | | `creditsConsumed` | number | Credits this item cost | | `person` | object \| null | The person found, in the shape of [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person#response-structure). `null` when no one was found or the job did not ask for the profile. | | `isFullProfile` | boolean | `true` when `person` is the full profile | | `linkedinUrl` | string | Without `profile` in the job: the Social profile URL the finders searched from | | `contactEmail` | object | With `contact_email`: `status` (`found`, `not_found`, `error` or `skipped`, `null` when the finder did not run), `value`, and `personalValue` with `emailType: "any"` | | `contactPhone` | object | With `contact_phone`: `status` (`found`, `not_found`, `error` or `skipped`, `null` when the finder did not run), `value` and `type` | `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). ## Errors | Status | Code | Meaning | | ------ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | `JOB_INVALID_INPUT` | `cursor` is not a position of this job, such as a cursor of another job | | `404` | `JOB_NOT_FOUND` | No `enrich_persons` job with this id in your workspace, or one your key may not read. A List job or a Search export has no results here. | | `409` | `JOB_RESULTS_FORMAT_UNAVAILABLE` | The results of this job were stored before this endpoint existed: download them as CSV with [Download job results](https://docs.reversecontact.com/endpoints/job-results) | | `422` | `VALIDATION_ERROR` | `jobId` is not a UUID, `limit` is not an integer from 1 to 100, or `cursor` is longer than 512 characters | | `429` | `JOB_ROWS_BUSY` | Too many pages are being read at once: wait `error.details.retryAfter` seconds, given in the `Retry-After` header too, then read again | A `429` can also come from your rate limits: see [Rate limits](https://docs.reversecontact.com/guides/rate-limits). ## Code examples **Code Examples** ```bash [cURL] curl "https://api.reversecontact.com/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60/rows?limit=100" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript [JavaScript] const jobId = "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60"; const headers = { "Authorization": "Bearer YOUR_API_KEY" }; const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000)); let cursor = null; do { const query = new URLSearchParams({ limit: "100", ...(cursor ? { cursor } : {}) }); const response = await fetch(`https://api.reversecontact.com/v2/jobs/${jobId}/rows?${query}`, { headers }); if (response.status === 429) { await sleep(Number(response.headers.get("retry-after") ?? 2)); continue; } const { data } = await response.json(); for (const item of data.items) console.log(item.row, item.input.externalId, item.status); if (data.items.length === 0 && data.nextCursor) { if (data.status === "paused") break; await sleep(Number(response.headers.get("retry-after") ?? 2)); } cursor = data.nextCursor; } while (cursor); ``` ```python [Python] import time import requests job_id = "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60" headers = {"Authorization": "Bearer YOUR_API_KEY"} cursor = None while True: params = {"limit": 100, **({"cursor": cursor} if cursor else {})} response = requests.get( f"https://api.reversecontact.com/v2/jobs/{job_id}/rows", headers=headers, params=params ) if response.status_code == 429: time.sleep(int(response.headers.get("Retry-After", 2))) continue data = response.json()["data"] for item in data["items"]: print(item["row"], item["input"].get("externalId"), item["status"]) if not data["items"] and data["nextCursor"]: if data["status"] == "paused": break time.sleep(int(response.headers.get("Retry-After", 2))) cursor = data["nextCursor"] if cursor is None: break ``` ## Example response The example cuts `person` after its first fields: a page carries the whole profile. **Example Response** ```json [200 - A page] { "success": true, "data": { "items": [ { "row": 1, "input": { "linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001" }, "status": "enriched", "message": "Profile and email found (4 credits)", "creditsConsumed": 4, "person": { "id": "prs_9f8e7d6c5b4a", "firstName": "Jane", "lastName": "Doe", "linkedinUrl": "https://www.social.com/in/janedoe", "headline": "VP of Marketing @ Acme Corp" }, "isFullProfile": true, "contactEmail": { "status": "found", "value": "jane.doe@acme.com", "personalValue": null } }, { "row": 2, "input": { "firstName": "John", "lastName": "Smith", "companyDomain": "acme.com", "externalId": "crm-002" }, "status": "partial", "message": "Profile found, no email (2 credits)", "creditsConsumed": 2, "person": { "id": "prs_1a2b3c4d5e6f", "firstName": "John", "lastName": "Smith", "linkedinUrl": "https://www.social.com/in/john-smith", "headline": "Head of Sales at Acme" }, "isFullProfile": true, "contactEmail": { "status": "not_found", "value": null, "personalValue": null } } ], "nextCursor": "eyJ2IjoxLCJyb3ciOjN9", "status": "running" }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4531, "left": 5469 } } }, "metadata": { "requestId": "41f7ce39-14eb-4f81-a499-bcbd97d437ed", "executionTimeMs": 42 } } ``` ```json [200 - Next item still running] { "success": true, "data": { "items": [], "nextCursor": "eyJ2IjoxLCJyb3ciOjN9", "status": "running" }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4531, "left": 5469 } } }, "metadata": { "requestId": "6f4a5cba-ef9e-470b-ab60-821d4781673c", "executionTimeMs": 18 } } ``` ```json [429 - Busy] { "success": false, "data": null, "error": { "code": "JOB_ROWS_BUSY", "message": "Too many result pages read at once: retry in 2 seconds", "details": { "retryAfter": 2 } }, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4531, "left": 5469 } } }, "metadata": { "requestId": "def7c408-4e1c-4b83-8858-8e87de88925a", "executionTimeMs": 3 } } ``` --- # Cancel a job > Stop a job an API key or an AI assistant started, for good. Items not started are never charged, and the results of the items done stay downloadable. - Page: https://docs.reversecontact.com/endpoints/job-cancel - Endpoint: `POST https://api.reversecontact.com/v2/jobs/:jobId/cancel` - Credits: Free. Items that had not started are never charged. - Answer: synchronous. - Available in: REST API, MCP tool `cancel_job`, TypeScript and Python SDKs, CLI command `rc bulk cancel`. - Last verified: 2026-11-04 ## Introduction The Cancel a job endpoint stops a job for good. Items already in flight may finish and are charged as usual; items not started are never charged and read `skipped` in the results. The job then reads `cancelled`, and the results of the items done can be downloaded with [Download job results](https://docs.reversecontact.com/endpoints/job-results). An item in flight finishes within the hour: until then, its row of the results file reads `running`, and [Read job results by page](https://docs.reversecontact.com/endpoints/job-rows) waits for it. Any key of the workspace that can read a job can cancel it, whichever key started it: a rotated key can still stop a job its predecessor created. This covers every job of the [history](https://docs.reversecontact.com/endpoints/jobs) that an API key or an AI assistant started (`source` is `api` or `mcp`), a List import included. A job a person started in the app or with Ask is stopped there: cancelling it here returns `403 JOB_STARTED_BY_A_PERSON`. Cancelling is **free** (0 credits), also at a zero balance, and final: a cancelled job is never resumed, and a new run needs a new job. Cancelling a job already cancelled answers it again. When the job has a `webhookUrl`, that URL receives the `cancelled` state. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request The route is `POST /v2/jobs/:jobId/cancel`. ### Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | -------------------------- | | `jobId` | string | Yes | The `id` of the job (UUID) | The request has no body. ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------- | | `success` | boolean | `true` if the job was cancelled | | `error` | `null` | Always `null` on success | `data` is the cancelled job, with the fields of [Get a job](https://docs.reversecontact.com/endpoints/job#response-structure) and `status` set to `cancelled`. Its `progress` can still move while the items in flight finish, and `credits.used` stays `null` until the job has settled its credits (a List import counts them as it goes): read the job again to see the final counts. `quotas` and `metadata` are the blocks of every V2 response: see [Usage](https://docs.reversecontact.com/endpoints/usage#response-structure). ## Errors | Status | Code | Meaning | | ------ | ------------------------- | ------------------------------------------------------------------- | | `403` | `JOB_STARTED_BY_A_PERSON` | A person started this job in the app or with Ask: stop it there | | `404` | `JOB_NOT_FOUND` | No job with this id in your workspace, or one your key may not read | | `409` | `JOB_NOT_CANCELLABLE` | The job already completed or failed: there is nothing left to stop | | `422` | `VALIDATION_ERROR` | `jobId` is not a UUID | ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60/cancel \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript [JavaScript] const jobId = "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60"; const response = await fetch(`https://api.reversecontact.com/v2/jobs/${jobId}/cancel`, { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY" } }); const { data, error } = await response.json(); console.log(error ? error.code : `${data.status}, ${data.progress.remaining} items not started`); ``` ```python [Python] import requests job_id = "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60" response = requests.post( f"https://api.reversecontact.com/v2/jobs/{job_id}/cancel", headers={"Authorization": "Bearer YOUR_API_KEY"} ) payload = response.json() print(payload["error"]["code"] if payload["error"] else payload["data"]["status"]) ``` ## Example response **Example Response** ```json [200 - Cancelled] { "success": true, "data": { "id": "5e8d2c1b-9a4f-4b3e-8c7d-6f5a4b3c2d10", "type": "enrich_persons", "entity": "persons", "status": "cancelled", "source": "api", "name": "Leads, Q4", "listId": null, "progress": { "total": 500, "processed": 120, "succeeded": 97, "failed": 23, "skipped": 0, "remaining": 380 }, "credits": { "estimated": 1000, "used": null }, "estimatedSecondsRemaining": null, "error": null, "createdAt": "2026-11-04T10:02:00.000Z", "startedAt": "2026-11-04T10:02:01.000Z", "completedAt": "2026-11-04T10:04:30.000Z", "links": { "self": "/v2/jobs/5e8d2c1b-9a4f-4b3e-8c7d-6f5a4b3c2d10", "results": "/v2/jobs/5e8d2c1b-9a4f-4b3e-8c7d-6f5a4b3c2d10/results", "operation": null } }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 5472, "left": 4528 } } }, "metadata": { "requestId": "2373b182-0821-45fb-b10f-37428d3b8087", "executionTimeMs": 48 } } ``` ```json [409 - Already ended] { "success": false, "data": null, "error": { "code": "JOB_NOT_CANCELLABLE", "message": "The job is completed and can no longer be cancelled" }, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4528, "left": 5472 } } }, "metadata": { "requestId": "d5f5c787-9f5f-42c4-9de8-f94144955cec", "executionTimeMs": 21 } } ``` --- # Usage > Check your workspace credit balance and rate limit status with GET /v2/usage. The call is free and does not count toward your rate limit. - Page: https://docs.reversecontact.com/endpoints/usage - Endpoint: `GET https://api.reversecontact.com/v2/usage` - Credits: Free. - Answer: synchronous. - Available in: REST API, MCP tool `check_usage`, TypeScript and Python SDKs, CLI command `rc usage`. - Last verified: 2026-09-01 ## Introduction The Usage endpoint returns your current workspace credit balance and rate limit status. Use it to track your remaining credits and rate limit windows, **without spending any credits**. Unlike most V2 endpoints, this route uses `GET` and does not require a request body: send an authenticated request and read the `quotas` block in the response. ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Response structure | Field | Type | Description | | --------- | ------- | -------------------------------------------------------------------------------------------- | | `success` | boolean | `true` if the request was processed successfully | | `data` | `null` | Always `null` for this endpoint: the usage information is in the `quotas` block below | | `error` | `null` | Always `null` on success | **quotas** (V2QuotaInfo): Credits and rate limit usage for your workspace and API key | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------------------------- | | `creditsConsumed` | number | Credits consumed by this request. Always `0` for the Usage endpoint | **workspace** (Workspace): Workspace-level limits | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `id` | string | Your workspace identifier | | `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited | | `allowDailyOvercost` | boolean | Whether the workspace may exceed its daily cap | **credits** (Credits): Credit balance | Field | Type | Description | | ------- | ------ | -------------------------- | | `total` | number | Total credits in your plan | | `used` | number | Credits consumed so far | | `left` | number | Remaining credits | **dailyLimit** (DailyLimit): Daily request limit (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (MinuteRateLimit): Per-minute rate limit | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **key** (ApiKey): API key-level limits (`null` when request is from dashboard) | Field | Type | Description | | ----- | ------ | ------------------ | | `id` | string | API key identifier | **dailyLimit** (KeyDailyLimit): Daily request limit for this key (`null` if not configured) | Field | Type | Description | | ----------- | ------ | --------------------------------------- | | `limit` | number | Maximum requests per day | | `used` | number | Requests made today | | `left` | number | Remaining requests today | | `nextReset` | string | When the daily window resets (ISO 8601) | **minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key | Field | Type | Description | | ----------- | ------ | ---------------------------------------- | | `limit` | number | Maximum requests per minute | | `used` | number | Requests made in the current minute | | `left` | number | Remaining requests in the current minute | | `nextReset` | string | When the minute window resets (ISO 8601) | **creditLimits** (KeyCreditLimits): Credit limits of this key, each `null` when not configured | Field | Type | Description | | --------- | ------ | ------------------------------------------------------------------------ | | `daily` | object | Credits per day (UTC): `limit`, `used`, `left`, `nextReset` (00:00 UTC) | | `monthly` | object | Credits per calendar month (UTC): `limit`, `used`, `left`, `nextReset` | | `total` | object | Credits in total since the limit was set or reset: `limit`, `used`, `left`, `nextReset` (`null`) | **metadata** (V2ResponseMetadata): Request tracking information | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `requestId` | string | Unique identifier for this request | | `executionTimeMs` | number | Total execution time in milliseconds | ## Code examples **Code Examples** ```bash [cURL] curl https://api.reversecontact.com/v2/usage \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const usage = await client.usage.retrieve(); console.log("Credits left:", usage.quotas?.workspace.credits.left); console.log("Rate limit:", usage.quotas?.workspace.minuteRateLimit?.left, "requests remaining"); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: usage = client.usage.retrieve() if usage.quotas is not None: workspace = usage.quotas.workspace if workspace is not None and workspace.credits is not None: print("Credits left:", workspace.credits.left) print(usage.quotas.to_dict()) # every quota, with the camelCase names of the API ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": null, "error": null, "metadata": { "requestId": "429922e1-2190-4d6c-93c2-c53a26219c32", "executionTimeMs": 45 }, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 3, "left": 57, "nextReset": "2025-01-15T09:31:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 3, "left": 57, "nextReset": "2025-01-15T09:31:00.000Z" }, "creditLimits": { "daily": { "limit": 2000, "used": 1940, "left": 60, "nextReset": "2025-01-16T00:00:00.000Z" }, "monthly": null, "total": null } } } } ``` --- # People search available fields > Discover the filters accepted by the People Search endpoint at runtime, with every allowed value of closed lists such as the position taxonomy. - Page: https://docs.reversecontact.com/endpoints/search-people-available-fields - Endpoint: `GET https://api.reversecontact.com/v2/search/people/available-fields` - Credits: Free. - Answer: synchronous. - Available in: REST API, MCP tool `list_person_search_filters`, TypeScript and Python SDKs, CLI command `rc search fields people`. - Last verified: 2026-10-29 ## Introduction This endpoint returns metadata describing every filter accepted by [People search](https://docs.reversecontact.com/endpoints/search-people) and [People search count](https://docs.reversecontact.com/endpoints/search-people-count). It lets integrations (for example Clay) discover the accepted filters at runtime instead of hardcoding the list, so new filters appear as soon as we release them. For closed-vocabulary filters, such as the position taxonomy functions, sub-functions and seniorities, the `allowedValues` array lists every accepted value, including all 386 sub-functions. On `currentSubFunction` and `excludeCurrentSubFunction`, an `allowedValueGroups` object also gives the function each sub-function belongs to, so a filter panel can show them as a tree. On `location`, the `area` entry of `properties` lists the accepted zone ids in `allowedValues` and describes every zone in `areaCatalog`: its id, category, name and member countries (see [Area values](https://docs.reversecontact.com/reference/areas)). Like [Usage](https://docs.reversecontact.com/endpoints/usage), this route uses `GET`, takes no request body, and **costs 0 credits**. > Looking for company filters? See [Company search available fields](https://docs.reversecontact.com/endpoints/search-companies-available-fields). ## Endpoint | Method | URL | Describes | | ------ | ------------------------------------- | ------------- | | `GET` | `/v2/search/people/available-fields` | People Search | ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (SearchFields): Container for the discovered fields | Field | Type | Description | | -------- | -------- | ------------------------------------------------- | | `object` | string | Always `"search_fields"` | | `entity` | string | Always `"person"` for this endpoint | | `fields` | `Field[]` | List of accepted filters (see field shape below) | **fields** (Field): One accepted search filter | Field | Type | Description | | -------------- | --------- | --------------------------------------------------------------------------------- | | `name` | string | Filter name, used as a key in the search request body | | `type` | string | `string`, `boolean`, `number`, `range`, `location`, `date-time`, `experience`, or `string \| string[]` | | `required` | boolean | Whether the filter is mandatory (always `false` today; all filters are optional) | | `description` | string | Human-readable explanation of the filter | | `example` | unknown | Example value, when helpful (optional) | | `allowedValues`| `unknown[]` | Closed set of accepted values, when applicable, such as the position taxonomy lists (optional) | | `allowedValueGroups` | object | Sub-function filters only: `parentField` (`"currentFunction"`) and `groups`, which maps each function to its sub-functions (optional) | | `properties` | object | Sub-field shapes for `range`, `location` and `experience` filters (optional) | ## Code examples **Code Examples** ```bash [cURL] curl https://api.reversecontact.com/v2/search/people/available-fields \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.search.peopleFields(); console.log(result.data.fields?.map((field) => field.name)); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.search.people_fields() fields = result.data.fields if result.data and result.data.fields else [] print([field.name for field in fields if field is not None]) ``` ## Example response The response below is truncated to three fields: the catalog describes every filter accepted by People search. **Example Response** ```json [200 - Success] { "success": true, "data": { "object": "search_fields", "entity": "person", "fields": [ { "name": "firstName", "type": "string", "required": false, "description": "Filter by first name", "example": "John" }, { "name": "currentSeniority", "type": "string | string[]", "required": false, "description": "Current job-level taxonomy value", "allowedValues": [ "CXO", "VP", "Director", "Head", "Manager", "Partner", "Owner / Founder", "Individual Contributor", "Intern", "Other" ] }, { "name": "experience", "type": "experience", "required": false, "description": "Required past experience dimensions, AND-combined (maximum 10)", "properties": { "dimension": { "type": "string", "allowedValues": ["function", "subFunction", "seniority"] }, "value": { "type": "string", "description": "Value from the selected dimension taxonomy" }, "minMonths": { "type": "number", "description": "Minimum tenure in months (0-720)" } } } ] }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "724d4d3e-d582-43e0-bee0-5586f9d58da1", "executionTimeMs": 3 } } ``` --- # Company search available fields > Discover the filters accepted by the Company Search endpoint at runtime, with every allowed value of closed lists such as industries and country codes. - Page: https://docs.reversecontact.com/endpoints/search-companies-available-fields - Endpoint: `GET https://api.reversecontact.com/v2/search/organizations/available-fields` - Credits: Free. - Answer: synchronous. - Available in: REST API, MCP tool `list_company_search_filters`, TypeScript and Python SDKs, CLI command `rc search fields organizations`. - Last verified: 2026-10-16 ## Introduction This endpoint returns metadata describing every filter accepted by [Company search](https://docs.reversecontact.com/endpoints/search-companies) and [Company search count](https://docs.reversecontact.com/endpoints/search-companies-count), plus the `cursor` and `perPage` pagination parameters of Company search. Integrations such as Clay use it to discover the available inputs instead of hardcoding the filter list: a filter we add appears in the response as soon as we release it. For closed-vocabulary filters, the `allowedValues` array lists every accepted value: the industries of `industry`, and the ISO country codes of `location.countryCode` and the zone ids of `location.area` under `properties`. The `area` entry also describes every zone in `areaCatalog`: its id, category, name and member countries (see [Area values](https://docs.reversecontact.com/reference/areas)). Like [Usage](https://docs.reversecontact.com/endpoints/usage), this route uses `GET`, takes no request body, and **costs 0 credits**. > For people filters, see [People search available fields](https://docs.reversecontact.com/endpoints/search-people-available-fields). ## Endpoint | Method | URL | Describes | | ------ | -------------------------------------------- | ------------------- | | `GET` | `/v2/search/organizations/available-fields` | Company Search | ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (SearchFields): Container for the discovered fields | Field | Type | Description | | -------- | --------- | ------------------------------------------------- | | `object` | string | Always `"search_fields"` | | `entity` | string | Always `"organization"` for this endpoint | | `fields` | `Field[]` | List of accepted filters and pagination parameters (see field shape below) | **fields** (Field): One accepted search filter or pagination parameter | Field | Type | Description | | -------------- | ----------- | --------------------------------------------------------------------------------- | | `name` | string | Field name, used as a key in the search request body | | `type` | string | `string`, `number`, `range`, `location`, `date-time`, or `string \| string[]` | | `required` | boolean | Whether the field is mandatory on its own (always `false`: each filter is optional, but a search needs at least one) | | `description` | string | Human-readable explanation of the field | | `example` | unknown | Example value, when helpful (optional) | | `allowedValues`| `unknown[]` | Closed set of accepted values, when applicable, such as the industry list (optional) | | `properties` | object | Sub-field shapes for `range` and `location` filters (optional) | ## Code examples **Code Examples** ```bash [cURL] curl https://api.reversecontact.com/v2/search/organizations/available-fields \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.search.organizationsFields(); console.log(result.data.fields?.map((field) => field.name)); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.search.organizations_fields() fields = result.data.fields if result.data and result.data.fields else [] print([field.name for field in fields if field is not None]) ``` ## Example response The response below is truncated to three fields: the catalog describes every filter accepted by Company search, then `cursor` and `perPage`. **Example Response** ```json [200 - Success] { "success": true, "data": { "object": "search_fields", "entity": "organization", "fields": [ { "name": "specialities", "type": "string | string[]", "required": false, "description": "Speciality listed on the company profile, or an array of specialities (OR matching)", "example": ["SaaS", "Machine Learning"] }, { "name": "foundedYear", "type": "range", "required": false, "description": "Filter by founding year. Both bounds are optional and inclusive (range 0 - 2100).", "example": { "min": 2015, "max": 2020 }, "properties": { "min": { "type": "number", "description": "Minimum value, inclusive" }, "max": { "type": "number", "description": "Maximum value, inclusive" } } }, { "name": "maxDataAgeDate", "type": "date-time", "required": false, "description": "Only return companies refreshed after this ISO 8601 datetime. Needs at least one other filter." } ] }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "df926b52-ee19-47ac-a0c6-02ce6b39f675", "executionTimeMs": 3 } } ``` --- # People auto-suggestion > Add free skill auto-suggestion to a people search box, then pass the selected value to the People search skills filter or its count endpoint. - Page: https://docs.reversecontact.com/endpoints/search-people-auto-suggestion - Endpoint: `POST https://api.reversecontact.com/v2/search/people/auto-suggestion` - Credits: Free. - Answer: synchronous. - Available in: REST API, TypeScript and Python SDKs. - Last verified: 2026-11-03 ## Introduction The People Auto-suggestion endpoint returns the skills that start with what someone has typed, most common first. It returns values only, never a profile. It is made for products: use it to offer auto-suggestion in a search box of your own interface, then send the value your user picks to the `skills` filter of [People search](https://docs.reversecontact.com/endpoints/search-people#skills) or of its [free count](https://docs.reversecontact.com/endpoints/search-people-count). This endpoint **costs 0 credits** and has its own rate limit: 1,000 calls per minute per workspace, or 500 on the free trial. Its calls never use up your workspace's [requests per minute](https://docs.reversecontact.com/guides/rate-limits). ## Authorization | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `apikey` | string | Yes | Your API key from the developer dashboard. Pass it via `Authorization: Bearer YOUR_API_KEY` header. | ## Request body | Name | Type | Required | Description | | ------- | ------ | -------- | -------------------------------------------------------------------------------- | | `field` | string | Yes | The filter to complete. `skills` is the only value today | | `query` | string | Yes | What has been typed so far, 1 to 100 characters. Case and accents are ignored | | `limit` | number | No | Maximum values returned, from 1 to 25. Defaults to 10 | A value matches when it starts with `query`, spaces included: `machine l` returns `machine learning`, and `script` returns `scripting` but not `javascript`. Any other `field`, an empty `query` or a `limit` out of range returns HTTP `422` and names the expected value. ## Response structure | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `success` | boolean | `true` if the request was processed successfully | | `error` | `null` | Always `null` on success | **data** (SearchAutoSuggestion): Values that start with the query | Field | Type | Description | | ------------- | -------- | ------------------------------------------------------------------------ | | `object` | string | Always `search_auto_suggestions` | | `entity` | string | Always `person` | | `field` | string | The filter completed | | `suggestions` | object[] | The values, most common first. Empty when no value starts with the query | **suggestions[]** (AutoSuggestionValue): One value | Field | Type | Description | | ------- | ------ | ----------------------------------------------------------------- | | `value` | string | The value to show, and to send as it is in the `skills` filter | Values come in lowercase and without accents, as they are held for matching. The `skills` filter ignores case, so you can restyle a value for display and still send it. ## Build a search box A few habits keep the box fast and within its limit: - **Call from your server.** The call needs your API key, which must never reach a browser. - **Wait for a pause in the typing.** Calling about 200 ms after the last key sends one request for a burst of keys. - **Start at two characters.** One letter matches too much to help. - **Cancel the previous request** when a new one starts, so a late answer never replaces a newer one. - **Keep the answers** for the session: the same prefix returns the same values. - **Let people add what they typed.** An empty list only means that no value starts this way, so a person can still search on their own wording. - **Treat a `429` like an empty list.** The call returns `429` with the code `RATE_LIMIT_EXCEEDED` past its limit, and also at times of very high demand, with a `Retry-After` header. Show no list, keep accepting what the person types, and try again on the next key. ## Code examples **Code Examples** ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/search/people/auto-suggestion \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "field": "skills", "query": "pyth", "limit": 5 }' ``` ```typescript [TypeScript SDK] import ReverseContact from "@reverse-contact/sdk"; const client = new ReverseContact({ apiKey: process.env.RC_API_KEY }); const result = await client.search.peopleAutoSuggestion({ field: "skills", query: "pyth", limit: 5 }); console.log(result.data.suggestions?.map((suggestion) => suggestion.value)); ``` ```python [Python SDK] import os from reversecontact import ReverseContact with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client: result = client.search.people_auto_suggestion( field="skills", query="pyth", limit=5, ) if result.data is not None and result.data.suggestions is not None: print([suggestion.value for suggestion in result.data.suggestions]) ``` ## Example response **Example Response** ```json [200 - Success] { "success": true, "data": { "object": "search_auto_suggestions", "entity": "person", "field": "skills", "suggestions": [ { "value": "python programming language" }, { "value": "python" }, { "value": "python langage de programmation" }, { "value": "python programmiersprache" } ] }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "5e0c03b5-38c7-46e2-9992-69478665e1b8", "executionTimeMs": 170 } } ``` ---