Explee Public API

Version: 1.0.0

> Tip: This documentation is designed for humans. To integrate with AI agents, provide them with the OpenAPI schema at https://api.explee.com/public/api/openapi.json instead.   Search millions of companies worldwide using natural language queries and structured filters! Rate limit: 10000 requests per hour, 150 concurrent requests per organization. Timeout: 90s.

Authentication

ApiKeyAuth: apiKey - Get your API key at [API Keys](https://explee.com/app-auto-gtm/api-keys)

Endpoints

POST /public/api/v1/search/companies

Companies

Search for companies using structured definitions. ## How It Works Provide a structured definition of your target companies in the `definition` field — our AI will find matching companies. All other filters are optional. See the request schema below for the full list of available filters. **Definition examples:** - `"YouTube video summarization tool"` - `"electronic signature platform"` - `"AI B2B SaaS"` - `"real estate company"` ## AI Enrichment Use the `criteria` parameter to score companies against custom criteria. Each company will be evaluated and scored (0-5) with reasoning for each criterion. ## Pricing - **First 10 results are free** (including AI enrichment) - A **positive credit balance is required for every request**, free results included (an organization at 0 credits gets a 402) - Free results are limited per organization by plan: **free 50/day + 10/min · starter 200/day + 15/min · growth 500/day + 20/min · pro 750/day + 25/min · agency 1500/day + 30/min · enterprise 3000/day + 30/min** requests that include free results. An entirely-free request beyond the limit gets 429; a request with `page_size` above the free zone is never rejected, but once the day's limit is spent its whole page is billed - **0.5 credits** per company after the first 10 - **+0.1 credits** per company per criterion after the first 10 Example: 150 companies with 2 criteria = 50 billable × (0.5 + 0.1 × 2) = 35 credits (first 10 are free) Empty results = 0 credits. ## Deduplication Pass dedup list ids in `exclude_lists` to skip companies you already own — they are not returned and not charged. Free results zone does not apply. See the Deduplication endpoints.

Request Body

Request body required

Responses

POST /public/api/v1/search/companies-by-domains

Companies by Domains

Find companies by their domains. **How it works:** 1. Provide a list of company domains (up to 1000) 2. The system looks up each domain and returns **at most one** company profile per domain (the best match by quality score) **Pricing:** - **0.5 credit(s)** per company found - **0 credits** for domains not found **Limits:** - Maximum 1000 domains per request - Maximum 10000 companies per response **Deduplication:** - Pass dedup list ids in `exclude_lists` — matched companies are not returned and not charged

Request Body

Request body required

Responses

POST /public/api/v1/search/people

People

Search for people (employees) at companies matching your criteria. ## How It Works Provide `job_titles` in `people_filters` to find people by their role. Use `company_filters.definition` to narrow down which companies to search. Use `company_linkedin_ids` to restrict results to a specific list of company LinkedIn IDs. **Job title examples:** - `["Head of Sales", "VP Sales"]` - `["CTO", "VP Engineering"]` - `["Founder", "CEO"]` ## AI Enrichment Use the `criteria` parameter in `people_filters` to score people against custom criteria. Each person will be evaluated and scored (0-5) with reasoning for each criterion. ## Pricing - **First 10 results are free** (including AI enrichment) - A **positive credit balance is required for every request**, free results included (an organization at 0 credits gets a 402) - Free results are limited per organization by plan: **free 50/day + 10/min · starter 200/day + 15/min · growth 500/day + 20/min · pro 750/day + 25/min · agency 1500/day + 30/min · enterprise 3000/day + 30/min** requests that include free results. An entirely-free request beyond the limit gets 429; a request with `page_size` above the free zone is never rejected, but once the day's limit is spent its whole page is billed - **1.0 credit** per person after the first 10 - **+0.1 credits** per person per criterion after the first 10 Example: 150 people with 2 criteria = 50 billable × (1.0 + 0.1 × 2) = 60 credits (first 10 are free) Empty results = 0 credits. ## Deduplication Pass dedup list ids in `exclude_lists` to skip people you already own — they are not returned and not charged. A companies list excludes everyone at its domains. Free results zone does not apply. See the Deduplication endpoints.

Request Body

Request body required

Responses

POST /public/api/v1/search/people-by-domains

People by Domains

Find people at companies by their domains. **How it works:** 1. Provide a list of company domains (up to 1000) and job titles to search for 2. The system finds companies by domains 3. Returns up to `people_per_company` people per found company with matching job titles **Job title matching:** - Uses semantic search: "Head of Sales" will match "VP Sales", "Sales Director", etc. - Multiple job titles are combined with OR logic **Parameters:** - `people_per_company`: Number of people to return per company (1-1000, default: 1) **Limits:** - Maximum 1000 domains per request - Maximum 20 job titles per request - Maximum 1000 people per company - Maximum 10000 people per response (total) **Billing:** - 1 credit per person returned **Deduplication:** - Pass dedup list ids in `exclude_lists` — matched people are not returned and not charged

Request Body

Request body required

Responses

POST /public/api/v1/search/nl-to-filters

NL to Filters

Convert a free-form natural language query into structured filters for companies and people search. ## How It Works Send any query in plain English (or another language), for example: - `"SaaS companies using Stripe"` - `"Founders of AI startups in Germany"` - `"B2B fintech companies in US and Canada"` The endpoint returns parsed filters in the same shape used by search endpoints: - `companies_filters` - `people_filters` - `focus` (`companies` or `people`) This endpoint does not perform a search and does not charge credits.

Request Body

Request body required

Responses

POST /public/api/v1/enrich/email

Find email by name

Find email address for a single person. ## How It Works Provide the person's first name, last name, and company domain. ## Presets | Preset | Cost | Success Rate | Description | |--------|------|--------------|-------------| | **basic** | 1.5 credits | ~50% | Single provider, pattern-based lookup. Fast and cheap. | | **premium** | 5.0 credits | ~78% | 6 providers with cross-validation. Higher accuracy. | ## Pricing You are only charged when an email is found. Email not found = 0 credits.

Request Body

Request body required

Responses

POST /public/api/v1/enrich/phone

Find phone by LinkedIn URL

Find the work phone number for a single person. ## How It Works Provide the person's LinkedIn profile URL (`linkedin_url`) and we'll find their work phone. You can also pass a known `email` if you have one. ## Presets | Preset | Cost | Success Rate | Description | |--------|------|--------------|-------------| | **basic_new** | 15.0 credits | ~45% | Faster, lower-cost lookup. | | **premium** | 30.0 credits | ~60% | Broader, multi-source lookup. Higher hit rate. | ## Pricing You are only charged when a phone is found. Phone not found = 0 credits.

Request Body

Request body required

Responses

POST /public/api/v1/enrich/email/batch

Find email by name (batch request)

Find email addresses for multiple contacts in one request (async). **How it works:** 1. Submit up to 100 contacts with first_name, last_name, company_domain 2. Receive a `task_id` immediately 3. Poll GET endpoint with `task_id` to check status and retrieve results **Limits:** - Maximum 100 contacts per batch **Pricing:** - **basic**: 1.5 credits per found email - **premium**: 5 credits per found email - Only charged for emails found (not found = 0 credits)

Request Body

Request body required

Responses

GET /public/api/v1/enrich/email/batch/{task_id}

Find email by name (batch results)

Check status and retrieve results for a batch request. **Statuses (in meta.status):** - `pending` — processing in progress, poll again in a few seconds - `completed` — results ready, `contacts` array contains enriched data - `failed` — error occurred, see `meta.error` for details **Response structure:** - `contacts` — null while pending, array when completed - `meta.status` — current processing status - `meta.error` — error description (null if no error) - `meta.credits_charged` — credits used (only charged for found emails) **Typical workflow:** 1. POST to submit batch, get `task_id` 2. GET with `task_id`, check `meta.status` 3. If `pending`, wait 2-5 seconds and poll again 4. When `completed`, read `contacts` array

Parameters

Responses

POST /public/api/v1/find-and-enrich

Find people and enrich their emails (async)

Search for people matching your filters and enrich their emails in a single async job. **How it works:** 1. Submit search filters + `max_contacts` (how many contacts with emails you want, max 500 per call) 2. Receive a `task_id` immediately 3. Poll the GET endpoint with `task_id` for `progress` (with `eta_seconds`) and results 4. For more than `max_contacts`, call again with the previous response's `next_cursor` **Pricing:** - **Search is free** — you are never charged for people searched. - **basic**: 1.5 credits per found email · **premium**: 5 credits per found email. - You are charged ONLY for emails actually found. Worst-case credits are held up front (402 if insufficient). **Deduplication:** pass `exclude_lists` to skip people you already own — they are not enriched and not charged. A companies list skips everyone at its domains.

Request Body

Request body required

Responses

GET /public/api/v1/find-and-enrich/{task_id}

Find-and-enrich job status and results

Check progress and retrieve results for a find-and-enrich job. **Statuses (in meta.status):** - `pending` — running; read `meta.progress` (incl. `eta_seconds`) and poll again - `completed` — results ready; `contacts` holds the enriched contacts, `meta.next_cursor`/`meta.has_more` for paging - `failed` — error occurred (or timed out — credits released), see `meta.error` Only contacts with a found email are returned. `meta.credits_charged` reflects found emails only.

Parameters

Responses

GET /public/api/v1/tasks

List async tasks

List all async tasks for your organization, including find-and-enrich jobs, batch email enrichments, and campaign lead imports. **Use cases:** - Track async jobs across every API key owned by the same organization - Find task IDs you may have lost - Monitor pending vs completed tasks **Response:** - `tasks` — array of task objects with status and result URL - `total` — total count for pagination **Filtering:** - Use `status` parameter to filter by task status (pending, completed, failed)

Parameters

Responses

GET /public/api/v1/billing/balance

Get balance

Return your organization's net credit balance — the same number shown on the Explee dashboard. Poll this to decide when to top up — when `remain` drops below the threshold you need for upcoming requests, add credits on your billing page (https://explee.com/app-auto-gtm/billing). ## Response ```json { "remain": 4200 } ``` `remain` is the net balance in credits (1 credit = $0.01): prepaid funds minus any active postpaid (outreach) usage that hasn't been collected yet. It can be **negative** if the org is carrying postpaid debt.

Responses

GET /public/api/v1/autogtm/projects

List outreach projects

List the outreach projects owned by your organization. Authenticated by the same `X-API-Key` as the rest of this API — the key resolves to your organization, and its active projects are returned newest first. **Response:** - `projects` — active projects with `id`, `domain` (the project's identity in the app), and `daily_budget_usd` (the project-wide daily budget anchor, `null` when not set). - `total` — number of projects returned. Use a project `id` to filter the campaigns list (`GET /public/api/v1/autogtm/campaigns?project_id=…`).

Responses

POST /public/api/v1/autogtm/projects

Create outreach project

Create an outreach project — the app's **New project** dialog in one call. For an agency: one project per client. Send **either** the company `domain` **or**, for a company with no website, `no_website` with the dialog's three answers (`business` — what it sells, `customer` — the ideal customer, `geo` — the markets). The call answers at once with the project's `id` and `status: creating`. Explee then analyses the company (about 1-2 minutes) and adds the project's suggested campaigns. Poll `GET /public/api/v1/autogtm/projects/{project_id}`: - `creating` — still working; poll again in 15-30 seconds; - `ready` — the campaigns are there (`GET /public/api/v1/autogtm/campaigns?project_id=…`), each in `discovery`; - `failed` — nothing to build from (`failure_reason`); the domain is free to create the project again. Nothing sends after this call. Start a campaign with `POST /public/api/v1/autogtm/campaigns/{campaign_id}/start`, or add your own with `POST /public/api/v1/autogtm/campaigns`. Sending stays within the project's daily budget (`daily_budget_usd`, change it with `PATCH /public/api/v1/autogtm/projects/{project_id}/budget`). All projects spend your organization's one balance; `GET /public/api/v1/autogtm/billing/usage` splits the spend by `project_id`. Available to organizations that have paid at least once (a top-up or a plan). **Errors** (`code` in the body): - `payment_required` (402) — your organization has never paid; top up first. - `project_exists` (409) — your organization already has a project on this domain; `detail` names its id. A retry after a timeout lands here, so it is safe to repeat a `domain` call. A `no_website` call is not deduplicated: after a timeout, list your projects before repeating it. - `domain_unavailable` (409) — the domain can't host a project: a free-mail, example or public-company domain, a domain several other organizations already use, or one Explee does not serve. Contact support if it is your client's real domain. - `creation_in_progress` (429) — 5 projects of your organization are still `creating`; wait until one is `ready` or `failed`. - `analysis_unavailable` (503) — the analysis could not start; retry in a minute. Optional `sender_name` sets the name first emails go out under, with the checks and errors of `PUT /public/api/v1/autogtm/projects/{project_id}/sender-name` (`sender_name_invalid`, `sender_name_rejected`, `sender_name_not_in_plan`, `sender_name_check_unavailable`). It is checked before anything is created: a refused name creates no project. Free. At most 20 calls per hour per organization, refusals included.

Request Body

Request body required

Responses

GET /public/api/v1/autogtm/projects/{project_id}

Get one outreach project

One active project of your organization — the same fields as an item of `GET /public/api/v1/autogtm/projects`.

Parameters

Responses

GET /public/api/v1/autogtm/projects/{project_id}/sender-name

Get the project's sender name

The project's sender name: the From name its first emails go out under. An empty `sender_name` is Automatic: each mailbox sends under its own name.

Parameters

Responses

PUT /public/api/v1/autogtm/projects/{project_id}/sender-name

Set the project's sender name

Set the project's sender name — the app's **Emails → Sender name**. For an agency: put the name of the person at your client who signs the emails. Send `{"sender_name": "Maria Lopez"}`; `{"sender_name": ""}` goes back to Automatic (each mailbox sends under its own name). Spaces are collapsed. Saving the same name again changes nothing. **Where the name applies.** Emails go out from Explee's shared mailboxes. A mailbox takes your name only when its address fits the name (e.g. `m@…` for Maria Lopez); the others send under their own name, and so do some emails in Explee's sender-name tests. A conversation keeps the name of its first email: a new name applies to new conversations only. Mailboxes bought for your own domain send under the names chosen in the order. **Errors** (`code` in the body): - `sender_name_invalid` (422) — over 100 characters, an email address or a control character in it; - `sender_name_rejected` (422) — not a real person's first and last name: a company, brand, team or role, a famous person, a placeholder. `detail` says which; nothing is saved; - `sender_name_not_in_plan` (403) — the Starter plan has no custom sender name; clearing it still works; - `sender_name_check_unavailable` (503) — the name could not be checked right now; retry after `Retry-After` seconds. Free. At most 20 saves per 10 minutes per organization.

Parameters

Request Body

Request body required

Responses

GET /public/api/v1/autogtm/projects/{project_id}/monthly-cap

Get the project's monthly cap

The project's monthly cap: the most it may spend on emails in a calendar month (UTC), and what it has spent since the 1st. `monthly_cap_usd: null` = no cap.

Parameters

Responses

PUT /public/api/v1/autogtm/projects/{project_id}/monthly-cap

Set the project's monthly cap

Set or remove the project's monthly cap — the app's **Budget → Monthly cap**. Send `{"monthly_cap_usd": 2000}`; `{"monthly_cap_usd": null}` removes the cap. The month is the calendar month in UTC. Once the next email would take the month's spend past the cap, nothing sends until the 1st or until the cap is raised (the campaigns show `monthly_cap_reached`). Raising or removing the cap resumes sending within minutes. The daily budget still applies inside the cap. Range 500–1,000,000 USD. Free.

Parameters

Request Body

Request body required

Responses

GET /public/api/v1/autogtm/campaigns

List outreach campaigns

List the outreach campaigns owned by your organization. Authenticated by the same `X-API-Key` as the rest of this API — the key resolves to your organization, and campaigns are returned across all of its projects. Pass `project_id` (see `GET /public/api/v1/autogtm/projects`) to get one project's campaigns only. **Response:** - `campaigns` — campaigns with `id`, `project_id`, `name`, `target_url`, `daily_limit_usd`, and lifecycle `status`. Archived campaigns are left out unless you pass `include_archived=true`. - `total` — number of campaigns returned. Use a campaign `id` to scope follow-up management calls (inbox replies, budget, start/stop, analytics).

Parameters

Responses

POST /public/api/v1/autogtm/campaigns

Create campaign

Create a campaign that Explee finds the leads for — the app's **New campaign** button. Only `project_id` and `name` are required. Explee drafts every targeting field you leave out (offer, customer problem, criteria, example clients) from the name and your project; a field you send replaces the draft (`[]` means "none"). Search `keywords` are always drafted from the name. Geography and company size default to the project's. Each call creates a new campaign — unless you retry it with the same `Idempotency-Key` (below): then you get the campaign the first call created. **Lifecycle after this call** (`status` in `GET /public/api/v1/autogtm/campaigns`): 1. `discovery` — created, nothing runs yet. Review or edit it with `GET` / `PATCH /public/api/v1/autogtm/campaigns/{campaign_id}`. 2. `POST …/campaigns/{campaign_id}/start` — Explee searches for leads. Works with Auto-manage campaigns on or off. 3. `review` — the lead list is ready. Approve it with `POST …/campaigns/{campaign_id}/approve`. With Auto-manage campaigns ON it is also approved automatically at the end of the project's review window (12 hours by default); with it OFF only `approve` sends it. 4. `outreach` — sending, within the campaign and project budgets. `POST …/stop` at any point parks the campaign (`listening`); a later `start` resumes it — from a stopped review that means sending without a further approve. Creation is free. Returns the campaign's full definition (on a repeat with the same `Idempotency-Key`: that campaign's definition as it is now). **Safe retries:** send an `Idempotency-Key` header (any unique string, e.g. a UUID) and repeat the exact same request with the same key if you don't get an answer. Within 24 hours a repeat returns what the first request created (the same task, list or campaign) and does nothing again. The same key with a different body is rejected (`422`, `idempotency_key_reused`); a repeat that arrives while the first request is still running gets `409` `idempotency_key_in_use` — wait `Retry-After` seconds and repeat. A key replays its first answer even if that import later `failed` — to retry a failed import, send a new key.

Parameters

Request Body

Request body required

Responses

GET /public/api/v1/autogtm/campaigns/{campaign_id}

Get campaign definition

Read everything that decides who a campaign contacts and what it says — the app's **Campaign** tab in one call. **Who it targets:** `offer`, `customer_problem`, `target_role`, `target_geography`, `target_company_size`, `positive_criteria`, `negative_criteria`, `example_clients`, and the derived `keywords`. **What it says:** `instructions` (first email), `followup_instructions` (shared by every follow-up), `language`, and the project-level `reply_instructions`. **When it says it:** `schedule` (the send days) and `followups` (how many emails the sequence has and how far apart — a project-level setting, the same for every campaign of the project). `targeting_editable` is `false` for a campaign built from your own imported lead list — its audience is that list, so the targeting fields are locked. Free, and the natural first call when auditing an account: one request per campaign tells you whether it is aimed at the right people, before a single email goes out.

Parameters

Responses

PATCH /public/api/v1/autogtm/campaigns/{campaign_id}

Update campaign definition

Correct a campaign's definition — send only the fields you are changing. An omitted field keeps its value; an empty string, empty list or `null` clears it — except `name`, which a campaign always has, so clearing it is a `422`. An unknown field name is rejected with `422` rather than silently ignored, so a typo can never look like a successful write. Returns the full definition as it now stands. **Re-aiming costs leads.** Every field Explee searches with re-aims the campaign, exactly as in the app: `name`, `offer`, `customer_problem`, `target_role`, `target_geography`, `target_company_size`, `positive_criteria`, `negative_criteria` and `example_clients`. A real change to any of them drops the leads this campaign has not contacted yet and makes Explee search again. Sending the value a field already has is not a change. Only `language` and `schedule` change nothing about who is contacted. Emails already sent are never affected, and contacts already in a sequence keep it. **The new search is not instant.** On a running campaign it starts about 5 minutes after your *last* targeting change, so a series of edits costs one search, not one per edit. Each further targeting change restarts that wait: send all your changes, then leave the campaign alone. Until the search starts, the campaign keeps its current `status` and lead pool; when it starts, `status` turns `searching`. On a stopped campaign the new targeting applies when you start it again; on a campaign not launched yet, at launch. **A running campaign pauses for review.** When the new search finishes, a campaign that was sending moves to `review` (`status_reason` `re_search_from_outreach`) and sends nothing until its new lead list is approved: `POST /public/api/v1/autogtm/campaigns/{campaign_id}/approve`. With Auto-manage campaigns ON it is also approved automatically at the end of the project's review window (12 hours by default); with it OFF only `approve` resumes sending. **Briefs are moderated before they are saved.** `instructions` and `followup_instructions` go through the same two checks the app's email editor runs before it lets you save a brief: the brief itself is judged together with the campaign's offer (the `offer`, `customer_problem` and criteria sent in the same request, if any), and then a sample email is written from the brief for one of the campaign's leads and that email is checked too. The answer usually takes 10–30 seconds (up to about 85). A brief or sample email that does not pass is a `422` and the whole request writes nothing. If moderation cannot check it in time, the answer is a `503` (with `Retry-After`) and nothing is written either: send the same request again. At most 3 brief checks per organization run at once; a fourth is a `429` (with `Retry-After`). A passing brief applies to the emails written from now on; it does not re-aim the campaign and does not drop leads. The rule for this endpoint: **anything a person can edit on the campaign in the app, you can edit here** — with the exceptions below, each of which exists because the app validates it in a second step this endpoint cannot reproduce. Read-only here: `target_url` and `reply_instructions` (their own two-step save in the app), `keywords` (Explee derives it; read-only in the app too) and `followups` (a project-level setting shared by every campaign of the project: sending the current values back is accepted, different ones are a `422`). Budget has `PATCH /public/api/v1/autogtm/campaigns/{campaign_id}/budget`, status has `start` / `stop`.

Parameters

Request Body

Request body required

Responses

GET /public/api/v1/autogtm/campaigns/{campaign_id}/my-copy

Get campaign My copy

Read the campaign's first email setup — the two switches of the app's first-email editor: **Instructions | My copy** (`mode`) and **From project | Custom** (`source`). `mode` `instructions`: Explee writes each first email from the `instructions`. `mode` `my_copy`: your `subject` and `body` go out, with the variables filled in for each lead. Both texts are always kept, so switching loses nothing. In `my_copy` mode 90% of a campaign's leads get your text and 10% get the AI email from the saved `instructions`, so you can compare the two. Follow-ups are never affected. `source` `project`: the campaign sends the project's first email, and `mode`, `subject` and `body` are the project's (`GET /public/api/v1/autogtm/projects/{project_id}/my-copy`). Switch it with `PUT .../my-copy/source`. `stopped` is set when Explee switched the first email back to `instructions` on its own because 10% or more of your text bounced. Free.

Parameters

Responses

PATCH /public/api/v1/autogtm/campaigns/{campaign_id}/my-copy

Update campaign My copy

Turn My copy on or off for one campaign with its own first email (`source` `campaign`), or change its text — the editor's **Apply**. Send only the fields you change. `mode` `my_copy` needs a `body`: send it in the same request unless the campaign already has one. A campaign with `source` `project` sends the project's first email and is not edited here (`409` `follows_project`): change the project's (`PATCH /public/api/v1/autogtm/projects/{project_id}/my-copy`), or give the campaign its own first (`PUT .../my-copy/source`) and edit that. The save runs the app's checks before anything is written, so it takes a few seconds, up to about a minute: - **The text.** Variables are `{{first_name}}`, `{{company}}`, `{{job_title}}`, `{{company_city}}`, `{{sender_first_name}}`, `{{sender_company}}`, each optionally with a fallback: `{{company_city|your city}}`. `{a|b}` picks one variant per lead. `{{ai: instruction}}` is a line Explee writes for each lead (up to 5). No links, domains, emails, phone numbers or HTML; the subject can't start with `Re:` or `Fwd:`. The sign-off is Explee's: the mailbox persona's name is added under your text. - **Moderation.** Your text is rendered for 3 leads and checked by the same moderation as in the app. A text that fails is a `422` with `code` `my_copy_invalid`, every finding in `errors` (and `warnings`), and nothing saved: fix it and send it again. A text that passes is saved and goes live for the next first emails; findings worth fixing come back in `warnings`. Sending the text that is already live is not checked again. `409` with `code` `my_copy_changed`: the text changed while it was being checked — read it and send your change again. Two saves run at a time per organization; a third gets `429`: retry it when one finishes.

Parameters

Request Body

Request body required

Responses

PUT /public/api/v1/autogtm/campaigns/{campaign_id}/my-copy/source

Set campaign My copy source

Choose where the campaign takes its first email from — the app's **From project | Custom** switch. `project` makes the campaign send the project's first email, `campaign` gives it its own. The switch moves the whole first email, `instructions` included, and loses nothing: switching back brings the other text back. A campaign that never had a first email of its own starts from a copy of the project's (when the project has one), so what its leads get doesn't change until you edit it. Free, no checks: both texts already passed them.

Parameters

Request Body

Request body required

Responses

GET /public/api/v1/autogtm/campaigns/{campaign_id}/my-copy/preview

Preview campaign first email

See the campaign's first email the way leads get it — the app's **What leads get** panel. The email that is live now (My copy or the AI email from the instructions, the campaign's own or the project's) is rendered for the campaign's 3 preview leads, the same leads the app shows: variables, `{a|b}` and `{{ai: …}}` lines filled in, signed by the sender. A rendered email is kept until the text changes, so asking again is instant. A fresh render writes the `{{ai: …}}` lines and takes seconds per lead. Every preview call runs in the same two-at-a-time slot per organization as a save (`429` beyond). A lead that gets no email comes back with `error` instead of failing the call: `my_copy_skipped` (the lead would be skipped — an empty variable with no fallback, say), `generation_failed` (retry), `render_later` (not rendered in this call to keep it short — ask again, finished renders are kept), `no_leads_yet` (the campaign has no leads to preview on yet). Free.

Parameters

Responses

GET /public/api/v1/autogtm/projects/{project_id}/my-copy

Get project My copy

Read the project's default first email — **Project defaults** in the app's first-email editor. Every campaign in `campaign_ids` sends it. `mode` `instructions`: Explee writes each first email from the `instructions`. `mode` `my_copy`: your `subject` and `body` go out, with the variables filled in for each lead. Both texts are always kept, so switching loses nothing. In `my_copy` mode 90% of a campaign's leads get your text and 10% get the AI email from the saved `instructions`, so you can compare the two. Follow-ups are never affected. Free.

Parameters

Responses

PATCH /public/api/v1/autogtm/projects/{project_id}/my-copy

Update project My copy

Turn My copy on or off for the project's default first email, or change its text. Send only the fields you change. Every campaign in `campaign_ids` (`source` `project`) sends the result; campaigns with their own first email are not touched. Turning My copy on needs `body` in the same request, as in the app. The save runs the app's checks before anything is written, so it takes a few seconds, up to about a minute: - **The text.** Variables are `{{first_name}}`, `{{company}}`, `{{job_title}}`, `{{company_city}}`, `{{sender_first_name}}`, `{{sender_company}}`, each optionally with a fallback: `{{company_city|your city}}`. `{a|b}` picks one variant per lead. `{{ai: instruction}}` is a line Explee writes for each lead (up to 5). No links, domains, emails, phone numbers or HTML; the subject can't start with `Re:` or `Fwd:`. The sign-off is Explee's: the mailbox persona's name is added under your text. - **Moderation.** Your text is rendered for 3 leads and checked by the same moderation as in the app. A text that fails is a `422` with `code` `my_copy_invalid`, every finding in `errors` (and `warnings`), and nothing saved: fix it and send it again. A text that passes is saved and goes live for the next first emails; findings worth fixing come back in `warnings`. Sending the text that is already live is not checked again. `409` with `code` `my_copy_changed`: the text changed while it was being checked — read it and send your change again. Two saves run at a time per organization; a third gets `429`: retry it when one finishes.

Parameters

Request Body

Request body required

Responses

POST /public/api/v1/autogtm/campaigns/{campaign_id}/approve

Approve campaign lead list

Approve a campaign's lead list so it starts sending now, instead of waiting for the automatic approval at the end of the review window — the **Approve** button in the app. Only for a campaign in `review`. A campaign already sending (`outreach`) is a no-op; any other status is a `409`.

Parameters

Responses

POST /public/api/v1/autogtm/campaigns/import

Create a campaign from your own leads (import)

Create a new campaign from your own lead list (async). Each call creates a **new** campaign in the given project — the campaign is built for your leads only (no agent lead discovery) and starts sending automatically once live, within your project budget. **How it works:** 1. Submit `project_id`, a campaign `name`, and up to 30,000 `leads` 2. Receive a `task_id` immediately 3. Poll `GET /public/api/v1/autogtm/campaigns/import/{task_id}` until `completed` 4. `result.campaign_id` is your new campaign — it appears in the campaigns list **Per lead** — mandatory: `email`, `first_name`, `last_name`, `company_domain`, `job_title`; recommended: `linkedin_url` (enriches the lead from Explee's contact base for a richer email). Leads missing mandatory fields are skipped (reported in the result), never a request-level error. Duplicates of leads already contacted in the project and do-not-contact entries are dropped. **Your copy brief** — optional `instructions` (first email), `followup_instructions` (all follow-ups) and `language`: the same brief you would type in the app's email editor, set here at import time so no manual step is needed. Omit them and the campaign is briefed from your project description alone. A brief is checked by content moderation before the import is queued, as `PATCH /autogtm/campaigns/{campaign_id}` checks it: the brief itself, then a sample email written from it (usually a few seconds, up to about 85): a refused brief or sample email is a `422`, a check that could not finish is a `503`, and in both cases nothing is imported. **Map every row back:** give each lead an optional `client_ref` (your own id for that row, e.g. a CRM contact id). Once the import is `completed`, `GET /public/api/v1/autogtm/campaigns/import/{task_id}/rows` lists every submitted lead with its `index`, your `client_ref` and its outcome: `imported` with the lead's `person_id`, or `skipped` with a reason code. **Pricing:** the import itself is free. Emails are verified at send time at Explee's cost; sending is billed as usual. **Safe retries:** send an `Idempotency-Key` header (any unique string, e.g. a UUID) and repeat the exact same request with the same key if you don't get an answer. Within 24 hours a repeat returns what the first request created (the same task, list or campaign) and does nothing again. The same key with a different body is rejected (`422`, `idempotency_key_reused`); a repeat that arrives while the first request is still running gets `409` `idempotency_key_in_use` — wait `Retry-After` seconds and repeat. A key replays its first answer even if that import later `failed` — to retry a failed import, send a new key.

Parameters

Request Body

Request body required

Responses

GET /public/api/v1/autogtm/campaigns/import/{task_id}

Campaign import status/result

Check status and retrieve the result of a campaign import. **Statuses:** - `pending` — import in progress; `progress` shows the current stage. Poll every few seconds. - `completed` — done; `result.campaign_id` is the new campaign (or `null` if no leads survived validation/dedup — then no campaign was created). - `failed` — see `error`.

Parameters

Responses

GET /public/api/v1/autogtm/campaigns/import/{task_id}/rows

Campaign import: every lead's outcome

The outcome of **every** lead you submitted to a completed import, in the order you sent them — one row per lead, so `total` always equals the number of leads. Each row carries its `index` in your `leads` array, the `client_ref` you gave it, and either `outcome: "imported"` with the lead's `person_id`, or `outcome: "skipped"` with a `reason`: `missing_fields`, `invalid_email`, `duplicate_in_file`, `already_contacted`, `already_in_project`, `do_not_contact` (on your suppress list or opted out of contact) or `unknown`. Page with `offset` / `limit` (up to 5,000 rows per call) until `has_more` is false. Before the import is `completed` this answers `409` `import_not_completed`.

Parameters

Responses

GET /public/api/v1/autogtm/hot-leads

List hot leads

Every **hot lead** across your organization — people who replied to your outreach with real interest — newest first, as enriched contacts ready to map into CRM fields. **Polling:** pass `since` (ISO 8601) with your last poll time to get only leads that went hot after it — the Zapier/Make pattern. `became_hot_at` of the newest item is your next cursor. `became_hot_at` is when the lead turned hot in Explee, usually a few minutes after they wrote (`replied_at`). A lead can come back with a newer `became_hot_at` when they reply again, so upsert on `person_id`. Pass `project_id` to get one project's leads only. A lead leaves the feed when you dismiss it in the inbox («Not a hot lead») or when it later unsubscribes. To read the full conversation or answer: `GET /autogtm/campaigns/{campaign_id}/inbox/{person_id}` and `…/reply`.

Parameters

Responses

GET /public/api/v1/autogtm/campaigns/{campaign_id}/leads

List contacted leads

Every lead this campaign has already emailed — the contact, how many emails they got and when, and where the conversation stands (replied, bounced, unsubscribed, the status your team set, the latest reply). Most recently emailed first. The same list as **Export CSV** on the Sent tab. The list is live: a lead emailed again moves to the top, so upsert on `person_id` when you walk the pages by `offset`. **To walk a live campaign without missing a lead,** pass `order=first_emailed`: the leads come oldest first email first, and each response carries `next_cursor`. Pass only `cursor` (the `next_cursor` of the previous response) on every next call. While `has_more` is true, call again; once it is false, keep `next_cursor` and come back with it later to read the leads emailed since. A lead's first email never changes, so this walk doesn't skip or repeat a lead while the campaign sends. A lead appears here once its first email has settled: usually a minute after it, longer while an earlier email of the campaign is still being confirmed. Leads that haven't been emailed yet are not listed. Available on the plans that include lead export; other plans get `403`. To read the full conversation or answer: `GET /autogtm/campaigns/{campaign_id}/inbox/{person_id}` and `…/reply`.

Parameters

Responses

GET /public/api/v1/autogtm/projects/{project_id}/inbox

List project inbox conversations

List the project's conversations (contacted people) across all its campaigns, most recent first — the app's Inbox. Pass `campaign_id` for one campaign only. **Tabs:** `need_reply` — replies awaiting a human answer (start here), `replied` — everyone who replied, `sent` — everyone contacted. Omit for the full universe. Each conversation carries its `campaign_id`: read the thread and reply at `…/campaigns/{campaign_id}/inbox/{person_id}`. To poll for what changed since your last call, use `…/projects/{project_id}/inbox/changes`.

Parameters

Responses

GET /public/api/v1/autogtm/projects/{project_id}/inbox/changes

List project inbox changes

The project's conversations, across all its campaigns, with a new message since your last call. Pass `campaign_id` for one campaign only. Each conversation carries its `campaign_id` for the thread and reply paths. Only a reply from the lead (and its classification) or your reply to them counts, oldest first. Campaign emails (first touch, follow-ups) don't count, and neither does a lead status you change in the app. Use it to react to replies without re-reading the inbox. **First call:** pass `since` (ISO 8601), or nothing to read every conversation that has a reply, from the start. **Every next call:** pass only `cursor` — the `next_cursor` of the previous response. While `has_more` is true, call again right away; once it is false, store `next_cursor` and come back with it on your next poll. A conversation comes back each time it gets a message, and now and then without one (a status write on its latest reply), so upsert on `person_id`. A page can hold fewer than `limit` conversations, or none, while `has_more` is true. A change shows up here about a minute after it happens. A conversation hidden from the inbox (e.g. it unsubscribed) stops coming back. `needs_reply` and `lead_status` are as of the call; a status changed in the app since does not bring the conversation back, so re-read the thread before you act on it.

Parameters

Responses

GET /public/api/v1/autogtm/campaigns/{campaign_id}/inbox

List campaign inbox conversations

**Deprecated — use `GET /public/api/v1/autogtm/projects/{project_id}/inbox?campaign_id=…`**, which reads the same conversations and the whole project in one call. This path keeps working. List the campaign's conversations (contacted people), most recent first. **Tabs:** `need_reply` — replies awaiting a human answer (start here), `replied` — everyone who replied, `sent` — everyone contacted. Omit for the full universe. To poll for what changed since your last call, use `…/inbox/changes`.

Parameters

Responses

GET /public/api/v1/autogtm/campaigns/{campaign_id}/inbox/changes

List campaign inbox changes

**Deprecated — use `GET /public/api/v1/autogtm/projects/{project_id}/inbox/changes?campaign_id=…`**. This path keeps working. The campaign's conversations with a new message since your last call. Only a reply from the lead (and its classification) or your reply to them counts, oldest first. Campaign emails (first touch, follow-ups) don't count, and neither does a lead status you change in the app. Use it to react to replies without re-reading the inbox. **First call:** pass `since` (ISO 8601), or nothing to read every conversation that has a reply, from the start. **Every next call:** pass only `cursor` — the `next_cursor` of the previous response. While `has_more` is true, call again right away; once it is false, store `next_cursor` and come back with it on your next poll. A conversation comes back each time it gets a message, and now and then without one (a status write on its latest reply), so upsert on `person_id`. A page can hold fewer than `limit` conversations, or none, while `has_more` is true. A change shows up here about a minute after it happens. A conversation hidden from the inbox (e.g. it unsubscribed) stops coming back. `needs_reply` and `lead_status` are as of the call; a status changed in the app since does not bring the conversation back, so re-read the thread before you act on it.

Parameters

Responses

GET /public/api/v1/autogtm/campaigns/{campaign_id}/inbox/{person_id}

Read a conversation thread

The full conversation with one contact: our outbound emails and their replies, in order, plus the reply gate (`can_reply`). Every message carries its `cc` addresses and the metadata of its `attachments` (name, type, size). To answer, POST to `/reply` with just your message text.

Parameters

Responses

GET /public/api/v1/autogtm/campaigns/{campaign_id}/inbox/{person_id}/attachments/{attachment_id}

Download an attachment

Get a download link for a file attached to a message in this conversation — one the lead sent you or one you sent. Take `attachment_id` from `messages[].attachments[].id` of the thread. Returns `url`, a signed link to the file valid for 5 minutes (`expires_at`): fetch it with a plain GET, no API key. A file listed with `stored: false` was received but could not be stored — `410`.

Parameters

Responses

POST /public/api/v1/autogtm/campaigns/{campaign_id}/attachments

Upload an attachment

Upload a file to attach to a reply in this campaign. Send it as `multipart/form-data` with the field `file`, then pass the returned `id` in `attachment_ids` of `…/inbox/{person_id}/reply`. **Limits:** up to 25 MB per file; PDF, PNG, JPEG, GIF, WebP, DOCX, PPTX, XLSX, TXT. Every file is checked by the same content moderation as uploads in the app — a refused file is `400` with the reason; if the check is briefly unavailable, `503` (retry). A reply carries up to 10 files, and their total must fit the sending mailbox (`413` on `…/reply`). Up to 200 uploads per hour per API key (`429` beyond that).

Parameters

Request Body

Request body required

Responses

POST /public/api/v1/autogtm/campaigns/{campaign_id}/inbox/{person_id}/reply

Reply in a thread

Reply to this contact — send just the message text. You reply to the **lead**, not to an email address: the server resolves the recipient, the subject (`Re: …`) and the threading from the contact's real conversation, so a reply can only ever go back to the person who wrote to you — never to an address you supply. The message goes through the same compliance gate and content moderation as replies sent from the app. **Files:** upload each via `POST …/attachments` and pass the ids in `attachment_ids` (up to 10). Allowed only in a thread the lead has replied to. **Cc:** by default the reply keeps the people the lead had on Cc (`reply_all`, only when their latest message is a real conversation — as in the app) plus your campaign's `reply_cc_emails`. Add per-reply addresses in `cc_emails`; set `reply_all: false` to drop the lead's Cc. Allowed only for a contact who has **replied** and isn't `unsubscribe` — otherwise `403`. You may send at most three replies per message the lead sent you (`429` if exceeded). The send is recorded under your organization's owner identity (an API key carries no user).

Parameters

Request Body

Request body required

Responses

GET /public/api/v1/autogtm/campaigns/{campaign_id}/inbox/{person_id}/note

Read the lead note

The team-shared note on this lead — the same free-text field your team sees and edits in the Explee inbox (Lead info panel). `note` is `null` when no note has been set.

Parameters

Responses

POST /public/api/v1/autogtm/campaigns/{campaign_id}/inbox/{person_id}/note

Set the lead note

Set (or clear) the lead's team-shared note. The note is one shared text per lead, last-write-wins — a write here replaces what a teammate typed in the app, and vice versa. Pass `{"note": null}` (or an empty string) to clear it.

Parameters

Request Body

Request body required

Responses

POST /public/api/v1/autogtm/campaigns/{campaign_id}/inbox/{person_id}/status

Set the lead status

Set (or clear) the lead's status — the same status picker your team uses in the Explee inbox, and the `lead_status` the inbox list returns. - `status` is one of the project's statuses: `Hot`, `Demo`, `Won`, `Lost`, `Unqualified`, or a custom stage added in the app. An unknown name answers 400 with the allowed list. `null` clears the status. - A status can be set only after the lead has replied (409 before that); clearing always works. - Any status but `Hot` cancels a pending autopilot reply to this lead. - `Hot`, `Demo` and `Won` push the lead to your connected HubSpot, as in the app.

Parameters

Request Body

Request body required

Responses

POST /public/api/v1/autogtm/campaigns/{campaign_id}/inbox/{person_id}/mark-replied

Mark the conversation as replied

Mark the conversation as replied — you answered the lead outside Explee. The conversation leaves the `need_reply` tab and the inbox badge, and a pending autopilot reply to this lead is cancelled. The lead's status does not change. If the lead writes again, the conversation comes back to `need_reply`.

Parameters

Responses

GET /public/api/v1/autogtm/projects/{project_id}/budget

Get project daily budget

Read the project's total daily outreach budget (the anchor the dashboard slider sets). Per-campaign allocations are derived from this number automatically — see `daily_limit_usd` on each campaign in `GET /autogtm/campaigns`.

Parameters

Responses

PATCH /public/api/v1/autogtm/projects/{project_id}/budget

Set project daily budget

Set the project's total daily outreach budget. The change is spread across the project's active campaigns proportionally — exactly what the dashboard slider does. **`0` pauses all sending** while keeping campaigns configured (the explicit off switch): the sending campaigns park at once, as with the app's Stop. Range `0`–`300`, the slider's range.

Parameters

Request Body

Request body required

Responses

PATCH /public/api/v1/autogtm/campaigns/{campaign_id}/budget

Set campaign daily budget

Set one campaign's daily budget. The campaign's `daily_limit_usd` is set directly and the project's total daily budget follows as the sum of its running campaigns — the other campaigns are not touched (grow or shrink the total spend, not redistribute a fixed one). **Editable only while Autopilot is OFF** (`GET/PATCH …/projects/{id}/autopilot`): while it's ON the agent owns the split and this returns `409`. A paused / not-yet- running campaign holds no allocation, so setting its budget also returns `409` — resume it first. To set the whole-project budget instead, use `PATCH …/projects/{id}/budget`.

Parameters

Request Body

Request body required

Responses

POST /public/api/v1/autogtm/campaigns/{campaign_id}/stop

Stop campaign

Stop a campaign **from any status**, idempotently: interrupts a running lead search, cancels queued outreach emails, and freezes a pending review's auto-approve countdown. Nothing sends until it is started again. Always available, in either mode — stopping is the user's right; only (re)starting is agent-owned while Auto-manage campaigns is ON.

Parameters

Responses

POST /public/api/v1/autogtm/campaigns/{campaign_id}/start

Start campaign

Re-enable a stopped campaign — the status-aware inverse of stop: - stopped while sending → resumes sending (requires an affordable balance); - stopped in review → un-freezes the review countdown; - stopped during lead search → re-runs the search. Already-running or archived campaigns are a no-op. Returns **409** while Auto-manage campaigns is enabled for the project — Explee decides when campaigns start. Exception: a campaign you parked yourself (stopped, or restored from archive) can always be started again, in either mode. Turn Autopilot off (`PATCH …/projects/{project_id}/autopilot`) to start any other campaign yourself. A campaign paused by billing (daily budget $0, no card, unpaid balance) gets a 409 that names that cause: fix it and the campaign resumes on its own.

Parameters

Responses

POST /public/api/v1/autogtm/campaigns/{campaign_id}/archive

Archive campaign

Archive a campaign **from any status**: signals a running lead search to stop, cancels queued outreach emails and pending auto-replies, and drops the campaign from `GET /public/api/v1/autogtm/campaigns` and from the budget split. Always available, in either mode, like stop. Archiving an archived campaign is a no-op. Undo with `POST …/campaigns/{campaign_id}/unarchive`.

Parameters

Responses

POST /public/api/v1/autogtm/campaigns/{campaign_id}/unarchive

Unarchive campaign

Restore an archived campaign. It comes back **paused** and sends nothing until you call `POST …/campaigns/{campaign_id}/start`, which is allowed even while Auto-manage campaigns is on. Auto-reply stays off. A campaign that is not archived is left as is (no-op). Find archived campaign ids with `GET /public/api/v1/autogtm/campaigns?include_archived=true`.

Parameters

Responses

GET /public/api/v1/autogtm/campaigns/{campaign_id}/analytics

Campaign analytics

Campaign performance for a period: emails sent, replies, reply rate, hot leads, spend, cost per lead, budget allocation, the leads pool and the manual lead funnel. The same numbers the Explee dashboard shows — use them to drive your own budget/start/stop decisions.

Parameters

Responses

GET /public/api/v1/autogtm/projects/{project_id}/analytics

Project analytics

Project-level performance for a period: the aggregate totals (emails, replies, reply rate, hot leads, spend) plus the per-campaign breakdown — exactly what the Explee dashboard shows for a project. Both the totals and the per-campaign rows cover the requested `period`.

Parameters

Responses

GET /public/api/v1/autogtm/campaigns/{campaign_id}/status

Campaign send state

Is this campaign sending right now, and if not, why — the status line the Explee app shows on the project page, for one campaign. - `state` — one stable code (see the field). `sending_now` is `true` only for `sending` and `scheduled`. - `message` — the reason in one sentence; `next_action` — the call (or app step) that unblocks it, `null` when there is nothing to do but wait. - `today` — emails sent and queued today, the next queued send, today's spend against the campaign's daily budget. - `allowed_actions` — the control calls the campaign accepts now (`start`, `stop`, `approve`, `archive`, `unarchive`); anything not listed answers 409 or can only be a no-op. `start` while a lead search already runs is accepted and changes nothing. `start` is left out while the project's daily budget is $0 or a payment problem stops sending (`zero_budget`, `no_payment_method`, `balance_empty`, `payment_failed`): fix that first. Project- and account-level blockers (a $0 project budget, no payment method, an empty balance, a failed payment, moderation) stop this campaign too, so they are reported here as its `state`. Free, read-only; poll it no more than once a minute — the counters behind it refresh every two minutes.

Parameters

Responses

POST /public/api/v1/autogtm/suppress-list/people

Suppress people

Store a list of people to suppress — the same lists the **Suppress list** page in the app shows, so a list pushed from your CRM sits next to the CSVs your team uploaded and behaves identically. A list applies to **every** outreach campaign of the projects it covers, running or not: all projects of your organization by default, or only the ones you name in `project_ids` (an agency keeps each client's list to that client's projects). Every email is checked against it right before it goes out, so an address added here stops a sequence that is already mid-flight, not just future ones. Matching is exact: a person by their email address (case-insensitive, but `[email protected]` and `[email protected]` are two different entries), a company by its domain (the lead's company domain or the domain of its email address — `acme.com` does not cover `eu.acme.com`, list each domain you use). One call = one list, exactly like uploading one CSV: a `list` name that is already taken gets a `(2)` suffix rather than merging, and lists are independent — the same person on two lists is suppressed until both are gone. Delete a list you no longer want. Each person needs at least one identifier: an email, a LinkedIn URL, or a full name; a row left without a usable one (including a malformed address) comes back in `skipped` instead of failing the whole batch. **Outreach stops on the email** — the name and LinkedIn fields are stored but not actively used today; they are kept because a suppressed person is more than their email address, and the day outreach runs somewhere other than email (LinkedIn and the like) those are the fields that will match. To suppress everyone at a company use the companies endpoint. **Safe retries:** send an `Idempotency-Key` header (any unique string, e.g. a UUID) and repeat the exact same request with the same key if you don't get an answer. Within 24 hours a repeat returns what the first request created (the same task, list or campaign) and does nothing again. The same key with a different body is rejected (`422`, `idempotency_key_reused`); a repeat that arrives while the first request is still running gets `409` `idempotency_key_in_use` — wait `Retry-After` seconds and repeat. A key replays its first answer even if that import later `failed` — to retry a failed import, send a new key.

Parameters

Request Body

Request body required

Responses

GET /public/api/v1/autogtm/suppress-list/people

People suppress lists

Your people lists — the ones on the app's Suppress list page, newest first.

Responses

GET /public/api/v1/autogtm/suppress-list/people/{list_name}

Read a people list

The people stored in one list. `total` is the exact size; page through it with `limit`/`offset`.

Parameters

Responses

PATCH /public/api/v1/autogtm/suppress-list/people/{list_name}

Change a people list's projects

Change which projects a list applies to: `{"project_ids": [4021, 4022]}`, or `{"project_ids": null}` for all projects of your organization. Takes effect on the next email: narrowing a list re-opens sending to the projects it no longer covers, widening it stops them. Emails already cancelled by the list stay cancelled. An empty array is a `422` — to apply a list nowhere, delete it. Ids the list already names are kept as-is: sending back the id of a project you have since deleted does not fail.

Parameters

Request Body

Request body required

Responses

DELETE /public/api/v1/autogtm/suppress-list/people/{list_name}

Delete a people list

Remove a whole list, exactly like deleting it on the Suppress list page — its people stop being suppressed unless another list still holds them.

Parameters

Responses

POST /public/api/v1/autogtm/suppress-list/companies

Suppress companies

Store a list of company domains to suppress — nobody at these companies is contacted. A list applies to **every** outreach campaign of the projects it covers, running or not: all projects of your organization by default, or only the ones you name in `project_ids` (an agency keeps each client's list to that client's projects). Every email is checked against it right before it goes out, so an address added here stops a sequence that is already mid-flight, not just future ones. Matching is exact: a person by their email address (case-insensitive, but `[email protected]` and `[email protected]` are two different entries), a company by its domain (the lead's company domain or the domain of its email address — `acme.com` does not cover `eu.acme.com`, list each domain you use). One call = one list, exactly like uploading one CSV: a `list` name that is already taken gets a `(2)` suffix rather than merging, and lists are independent. Delete a list you no longer want. Domains are normalized (`https://www.acme.com/careers` → `acme.com`), so two spellings of one company collapse into a single entry. Values that are not a domain come back as `skipped`. Subdomains are separate entries — `acme.com` does not cover `eu.acme.com`. **Safe retries:** send an `Idempotency-Key` header (any unique string, e.g. a UUID) and repeat the exact same request with the same key if you don't get an answer. Within 24 hours a repeat returns what the first request created (the same task, list or campaign) and does nothing again. The same key with a different body is rejected (`422`, `idempotency_key_reused`); a repeat that arrives while the first request is still running gets `409` `idempotency_key_in_use` — wait `Retry-After` seconds and repeat. A key replays its first answer even if that import later `failed` — to retry a failed import, send a new key.

Parameters

Request Body

Request body required

Responses

GET /public/api/v1/autogtm/suppress-list/companies

Company suppress lists

Your blocked-company lists — the ones on the app's Suppress list page, newest first.

Responses

GET /public/api/v1/autogtm/suppress-list/companies/{list_name}

Read a company list

The domains stored in one list. `total` is the exact size; page through it with `limit`/`offset`.

Parameters

Responses

PATCH /public/api/v1/autogtm/suppress-list/companies/{list_name}

Change a company list's projects

Change which projects a list applies to: `{"project_ids": [4021, 4022]}`, or `{"project_ids": null}` for all projects of your organization. Takes effect on the next email: narrowing a list re-opens sending to the projects it no longer covers, widening it stops them. Emails already cancelled by the list stay cancelled. An empty array is a `422` — to apply a list nowhere, delete it. Ids the list already names are kept as-is: sending back the id of a project you have since deleted does not fail.

Parameters

Request Body

Request body required

Responses

DELETE /public/api/v1/autogtm/suppress-list/companies/{list_name}

Delete a company list

Remove a whole list, exactly like deleting it on the Suppress list page — its companies stop being suppressed unless another list still blocks them.

Parameters

Responses

POST /public/api/v1/autogtm/webhooks

Add a webhook

Add a webhook: we POST each new event of the project to `url`, signed with the webhook's own secret (returned here). A project can have up to 20 webhooks; they also appear in the app under **Settings → Integrations**. Zapier REST hooks: subscribe with this call, unsubscribe with `DELETE /autogtm/webhooks/{id}`. **Events.** Two different events: one is every reply, the other is the moment a lead turns hot. Subscribe to the one your workflow needs; with both, a hot reply arrives as each. `hot_lead.received` — **lead turns hot**: once per lead, when its reply is Hot. A later reply of the same lead does not send it again. Use it to create a deal. The body is the lead exactly as `GET /autogtm/hot-leads` returns it, in an envelope: ```json {"event": "hot_lead.received", "delivery_id": "…", "created_at": "…", "project_id": 12, "test": false, "lead": {…}} ``` `reply.received` — **every reply**: each reply the inbox shows, hot, not interested, auto-replies and out-of-office included. What the inbox hides is not sent (unsubscribes, system noise, removal requests). The body carries the lead and the reply: ```json {"event": "reply.received", "delivery_id": "…", "created_at": "…", "project_id": 12, "test": false, "lead": {"person_id": "…", "campaign_id": 34, "name": "…", "email": "…", …}, "reply": {"id": "…", "intent": "not_interested", "is_hot": false, "subject": "…", "text": "…", "received_at": "…"}} ``` Only leads that turn hot, and replies that arrive, after the webhook was added are sent, each once per webhook; earlier ones are in the hot-leads feed and the inbox. A delivery that fails on our side or gets a 5xx / 408 / 429 is retried for at least 6 hours; any other 3xx or 4xx is your endpoint's answer and is not retried (`last_error` says why). Webhooks need a plan with CRM push (Standard and up): otherwise `403`.

Request Body

Request body required

Responses

GET /public/api/v1/autogtm/webhooks

List webhooks

The webhooks of one project, oldest first, as **Settings → Integrations** shows them.

Parameters

Responses

GET /public/api/v1/autogtm/webhooks/{webhook_id}

Get a webhook

One webhook with its signing secret.

Parameters

Responses

DELETE /public/api/v1/autogtm/webhooks/{webhook_id}

Delete a webhook

Stop sending events to this URL and drop its secret. Zapier calls this when a zap is turned off.

Parameters

Responses

GET /public/api/v1/autogtm/projects/{project_id}/domain-suggestions

Suggest domains to order

Free `.com` names for the project's brand, ready to order, and what to order: - `domains` — each was free at the registry just now and is in nobody's order. The brand is the project's website; pass `brand` to build names from another one, and the names you already have in `exclude` to get the next ones. - `package` — one domain + 5 mailboxes: price a month and emails a day at full speed. - `plan` — how many domains the project's daily budget needs, how many its orders already hold, and `domains_to_buy`. Registry lookups are limited per organization per hour (`429` `domain_lookup_limit`, with `Retry-After`). `order_url` opens the app's order form with the free names on it (up to 20; `null` with fewer than 2 free). Give it to a person: they add the senders and the domains' legal owner, confirm the mailbox addresses and pay the first month by card. Our team then buys the domains and sets up the mailboxes; they warm up for 7 days, then send this project's campaigns first.

Parameters

Responses

GET /public/api/v1/autogtm/projects/{project_id}/domain-availability

Check domains

Check your own picks, comma-separated: each is `free`, `taken` (registered, or in someone's order), `unknown` (the registry didn't answer, retry) or `invalid` (not a second-level `.com` we can buy). `order_url` opens the app's order form with the free names on it (up to 20; `null` with fewer than 2 free). Give it to a person: they add the senders and the domains' legal owner, confirm the mailbox addresses and pay the first month by card. Our team then buys the domains and sets up the mailboxes; they warm up for 7 days, then send this project's campaigns first.

Parameters

Responses

GET /public/api/v1/autogtm/projects/{project_id}/autopilot

Get autopilot settings

Read the project's autopilot and auto-reply settings. - `autopilot_enabled` — the Explee autopilot agent. While ON it manages campaigns and the per-campaign budget split (per-campaign budget edits return 409); turn it OFF to control budgets yourself. - `auto_reply_enabled` / `auto_reply_delay_minutes` — AI auto-reply behaviour. - `reply_cc_emails` — project defaults added to CC on every manual or automatic reply.

Parameters

Responses

PATCH /public/api/v1/autogtm/projects/{project_id}/autopilot

Set autopilot settings

Update the project's autopilot / auto-reply settings. Send only the fields you want to change — at least one is required. Turning `autopilot_enabled` **off** hands per-campaign budgets back to you (`PATCH …/campaigns/{id}/budget` stops returning 409); turning it **on** makes them agent-managed again.

Parameters

Request Body

Request body required

Responses

GET /public/api/v1/autogtm/billing

Outreach balance

Your outreach balance and today's spend — what the Billing tab of the app shows. Free, read-only. Check `send_status` before you start or scale campaigns: anything but `ok` means emails stop until a person tops up or adds a card on the billing page (https://explee.com/app-auto-gtm/billing). `daily_budget_usd` is a ceiling, not a promise to spend: spend stays lower on days when fewer leads are ready.

Responses

GET /public/api/v1/autogtm/billing/usage

Outreach spend by day

Outreach spend by UTC day for the last `days` days (today included) — the Usage panel of the app's Billing tab. Free, read-only. Spend is emails sent × your email price (paid LinkedIn touches count as emails). Replies are free. The window total is also split by what paid for it: trial credits, the plan's included emails, or money.

Parameters

Responses

GET /public/api/v1/autogtm/billing/activity

Credits activity

Where your balance came from: top-ups, plan emails, trial and referral credits — the last 50 deposits, newest first, as the Credits activity list of the app shows them. Free, read-only.

Responses

GET /public/api/v1/me

Get API key owner

Who this API key is: the organization it belongs to, its plan, the credit balance, what the key may reach (`key`), the rate limits and where the docs are. A good first call to check a key works. Free. ## Response ```json { "organization": {"id": "4ca77b5d-…", "name": "Acme"}, "plan": "pro", "balance": {"remain": 4200}, "key": {"project_ids": null, "read_only": false}, "rate_limits": { "requests_per_hour": 10000, "max_concurrent_requests": 150, "heavy_read_max_concurrent_requests": 4, "free_zone": {"per_day": 750, "per_minute": 25}, "timeout_seconds": 90 }, "docs": {"reference": "https://api.explee.com/public/api/docs", "openapi": "…/openapi.json", "llms_txt": "…/llms.txt"} } ```

Responses

POST /public/api/v1/feedback

Send feedback

Send us freeform feedback about the API: request a feature, report a problem, tell us data is missing or wrong — anything, in your own words. The message lands directly with the Explee team. We read everything, though we don't guarantee an individual reply. No credits are charged.

Request Body

Request body required

Responses

GET /public/api/v1/dedup/people

People Lists

Responses

POST /public/api/v1/dedup/people

Create People List

Create an immutable list of people you already own. Pass the person objects returned by people search or find-and-enrich in `people`; they are matched by LinkedIn URL, normalized email, or first name + last name + company domain. Existing integrations can keep using the legacy `linkedin_urls` field. Pass the returned id in `exclude_lists` on people search — matched people are not returned and not charged. Lists can't be modified: to grow your ledger, create a new list per batch and pass all ids; consolidate periodically. URLs are normalized on write; unparseable values are skipped and reported. **Limits:** 100,000 values per list; per search request: 100 lists, 500,000 values combined.

Request Body

Request body required

Responses

GET /public/api/v1/dedup/people/{list_id}

Get People List

Returns the list metadata and all stored values.

Parameters

Responses

DELETE /public/api/v1/dedup/people/{list_id}

Delete People List

Removes the whole list. Excluded people will appear (and be charged) in search results again.

Parameters

Responses

GET /public/api/v1/dedup/companies

Companies Lists

Responses

POST /public/api/v1/dedup/companies

Create Companies List

Create an immutable list of companies you already own (`domain` values from search results). Pass the returned id in `exclude_lists` on companies search — matched companies are not returned and not charged. The same id works on people search, people-by-domains and find-and-enrich: everyone at a listed company is skipped (exact domain match, subdomains are separate companies). People and companies lists can be mixed in one request. Lists can't be modified: to grow your ledger, create a new list per batch and pass all ids; consolidate periodically. Domains are normalized on write; unparseable values are skipped and reported. **Limits:** 100,000 values per list; per search request: 100 lists, 500,000 values combined.

Request Body

Request body required

Responses

GET /public/api/v1/dedup/companies/{list_id}

Get Companies List

Returns the list metadata and all stored values.

Parameters

Responses

DELETE /public/api/v1/dedup/companies/{list_id}

Delete Companies List

Removes the whole list. Excluded companies will appear (and be charged) in search results again.

Parameters

Responses