AI Ark is a broad B2B database that works from the company downward. Find the firms that match your brief, then the people in the roles you care about, then their emails and mobiles. It runs in reverse too: hand it a number and it tells you whose it is.
AI Ark is one of the first tools hyreflow reaches for when you ask for a list from nothing.
run both and merge, the coverage rarely overlaps completely
add a hiring signal to a company list
validate before anything sends
file the companies and their contacts in one write
A skill is a whole pipeline your agent already knows. These ones call AI Ark as part of the run.
Placed candidate updates LinkedIn. Track via AI Ark, log Fathom call notes, alert the original client.
Every tool ships with a written playbook, and the agent loads it before the first call. Auth, rate limits, what each call costs, which actions need your approval, and the mistakes worth avoiding. It is the difference between an agent that knows the tool and one that guesses at it.
Read the AI Ark docs →HOW in lib/aiark.py. Docs cached at reference/docs/aiark/raw/; full filter schema in reference/docs/aiark/raw/people_search.openapi.md.
https://api.ai-ark.com/api/developer-portal/v1 · Auth: header X-TOKEN (env AIARK_API_KEY; never hardcode)..md + llms.txt, Tier-2 plain HTTP). Watch the shapes: company search = POST /v1/companies (plural), email finder = POST /v1/people/email-finder, async status/results use {trackId} path segments and the results path is /inquiries, credits = GET /v1/payments/credits. Full endpoint index at reference/docs/aiark/raw/endpoints.md; people-search schema + enums at reference/docs/aiark/raw/people_search.openapi.md.People search body = {account, contact, lists, page, size}; page (0-based) and size (≤100) are required. account/contact filters take an any and/or all wrapper, each with include/exclude:
{ "mode": "SMART"|"WORD"|"STRICT", "content": [...] } (e.g. account name, url, productAndServices, industries, technologies; contact fullName, skill, certification, title (under experience), language). The mode enum is SMART/WORD/STRICT — not "FUZZY".{ "include": [...] } (e.g. account domain, linkedin, location, naics, technology, type; contact seniority, departmentAndFunction, socialMedia, profileBadge).contact.location (not contactLocation); titles/durations live under contact.experience.{latest,current,previous}.account.domain / account.linkedin / account.name. contact.company.{latest,current,previous} takes company UUIDs and returns contaminated rows even for a correct UUID — use the account.* fields, with root domains.totalElements of ~414,000,000 means the filters were ignored (that's the whole database), not that you matched a lot. An unrecognized filter key is dropped silently behind a 200, so peek the count with size: 1 when trying a new filter shape: adding a filter must reduce it.Response shape: top-level { content: [...people...], pageable, totalElements, totalPages, number, size, trackId, first, last, empty } — read people at response["content"]; the find-emails trackId is top-level. Errors: 404 data-not-found; 501 = socialMediaFollower on a non-LinkedIn platform (only linkedin supported).
export_people → trackId → poll export_statistics (DONE) → export_results (paginated).people_search (returns trackId) → find_emails(trackId,+webhook) → poll email_finder_statistics → email_finder_results. trackId is single-use, expires ~6h.mobile_phone_finder is expensive (0.4 credits) — use only on high-confidence matches. fetch_credit() = safe read-only pilot. Pilot small; gate bulk export/find-emails.Sourcing + enrichment: company→people search → email/phone (export/find-emails or mobile finder) → ATS/sequencer. Discover companies first, then people.
Field-shape note: these are vendor-native operational notes. The client returns the raw vendor JSON and uses the method names in this file — read field shapes accordingly (no normalized-wrapper /
result.data.prefix).
Base URL: server is https://api.ai-ark.com/api/developer-portal; the people-search path is /v1/people, so the full URL is .../developer-portal/v1/people. BASE_URL folds /v1 in and people_search posts to people.
The filter shapes, mode enum (
SMART/WORD/STRICT), field names, and response shape are documented from the spec in the Filter structure section above.
Credit costs: company search 0.1/result; people search 0.1/result; reverse lookup 1.4/req; mobile phone finder 0.4/req (use only after a high-confidence match); export/email-finder 0.1/email; people analysis 0.3/req; polling, statistics, results = free.
Async (export_people / find_emails): launch -> trackId -> poll *_statistics until state:"DONE" -> fetch *_results (paginated).
find_emails trackId is single-use, expires 6h after the people_search that generated it.
Pagination: zero-based page + size (search/export use JSON body; results browsing uses query params).
Errors: 409 = you fetched results while the async job is still running (poll statistics first); 404 = profile not found; 429 = rate limit (resets every 60s; limits 5/s, 300/min, 18k/hr).
Constraints: export max 10,000 results; webhooks auto-retry up to 30x (respond 200 immediately, HTTPS only).
lib/aiark.pyImport: from lib.aiark import AiArk → instantiate AiArk() (reads key from env). Base: https://api.ai-ark.com/api/developer-portal/v1. Generic passthrough: request(method, path, *, params, json).
company_search(payload: dict) -> dict — POST /v1/companies — company search (CONFIRMED). Body: filters + page (0-based) + size (<=100).email_finder_results(track_id: str, *, page: int | None = None, size: int | None = None) -> dict — GET /v1/people/email-finder/{trackId}/inquiries — paginated resultsemail_finder_statistics(track_id: str) -> dict — GET /v1/people/email-finder/{trackId}/statistics — job status (free, CONFIRMED).export_people(payload: dict) -> dict — POST /v1/people/export — async export (filters + page + size 1..10000 + webhook).export_results(track_id: str, *, page: int | None = None, size: int | None = None) -> dict — GET /v1/people/export/{trackId}/inquiries — paginated export resultsexport_single(payload: dict) -> dict — POST /v1/people/export/single — export one person with email by id OR url (CONFIRMED).export_statistics(track_id: str) -> dict — GET /v1/people/export/{trackId}/statistics — export job status (free, CONFIRMED).fetch_credit() -> dict — GET /v1/payments/credits — remaining credits (read-only, safe pilot; CONFIRMED).find_emails(payload: dict) -> dict — POST /v1/people/email-finder — async email finder from a prior people_search trackIdmobile_phone_finder(payload: dict) -> dict — POST /v1/people/mobile-phone-finder — mobile by linkedin OR domain+name (+type).people_analysis(payload: dict) -> dict — POST /v1/people/analysis — personality analysis from a LinkedIn profile url (CONFIRMED).people_search(payload: dict) -> dict — POST /v1/people — people search (CONFIRMED path). Strict nested filter structure:reverse_lookup(payload: dict) -> dict — POST /v1/people/reverse-lookup — look up a person by email/phone via search (CONFIRMED).save_list(payload: dict) -> dict — POST /v1/lists — create/update a list (up to 10k items) for use in the people_search