# ReverseContact API - Complete Documentation > B2B data enrichment API. Enrich LinkedIn profiles and company pages with structured professional data. - Base URL: `https://api.reversecontact.com/v2` - Authentication: `Authorization: Bearer rc_your_api_key` - MCP Server: `https://api.reversecontact.com/mcp` (Streamable HTTP, Bearer token auth) --- # Getting started > Set up your first Reverse Contact request in less than 5 minutes. ## Before you begin 1. **Create an account.** [Sign up](https://app.reversecontact.com/auth/signup) if you don't have one yet 2. **Generate an API key.** Open Workspace > [API Keys](/api-keys), create a new key, copy and keep it secure. 3. **Have a LinkedIn URL ready,** e.g. `https://www.social.com/in/janedoe`, or a person's full name and company domain. See [Authentication](/docs/authentication) for more details on API keys and permissions. ## 1. Make your first request All endpoints are served from `https://api.reversecontact.com/v2/`. Here is a quick example: ::: code-group [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); ``` ```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"]) ``` > [!TIP] > Want to test without writing code? Try the [API Playground](/docs/playground) to run requests directly from your browser. ## 2. Read the response Every API response follows the same structure. Here is what you get back: | 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](/docs/api-reference) for the full response format, metadata, quotas, and shared types. ## 3. Need real-time data? Use the live endpoints The `live` and Contact Email Finder endpoints fetch fresh data asynchronously. They reply immediately with a `webhookId`, then deliver the actual result once it's ready, usually within seconds. > [!IMPORTANT] > **Async, not synchronous.** We aim to deliver each result in under 15 seconds, but it can take tens of minutes depending on conditions. Live endpoints are built to update databases and feed async pipelines, not to block a synchronous, real-time request a user is waiting on. For live profile endpoints, the simplest way to retrieve that result is **polling**: trigger the job, then call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. For Contact Email Finder, provide a `webhookUrl` and handle the callback described in the [Find Email endpoint guide](/docs/endpoints/contact-email). ::: code-group [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)); } ``` ```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 ``` > [!TIP] > Polling is the simplest way to use live endpoints. No webhook receiver required, free to call, off the rate-limit clock. See the [Polling guide](/docs/guides/polling) for the full integration walkthrough, comparison with webhooks, status codes, polling frequency, and retention details. --- # Authentication > Authenticate requests to the Reverse Contact API. Every API request requires your key in the `Authorization` header. Two steps and you're set. ## 1. Copy your API key Open **Workspace > [API Keys](/api-keys)** and copy your key. It starts with `rc_*****`. > [!TIP] > Don't have an account yet? Start with [Getting started](/docs/getting-started). ## 2. Add it to your request Test with the [Usage endpoint](/docs/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](/docs/endpoints/usage) for all fields. | Check | Expected | | ----- | -------- | | HTTP status | `200` | | `success` | `true` | | `quotas.workspace.credits.left` | Your remaining credits | > [!NOTE] > You're all set! Your API key is working. Ready to enrich your first profile? Continue with [Your first API call](/docs/guides/first-api-call). ## Something went wrong? A `401` response means the API key was missing, malformed, or not recognized: ::: code-group [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](/api-keys) | | Revoked or deleted key | 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 for a suspended workspace returns `403 WORKSPACE_DISABLED`, because the request is authenticated but the workspace is not authorized to use the API. For all error codes and retry strategies, see [Error handling & retries](/docs/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 keys.** Revoke compromised keys and generate new ones from [API Keys](/api-keys). - **Never expose keys** in client-side code, public repos, or browser JavaScript. - **Monitor usage** with `GET /v2/usage` to track credits and rate limits. --- # API reference > Common response structure, shared types, and error codes for all endpoints. Every endpoint returns a consistent JSON structure. This page documents the shared envelope, types, and error codes that apply across all endpoints. ## Response envelope Every response uses a consistent JSON envelope with 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 contacting 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) | 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 multiple endpoints. They are documented here once to avoid repetition -- individual endpoint pages reference 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 or legacy V1/RC endpoint validation (`VALIDATION_ERROR`) | | `401` | Missing, malformed, invalid, or expired API key | | `402` | No credits left, purchase more to continue. The free /check endpoints return `NO_CREDITS` | | `403` | Workspace disabled (`WORKSPACE_DISABLED`) or API key lacks permission | | `404` | Data not found | | `422` | V2 schema/validation error, `INVALID_LINKEDIN_URL`, disposable email domain, or missing webhook URL on async endpoints | | `422` | `PAGINATION_LIMIT_EXCEEDED` on search endpoints when `page` exceeds `500` or `perPage` exceeds `100`. Details: `{ "field": "page", "max": 500 }` or `{ "field": "perPage", "max": 100 }` | | `429` | Rate limit exceeded , wait and retry after `nextReset` | | `451` | Data Subject Blocked , the profile has been removed per GDPR/CCPA data subject request | | `5xx` | Temporary upstream or platform issue , retry with backoff | > [!TIP] > For `429` errors, read the `quotas.workspace.minuteRateLimit.nextReset` field and retry after that timestamp. > [!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` is `422` on both V1 and V2. `VALIDATION_ERROR` remains route-sensitive: legacy V1/RC routes return `400`, while V2 routes return `422`. > [!NOTE] > For async endpoints, the initial response is a standard V2 acknowledgement containing a `webhookId`. Most async endpoints support polling or a [webhook callback](/docs/guides/webhooks); the [Contact Email Finder](/docs/endpoints/contact-email) uses the webhook flow and delivers an email array or an error code in its callback. Webhook callback payloads do not use the full V2 envelope. --- # Platform update for July 1, 2026 > Important changes coming to the platform on July 1, 2026, including the retirement of legacy endpoints, the focus on V2, and the new synchronous Enrichment that returns a match in a single request. Read this to prepare your integration. ## A note before you read Over the past months, we've worked hard to keep our legacy endpoints (the original `/enrichment` API and `/v1/`) running alongside our newer V2 platform. That dual life has reached its limit, and maintaining both is now hurting the reliability you depend on. We've made the decision to focus the platform on a single, modern foundation: **V2**. This guide explains what's changing on July 1, 2026, why we're doing it, and how to prepare. > [!NOTE] > This page describes the July 1 transition. The Contact Email Finder was reactivated after that transition as an opt-in V2 capability at `POST /v2/contact/email`. It requires the `contact_email_finder` workspace feature flag, runs asynchronously, and delivers results by webhook. > [!IMPORTANT] > If your integration relies on a legacy `/enrichment` or `/v1/` endpoint, or on Activities endpoints, please plan your migration before **July 1, 2026**. We strongly recommend starting now to avoid any disruption. ## The short version Three things are happening on July 1, 2026: 1. All **legacy `/enrichment` endpoints** (the original RC API) will be permanently shut down. 2. All **V1 endpoints** will be permanently shut down. 3. **Activities** endpoints will be discontinued, even those currently exposed under V2. They will not be replaced, on V2 or anywhere else. Everything else in our [V2 documentation](/docs) is here to stay and remains our long-term supported infrastructure. And it isn't all removals: V2 now ships a new **synchronous Enrichment** that returns a match in a single request, with no polling and no webhooks. More on that just below. ## Why we're focusing on V2 Running multiple generations of APIs at the same time has added complexity that touches every part of the platform: routing, caching, quotas, billing, monitoring, and support. Each layer carries assumptions from a different era, and reconciling them in real time has been a major source of instability. Focusing fully on V2 lets us operate one consistent infrastructure end to end, which is what we need to give you the reliability you depend on. We also looked honestly at our own scope. Spreading our attention across too many endpoint families slowed our ability to deliver the highest quality on what most customers actually rely on: **B2B identity resolution**. Activities remain outside that focus and were retired. Email Finder is now available again as a separately gated asynchronous capability for workspaces that need it. ## What's new: synchronous enrichment This update isn't only about retiring old surfaces. The headline addition in V2 is **Enrichment**, the simplest way we've ever shipped to turn the details you already have into a full profile or company record. > [!TIP] > **Enrich Profile** and **Enrich Company** return a complete match **synchronously**, right in the API response. One request in, one answer back. No job to start, no polling loop, no webhook endpoint to host. Why teams are moving to it: - **One request, one response.** Send what you have (an email, a name, a company domain) and read the match straight from `response.data`. Nothing to wait on. - **Any input works.** Every field is optional and independent, so an email on its own is enough to resolve a person. The more you provide, the more accurate the match. - **Far less to build.** If you resolve identities asynchronously today, you can drop the callback infrastructure entirely and delete a whole class of edge cases. - **Same credits, same price.** Enrichment costs **2 credits**, billed **only when a match is found**. A not-found response (`404`) is free, and it draws from the same workspace credits as the rest of V2. It really is this small: ```bash 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"}' ``` The profile comes back in the same response, ready to use. > [!NOTE] > This is **not** the legacy `/enrichment` API. The original `/enrichment` endpoint is one of the legacy generations being shut down on July 1, 2026. The new V2 Enrichment is a different, modern endpoint family living at `/v2/enrich/persons` and `/v2/enrich/companies`. Same familiar name, much stronger foundation. Start here: [Enrich profile](/docs/endpoints/enrich-person) and [Enrich company](/docs/endpoints/enrich-company). If you're coming from asynchronous Resolve, they accept the same inputs and return the same result in a single synchronous request. ## The timeline | When | What happens | | ---- | ------------ | | **Now** | V2 is the recommended path for every new and existing integration. Migration guides and V2 alternatives are ready in the docs. | | **Until July 1, 2026** | Legacy `/enrichment` and `/v1/` endpoints remain accessible. They may behave unevenly during this transition, so please plan your migration as early as possible. | | **July 1, 2026** | All legacy endpoints and Activities are permanently shut down. | | **August 13, 2026** | The Contact Email Finder is available again for opted-in workspaces. | > [!WARNING] > Legacy `/enrichment` and `/v1/` endpoints may remain unstable during the transition window. They will not receive new fixes or improvements. Treat them as best-effort until shutdown, and prioritize moving critical workloads to V2. ## What is being shut down These are the endpoints affected on July 1, 2026: ### Legacy generations - Every endpoint under `/enrichment...` (the original RC API) - Every endpoint under `/v1/...` ### Activities endpoints (under V2) - `POST /v2/fetch/persons/posts/live` - `POST /v2/fetch/persons/comments/live` - `POST /v2/fetch/persons/reactions/live` - `POST /v2/fetch/companies/posts/live` - `POST /v2/fetch/post/live` - `POST /v2/fetch/posts/comments/live` ### Contact Email Finder status The Contact Email Finder is available at `POST /v2/contact/email` for workspaces with the `contact_email_finder` feature enabled. It accepts a LinkedIn profile URL or a person's full name and company domain, and delivers the result asynchronously by webhook. The endpoint costs 3 credits only when an email is found. ## What stays Everything visible in the current [V2 documentation](/docs) sidebar stays. That includes: - Person and company **Enrichment** endpoints, the new synchronous way to resolve an identity in a single request. - Person and company **Fetch** endpoints, sync and live. - Person and company **Resolve** endpoints, still fully supported if you prefer the asynchronous flow. - Person and company **Search** endpoints. - The **Contact Email Finder**, when enabled for the workspace, for asynchronous email discovery by webhook. - Free **Check** endpoints for freshness and existence. - Usage, quotas, webhooks, and dashboard tooling. V2 is our long-term supported infrastructure. ## Your migration plan You don't need to rewrite everything at once. Here's a step by step path that works well in practice. ### 1. Map your traffic Open the [Logs page](/logs) in your dashboard and look at which endpoints your integration is actually using. You may be surprised: in most accounts, a small number of endpoints carry the majority of the volume. Migrate those first. ### 2. Generate a V2 API key Create a new key (with the `rc_` prefix) from the [API Keys page](/api-keys). Your existing legacy keys keep working until shutdown, so you can run both side by side during the transition. ### 3. Switch endpoint by endpoint V2 uses standard Bearer authentication, JSON request bodies, and a consistent response envelope. The full mapping (V1 to V2) is already documented in the [Migration Guide](/docs/guides/migrating-v1-to-v2#endpoint-mapping). ### 4. Update your response parsing V2 standardizes data under `response.data`, errors under `response.error`, and quotas under `response.quotas`. The [Migration Guide](/docs/guides/migrating-v1-to-v2#response-format) shows side by side examples for every field. ### 5. Validate, then cut over Run your new V2 integration in parallel for a few days, compare results, and only then point production traffic to the V2 key. Once you're confident, you can retire the legacy key entirely. ## If you rely on Activities We understand this is the harder part of this update, and we want to be fully transparent: there will be **no replacement** for Activities, neither on V2 nor anywhere else. Once these endpoints are shut down on July 1, 2026, the capability is retired for good, not moved to a new home. We've chosen to concentrate the platform on stable B2B identity resolution infrastructure. If your workflow depends on Activities, please reach out to our team as early as possible. Depending on your use case, we can help you in two ways: 1. **Discuss alternatives.** Some workflows that use Activities today can be reshaped around our identity resolution endpoints. We're happy to walk through your specific case with you. 2. **Plan the timing.** If you need more lead time to adapt your product, the earlier you tell us, the more we can do to make the transition smooth on your side. You can contact us from the [Support page](/support) inside your dashboard. Mention that your request is about the July 2026 update so it reaches the right team. ## Frequently asked questions ### Do my legacy API keys stop working today? No. Your legacy keys remain valid until July 1, 2026. After that date, calls to `/enrichment` and `/v1/` endpoints, and to the Activities routes listed above, will no longer be processed. The Contact Email Finder is a separate V2 capability available to opted-in workspaces. ### Wait, isn't `/enrichment` being shut down? How can Enrichment be new? Both are true, because these are two different things that happen to share a name. The legacy `/enrichment` API (one of the original generations) is being retired on July 1, 2026. The new V2 **Enrichment** is a separate, modern endpoint family at `/v2/enrich/persons` and `/v2/enrich/companies`, and it is here to stay. If you're on legacy `/enrichment` today, moving to V2 Enrichment is the natural next step. ### Will V2 credits be the same as V1 credits? Your workspace credit balance is shared across V1 and V2 today, and that remains the case during the transition. V2 uses contextual pricing, see [Rate limits & credits](/docs/guides/rate-limits-credits) for the full breakdown per endpoint. ### Can I migrate gradually? Yes. V1 and V2 coexist until July 1, 2026, so you can move endpoint by endpoint at your own pace. The earlier you start, the more time you have to validate each step in production. ### What if I can't finish migrating by July 1, 2026? Please contact our team **before** July, not after. We'd rather hear about constraints early and help you plan, than discover blockers on the day of shutdown. Reach out from the [Support page](/support). ### Where do I find the V2 equivalent of an endpoint I'm using today? The full mapping is in the [Migration Guide](/docs/guides/migrating-v1-to-v2#endpoint-mapping). If your endpoint isn't listed, please contact us so we can confirm the best V2 path for your use case. ### Will Activities get a replacement? No. Activities (person and company posts, comments, and reactions) are retired with no replacement, on V2 or anywhere else. This is a deliberate choice to focus the platform on B2B identity resolution. If these endpoints are part of your workflow, contact us early from the [Support page](/support) and we'll help you find the best path forward. ## A final word We don't take this decision lightly. We know that any migration costs your team time, and we appreciate the trust you place in us by going through it with us. The reason is simple: we want the platform you rely on to be the most stable, predictable, and accurate B2B identity resolution infrastructure available. Focusing on V2 is how we get there. If anything in this update is unclear, or if you'd like guidance on your specific integration, our team is here to help. Reach out from the [Support page](/support) at any time. --- # Your first API call > Send your first enrichment request and parse the response. You have your API key ready. Let's make your first profile enrichment call and inspect the result. > [!TIP] > Don't have an API key yet? Start with [Authentication](/docs/authentication). ## 1. Send the request The Person Profile endpoint retrieves a complete professional profile from a Social URL. It costs **1 credit** (cached data). ::: code-group [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] > Want to test without writing code? Try the [API Playground](/docs/playground) to run requests directly from your browser. ## 2. Read the response A successful response returns the full profile inside `data`: ::: code-group [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", "country": "United States" }, "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 } } ``` 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](/docs/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](/docs/endpoints/fetch-profile) for all available fields, and [API reference](/docs/api-reference) for the full response structure. ## 3. Handle errors If the URL is invalid, you get a `422` with zero credits charged: ::: code-group [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 LinkedIn URL | Use the full URL: `https://www.linkedin.com/in/username` | | `401` Unauthorized | Check your API key. See [Authentication](/docs/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](/docs/endpoints/fetch-profile-live) | For all error codes and retry strategies, see [Error handling & retries](/docs/guides/errors-retries). --- # API key management > Create, configure, and secure API keys with permissions, rate limits, and expiration. Your keys work out of the box with full access and no configuration needed. This guide covers optional features you can use as your integration grows. > [!TIP] > Don't have an API key yet? Start with [Authentication](/docs/authentication). ## 1. Create a key 1. Open **[API Keys](/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. That's it: your key is ready to use with full access to all endpoints. ## 2. Fine-tune permissions (optional) By default, every key has **full access**. No setup needed. As your team grows, you can scope each key to specific capabilities: | Scope | What it covers | | -------------------- | --------------------------------------------------------- | | Person Enrichment | Enrich person profiles from Social URLs | | Person Activities | Fetch posts, comments, and reactions for people | | Company Enrichment | Enrich company profiles from Social URLs or domains | | Company Activities | Fetch posts and activities for companies | | Search | Search for people and companies | | Contact (Email) | Find professional email addresses | | Posts | Fetch individual posts and their activities | Select only the scopes your integration needs, or leave them all checked for full access. > [!NOTE] > Free endpoints like `GET /v2/usage` are always accessible, regardless of permissions. If a key tries to call an endpoint outside its scopes, the API returns a `403` status code. ## 3. Set rate limits (optional) Per-key rate limits let you protect your workspace budget by capping individual integrations: | Setting | Description | | ----------- | ----------------------------------------- | | RPM limit | Maximum requests per minute for this key | | Daily limit | Maximum requests per day for this key | If you don't set any, your workspace defaults apply automatically. Per-key limits can only cap usage below the workspace maximum. They can never exceed it. When a per-key limit is reached, the API returns `429 Too Many Requests`. ## 4. Add an expiration date (optional) Useful for temporary access: contractor keys, demo integrations, or test environments. 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 **Edit.** Click the edit icon on any active key to update its name, permissions, rate limits, or expiration. Changes take effect immediately. **Revoke.** Revoked keys stop working immediately. They stay 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 - **One key per environment.** Separate keys for dev, staging, and production keep things clean. - **Name keys clearly.** A name like `prod-crm-sync` or `staging-enrichment` makes auditing easy. - **Scope when you need to.** Start with full access. Restrict later as your team and integrations grow. ## Something not working? | Symptom | Fix | | ------- | --- | | `403 Forbidden` | The key is missing a required permission. Edit it in [API Keys](/api-keys) | | `429 Too Many Requests` | A per-key rate limit was reached. Raise it or wait for the reset | | `401 Unauthorized` (expired key) | The key passed its expiration date. Create a new one | For all error codes and retry strategies, see [Error handling & retries](/docs/guides/errors-retries). --- # Which endpoint should I use? > Pick the right Reverse Contact endpoint for profile lookup, enrichment, search, and live scraping. Not sure where to start? Tell us what you have and we'll show you the fastest way to get the data you need. > [!TIP] > New here? Start with [Getting started](/docs/getting-started) to create your account and API key. ## Pick your starting point - **I have a Social URL** → [Get their full profile](#profiles) - **I have an email, name, or domain** → [Find the matching profile](#enrichment) - **I have filters** (job title, location...) → [Search our database](#search) - **I need a professional email address** → [Find an email](#contact) ## Profiles Turn a Social URL into a complete person or company profile. | Endpoint | Credits | Delivery | When to use | | -------- | :-----: | :------: | ----------- | | [`POST /v2/fetch/persons/check`](/docs/endpoints/check-person) | 0 | Sync | Check if a person profile exists before spending credits | | [`POST /v2/fetch/companies/check`](/docs/endpoints/check-company) | 0 | Sync | Check if a company profile exists before spending credits | | [`POST /v2/fetch/persons`](/docs/endpoints/fetch-profile) | 1 | Sync | Retrieve a cached person profile. Fast and cheap | | [`POST /v2/fetch/companies`](/docs/endpoints/fetch-company) | 1 | Sync | Retrieve a cached company profile. Fast and cheap | | [`POST /v2/fetch/persons/live`](/docs/endpoints/fetch-profile-live) | 2 | Async | Force a fresh scrape when you need the latest data | | [`POST /v2/fetch/companies/live`](/docs/endpoints/fetch-company-live) | 2 | Async | Force a fresh scrape when you need the latest data | > [!TIP] > **Recommended flow:** Call **Check** first (free), then **Fetch** if the record is recent, or **Live** if it's missing or stale. See [Data freshness](/docs/guides/data-freshness) for the full optimization strategy. ## Enrichment Find a profile when you don't have a Social URL. Send the details you have, get the full profile back in the same response. | Endpoint | Credits | Delivery | Input | | -------- | :-----: | :------: | ----- | | [`POST /v2/enrich/persons`](/docs/endpoints/enrich-person) | 2 | Sync | Any combination of email, name, company name or domain | | [`POST /v2/enrich/companies`](/docs/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/persons`](/docs/endpoints/search-persons) | 1 | Sync | Search persons by job title, location, company... | | [`POST /v2/search/companies`](/docs/endpoints/search-companies) | 1 | Sync | Search companies by industry, size, location... | > [!NOTE] > 1 credit per search, up to 100 results per request. ## Contact Find a professional email address from a LinkedIn profile URL, or from a person's full name and company domain. | Endpoint | Credits | Delivery | When to use | | -------- | :-----: | :------: | ----------- | | [`POST /v2/contact/email`](/docs/endpoints/contact-email) | 3 when found | Async webhook | Find a professional email address | > [!NOTE] > The Contact Email Finder is in beta. It costs 3 credits only when an email is found. ## Sync vs async Most endpoints return data instantly in the API response. Live endpoints and the Contact Email Finder deliver their results asynchronously. | Delivery | How it works | | -------- | ------------ | | **Async** | Results pulled on demand via [polling](/docs/guides/polling) for live endpoints, or pushed to a webhook URL ([advanced](/docs/guides/webhooks)) | | **Sync** | Instant JSON response | > [!TIP] > New to async? Start with [Polling](/docs/guides/polling). No setup, no public endpoint, free to call. If you already run a public receiver and need push delivery, see [Webhooks](/docs/guides/webhooks). --- # Error handling & retries > Handle API errors predictably with safe retry strategies. Every error response follows the same structure and includes a clear 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: ::: code-group [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 instantly. ## Error reference | Status | What to do | | ------ | ---------- | | `422` | Check your request body: a required field is missing or malformed (`INVALID_LINKEDIN_URL` is also returned for an invalid URL). For async endpoints, make sure a webhook URL is configured. On the search endpoints, see [Search filter validation](#search-filter-validation) for the exact reason. | | `401` | Check your API key: it may be missing, malformed, invalid, or expired. See [Authentication](/docs/authentication). | | `402` | No credits left. Paid lookups (`INSUFFICIENT_CREDITS`) and the free `/check` endpoints (`NO_CREDITS`) are both blocked until you top up. Purchase more or [upgrade your plan](/settings/billing). | | `403` | Your workspace may be disabled (`WORKSPACE_DISABLED`), your key may lack a required permission, or the endpoint may require a paid plan. [Upgrade](/settings/billing?tab=credits) to unlock live enrichment, email discovery, and more. | | `404` | No result found for your query. Some endpoints still charge credits; see [Not found pricing](/docs/guides/rate-limits-credits#note). | | `429` | Rate limit reached. Wait and retry with backoff (see below). | | `451` | This person has been removed per GDPR/CCPA. Enrichment is permanently blocked. | | `503` | Our service is temporarily unavailable. Retry with backoff (see below). | | `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. This is deliberate: a filter we cannot honour would otherwise return an empty page that looks like a legitimate "no results", and bill you for it. `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. | | `INVALID_FILTER_VALUE` | The value is outside a closed list, currently `industry` / `currentCompanyIndustry` and `location.countryCode`. `details.suggestions` proposes up to three valid values. | | `EMPTY_FILTER_VALUE` | A blank string, empty array or empty object was sent. Remove the field instead. | | `INVALID_RANGE` | A `min`/`max` pair is negative, fractional, or inverted (`min` greater than `max`). | | `INCOMPLETE_RANGE_FILTER` | `followersCount` used as the only filter requires both bounds. | | `INVALID_PAGINATION` | `page` or `perPage` is not a whole number. | | `PAGINATION_LIMIT_EXCEEDED` | `page` above 500 or `perPage` above 100. `details` carries `field` and `max`. | | `INVALID_ID_FORMAT` | `currentCompanyId` is not a `com_` identifier returned by our API. | | `INVALID_DATE_FORMAT` | `maxDataAgeDate` is not an ISO 8601 date-time. | | `CONTRADICTORY_FILTERS` | The same title appears in `currentPositionTitle` and `excludeCurrentPositionTitle`, which can never match. | | `NO_FILTER_PROVIDED` | The body carries pagination only. | | `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 normalised for you. Only a genuinely unknown value is refused. ## When to retry These statuses are safe to retry with exponential backoff: - **`429`** you've hit a rate limit. Wait for the reset window and retry. - **`503`** temporary service issue. Retry after a short delay. - **`504`** the lookup timed out before completing. Retry after a short delay. All other errors require a change to your request or configuration before retrying. Wait a little longer between each retry to give the server time to recover: ::: code-group [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) ``` ## Credits and errors All error responses (`401`, `402`, `403`, `422`, `429`, `451`, `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 [Not found pricing](/docs/guides/rate-limits-credits#note) for the full breakdown. --- # Rate limits & credits > Understand the credit model, rate limits, and strategies to optimize API costs. Every request costs between 0 and 3 credits depending on the endpoint. Most errors and status checks are always free. ## You're never charged for - **Errors** all 4xx and 5xx responses cost 0 credits, except `404` which depends on the endpoint (see the pricing catalog). - **Check and Usage endpoints** always 0 credits, designed for testing and decision-making before spending. You can test the API freely before committing credits. > [!IMPORTANT] > **When your workspace reaches 0 credits, 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 no enrichment can follow until you top up. Status and result endpoints stay open: `GET /v2/usage`, `GET /v2/flags`, the Search `available-fields` endpoints, and webhook polling (`GET /v2/webhooks/:webhookId`) keep working so you can still read your balance and retrieve already-paid results. [Top up →](/settings/billing?tab=credits) ## Per-endpoint pricing The full credit-cost matrix lives in your dashboard. It covers every scenario (cache hit, live scrape, 404 cache, 404 live), async refund rules, and plan gating: > **[Open the API pricing catalog →](/settings/billing?tab=api-pricing)** > > One row per endpoint, one badge per scenario. Updated automatically with the gateway runtime, so what you read there is what gets billed. The catalog also explains the **async billing model** in detail: how credits are charged at request creation and refunded automatically based on the outcome (API error, no result on Search, no email found by Contact Email Finder, or 404 on Live endpoints). Refunds are auditable in the [Webhook Events](/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. ## Rate limits Two rate limits apply per workspace: | Limit | Scope | Description | | ----- | ----- | ----------- | | Minute limit | Workspace | Maximum requests per minute (e.g. 60 req/min) | | Daily limit | Workspace | Maximum requests per day (Enterprise only, disabled by default) | The minute limit applies to inbound API requests only. The outbound callback POST we send to your `webhookUrl` does not count toward workspace or per-key RPM. The request that creates an async job does count as one request, while polling `GET /v2/webhooks/:webhookId` remains exempt. You can also set per-key rate limits to cap individual integrations below the workspace maximum. See [API key management](/docs/guides/api-key-management#3-set-rate-limits-optional) for details. When a rate limit is exceeded, the API returns `429 Too Many Requests`. Check the `quotas` object in any response to see your current usage: ::: code-group [Rate limit response] ```json [JSON] "minuteRateLimit": { "limit": 60, "used": 58, "left": 2, "nextReset": "2025-01-15T09:31:00.000Z" } ``` For retry strategies on `429`, see [Error handling & retries](/docs/guides/errors-retries#when-to-retry). ## Agent, MCP, and dashboard usage Not every credit-consuming action shows up as the same kind of usage event: - **Agent LLM turns** consume credits, but they do **not** hit the gateway directly and do **not** count toward gateway RPM. - **Agent tool calls** use the same gateway endpoints as the REST API. They consume endpoint credits and **do** count toward gateway RPM when the endpoint itself is RPM-metered. - **MCP calls** use the same credits and the same rate-limit rules as the REST API. - **Polling and status-only endpoints** such as `GET /v2/usage` and `GET /v2/webhooks/:webhookId` stay free and do **not** affect gateway RPM. - **Webhook callbacks** (the POST we send to your `webhookUrl`) do **not** count toward gateway RPM. In the dashboard, the **Usage channels** section is the best view to understand where credits are going across Agent, MCP, and API usage. The request analytics tabs remain HTTP-request-centric on purpose. ## Save credits **Check before you fetch.** Call the free Check endpoint first to see if a profile exists and when it was last updated. Then decide: cached fetch (1 credit) or live scrape (2 credits). See [Data freshness](/docs/guides/data-freshness) for the full strategy. **Use Search for bulk discovery.** 1 credit per search, up to 100 results per request. Find matching profiles first, then fetch only the ones you need. --- # Polling, zero setup > The recommended way to retrieve async enrichment results. Trigger a job, pull the result on demand, free of charge, no public HTTPS endpoint required. ## The recommended way to retrieve async results All `live` endpoints run in the background. They reply immediately with a `webhookId`, then deliver the actual result once it's ready, usually within seconds. The [Contact Email Finder](/docs/endpoints/contact-email) works the same way. > [!IMPORTANT] > **Async by design, 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 pull the result on demand by calling `GET /v2/webhooks/:webhookId` until it's ready. It works without any infrastructure on your side: no public URL to expose, no receiver to deploy, no tunnel to maintain. The same delivery model fits backend services, batch pipelines, mobile apps, CLI tools, notebooks, and local development. > [!TIP] > **Recommended for most teams.** > > - **No public HTTPS endpoint required.** Nothing to deploy, no tunnel to maintain. > - **Free.** Status checks cost 0 credits, whatever the status returned. > - **Off the rate-limit clock.** Polling does not count toward your per-minute RPM. > - **Works everywhere.** Backend, mobile, CLI, notebook, behind a firewall. ## Trigger and poll Trigger any async endpoint, save the `webhookId` it returns, then call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. That's the entire integration. ::: code-group [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 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)); } ``` ```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] 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: data = requests.get(f"{BASE}/v2/webhooks/{webhook_id}", headers=H).json()["data"] if data["status"] == "succeeded": print(data["result"]) break if data["status"] == "errored": raise RuntimeError(data.get("errorCode") or "Job failed") time.sleep(2) ``` > [!TIP] > **No code yet? Try it from the dashboard.** Trigger any async endpoint, copy the `webhookId`, paste it in **[Logs > Job Lookup](/logs/jobs)**, and watch the status update in real time. Same API under the hood, zero integration required. ## 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 is the simplest path for every team. Webhooks are an alternative for teams that already operate a public HTTPS receiver and prefer a push model. ## Polling vs webhooks Both handle the same volumes, the same data, and the same enrichment cost. The only real difference is the delivery model — pull vs push — and the trade-offs that come with it. | 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 | Same endpoints, same data, same enrichment cost. Only the delivery mechanism differs. ## When to use polling Polling fits the vast majority of integrations: - **Backend services and APIs.** Server-to-server calls where you want the simplest possible delivery model, with one less moving part to monitor. - **Batch jobs and pipelines.** A pipeline step that needs to block on the enrichment result as if the call were synchronous, without standing up a callback receiver. - **Internal tools behind a firewall or VPN.** Admin consoles or back-office systems with no public surface. - **Mobile, desktop, CLI, notebooks.** Anything without a backend to receive callbacks. - **Compliance-constrained environments.** Security posture that disallows exposing inbound webhook receivers, or restricts which services can receive third-party callbacks. - **Local development and prototyping.** Skip ngrok / Cloudflare Tunnel entirely while you're iterating. 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. Polling and webhook callbacks are not mutually exclusive. You can use both, e.g. 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) at the end of this guide. ## How it works 1. **Create a job.** Call any async endpoint (e.g. `POST /v2/fetch/persons/live`). You can omit `webhookUrl` from the request body when you intend to retrieve the result by polling. 2. **Receive the webhookId.** The API responds immediately with `{ status: "created", webhookId, pollUrl }`. 3. **Poll for the result.** Call `GET /v2/webhooks/:webhookId` periodically until `status` is `"succeeded"` or `"errored"`. 4. **Read the data.** 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://linkedin.com/in/williamhgates" }) }) 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://linkedin.com/in/williamhgates" }' ``` ```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://linkedin.com/in/williamhgates"} ) data = res.json()["data"] print(data["webhookId"], data["pollUrl"]) ``` The response includes a `pollUrl` hint pointing to the polling endpoint: ::: code-group [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 } ``` ## Step 2: Poll for the result Call `GET /v2/webhooks/:webhookId` until you receive a terminal status (`succeeded` or `errored`). ```javascript [JavaScript] async function pollUntilDone(webhookId) { while (true) { 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") await new Promise(r => setTimeout(r, 2000)) // wait 2s before next poll } } ``` ```bash [cURL] curl "https://api.reversecontact.com/v2/webhooks/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python [Python] import time import requests def poll_until_done(webhook_id): while True: res = requests.get( f"https://api.reversecontact.com/v2/webhooks/{webhook_id}", headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = res.json()["data"] if data["status"] == "succeeded": return data["result"] if data["status"] == "errored": raise RuntimeError(data.get("errorCode") or "Job failed") time.sleep(2) ``` ### Response shapes While the job is still running: ::: code-group [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: ::: code-group [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: ::: code-group [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 [Rate limits & credits](/docs/guides/rate-limits-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**. That said, please poll responsibly: | Use case | Recommended interval | | -------- | -------------------- | | Interactive UI (a user is waiting) | every 2 seconds | | Background job (batch processing) | every 5 to 10 seconds | | Long-running enrichment | every 30 seconds with exponential backoff | Most jobs complete within a few seconds. Polling more aggressively than once per second offers no real benefit and increases load on your application. ## Inspect and retrieve results from the dashboard If you don't want to write code yet, or you just need to check a single job, you can use **Job Lookup** directly from the dashboard: 1. Open [Logs › Job Lookup](/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 full result payload, and download it as JSON. You can also jump straight to Job Lookup from the **[Webhook Events](/logs/events)** page. Every row has an action button on the right that opens the lookup pre-filled for that event. This is the fastest way to test the polling experience end-to-end without writing a single line of integration code, and a convenient tool for support, ops, and manual debugging sessions. It uses the exact same API under the hood, so what you see in the dashboard is exactly what your code will receive 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 | ## Combining polling with a webhookUrl If you provide a `webhookUrl` in the request body, the API will deliver the result to that URL **and** the same result remains retrievable via polling. This is useful when: - You want webhooks as the primary path but need a fallback mechanism 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. --- # Webhooks > Push delivery for async enrichment results. Use when you already run a public HTTPS receiver and prefer a push model. > [!TIP] > **Most teams ship faster with [Polling](/docs/guides/polling).** > > No public endpoint required, free to call, around 5 minutes to first result. The webhook setup below is documented for teams that already run a public HTTPS receiver and need real-time push delivery. > > - No public HTTPS endpoint required > - Free: zero credits per poll > - Off the clock: doesn't count toward your per-minute rate limit > > → **[Start with Polling](/docs/guides/polling)** ## 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. Same endpoints, same data, same enrichment cost. See the [Polling guide](/docs/guides/polling) to compare 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. This is what happens step by step: 1. **You send a request.** Call any async endpoint (e.g. `POST /v2/fetch/persons/live`). 2. **The API confirms instantly.** You get back a `webhookId` that uniquely identifies this request. Use it to match incoming callbacks back to your original request. 3. **Processing happens in the background.** The API fetches and prepares the data for you. 4. **The result is delivered to you.** As soon as the data is ready, the API sends a POST request to your webhook URL with the full JSON payload. 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 & credits](/docs/guides/rate-limits-credits) for the full rate-limit rules. > [!IMPORTANT] > **Async by design, 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 All of these endpoints deliver results via webhook: | | Endpoint | | - | -------- | | **Profiles** | | | Person Profile Live | [`POST /v2/fetch/persons/live`](/docs/endpoints/fetch-profile-live) | | Company Profile Live | [`POST /v2/fetch/companies/live`](/docs/endpoints/fetch-company-live) | | **Contact** | | | Email Finder | [`POST /v2/contact/email`](/docs/endpoints/contact-email) | ## 1. Configure your webhook URL ### Workspace default (recommended) Set a single HTTPS URL at workspace level so all async results are delivered automatically. No need to include `webhookUrl` in every request. 1. Go to **[Settings > Webhooks](/settings/webhooks)** 2. Enter your HTTPS endpoint URL 3. Click **Save** 4. Click **Test** to send a test payload and verify connectivity > [!TIP] > The workspace default URL enables full delivery tracking in the **[Webhook Events](/logs/events)** dashboard. Note that delivery failures are not refunded: the data was fetched, so the credit is kept and the result stays retrievable via [polling](/docs/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: ::: code-group [Local tunnel] ```bash [Bash] ngrok http 3000 ``` Then use the forwarding URL in your request: ::: code-group [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 API returns `422` immediately: ::: code-group [422 WEBHOOK_URL_REQUIRED] ```json [JSON] { "success": false, "data": null, "error": { "code": "WEBHOOK_URL_REQUIRED", "message": "A webhook URL is required to receive results. Either provide a \"webhookUrl\" in the request body, or configure a default webhook URL in your workspace settings." } } ``` No credits are consumed. Configure a default URL in **[Settings > Webhooks](/settings/webhooks)** or include `webhookUrl` in your request body. ## 2. Set up a receiver > [!TIP] > Don't already operate a public HTTPS receiver? Skip this entirely with [Polling](/docs/guides/polling). You'll trigger jobs the same way and pull results on demand, no server required. A webhook receiver is a standard POST endpoint that accepts JSON and returns `200`. That's it. ```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 something goes wrong, you receive a callback with an `errorCode` inside `data` describing the issue. The original input fields are echoed back alongside the error code: ::: code-group [Error callback] ```json [JSON] { "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "errorCode": "..." } ``` ### Result callback When the job completes successfully, the callback carries the requested data in `data`: ::: code-group [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 it 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. | ### 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` and the `result` is 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` + `data`. Check for `errorCode` inside `data` to distinguish errors from results. Ignore anything that doesn't match. - **Verify the webhookId.** Store the `webhookId` from the initial response and check it against incoming callbacks. This is your correlation and validation key. - **Respond quickly.** Return a `200` status code promptly. If you need to do heavy processing, acknowledge receipt first and process asynchronously. ## Dashboard monitoring The **[Webhook Events](/logs/events)** page in your dashboard gives you full visibility into every webhook delivery: - **Status distribution.** A summary showing succeeded, errored, and in-progress events at a glance. - **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 quickly find specific deliveries. ## Go to production Before going live, make sure your webhook integration is solid: - **Deduplicate on webhookId.** In rare cases, a callback may be delivered more than once. - **Log the webhookId.** Include it 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. Refundable errors are automatically refunded. See [Rate limits & credits](/docs/guides/rate-limits-credits) for details. For the full hardening checklist, see [Production checklist](/docs/guides/production-checklist). ## Troubleshooting **Not receiving webhooks** - Ensure your endpoint is publicly accessible from the internet (not just `localhost`) - If you omitted `webhookUrl` from the request, verify that a workspace default URL is configured in **[Settings > Webhooks](/settings/webhooks)** - Verify it accepts `POST` requests with a `Content-Type: application/json` body - Check your server/firewall logs for incoming requests **Timeout errors** - Your webhook endpoint must respond within 10 seconds - If processing takes longer, return `200` immediately and handle the data asynchronously **Receiving an error or not-found callback instead of data** - This is expected. The API sends a callback with an `errorCode` in `data` when the result is not available (see [Error codes](#error-codes)) - Verify the URL does not require authentication or return redirects **Duplicate deliveries** - Use the `webhookId` to deduplicate on your side **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 --- # 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. 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. This guide explains, in plain language, the two rules behind 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 follow a clear, fixed set of tie-breakers, always in the same order, so the result is predictable. ## 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?** 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 (no end date), pick the one they started most recently. 4. **Last resort: pick the most recent.** If nothing above applies, pick whichever job they started most recently. "Most recently started" is the consistent tie-breaker at every step. If a job has no start date at all, it always loses to a job that has one. If the person has no jobs listed, there is simply 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. And it does not depend on where the profile came from: a result served from our database and a fresh live lookup are ordered identically. ## A quick example 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. Either way the rule lands on the same sensible answer, and it gets there the same way every time. And regardless of how the current position is resolved, her `experience` array always 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 there is one set of rules, applied everywhere the API returns a profile, so the result is consistent and explainable. --- # Data freshness > Choose the right endpoint mode for your freshness and cost requirements. You control the balance between data freshness and cost. The API offers three modes. Pick the one that fits your use case. ## Three modes | Mode | Endpoint pattern | What you get | Credits | Speed | | ---- | ---------------- | ------------ | :-----: | :---: | | **Check** | [`*/check`](/docs/endpoints/check-person) | Whether the profile exists and when it was last updated | 0 | Instant | | **Fetch** | [`/v2/fetch/*`](/docs/endpoints/fetch-profile) | Cached profile data | 1 | Instant | | **Live** | [`*/live`](/docs/endpoints/fetch-profile-live) | Fresh data delivered via webhook | 2 | Async | ## Optimize your requests Use Check → Fetch → Live to optimize every request: ::: code-group [Decision tree] ```bash [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 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 smart 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) - For activity endpoints (posts, comments, reactions) which are always live ## Example: batch pipeline Here's a pipeline that enriches 1,000 profiles at minimum cost: ::: code-group [Batch pipeline] ```bash [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) --- # Production checklist > Hardening checklist for auth, retries, quotas, webhook handling, and support readiness. You've built your integration. Run through this checklist to make sure everything is production-ready. ## Before go-live - Store API keys server-side or in a secure secrets manager. See [Authentication](/docs/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](/docs/endpoints/usage). - Route async workflows through a durable [webhook receiver](/docs/guides/webhooks#2-set-up-a-receiver), not a manual test endpoint. - Make webhook processing idempotent by deduplicating on `webhookId`. See [Webhooks](/docs/guides/webhooks#security-best-practices). ## Request safety - Use timeouts on every outbound request. - Retry only `429` and `5xx` with exponential backoff. See [Error handling](/docs/guides/errors-retries#when-to-retry). - Respect `minuteRateLimit.nextReset` before retrying a throttled request. - 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](/docs/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 [Rate limits & credits](/docs/guides/rate-limits-credits). - Keep one smoke test for a sync endpoint and one for an async endpoint (see below). - Document which team owns [API key rotation](/docs/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 If you've checked every item above, your integration is solid. Ship it. --- # Industry values > Complete list of accepted industry values for the search endpoints. The `industry` parameter in [Company search](/docs/endpoints/search-companies) and `currentCompanyIndustry` in [People search](/docs/endpoints/search-persons) 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 normalised for you, 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 (491) - 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 --- # Connect AI agents (MCP) > Connect Claude, ChatGPT, Cursor, and other AI agents to ReverseContact using the Model Context Protocol. ## 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, and more, all through natural conversation. ReverseContact exposes **8 tools** via MCP. Same data, same credits, same rate limits as the REST API. ## Prerequisites - A ReverseContact 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 ReverseContact 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 ReverseContact 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). Prefer to add it by hand, or 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. The fastest path is one command in your terminal, no API key needed: ::: code-group [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: ::: code-group [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: ::: code-group [.mcp.json] ```json [JSON] { "mcpServers": { "reversecontact": { "type": "http", "url": "https://api.reversecontact.com/mcp", "headers": { "Authorization": "Bearer rc_your_api_key" } } } } ``` ### ChatGPT ReverseContact 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 ReverseContact 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). Prefer to add it by hand, or your organization restricts the directory? Enable Developer mode under **Settings** > **Connectors** > **Advanced settings** (paid ChatGPT plan: Plus, Pro, Business, Enterprise or Edu), then **Create** a connector: name it `ReverseContact`, 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: ::: code-group [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`: ::: code-group [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): ::: code-group [.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 ReverseContact to `~/.gemini/settings.json` for your user account, or `.gemini/settings.json` for one project: ::: code-group [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: ::: code-group [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 LinkedIn profile from cache, or resolve by email/name | 1 (cached) / 2 (resolve 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 LinkedIn profile from cache (description, size, HQ, industry) | 1 (cached) | 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 | 1 / page | Instant | | `search_companies` | Search companies by name, domain, industry, size, location | 1 / page | Instant | ### Utility (free) | Tool | What it does | Credits | Response | |------|-------------|---------|----------| | `check_usage` | Check your credit balance and rate limit status | 0 | Instant | | `check_task` | Poll the status of an async operation | 0 | Instant | ## 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 LinkedIn profile for `alex@acme.com` and summarize current title, company, and recent experience. 2. **Enrich a company from a domain** > Look up the company for domain `stripe.com` and return 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. **Search companies with filters** > Search for fintech companies with 50 to 200 employees headquartered in France. List name, domain, and size. 5. **Check usage before a batch** > Check my ReverseContact credit balance and rate limit status before I run a larger enrichment batch. 6. **Async live enrichment workflow** > Live-enrich the LinkedIn profile `https://www.linkedin.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. Tip: for live enrichments the agent receives a `taskId` from `enrich_person_live` or `enrich_company_live` and should call `check_task` until 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`) run asynchronously. They return a `taskId` instead of data: ::: code-group [Async response] ```json [JSON] { "taskId": "abc123", "status": "processing", "message": "Scraping LinkedIn profile. Poll with check_task in ~10 seconds." } ``` Use `check_task` with the `taskId` to poll for results. Tasks typically complete in 10–60 seconds. **Status flow:** `in_progress` → `succeeded` or `errored` When succeeded, `check_task` returns the full data payload. Your AI agent handles this polling automatically. Just ask for what you need. ## Credits and rate limits MCP tools consume the same credits and respect the same rate limits as the REST API. That means: - MCP calls use the same endpoint pricing as REST. - MCP calls count toward the same gateway RPM limits as REST API calls. - Agent LLM turns are different: they consume credits but do not affect gateway RPM until the Agent triggers a real tool call. Use `check_usage` to inspect your current balance before expensive operations. See [Rate limits & credits](/docs/guides/rate-limits-credits) for details on 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 | | `TRIAL_RESTRICTED` | Trial plans cannot use live/async endpoints | Upgrade to a paid plan | | Connection timeout | Client cannot reach the server | Verify the URL is `https://api.reversecontact.com/mcp` and your network allows HTTPS | --- # Migrating from V1 to V2 > Complete guide to migrate your integration from the legacy V1 API to the modern V2 API. ## Introduction The V2 API is the recommended version for all new integrations. It offers a cleaner architecture, standardized responses, contextual pricing, and better monitoring capabilities. **The V1 API remains fully available**; there is no deprecation date. You can migrate at your own pace, endpoint by endpoint. Both versions coexist and share your workspace credits. ### Why migrate? - **Standard authentication** Bearer token header instead of query parameter - **Structured responses** consistent `data` / `error` / `metadata` / `quotas` envelope - **Contextual pricing** pay less for cached results - **Free Check endpoints** verify data freshness before spending credits - **GDPR/CCPA compliance** automatic blocklist enforcement with clear `451` status codes - **Per-key rate limits** finer control over API key usage alongside workspace limits - **Webhook simplification** workspace-level URL or per-request override, no dedicated alert endpoints ## Migration checklist Here's everything you need to do. Each step links to the detailed section below. 1. [Generate a V2 API key](#authentication) (`rc_` prefix) from the dashboard 2. [Switch to Bearer header authentication](#authentication). `Authorization: Bearer rc_...` 3. [Update endpoint URLs](#endpoint-mapping). See the mapping tables 4. [Convert GET to POST with JSON body](#request-format-changes) where applicable 5. [Update response parsing](#response-format). Access data via `response.data` instead of root-level fields 6. [Update error handling](#error-handling). Use `error.code` and `error.message` instead of `title` and `msg` 7. [Update quota tracking](#rate-limiting). Use the new `quotas.workspace` and `quotas.key` structure 8. [Configure a webhook URL](#webhooks) at workspace level if using async endpoints 9. Test your new key with `GET /v2/usage` (free) to validate connectivity 10. Migrate endpoint by endpoint. Both versions coexist, so you can move gradually > [!TIP] > Start by migrating your most-used endpoint first. Once you are comfortable with the V2 response format, the remaining endpoints follow the same pattern. ## Authentication ### V1: Query parameter V1 authenticates via a query parameter using keys with the `sk_` prefix: ::: code-group [V1 Authentication] ```bash [Bash] curl "https://api.reversecontact.com/v1/enrichment/profile?apikey=sk_your_api_key&linkedin_url=https://social.com/in/example" ``` ### V2: Bearer header (mandatory) V2 uses the standard `Authorization` header with keys using the `rc_` prefix: ::: code-group [V2 Authentication] ```bash [Bash] curl -X POST https://api.reversecontact.com/v2/fetch/persons \ -H "Authorization: Bearer rc_your_api_key" \ -H "Content-Type: application/json" \ -d '{"url": "https://social.com/in/example"}' ``` > [!TIP] > Generate a V2 API key (`rc_` prefix) from the [API Keys](/api-keys) page in your dashboard. Your existing `sk_` keys continue to work with V1 endpoints. ## Response format ### V1 response V1 mixes data fields at the root level alongside quota information: ::: code-group [V1 Response] ```json [JSON] { "success": true, "credits_consumed": 2, "credits_left": 5479, "rate_limit_left": 997, "daily_rate_limit_left": 997, "minute_rate_limit_left": 48, "next_minute_rate_limit_reset": "2025-01-15T09:31:00.000Z", "quotas": { "credits": { "total": 10000, "used": 4521, "left": 5479 }, "daily_rate_limit": { "limit": 1000, "used": 3, "left": 997, "next_reset": "..." }, "minute_rate_limit": { "limit": 50, "used": 2, "left": 48, "next_reset": "..." } }, "metadata": { "source": "cache", "request_id": "a1b2c3d4-...", "executionTimeMs": 1234 }, "person": { "firstName": "Jane", "lastName": "Doe" } } ``` ### V2 response V2 uses a standardized envelope with clear separation between data, errors, metadata, and quotas: ::: code-group [V2 Response] ```json [JSON] { "success": true, "data": { "firstName": "Jane", "lastName": "Doe" }, "error": null, "metadata": { "requestId": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", "executionTimeMs": 1234 }, "quotas": { "creditsConsumed": 2, "workspace": { "id": "ws_...", "credits": { "total": 10000, "used": 4521, "left": 5479 }, "minuteRateLimit": { "limit": 50, "used": 2, "left": 48, "nextReset": "..." }, "dailyLimit": null, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_...", "dailyLimit": null, "minuteRateLimit": { "limit": 50, "used": 2, "left": 48, "nextReset": null } } } } ``` ### Key differences | Aspect | V1 | V2 | | ------ | -- | -- | | Data location | Root level (e.g. `response.person`) | Inside `response.data` | | Request ID | `metadata.request_id` (snake_case) | `metadata.requestId` (camelCase) | | Data source | `metadata.source` (`"cache"` or `"fresh"`) | Not in body (available in logs) | | Quota fields | Flat fields at root + nested `quotas` | Structured `quotas.workspace` + `quotas.key` | | Rate limits | Workspace-level only | Workspace + per-key limits | > [!TIP] > When migrating your response parser, the main change is accessing `response.data` instead of the root-level data field (e.g. `response.person`, `response.company`). ## Error handling ### V1 error format ::: code-group [404 Not Found] ```json [JSON] { "success": false, "title": "Not Found", "msg": "No data found" } ``` ### V2 error format ::: code-group [404 Not Found] ```json [JSON] { "success": false, "data": null, "error": { "code": "REQUEST_ERROR", "message": "Profile not found" }, "metadata": { "requestId": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", "executionTimeMs": 234 } } ``` ### V2 error codes | Status | Description | | ------ | ----------- | | `400` | Invalid JSON body or endpoint-specific validation | | `401` | Missing or invalid API key | | `402` | Insufficient credits | | `403` | API key lacks required permission, or endpoint requires a paid plan | | `404` | No result found for your query | | `422` | Schema or validation error (e.g. malformed input, disposable email, missing webhook URL) | | `429` | Rate limit exceeded. Wait and retry after `nextReset` | | `451` | Data Subject Blocked. Profile removed per GDPR/CCPA request | | `503` | Temporary service issue. Retry with backoff | | `504` | The lookup timed out. Retry with backoff | For the full error reference, see [Error handling & retries](/docs/guides/errors-retries). ## Endpoint mapping Most V1 endpoints have a V2 equivalent. V2 standardizes all endpoints to **POST with JSON body** and **Bearer header authentication**. ### Person endpoints | V1 | V2 | Delivery | | -- | -- | :------: | | `GET /v1/enrichment/profile` | `POST /v2/fetch/persons` | Sync | | | `POST /v2/fetch/persons/live` | Async | | `GET /v1/enrichment` | `POST /v2/enrich/persons` | Sync | | `POST /v1/enrichment/match` | `POST /v2/enrich/persons` | Sync | | `POST /v1/enrichment/persons/search` | `POST /v2/search/persons` | Sync | ### Company endpoints | V1 | V2 | Delivery | | -- | -- | :------: | | `GET /v1/enrichment/company` | `POST /v2/fetch/companies` | Sync | | | `POST /v2/fetch/companies/live` | Async | | `GET /v1/enrichment/company/domain` | `POST /v2/enrich/companies` | Sync | | `POST /v1/enrichment/companies/search` | `POST /v2/search/companies` | Sync | ### Email & contact endpoints | V1 | V2 | Delivery | | -- | -- | :------: | | `POST /v1/enrichment/emails/finder` | `POST /v2/contact/email` | Async webhook | | `POST /v1/enrichment/emails/finder/url` | `POST /v2/contact/email` | Async webhook | | `GET /v1/enrichment/resolve/email` | `POST /v2/enrich/persons` | Sync | | `POST /v1/enrichment/profile-from-email` | `POST /v2/enrich/persons` | Sync | ### Job endpoints | V1 | V2 | Delivery | | -- | -- | :------: | | `GET /v1/enrichment/jobs/*` | _Not ported_ | | ### Utility endpoints | V1 | V2 | Delivery | | -- | -- | :------: | | `GET /v1/workspaces/quotas` | `GET /v2/usage` | Sync | | `GET /v1/logs/requests` | Via [Dashboard](/logs) | | | `GET /v1/logs/reports`, `POST /v1/logs/report` | Via [Dashboard](/logs) | | ## Request format changes Here are two common examples. All other endpoints follow the same pattern; see the full [Endpoint mapping](#endpoint-mapping) above. ### Fetch a person profile (live) **Before (V1):** ::: code-group [V1] ```bash [Bash] curl "https://api.reversecontact.com/v1/enrichment/profile?apikey=sk_your_key&linkedin_url=https://social.com/in/janedoe" ``` **After (V2):** ::: code-group [V2] ```bash [Bash] # webhookUrl is optional if a default is configured in Settings > Webhooks curl -X POST https://api.reversecontact.com/v2/fetch/persons/live \ -H "Authorization: Bearer rc_your_key" \ -H "Content-Type: application/json" \ -d '{"url": "https://social.com/in/janedoe"}' ``` ## Credits & pricing V2 introduces contextual pricing: the credit cost depends on whether data comes from a live scrape or cache, and whether a result was found or not. **For the full credit breakdown per endpoint, see [Rate limits & credits](/docs/guides/rate-limits-credits).** ### What's new in V2 - **Check endpoints** [free endpoints](/docs/endpoints/check-person) to verify data existence and freshness before spending credits. - **Company Search** new [company search](/docs/endpoints/search-companies) endpoint (1 credit). - **GDPR/CCPA blocklist** automatic enforcement with `451` status code. - **Disposable email detection** `422` error when a disposable email domain is used. ## Rate limiting Both V1 and V2 enforce minute-level and daily rate limits at the workspace level. ### What changes in V2 - **Per-key limits** V2 adds rate limits scoped to individual API keys. See [API key management](/docs/guides/api-key-management#3-set-rate-limits-optional). - **Structured quota response** rate limit status is available in every response under `quotas.workspace.minuteRateLimit` and `quotas.key.minuteRateLimit`. - **`nextReset` field** tells you exactly when the current minute window resets (ISO 8601 timestamp). For the full details, see [Rate limits & credits](/docs/guides/rate-limits-credits). ## Async results V2 delivers most async results via polling or a webhook URL configured in **[Settings > Webhooks](/settings/webhooks)** or supplied per request with `webhookUrl`. The Contact Email Finder uses the webhook flow: the initial acknowledgement contains `webhookId`, followed by a callback containing the email array or an error code. See the [Polling guide](/docs/guides/polling) for the recommended path with side-by-side comparison, or [Webhooks](/docs/guides/webhooks) for push delivery setup. --- # Enrich profile > Get a person's professional profile from any details you have. ## Introduction The Enrich Profile endpoint returns a person's professional profile from any combination of identifying details. Every input is optional and independent; you only need to provide at least one, and the more fields you include, the more accurate the match. This endpoint costs **2 credits**, charged **only when a person is found**. Add **+1 credit** when `fullProfile: true` and a match is found. A not-found response (`404`) is always free, even with `fullProfile`: 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 | Optional, defaults to `false`. When `true` and a match is found, returns the full profile (same fields as the corresponding fetch endpoint) for 1 extra credit. | ## Best practices Every field you add narrows the search. That is a trade-off, not a free win. **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 Dupont. Nothing in that response guarantees it is *your* Pierre Dupont. In any case, whenever more than one person matches your request, the extra candidates are returned in the response 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 actually known at Acme. You will get more `404`s, and the matches you do get are 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`. ### Full profile Set `fullProfile` to `true` to return the same person shape as `/v2/fetch/persons` when a match is found. The primary `data.person` then includes full-profile fields such as `summary`, `photoUrl`, `experience`, `education`, `skills`, `languages`, `certifications`, and more. `data.alternativePersons` always stays in the light enrich shape. ```json { "email": "jane.doe@acme.com", "fullProfile": true } ``` The complete full-profile field reference is documented on the [Person Profile endpoint](/docs/endpoints/fetch-profile). If the full profile cannot be assembled for the matched person, the response falls back to the light shape and only the base 2 credits are charged: the extra credit applies only when the full profile is actually delivered. To know which shape you received, check `quotas.creditsConsumed` (`3` for full, `2` for light) or the presence of a full-profile field such as `experience`. | 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) **person:** - EnrichedPerson - The person profile | 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](/docs/guides/current-position-resolution)) | | `currentCompanyId` | string | Internal identifier of the current company | | `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) | **alternativePersons:** - EnrichedPerson[] - Other candidates that matched your input but scored lower than `person`, ordered best first (up to 9). Empty when the match is unique, which is the typical case when enriching by LinkedIn URL. Alternatives mostly show up when resolving by email or by name and company, where several people can match. | 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](/docs/guides/current-position-resolution)) | | `currentCompanyId` | string | 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-group [Code Examples] ```javascript [JavaScript] const response = await fetch("https://api.reversecontact.com/v2/enrich/persons", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ email: "jane.doe@acme.com" }) }); const { data } = await response.json(); console.log(data.person.firstName, data.person.lastName, data.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] import requests response = requests.post( "https://api.reversecontact.com/v2/enrich/persons", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={"email": "jane.doe@acme.com"} ) person = response.json()["data"]["person"] print(person["firstName"], person["lastName"], person["currentCompanyName"]) ``` ## Example response ::: code-group [Example Response] ```json [200 - Success] { "success": true, "data": { "person": { "id": "prs_9f8e7d6c5b4a", "publicId": "jane-doe", "linkedinUrl": "https://www.social.com/in/jane-doe", "firstName": "Jane", "lastName": "Doe", "headline": "VP of Marketing @ Acme Corp | Demand Gen & Brand", "currentPositionTitle": "VP of Marketing", "currentCompanyId": "cmp_12345678", "currentCompanyName": "Acme Corp", "currentCompanyLinkedinId": "12345678", "updateDate": "2026-04-07T14:22:17.271Z", "location": { "city": "San Francisco", "state": "California", "country": "United States", "countryCode": "US", "rawLocation": "San Francisco Bay Area" } }, "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", "currentPositionTitle": "Marketing Manager", "currentCompanyId": null, "currentCompanyName": "Acme Corp Europe", "currentCompanyLinkedinId": "87654321", "updateDate": "2026-03-11T09:04:52.118Z", "location": { "city": "London", "state": null, "country": "United Kingdom", "countryCode": "GB", "rawLocation": "London, England, United Kingdom" } } ] }, "error": null, "metadata": { "requestId": "req_e1n2r3i4c5h6", "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 profile from a domain. ## Introduction The Enrich Company endpoint returns a company's profile from a domain. This endpoint costs **2 credits**, charged **only when a company is found**. Add **+1 credit** when `fullProfile: true` and a match is found. A not-found response (`404`) is always free, even with `fullProfile`: 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 | 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 | Optional, defaults to `false`. When `true` and a match is found, returns the full profile (same fields as the corresponding fetch endpoint) for 1 extra credit. | ## Response structure The company is returned nested under `data.company`. ### Full profile Set `fullProfile` to `true` to return the same company shape as `/v2/fetch/companies` when a match is found. The primary `data.company` then includes the full company profile owned by the fetch endpoint rather than the light enrich shape. ```json { "domain": "reversecontact.com", "fullProfile": true } ``` The complete full-profile field reference is documented on the [Company Profile endpoint](/docs/endpoints/fetch-company). Note that the full shape uses the fetch naming: the company name is returned as `companyName` instead of `name`. If the full profile cannot be assembled for the matched company, the response falls back to the light shape and only the base 2 credits are charged: the extra credit applies only when the full profile is actually delivered. To know which shape you received, check `quotas.creditsConsumed` (`3` for full, `2` for light) or the presence of a full-profile field such as `description`. | 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) **company:** - EnrichedCompany - The company profile | Field | Type | Description | | -------------------- | -------------- | ---------------------------------------------------------------- | | `id` | string | Internal identifier for the company | | `name` | string | Company name | | `publicId` | string | Universal name / slug (the `/company/xxx` part of the URL) | | `linkedinId` | string | Social identifier of the company | | `linkedinUrl` | string | Full Social company URL | | `industry` | string | Industry (e.g. `"Software Development"`) | | `tagline` | string | Short company tagline | | `websiteUrl` | string | Official company website URL | | `employeesCount` | number | Exact number of employees | | `followersCount` | number | Number of followers on the company profile | | `companyUpdateDate` | string | When the record was last refreshed (ISO 8601) | **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-group [Code Examples] ```javascript [JavaScript] const response = await fetch("https://api.reversecontact.com/v2/enrich/companies", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ domain: "reversecontact.com" }) }); const { data } = await response.json(); console.log(data.company.name, 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] import requests response = requests.post( "https://api.reversecontact.com/v2/enrich/companies", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={"domain": "reversecontact.com"} ) company = response.json()["data"]["company"] print(company["name"], company["industry"]) ``` ## Example response ::: code-group [Example Response] ```json [200 - Success] { "success": true, "data": { "company": { "id": "com_01jbd5vewyenebxgaz869ffe5t", "name": "Reverse Contact", "publicId": "reverse-contact", "linkedinId": "89181966", "linkedinUrl": "https://www.social.com/company/reverse-contact", "industry": "Software Development", "tagline": "The first reverse email and phone lookup powered by LinkedIn data", "websiteUrl": "https://www.reversecontact.com", "employeesCount": 19, "followersCount": 2075, "employeeCountRange": { "start": 11, "end": 50 }, "companyUpdateDate": "2026-04-07T14:22:17.271Z", "location": { "city": "Paris", "country": "FR" } } }, "error": null, "metadata": { "requestId": "req_c1o2m3p4a5n6", "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 the database for person profiles using multiple filters. ## Introduction The People Search endpoint returns paginated person profiles matching a combination of filters. All filters are optional and combined with **AND** logic. Results are drawn from our cache-based search index. This endpoint never triggers a live fetch. Use this endpoint to discover profiles that match criteria like job title, company, industry, or location, then chain into [Profile](/docs/endpoints/fetch-profile) for the complete data of any hit. ## 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. Scalar filters live in the table below; the three nested-object filters (`followersCount`, `currentCompanyEmployeeCountRange`, `location`) are documented as expandable field items right after. | 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 | | `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 | | `currentCompanyName` | string | No | Match the current company name | | `currentCompanyLinkedinId` | string | No | Match the current company Social identifier | | `currentCompanyPublicId` | string | No | Match the current company public ID / slug | | `currentCompanyId` | string | No | ReverseContact internal company id (`com_...`) of the current employer. Single value only. Strictly matches people currently working at the company. This id comes from the `currentCompanyId` field in person search results, or the `id` field returned by company enrichment endpoints. | | `currentCompanyIndustry` | string \| string[] | No | Industry name or array of industries. See [Industry values](/docs/guides/industries) for the full list | | `maxDataAgeDate` | string | No | ISO 8601 datetime. Only returns profiles refreshed after this date | | `page` | number | No | Page number (1 to 500, default `1`) | | `perPage` | number | No | Results per page (1 to 100, default `100`) | > **Filter validation:** filters are validated before the query runs. Unknown field names, blank values, values outside a closed list (`currentCompanyIndustry`, `location.countryCode`) 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. See [Search filter validation](/docs/guides/errors-retries#search-filter-validation). > **Pagination limits:** `page` must be between `1` and `500`, and `perPage` must be between `1` and `100`, inclusive. Requests above either limit return HTTP `422` with `error.code` `PAGINATION_LIMIT_EXCEEDED`. The `error.details` object identifies the field and maximum, for example `{ "field": "page", "max": 500 }` or `{ "field": "perPage", "max": 100 }`. Narrow your filters or request fewer results per page. > **Note:** Title matching is token-based, not exact: `"CTO"` also matches `"Deputy CTO"`. Multiple titles are OR-matched, and exclusions always win over matches. **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 | **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` array. The request is still billed, so check your bounds against the list below. | 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+` | > **Examples:** `{ "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. **location:** - LocationFilter - Multi-field location filter. Each sub-field is optional and accepts a single string or an array of strings (OR matching). | Field | Type | Description | | ------------- | ------------------ | ----------------------------------- | | `country` | string \| string[] | Country name (e.g. `"France"`) | | `countryCode` | string \| string[] | ISO 3166 country code (e.g. `"FR"`) | | `region` | string \| string[] | State or region | | `city` | string \| string[] | City | | `area` | string \| string[] | Broader geographic area | ## Response structure > **Note:** This endpoint uses a **nested envelope**. The top-level V2 `data` field wraps an inner search payload that contains the actual 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 - PersonSearchResult[] - Array of matching person profiles wrapped under `data.data` in the envelope. Empty array if no match. | Field | Type | Description | | -------------------------- | -------------- | -------------------------------------------------------------------- | | `id` | string | ReverseContact internal person identifier (`prs_...`) | | `publicId` | string \| null | Social public identifier (the `/in/xxx` slug) | | `linkedinUrl` | string \| null | Full Social profile URL | | `firstName` | string | First name | | `lastName` | string | Last name | | `headline` | string \| null | Profile headline | | `currentPositionTitle` | string \| null | Current job title ([how it is chosen](/docs/guides/current-position-resolution)) | | `currentCompanyId` | string \| null | ReverseContact internal identifier of the current company (`com_...`) | | `currentCompanyName` | string \| null | Current company name | | `currentCompanyLinkedinId` | string \| null | Current company Social identifier | | `updateDate` | string \| null | When ReverseContact last refreshed this record (ISO 8601) | **location:** - PersonSearchLocation - 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 | **data:**.metadata - SearchPagination - Pagination metadata wrapped under `data.metadata` in the envelope. | Field | Type | Description | | ------------- | ------ | --------------------------------------------------- | | `currentPage` | number | Current page number (1-indexed) | | `pageNumber` | number | Total number of pages available | | `perPage` | number | Results per page | | `total` | number | Total number of matching records across all pages | **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-group [Code Examples] ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/search/persons \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "currentPositionTitle": ["VP of Marketing", "Head of Marketing"], "excludeCurrentPositionTitle": "Assistant", "currentCompanyIndustry": ["Software Development", "Technology, Information and Internet"], "location": { "country": "United States", "city": ["San Francisco", "New York"] }, "followersCount": { "min": 1000 }, "isOpenToWork": false, "page": 1, "perPage": 100 }' ``` ```javascript [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", "Head of Marketing"], excludeCurrentPositionTitle: "Assistant", currentCompanyIndustry: ["Software Development", "Technology, Information and Internet"], location: { country: "United States", city: ["San Francisco", "New York"] }, followersCount: { min: 1000 }, isOpenToWork: false, page: 1, perPage: 100 }) }); const payload = await response.json(); const results = payload.data.data; const pagination = payload.data.metadata; console.log(`Found ${pagination.total} people, showing ${results.length} on page ${pagination.currentPage}`); ``` ```python [Python] import requests response = requests.post( "https://api.reversecontact.com/v2/search/persons", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "currentPositionTitle": ["VP of Marketing", "Head of Marketing"], "excludeCurrentPositionTitle": "Assistant", "currentCompanyIndustry": ["Software Development", "Technology, Information and Internet"], "location": { "country": "United States", "city": ["San Francisco", "New York"] }, "followersCount": {"min": 1000}, "isOpenToWork": False, "page": 1, "perPage": 100 } ) payload = response.json() results = payload["data"]["data"] pagination = payload["data"]["metadata"] print(f"Found {pagination['total']} people, showing {len(results)} on page {pagination['currentPage']}") ``` ## Example response ::: code-group [Example Response] ```json [200 - Success] { "success": true, "data": { "data": [ { "id": "prs_01h2x3y4z5a6b7c8d9e0f1g2h3", "currentCompanyId": "com_01j2x3y4z5a6b7c8d9e0f1g2h3", "publicId": "janedoe", "linkedinUrl": "https://www.social.com/in/janedoe", "firstName": "Jane", "lastName": "Doe", "headline": "VP of Marketing @ Acme Corp | Demand Gen & Brand", "currentPositionTitle": "VP of Marketing", "currentCompanyName": "Acme Corp", "currentCompanyLinkedinId": "12345678", "updateDate": "2026-03-18T09:12:44.000Z", "location": { "city": "San Francisco", "state": "California", "country": "United States of America", "countryCode": "US", "rawLocation": "San Francisco Bay Area" } }, { "id": "prs_01k4x3y4z5a6b7c8d9e0f1g2h3", "currentCompanyId": null, "publicId": "marcus-rivera", "linkedinUrl": "https://www.social.com/in/marcus-rivera", "firstName": "Marcus", "lastName": "Rivera", "headline": "VP of Marketing at Globex | B2B SaaS Growth", "currentPositionTitle": "VP of Marketing", "currentCompanyName": "Globex", "currentCompanyLinkedinId": "98765432", "updateDate": "2026-02-27T14:40:05.000Z", "location": { "city": "Austin", "state": "Texas", "country": "United States of America", "countryCode": "US", "rawLocation": null } } ], "metadata": { "currentPage": 1, "pageNumber": 3, "perPage": 100, "total": 237 } }, "error": null, "metadata": { "requestId": "req_s1e2a3r4c5h6", "executionTimeMs": 340 }, "quotas": { "creditsConsumed": 1, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4522, "left": 5478 }, "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 search > Search the database for company profiles using multiple filters. ## Introduction The Company Search endpoint returns paginated company profiles matching a combination of filters. All filters are optional and combined with **AND** logic. Results are drawn from our cache-based search index. This endpoint never triggers a live fetch. Use this endpoint to discover companies that match criteria like industry, size, or headquarters location, then chain into [Profile](/docs/endpoints/fetch-company) for the complete data of any hit. ## 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. Scalar filters live in the table below; the two nested-object filters (`employeeCountRange`, `location`) are documented as expandable field items right after. | Name | Type | Required | Description | | ---------------- | ------------------ | -------- | -------------------------------------------------------------------------- | | `companyName` | string | No | Filter by company name | | `companyDomain` | string | No | Filter by company website domain. Disposable domains are rejected with `422` | | `industry` | string \| string[] | No | Industry name or array of industries. See [Industry values](/docs/guides/industries) for the full list | | `maxDataAgeDate` | string | No | ISO 8601 datetime. Only returns companies refreshed after this date | | `page` | number | No | Page number (1 to 500, default `1`) | | `perPage` | number | No | Results per page (1 to 100, default `100`) | > **Filter validation:** filters are validated before the query runs. Unknown field names, blank values, values outside a closed list (`industry`, `location.countryCode`) 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. See [Search filter validation](/docs/guides/errors-retries#search-filter-validation). > **Pagination limits:** `page` must be between `1` and `500`, and `perPage` must be between `1` and `100`, inclusive. Requests above either limit return HTTP `422` with `error.code` `PAGINATION_LIMIT_EXCEEDED`. The `error.details` object identifies the field and maximum, for example `{ "field": "page", "max": 500 }` or `{ "field": "perPage", "max": 100 }`. Narrow your filters or request fewer results per page. **employeeCountRange:** - RangeFilter - Filter by 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` array. The request is still billed, so check your bounds against the list below. | 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+` | > **Examples:** `{ "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. **location:** - LocationFilter - Multi-field location filter. Each sub-field is optional and accepts a single string or an array of strings (OR matching). Note: there is no `area` field here (unlike the person location filter). | Field | Type | Description | | ------------- | ------------------ | --------------------------------------- | | `country` | string \| string[] | Country name (e.g. `"United States"`) | | `countryCode` | string \| string[] | ISO 3166 country code (e.g. `"US"`) | | `region` | string \| string[] | State or region | | `city` | string \| string[] | City | ## Response structure > **Note:** This endpoint uses a **nested envelope**. The top-level V2 `data` field wraps an inner search payload that contains the actual 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 - CompanySearchResult[] - Array of matching company profiles wrapped under `data.data` in the envelope. Empty array if no match. | Field | Type | Description | | -------------------- | -------------------------- | -------------------------------------------------------------------------- | | `id` | string | ReverseContact company id (`com_...`). Use it as the `id` parameter of the [Company Profile endpoint](/docs/endpoints/fetch-company) | | `name` | string | Company name | | `publicId` | string | Universal name / slug (the `/company/xxx` part of the URL) | | `linkedinId` | string | Social company identifier | | `linkedinUrl` | string \| null | Full Social company URL | | `websiteUrl` | string \| null | Official company website URL | | `industry` | string \| null | Industry (e.g. `"Software Development"`) | | `tagline` | string \| null | Tagline / slogan | | `employeesCount` | number | Exact number of employees | | `followersCount` | number | Number of Social followers | | `employeeCountRange` | EmployeeCountRange \| null | Employee count bracket. `null` if unknown | | `companyUpdateDate` | string \| null | When ReverseContact last refreshed this record (ISO 8601) | **employeeCountRange:** - EmployeeCountRange - Employee count bracket (`null` if unknown). Note the `start` / `end` field names (different from the request `RangeFilter` which uses `min` / `max`). Buckets mirror LinkedIn'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+`), LinkedIn has no upper bound and returns `end: 1` as a sentinel — interpret any range where `end < start` as `start+` (unbounded). Use `employeeCount` for the exact headcount when available. | Field | Type | Description | | ------- | ------ | ---------------------------------------------------------------------------------------- | | `start` | number | Lower bound of the range | | `end` | number | Upper bound of the range. If `end < start`, the bucket is unbounded (`start+`, i.e. `10,001+`) | > **Example (10,001+ bucket):** `{ "start": 10001, "end": 1 }`. Since `end` (1) < `start` (10001), this means the company has 10,001 or more employees. No upper bound is available for this bucket. **location:** - CompanySearchLocation - Primary location of the company (always present, fields may be `null`) | Field | Type | Description | | --------- | -------------- | ---------------------------------------- | | `city` | string \| null | City | | `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 | | ------------- | ------ | --------------------------------------------------- | | `currentPage` | number | Current page number (1-indexed) | | `pageNumber` | number | Total number of pages available | | `perPage` | number | Results per page | | `total` | number | Total number of matching records across all pages | **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-group [Code Examples] ```bash [cURL] curl -X POST https://api.reversecontact.com/v2/search/companies \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "industry": ["Software Development", "Technology, Information and Internet"], "employeeCountRange": { "min": 50, "max": 500 }, "location": { "country": "United States", "city": ["San Francisco", "Austin"] }, "page": 1, "perPage": 100 }' ``` ```javascript [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", "Technology, Information and Internet"], employeeCountRange: { min: 50, max: 500 }, location: { country: "United States", city: ["San Francisco", "Austin"] }, page: 1, perPage: 100 }) }); const payload = await response.json(); const results = payload.data.data; const pagination = payload.data.metadata; console.log(`Found ${pagination.total} companies, showing ${results.length} on page ${pagination.currentPage}`); ``` ```python [Python] import requests response = requests.post( "https://api.reversecontact.com/v2/search/companies", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "industry": ["Software Development", "Technology, Information and Internet"], "employeeCountRange": {"min": 50, "max": 500}, "location": { "country": "United States", "city": ["San Francisco", "Austin"] }, "page": 1, "perPage": 100 } ) payload = response.json() results = payload["data"]["data"] pagination = payload["data"]["metadata"] print(f"Found {pagination['total']} companies, showing {len(results)} on page {pagination['currentPage']}") ``` ## Example response ::: code-group [Example Response] ```json [200 - Success] { "success": true, "data": { "data": [ { "id": "com_01jbd5vewyenebxgaz869ffe5t", "name": "Acme Corp", "publicId": "acme-corp", "linkedinId": "12345678", "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", "industry": "Software Development", "companyUpdateDate": "2026-03-18T09:12:44.000Z", "location": { "city": "San Francisco", "country": "US" } }, { "id": "com_01k2f8aq3xvznebxgaz869ffe5t", "name": "Globex Inc.", "publicId": "globex-inc", "linkedinId": "98765432", "linkedinUrl": "https://www.social.com/company/globex-inc", "employeesCount": 187, "followersCount": 12340, "employeeCountRange": { "start": 51, "end": 200 }, "websiteUrl": "https://www.globex.com", "tagline": "Ship faster, ship smarter", "industry": "Software Development", "companyUpdateDate": "2026-02-27T14:40:05.000Z", "location": { "city": "Austin", "country": "US" } } ], "metadata": { "currentPage": 1, "pageNumber": 2, "perPage": 100, "total": 184 } }, "error": null, "metadata": { "requestId": "req_c1o2m3p4a5n6", "executionTimeMs": 280 }, "quotas": { "creditsConsumed": 1, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4523, "left": 5477 }, "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 6, "left": 54, "nextReset": "2025-01-20T14:16:00.000Z" }, "hasUnlimitedCredits": false, "allowDailyOvercost": false }, "key": { "id": "key_abc123", "dailyLimit": null, "minuteRateLimit": { "limit": 60, "used": 6, "left": 54, "nextReset": "2025-01-20T14:16:00.000Z" } } } } ``` --- # Profile > Retrieve a complete professional profile from a Social URL. ## Introduction The Person Profile endpoint retrieves a complete professional profile from a Social URL. It returns structured data including work experience, education, skills, certifications, and more. ## 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 | ReverseContact 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 | ReverseContact 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 memorialised | | `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, predictable rules. See [Current position and work history ordering](/docs/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"`) | | `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"`) | | `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-group [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"}' ``` ```javascript [JavaScript] const response = await fetch("https://api.reversecontact.com/v2/fetch/persons", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://social.com/in/janedoe" }) }); const data = await response.json(); console.log(data.data.firstName, data.data.lastName); ``` ```python [Python] import requests response = requests.post( "https://api.reversecontact.com/v2/fetch/persons", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={"url": "https://social.com/in/janedoe"} ) data = response.json() print(data["data"]["firstName"], data["data"]["lastName"]) ``` ## Example response ::: code-group [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", "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", "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", "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": "req_a1b2c3d4e5f6", "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" } } } } ``` --- # Profile live > Trigger a live fetch of a Social profile and retrieve results via polling or webhook callback. ## 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](/docs/guides/polling) (recommended, no public endpoint required) or via a [webhook callback](/docs/guides/webhooks) (advanced, push delivery). > [!NOTE] > **Async, not real-time.** We aim to deliver each result in under 15 seconds, but it can take tens of minutes depending on conditions. Built to update databases and feed async pipelines, not to back a synchronous request a user is waiting on. See [Polling](/docs/guides/polling) for delivery details. > [!IMPORTANT] > **Not available on the free trial.** This endpoint requires an active pay-as-you-go balance or an Enterprise plan. [Add credits](/settings/billing) to your workspace to unlock it, or contact sales to discuss Enterprise pricing. ## 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 | ReverseContact 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](/docs/guides/polling) (recommended). See the [Webhooks guide](/docs/guides/webhooks) when you need push delivery. | ## Response structure This endpoint replies immediately with a `webhookId`. You then have two ways to retrieve the actual result: 1. **Polling (recommended).** Call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. No webhook URL needed. See the [Polling guide](/docs/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.** Use 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) 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 is **not wrapped** in an envelope (no `success`, `error`, `metadata`, or `quotas` keys at the top level, unlike the synchronous response). | 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 | ReverseContact person id (`prs_...`). Use it as the `id` parameter of the [Person Profile endpoint](/docs/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 memorialised | | `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, predictable rules. See [Current position and work history ordering](/docs/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"`) | | `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"`) | | `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` does **not** contain the profile fields. Instead, it contains the original input echoed back together with an `errorCode` identifying the failure reason: ::: code-group [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` will be one of the following 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. | > [!NOTE] > **Billed** means the credits consumed at request time are kept. **Not billed** means the credits are fully refunded to your workspace balance. ## Code examples ::: code-group [Code Examples] ```javascript [JavaScript - Polling (recommended)] 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(); async function pollUntilDone(id) { while (true) { const res = await fetch(`${BASE}/v2/webhooks/${id}`, { headers: { "Authorization": `Bearer ${KEY}` } }); const { data } = await res.json(); if (data.status === "succeeded") return data.result; if (data.status === "errored") throw new Error(data.errorCode ?? "Job failed"); await new Promise(r => setTimeout(r, 2000)); } } const result = await pollUntilDone(webhookId); console.log(result); ``` ```javascript [JavaScript - Webhook] const response = 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", webhookUrl: "https://your-app.com/webhooks/reversecontact" }) }); const result = await response.json(); console.log(result.data.status, result.data.webhookId); ``` ```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 - Polling (recommended)] import time import 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"] def poll_until_done(wid): while True: data = requests.get(f"{BASE}/v2/webhooks/{wid}", headers=H).json()["data"] if data["status"] == "succeeded": return data["result"] if data["status"] == "errored": raise RuntimeError(data.get("errorCode") or "Job failed") time.sleep(2) result = poll_until_done(webhook_id) print(result) ``` ```python [Python - Webhook] import requests response = 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", "webhookUrl": "https://your-app.com/webhooks/reversecontact" } ) result = response.json() print(result["data"]["status"], result["data"]["webhookId"]) ``` ## Example response ::: code-group [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": "req_a1b2c3d4e5f6", "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, "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", "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" } } ``` --- # Profile status > Check if a person profile exists in the database before requesting enrichment. ## 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:** get existence status, last update date, and content counts (experience, skills, education). 2. **If the profile exists and is recent:** use [Profile](/docs/endpoints/fetch-profile) (cached, 1 credit) to retrieve the full enriched data. 3. **If the profile is stale or missing:** use [Profile live](/docs/endpoints/fetch-profile-live) (fresh from Social, 2 credits) for an up-to-date result. This simple check → fetch/live decision can save significant credits at scale. > [!NOTE] > Profile Status is free, but it still requires a non-empty credit balance. When your workspace reaches 0 credits it returns `402 NO_CREDITS` (top up to resume), since no fetch or live pull could follow 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 | ReverseContact 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 ReverseContact 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-group [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"}' ``` ```javascript [JavaScript] const response = await fetch("https://api.reversecontact.com/v2/fetch/persons/check", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://social.com/in/janedoe" }) }); const data = await response.json(); if (data.data.exists) { console.log(`Profile exists, last updated ${data.data.lastUpdate}`); } else { console.log("Profile not in database yet"); } ``` ```python [Python] import requests response = requests.post( "https://api.reversecontact.com/v2/fetch/persons/check", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={"url": "https://social.com/in/janedoe"} ) data = response.json() if data["data"]["exists"]: print(f"Profile exists, last updated {data['data']['lastUpdate']}") else: print("Profile not in database yet") ``` ## Example response ::: code-group [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": "req_c1d2e3f4g5h6", "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": "req_d4e5f6g7h8i9", "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" } } } } ``` --- # Profile > Retrieve a complete company profile from a Social company URL. ## Introduction The Company Profile endpoint retrieves a complete company profile from a Social company URL. It returns structured data including basic information, metrics, headquarters, specialties, and more. ## 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 | ReverseContact 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 | ReverseContact 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 LinkedIn'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+`), LinkedIn 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. If `end < start`, the bucket is unbounded (`start+`, i.e. `10,001+`) | > **Example (10,001+ bucket):** `{ "start": 10001, "end": 1 }`. Since `end` (1) < `start` (10001), this means the company has 10,001 or more employees. No upper bound is available for this bucket. **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-group [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"}' ``` ```javascript [JavaScript] const response = await fetch("https://api.reversecontact.com/v2/fetch/companies", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://social.com/company/acme-corp" }) }); const data = await response.json(); console.log(data.data.companyName, data.data.industry); ``` ```python [Python] import requests response = requests.post( "https://api.reversecontact.com/v2/fetch/companies", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={"url": "https://social.com/company/acme-corp"} ) data = response.json() print(data["data"]["companyName"], data["data"]["industry"]) ``` ## Example response ::: code-group [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": "req_x7y8z9a0b1c2", "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" } } } } ``` --- # Profile live > Trigger a live fetch of a Social company profile and retrieve results via polling or webhook callback. ## 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](/docs/guides/polling) (recommended, no public endpoint required) or via a [webhook callback](/docs/guides/webhooks) (advanced, push delivery). > [!NOTE] > **Async, not real-time.** We aim to deliver each result in under 15 seconds, but it can take tens of minutes depending on conditions. Built to update databases and feed async pipelines, not to back a synchronous request a user is waiting on. See [Polling](/docs/guides/polling) for delivery details. > [!IMPORTANT] > **Not available on the free trial.** This endpoint requires an active pay-as-you-go balance or an Enterprise plan. [Add credits](/settings/billing) to your workspace to unlock it, or contact sales to discuss Enterprise pricing. ## 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 | ReverseContact 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](/docs/guides/polling) (recommended). See the [Webhooks guide](/docs/guides/webhooks) when you need push delivery. | ## Response structure This endpoint replies immediately with a `webhookId`. You then have two ways to retrieve the actual result: 1. **Polling (recommended).** Call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. No webhook URL needed. See the [Polling guide](/docs/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.** Use 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) 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 is **not wrapped** in an envelope (no `success`, `error`, `metadata`, or `quotas` keys at the top level, unlike the synchronous response). | 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 | ReverseContact company id (`com_...`). Use it as the `id` parameter of the [Company Profile endpoint](/docs/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 LinkedIn'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+`), LinkedIn has no upper bound and returns `end: 1` as a sentinel — interpret any range where `end < start` as `start+` (unbounded). Use `employeeCount` for the exact headcount when available. | Field | Type | Description | | ------- | ------ | ---------------------------------------------------------------------------------------- | | `start` | number | Lower bound of the range | | `end` | number | Upper bound of the range. If `end < start`, the bucket is unbounded (`start+`, i.e. `10,001+`) | > **Example (10,001+ bucket):** `{ "start": 10001, "end": 1 }`. Since `end` (1) < `start` (10001), this means the company has 10,001 or more employees. No upper bound is available for this bucket. **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` does **not** contain the company fields. Instead, it contains the original input echoed back together with an `errorCode` identifying the failure reason: ::: code-group [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` will be one of the following 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. | > [!NOTE] > **Billed** means the credits consumed at request time are kept. **Not billed** means the credits are fully refunded to your workspace balance. ## Code examples ::: code-group [Code Examples] ```javascript [JavaScript - Polling (recommended)] const KEY = "YOUR_API_KEY"; const BASE = "https://api.reversecontact.com"; const trigger = await fetch("https://api.reversecontact.com/v2/fetch/companies/live", { method: "POST", headers: { "Authorization": `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://social.com/company/acme-corp" }) }); const { data: { webhookId } } = await trigger.json(); async function pollUntilDone(id) { while (true) { const res = await fetch(`${BASE}/v2/webhooks/${id}`, { headers: { "Authorization": `Bearer ${KEY}` } }); const { data } = await res.json(); if (data.status === "succeeded") return data.result; if (data.status === "errored") throw new Error(data.errorCode ?? "Job failed"); await new Promise(r => setTimeout(r, 2000)); } } const result = await pollUntilDone(webhookId); console.log(result); ``` ```javascript [JavaScript - Webhook] const response = await fetch("https://api.reversecontact.com/v2/fetch/companies/live", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://social.com/company/acme-corp", webhookUrl: "https://your-app.com/webhooks/reversecontact" }) }); const result = await response.json(); console.log(result.data.status, result.data.webhookId); ``` ```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 - Polling (recommended)] import time import requests KEY = "YOUR_API_KEY" BASE = "https://api.reversecontact.com" trigger = requests.post( "https://api.reversecontact.com/v2/fetch/companies/live", headers={ "Authorization": f"Bearer {KEY}", "Content-Type": "application/json" }, json={ "url": "https://social.com/company/acme-corp" } ) webhook_id = trigger.json()["data"]["webhookId"] def poll_until_done(wid): while True: res = requests.get( f"{BASE}/v2/webhooks/{wid}", headers={"Authorization": f"Bearer {KEY}"} ) data = res.json()["data"] if data["status"] == "succeeded": return data["result"] if data["status"] == "errored": raise RuntimeError(data.get("errorCode") or "Job failed") time.sleep(2) result = poll_until_done(webhook_id) print(result) ``` ```python [Python - Webhook] import requests response = requests.post( "https://api.reversecontact.com/v2/fetch/companies/live", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "url": "https://social.com/company/acme-corp", "webhookUrl": "https://your-app.com/webhooks/reversecontact" } ) result = response.json() print(result["data"]["status"], result["data"]["webhookId"]) ``` ## Example response ::: code-group [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": "req_d4e5f6a7b8c9", "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" } } ``` --- # Profile status > Check if a company profile exists in the database before requesting enrichment. ## 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:** get existence status, last update date, and specialty count. 2. **If the company exists and is recent:** use [Profile](/docs/endpoints/fetch-company) (cached, 1 credit) to retrieve the full company profile. 3. **If the company is stale or missing:** use [Profile live](/docs/endpoints/fetch-company-live) (fresh from Social, 2 credits) for an up-to-date result. This simple check → fetch/live decision can save significant credits at scale. > [!NOTE] > Profile Status is free, but it still requires a non-empty credit balance. When your workspace reaches 0 credits it returns `402 NO_CREDITS` (top up to resume), since no fetch or live pull could follow 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 | ReverseContact 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 ReverseContact last fetched this company from the source (ISO 8601). `null` when `exists` is `false` | | `specialityCount` | number | Number of company specialties. 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-group [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"}' ``` ```javascript [JavaScript] const response = await fetch("https://api.reversecontact.com/v2/fetch/companies/check", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://social.com/company/acme-corp" }) }); const data = await response.json(); if (data.data.exists) { console.log(`Company exists, last updated ${data.data.lastUpdate}`); } else { console.log("Company not in database yet"); } ``` ```python [Python] import requests response = requests.post( "https://api.reversecontact.com/v2/fetch/companies/check", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={"url": "https://social.com/company/acme-corp"} ) data = response.json() if data["data"]["exists"]: print(f"Company exists, last updated {data['data']['lastUpdate']}") else: print("Company not in database yet") ``` ## Example response ::: code-group [Example Response] ```json [200 - Found] { "success": true, "data": { "exists": true, "lastUpdate": "2025-01-10T14:22:00.000Z", "specialityCount": 8 }, "error": null, "metadata": { "requestId": "req_e5f6g7h8i9j0", "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": "req_f6g7h8i9j0k1", "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 email address from a LinkedIn URL or full name and company domain, then receive the result by webhook. ## Introduction The Find Email endpoint discovers professional email addresses for a person. You can identify the person with a LinkedIn profile URL, 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](/docs/guides/polling) (recommended, no public endpoint required) or through a [webhook callback](/docs/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**, or provide `webhookUrl` in the request. > [!NOTE] > **Async, not real-time.** We aim to deliver each result in under 15 seconds, but it can take tens of minutes depending on conditions. Built to update databases and feed async pipelines, not to back a synchronous request a user is waiting on. See [Polling](/docs/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 Choose one lookup strategy. Use either a LinkedIn profile URL, or a full name with a company domain. The split `firstName` + `lastName` form is also accepted. | Name | Type | Required | Description | | --------------- | ------ | ----------- | -------------------------------------------------------------------------------------------------------- | | `url` | string | Conditional | Public LinkedIn profile URL, such as `https://www.linkedin.com/in/janedoe`. Use this for the URL 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 `fullName` + `companyDomain`, or `firstName` + `lastName` + `companyDomain`. ## Response structure This endpoint replies immediately with a `webhookId`. You then have two ways to retrieve the actual result: 1. **Polling (recommended).** Call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. No webhook URL needed. See the [Polling guide](/docs/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.** Use 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, ReverseContact 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. ::: code-group [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, typically `professional` | ## Webhook callback errors (advanced) When the lookup cannot return an email, `data` is **not** an array. Instead, it contains an `errorCode` identifying the failure reason: ::: code-group [Error payload] ```json [Webhook callback (error)] { "webhookId": "b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4", "data": { "errorCode": "email-not-found" } } ``` `data.errorCode` will be one of the following 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 costs **3 credits only when an email is found**. A lookup that returns `email-not-found`, `person-not-found`, `invalid-request`, or `fetch-data-error` costs 0 credits. A delivery failure remains billed when the email was successfully found. ## Code examples ::: code-group [Code Examples] ```javascript [JavaScript - Polling (recommended)] const KEY = "YOUR_API_KEY"; const BASE = "https://api.reversecontact.com"; const trigger = await fetch("https://api.reversecontact.com/v2/contact/email", { method: "POST", headers: { "Authorization": `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://www.linkedin.com/in/janedoe" }) }); const { data: { webhookId } } = await trigger.json(); async function pollUntilDone(id) { while (true) { const res = await fetch(`${BASE}/v2/webhooks/${id}`, { headers: { "Authorization": `Bearer ${KEY}` } }); const { data } = await res.json(); if (data.status === "succeeded") return data.result; if (data.status === "errored") throw new Error(data.errorCode ?? "Job failed"); await new Promise(r => setTimeout(r, 2000)); } } const result = await pollUntilDone(webhookId); console.log(result); ``` ```javascript [JavaScript - Webhook] const response = await fetch("https://api.reversecontact.com/v2/contact/email", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ fullName: "Jane Doe", companyDomain: "acme.com", webhookUrl: "https://your-app.com/webhooks/reversecontact" }) }); const result = await response.json(); console.log(result.data.status, result.data.webhookId); ``` ```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.linkedin.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", "webhookUrl": "https://your-app.com/webhooks/reversecontact" }' ``` ```python [Python - Polling (recommended)] import time import requests KEY = "YOUR_API_KEY" BASE = "https://api.reversecontact.com" trigger = requests.post( "https://api.reversecontact.com/v2/contact/email", headers={ "Authorization": f"Bearer {KEY}", "Content-Type": "application/json" }, json={ "url": "https://www.linkedin.com/in/janedoe" } ) webhook_id = trigger.json()["data"]["webhookId"] def poll_until_done(wid): while True: res = requests.get( f"{BASE}/v2/webhooks/{wid}", headers={"Authorization": f"Bearer {KEY}"} ) data = res.json()["data"] if data["status"] == "succeeded": return data["result"] if data["status"] == "errored": raise RuntimeError(data.get("errorCode") or "Job failed") time.sleep(2) result = poll_until_done(webhook_id) print(result) ``` ```python [Python - Webhook] import requests response = requests.post( "https://api.reversecontact.com/v2/contact/email", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "fullName": "Jane Doe", "companyDomain": "acme.com", "webhookUrl": "https://your-app.com/webhooks/reversecontact" } ) result = response.json() print(result["data"]["status"], result["data"]["webhookId"]) ``` ## Example response ::: code-group [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": "req_e1m2a3i4l5f6", "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" } ] } ``` --- # Usage > Check your workspace credit balance and rate limit status. ## Introduction The Usage endpoint returns your current workspace credit balance and rate limit status. Use it to monitor your consumption, check remaining credits, and verify rate limit windows, all **without spending any credits**. Unlike the other 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 - all useful information lives 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 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-group [Code Examples] ```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 data = await response.json(); console.log("Credits left:", data.quotas.workspace.credits.left); console.log("Rate limit:", data.quotas.workspace.minuteRateLimit.left, "requests remaining"); ``` ```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"]) print("Rate limit:", data["quotas"]["workspace"]["minuteRateLimit"]["left"], "requests remaining") ``` ## Example response ::: code-group [Example Response] ```json [200 - Success] { "success": true, "data": null, "error": null, "metadata": { "requestId": "req_u1s2a3g4e5f6", "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" } } } } ``` --- # People search available fields > Discover the filters accepted by the People Search endpoint. ## Introduction This endpoint returns metadata describing every filter accepted by [People search](/docs/endpoints/search-persons). It lets integrations (for example Clay) discover available inputs dynamically instead of hardcoding the filter list, so new search capabilities surface automatically as we release them. Like [Usage](/docs/endpoints/usage), this route uses `GET`, takes no request body, and **costs 0 credits**. > Looking for company filters? See [Company search available fields](/docs/endpoints/search-companies-available-fields). ## Endpoint | Method | URL | Describes | | ------ | ------------------------------------- | ------------- | | `GET` | `/v2/search/persons/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`, 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 (e.g. `[true, false]`) (optional) | | `properties` | object | Sub-field shapes for `range` and `location` filters (optional) | ## Code examples ::: code-group [Code Examples] ```bash [cURL] curl https://api.reversecontact.com/v2/search/persons/available-fields \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript [JavaScript] const response = await fetch( "https://api.reversecontact.com/v2/search/persons/available-fields", { headers: { "Authorization": "Bearer YOUR_API_KEY" } } ); const data = await response.json(); console.log(data.data.fields.map((f) => f.name)); ``` ```python [Python] import requests response = requests.get( "https://api.reversecontact.com/v2/search/persons/available-fields", headers={ "Authorization": "Bearer YOUR_API_KEY" } ) data = response.json() print([f["name"] for f in data["data"]["fields"]]) ``` ## Example response ::: code-group [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": "isOpenToWork", "type": "boolean", "required": false, "description": "Restrict to profiles flagged open-to-work", "allowedValues": [true, false] }, { "name": "followersCount", "type": "range", "required": false, "description": "Filter by follower count range. Both bounds are optional and inclusive.", "properties": { "min": { "type": "number", "description": "Minimum value, inclusive" }, "max": { "type": "number", "description": "Maximum value, inclusive" } } } ] }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "req_f1e2l3d4s5", "executionTimeMs": 3 } } ``` --- # Company search available fields > Discover the filters accepted by the Company Search endpoint. ## Introduction This endpoint returns metadata describing every filter accepted by [Company search](/docs/endpoints/search-companies). It lets integrations (for example Clay) discover available inputs dynamically instead of hardcoding the filter list, so new search capabilities surface automatically as we release them. Like [Usage](/docs/endpoints/usage), this route uses `GET`, takes no request body, and **costs 0 credits**. > Looking for people filters? See [People search available fields](/docs/endpoints/search-persons-available-fields). ## Endpoint | Method | URL | Describes | | ------ | --------------------------------------- | -------------- | | `GET` | `/v2/search/companies/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 `"company"` 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`, 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 (e.g. `[true, false]`) (optional) | | `properties` | object | Sub-field shapes for `range` and `location` filters (optional) | ## Code examples ::: code-group [Code Examples] ```bash [cURL] curl https://api.reversecontact.com/v2/search/companies/available-fields \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript [JavaScript] const response = await fetch( "https://api.reversecontact.com/v2/search/companies/available-fields", { headers: { "Authorization": "Bearer YOUR_API_KEY" } } ); const data = await response.json(); console.log(data.data.fields.map((f) => f.name)); ``` ```python [Python] import requests response = requests.get( "https://api.reversecontact.com/v2/search/companies/available-fields", headers={ "Authorization": "Bearer YOUR_API_KEY" } ) data = response.json() print([f["name"] for f in data["data"]["fields"]]) ``` ## Example response ::: code-group [Example Response] ```json [200 - Success] { "success": true, "data": { "object": "search_fields", "entity": "company", "fields": [ { "name": "companyName", "type": "string", "required": false, "description": "Filter by company name" }, { "name": "industry", "type": "string | string[]", "required": false, "description": "Industry name or array of industries" }, { "name": "employeeCountRange", "type": "range", "required": false, "description": "Filter by employee count range. Both bounds are optional and inclusive (range 0 - 1,000,000,000).", "properties": { "min": { "type": "number", "description": "Minimum value, inclusive" }, "max": { "type": "number", "description": "Maximum value, inclusive" } } } ] }, "error": null, "quotas": { "creditsConsumed": 0, "workspace": { "id": "ws_abc123", "credits": { "total": 10000, "used": 4521, "left": 5479 } } }, "metadata": { "requestId": "req_f1e2l3d4s5", "executionTimeMs": 3 } } ``` --- # JavaScript integration > Production-ready fetch patterns for Reverse Contact endpoints. ## Minimal fetch helper ```ts const API_BASE_URL = 'https://api.reversecontact.com/v2' export async function rcRequest(path: string, init: RequestInit = {}) { const apiKey = process.env.RC_API_KEY if (!apiKey) { throw new Error('Missing RC_API_KEY') } const response = await fetch(`${API_BASE_URL}${path}`, { ...init, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, ...init.headers } }) if (!response.ok) { throw new Error(`Reverse Contact request failed: ${response.status}`) } return response.json() } ``` ## Usage ```ts const response = await rcRequest('/fetch/persons', { method: 'POST', body: JSON.stringify({ url: 'https://social.com/in/example' }) }) console.log(response.data.firstName) ``` ---