Launch special: 50% off paid Self-Serve plans, stacking with 20% off annual billing.

Developers

HuntSales API

A REST API over the same People Search and credit system as the app, plus an MCP server for AI agents. Search people, look up their work email, check your balance and manage your webhook endpoints from your own tools, and read and change your companies, deals, tasks, notes and Suppression List. Contacts, lists and campaigns are available over MCP, not REST.

Authentication

Create a key in HuntSales under Admin, then Developer (shown once, copy it then). Any member whose role includes the API Keys & MCP permission can create their own key; a key can never do more than its owner's role allows, and follows the role if it changes. A new key cannot launch campaigns unless you tick Allow this key to launch campaigns when you make it. Send it as a bearer token, or in an X-API-Key header. Base URL: https://huntsales.io/api/v1 (webhook endpoints live at /api/webhook-endpoints, below). Each key may make 120 requests a minute to the REST API, and separately 120 a minute over MCP; past that the answer is 429 with a Retry-After header, the seconds to wait.

Reads that return contacts also count toward a daily limit for each member, shared by the REST API, MCP and connected AI apps. The plan sets it: Free 200, Scout 1,000, Hunter 5,000 and Apex 20,000 contacts a day per member. All of a workspace's workspace keys share one limit of that size between them, so more keys never read more. It resets at midnight Singapore time. Past it, a read is refused with the time it resets. People search and collect are paid per person in credits and do not count.

curl https://huntsales.io/api/v1/credits \
  -H "Authorization: Bearer hs_live_xxxxxxxxxxxx"

Permissions & credit costs

A key inherits its owner's role: people search and collect need the People Search permission on your role (a 403 tells you which permission is missing), managing webhook endpoints needs a Super Admin's key, and the credit balance is readable by every member's key. API calls draw down the same People Search credits as the app. Top-up packs run S$0.050 to S$0.040 per credit depending on pack size, and the credits included with Hunter and Apex work out cheaper still:

ActionScopeCredits
People search (per page of 50)people:searchUp to 5 (0.1 per person)
Collect (per person)people:collect2 with an email, 0 without
Credit balancecredits:readfree
Webhook endpoints and deliverieswebhooks:managefree

Search returns a 402 when your balance can't cover a single result, and a shorter page when it can't cover a full one. Collect returns a 402 when your balance can't cover the most the call could cost (2 per person), and settles to the people who come with an email. Phone numbers are not available, and nothing is charged for phone.

get/api/v1/credits

Returns your org's HuntSales credit balance.

Scope: credits:read · Cost: free

{ "balance": 1240, "plan_remaining": 1000, "topup_balance": 240 }
post/api/v1/people/search

Search B2B professionals. Returns up to 50 preview rows per page with no email or phone; the work email is revealed by collect. Each row's people_id is the person's LinkedIn profile URL. A page costs up to 5 credits, charged at 0.1 per person returned. A page fetched in the last 7 days is served from cache at no cost, and an empty page is fully refunded. Page on has_more, which stays true for as long as there are more people to return. Pages come in order, so request page 1, then 2, and so on, and every page is charged: ask for the pages you need rather than walking a broad search to its end. Asking for a page you have not reached yet is refused and charged nothing. Paging stops at page 500, a guard against a runaway client rather than a limit on the search. total_results is an estimate kept for compatibility.

Scope: people:search · Cost: up to 5 credits per page

curl -X POST https://huntsales.io/api/v1/people/search \
  -H "Authorization: Bearer hs_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "job_title": ["founder","ceo"], "country_list": ["Singapore"], "page": 1 }'
{ "results": [ { "people_id": "https://www.linkedin.com/in/...", "full_name": "...", "experience": [...] } ],
  "total_results": 482, "has_more": true, "page": 1, "per_page": 50, "cached": false,
  "credits_remaining": 1235, "credits_charged": 5 }

To search by city, send city_list, a list of city names such as ["Kuala Lumpur", "Petaling Jaya"]. Short forms such as KL, PJ or HCMC are read as the city. A city is searched instead of its whole country. If you also send country_list, each city narrows the country it is in, a country with no city in it is searched whole, only people in those countries are returned, and a known city in a country you did not send is not searched. Each row carries city and address when the person's address names a city.

To filter by the industry of the company a person works at, send industries, a list of group ids: accounting (Accounting and audit), banking_finance (Banking and finance), construction (Construction and engineering), education (Education and training), energy (Energy and utilities), fnb (F&B), government (Government and public sector), healthcare (Healthcare and pharma), hospitality (Hospitality and travel), insurance (Insurance), legal (Legal), logistics (Logistics and transport), manufacturing (Manufacturing), marketing (Marketing and advertising), media (Media and entertainment), non_profit (Non-profit), professional_services (Professional services and consulting), real_estate (Real estate), recruitment (Recruitment and HR services), retail (Retail and e-commerce), technology (Technology and software), wholesale (Wholesale and trading). Industry is sent with the search, so the count is exact and a page is full. A person whose company has no industry on record is left out.

To filter by company size, send headcount_bands, a list of { "min": 51, "max": 200 } bands (or headcount_gt and headcount_lt for one band). One band is sent with the search: the count is exact, and a person whose company size is unknown is left out. Several bands are searched as one range and each result is checked against them, so the count is an upper bound and a page can hold fewer than 50.

Send recently_changed_jobs: true for people who joined their current company in about the last year. It is counted in whole years, so it is about a year, not a set number of months.

job_function takes a list of: accounting / finance, administrative, consulting, customer experience, design, education, engineering, human resources, it / product, legal / compliance, marketing / communications, medical & healthcare, operations, product, purchase / procurement, quality assurance, recruitment, research & development, sales. it / product finds IT titles only. Send product as well for product titles such as Product Manager.

The filters above are the ones the API takes, with person_name, company_keyword, job_seniority and page. The app's other People Search filters, such as keywords, skills, past_company_names, schools and revenue_gt, are refused with a 400 (unsupported_filters, naming them) before anything is charged.

post/api/v1/people/collect

Add selected people_ids (max 50) to your CRM. 2 credits per person added with an email. No email, no charge. A people_id is the LinkedIn profile URL returned by search; anyone already in your CRM is skipped free, as is an id we cannot match. An email our database already holds is delivered with the contact and charged in credits_charged; if it does not pass its deliverability check, those credits come back and, unless reveal_email is false, we look the person up once more, charged 2 only if a new email is found. Otherwise the included work email lookup runs in the background: the call returns 202 with a job id per lookup, which you poll until the address lands on the contact. Each lookup holds 2 credits (credits_held) and keeps them only if it finds an address; a lookup that finds nothing hands them all back. Any email that comes with a contact, now or when the lookup finds one later, gets a deliverability check at no extra cost, with the verdict on the contact shortly after. Phone numbers are not available, and nothing is charged for phone.

Scope: people:collect · Cost: 2 / contact added with an email

curl -X POST https://huntsales.io/api/v1/people/collect \
  -H "Authorization: Bearer hs_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "people_ids": ["https://www.linkedin.com/in/..."], "reveal_email": true }'
{ "status": "pending",
  "job_ids": ["9f3c1a80-..."],
  "results": [ { "people_id": "https://www.linkedin.com/in/...", "contact_id": "d41f...", "job_id": "9f3c1a80-...", "status": "pending" } ],
  "contacts_added": 1, "credits_charged": 0, "credits_held": 2,
  "warnings": ["phone_reveals_unavailable"],
  "credits_remaining": 1238 }

Collect answers 202. Each person comes back with a contact_id and a status: added when the contact landed (with email when one came with it), pending when the included email lookup is still running (poll its job_id below), or skipped when they were already in your CRM, there is no matching profile, or the person asked not to be in our data. Skips are free.

get/api/v1/people/collect?job_id=

Poll one submitted email lookup. Returns the found address once it lands (it is written onto the contact, whose deliverability check then runs at no extra cost), or pending while it is still running. credits_charged is what the lookup settled to: 2 when it found an address, 0 when it found none. email_domain_mismatch is true when the address is at a domain that does not match the company it was looked up by: it is still delivered and charged as found, with a note on the contact saying so.

Scope: people:collect · Cost: free

curl "https://huntsales.io/api/v1/people/collect?job_id=9f3c1a80-..." \
  -H "Authorization: Bearer hs_live_xxx"
{ "job_id": "9f3c1a80-...", "status": "found",
  "email": "...", "email_status": "unknown",
  "email_domain_mismatch": false, "credits_charged": 2 }

Companies, deals, tasks, notes and the Suppression List

The same records the app shows, and the same tools MCP offers. Each call is gated exactly as its page in the app is for the key's owner, no more and no less, and every write is recorded in the activity log against the key. One rule is deliberately stricter than the app: the API never reads or changes anything on a contact you cannot open yourself, so a task's contact must be one you may work and notes need a contact you can open. Free: no credits.

GET /api/v1/companies?search=&owner_id=&limit= contacts:read lists companies, newest first, as the Companies page does (needs View All Contacts). search matches part of the name or location and needs at least 3 characters. The answer is { companies, has_more }.

GET /api/v1/companies/{id} contacts:read one company, as its panel shows it. Another member's company in a workspace that keeps prospects to their assignee shows only its name and owner, unless you hold people there.

GET /api/v1/deals?status=&contact_id=&company_id=&limit= contacts:read logged deals: the ones you logged, or every deal with View All Deals.

POST /api/v1/deals contacts:write logs a deal from { deal_size, status?, contact_id?, company_id?, close_date?, comments? }. The contact must be one you may work. Fires Deal created, and Deal won when logged as won.

PATCH /api/v1/deals/{id} contacts:write changes a deal you may change. A move to won fires Deal won.

DELETE /api/v1/deals/{id}?confirm=true contacts:write deletes a deal you may change.

GET /api/v1/tasks?status=open|completed|all&overdue_only=&limit= contacts:read the tasks you created, are assigned or watch.

POST /api/v1/tasks contacts:write creates a task from { title, description?, due_date?, priority?, assigned_to?, contact_id?, company_id? } (needs Tasks). assigned_to takes a teammate's id or email; contact_id must be a contact you may work.

PATCH /api/v1/tasks/{id} contacts:write changes a task you created, are assigned or watch (needs Tasks, except moving your own assigned task through its statuses).

DELETE /api/v1/tasks/{id}?confirm=true contacts:write deletes a task (needs Tasks).

GET /api/v1/contacts/{id}/notes?limit= contacts:read the notes on a contact you can open, newest first.

POST /api/v1/contacts/{id}/notes contacts:write adds a note from { text } to a contact you can open.

GET /api/v1/suppression?search=&reason=&page=&limit= contacts:read the Suppression List, a page at a time (needs Campaigns & Email Setup).

POST /api/v1/suppression/check contacts:read whether each of up to 50 { emails } is blocked, and why.

POST /api/v1/suppression contacts:write adds up to 100 { emails, note?, confirm }. A Super Admin's key adds them directly; anyone else's goes to the Super Admin for approval on Edit Requests.

DELETE /api/v1/suppression contacts:write removes { ids, confirm }, the ids list_suppression returns. Only a Super Admin's key removes an entry that makes someone Do Not Call.

Deleting a deal or a task, and adding to or removing from the Suppression List, answer with a preview (dry_run) and change nothing until you send confirm: true. A refusal is a 403 naming the permission to ask for, and a record you may not see is a 404, as if it did not exist. Each address the Suppression List returns counts toward your daily read limit; past it the answer is 429 with code: "daily_read_limit" and a Retry-After header.

curl -X POST https://huntsales.io/api/v1/deals \
  -H "Authorization: Bearer hs_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "deal_size": 12000, "status": "open", "contact_id": "d41f...", "close_date": "2026-11-30" }'
{ "deal": { "id": "7c2e...", "status": "open", "deal_size": 12000,
    "close_date": "2026-11-30", "contact_id": "d41f...", "logged_by_you": true } }

Webhook endpoints

Register a URL of your own and HuntSales POSTs to it when an event happens. Managing endpoints over the API lets a workflow register its own endpoint, as the n8n trigger node does. Needs a key with webhooks:manage. Free.

In the app only a superadmin manages webhooks, under Admin, then Developer, and the API follows the same rule. Only a Super Admin's key manages webhook endpoints, every endpoint in the workspace; any other key gets a 403. An endpoint a key creates is saved as its owner's.

GET /api/webhook-endpoints lists your endpoints (id, url, events, active, last status and last delivery time) and every event you can subscribe to.

POST /api/webhook-endpoints creates one from { "url", "events" }. The URL must be a public https address, with at least one event. It answers 201 with the endpoint and its signing secret, shown only this once.

DELETE /api/webhook-endpoints?id= removes an endpoint.

GET /api/webhook-endpoints/deliveries?endpoint_id= lists an endpoint's newest 50 deliveries: event, status code, attempt, error and next retry time.

curl -X POST https://huntsales.io/api/webhook-endpoints \
  -H "Authorization: Bearer hs_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/huntsales", "events": ["reply.received", "deal.won"] }'
{ "endpoint": { "id": "5b1e...", "url": "https://example.com/hooks/huntsales",
    "events": ["reply.received", "deal.won"], "active": true, "created_at": "..." },
  "secret": "whsec_..." }

Each delivery is a JSON body { "event", "created_at", "data" } with an X-HuntSales-Event header, an X-HuntSales-Delivery header with the delivery's id, and an X-HuntSales-Signature header of the form sha256= followed by the hex HMAC-SHA256 of the exact body under your secret. Every event is written down before it is sent. A delivery is tried up to 10 times in total: when your endpoint cannot be reached, takes longer than 8 seconds, or answers 429 or a 5xx error, it is retried with growing gaps, from a minute to 8 hours, about a day in all. A redirect is not followed and not retried: the delivery fails at once, and its log says so. Every try sends the same body, byte for byte, and the same delivery id, so skip one you already have. An endpoint whose deliveries keep failing for about a day (23 hours and at least 5 tries) is turned off and the Super Admins are emailed. Read back anything that must not be missed. Adding an endpoint emails the workspace's Super Admins, and adding or deleting one is in the activity log. An endpoint a key adds stops when that key is revoked: it is turned off and the Super Admins are emailed. A Super Admin can turn it back on in the app, after confirming that it then keeps sending as the workspace's own. It also stops sending, without being turned off, while the key's owner is no longer an active Super Admin (their role changed, or they are paused); those events are not kept, and it sends again once the owner is a Super Admin again or is resumed.

To send events into HuntSales (from Zapier, Make, n8n, a form or your own system), give an automation an Incoming webhook trigger and POST JSON to the URL it shows, https://huntsales.io/api/automation-hooks/{token}. It needs no API key: the token is the secret, so keep the URL private. Each automation takes up to 60 starts a minute. A body is at most 64 KB and must be JSON (a larger one gets 413, one that is not JSON 400), and {{ }} in it is removed before any step reads it. Once the workspace's monthly automation run allowance is used up, a call gets 429 and starts nothing.

MCP (connect your AI)

HuntSales runs a hosted MCP server at https://huntsales.io/api/mcp, authenticated with your API key, so AI agents can work with your CRM. We have tested Claude Code, which connects to it directly with an Authorization: Bearer header, and Claude Desktop on Mac, which reaches it through the mcp-remote bridge, with the key written directly in the header argument. The Claude Desktop config below puts the key in env instead, because Claude Desktop on Windows breaks an argument that contains a space. That config, and the MCP guide's configs for Cursor and VS Code, follow each vendor's documentation and are not yet tested by us:

{
  "mcpServers": {
    "huntsales": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote", "https://huntsales.io/api/mcp",
        "--header", "Authorization:${AUTH_HEADER}"
      ],
      "env": {
        "AUTH_HEADER": "Bearer hs_live_xxxxxxxxxxxx"
      }
    }
  }
}

The MCP guide has the setup for each client. claude.ai, the Claude mobile app and ChatGPT connect by OAuth sign-in instead of a key: add a custom connector with the URL https://huntsales.io/api/mcp and sign in to HuntSales.

Every tool is limited to your own organisation by your API key and can do only what your role allows. MCP has no people search or email lookup tools. Read tools never change data. The tools that add prospects, build a list from a filter, enrol a list or launch a campaign (claim_prospects, create_list_from_filter, enroll_list, launch_campaign) are dry-run by default: they return a preview, and only execute when you pass confirm: true. So is enroll_contacts when the campaign is live (active or scheduled). See the MCP guide for all 37 tools.

Read

  • get_credits - your credit balance and plan remaining.
  • query_contacts - search your CRM contacts by name, email, or job title, optionally filtered by pipeline stage.
  • get_pipeline_summary - a count of your contacts in each pipeline stage.
  • count_prospects - count prospects that match a profile (job title, country, industry) and are not yet in your CRM, among the people in our shared contact database, which covers part of the market, not all of it. Returns matched; the count stops at 10,000, and matched_capped true means more than that match.
  • get_call_stats - calls for any period, in total and per agent: placed, received, answered, talk time, outcomes and appointments set. Needs Calling or See all calls on the key owner's role.
  • list_campaigns - your campaigns with their id, status, and sent and replied counts.
  • get_campaign_engagement - a campaign's sent, delivered, opened, clicked and replied counts and rates, plus contact progress.

Write

  • claim_prospects - add prospects that match a profile into your CRM (spends 2 People Search credits per contact added with an email, mailbox check included; no email, no charge; capped at 500; dry-run by default).
  • create_contact - create a CRM contact (stage defaults to Prospect).
  • update_contact_stage - move a contact to a different pipeline stage, with an optional note.
  • create_list - create an empty named contact list.
  • add_contacts_to_list - add contacts to one of your lists by contact id (capped at 2,000).
  • create_campaign - create a draft email campaign with its sequence steps (never sent automatically).
  • enroll_contacts - enrol contacts, by ids or a list, into a campaign as its audience (dry-run by default on a live campaign).
  • launch_campaign - start sending a draft campaign (irreversible; dry-run by default).

Each key may make 120 requests a minute to the REST API and, separately, 120 a minute over MCP. Outbound webhooks are also set up under Admin, then Developer (and via the n8n trigger node; see the n8n guide). Questions? Contact support from your dashboard.