# API Pricing Source: https://docs.signalhire.com/api-pricing API usage is billed through existing SignalHire credits and quotas — no separate API subscription is required. To view SignalHire's pricing plans, visit the [pricing page](https://www.signalhire.com/pricing). ## Credit Usage by Endpoint ### Person API — with contacts Each successful person lookup consumes **standard SignalHire credits**. These are the same credits used across the SignalHire website, browser extension and api — the balance is shared between all three. ### Person API — without contacts Retrieving profiles without contact details requires a **separate credit type**, independent of the standard balance. To purchase without-contacts credits, contact [support@signalhire.com](mailto:support@signalhire.com). ### Search API Search requests are not billed per credit — instead, they count against a **daily search quota**. This quota is shared between the SignalHire website and the API, so searches made in either place consume from the same daily limit. * The quota covers both the **number of search queries** and the **total number of profiles returned** — both are tracked separately against daily limits * The quota resets daily * The quota varies by plan and is set individually — trial accounts have a significantly lower limit than paid accounts * When the daily quota is exhausted, the API returns HTTP `402` ## Summary | Method | Billing | | ----------------------------- | ------------------------------------------------- | | Person API — with contacts | Standard credits (shared with site & extension) | | Person API — without contacts | Separate credit type, contact support to purchase | | Search API | Daily search quota (shared with site) | ## Tracking Usage To check remaining credits at any time, use the [Get Remaining Credits](/person-api/credits) endpoint or check the `X-Credits-Left` header included in every API response. ## Company API The Company API uses one purchasable credit type, separate from Person API credits: * **Company data credits** — consumed by the `/company/info` endpoint. Each successful call consumes one credit. Contact [support@signalhire.com](mailto:support@signalhire.com) to purchase. `/company/getProfiles` does not consume a purchasable credit. Instead, each profile delivered counts against the same **daily profile-view quota** shared with the SignalHire website. The quota varies by plan, up to **2000 profiles/day**. The `/company/findById` and `/company/getCount` endpoints do not consume any credits or quota. The Company API is available on request — contact [support@signalhire.com](mailto:support@signalhire.com) to enable access. # API Authentication Source: https://docs.signalhire.com/authentication To access the SignalHire API, you need to authenticate using an API key. The API key serves as a unique identifier and ensures that only authorized requests are processed. All requests must include the API key in the request header. ## Obtaining an API Key 1. Register for a [SignalHire account](https://www.signalhire.com/registration?package=trial) 2. Open the left sidebar and navigate to **Other tools → Integrations & API** 3. Click **Create new API Key**, then copy the generated key using the copy icon next to it Integrations and API in the left sidebar Create new API Key button and copy icon ## Using the API Key Include the `apikey` header in every request: ```bash cURL theme={null} curl -X POST https://www.signalhire.com/api/v1/candidate/search \ -H 'apikey: your_secret_api_key' \ --data '{"items": [...], "callbackUrl": "https://yourdomain.com/callback"}' ``` ```python Python theme={null} import requests response = requests.post( "https://www.signalhire.com/api/v1/candidate/search", headers={"apikey": "your_secret_api_key"}, json={ "items": [...], "callbackUrl": "https://yourdomain.com/callback" } ) ``` ```javascript Node.js theme={null} const axios = require('axios'); const response = await axios.post( 'https://www.signalhire.com/api/v1/candidate/search', { items: [...], callbackUrl: 'https://yourdomain.com/callback' }, { headers: { apikey: 'your_secret_api_key' } } ); ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://www.signalhire.com/api/v1/candidate/search")) .header("apikey", "your_secret_api_key") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString( "{\"items\": [...], \"callbackUrl\": \"https://yourdomain.com/callback\"}" )) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ```ruby Ruby theme={null} require 'net/http' require 'json' uri = URI('https://www.signalhire.com/api/v1/candidate/search') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request['apikey'] = 'your_secret_api_key' request['Content-Type'] = 'application/json' request.body = { items: [...], callbackUrl: 'https://yourdomain.com/callback' }.to_json response = http.request(request) ``` API keys must be kept secure. Exposing a key in public repositories or sharing it with unauthorized parties allows others to make API calls consuming paid credits. ## Checking Remaining Credits The `X-Credits-Left` header is included in every API response and shows the current credit balance. ```http theme={null} HTTP/2 201 Content-Type: application/json X-Credits-Left: 243 ``` Credits can also be checked explicitly — see [Get Remaining Credits](/person-api/credits). # Company Fields Search Source: https://docs.signalhire.com/company-api/company-info Retrieve full company profile including locations, industry, headcount, and more. Returns detailed information about a company. Each successful call consumes one **company data credit**. *** **Endpoint:** `GET https://www.signalhire.com/api/v1/company/info` ## Request Parameters Secret API key. See [Authentication](/authentication). Company ID (numeric) or company slug. Use [Find Company](/company-api/find-company) to resolve a company name to an ID. ## Request Example ```bash cURL theme={null} curl -X GET \ -H 'apikey: your_secret_api_key' \ 'https://www.signalhire.com/api/v1/company/info?id=1033' ``` ```python Python theme={null} import requests response = requests.get( "https://www.signalhire.com/api/v1/company/info", headers={"apikey": "your_secret_api_key"}, params={"id": "1033"} ) ``` ```javascript Node.js theme={null} const axios = require('axios'); const response = await axios.get( 'https://www.signalhire.com/api/v1/company/info', { headers: { apikey: 'your_secret_api_key' }, params: { id: '1033' } } ); ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://www.signalhire.com/api/v1/company/info?id=1033")) .header("apikey", "your_secret_api_key") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ```ruby Ruby theme={null} require 'net/http' uri = URI('https://www.signalhire.com/api/v1/company/info?id=1033') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Get.new(uri) request['apikey'] = 'your_secret_api_key' response = http.request(request) ``` ## Response Example (HTTP 200) ```http theme={null} HTTP/2 200 Content-Type: application/json X-Credits-Left: 49 ``` ```json theme={null} { "name": "Accenture", "linkedInId": "1033", "industry": "Information Technology and Services", "description": "Accenture is a global professional services company...", "founded": 1989, "logo": "https://media.licdn.com/dms/image/C560BAQHx.../company-logo_200_200/...", "url": "https://www.linkedin.com/company/accenture", "website": "http://www.accenture.com", "phone": null, "type": "Public Company", "staffCount": 546636, "staffCountRange": 10001, "staffingCompany": false, "specialities": [ "Management Consulting", "Systems Integration and Technology", "Business Process Outsourcing", "Application and Infrastructure Outsourcing" ], "confirmedLocations": [ { "country": "IE", "city": "Dublin 2", "line1": "Grand Canal Harbour", "headquarter": true, "streetAddressOptOut": false }, { "country": "US", "geographicArea": "California", "city": "San Francisco", "postalCode": "94105", "line1": "415 Mission Street", "line2": "Floor 31-34", "headquarter": false, "streetAddressOptOut": false } ] } ``` ## Response Fields Company display name. Company ID. Primary industry category. Company description. Year the company was founded. URL of the company logo. LinkedIn company page URL. Company website URL. Company phone number. Company type (e.g. `"Public Company"`, `"Private Company"`). Approximate number of employees. Employee range bucket identifier. Whether the company is a staffing agency. List of company specialities. May be empty. List of confirmed office locations. Each item may contain `country`, `city`, `geographicArea`, `postalCode`, `line1`, `line2`, `description`, `headquarter`, `streetAddressOptOut`. All fields are `string | boolean | null`. ## Response Codes | Code | Description | | ----- | -------------------------------------------------------- | | `200` | Company data returned. One company data credit consumed. | | `402` | No company data credits remaining. | | `403` | Company API access not enabled for this account. | | `404` | Company not found. | | `422` | `id` parameter missing. | # Company Profiles Source: https://docs.signalhire.com/company-api/company-profiles Check how many employee profiles are available and retrieve them via callback. The Company Profiles API consists of two endpoints: one to check the available profile count for a company, and one to retrieve those profiles via an async callback. This is particularly useful when needing to fetch all employees of a specific company by its exact ID or slug — unlike the Search API which filters by company name (a non-unique string), this endpoint works with the unique LinkedIn company ID or slug from [Find Company](/company-api/find-company). ## Get Profile Count Check how many profiles are associated with a company by employment status — no credits consumed. **Endpoint:** `GET https://www.signalhire.com/api/v1/company/getCount` Secret API key. Company ID (numeric) or company slug. Employment status filter. Possible values: `current`, `past`, `both`. ### Request Example ```bash theme={null} curl -X GET \ -H 'apikey: your_secret_api_key' \ 'https://www.signalhire.com/api/v1/company/getCount?id=1033&status=both' ``` ### Response Example (HTTP 200) ```json theme={null} { "total_profiles": 2 } ``` Total number of profiles matching the status filter. *** ## Get Profiles Fetch employee profiles for a company asynchronously via callback. **Endpoint:** `GET https://www.signalhire.com/api/v1/company/getProfiles` Secret API key. Company ID (numeric) or company slug. Employment status filter. Possible values: `current`, `past`, `both`. URL where results will be POSTed once processing is complete. Same callback format and delivery rules as [Person API callbacks](/person-api/retrieve-person#callback-delivery). ### How It Works 1. The request returns a `searchId` immediately with HTTP `200` 2. Profiles are delivered to the `callbackUrl` in batches of up to **100 profiles per callback** 3. Each profile delivered counts as one view against your **daily profile-view quota** — the same quota shared with the SignalHire website. The quota varies by plan, up to **2000 profiles/day**; if the company has more profiles than your remaining quota, the request is rejected with HTTP `429` — contact [support@signalhire.com](mailto:support@signalhire.com) if a higher limit is needed. 4. Profiles are returned **without contact details** (no emails or phone numbers) To subsequently retrieve contact details for specific profiles, pass their `uid` values to the [Person API](/person-api/retrieve-person), which consumes standard credits. ### Request Example ```bash theme={null} curl -X GET \ -H 'apikey: your_secret_api_key' \ 'https://www.signalhire.com/api/v1/company/getProfiles?id=accenture&status=both&callbackUrl=https://yourdomain.com/callback' ``` ### Response Example (HTTP 200) ```json theme={null} { "searchId": 1 } ``` Results are delivered to the `callbackUrl` as an array of profile objects without contact details. ### Response Codes | Code | Description | | ----- | -------------------------------------------------------- | | `200` | Request accepted. Results will be sent to `callbackUrl`. | | `429` | Daily profile-view quota exhausted. | | `403` | Company API access not enabled for this account. | # Company Name Search Source: https://docs.signalhire.com/company-api/find-company Look up a company by ID or slug to get its identifier and name. Use this endpoint to resolve a company name or slug to a stable numeric ID. The result can be used with other Company API endpoints. This endpoint does **not** consume any credits. **Endpoint:** `GET https://www.signalhire.com/api/v1/company/findById` ## Request Parameters Secret API key. See [Authentication](/authentication). Company ID (numeric) or company slug. Examples: `1033`, `accenture`. ## Request Example ```bash cURL theme={null} curl -X GET \ -H 'apikey: your_secret_api_key' \ 'https://www.signalhire.com/api/v1/company/findById?id=accenture' ``` ```python Python theme={null} import requests response = requests.get( "https://www.signalhire.com/api/v1/company/findById", headers={"apikey": "your_secret_api_key"}, params={"id": "accenture"} ) ``` ```javascript Node.js theme={null} const axios = require('axios'); const response = await axios.get( 'https://www.signalhire.com/api/v1/company/findById', { headers: { apikey: 'your_secret_api_key' }, params: { id: 'accenture' } } ); ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://www.signalhire.com/api/v1/company/findById?id=accenture")) .header("apikey", "your_secret_api_key") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ```ruby Ruby theme={null} require 'net/http' uri = URI('https://www.signalhire.com/api/v1/company/findById?id=accenture') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Get.new(uri) request['apikey'] = 'your_secret_api_key' response = http.request(request) ``` ## Response Example (HTTP 200) ```json theme={null} { "id": 1033, "slug": "accenture", "company": "Accenture" } ``` Numeric company ID. Use this with other Company API endpoints. URL-friendly company identifier. Can be used interchangeably with `id` in other endpoints. Company display name. ## Response Codes | Code | Description | | ----- | ------------------------------------------------ | | `200` | Company found. | | `403` | Company API access not enabled for this account. | | `404` | Company not found. | | `422` | `id` parameter missing. | # Company API Overview Source: https://docs.signalhire.com/company-api/overview Company API — look up companies, get employee counts, and retrieve full company profiles. The Company API provides access to company data from SignalHire's database. It allows looking up companies by identifier, checking how many profiles are associated with a company, retrieving full company details, and fetching employee profiles via callback. The Company API is not enabled by default. To request access, contact [support@signalhire.com](mailto:support@signalhire.com). ## Available Endpoints | Endpoint | Description | | ----------------------------- | -------------------------------------------------------------------------- | | `/api/v1/company/findById` | Look up a company by ID or slug. No credits consumed. | | `/api/v1/company/getCount` | Get the number of profiles associated with a company. No credits consumed. | | `/api/v1/company/info` | Get full company details. Consumes company data credits. | | `/api/v1/company/getProfiles` | Fetch employee profiles via callback. Consumes daily profile-view quota. | ## Credits & Quotas * **Company data credits** — consumed by `/company/info`. Each successful call consumes one credit. Contact [support@signalhire.com](mailto:support@signalhire.com) to purchase. The `X-Credits-Left` header in the response reflects the remaining balance. * **Daily profile-view quota** — `/company/getProfiles` draws from the same daily quota used when viewing candidate profiles on the SignalHire website. It varies by plan, up to **2000 profiles/day**, and is not a purchasable credit type — the response does not include a credits header. When the quota is exhausted, the endpoint returns HTTP `429`. Contact [support@signalhire.com](mailto:support@signalhire.com) if a higher limit is needed. `/company/findById` and `/company/getCount` consume neither credits nor quota. # FAQ Source: https://docs.signalhire.com/faq Frequently asked questions about the SignalHire API. ## General No. API access uses the same credits and quotas as the SignalHire website and browser extension. There is no separate API subscription. The Company API is the only exception — it requires a separate request to support to enable. In the SignalHire web app, open the left sidebar and navigate to **Other tools → Integrations & API**. Click **Create new API Key** and copy the generated key. See [Authentication](/authentication) for details. Every API response includes the `X-Credits-Left` header with the current balance. For standard credits, the value updates automatically after each request. Credits can also be checked explicitly via the [Get Remaining Credits](/person-api/credits) endpoint. *** ## Credits There are three independent credit types: * **Standard credits** — used by the Person API when fetching profiles with contacts. Shared across the website, browser extension, and API. * **Without-contacts credits** — used by the Person API with `withoutContacts: true`. Independent from standard credits. Contact [support@signalhire.com](mailto:support@signalhire.com) to purchase. * **Company data credits** — used by `/company/info`. Independent from all other credit types. Part of the Company API which requires separate access. Contact [support@signalhire.com](mailto:support@signalhire.com) to purchase. The `X-Credits-Left` response header always reflects the balance of the credit type relevant to the request made. Standard credits are shared — any usage on the website, browser extension, or API draws from the same balance. Without-contacts credits and company data credits are API-only and have no equivalent on the website. Use the [Get Remaining Credits](/person-api/credits) endpoint: * Standard credits: `GET /api/v1/credits` * Without-contacts credits: `GET /api/v1/credits?withoutContacts=true` For company data credits, check the `X-Credits-Left` header returned by `/company/info`. A credit is consumed at the moment a profile is successfully matched — not when the callback is delivered. If the callback fails to reach the endpoint, the credit is still charged. To retrieve the data, a new API request must be submitted, which will consume credits again. *** ## Person API The default async mode requires a callback because the API queries multiple external sources to maximize contact coverage — this takes time. If a simpler flow is needed without setting up a server endpoint, use [`withoutWaterfall: true`](/person-api/without-waterfall). Results will be returned synchronously in the response, though contact coverage may be slightly lower. Custom HTTP headers in the `callbackUrl` are not supported — SignalHire POSTs to the provided URL without additional headers. Two recommended patterns for production: **1. Secret token in the callback URL** ``` https://yourdomain.com/callback/{secret} https://yourdomain.com/callback?token=your_secret_token ``` Generate a unique token, embed it in the URL, and verify it on your server against the expected value. This is sufficient for most production use cases. **2. IP allowlisting** All callbacks originate from SignalHire servers — incoming requests can be restricted by IP. This is less reliable as IPs may change, so it's better used as an additional layer rather than the sole mechanism. The most common causes: the callback URL is not publicly accessible, it times out (SignalHire waits up to 10 seconds), or it doesn't respond with HTTP `200`. If delivery fails after 3 retries, the callback is discarded. SignalHire sends an email notification with the affected request IDs — check your inbox and resubmit the failed requests. See [Callback Delivery](/person-api/retrieve-person#callback-delivery) for details. Credits are consumed at the moment a profile is matched — regardless of whether the callback was successfully delivered. If the callback failed, the credit was still charged. The request IDs of undelivered callbacks appear in the email notification sent by SignalHire. To get the data, submit a new API request with those identifiers. Yes. The `items` array accepts any combination of LinkedIn URLs, email addresses, phone numbers, and 32-character SignalHire UIDs in a single request — up to 100 items per request. The profile could not be found in the database, or the identifier provided did not match any known profile. No credits are consumed for failed items. They are independent options. `withoutContacts: true` controls **what** is returned — profile data without emails and phone numbers, using a separate credit type that must be purchased via [support@signalhire.com](mailto:support@signalhire.com). `withoutWaterfall: true` controls **how** results are delivered — synchronously in the response instead of via callback, without querying external sources. There is no fixed maximum delivery time. SignalHire queries external APIs in real time for each request — most callbacks arrive within a few seconds, though some may take 30–60 seconds or more depending on external API response times. There is no server-side timeout that would cause a callback to be silently dropped. The callback is always sent once processing completes. If a callback never arrives, check the failed delivery email notification from SignalHire and resubmit the affected request IDs. Yes. Use [`withoutWaterfall: true`](/person-api/without-waterfall) — it returns results synchronously in the response body without requiring a callback URL. This is particularly useful for local development, scripts, or environments where setting up a publicly accessible endpoint is not practical. Keep in mind that contact coverage may be lower than in standard async mode, as only contacts already cached in the database are returned. Yes, but only contacts already present in the SignalHire database at the time of the request. No external API queries are made, so the contact coverage may be lower than in standard async mode. The `contacts` array may be empty for profiles where no contacts have been cached yet. *** ## Search API The 15 seconds is the window between consecutive scroll requests, not the total session duration. The server keeps the search context alive for the whole session — as long as each next request arrives within 15 seconds of the previous response, the session stays active indefinitely. The scroll is designed for continuous sequential fetching, not for periodic polling. No. Once a `scrollId` expires (15 seconds after it was issued), the scroll session is lost and cannot be resumed. The correct approach is to fetch all batches in one continuous session, save each batch to the database as it arrives, and process everything afterwards. No. Only `searchByQuery` calls count as searches. However, every profile returned by both `searchByQuery` and `scrollSearch` counts against the daily profile quota. Both limits — queries and profiles — are tracked independently. The location field accepts country names (`"Germany"`), US and Canadian states (`"California"`, `"Ontario, Canada"`), and city strings (`"London, United Kingdom"`). The value must be recognizable — if it cannot be resolved, a `422` is returned. For precise searches, use `latitude`/`longitude` or `coordinates` instead, which bypass text resolution entirely. See [Location Formats](/search-api/search-by-query#location-formats). No. All exclude filters (`excludeRevealed`, `excludeWatched`, `excludeInLists`, `excludeInProgress`, `excludeEmailed`) must be combined with at least one non-exclude filter such as `currentTitle` or `industry`. A request with only exclude filters returns `422`. Use the `uid` field from each search profile and pass it to the [Person API](/person-api/retrieve-person). The Person API will return the full profile including emails and phone numbers. Standard credits are consumed for each successfully matched profile. The API and the SignalHire web app both run on the same account, so all searches — regardless of where they originate — draw from the same daily allowance. This is by design to ensure fair usage across all channels. *** ## Company API The Company API is not enabled by default. Contact [support@signalhire.com](mailto:support@signalhire.com) to request access and purchase the required credits. No. `/company/getProfiles` does not consume a purchasable credit at all — each profile delivered counts against the same daily profile-view quota shared with the SignalHire website. The endpoint returns HTTP `429` with a message indicating the daily profile-view quota has been reached. The quota varies by plan, up to 2000 profiles/day. Contact [support@signalhire.com](mailto:support@signalhire.com) if you consistently need a higher limit. Not directly. Use `/company/findById` with a company slug (e.g. `accenture`) or a numeric LinkedIn company ID to look up a company. If you don't know the ID or slug, start with the slug — it is usually a lowercase version of the company name. # SignalHire MCP Server Source: https://docs.signalhire.com/mcp-server Connect SignalHire to AI assistants and agents via the Model Context Protocol. The SignalHire MCP server lets AI assistants and agents query our dataset of professional profiles, company information, and contact details — directly from Claude, Cursor, Windsurf, VS Code, or any MCP-compatible client. ## Getting Started ### Step 1 — generate your API key **Nothing works until your SignalHire account has an API key.** Generate it once: Go to [signalhire.com/profile](https://www.signalhire.com/profile). Direct link: [signalhire.com/apiIntegrations](https://www.signalhire.com/apiIntegrations). Generate an API key and copy it. **This step is required even if you connect through OAuth.** The OAuth paths never ask you to *type* a key, but the token they issue carries your account's key inside it — an account without one produces a token the server rejects with `401`. Generate the key first, then connect however you like. You also need: * An active SignalHire account (see the account rules under [Authentication](#authentication)) * Contact credits, if you plan to reveal emails and phone numbers ### Starter prompts Once connected, paste one of these to get a real result on the first try. None of them spends a Contact Credit: ``` Find 5 software engineers in New York and show me their profiles. ``` ``` Which seniority levels and departments can I filter by? ``` ``` How much of my daily search quota is left? ``` From there, ask the assistant to reveal contacts for a specific candidate — it will ask your permission before spending anything. *** ## Connection Details | | | | ------------- | ------------------------------------- | | **Endpoint** | `https://mcp.signalhire.services/mcp` | | **Transport** | MCP Streamable HTTP | ## Authentication The server is a stateless proxy — it stores no global key and forwards your credential per request. It detects the token type automatically. | Mode | How | When to use | | --------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------- | | **API key (Bearer)** | HTTP header `Authorization: Bearer ` in a JSON config | IDEs and services configured via JSON, programmatic clients, automation | | **OAuth 2.0 (Auth0)** | Interactive login, no key typed by hand | Claude.ai / Claude Desktop connector UI, the ChatGPT app directory | **Both modes need an API key on the account.** With OAuth you never type it — the Auth0 token carries it as a claim and the server reads it from there. But the key has to exist: if the account has none, the claim is empty and every request comes back `401`. [Generate it first](#step-1--generate-your-api-key). **You must sign in with an existing SignalHire account.** Whichever way you authenticate, the connector attaches to an account that already exists — the OAuth screen is a login, not a sign-up, and there is no way to create an account from inside the AI client. Register at [signalhire.com](https://www.signalhire.com) first if you don't have one. The account you sign in with is the account that gets charged, and its credits and daily quota are the ones the tools report. If you keep several SignalHire accounts, make sure the right one is the active session in your browser **before** you start the OAuth flow — the connector binds to whoever is logged in, and switching afterwards means disconnecting and reconnecting. On the API-key paths the same rule applies through the key itself: a key belongs to one account, so grabbing it from the wrong login silently bills the wrong balance. **Signing in with a social login? Check which one.** If your SignalHire account was created through a social provider rather than an email and password, you have to sign in through that **same** provider *and* with the **same** identity behind it. Browsers routinely keep several accounts of one provider signed in at once and will hand the flow whichever is default, without asking. Read the account shown on the provider's screen before you confirm, and use its account picker — or a private window — if more than one is in play. Picking the wrong one does not produce an error: it authorises a different SignalHire identity, and you land on an account with someone else's credits, or on an empty one. **OAuth details.** OAuth is handled by SignalHire's Auth0 authorization server (`https://signalhire.eu.auth0.com/`). When you add the connector, the client (Claude or ChatGPT) discovers the authorization server via RFC 9728 metadata, you log in and consent on the SignalHire/Auth0 screen, and the client receives a token — no API key is entered by hand. Expired tokens return `401` and the client refreshes them automatically. *** ## Connecting via JSON (API key) There are two JSON connection types depending on whether your client can talk to a remote server directly. The client speaks MCP Streamable HTTP straight to the server. **No Node.js, no bridge.** This is the preferred type — use it whenever your client supports it. Get the key from your [API integrations page](https://www.signalhire.com/apiIntegrations). ```json theme={null} { "mcpServers": { "SignalHire": { "type": "http", "url": "https://mcp.signalhire.services/mcp", "headers": { "Authorization": "Bearer YOUR_SIGNALHIRE_API_KEY" } } } } ``` **Per-client differences** (everything else stays the same): | Client | Config file | Difference | | -------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------- | | **Claude Code** | `.mcp.json` (project root) | None — use as-is | | **Cursor** | `~/.cursor/mcp.json` or `.cursor/mcp.json` (Windows: `%USERPROFILE%\.cursor\mcp.json`) | Omit the `"type"` field | | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | Rename `"url"` → `"serverUrl"` | | **VS Code** (Copilot agent mode) | `.vscode/mcp.json` | Rename `"mcpServers"` → `"servers"` | **VS Code without editing JSON:** Command Palette → **MCP: Add Server** → **HTTP** → URL `https://mcp.signalhire.services/mcp` → name it `SignalHire` → add the header `Authorization` with the value `Bearer YOUR_SIGNALHIRE_API_KEY`. Some clients can only launch a local stdio process and cannot open a remote connection themselves — most notably **Claude Desktop**. For those, `npx mcp-remote` runs locally and bridges stdio to the remote HTTP endpoint. **Requires Node.js installed.** ```json theme={null} { "mcpServers": { "SignalHireMCP": { "command": "npx", "args": [ "-y", "mcp-remote", "https://mcp.signalhire.services/mcp", "--header", "Authorization: Bearer YOUR_SIGNALHIRE_API_KEY" ] } } } ``` **Claude Desktop config path:** * **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` * **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` ### Which type do I use? | Your client | Type | | ------------------------------------------------------ | --------------------------------------------------------------------- | | Claude Code, Cursor, Windsurf, VS Code | **Type 1 — Direct HTTP** | | Claude Desktop (JSON config), or any stdio-only client | **Type 2 — mcp-remote bridge** | | Claude.ai / Claude Desktop connector UI | **Neither — use OAuth** (no JSON needed) | | ChatGPT | **Neither** — install from the app directory; see the ChatGPT section | | Codex | **Neither** — TOML config; see the Codex section | | n8n | **Neither** — MCP Client Tool node; see the n8n section | *** ## Claude.ai / Claude Desktop — Custom Connector (OAuth) Go to **Settings → Connectors → Add custom connector**. Set the URL to `https://mcp.signalhire.services/mcp` Leave the advanced OAuth fields empty and click **Connect** — you'll be redirected to the SignalHire login (Auth0). After consent no API key is needed. ## ChatGPT — app directory Connect via SignalHire's official listing in the ChatGPT app directory. No URL, no client ID and no API key. Go to [`chatgpt.com/apps?q=signalhire`](https://chatgpt.com/apps?q=signalhire). Click **Connect**. Complete OAuth authentication — you'll be redirected to the SignalHire login (Auth0) and back. Open any chat, click the tools menu, and confirm the SignalHire tools are listed. Prefer not to use OAuth? Any ChatGPT client that reads a JSON MCP config can use the API-key form shown above (`Authorization: Bearer YOUR_SIGNALHIRE_API_KEY`). ## Codex — TOML config (API key) Codex speaks Streamable HTTP to remote MCP servers and reads its config from `~/.codex/config.toml` (or a project-scoped `.codex/config.toml`). Get your key from the [API integrations page](https://www.signalhire.com/apiIntegrations). ```toml theme={null} [mcp_servers.signalhire] url = "https://mcp.signalhire.services/mcp" bearer_token_env_var = "SIGNALHIRE_API_KEY" ``` Then export the key before starting Codex: ```bash theme={null} export SIGNALHIRE_API_KEY=YOUR_SIGNALHIRE_API_KEY ``` `bearer_token_env_var` makes Codex send `Authorization: Bearer ` — exactly what this server expects, and it keeps the key out of the config file. To register the server from the CLI instead of editing TOML: ```bash theme={null} codex mcp add signalhire --url https://mcp.signalhire.services/mcp ``` Use the API-key form above rather than `codex mcp login` — SignalHire's authorization server issues OAuth clients for the Claude and ChatGPT directory listings, not for arbitrary clients registering themselves. ## n8n — MCP Client Tool node Requires n8n **1.88.0 or newer**. Get your key from the [API integrations page](https://www.signalhire.com/apiIntegrations). Add an **MCP Client Tool** node to your workflow. Set **Server Transport / Connection Type** to `HTTP Streamable`. Set **Endpoint / URL** to `https://mcp.signalhire.services/mcp` Choose `Bearer Auth` and paste your SignalHire API key as the token. If your n8n version only offers `Header Auth`, use header name `Authorization` and value `Bearer YOUR_SIGNALHIRE_API_KEY` — both produce the same request. Select which tools to expose, then connect the node to your **AI Agent** node. Store the key in n8n's built-in **Credentials** system rather than typing it into the node field, so it isn't written into the exported workflow. ## Any other MCP client Not on the list above? Anything that speaks MCP over Streamable HTTP will work — there is nothing client-specific on our side. Point it at: | | | | --------------- | --------------------------------------------------------------------- | | **URL** | `https://mcp.signalhire.services/mcp` | | **Transport** | MCP Streamable HTTP (not SSE — the legacy `/sse` endpoint is retired) | | **Auth header** | `Authorization: Bearer YOUR_SIGNALHIRE_API_KEY` | Get the key from your [API integrations page](https://www.signalhire.com/apiIntegrations). If your client can only launch local stdio processes, use the `mcp-remote` bridge from Type 2 above. ## Claude Code — CLI ```bash theme={null} claude mcp add --transport http SignalHire https://mcp.signalhire.services/mcp \ --header "Authorization: Bearer YOUR_SIGNALHIRE_API_KEY" ``` ## Python SDK (programmatic) ```python theme={null} from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client url = "https://mcp.signalhire.services/mcp" headers = {"Authorization": "Bearer YOUR_SIGNALHIRE_API_KEY"} async with streamablehttp_client(url, headers=headers) as (read_stream, write_stream, _): async with ClientSession(read_stream, write_stream) as session: await session.initialize() tools = await session.list_tools() print(tools) ``` Claude connects to the server itself, so you don't run an MCP client. Both the `mcp_servers` entry and the matching `mcp_toolset` in `tools` are required — sending only the first is rejected as a validation error. ```python theme={null} import anthropic client = anthropic.Anthropic() response = client.beta.messages.create( model="claude-opus-5", max_tokens=1024, betas=["mcp-client-2025-11-20"], mcp_servers=[{ "type": "url", "url": "https://mcp.signalhire.services/mcp", "name": "signalhire", "authorization_token": "YOUR_SIGNALHIRE_API_KEY", }], tools=[{"type": "mcp_toolset", "mcp_server_name": "signalhire"}], messages=[{"role": "user", "content": "Find senior engineers in San Francisco"}], ) ``` *** ## Available Tools Once connected, your AI assistant can call the following tools and decides which to use based on your prompt. **Response format:** the five data tools — `search_candidates_query`, `scroll_search`, `retrieve_person_no_contacts`, `retrieve_person_profile`, `get_company_info` — accept a `response_format` parameter (default `"markdown"`); set it to `"csv"` or `"json"` as needed. `find_company`, the two balance tools and the three dictionary tools do not take it. **Credits:** the remaining balance is automatically appended to the tool's response — no separate balance check is needed after each call. ### Search & Discovery Search the database using advanced filters like title, location, and keywords. Supports Boolean logic (e.g. `(Software AND Engineer)`). Parameters: `current_title`, `current_past_title`, `location`, `current_company`, `current_past_company`, `full_name`, `keywords`, `industry`, `industries`, `department` (Enum), `level` (Enum), `years_experience_from`, `years_experience_to`, `years_current_past_experience_from`, `years_current_past_experience_to`, `open_to_work`, `exclude_revealed`, `exclude_watched`, `exclude_in_lists`, `exclude_in_progress`, `exclude_emailed`, `size` (default: 10), `response_format`. **Credit cost:** Uses daily search quota. No contact credits. Fetch the next batch of search results for pagination. Must be called within **15 seconds** of the initial search. Parameters: `request_id`, `scroll_id`, `response_format`. **Credit cost:** Uses daily search quota. ### Profile Retrieval Retrieve a candidate's full professional profile (experience, skills, education) **without** unlocking contact details. Parameters: `items` (array of UIDs), `response_format`. **Credit cost:** Uses daily search quota. No contact credits. Retrieve a full profile **including contact information** (emails, phone numbers, social links). Parameters: `items` (array of UIDs, LinkedIn URLs, or emails), `spend_credits_confirmed` (boolean), `response_format`. **Credit cost:** ⚠️ 1 Contact Credit per successful match. ### Company Look up a company by name, domain, or slug to retrieve its stable numeric ID. Parameters: `company_id_or_slug`. This tool takes no `response_format`. **Credit cost:** Free. Retrieve comprehensive company data (locations, industries, exact headcount). Parameters: `company_id` (numeric ID), `spend_credits_confirmed` (boolean), `response_format`. **Credit cost:** ⚠️ 1 Company Credit. ### Utility Check the remaining standard Contact credits for the current API key. Does not include Company credits. **Credit cost:** Free. Check the remaining daily quota for profiles retrieved without revealing contact information. Applies to `retrieve_person_no_contacts` only. **Credit cost:** Free. Retrieve the complete list of candidate seniority levels supported by the search API. **Credit cost:** Free. Retrieve the complete list of candidate professional departments supported by the search API. **Credit cost:** Free. Retrieve the full list of all 146 supported industry categories for the search API. **Credit cost:** Free. *** ## Searching effectively ### Boolean syntax The text fields — `keywords`, `current_title`, `current_past_title`, `current_company`, `current_past_company` — accept boolean expressions: ``` (Python AND AWS) OR (GCP AND Java) (Mandarin OR Chinese) AND (Python OR PHP) ``` Just phrase it naturally in the chat; the assistant passes the expression through. Full syntax reference: [Boolean query](/search-api/boolean-query). ### `industry` vs `industries` Two different filters, easy to confuse: * **`industry`** — free-text match on a single industry (e.g. `"Software"`). Use it when you're describing the sector in words. * **`industries`** — a list of *exact* industry IDs. Use it when you already know the IDs, from `get_available_industries` (146 categories). ### Why the assistant asks you to pick from a list When you search by **department**, **level** or **industry**, the assistant is instructed to first fetch the valid options, show them to you, and let you choose — rather than guessing a value that doesn't exist. That extra step is deliberate and costs nothing. To skip it, name a value from these lists directly. `Marketing` · `Sales` · `Product & Project` · `Engineering / Development` · `Data & Analytics` · `Design / UX` · `HR & Recruitment` · `Finance & Accounting` · `Legal / Compliance` · `Customer Support / Success` · `Health, Beauty & Hospitality` · `Education & Training` · `Operations & Admin` · `Facility Services` · `Entertainment / Production` `Intern / Entry Level` · `Junior` · `Mid-Level` · `Senior` · `Lead` · `Head` · `VP` · `C-Level` · `Founder / Owner` · `Board / Advisory` · `Directors` Industries are too many to list — ask the assistant for them, or call `get_available_industries`. ### Getting more results: raise `size`, don't paginate `search_candidates_query` takes `size` up to **100** (default 10). Pagination via `scroll_search` exists, but the `scrollId` expires after **15 seconds** — too short for a conversational back-and-forth. If you need more results, ask for a bigger batch up front ("find me 50 …") instead of asking for the next page. *** ## Credits & confirmation Three separate pools are consumed, and each is reported on its own: | Pool | Spent by | Check with | | ---------------------- | ----------------------------- | ------------------------------------------------------ | | **Daily search quota** | `retrieve_person_no_contacts` | `check_without_contacts_credits` | | **Contact Credits** | `retrieve_person_profile` | `check_balance` | | **Company Credits** | `get_company_info` | Not exposed by the API — check your SignalHire account | `search_candidates_query` and `scroll_search` do not consume the daily search quota — they use a separate internal quota that is not visible via any utility tool. Their responses still report a remaining balance for it. The two paid tools (`retrieve_person_profile` and `get_company_info`) refuse to run until the assistant passes `spend_credits_confirmed=true`. **The first call therefore always comes back asking for your permission — that's the intended behaviour, not a failure.** Confirm in the chat and the assistant retries; the refusal itself charges nothing. After a successful charge the remaining balance for that pool is appended to the response, so there's no need to run a balance check afterwards. To top up, visit [signalhire.com/profile/billing](https://www.signalhire.com/profile/billing). **Company Credits are the exception:** there is no tool that reads that balance — `check_balance` covers Contact Credits only. Check it in your SignalHire account before a run that needs company data. *** ## Example Prompts ``` Find me 5 Senior Python Developers in London. Show me their skills. Do not fetch their contacts yet. ``` Uses `search_candidates_query` → `retrieve_person_no_contacts` ``` Reveal the contact information (emails and phone) for that candidate. ``` Uses `retrieve_person_profile` ``` What is the exact headcount and headquarters location of Google? Search for the company ID first. ``` Uses `find_company` → `get_company_info` ``` Search for engineers with (Python AND AWS) OR (GCP AND Java) experience. ``` Boolean logic in `keywords` ``` Find product managers in London who are open to new opportunities. ``` Uses the `open_to_work` filter ``` Find sales reps with 3 to 5 years of experience in the SaaS industry. ``` Uses `years_experience_from` / `years_experience_to` ``` Search for marketing directors in Germany, exclude anyone I've already emailed or revealed. ``` Uses `exclude_emailed` / `exclude_revealed` ``` How many contact credits do I have left? I want to reveal 50 profiles. ``` Uses `check_balance` *** ## Troubleshooting Connecting through a chat client's UI (Claude.ai, Claude Desktop, ChatGPT) requires a paid plan on that client; MCP connectors are not offered on their free tiers. Check your subscription before debugging anything else. This applies to the connector UIs only — the API-key setups on this page (Claude Code, Cursor, Windsurf, VS Code, Codex, n8n, any other MCP client) don't depend on an AI-client subscription. | Client | What to check | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------ | | **Claude.ai** | Settings → Connectors → confirm SignalHire is connected and enabled, then refresh the page | | **Claude Desktop** | Restart the app after adding the connector | | **ChatGPT** | Open the tools menu in a chat and confirm the SignalHire tools are listed; reconnect from Settings → Connectors if not | | **Cursor** | Verify `mcp.json` is valid JSON and in the right path, then restart Cursor fully | | **VS Code** | Confirm you're in Agent mode in Copilot Chat, and check `servers` in `.vscode/mcp.json` | | **Windsurf** | Check the config is saved and the server URL is correct | | **Codex** | Check `~/.codex/config.toml` is valid TOML and that `SIGNALHIRE_API_KEY` is exported in the shell you launched Codex from | | **n8n** | Confirm the MCP Client Tool node is connected to an AI Agent node and the workflow is active; re-check the URL and auth header | This server reads the API key **only** from `Authorization: Bearer `. A bare key with no `Bearer ` prefix, a different header name (`apikey`, `x-api-key`), or the key in the URL as `?token=...` are all ignored and come back as `401`. The `scrollId` expired (15-second TTL). Run a new `search_candidates_query` with a larger `size` (up to 100) instead of paginating. It caches per-server state. Clear it and restart the client: ```bash theme={null} rm -rf ~/.mcp-auth ``` Make sure Node.js is installed; you can also install the bridge globally with `npm i -g mcp-remote`. By design — see [Credits & confirmation](#credits--confirmation). Say yes and it will retry the same call. The relevant pool is empty. Top up the account, or wait for the daily quota to reset; retrying the call will not help. The account probably has no API key. OAuth carries the key inside the token, so an account that never generated one issues a token with an empty key claim and the server rejects it. Generate the key at [signalhire.com/apiIntegrations](https://www.signalhire.com/apiIntegrations), then disconnect and reconnect the connector so it picks up a fresh token. Missing or invalid `Authorization: Bearer` header, or an expired OAuth token (the client should refresh automatically). Check the header is present and that the key is still active in your [API integrations page](https://www.signalhire.com/apiIntegrations) — generate a new one if it was revoked. A key containing non-ASCII characters (for example a placeholder pasted from a translated guide) is rejected before any request is sent. Disconnect the connector and add it again, and make sure you're already signed in to SignalHire in the same browser before starting the flow. The flow signs you in to an **existing** account; it cannot create one. You're most likely connected under a different SignalHire account than you think. Run `check_balance` and compare it with the balance shown in the SignalHire web app; if they disagree, disconnect the connector, sign in to the correct account in your browser, and connect again. On the API-key paths, re-copy the key from the right account. With a social login this is the usual cause: the provider silently used a different one of your accounts. Redo the flow, and on the provider's screen pick the identity your SignalHire account is actually linked to instead of accepting the default. Too many requests in a short window. Wait a moment and retry; the limit is applied upstream by the SignalHire API, not by this server. You're connecting by IP instead of the domain. Always use `https://mcp.signalhire.services/mcp` — the server filters traffic by the `Host` header. If your client's CA bundle is out of date, update it (`pip install -U certifi` for Python). Do **not** disable TLS verification. *** ## Security * The MCP server is a **stateless proxy** — your API key is never stored server-side. Each request forwards your credential to the SignalHire API and discards it. * Prefer OAuth (Claude.ai, Claude Desktop, ChatGPT) where it's available — there's no key to manage or leak. * Store API keys in a secure credential store — your OS keychain, a password manager, or your editor's secrets manager. Don't hardcode them into config files you share. * For team setups, pass the key through an environment variable (as in the Codex config above) instead of committing it to version control. In n8n, use the built-in **Credentials** system rather than pasting the key into a node field. * Rotate your API key at [signalhire.com/apiIntegrations](https://www.signalhire.com/apiIntegrations) periodically, and immediately if you suspect it has been exposed. * Remove connectors and MCP entries you no longer use, and expose only the tools your use case needs. # API to Find Contacts Source: https://docs.signalhire.com/overview SignalHire API — access contact information, emails, phone numbers, and social profiles from a global database of professionals. The SignalHire API allows developers to integrate the capabilities of SignalHire directly into their own applications. It provides access to a wealth of contact information — including emails, phone numbers, and social media profiles — from a global database of professionals. The API enables businesses to find and verify contact details, improve customer outreach, and enrich data for CRM systems. SignalHire API is designed to be simple, scalable, and secure. It offers endpoints to retrieve detailed information about individuals based on known identifiers, as well as powerful search capabilities to discover new prospects. The flexibility of the API allows for easy integration into existing applications and workflows, giving access to accurate data when needed. ## Use Cases Automatically enrich CRM contacts with verified emails, phone numbers, and social profiles using the Person API. Discover new prospects by searching the SignalHire database with filters like job title, location, company, and industry. Find and contact candidates matching specific skills, experience level, and location criteria. Explore professional landscape data across industries, companies, and geographies. ## Available APIs Retrieve full, detailed information about a specific individual using a LinkedIn URL, email, phone, or profile ID. May return contact details depending on the request mode. Search for individuals using filters like job title, location, company, or industry. Returns a list of matching profiles for lead generation and talent sourcing. Look up companies, retrieve full company profiles, and fetch employee lists. Available on request — contact support to enable. ## Key Differences Between APIs | | Person API | Search API | Company API | | -------- | --------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------- | | Input | LinkedIn URL, email, phone, or UID | Search filters (title, location, etc.) | Company ID or slug | | Output | One full profile, may include contacts | Multiple brief profiles, no contacts | Company data or employee profiles | | Use case | Enrich a known person's data | Discover new candidates or leads | Company research and employee sourcing | | Delivery | Async (callback) or sync (`withoutWaterfall`) | Sync — first batch in response, pagination via `scrollId` | Sync for lookup and info, async callback for profiles | | Access | Standard | Standard | On request via support | ## Getting Started Register for a SignalHire account and navigate to **Integrations & API** to generate a key. See [Authentication](/authentication). Start with the [Person API](/person-api/retrieve-person) to retrieve data for a known LinkedIn profile or email address. Use the [Search API](/search-api/search-by-query) to find new prospects using filters like job title, location, or company. # Candidate Object: Data Structure & Fields Source: https://docs.signalhire.com/person-api/candidate-object Full structure of the candidate object returned by the Person API. The `candidate` object contains the full profile data returned by the Person API for a successfully matched individual. It appears in two contexts: * In the **callback payload** (async mode) — inside each array item where `status` is `success` * In the **response body** (sync mode, `withoutWaterfall: true`) — same structure, returned directly without a callback The object includes personal details, professional experience, education, contact information, social profiles, and additional data such as certifications, publications, and patents. Not all fields are guaranteed to be populated — availability depends on the data present in SignalHire's database for a given profile. Fields with no data are returned as `null` or an empty array `[]`. Below is a full description of all fields. ## Full Example ```json theme={null} { "uid": "abc123def456gh789ijk012lmn345op6", "fullName": "John Doe", "gender": null, "headLine": "Founder / Owner Doe Law Offices", "summary": "Experienced lawyer with expertise in family law and criminal defense.", "photo": { "url": "https://media.cdn.com/image/C4A03AQH-wVPhNTP7cw/0/1577297087305" }, "locations": [ { "name": "New York, New York, United States" } ], "skills": ["Civil Litigation", "Corporate Law", "Litigation"], "contacts": [ { "type": "email", "value": "john.doe@doelaw.com", "rating": 100, "subType": "work" }, { "type": "email", "value": "john.doe@gmail.com", "rating": 100, "subType": "personal" }, { "type": "phone", "value": "+1 555-123-4567", "rating": 100, "subType": "work_phone", "info": "..." } ], "social": [ { "type": "li", "link": "https://www.linkedin.com/in/john-doe-12345678", "rating": 100 }, { "type": "fb", "link": "https://www.facebook.com/johndoe", "rating": 100 } ], "experience": [ { "position": "Owner / Managing Attorney", "company": "Doe Law Offices LLC", "location": null, "current": true, "started": "2015-01-01T00:00:00+00:00", "ended": null, "summary": "Helping clients navigate family law and criminal defense cases.", "companyUrl": "https://www.linkedin.com/company/doe-law-offices", "companySize": "1-10", "staffCount": 5, "industry": "Law Practice", "website": "http://www.doe-law.com" } ], "education": [ { "faculty": "Law", "university": "New York University School of Law", "url": "https://www.linkedin.com/school/nyu-law/", "startedYear": 2005, "endedYear": 2008, "degree": ["JD"] } ], "language": [ { "name": "English", "proficiency": "Native or bilingual" }, { "name": "Portuguese", "proficiency": "Professional working" } ], "organization": [ { "name": "New York Bar Association", "position": null, "startDate": "January 2015", "endDate": null } ], "certification": [ { "name": "Responsible AI", "license": "10748511", "authority": "Google" } ], "course": [ { "name": "Strategic Management [FGV]" } ], "project": [ { "name": "Project Name", "description": null, "url": "url", "startDate": "July 2017", "endDate": null } ], "publication": [ { "name": "TRIAL Magazine", "description": "...", "issue": "American Association for Justice", "url": null, "date": "July 2006" } ], "patent": [ { "name": "Secure geolocation-based data access control", "issue": "US12147159", "patentNumber": null, "date": "November 2024" } ], "honorAward": [ { "name": "Award name", "description": "...", "issue": "Great British Entrepreneur Awards", "date": "June 2024" } ] } ``` ## Top-Level Fields Unique 32-character SignalHire profile identifier. Full name of the person. Gender of the person. Possible values: `male`, `female`, or `null` if not available. Professional headline. Profile bio or summary text. Profile photo. Contains a single `url` string field, or `null` if no photo is available. List of location objects. Each object contains a single `name` string field (e.g. `"New York, New York, United States"`). List of professional skills. Total years of experience across all roles. ## contacts Array of contact items. When `withoutContacts: true` is used, this field is always returned as an empty array `[]`. Each item has the following fields: Contact type. Possible values: `email`, `phone`, `link`, `skype`, `telegram`, `whatsapp`, `viber`, `hangouts`, `wechat`, `qq`, `icq`, `gtalk`, `aim`, `windows_live_messenger`, `yahoo_messenger`. The contact value (email address, phone number, username, or URL depending on type). Confidence score indicating contact validity. Possible values: `70` (likely valid) or `100` (high confidence). For emails: `work`, `personal`, or `null`. For phones: `work_phone`, `mobile`, or `null`. For other types: `null`. ## social Array of social profile links. When `withoutContacts: true` is used, only the LinkedIn profile link is returned (if available). Each item has the following fields: Platform identifier. Possible values: | Value | Platform | | ----- | -------------- | | `li` | LinkedIn | | `fb` | Facebook | | `tw` | Twitter | | `x` | X | | `ig` | Instagram | | `gh` | GitHub | | `yt` | YouTube | | `pt` | Pinterest | | `so` | Stack Overflow | | `be` | Behance | | `dr` | Dribbble | | `sc` | SoundCloud | | `vm` | Vimeo | | `rd` | Reddit | | `qu` | Quora | | `xi` | XING | | `vi` | Viadeo | | `vk` | ВКонтакте | | `ok` | Odnoklassniki | | `am` | About.me | | `gr` | Gravatar | | `wp` | WordPress | | `wc` | WordPress.com | | `wo` | WordPress.org | | `tm` | Tumblr | | `fs` | Foursquare | | `ss` | SlideShare | | `mu` | Meetup | | `fc` | Flickr | | `bl` | Blogger | | `lj` | Live Journal | | `ms` | MySpace | | `lf` | Last.fm | | `gp` | Google+ | | `kl` | Klout | | `ba` | Badoo | | `bb` | Bebo | | `dg` | Digg | | `tg` | Tagged | | `dl` | Delicious | | `su` | StumbleUpon | | `ni` | Ning | | `re` | Renren | | `ye` | Yelp | | `fl` | Freelancer | | `ta` | TripAdvisor | | `di` | Disqus | | `pl` | Plaxo | | `eb` | eBay | | `mh` | MyHeritage | | `pb` | Photobucket | | `ps` | PeopleSmart | Full URL to the social profile. Confidence score indicating link validity. Possible values: `70` (likely valid) or `100` (high confidence). ## experience Array of work experience entries. Each item contains: Job title. Company name. Location of the role, or `null` if not specified. Whether this is the person's current role. Start date in ISO 8601 format (e.g. `"2015-01-01T00:00:00+00:00"`), or `null` if unknown. End date in ISO 8601 format, or `null` if the role is current or end date is unknown. Description of responsibilities or achievements in the role. LinkedIn URL of the company. Returns `"n/a"` if not available. Employee count range (e.g. `"1-10"`, `"50-100"`). Returns `"n/a"` if not available. Approximate number of employees. Returns `"n/a"` if not available. Industry category of the company. Returns `"n/a"` if not available. Company website URL. Returns `"n/a"` if not available. ## education Array of education entries. Each item contains: Name of the institution. Department or field of study. List of degrees obtained (e.g. `["JD"]`, `["Bachelor of Arts"]`). LinkedIn URL of the institution. Year of enrollment. Year of graduation. ## Other Fields The following fields follow the same pattern — each is an array of objects that may be empty (`[]`). All fields within each object are `string | null`. Languages spoken. Each item contains `name` and `proficiency`. Professional organizations. Each item contains `name`, `position`, `startDate`, `endDate`. Certifications. Each item contains `name`, `license`, `authority`. Courses completed. Each item contains `name`. Projects. Each item contains `name`, `description`, `url`, `startDate`, `endDate`. Publications. Each item contains `name`, `description`, `issue`, `url`, `date`. Patents. Each item contains `name`, `issue`, `patentNumber`, `date`. Awards and honors. Each item contains `name`, `description`, `issue`, `date`. ## Additional Fields For a small subset of profiles, additional data may be present. These fields should not be relied upon as they are available only occasionally. They are **omitted entirely** when not available — never returned as `null` or `[]`. Physical addresses associated with the person. Date of birth. Alternative names or name variations. # Check Remaining Credits Source: https://docs.signalhire.com/person-api/credits Check how many credits remain in your account. Credits can be checked at any time using a dedicated endpoint, or by inspecting the `X-Credits-Left` header included in every API response. **Endpoint:** `GET https://www.signalhire.com/api/v1/credits` ## Standard Credits Returns the remaining balance of standard credits used by the Person API (with contacts). ```bash cURL theme={null} curl -X GET \ -H 'apikey: your_secret_api_key' \ https://www.signalhire.com/api/v1/credits ``` ```python Python theme={null} import requests response = requests.get( "https://www.signalhire.com/api/v1/credits", headers={"apikey": "your_secret_api_key"} ) ``` ```javascript Node.js theme={null} const axios = require('axios'); const response = await axios.get( 'https://www.signalhire.com/api/v1/credits', { headers: { apikey: 'your_secret_api_key' } } ); ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://www.signalhire.com/api/v1/credits")) .header("apikey", "your_secret_api_key") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ```ruby Ruby theme={null} require 'net/http' uri = URI('https://www.signalhire.com/api/v1/credits') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Get.new(uri) request['apikey'] = 'your_secret_api_key' response = http.request(request) ``` **Response (HTTP 200):** ```http theme={null} HTTP/2 200 Content-Type: application/json X-Credits-Left: 27 { "credits": 27 } ``` ## Without Contacts Credits To check the remaining balance of without-contacts credits, add the `withoutContacts=true` query parameter. See [Profiles Without Contacts](/person-api/without-contacts) for details on this credit type. ```bash cURL theme={null} curl -X GET \ -H 'apikey: your_secret_api_key' \ 'https://www.signalhire.com/api/v1/credits?withoutContacts=true' ``` ```python Python theme={null} import requests response = requests.get( "https://www.signalhire.com/api/v1/credits", headers={"apikey": "your_secret_api_key"}, params={"withoutContacts": "true"} ) ``` ```javascript Node.js theme={null} const axios = require('axios'); const response = await axios.get( 'https://www.signalhire.com/api/v1/credits', { headers: { apikey: 'your_secret_api_key' }, params: { withoutContacts: true } } ); ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://www.signalhire.com/api/v1/credits?withoutContacts=true")) .header("apikey", "your_secret_api_key") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ```ruby Ruby theme={null} require 'net/http' uri = URI('https://www.signalhire.com/api/v1/credits?withoutContacts=true') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Get.new(uri) request['apikey'] = 'your_secret_api_key' response = http.request(request) ``` ## X-Credits-Left Header Every API response includes the `X-Credits-Left` header with the current credit balance. The value automatically reflects the credit type matching the request — without-contacts credits when `withoutContacts: true` is set, standard credits otherwise. # Rate Limits & Request Quotas Source: https://docs.signalhire.com/person-api/rate-limits Usage limits for the Person API. The Person API enforces two types of limits: a per-request item cap and a per-minute throughput limit. Both are checked on every request before processing begins. ## Limits | Limit | Value | | --------------------- | ---------------------- | | Max items per request | 100 | | Max items per minute | 600 (paid) / 5 (trial) | | Concurrent requests | No strict limit | The per-minute limit depends on the account type: **600 items/min** for paid accounts, **5 items/min** for trial accounts. ## Exceeding the Limits **More than 100 items in a single request** — returns HTTP `406`: ```json theme={null} { "error": "Maximum 100 items are allowed in one request" } ``` **Per-minute limit exceeded** — returns HTTP `429`: ```json theme={null} { "error": "Limit of 600 items per minute is exceeded" } ``` Retry logic with **exponential backoff** should be implemented to handle `429` responses gracefully. ## Examples * 6 requests × 100 items = 600 items/min ✓ * 600 requests × 1 item = 600 items/min ✓ * 7 requests × 100 items = 700 items/min ✗ → `429` # People Profiles & Contact Information Source: https://docs.signalhire.com/person-api/retrieve-person Retrieve detailed profile and contact information for a specific individual by LinkedIn URL, email, phone, or profile UID. ## Overview SignalHire's database contains detailed professional profiles for hundreds of millions of people worldwide. The Person API allows looking up individuals using known identifiers — a LinkedIn profile URL, email address, phone number, or SignalHire profile UID — and retrieving their full profile data including contact information. Up to **100 identifiers** can be submitted in a single request, and identifier types can be mixed freely within the same batch. This endpoint consumes **standard SignalHire credits** for each successfully matched profile. Credits are shared across the website, browser extension, and API. See [API Pricing](https://www.signalhire.com/pricing) for details. The API works in two modes: * **Async mode** (default) — submit a request with a `callbackUrl`, receive a `requestId` immediately, and get results POSTed to your URL once ready. Suitable when maximum contact coverage is needed. * **Sync mode** (Without Waterfall) — submit a request without a `callbackUrl` and receive results immediately in the response. Faster, but may return fewer or slightly older contacts. See [Without Waterfall mode](/person-api/without-waterfall). ## Before You Start Make sure you have: * A valid API key — see [Authentication](/authentication) * A publicly accessible callback URL ready to receive POST requests (for async mode) * Sufficient credits — see [Get Remaining Credits](/person-api/credits) *** **Endpoint:** `POST https://www.signalhire.com/api/v1/candidate/search` ## Request Parameters **In Request Headers** Your secret API key. See [Authentication](/authentication). **In Request Body** Array of identifiers to look up. Each item can be a LinkedIn profile URL, email address, phone number, or 32-character SignalHire profile UID. Maximum 100 items per request. Types can be mixed freely within a single request. URL on your server where results will be POSTed once processing is complete. Required unless using `withoutWaterfall`. See [Callback Delivery](#callback-delivery). If `true`, returns results synchronously in the response body instead of via callback. Cannot be combined with `callbackUrl`. See [Without Waterfall mode](/person-api/without-waterfall). If `true`, returns profile data without contact details (emails, phones). Requires a separate credit type. See [Without Contacts](/person-api/without-contacts). ## How It Works (Async Mode) 1. Submit a POST request with an `items` array and a `callbackUrl` 2. SignalHire immediately returns a `requestId` with HTTP `201` 3. The server queries internal and external sources for each item in parallel 4. Once complete, results are POSTed to the `callbackUrl` For a simpler synchronous flow without setting up a callback server, use [Without Waterfall mode](/person-api/without-waterfall). ## Request Example ```bash cURL theme={null} curl -X POST https://www.signalhire.com/api/v1/candidate/search \ -H 'apikey: your_secret_api_key' \ --data '{ "items": [ "https://www.linkedin.com/in/profile1", "email@example.com", "+44 0 123 456 789", "10000000000000000000000000000001" ], "callbackUrl": "https://www.yourdomain.com/yourCallbackUrl" }' ``` ```python Python theme={null} import requests response = requests.post( "https://www.signalhire.com/api/v1/candidate/search", headers={"apikey": "your_secret_api_key"}, json={ "items": [ "https://www.linkedin.com/in/profile1", "email@example.com", "+44 0 123 456 789", "10000000000000000000000000000001" ], "callbackUrl": "https://www.yourdomain.com/yourCallbackUrl" } ) ``` ```javascript Node.js theme={null} const axios = require('axios'); const response = await axios.post( 'https://www.signalhire.com/api/v1/candidate/search', { items: [ 'https://www.linkedin.com/in/profile1', 'email@example.com', '+44 0 123 456 789', '10000000000000000000000000000001' ], callbackUrl: 'https://www.yourdomain.com/yourCallbackUrl' }, { headers: { apikey: 'your_secret_api_key' } } ); ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; HttpClient client = HttpClient.newHttpClient(); String body = """ { "items": [ "https://www.linkedin.com/in/profile1", "email@example.com", "+44 0 123 456 789", "10000000000000000000000000000001" ], "callbackUrl": "https://www.yourdomain.com/yourCallbackUrl" } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://www.signalhire.com/api/v1/candidate/search")) .header("apikey", "your_secret_api_key") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ```ruby Ruby theme={null} require 'net/http' require 'json' uri = URI('https://www.signalhire.com/api/v1/candidate/search') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request['apikey'] = 'your_secret_api_key' request['Content-Type'] = 'application/json' request.body = { items: [ 'https://www.linkedin.com/in/profile1', 'email@example.com', '+44 0 123 456 789', '10000000000000000000000000000001' ], callbackUrl: 'https://www.yourdomain.com/yourCallbackUrl' }.to_json response = http.request(request) ``` ## Initial Response (HTTP 201) ```http theme={null} HTTP/2 201 Content-Type: application/json X-Credits-Left: 243 ``` ```json theme={null} { "requestId": 1748 } ``` HTTP `201` means the request was accepted and processing has started. The `requestId` uniquely identifies this request — it will be included in the `Request-Id` header of the callback payload, allowing incoming results to be matched to the original request. For other possible response codes — such as `401`, `402`, `429` — see [Response Codes](/response-codes). ## Callback Format Once processing is complete, SignalHire POSTs an array to your `callbackUrl`. Each element corresponds to one item from the request and contains: * **`item`** — the original identifier submitted (LinkedIn URL, email, phone, or UID) * **`status`** — result of processing for this item (see status values below) * **`candidate`** — full profile object, present only when `status` is `success`. See [Candidate Object](/person-api/candidate-object) for a complete field reference. ```json theme={null} [ { "item": "https://www.linkedin.com/in/profile1", "status": "success", "candidate": { "fullName": "Profile1", "contacts": [...], ... } }, { "item": "email@email.com", "status": "failed" }, { "item": "+44 0 808 189 3171", "status": "success", "candidate": { "fullName": "Profile2", ... } }, { "item": "10000000000000000000000000000001", "status": "credits_are_over" } ] ``` ### Callback Status Values | Status | Description | | ------------------ | ------------------------------------------------------- | | `success` | Profile found and returned in the `candidate` field. | | `failed` | Item not found or could not be processed. | | `credits_are_over` | Ran out of credits while processing this item. | | `timeout_exceeded` | Processing timed out (10-second limit). | | `duplicate_query` | Same request was submitted again within a short period. | ## Callback Delivery Setting up a reliable callback endpoint is the responsibility of the API client. The endpoint must be publicly accessible, accept POST requests, and respond with HTTP `200` within 10 seconds. SignalHire has no way to re-initiate processing — if a callback is lost, a new API request must be submitted. **Delivery flow:** * Delivery is considered successful when the callback endpoint responds with HTTP `200` * If the connection fails or times out (10 seconds), SignalHire retries **3 times** sequentially * If all retries fail, the callback is discarded and the results are lost * The `Request-Id` header in the callback payload contains the original request ID for tracking The callback endpoint must be publicly accessible and respond within 10 seconds. Heavy processing should not be performed inside the callback handler — acknowledge with `200` immediately and process the data asynchronously. **Delivery timing:** Callback delivery time varies per request because SignalHire queries external APIs in real time to enrich contact data. Most requests complete within a few seconds, though some may take 30–60 seconds or more depending on external API response times. There is no fixed maximum — the callback is always sent once processing completes, regardless of how long it takes. There is no server-side timeout that would cause a callback to be silently dropped. ### Failed Delivery Notifications If callbacks could not be delivered, SignalHire sends an **email notification once per hour** listing the affected request IDs and error details. Example: ``` We weren't able to deliver 2 responses to your callback URL due to the network error on your end. cURL error 28: Operation timed out after 10001 milliseconds with 0 out of -1 bytes received. Kindly check the request IDs below and make the required adjustments. 132725817 132725819 You need to initiate new API calls to receive these responses. ``` Upon receiving such a notification, the issue with the callback endpoint should be resolved and new API requests submitted for the listed request IDs. ## Rate Limits For information on request limits and throttling, see [Rate Limits](/person-api/rate-limits). # Profiles Without Contacts Source: https://docs.signalhire.com/person-api/without-contacts Retrieve public profile data without contact details using a separate credit type. ## Overview By adding `withoutContacts: true` to a Person API request, a profile's public data can be retrieved — name, location, work experience, education, skills — **without consuming standard contact credits**. The LinkedIn profile link is still included in the response. This is useful when only profile metadata is needed without contact information. This mode uses a **separate credit type**. Contact [support@signalhire.com](mailto:support@signalhire.com) to purchase these credits. ## Request Parameters Uses the same endpoint and parameters as [Retrieve Person Data](/person-api/retrieve-person#request-parameters), with `withoutContacts` set to `true`. All other parameters, response codes, and rate limits apply identically. `withoutContacts` can also be combined with `withoutWaterfall: true` for a fully synchronous flow — profile metadata returned immediately without contacts and without needing a callback server. In this case `callbackUrl` must not be included. ## Request Example (Async with Callback) ```bash cURL theme={null} curl -X POST https://www.signalhire.com/api/v1/candidate/search \ -H 'apikey: your_secret_api_key' \ --data '{ "items": ["https://www.linkedin.com/in/profile1"], "withoutContacts": true, "callbackUrl": "https://www.yourdomain.com/yourCallbackUrl" }' ``` ```python Python theme={null} import requests response = requests.post( "https://www.signalhire.com/api/v1/candidate/search", headers={"apikey": "your_secret_api_key"}, json={ "items": ["https://www.linkedin.com/in/profile1"], "withoutContacts": True, "callbackUrl": "https://www.yourdomain.com/yourCallbackUrl" } ) ``` ```javascript Node.js theme={null} const axios = require('axios'); const response = await axios.post( 'https://www.signalhire.com/api/v1/candidate/search', { items: ['https://www.linkedin.com/in/profile1'], withoutContacts: true, callbackUrl: 'https://www.yourdomain.com/yourCallbackUrl' }, { headers: { apikey: 'your_secret_api_key' } } ); ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; HttpClient client = HttpClient.newHttpClient(); String body = """ { "items": ["https://www.linkedin.com/in/profile1"], "withoutContacts": true, "callbackUrl": "https://www.yourdomain.com/yourCallbackUrl" } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://www.signalhire.com/api/v1/candidate/search")) .header("apikey", "your_secret_api_key") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ```ruby Ruby theme={null} require 'net/http' require 'json' uri = URI('https://www.signalhire.com/api/v1/candidate/search') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request['apikey'] = 'your_secret_api_key' request['Content-Type'] = 'application/json' request.body = { items: ['https://www.linkedin.com/in/profile1'], withoutContacts: true, callbackUrl: 'https://www.yourdomain.com/yourCallbackUrl' }.to_json response = http.request(request) ``` ## Request Example (Sync without Callback) To retrieve profile metadata synchronously without contacts and without a callback server, combine `withoutContacts: true` with `withoutWaterfall: true`: ```bash cURL theme={null} curl -X POST https://www.signalhire.com/api/v1/candidate/search \ -H 'apikey: your_secret_api_key' \ --data '{ "items": ["https://www.linkedin.com/in/profile1"], "withoutContacts": true, "withoutWaterfall": true }' ``` ```python Python theme={null} import requests response = requests.post( "https://www.signalhire.com/api/v1/candidate/search", headers={"apikey": "your_secret_api_key"}, json={ "items": ["https://www.linkedin.com/in/profile1"], "withoutContacts": True, "withoutWaterfall": True } ) ``` ```javascript Node.js theme={null} const axios = require('axios'); const response = await axios.post( 'https://www.signalhire.com/api/v1/candidate/search', { items: ['https://www.linkedin.com/in/profile1'], withoutContacts: true, withoutWaterfall: true }, { headers: { apikey: 'your_secret_api_key' } } ); ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; HttpClient client = HttpClient.newHttpClient(); String body = """ { "items": ["https://www.linkedin.com/in/profile1"], "withoutContacts": true, "withoutWaterfall": true } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://www.signalhire.com/api/v1/candidate/search")) .header("apikey", "your_secret_api_key") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ```ruby Ruby theme={null} require 'net/http' require 'json' uri = URI('https://www.signalhire.com/api/v1/candidate/search') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request['apikey'] = 'your_secret_api_key' request['Content-Type'] = 'application/json' request.body = { items: ['https://www.linkedin.com/in/profile1'], withoutContacts: true, withoutWaterfall: true }.to_json response = http.request(request) ``` ## Response The callback format is identical to the standard [Retrieve Person Data](/person-api/retrieve-person#callback-format) response, but contact fields (phone numbers, email addresses) will be absent from the `candidate` object. The LinkedIn profile link remains available in the `social` array. See [Candidate Object](/person-api/candidate-object) for the full field reference. When using sync mode (`withoutWaterfall: true`), results are returned directly in the response body in the same format — no callback is sent. ## Callback Delivery Callback delivery rules are identical to the standard mode — see [Callback Delivery](/person-api/retrieve-person#callback-delivery) for details on timeouts, retries, and failed delivery notifications. This section applies only to async mode. When using `withoutWaterfall: true`, there is no callback. ## Checking Credits This mode uses a separate credit type, independent of standard contact credits. To check the remaining without-contacts balance, use the `withoutContacts=true` parameter with the [Get Remaining Credits](/person-api/credits) endpoint. The `X-Credits-Left` header is included in every response and automatically reflects the credit type matching the request — without-contacts credits when `withoutContacts: true` is set, standard credits otherwise. # Lookup Without Waterfall Mode Source: https://docs.signalhire.com/person-api/without-waterfall Get results synchronously in a single request, without setting up a callback server. By default, the Person API is asynchronous — a request is submitted and results arrive via a callback URL. The `withoutWaterfall` mode changes this to a **synchronous flow**: results are returned immediately in the response body. ## How It Differs | | Standard (async) | withoutWaterfall (sync) | | ------------------------ | ------------------------------ | ---------------------------------------------------- | | Response time | Slower (queries external APIs) | **Fast, immediate** | | Requires callback server | Yes | **No** | | Callback Url | Required | Not allowed | | External API enrichment | **Yes** | No — only internal data | | Contact coverage | **Higher** | Lower | | Data freshness | **Higher** | May be less current | | Without contacts mode | Supported | **Supported** — combine with `withoutContacts: true` | Use `withoutWaterfall` when a fast response is needed and fewer or slightly older contacts are acceptable. Use the standard async mode when completeness matters more than speed. ## Request Parameters The same parameters apply as in the standard [Retrieve Person Data](/person-api/retrieve-person#request-parameters) endpoint, with two differences: * `withoutWaterfall` must be set to `true` * `callbackUrl` must **not** be included Optionally, `withoutContacts: true` can be added to retrieve profiles without contact details synchronously. This uses [without-contacts credits](/person-api/without-contacts#checking-credits) instead of standard credits. ## Request Example ```bash cURL theme={null} curl -X POST https://www.signalhire.com/api/v1/candidate/search \ -H 'apikey: your_secret_api_key' \ --data '{ "items": [ "https://www.linkedin.com/in/jennwolf", "10000000000000000000000000001001", "+123 45 6777" ], "withoutWaterfall": true }' ``` ```python Python theme={null} import requests response = requests.post( "https://www.signalhire.com/api/v1/candidate/search", headers={"apikey": "your_secret_api_key"}, json={ "items": [ "https://www.linkedin.com/in/jennwolf", "10000000000000000000000000001001", "+123 45 6777" ], "withoutWaterfall": True } ) ``` ```javascript Node.js theme={null} const axios = require('axios'); const response = await axios.post( 'https://www.signalhire.com/api/v1/candidate/search', { items: [ 'https://www.linkedin.com/in/jennwolf', '10000000000000000000000000001001', '+123 45 6777' ], withoutWaterfall: true }, { headers: { apikey: 'your_secret_api_key' } } ); ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; HttpClient client = HttpClient.newHttpClient(); String body = """ { "items": [ "https://www.linkedin.com/in/jennwolf", "10000000000000000000000000001001", "+123 45 6777" ], "withoutWaterfall": true } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://www.signalhire.com/api/v1/candidate/search")) .header("apikey", "your_secret_api_key") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ```ruby Ruby theme={null} require 'net/http' require 'json' uri = URI('https://www.signalhire.com/api/v1/candidate/search') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request['apikey'] = 'your_secret_api_key' request['Content-Type'] = 'application/json' request.body = { items: [ 'https://www.linkedin.com/in/jennwolf', '10000000000000000000000000001001', '+123 45 6777' ], withoutWaterfall: true }.to_json response = http.request(request) ``` `callbackUrl` cannot be used together with `withoutWaterfall`. Including both will return HTTP `406`. ## Response Example (HTTP 200) The response format is identical to the callback payload in standard mode — an array of result objects returned directly. Each element contains `item`, `status`, and `candidate` when successful. See [Candidate Object](/person-api/candidate-object) for a complete field reference. ```http theme={null} HTTP/2 200 Content-Type: application/json X-Credits-Left: 241 ``` ```json theme={null} [ { "item": "https://www.linkedin.com/in/jennwolf", "status": "success", "candidate": { "fullName": "Jenn Wolf", "contacts": [...], ... } }, { "item": "10000000000000000000000000001001", "status": "success", "candidate": { "fullName": "John Dou", "contacts": [...], ... } }, { "item": "+123 45 6777", "status": "failed" } ] ``` For other possible response codes — such as `401`, `402`, `406`, `429` — see [Response Codes](/response-codes). ### Response Status Values | Status | Description | | ------------------ | ------------------------------------------------------- | | `success` | Profile found and returned in the `candidate` field. | | `failed` | Item not found or could not be processed. | | `credits_are_over` | Credits ran out while processing this item. | | `duplicate_query` | Same request was submitted again within a short period. | ## Rate Limits For information on request limits and throttling, see [Rate Limits](/person-api/rate-limits). ## When to Use withoutWaterfall Use `withoutWaterfall` when: * A fast synchronous response is more important than maximum contact coverage * Setting up a publicly accessible callback server is not feasible — for example, in local development, scripts, or environments without a public endpoint. Use standard async mode with `callbackUrl` when completeness matters more than speed — it queries external APIs in real time and returns the most up-to-date contact data available. # Response Codes Source: https://docs.signalhire.com/response-codes HTTP status codes returned by the SignalHire API. | Code | Meaning | | ----- | ------------------------------------------------------------------------------------ | | `200` | Request completed successfully, data returned. | | `201` | Request accepted, server started collecting data (async mode). | | `204` | Request still in progress. | | `401` | Authentication failed. Check your API key. | | `402` | Credits exhausted. | | `403` | Account disabled, or you queried a request that doesn't belong to you. | | `404` | Endpoint not found, invalid JSON, or non-existing request ID. | | `406` | Request validation error (e.g. `callbackUrl` used together with `withoutWaterfall`). | | `422` | Malformed or incorrect parameters. | | `429` | Rate limit exceeded — 600 items/min for Person API, 3 concurrent for Search API. | | `500` | Internal server error. | # Boolean Query Guide for People Search Source: https://docs.signalhire.com/search-api/boolean-query How to use Boolean operators in title, company, and keyword filters. The `currentTitle`, `currentPastTitle`, `currentCompany`, `currentPastCompany`, and `keywords` fields support Boolean search operators. | Operator | Description | Example | | -------- | ----------------------------- | ----------------------------- | | `AND` | Both terms must appear | `PHP AND JavaScript` | | `OR` | At least one term must appear | `Python OR Java` | | `NOT` | Exclude a term | `Manager NOT Assistant` | | `()` | Group terms for logic | `(Java AND Spring) OR Python` | | `""` | Exact phrase match | `"Software Engineer"` | ## Examples Find software engineers or developers with PHP and JavaScript skills: ```json theme={null} { "currentTitle": "(\"Software Engineer\") OR Developer", "keywords": "PHP AND JavaScript" } ``` Find managers excluding assistants at Google or Microsoft: ```json theme={null} { "currentTitle": "Manager NOT Assistant", "currentCompany": "Google OR Microsoft" } ``` # Full People Search Example with Code Source: https://docs.signalhire.com/search-api/full-search-example End-to-end example of fetching all search results using searchByQuery and scrollSearch. ## Overview This example shows how to fetch all matching profiles from a search query by combining `searchByQuery` with `scrollSearch`. Each batch is saved to the database immediately as it arrives — rather than collecting everything in memory first — to minimize memory usage and avoid data loss if the process is interrupted. The pattern is: 1. Send the initial `searchByQuery` request — save the first batch, get a `scrollId` 2. Loop: save each batch, send `scrollSearch` with the new `scrollId` 3. Stop when no `scrollId` is returned in the response The next `scrollSearch` request must be sent within **15 seconds** of the previous response. Do not perform slow operations (heavy processing, external API calls) between scroll requests — save to the database and immediately request the next batch. ```python Python theme={null} import requests import psycopg2 import json API_KEY = "your_secret_api_key" BASE_URL = "https://www.signalhire.com/api/v1/candidate" HEADERS = {"apikey": API_KEY, "Content-Type": "application/json"} def save_batch(cur, profiles: list): for profile in profiles: cur.execute( """ INSERT INTO candidates (uid, full_name, location, skills, open_to_work, raw_data) VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT (uid) DO UPDATE SET full_name = EXCLUDED.full_name, location = EXCLUDED.location, skills = EXCLUDED.skills, open_to_work = EXCLUDED.open_to_work, raw_data = EXCLUDED.raw_data """, ( profile["uid"], profile.get("fullName"), profile.get("location"), json.dumps(profile.get("skills", [])), profile.get("openToWork", False), json.dumps(profile), ) ) def search_and_save(query: dict): conn = psycopg2.connect("postgresql://user:password@localhost/mydb") cur = conn.cursor() total_saved = 0 # Initial search response = requests.post(f"{BASE_URL}/searchByQuery", headers=HEADERS, json=query) response.raise_for_status() data = response.json() request_id = data["requestId"] scroll_id = data.get("scrollId") total = data["total"] print(f"Total profiles found: {total}") save_batch(cur, data.get("profiles", [])) conn.commit() total_saved += len(data.get("profiles", [])) print(f"Saved {total_saved} / {total}") # Scroll through remaining batches while scroll_id: response = requests.post( f"{BASE_URL}/scrollSearch/{request_id}", headers=HEADERS, json={"scrollId": scroll_id} ) response.raise_for_status() data = response.json() save_batch(cur, data.get("profiles", [])) conn.commit() total_saved += len(data.get("profiles", [])) scroll_id = data.get("scrollId") print(f"Saved {total_saved} / {total}") cur.close() conn.close() print("Done") search_and_save({ "currentTitle": "(Software AND Engineer) OR Developer", "location": "New York, New York, United States", "keywords": "PHP AND JavaScript", "size": 50 }) ``` ```javascript Node.js theme={null} const axios = require('axios'); const { Pool } = require('pg'); const API_KEY = 'your_secret_api_key'; const BASE_URL = 'https://www.signalhire.com/api/v1/candidate'; const HEADERS = { apikey: API_KEY, 'Content-Type': 'application/json' }; const pool = new Pool({ connectionString: 'postgresql://user:password@localhost/mydb' }); async function saveBatch(client, profiles) { for (const profile of profiles) { await client.query( `INSERT INTO candidates (uid, full_name, location, skills, open_to_work, raw_data) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (uid) DO UPDATE SET full_name = EXCLUDED.full_name, location = EXCLUDED.location, skills = EXCLUDED.skills, open_to_work = EXCLUDED.open_to_work, raw_data = EXCLUDED.raw_data`, [ profile.uid, profile.fullName ?? null, profile.location ?? null, JSON.stringify(profile.skills ?? []), profile.openToWork ?? false, JSON.stringify(profile), ] ); } } async function searchAndSave(query) { const client = await pool.connect(); let totalSaved = 0; try { // Initial search const initial = await axios.post(`${BASE_URL}/searchByQuery`, query, { headers: HEADERS }); const { requestId, total } = initial.data; let scrollId = initial.data.scrollId; console.log(`Total profiles found: ${total}`); await client.query('BEGIN'); await saveBatch(client, initial.data.profiles); await client.query('COMMIT'); totalSaved += initial.data.profiles.length; console.log(`Saved ${totalSaved} / ${total}`); // Scroll through remaining batches while (scrollId) { const response = await axios.post( `${BASE_URL}/scrollSearch/${requestId}`, { scrollId }, { headers: HEADERS } ); scrollId = response.data.scrollId; await client.query('BEGIN'); await saveBatch(client, response.data.profiles); await client.query('COMMIT'); totalSaved += response.data.profiles.length; console.log(`Saved ${totalSaved} / ${total}`); } console.log('Done'); } catch (err) { await client.query('ROLLBACK'); throw err; } finally { client.release(); await pool.end(); } } searchAndSave({ currentTitle: '(Software AND Engineer) OR Developer', location: 'New York, New York, United States', keywords: 'PHP AND JavaScript', size: 50, }).catch(console.error); ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; import java.sql.*; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.*; public class SignalHireSearch { private static final String API_KEY = "your_secret_api_key"; private static final String BASE_URL = "https://www.signalhire.com/api/v1/candidate"; private static final HttpClient client = HttpClient.newHttpClient(); private static final ObjectMapper mapper = new ObjectMapper(); static void saveBatch(Connection conn, List> profiles) throws Exception { String sql = """ INSERT INTO candidates (uid, full_name, location, open_to_work, raw_data) VALUES (?, ?, ?, ?, ?::jsonb) ON CONFLICT (uid) DO UPDATE SET full_name = EXCLUDED.full_name, location = EXCLUDED.location, open_to_work = EXCLUDED.open_to_work, raw_data = EXCLUDED.raw_data """; try (PreparedStatement stmt = conn.prepareStatement(sql)) { for (Map profile : profiles) { stmt.setString(1, (String) profile.get("uid")); stmt.setString(2, (String) profile.get("fullName")); stmt.setString(3, (String) profile.get("location")); stmt.setBoolean(4, Boolean.TRUE.equals(profile.get("openToWork"))); stmt.setString(5, mapper.writeValueAsString(profile)); stmt.addBatch(); } stmt.executeBatch(); } } static Map post(String path, Object body) throws Exception { String json = mapper.writeValueAsString(body); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + path)) .header("apikey", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); return mapper.readValue(response.body(), Map.class); } public static void searchAndSave(Map query) throws Exception { try (Connection conn = DriverManager.getConnection( "jdbc:postgresql://localhost/mydb", "user", "password")) { conn.setAutoCommit(false); int totalSaved = 0; // Initial search Map data = post("/searchByQuery", query); int requestId = (int) data.get("requestId"); int total = (int) data.get("total"); String scrollId = (String) data.get("scrollId"); System.out.println("Total profiles found: " + total); saveBatch(conn, (List) data.get("profiles")); conn.commit(); totalSaved += ((List) data.get("profiles")).size(); System.out.println("Saved " + totalSaved + " / " + total); // Scroll through remaining batches while (scrollId != null) { Map scrollData = post( "/scrollSearch/" + requestId, Map.of("scrollId", scrollId) ); List> profiles = (List) scrollData.get("profiles"); scrollId = (String) scrollData.get("scrollId"); saveBatch(conn, profiles); conn.commit(); totalSaved += profiles.size(); System.out.println("Saved " + totalSaved + " / " + total); } System.out.println("Done"); } } public static void main(String[] args) throws Exception { Map query = new HashMap<>(); query.put("currentTitle", "(Software AND Engineer) OR Developer"); query.put("location", "New York, New York, United States"); query.put("keywords", "PHP AND JavaScript"); query.put("size", 50); searchAndSave(query); } } ``` ```ruby Ruby theme={null} require 'net/http' require 'json' require 'pg' API_KEY = 'your_secret_api_key' BASE_URL = 'https://www.signalhire.com/api/v1/candidate' def api_post(path, body) uri = URI("#{BASE_URL}#{path}") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request['apikey'] = API_KEY request['Content-Type'] = 'application/json' request.body = body.to_json JSON.parse(http.request(request).body) end def save_batch(conn, profiles) profiles.each do |profile| conn.exec_params( <<~SQL, INSERT INTO candidates (uid, full_name, location, open_to_work, raw_data) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (uid) DO UPDATE SET full_name = EXCLUDED.full_name, location = EXCLUDED.location, open_to_work = EXCLUDED.open_to_work, raw_data = EXCLUDED.raw_data SQL [ profile['uid'], profile['fullName'], profile['location'], profile['openToWork'] ? 't' : 'f', profile.to_json ] ) end end def search_and_save(query) conn = PG.connect(dbname: 'mydb', user: 'user', password: 'password', host: 'localhost') total_saved = 0 # Initial search data = api_post('/searchByQuery', query) request_id = data['requestId'] total = data['total'] scroll_id = data['scrollId'] puts "Total profiles found: #{total}" conn.transaction { save_batch(conn, data['profiles']) } total_saved += data['profiles'].size puts "Saved #{total_saved} / #{total}" # Scroll through remaining batches while scroll_id data = api_post("/scrollSearch/#{request_id}", { scrollId: scroll_id }) scroll_id = data['scrollId'] conn.transaction { save_batch(conn, data['profiles']) } total_saved += data['profiles'].size puts "Saved #{total_saved} / #{total}" end conn.close puts 'Done' end search_and_save( currentTitle: '(Software AND Engineer) OR Developer', location: 'New York, New York, United States', keywords: 'PHP AND JavaScript', size: 50 ) ``` # Supported Industry Values for People Search Source: https://docs.signalhire.com/search-api/industries List of allowed values for the industry filter in the Search API. The following values are accepted by the `industry` parameter in [Search by Query](/search-api/search-by-query). Passing an unrecognized value returns HTTP `422`. | Value | | ------------------------------------ | | Accounting | | Airlines/Aviation | | Alternative Dispute Resolution | | Alternative Medicine | | Animation | | Apparel & Fashion | | Architecture & Planning | | Arts and Crafts | | Automotive | | Aviation & Aerospace | | Banking | | Biotechnology | | Broadcast Media | | Building Materials | | Business Supplies and Equipment | | Capital Markets | | Chemicals | | Civic & Social Organization | | Civil Engineering | | Commercial Real Estate | | Computer & Network Security | | Computer Games | | Computer Hardware | | Computer Networking | | Computer Software | | Construction | | Consumer Electronics | | Consumer Goods | | Consumer Services | | Cosmetics | | Dairy | | Defense & Space | | Design | | E-Learning | | Education Management | | Electrical/Electronic Manufacturing | | Environmental Services | | Events Services | | Executive Office | | Facilities Services | | Farming | | Fine Art | | Financial Services | | Fishery | | Food & Beverages | | Food Production | | Fund-Raising | | Furniture | | Gambling & Casinos | | Glass, Ceramics & Concrete | | Government Administration | | Government Relations | | Graphic Design | | Health, Wellness and Fitness | | Higher Education | | Hospitality | | Hospital & Health Care | | Human Resources | | Import and Export | | Individual & Family Services | | Industrial Automation | | Information Services | | Information Technology and Services | | Insurance | | International Affairs | | International Trade and Development | | Internet | | Investment Banking | | Investment Management | | Judiciary | | Law Enforcement | | Law Practice | | Legal Services | | Legislative Office | | Leisure, Travel & Tourism | | Libraries | | Logistics and Supply Chain | | Luxury Goods & Jewelry | | Machinery | | Management Consulting | | Maritime | | Market Research | | Marketing and Advertising | | Mechanical or Industrial Engineering | | Media Production | | Medical Devices | | Medical Practice | | Mental Health Care | | Military | | Mining & Metals | | Motion Pictures and Film | | Museums and Institutions | | Music | | Nanotechnology | | Newspapers | | Nonprofit Organization Management | | Oil & Energy | | Online Media | | Outsourcing/Offshoring | | Package/Freight Delivery | | Packaging and Containers | | Paper & Forest Products | | Performing Arts | | Pharmaceuticals | | Philanthropy | | Photography | | Plastics | | Political Organization | | Primary/Secondary Education | | Printing | | Professional Training & Coaching | | Program Development | | Public Policy | | Public Relations and Communications | | Public Safety | | Publishing | | Railroad Manufacture | | Ranching | | Real Estate | | Recreational Facilities and Services | | Religious Institutions | | Renewables & Environment | | Research | | Restaurants | | Retail | | Security and Investigations | | Semiconductors | | Shipbuilding | | Sporting Goods | | Sports | | Staffing and Recruiting | | Supermarkets | | Telecommunications | | Textiles | | Think Tanks | | Tobacco | | Translation and Localization | | Transportation/Trucking/Railroad | | Utilities | | Venture Capital & Private Equity | | Veterinary | | Warehousing | | Wholesale | | Wine and Spirits | | Wireless | | Writing and Editing | # Scroll Search: Paginate Large Result Sets Source: https://docs.signalhire.com/search-api/scroll-search Paginate through search results using scrollId. ## Overview After an initial [Search by Query](/search-api/search-by-query) returns a `scrollId`, use this endpoint to fetch subsequent batches of results. The mechanism works similarly to Elasticsearch scroll — the server keeps the search context alive while batches are being retrieved one after another. **The intended usage pattern is sequential and continuous**: fetch all needed results in one session, batch by batch, then process them. The scroll session is not designed for periodic polling — for example, fetching 100 profiles every hour is not a supported use case. The `scrollId` expires after **15 seconds**. Send your next request within this window or the scroll session will be lost. ## Understanding the 15-Second Timeout The `scrollId` expires **15 seconds** after it is issued. This is intentional — it represents the maximum time allowed to request the *next* batch after receiving the current one. The server keeps the search context alive for the entire duration of an active scroll session. The 15-second window applies only between consecutive requests, not to the total session length. As long as each subsequent request arrives within 15 seconds of the previous response, the session remains active regardless of total session duration. A common misconception is treating the scroll like a bookmark — fetching a batch, processing it for several minutes, then resuming. This will not work. The correct approach: 1. Start with `searchByQuery` — receive the first batch and `scrollId` 2. Immediately send the next `scrollSearch` request using the new `scrollId` 3. Repeat until no `scrollId` is returned in the response (no more results) 4. Process all collected results after the session is complete *** **Endpoint:** `POST https://www.signalhire.com/api/v1/candidate/scrollSearch/{requestId}` ## Request Parameters The `requestId` from the initial `searchByQuery` response. Included in the URL path. The `scrollId` from the previous `searchByQuery` or `scrollSearch` response. ## Request Example ```bash cURL theme={null} curl -X POST https://www.signalhire.com/api/v1/candidate/scrollSearch/3 \ -H 'apikey: your_secret_api_key' \ --data '{"scrollId": "abc123"}' ``` ```python Python theme={null} import requests response = requests.post( "https://www.signalhire.com/api/v1/candidate/scrollSearch/3", headers={"apikey": "your_secret_api_key"}, json={"scrollId": "abc123"} ) ``` ```javascript Node.js theme={null} const axios = require('axios'); const response = await axios.post( 'https://www.signalhire.com/api/v1/candidate/scrollSearch/3', { scrollId: 'abc123' }, { headers: { apikey: 'your_secret_api_key' } } ); ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://www.signalhire.com/api/v1/candidate/scrollSearch/3")) .header("apikey", "your_secret_api_key") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString("{\"scrollId\": \"abc123\"}")) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ```ruby Ruby theme={null} require 'net/http' require 'json' uri = URI('https://www.signalhire.com/api/v1/candidate/scrollSearch/3') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request['apikey'] = 'your_secret_api_key' request['Content-Type'] = 'application/json' request.body = { scrollId: 'abc123' }.to_json response = http.request(request) ``` ## Response Example (HTTP 200) The response format is identical to the initial [Search by Query](/search-api/search-by-query#response-example-http-200) response — same fields, same structure. The `total` field always reflects the overall count across all batches, not just the current one. ```json theme={null} { "requestId": 3, "total": 12, "scrollId": "xyz456", "profiles": [ { "uid": "10000000000000000000000000001001", "fullName": "John Smith", "location": "Brisbane Area, Australia", "experience": [ { "company": "Super company", "title": "PHP Developer" } ], "skills": ["PHP", "HTML", "Management"], "openToWork": true } ] } ``` Each response includes a new `scrollId` to use in the next request. When `scrollId` is absent from the response, all results have been retrieved and the scroll session is complete. See [Search Profile Object](/search-api/search-profile-object) for a full field reference. Scroll requests do not count as new searches against the daily query quota. However, every profile returned by a scroll does count against the daily profile quota — the same limit that applies to `searchByQuery` results. ## Response Status Codes | Code | Description | | ----- | ------------------------------- | | `200` | Results returned successfully | | `402` | Daily search quota exceeded | | `404` | Invalid or expired `scrollId` | | `429` | More than 3 concurrent requests | ## Full Example For a complete end-to-end example showing `searchByQuery` and `scrollSearch` working together with database saving, see [Full Search Example](/search-api/full-search-example). # People Search by Query Parameters Source: https://docs.signalhire.com/search-api/search-by-query Search for candidates in the SignalHire database using filters like title, location, company, and more. ## Overview The Search API allows searching SignalHire's database of professionals using a wide variety of filters — job title, location, company, industry, experience, and more. Unlike the Person API, it does not require a known identifier. Results are returned as **brief profile overviews without contact details**, making it ideal for discovering new candidates or leads at scale. Results are returned **synchronously** — the first batch is included directly in the response. For large result sets, subsequent batches are retrieved using a scroll mechanism similar to Elasticsearch: each response includes a `scrollId` that can be passed to the [Scroll Search](/search-api/scroll-search) endpoint to fetch the next batch. A typical workflow: 1. Submit a `searchByQuery` request with filters — receive the first batch and a `scrollId` 2. Use [Scroll Search](/search-api/scroll-search) with the `scrollId` to page through remaining results 3. Pass interesting profile UIDs to the [Person API](/person-api/retrieve-person) to retrieve full contact details Maximum **3 concurrent** Search API requests are allowed at a time. *** **Endpoint:** `POST https://www.signalhire.com/api/v1/candidate/searchByQuery` ## Request Parameters **Header** Secret API key. See [Authentication](/authentication). **Body** Boolean query for current job title. Cannot be combined with `currentPastTitle` — if both are provided, `currentPastTitle` takes precedence. See [Boolean Query Guide](/search-api/boolean-query). Boolean query for current or past job title. Takes precedence over `currentTitle` if both are provided. Geographical area for the search. Accepts a single city, state, or country as a string, or multiple locations as an array. Examples: `"Los Angeles, California"`, `["India", "Rome, Italy"]`. Returns `422` if the location cannot be recognized. Latitude coordinate for geo-based search. Must be provided together with `longitude`. Searches within a `radius` around this point (default 10km). Overrides `location` when set. Longitude coordinate for geo-based search. Must be provided together with `latitude`. Multiple geographic points to search around simultaneously. Each item must contain `latitude` and `longitude`. Overrides `location`. Used only when top-level `latitude`/`longitude` are not set. Each point uses the `radius` value (default 10km). ```json theme={null} "coordinates": [ { "latitude": 41.9028, "longitude": 12.4964 }, { "latitude": 45.4642, "longitude": 9.1900 } ] ``` Search radius **in kilometers** around each geo point, applied to `latitude`/`longitude`, `coordinates`, and city-level `location` values. Default: `10`, must be between `10` and `160`. Returns `422` if outside this range. Boolean query for current company name. Boolean query for current or past company name. Search by full name. Boolean query for skills, description, education, and other profile attributes. Filter by industry category. Returns `422` if the value is not recognized. See the full list of [allowed values](/search-api/industries). Filter by multiple industry categories simultaneously. Each value must be a valid industry name from the [allowed values](/search-api/industries) list. Returns `422` if any value is not recognized. Use instead of `industry` when filtering by more than one industry. Minimum years of experience in the current role and company. Maximum years of experience in the current role and company. Minimum years of experience across all roles and companies. Maximum years of experience across all roles and companies. Boolean query applied across university name, faculty, and degree. See [Boolean Query Guide](/search-api/boolean-query). Filter by department. Accepted values: `Marketing`, `Sales`, `Product & Project`, `Engineering / Development`, `Data & Analytics`, `Design / UX`, `HR & Recruitment`, `Finance & Accounting`, `Legal / Compliance`, `Customer Support / Success`, `Health, Beauty & Hospitality`, `Education & Training`, `Operations & Admin`, `Facility Services`, `Entertainment / Production` Filter by seniority level. Accepted values: `Intern / Entry Level`, `Junior`, `Mid-Level`, `Senior`, `Lead`, `Head`, `VP`, `C-Level`, `Founder / Owner`, `Board / Advisory`, `Directors` Filter by open-to-work status. Exclude profiles for which contacts have already been fetched. Exclude profiles already viewed in the web app or browser extension. Exclude profiles already added to a list. Exclude profiles already added to a job. Exclude profiles already emailed. Number of profiles to return per batch. Default: `10`, must be between `1` and `100`. Returns `422` if outside this range. At least one non-exclude filter must be provided — an empty request returns `422`. Exclude filters (`excludeRevealed`, `excludeWatched`, etc.) cannot be used as the only filters. ## Location Formats The `location` filter supports three levels of granularity. The value is resolved in this order: **Country** — pass the full English country name. Searches the entire country. ```json theme={null} "location": "Germany" "location": ["India", "Brazil"] ``` **State or Province** — supported for the United States and Canada. The country suffix is optional. ```json theme={null} "location": "California" "location": "California, United States" "location": "Ontario, Canada" ``` **City** — pass a city name with enough context to identify it unambiguously. Searches within a radius (in **kilometers**) of the city center, controlled by `radius` (default 10km). ```json theme={null} "location": "Miami, Florida" "location": "London, United Kingdom" "location": "Berlin, Germany" ``` If the value cannot be resolved to any known location, the request returns HTTP `422` with `"Location is not recognized"`. For precise geo-based search, use `latitude`/`longitude` or `coordinates` instead — they bypass text resolution entirely. `radius` applies to all of these. ## Request Example This example searches for profiles in New York whose current job title contains "Software Engineer" or "Developer", and who have both PHP and JavaScript mentioned in their profile. ```bash cURL theme={null} curl -X POST https://www.signalhire.com/api/v1/candidate/searchByQuery \ -H 'apikey: your_secret_api_key' \ --data '{ "currentTitle": "(Software AND Engineer) OR Developer", "location": "New York, New York, United States", "keywords": "PHP AND JavaScript" }' ``` ```python Python theme={null} import requests response = requests.post( "https://www.signalhire.com/api/v1/candidate/searchByQuery", headers={"apikey": "your_secret_api_key"}, json={ "currentTitle": "(Software AND Engineer) OR Developer", "location": "New York, New York, United States", "keywords": "PHP AND JavaScript" } ) ``` ```javascript Node.js theme={null} const axios = require('axios'); const response = await axios.post( 'https://www.signalhire.com/api/v1/candidate/searchByQuery', { currentTitle: '(Software AND Engineer) OR Developer', location: 'New York, New York, United States', keywords: 'PHP AND JavaScript' }, { headers: { apikey: 'your_secret_api_key' } } ); ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; HttpClient client = HttpClient.newHttpClient(); String body = """ { "currentTitle": "(Software AND Engineer) OR Developer", "location": "New York, New York, United States", "keywords": "PHP AND JavaScript" } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://www.signalhire.com/api/v1/candidate/searchByQuery")) .header("apikey", "your_secret_api_key") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ```ruby Ruby theme={null} require 'net/http' require 'json' uri = URI('https://www.signalhire.com/api/v1/candidate/searchByQuery') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request['apikey'] = 'your_secret_api_key' request['Content-Type'] = 'application/json' request.body = { currentTitle: '(Software AND Engineer) OR Developer', location: 'New York, New York, United States', keywords: 'PHP AND JavaScript' }.to_json response = http.request(request) ``` ## Response Example (HTTP 200) ```http theme={null} HTTP/2 200 Content-Type: application/json X-Credits-Left: 243 ``` ```json theme={null} { "requestId": 3, "total": 12, "scrollId": "abc123", "profiles": [ { "uid": "10000000000000000000000000001006", "fullName": "Aaron Smith", "location": "London, United Kingdom", "experience": [ { "company": "Saward Dawson", "title": "Accountant" } ], "skills": ["Accounting", "Analysis"], "contactsFetched": null, "openToWork": false } ] } ``` ### Response Fields | Field | Description | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `requestId` | ID of this search. Required for [Scroll Search](/search-api/scroll-search). | | `total` | Total matching profiles across all pages. | | `profiles` | First batch of results. See [Search Profile Object](/search-api/search-profile-object) for field reference. | | `scrollId` | Present when `total > size`. Pass to [Scroll Search](/search-api/scroll-search) to fetch the next batch. Expires in **15 seconds**. | For other possible response codes see [Response Codes](/response-codes). ## Search Quota Search requests are not billed per credit — they count against a **daily search quota**. This quota is shared between the SignalHire website and the API, so searches made in either place consume from the same daily limit. The daily quota covers two dimensions independently: the **number of search queries** and the **total number of profiles returned** across all searches. Both are tracked separately and either can be exhausted first depending on usage patterns. When the daily quota is exhausted, further search requests return HTTP `402` with an error message. The quota resets daily. There is also a concurrency limit: a maximum of **3 Search API requests** can be in progress at the same time. Exceeding this returns HTTP `429`. # Search Profile Object: Response Schema Source: https://docs.signalhire.com/search-api/search-profile-object Structure of the profile object returned by the Search API. Each item in the `profiles` array returned by [Search by Query](/search-api/search-by-query) and [Scroll Search](/search-api/scroll-search) has the following structure. Search profiles are brief overviews — they do not include contact details. To retrieve full profile data including emails and phone numbers, pass the `uid` to the [Person API](/person-api/retrieve-person). ## Example ```json theme={null} { "uid": "10000000000000000000000000001006", "fullName": "Aaron Smith", "location": "London, United Kingdom", "experience": [ { "company": "Saward Dawson", "title": "Accountant" }, { "company": "Previous Corp", "title": "Junior Accountant" } ], "skills": ["Accounting", "Analysis", "Excel"], "contactsFetched": "2024-03-15 10:22:00", "openToWork": false } ``` ## Fields Unique 32-character SignalHire profile identifier. Use this to request full profile data via the [Person API](/person-api/retrieve-person). Full name of the person. Primary location of the person (e.g. `"London, United Kingdom"`). Derived from the first location in the profile. List of work experience entries. Each item contains: * `company` — company name (`string | null`) * `title` — job title (`string | null`) List of professional skills. May be empty (`[]`). Date and time when contacts were last fetched for this profile, in `Y-m-d H:i:s` format. `null` if contacts have never been requested. Whether the person has indicated they are open to new opportunities.