- Test this endpoint in the playground
- Check out official Python client
Run a search query. This accepts a natural language query as query parameter along with additional settings. It returns a list of profiles that match the query and orders them by relevance.
The query can be:
- a simple search query like "software engineer from Ohio"
- a more complex query like "Find me engineers in California speaking at least basic english working in software industry with experience at FAANG with 2+ years of experience and at least 500 followers and at least BS degree".
- a full JD-style query that describes the ideal candidate in detail along with job description, responsibilities, requirements, etc.
If the query is already structured (for example JSON, structured text, or a boolean expression), you have two main options:
- Use the query parameter and pass the structured query as is. This is the simplest approach. Pearch.ai will interpret structured parts and priorities written in free-form text (such as “must have” or “very important”).
- Use the custom_filters parameter and map some or all structured parts to it. This gives you more control over filtering.
Use parameters to control tradeoffs between speed and quality. Here are some recipes:
- For highest quality results (30-180 seconds) use pro search with insights and profile scoring enabled:
{"query": "<YOUR QUERY HERE>", "type": "pro", "insights": true, "profile_scoring": true} - For medium to high quality results use "fast" search (<50s).
{"query": "<YOUR QUERY HERE>", "type": "fast", "insights": true, "profile_scoring": true} - For pagination, pass the previous response's
thread_idwithlimitand optionallyoffset. Omit the original query and search/filter parameters. To wait for an async search, send onlythread_idinstead; see Async search and polling below.
Results are generally returned in the order of decreasing relevancy. However, you may notice results with out of order score values in some cases especially when requesting large number of results. It should be safe to use stable sorting by score field on the client side in this cases. Note that score field is only calculated and returned when profile_scoring=true.
Check out other parameters below to adjust speed vs quality and additional data that can be returned by the endpoint.
Async search and polling
Start a search with POST /v2/search and async=true. Include your API key in the Authorization: Bearer <API_KEY> header and send a JSON body:
{"query":"Software engineer with Python experience in London","type":"fast","limit":10,"async":true}A newly started background search returns HTTP 202 with thread_id, task_id, and uuid (equal to task_id). Save thread_id to retrieve results. Keep task_id to correlate a completion webhook; separate searches or pagination tasks can share the same thread.
To wait for completion, repeat POST /v2/search with only thread_id:
{"thread_id":"<thread-uuid>"}Do not resend the query, filters, async, webhook_url, limit, or offset in this polling request.
- HTTP 202 with
statusequal topending,running, orstreaming: the thread has an active search. Wait before polling again; intermediate statuses may be skipped. A polling interval of a few seconds and an overall deadline are recommended client behavior. - HTTP 200 with
status="Done": readsearch_resultsdirectly from the response. There is no nestedresultenvelope. A bare thread read returns the currently stored results, which can include earlier pages; it is not restricted to the last requested page. - HTTP 4xx/5xx: handle the HTTP error rather than interpreting it as an in-progress status. An unknown thread returns 404. A thread with no reusable completed state can return 409, for example after its initial search fails. Do not retry every 409 indefinitely.
A cached request can return HTTP 200 immediately even when async=true; process that response without waiting for 202. Completed/cached reads have their own uuid, which need not equal the original task ID.
Thread polling retrieves the thread's current state, not a history of individual task outcomes. If a later task fails, previously completed results can remain available. Use the completion webhook's task_id and task_status when you need the outcome of a specific task.
Pagination after an async search
Wait for completion before requesting additional results. To request a specific page, send thread_id, limit, and offset to the same endpoint:
{"thread_id":"<thread-uuid>","offset":10,"limit":10,"async":true}A page already stored in the thread returns HTTP 200. If additional search work is needed, async=true starts it in the background and returns HTTP 202; poll again with only thread_id. After completion, send the explicit page bounds again to retrieve just that page. Without async=true, additional search work runs synchronously.
A request with limit or offset is pagination, not a status poll. While a search is active, requesting more profiles than are currently stored returns HTTP 409. For example, repeating the initial limit before any results have been stored can return 409. The existing search continues; wait using the bare thread polling request. A page already available in the completed state may still return HTTP 200 while another task runs, so that page's 200 response does not prove the active task finished.
Re-reading already billed results does not charge for those results again. Requesting additional results can consume credits. Omit the original query and search/filter parameters from pagination requests; changing the query or supplying filters with thread_id is rejected with HTTP 400 for standard clients.
Async search callbacks
As an alternative to polling, include webhook_url with async=true in the request that starts a search or an additional page.
Supply webhook_url, for example https://client.example.com/callback?token=<your-webhook-secret>. Use HTTPS and a dedicated secret generated and verified by your application. The supplied URL, including query parameters, is used for delivery. Callbacks have no HMAC signature or timestamp header; the URL secret does not provide cryptographic payload integrity or timestamp-based replay protection.
Each callback includes uuid, task_id (equal to uuid), thread_id, and task_status. Deduplicate by task_id, since separate tasks can share a thread. Every retry sends identical JSON bytes.
A successful callback preserves the synchronous result fields, including status="Done" and search_results, and adds task_id and task_status="completed". Polling through /v2/search instead returns HTTP 200 with status="Done" and top-level search_results.
Example successful callback (other search-result fields omitted):
{"uuid":"<task-uuid>","task_id":"<task-uuid>","thread_id":"<thread-uuid>","task_status":"completed","status":"Done","search_results":[]}A failed callback includes status="failed", task_status="failed", http, and a public error message. Optional code and action fields are preserved. For example:
{"uuid":"<task-uuid>","task_id":"<task-uuid>","thread_id":"<thread-uuid>","task_status":"failed","status":"failed","http":500,"error":"Internal server error"}Compatibility: older failed callbacks used status for the public error message. New failed callbacks use status="failed" and put that message in error. When accepting both formats, detect errors using http >= 400, prefer a nonempty string error for the message and fall back to status. Allow additional fields and treat newly added identifiers and task_status as optional when parsing legacy callbacks. Use a separate error model rather than the successful search-response model.
Handled cancellations use http=499 and error="Search task was cancelled before completion.". Requests rejected before a task is accepted return an HTTP error directly and do not produce a completion callback.
Delivery makes at most four attempts: one immediately, then retries after waits of 1, 2, and 4 seconds following the previous failure. Every non-2xx response or transport error triggers a retry. Each attempt uses a 10-second connection/read timeout; redirects are not followed. Return any 2xx promptly after safely accepting the event, including duplicates. Failed delivery does not rerun the search or change its completion status.
Retries run in the API worker, without a durable delivery queue. A process crash or restart can interrupt delivery, and delivery stops after the fourth failure. If a callback is missing, retrieve the current thread state with POST /v2/search and only thread_id, as described above. This recovers available thread results, not a guaranteed per-task failure history. For cached requests (HTTP 200), process the result in that HTTP response. Keep the requested page bounds if you need to reload a specific page after completion.
Cost: (number of profiles returned) × (credits for type + insights + high_freshness + reveal_emails + reveal_phones).
Rate limit: 60 actual search requests per user per 60 seconds. /v2/search and /v2/search/count share this limit with legacy search submissions. Fully cached thread reloads that do not run a new search use a separate higher limit of 120 cached requests per user per 60 seconds. When the rate limit is exceeded, the API returns HTTP 429 with details about the triggered window, current value, and limit.
400Invalid parameter values. Changing the query or supplying search/filter parameters with thread_id is rejected for standard clients. The public error body can use the generic status Bad Request.
401Invalid API key
404The requested thread was not found or is not accessible to this caller.
429Rate limit exceeded. JSON body contains the triggered window, current value, and limit.
