<!-- Moleman API docs, generated 2026-11-05 from https://moleman.io/docs -->

# Moleman API

The Moleman API finds business emails and checks that they work. Everything you can do in the app, you can do with a few HTTP calls: from your own scripts, Clay, Make, Zapier or n8n.

**Base URL:** `https://moleman.io/api/v1` · **Auth:** `Authorization: Bearer <your key>` · **Format:** JSON in, JSON out (CSV for exports)

## What you can do

| You want | Call | Costs |
| --- | --- | --- |
| The email of a person at a business | [`POST /find-contacts`](https://moleman.io/docs/api/find-contacts) | 1 credit per email that works |
| The business email (info@, office@ …) or the owner | [`POST /find-contacts`](https://moleman.io/docs/api/find-contacts) with `action` | 1 credit per email that works |
| Check emails you already have | [`POST /verify`](https://moleman.io/docs/api/verify) | 0.1 credit per email that works |
| Businesses in an area, with emails | [`POST /search`](https://moleman.io/docs/api/search) | 1 credit per email that works |
| A company summary and a first line | [`POST /deep-search`](https://moleman.io/docs/api/deep-search) | 0.5 credit per summary |
| Your credits, plan and limits | [`GET /account`](https://moleman.io/docs/api/account) | free |

You only pay for results: an email that does not work, or that we could not confirm, costs nothing. Anything your account already got in the last 30 days is answered again for free. See [Credits](https://moleman.io/docs/credits).

## Start here

1. Open the [API page in the app](https://moleman.io/app/api) and create a **test key** (`mm_test_…`). It returns made-up results and never costs anything.
2. Run the [Quickstart](https://moleman.io/docs/quickstart): three calls, and you have your first working email.
3. When it works, create a **live key** (`mm_live_…`) and use real data.

## How it works

Every request that finds or checks emails starts a **job**. Small jobs finish in seconds: send `"wait": 30` and the answer already has the results. Big jobs (up to 50,000 rows) run in the background: poll [`GET /jobs/{id}`](https://moleman.io/docs/api/jobs-get) or let us call your [webhook](https://moleman.io/docs/webhooks) when they are done.

Results use the same words as the app: **Works**, **Probably works**, **Not sure**, **On hold**, **Still checking…** Only "Works" is charged.

## For AI agents and tools

- [OpenAPI 3.1](https://moleman.io/api/v1/openapi.json) – generated from the same schemas the server checks requests with
- [All docs as one Markdown file](https://moleman.io/api/v1/docs.md)
- [llms.txt](https://moleman.io/llms.txt)


---

# Quickstart

Your first working email in three calls. It takes about five minutes.

## 1. Get a key

Log in, open the [API page](https://moleman.io/app/api) and click **Create key**. Pick **Test** for now: test keys return made-up results, never charge credits and never contact anyone. Copy the key right away; we only show it once.

```bash
export MOLEMAN_API_KEY="mm_test_..."
```

## 2. Run the three calls

1. `GET /api/v1/account` checks the key and shows your credits.
2. `POST /api/v1/find-contacts` asks for Sarah Miller's email at brightsmiledental.com and waits up to 30 seconds.
3. `GET /api/v1/jobs/{id}` reads the result (and keeps reading if the job needs longer than 30 seconds).

### curl

```bash title="quickstart.sh"
#!/usr/bin/env bash
# Moleman quickstart: your first working email in 3 calls.
# Needs: curl. Set MOLEMAN_API_KEY first (start with your mm_test_ key: free, made-up results).
set -euo pipefail
BASE="${MOLEMAN_BASE_URL:-https://moleman.io}"
AUTH="Authorization: Bearer ${MOLEMAN_API_KEY:?Set MOLEMAN_API_KEY to your API key}"

# 1. Check the key and your credits.
curl -sS "$BASE/api/v1/account" -H "$AUTH"
echo

# 2. Find Sarah Miller's email at brightsmiledental.com (waits up to 30 s for the answer).
ANSWER=$(curl -sS -X POST "$BASE/api/v1/find-contacts" \
  -H "$AUTH" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: quickstart-$(date +%s)" \
  -d '{"rows":[{"firstName":"Sarah","lastName":"Miller","website":"brightsmiledental.com"}],"wait":30}')
echo "$ANSWER"
JOB_ID=$(echo "$ANSWER" | sed -n 's/.*"jobId":\([0-9]*\).*/\1/p')

# 3. Read the job until it is done (a big job takes longer than 30 s; this loop waits for it).
while true; do
  JOB=$(curl -sS "$BASE/api/v1/jobs/$JOB_ID" -H "$AUTH")
  case "$JOB" in
    *'"status":"done"'*|*'"status":"cancelled"'*|*'"status":"failed"'*) break ;;
  esac
  sleep 5
done
echo "$JOB" | sed -n 's/.*"email":"\([^"]*\)".*"result":"\([^"]*\)".*/Email: \1 (\2)/p'
```

### Node.js

```js title="quickstart.mjs"
// Moleman quickstart: your first working email in 3 calls. Node 18 or newer, no packages.
// Run: MOLEMAN_API_KEY=mm_test_... node quickstart.mjs   (start with your test key: free, made-up results)
import { randomUUID } from 'node:crypto';

const BASE = process.env.MOLEMAN_BASE_URL ?? 'https://moleman.io';
const KEY = process.env.MOLEMAN_API_KEY;
if (!KEY) throw new Error('Set MOLEMAN_API_KEY to your API key.');

async function call(method, path, body) {
  const response = await fetch(`${BASE}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${KEY}`,
      ...(body ? { 'Content-Type': 'application/json', 'Idempotency-Key': randomUUID() } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await response.json();
  if (!response.ok) throw new Error(`${data.error.code}: ${data.error.message} (${data.error.docs_url})`);
  return data;
}

// 1. Check the key and your credits.
const account = await call('GET', '/api/v1/account');
console.log(`Key works (${account.key.mode}). Credits left: ${account.credits.balance}`);

// 2. Find Sarah Miller's email at brightsmiledental.com (waits up to 30 s for the answer).
let job = await call('POST', '/api/v1/find-contacts', {
  rows: [{ firstName: 'Sarah', lastName: 'Miller', website: 'brightsmiledental.com' }],
  wait: 30,
});

// 3. Read the job until it is done (a big job takes longer than 30 s; this loop waits for it).
while (!['done', 'cancelled', 'failed'].includes(job.status)) {
  await new Promise((resolve) => setTimeout(resolve, 5000));
  job = await call('GET', `/api/v1/jobs/${job.jobId}`);
}
for (const row of job.results) console.log(`Email: ${row.email ?? 'none'} (${row.result ?? row.findStatusText})`);
```

### Python

```python title="quickstart.py"
# Moleman quickstart: your first working email in 3 calls. Python 3.8 or newer, no packages.
# Run: MOLEMAN_API_KEY=mm_test_... python quickstart.py   (start with your test key: free, made-up results)
import json
import os
import time
import urllib.error
import urllib.request
import uuid

BASE = os.environ.get("MOLEMAN_BASE_URL", "https://moleman.io")
KEY = os.environ["MOLEMAN_API_KEY"]


def call(method, path, body=None):
    headers = {"Authorization": f"Bearer {KEY}"}
    data = None
    if body is not None:
        headers["Content-Type"] = "application/json"
        headers["Idempotency-Key"] = str(uuid.uuid4())
        data = json.dumps(body).encode()
    request = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(request, timeout=60) as response:
            return json.load(response)
    except urllib.error.HTTPError as error:
        problem = json.load(error)["error"]
        raise SystemExit(f"{problem['code']}: {problem['message']} ({problem['docs_url']})")


# 1. Check the key and your credits.
account = call("GET", "/api/v1/account")
print(f"Key works ({account['key']['mode']}). Credits left: {account['credits']['balance']}")

# 2. Find Sarah Miller's email at brightsmiledental.com (waits up to 30 s for the answer).
job = call("POST", "/api/v1/find-contacts", {
    "rows": [{"firstName": "Sarah", "lastName": "Miller", "website": "brightsmiledental.com"}],
    "wait": 30,
})

# 3. Read the job until it is done (a big job takes longer than 30 s; this loop waits for it).
while job["status"] not in ("done", "cancelled", "failed"):
    time.sleep(5)
    job = call("GET", f"/api/v1/jobs/{job['jobId']}")
for row in job["results"]:
    print(f"Email: {row['email'] or 'none'} ({row['result'] or row['findStatusText']})")
```

With a test key you get `sarah.miller@brightsmiledental.com` and the result **Works**. These examples are tested against our server with a test key on every change.

## 3. Go live

Create a live key (`mm_live_…`) on the API page and use your own rows. Each email that **Works** costs 1 credit; everything else is free. Your account starts with free trial credits; see [Credits](https://moleman.io/docs/credits).

## Next

- Send a whole list at once (up to 50,000 rows) and get a [webhook](https://moleman.io/docs/webhooks) when it is done.
- Find [businesses in an area](https://moleman.io/docs/api/search) instead of uploading a list.
- Put the results into [Instantly or Smartlead](https://moleman.io/docs/csv-export), or use [Clay](https://moleman.io/docs/clay), [Make, Zapier or n8n](https://moleman.io/docs/no-code).


---

# API keys

Every request (except the docs files) needs an API key in the `Authorization` header:

```http
Authorization: Bearer mm_live_Ab3x...
```

## Keys

- Create keys on the [API page](https://moleman.io/app/api) of the app. Give each integration its own key ("Clay", "Zapier", "CRM sync"), so you can see its usage and revoke it alone.
- A key is shown **once**, when you create it. We store only a hash of it, so we cannot show it again. Lost it? Create a new one and revoke the old one.
- Keys belong to your **account**, not to a person: everyone on your team sees the same keys and can revoke them. The list shows who created each key and when it was last used.
- **Revoke** works at once: the very next request with that key gets `401 revoked_api_key`.
- Never put a key in a web page, a mobile app or a public repository. Keep it on a server or in your tool's secret settings.

## Live and test keys

| | `mm_live_…` | `mm_test_…` |
| --- | --- | --- |
| Results | real | made up, realistic, always the same for the same input |
| Credits | charged for emails that work | never charged |
| Contacts mail servers, websites, data sources | yes | never |
| Webhooks | sent | sent (so you can test your endpoint) |
| Data | your account's lists | separate test data, deleted after 30 days |

Test and live data never mix: reading a live job with a test key returns `403 wrong_key_mode`. See [Test mode](https://moleman.io/docs/test-mode) for the test values.

## Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `missing_api_key` | No key in the request. |
| 401 | `invalid_api_key` | Not a key we know (typo, cut off, deleted account). |
| 401 | `revoked_api_key` | The key was revoked. |
| 403 | `wrong_key_mode` | Test key on live data, or the other way round. |

Too many requests with wrong keys from one network (more than 30 a minute) get `429 rate_limited` for a minute.


---

# Test mode

Test keys (`mm_test_…`) let you build and test an integration without spending credits.

- Same endpoints, same checks, same answer shapes and error codes as live.
- Results are **made up** but realistic, and always the same for the same input.
- **Never** charged, and they never contact a mail server, a website or any data source.
- Jobs are `running` for about 2 seconds, then `done`, so polling and `wait` behave like live.
- Webhooks are really sent to your URL (that is the point: you can test your endpoint).
- Answers have `"livemode": false`. Test lists and jobs are separate from live ones and deleted after 30 days.

## Test values

Use these to get each kind of result:

| Endpoint | Input | Result |
| --- | --- | --- |
| verify | any normal email | Works |
| verify | name part contains `bounce` or `invalid` (`bounce@acme.com`) | Does not work |
| verify | name part contains `unknown` | Not sure |
| verify | domain starts with `catchall.` (`sam@catchall.acme.com`) | Takes any name → Probably works |
| find-contacts | first + last name + website | `first.last@domain`, Works |
| find-contacts | last name `Nobody` | Not found |
| find-contacts | website starts with `catchall.` | Probably works |
| find-contacts | `action: companyInbox` | `info@domain`, Works |
| find-contacts | `action: roleContact` | a made-up owner, "Owner email from their website" |
| find-contacts | no website or domain | skipped (`no_eligible_rows` if none is left) |
| deep-search | company + website | a made-up summary and first line (registry facts for `country: "GB"`) |
| search | any category and place | up to 25 made-up businesses on `.example` domains |

`POST /verify` with `{"emails": ["bounce@acme.com", "sam@acme.com"]}` gives one "Does not work" and one "Works".


---

# Credits

The API uses the same credits as the app. You only pay for results you can use.

## What costs credits

| Result | Credits | Charged when |
| --- | --- | --- |
| Email found ([find-contacts](https://moleman.io/docs/api/find-contacts), any `action`) | 1 | you only pay for emails that work (everything else is free) |
| Email from a [business search](https://moleman.io/docs/api/search) | 1 | you only pay for emails that work (everything else is free) |
| Email checked ([verify](https://moleman.io/docs/api/verify)) | 0.1 | you only pay when the email works (bad or unclear emails are free) |
| Company summary ([deep-search](https://moleman.io/docs/api/deep-search)) | 0.5 | you only pay when we write the report |

## What is free

- Every email that does **not** work, that we could **not confirm** ("Probably works", "Not sure", "On hold"), or that we did not find.
- **30-day reuse:** if your account (app or API, any key) got an answer for the same email, the same person at the same business, or the same business's summary in the last 30 days, you get it again for free. The answer shows how many rows were reused in `cached30d`.
- Businesses without their own website in a business search (counted, never delivered).
- Reading jobs, lists and exports, `GET /account`, and webhooks.
- Everything with a **test key**.
- Requests that fail with an error.

## Holding credits

When a job starts we hold the most it can cost (`maxCreditsHeld`, for example 100 rows × 1 credit = 100 credits). As results come in, only emails that work are charged; when the job ends, the rest goes back at once. If you do not have enough credits for the hold, the request fails with `402 insufficient_credits` and nothing starts. Send fewer rows, or get credits in the app.

`GET /api/v1/account` shows your credits; every job shows `creditsCharged`.

## Bounces

If a "Works" email bounces, upload the bounce in the app within the guarantee period and you get the credit back (same rule as in the app).


---

# Jobs & webhooks

Big jobs run in the background. Two ways to get the result:

- **Polling:** call [`GET /api/v1/jobs/{id}`](https://moleman.io/docs/api/jobs-get) every 5–10 seconds until `status` is `done`, `cancelled` or `failed`.
- **Webhook:** add `"webhookUrl": "https://…"` to the POST. When the job is done, we POST the result to that URL.

For small jobs, `"wait": 30` on the POST is easiest: the answer comes back finished.

## Job status

`queued` → `running` → `done`. A job can also be `paused` (our checks wait for a mail server; it goes on by itself), `cancelled` (you or the app stopped it) or `failed`.

## What we send

```http
POST /your/webhook HTTP/1.1
Content-Type: application/json
X-Moleman-Event: job.completed
X-Moleman-Delivery: dlv_8f2c…
X-Moleman-Timestamp: 1762344014
X-Moleman-Signature: t=1762344014,v1=5f2c9b…
```

```json
{
  "id": "dlv_8f2c…",
  "event": "job.completed",
  "livemode": true,
  "jobId": 981,
  "listId": 377,
  "action": "findEmail",
  "status": "done",
  "creditsCharged": 1,
  "total": 1,
  "results": [{ "email": "sarah.miller@brightsmiledental.com", "result": "Works", "…": "…" }],
  "hasMore": false,
  "resultsUrl": "/api/v1/jobs/981"
}
```

Events: `job.completed` (find-contacts, verify, deep-search), `search.completed` (business search, with `runId` and the search's `run`), `ping` (the [test endpoint](https://moleman.io/docs/api/webhooks-test)). A body carries up to 1,000 results; with more, `hasMore` is true and you page through `resultsUrl`.

## Check the signature

Every webhook is signed with your account's **signing secret** (`whsec_…`, on the API page), or with the `webhookSecret` you sent with the request. The signature is an HMAC-SHA256 of `<timestamp>.<raw body>`:

1. Read the raw body as bytes. Do not parse and re-encode it first.
2. Split `X-Moleman-Signature` into `t` and `v1`.
3. Refuse it if `t` is more than 5 minutes away from now (this blocks replays).
4. Compute HMAC-SHA256(secret, `t + "." + raw body`) as hex and compare it with `v1` in constant time.

### Node.js

```js title="verify-webhook.mjs"
// Check that a webhook really comes from Moleman, then use it. Node 18 or newer, no packages.
// Run: MOLEMAN_WEBHOOK_SECRET=whsec_... node verify-webhook.mjs   (listens on port 8787)
import { createHmac, timingSafeEqual } from 'node:crypto';
import { createServer } from 'node:http';
import { fileURLToPath } from 'node:url';

const TOLERANCE_SECONDS = 300; // refuse anything older than 5 minutes (replays)

/** rawBody: the exact bytes we sent (do not parse and re-stringify). header: X-Moleman-Signature. */
export function verifyMolemanSignature(rawBody, header, secret, nowSeconds = Math.floor(Date.now() / 1000)) {
  const parts = Object.fromEntries(String(header ?? '').split(',').map((part) => part.trim().split('=', 2)));
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp) || !parts.v1) return false;
  if (Math.abs(nowSeconds - timestamp) > TOLERANCE_SECONDS) return false;
  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  const given = Buffer.from(parts.v1, 'utf8');
  return given.length === expected.length && timingSafeEqual(given, Buffer.from(expected, 'utf8'));
}

const seen = new Set(); // X-Moleman-Delivery ids you already handled (a retry has the same id)

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const secret = process.env.MOLEMAN_WEBHOOK_SECRET;
  if (!secret) throw new Error('Set MOLEMAN_WEBHOOK_SECRET (API page in the app).');
  createServer((request, response) => {
    let raw = '';
    request.on('data', (chunk) => (raw += chunk));
    request.on('end', () => {
      if (!verifyMolemanSignature(raw, request.headers['x-moleman-signature'], secret)) {
        response.writeHead(401).end('bad signature');
        return;
      }
      const delivery = request.headers['x-moleman-delivery'];
      if (!seen.has(delivery)) {
        seen.add(delivery);
        const event = JSON.parse(raw);
        console.log(event.event, event.jobId ?? '', `${event.results?.length ?? 0} results`);
      }
      response.writeHead(200).end('ok'); // answer 2xx fast; do slow work after
    });
  }).listen(8787, () => console.log('Waiting for webhooks on http://localhost:8787'));
}
```

### Python

```python title="verify_webhook.py"
# Check that a webhook really comes from Moleman, then use it. Python 3.8 or newer, no packages.
# Run: MOLEMAN_WEBHOOK_SECRET=whsec_... python verify_webhook.py   (listens on port 8787)
import hashlib
import hmac
import json
import os
import time
from http.server import BaseHTTPRequestHandler, HTTPServer

TOLERANCE_SECONDS = 300  # refuse anything older than 5 minutes (replays)


def verify_moleman_signature(raw_body: bytes, header: str, secret: str, now=None) -> bool:
    """raw_body: the exact bytes we sent (do not parse and re-dump). header: X-Moleman-Signature."""
    parts = dict(part.strip().split("=", 1) for part in (header or "").split(",") if "=" in part)
    try:
        timestamp = int(parts["t"])
        given = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs((now or time.time()) - timestamp) > TOLERANCE_SECONDS:
        return False
    signed = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, given)


seen = set()  # X-Moleman-Delivery ids you already handled (a retry has the same id)


class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        raw = self.rfile.read(int(self.headers.get("Content-Length", 0)))
        if not verify_moleman_signature(raw, self.headers.get("X-Moleman-Signature"), os.environ["MOLEMAN_WEBHOOK_SECRET"]):
            self.send_response(401)
            self.end_headers()
            return
        delivery = self.headers.get("X-Moleman-Delivery")
        if delivery not in seen:
            seen.add(delivery)
            event = json.loads(raw)
            print(event["event"], event.get("jobId", ""), len(event.get("results", [])), "results")
        self.send_response(200)  # answer 2xx fast; do slow work after
        self.end_headers()


if __name__ == "__main__":
    print("Waiting for webhooks on http://localhost:8787")
    HTTPServer(("", 8787), Handler).serve_forever()
```

## Retries

Answer with any 2xx status within 10 seconds. Anything else (or no answer) is retried: after 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 14 h (8 tries over about 18 hours). After the last try we give up; the results stay available with `GET /api/v1/jobs/{id}`.

A retry has the same `X-Moleman-Delivery` id and a new timestamp and signature. Remember the ids you handled and skip repeats. Do slow work after you answered.

We only send to public `https://` addresses and do not follow redirects.


---

# Rate limits

## Limits

| Limit | Value |
| --- | --- |
| Requests per key | 120 per minute |
| Rows per request (find-contacts, verify, lists) | 50,000 |
| Rows per deep-search request | 10,000 |
| `wait` on a POST | up to 30 seconds |
| Page size (`limit`) | up to 1,000 |
| Request body | 20 MB |

Need more? Write to us; we raise limits per account.

The request limit is about calls, not rows: one request with 10,000 rows counts once. Send lists in one request instead of one request per row.

## Headers

Every answer has:

```http
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 42
```

`X-RateLimit-Reset` is the number of seconds until the minute starts over.

## When you hit the limit

You get `429 rate_limited` with a `Retry-After` header (seconds). Wait that long, then go on. Nothing is charged for a refused request.

```js
if (response.status === 429) {
  await new Promise((r) => setTimeout(r, Number(response.headers.get('retry-after')) * 1000));
  // send the same request again, with the same Idempotency-Key
}
```


---

# Idempotency

Networks fail. If you send a POST and get no answer, you cannot know whether the job started. Send it again with the same **Idempotency-Key** and you are never charged twice.

```http
POST /api/v1/find-contacts
Idempotency-Key: 6f1c2a1e-3b0a-4c47-9c0e-1f2d3e4a5b6c
```

## How it works

- Every POST accepts an `Idempotency-Key` header: any unique string up to 255 characters. A new UUID per request is best.
- The first request runs. Any retry with the **same key and the same body** in the next 24 hours gets the first answer back, with the header `Idempotent-Replayed: true`. No second job, no second charge.
- The same key with a **different body** gets `422 idempotency_key_reused`. Use a new key for a new request.
- If the first request is still running, a retry gets `409 idempotency_in_progress`. Wait a moment and retry.
- Server errors (5xx) are not stored: a retry with the same key runs again.
- Keys are per account and per key kind (test and live never mix).

Make, Zapier and n8n: set the header to a value that is the same on a re-run of the same step, for example the record id from the trigger.


---

# Pages & CSV

## Pages

Lists of things (`GET /jobs`, `GET /lists`) and results (`GET /jobs/{id}`, `GET /lists/{id}`, `GET /lists/{id}/rows`) come in pages.

- `limit`: rows per page (up to 1,000).
- The answer has `nextCursor`. If it is not `null`, ask again with `cursor=<nextCursor>` for the next page.
- Cursors are opaque: pass them on unchanged. A made-up cursor gets `400 invalid_cursor`.

```bash
curl "https://moleman.io/api/v1/lists/380/rows?limit=500" -H "Authorization: Bearer $MOLEMAN_API_KEY"
# … "nextCursor": "YzE6NDYxMA"
curl "https://moleman.io/api/v1/lists/380/rows?limit=500&cursor=YzE6NDYxMA" -H "Authorization: Bearer $MOLEMAN_API_KEY"
```

## CSV

`GET /api/v1/lists/{id}/rows?format=csv` returns the whole list as one CSV file (no pages). Add `only=works` for emails that work only. See [CSV export](https://moleman.io/docs/csv-export) for the columns.


---

# Errors

Every error has the same shape, on every endpoint:

```json
{
  "error": {
    "code": "insufficient_credits",
    "message": "This job needs up to 120 credits, but your account has 80.",
    "docs_url": "https://moleman.io/docs/errors#insufficient_credits"
  }
}
```

- `code` never changes; build your logic on it. `message` is for people and may get clearer over time.
- `invalid_request` also has `details`: every problem with its path, for example `{ "path": "body.rows.0.website", "message": "Too long" }`.
- Nothing is charged for a request that ends in an error.

## All codes

### `missing_api_key`

**HTTP 401.** The request has no API key.

**What to do:** Send the header Authorization: Bearer <your key>. Keys are on the API page in the app.

### `invalid_api_key`

**HTTP 401.** The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted).

**What to do:** Copy the whole key again. It starts with mm_live_ or mm_test_. If you lost it, create a new one; we cannot show it again.

### `revoked_api_key`

**HTTP 401.** The API key was revoked on the API page. Revoking takes effect at once.

**What to do:** Create a new key on the API page and use that one.

### `wrong_key_mode`

**HTTP 403.** A test key asked for something made with a live key, or the other way round. Test and live data never mix.

**What to do:** Use a key of the same kind (mm_test_ or mm_live_) that created the job or list.

### `invalid_request`

**HTTP 400.** A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path.

**What to do:** Fix the parameters named in `details` (see the endpoint page for the allowed values).

### `invalid_json`

**HTTP 400.** The body is not valid JSON.

**What to do:** Send a JSON body with the header Content-Type: application/json.

### `invalid_cursor`

**HTTP 400.** The cursor is not one we gave out (they are opaque; do not build them yourself).

**What to do:** Start again without `cursor`, then pass the `nextCursor` of each page unchanged.

### `invalid_webhook_url`

**HTTP 400.** The webhook URL is not a public https address (private networks and other schemes are refused).

**What to do:** Use a public https URL, for example the one your Make, Zapier or n8n webhook trigger gives you.

### `no_eligible_rows`

**HTTP 400.** None of the rows can run this action (for example: no website or domain to find an email for, or no email to check).

**What to do:** Check the rows: find-contacts needs a website or domain, verify needs an email, deep-search needs a company or website.

### `insufficient_credits`

**HTTP 402.** The account has fewer credits than this request could use at most. We hold the maximum while the job runs and give back what it does not use.

**What to do:** Buy credits or pick a plan in the app, or send fewer rows. GET /api/v1/account shows your credits.

### `not_found`

**HTTP 404.** The job or list does not exist, belongs to another account, or was deleted.

**What to do:** Check the id. Lists you deleted, and results older than 30 days that were removed, cannot come back.

### `place_not_found`

**HTTP 404.** We could not find the place you searched in.

**What to do:** Use a city, county, state or ZIP / post code that exists in the country you sent.

### `route_not_found`

**HTTP 404.** There is no endpoint at this path with this method.

**What to do:** Check the path and method against the reference. All endpoints start with /api/v1/.

### `idempotency_in_progress`

**HTTP 409.** A request with the same Idempotency-Key is still running.

**What to do:** Wait a moment and send it again with the same key; you then get the first answer.

### `payload_too_large`

**HTTP 413.** The body is larger than 20 MB.

**What to do:** Send fewer rows per request (up to 50,000 rows; 10,000 for deep-search).

### `unsupported_media_type`

**HTTP 415.** The body is not JSON.

**What to do:** Send Content-Type: application/json.

### `idempotency_key_reused`

**HTTP 422.** The Idempotency-Key was already used in the last 24 hours with a different request.

**What to do:** Use a new key for every new request. Reuse a key only to retry exactly the same request.

### `rate_limited`

**HTTP 429.** More than 120 requests in one minute with this key.

**What to do:** Wait for the number of seconds in the Retry-After header, then go on. Big lists go in one request, not one request per row.

### `server_error`

**HTTP 500.** Something broke on our side. Nothing was charged for it.

**What to do:** Retry with the same Idempotency-Key after a few seconds. If it keeps happening, write to us.

### `search_unavailable`

**HTTP 503.** Business search is not available right now.

**What to do:** Try again in a few minutes.


---

# Data rules

The API follows exactly the same rules as the app. There is no way around them with an API key.

- **Opt-outs:** people and businesses who asked us not to process their data are never found, checked, delivered or charged, whatever the request.
- **Deletion:** when someone asks us to delete their data, it is removed from every list, including lists made with the API. A deleted list (in the app or with `DELETE /lists/{id}`) is gone for good.
- **30 days:** stored results follow the same retention as the app. The free 30-day reuse only uses your own account's results.
- **Owner names** come only from the business's own website. We never take a person's name from a company register or another source to find or guess their email. Company-register facts (Deep Search) are never linked to finding emails, and find results never include them.
- **US state rules:** for businesses in Vermont, Oregon and Connecticut we deliver owner emails only when the business prints them on its own website, never guessed ones. Business emails (info@, office@ …) are fine everywhere. Everything delivered through the API counts toward the same yearly limits as the app.
- **Your use:** you send to these emails under the laws of your country (CAN-SPAM, CASL, GDPR …) and our [terms](https://moleman.io/terms): a clear sender, an unsubscribe link, no purchased-list spam.

We do not tell you which outside services we use for a result, in the app or in the API.


---

# Versions & changelog

The API version is part of the path: `/api/v1`. Within v1:

- We **add** things (new endpoints, new fields, new error codes, new event types) without warning. Ignore fields you do not know.
- We **never** remove or rename a field, change its type or change what a status word means within v1.
- A breaking change gets a new version (`/api/v2`), announced by email at least 90 days before v1 changes.

The current schema version is `2026-11-05` (in `openapi.json` and in every webhook as `apiVersion`).

## Changelog

**2026-11-05**
- New: test keys (`mm_test_…`) with made-up results; `GET /account`; `GET /jobs`, `POST /jobs/{id}/cancel`, `GET /lists`, `GET /lists/{id}/rows` (JSON pages or CSV), `DELETE /lists/{id}`, `POST /webhooks/test`; `wait` on POSTs; `Idempotency-Key` on every POST; `X-RateLimit-*` headers; cursor pages; `role: "best"` on search.
- New fields on every result: `result`, `resultCode`, `resultMeaning` and the status words (`emailStatusText`, `findStatusText`, `emailTypeText`) – the same words as the app.
- Changed: errors are now `{ "error": { "code", "message", "docs_url" } }` (before: `{ "error": "text" }`).
- Changed: webhooks are signed as `X-Moleman-Signature: t=…,v1=…` over `<t>.<body>` with your account signing secret (before: `sha256=…` over the body, only with `webhookSecret`), and retried 8 times.
- Changed: find-contacts and verify results no longer carry company-register fields (they stay in deep-search).
- Changed: `GET /jobs/{id}` returns the job's own rows (before: the whole list), up to 1,000 per page.

**2026-10-31**
- First version: search, find-contacts, verify, deep-search, jobs, lists, webhooks.


---

# Clay

Use Moleman as a column in Clay with Clay's **HTTP API** enrichment. This is a plain HTTP setup, not an official Clay integration.

## Set up a column

1. In your Clay table, add a column → **HTTP API**.
2. **Method:** `POST`. **Endpoint:** `https://moleman.io/api/v1/find-contacts`
3. **Headers:** `Authorization` = `Bearer mm_live_…` (your key), `Content-Type` = `application/json`
4. **Body** (insert your columns with `/`):

```json
{
  "rows": [{ "firstName": "{{First Name}}", "lastName": "{{Last Name}}", "website": "{{Website}}" }],
  "wait": 25
}
```

5. Run it on a few rows. Map the fields from the answer: `results[0].email` → Email, `results[0].result` → Moleman result.

With `"wait": 25` most rows come back finished. If `status` is still `running`, add a second HTTP API column: `GET https://moleman.io/api/v1/jobs/{{jobId}}` with the same header, and map `results[0].email` from that.

## Other columns

| Column | Endpoint | Body |
| --- | --- | --- |
| Business email | `POST /api/v1/find-contacts` | `{"action":"companyInbox","rows":[{"website":"{{Website}}"}],"wait":25}` |
| Verify an email | `POST /api/v1/verify` | `{"emails":["{{Email}}"],"wait":25}` → `results[0].result` |
| Summary + first line | `POST /api/v1/deep-search` | `{"rows":[{"company":"{{Company}}","website":"{{Website}}"}],"wait":25}` → `results[0].briefLine` |

All settings in one file: [clay-template.json](https://moleman.io/api/v1/clay-template.json).

## Tips

- Try it with your **test key** first: free, made-up results.
- One row per request is fine in Clay; stay under 120 requests a minute per key (Clay's rate setting).
- Only "Works" costs credits. Rows you ran in the last 30 days are free when you run them again.


---

# Make, Zapier, n8n

Make, Zapier and n8n all work with the API through their generic HTTP and webhook steps. These are plain HTTP setups, not official integrations.

## The pattern

1. **Start the job** with an HTTP step: `POST https://moleman.io/api/v1/find-contacts` (or `/verify`, `/deep-search`).
   Headers: `Authorization: Bearer mm_live_…`, `Content-Type: application/json`, and `Idempotency-Key` = the trigger's record id (so a re-run never charges twice).
2. **Small jobs:** add `"wait": 25` to the body. The answer already has `results`; use `results[0].email` and `results[0].result` in the next step.
3. **Big lists:** add `"webhookUrl"` with the URL of a webhook trigger (a second scenario / Zap / workflow). We POST the finished job there.

## Make

- Module **HTTP → Make a request**. URL, method `POST`, headers as above, body type **Raw**, content type **JSON**, **Parse response: Yes**.
- For webhooks: a second scenario starting with **Webhooks → Custom webhook**. Copy its address into `webhookUrl`. Click **Redetermine data structure** and call [`POST /api/v1/webhooks/test`](https://moleman.io/docs/api/webhooks-test) with that address to send a sample.

## Zapier

- Action **Webhooks by Zapier → Custom Request** (method `POST`, data as raw JSON, headers as above). Zapier waits up to about 30 seconds, so keep `"wait": 25`.
- For webhooks: a Zap with trigger **Webhooks by Zapier → Catch Hook**. Use its URL as `webhookUrl`, and send a test with [`POST /api/v1/webhooks/test`](https://moleman.io/docs/api/webhooks-test).

## n8n

- Node **HTTP Request**: method `POST`, authentication **Generic → Header Auth** (name `Authorization`, value `Bearer mm_live_…`), body JSON.
- For webhooks: a workflow starting with a **Webhook** node (method POST, use the production URL). To check the signature, add a **Code** node with the code from [Webhooks](https://moleman.io/docs/webhooks) (enable "Raw body" on the Webhook node).

## Example body

```json
{
  "rows": [{ "firstName": "Sarah", "lastName": "Miller", "website": "brightsmiledental.com" }],
  "wait": 25
}
```

Use a **test key** while you build: free, made-up results, and webhooks are really sent.


---

# Instantly & Smartlead

Get a list as a CSV file that Instantly, Smartlead and most CRMs import without mapping work. This is a plain CSV export, not an official integration.

```bash
curl -o leads.csv "https://moleman.io/api/v1/lists/380/rows?format=csv&only=works" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY"
```

- `only=works`: only emails that **Work** (the ones we charged for). Leave it out for every row.
- The whole list in one file (no pages), UTF-8 with BOM, so Excel shows accents right.
- Cells that start with `=`, `+`, `-` or `@` get a leading `'` so a spreadsheet never runs them as formulas.

## Columns

| Column | What it is |
| --- | --- |
| `email` | The email |
| `first_name`, `last_name`, `full_name` | The person (when there is one) |
| `company_name`, `website` | The business |
| `job_title`, `phone`, `city`, `state`, `country` | When known |
| `result` | Works, Probably works, Not sure … |
| `email_type` | Business inbox / Owner email from their website / Owner email we found and checked |
| `personalization` | The first line from Deep Search (when you ran it) |
| `company_summary` | The Deep Search summary |

## Instantly

Leads → **Add leads → Upload CSV**. Instantly maps `email`, `first_name`, `last_name`, `company_name` and `website` by itself; map `personalization` to a custom variable and use `{{personalization}}` in your first line.

## Smartlead

Campaign → **Import leads → CSV**. Smartlead maps `email`, `first_name`, `last_name`, `company_name` and `website`; add `personalization` as a custom field.

To automate it: run the export from your script after the `job.completed` or `search.completed` [webhook](https://moleman.io/docs/webhooks), then upload the file with the tool's own API.


---

# Your credits, plan and rate limit

`GET /api/v1/account`

Shows which key you are using, how many credits are left, your plan, the prices per result and your rate limit. A good first call to check that your key works.

**Credits:** Free.

## Example request

```bash
curl "https://moleman.io/api/v1/account" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY"
```

## Example answer

```json
{
  "livemode": true,
  "key": {
    "name": "Clay",
    "prefix": "mm_live_Ab3x",
    "mode": "live"
  },
  "account": {
    "email": "you@yourcompany.com",
    "name": "Sam"
  },
  "credits": {
    "balance": 4210,
    "planCredits": 4000,
    "keptCredits": 210
  },
  "plan": {
    "id": "growth",
    "name": "Growth",
    "period": "monthly",
    "status": "active",
    "creditsPerMonth": 5000,
    "renewsAt": "2026-12-01T00:00:00.000Z"
  },
  "trial": null,
  "rateLimit": {
    "limit": 120,
    "remaining": 119,
    "resetSeconds": 60,
    "windowSeconds": 60
  },
  "limits": {
    "maxRowsPerRequest": 50000,
    "maxDeepSearchRows": 10000,
    "maxWaitSeconds": 30,
    "maxPageSize": 1000
  },
  "prices": {
    "verify": {
      "credits": 0.1,
      "charged": "you only pay when the email works (bad or unclear emails are free)"
    },
    "findEmail": {
      "credits": 1,
      "charged": "you only pay for emails that work (everything else is free)"
    }
  }
}
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |

---

# Find emails for people or businesses

`POST /api/v1/find-contacts`

Finds and checks emails. `findEmail`: a named person at a business (first name, last name and website). `companyInbox`: the business email (info@, office@ …). `roleContact`: the owner, only when the business names the owner on its own website.

Send up to 50,000 `rows` (or a `listId`). The job runs in the background: poll `GET /api/v1/jobs/{id}`, set `webhookUrl`, or send `wait` (up to 30 seconds) to get small jobs back finished.

Only emails that **Work** are delivered; the others come back with a plain reason and cost nothing.

**Credits:** 1 credit per email that Works (you only pay for emails that work (everything else is free)). Not found, not sure or "takes any name" results are free. Rows your account already got an answer for in the last 30 days are answered again for free (`cached30d`).

## Body (JSON)

| Name | Type | Required | What it is |
| --- | --- | --- | --- |
| `listId` | integer |  | Run on a list you already have (from POST /api/v1/lists or a search) instead of `rows`. |
| `contactIds` | array of integer |  | With listId: only these rows (default: all rows of the list). up to 50,000 |
| `name` | string |  | Name of the new list (shown in the app). Default: "API <action> (<date>)". |
| `action` | `"findEmail"` · `"companyInbox"` · `"roleContact"` |  | findEmail: a named person (first + last name + website). companyInbox: the business email (info@, office@ …). roleContact: the owner, only when the business names them on its own website. default `"findEmail"` |
| `rows` | array of objects |  | Up to 50,000 rows. up to 50,000 |
| `rows[].company` | string |  | Business name. |
| `rows[].website` | string |  | Website (any form: https://acme.com, acme.com/contact, www.acme.com). |
| `rows[].domain` | string |  | Domain, if you have it instead of a website. |
| `rows[].firstName` | string |  |  |
| `rows[].lastName` | string |  |  |
| `rows[].fullName` | string |  | Used when first and last name are not given separately. |
| `rows[].jobTitle` | string |  |  |
| `rows[].email` | string |  | The email to check (verify). |
| `rows[].phone` | string |  |  |
| `rows[].city` | string |  |  |
| `rows[].state` | string |  |  |
| `rows[].country` | string |  | Two-letter country code, e.g. US, CA, GB. |
| `rows[].address` | string |  | Business address. For US businesses it decides which state rules apply. |
| `rows[].category` | string |  |  |
| `webhookUrl` | string |  | Public https URL we POST the result to when the job is done (see the webhooks guide). |
| `webhookSecret` | string |  | Optional: sign this webhook with your own secret instead of the account signing secret. |
| `wait` | integer |  | Seconds to wait for the job to finish before answering (0–30). Handy for Zapier, Make and Clay: small jobs come back done, with results. default `0`, 0–30 |

## Example request

```bash
curl -X POST "https://moleman.io/api/v1/find-contacts" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"rows":[{"firstName":"Sarah","lastName":"Miller","website":"https://brightsmiledental.com"}],"wait":20}'
```

## Example answer

```json
{
  "jobId": 981,
  "listId": 377,
  "action": "findEmail",
  "status": "done",
  "livemode": true,
  "total": 1,
  "done": 1,
  "failed": 0,
  "creditsCharged": 1,
  "creditsHeld": 0,
  "message": null,
  "createdAt": 1762344000000,
  "finishedAt": 1762344014000,
  "results": [
    {
      "id": 4102,
      "company": "Bright Smile Dental",
      "domain": "brightsmiledental.com",
      "firstName": "Sarah",
      "lastName": "Miller",
      "fullName": "Sarah Miller",
      "jobTitle": null,
      "email": "sarah.miller@brightsmiledental.com",
      "result": "Works",
      "resultCode": "verified",
      "resultMeaning": "We checked it. This inbox is real, so you can send to it.",
      "emailStatus": "valid",
      "emailStatusText": "Works",
      "emailScore": 96,
      "resolveStatus": null,
      "resolveStatusText": null,
      "findStatus": "found",
      "findStatusText": "Found and checked",
      "emailType": null,
      "emailTypeText": null,
      "briefText": null,
      "briefLine": null,
      "briefRelevance": null,
      "extra": {}
    }
  ],
  "nextCursor": null,
  "queued": 1,
  "skipped": [],
  "cached30d": 0,
  "maxCreditsHeld": 1,
  "pollUrl": "/api/v1/jobs/981"
}
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |
| 400 | [`invalid_request`](https://moleman.io/docs/errors#invalid_request) | A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path. |
| 400 | [`invalid_json`](https://moleman.io/docs/errors#invalid_json) | The body is not valid JSON. |
| 409 | [`idempotency_in_progress`](https://moleman.io/docs/errors#idempotency_in_progress) | A request with the same Idempotency-Key is still running. |
| 422 | [`idempotency_key_reused`](https://moleman.io/docs/errors#idempotency_key_reused) | The Idempotency-Key was already used in the last 24 hours with a different request. |
| 400 | [`no_eligible_rows`](https://moleman.io/docs/errors#no_eligible_rows) | None of the rows can run this action (for example: no website or domain to find an email for, or no email to check). |
| 402 | [`insufficient_credits`](https://moleman.io/docs/errors#insufficient_credits) | The account has fewer credits than this request could use at most. We hold the maximum while the job runs and give back what it does not use. |
| 404 | [`not_found`](https://moleman.io/docs/errors#not_found) | The job or list does not exist, belongs to another account, or was deleted. |
| 400 | [`invalid_webhook_url`](https://moleman.io/docs/errors#invalid_webhook_url) | The webhook URL is not a public https address (private networks and other schemes are refused). |
| 403 | [`wrong_key_mode`](https://moleman.io/docs/errors#wrong_key_mode) | A test key asked for something made with a live key, or the other way round. Test and live data never mix. |

---

# Check if emails work

`POST /api/v1/verify`

Checks each email with the mail server. "Takes any name" addresses (the server accepts everything) get a second check and come back as Probably works or Not sure. Send `emails`, `rows` with an `email` field, or a `listId`.

**Credits:** 0.1 credits per email that Works (you only pay when the email works (bad or unclear emails are free)). Rows your account already got an answer for in the last 30 days are answered again for free (`cached30d`).

## Body (JSON)

| Name | Type | Required | What it is |
| --- | --- | --- | --- |
| `listId` | integer |  | Run on a list you already have (from POST /api/v1/lists or a search) instead of `rows`. |
| `contactIds` | array of integer |  | With listId: only these rows (default: all rows of the list). up to 50,000 |
| `name` | string |  | Name of the new list (shown in the app). Default: "API <action> (<date>)". |
| `emails` | array of string |  | The emails to check (or send `rows` with an `email` field). up to 50,000 |
| `rows` | array of objects |  | up to 50,000 |
| `rows[].company` | string |  | Business name. |
| `rows[].website` | string |  | Website (any form: https://acme.com, acme.com/contact, www.acme.com). |
| `rows[].domain` | string |  | Domain, if you have it instead of a website. |
| `rows[].firstName` | string |  |  |
| `rows[].lastName` | string |  |  |
| `rows[].fullName` | string |  | Used when first and last name are not given separately. |
| `rows[].jobTitle` | string |  |  |
| `rows[].email` | string |  | The email to check (verify). |
| `rows[].phone` | string |  |  |
| `rows[].city` | string |  |  |
| `rows[].state` | string |  |  |
| `rows[].country` | string |  | Two-letter country code, e.g. US, CA, GB. |
| `rows[].address` | string |  | Business address. For US businesses it decides which state rules apply. |
| `rows[].category` | string |  |  |
| `webhookUrl` | string |  | Public https URL we POST the result to when the job is done (see the webhooks guide). |
| `webhookSecret` | string |  | Optional: sign this webhook with your own secret instead of the account signing secret. |
| `wait` | integer |  | Seconds to wait for the job to finish before answering (0–30). Handy for Zapier, Make and Clay: small jobs come back done, with results. default `0`, 0–30 |

## Example request

```bash
curl -X POST "https://moleman.io/api/v1/verify" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"emails":["sarah.miller@brightsmiledental.com","info@brightsmiledental.com"],"webhookUrl":"https://hooks.zapier.com/hooks/catch/123/abc/"}'
```

## Example answer

```json
{
  "jobId": 982,
  "listId": 378,
  "status": "queued",
  "livemode": true,
  "queued": 2,
  "skipped": [],
  "cached30d": 0,
  "maxCreditsHeld": 0.2,
  "pollUrl": "/api/v1/jobs/982"
}
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |
| 400 | [`invalid_request`](https://moleman.io/docs/errors#invalid_request) | A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path. |
| 400 | [`invalid_json`](https://moleman.io/docs/errors#invalid_json) | The body is not valid JSON. |
| 409 | [`idempotency_in_progress`](https://moleman.io/docs/errors#idempotency_in_progress) | A request with the same Idempotency-Key is still running. |
| 422 | [`idempotency_key_reused`](https://moleman.io/docs/errors#idempotency_key_reused) | The Idempotency-Key was already used in the last 24 hours with a different request. |
| 400 | [`no_eligible_rows`](https://moleman.io/docs/errors#no_eligible_rows) | None of the rows can run this action (for example: no website or domain to find an email for, or no email to check). |
| 402 | [`insufficient_credits`](https://moleman.io/docs/errors#insufficient_credits) | The account has fewer credits than this request could use at most. We hold the maximum while the job runs and give back what it does not use. |
| 404 | [`not_found`](https://moleman.io/docs/errors#not_found) | The job or list does not exist, belongs to another account, or was deleted. |
| 400 | [`invalid_webhook_url`](https://moleman.io/docs/errors#invalid_webhook_url) | The webhook URL is not a public https address (private networks and other schemes are refused). |
| 403 | [`wrong_key_mode`](https://moleman.io/docs/errors#wrong_key_mode) | A test key asked for something made with a live key, or the other way round. Test and live data never mix. |

---

# Company summary and first line

`POST /api/v1/deep-search`

Reads each business's own website and writes a short summary, why your offer could matter to them (set "What you sell" in the app) and a personal first line. For GB and AU businesses it adds official company-registry facts. Registry data is never used to find or guess emails.

**Credits:** 0.5 credits per business we write a summary for (you only pay when we write the report). Rows your account already got an answer for in the last 30 days are answered again for free (`cached30d`).

## Body (JSON)

| Name | Type | Required | What it is |
| --- | --- | --- | --- |
| `listId` | integer |  | Run on a list you already have (from POST /api/v1/lists or a search) instead of `rows`. |
| `contactIds` | array of integer |  | With listId: only these rows (default: all rows of the list). up to 50,000 |
| `name` | string |  | Name of the new list (shown in the app). Default: "API <action> (<date>)". |
| `rows` | array of objects |  | Up to 10,000 businesses (company + website). up to 10,000 |
| `rows[].company` | string |  | Business name. |
| `rows[].website` | string |  | Website (any form: https://acme.com, acme.com/contact, www.acme.com). |
| `rows[].domain` | string |  | Domain, if you have it instead of a website. |
| `rows[].firstName` | string |  |  |
| `rows[].lastName` | string |  |  |
| `rows[].fullName` | string |  | Used when first and last name are not given separately. |
| `rows[].jobTitle` | string |  |  |
| `rows[].email` | string |  | The email to check (verify). |
| `rows[].phone` | string |  |  |
| `rows[].city` | string |  |  |
| `rows[].state` | string |  |  |
| `rows[].country` | string |  | Two-letter country code, e.g. US, CA, GB. |
| `rows[].address` | string |  | Business address. For US businesses it decides which state rules apply. |
| `rows[].category` | string |  |  |
| `webhookUrl` | string |  | Public https URL we POST the result to when the job is done (see the webhooks guide). |
| `webhookSecret` | string |  | Optional: sign this webhook with your own secret instead of the account signing secret. |
| `wait` | integer |  | Seconds to wait for the job to finish before answering (0–30). Handy for Zapier, Make and Clay: small jobs come back done, with results. default `0`, 0–30 |

## Example request

```bash
curl -X POST "https://moleman.io/api/v1/deep-search" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"rows":[{"company":"Bright Smile Dental","website":"brightsmiledental.com","city":"Austin","country":"US"}]}'
```

## Example answer

```json
{
  "jobId": 983,
  "listId": 379,
  "status": "queued",
  "livemode": true,
  "queued": 1,
  "skipped": [],
  "cached30d": 0,
  "maxCreditsHeld": 0.5,
  "pollUrl": "/api/v1/jobs/983"
}
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |
| 400 | [`invalid_request`](https://moleman.io/docs/errors#invalid_request) | A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path. |
| 400 | [`invalid_json`](https://moleman.io/docs/errors#invalid_json) | The body is not valid JSON. |
| 409 | [`idempotency_in_progress`](https://moleman.io/docs/errors#idempotency_in_progress) | A request with the same Idempotency-Key is still running. |
| 422 | [`idempotency_key_reused`](https://moleman.io/docs/errors#idempotency_key_reused) | The Idempotency-Key was already used in the last 24 hours with a different request. |
| 400 | [`no_eligible_rows`](https://moleman.io/docs/errors#no_eligible_rows) | None of the rows can run this action (for example: no website or domain to find an email for, or no email to check). |
| 402 | [`insufficient_credits`](https://moleman.io/docs/errors#insufficient_credits) | The account has fewer credits than this request could use at most. We hold the maximum while the job runs and give back what it does not use. |
| 404 | [`not_found`](https://moleman.io/docs/errors#not_found) | The job or list does not exist, belongs to another account, or was deleted. |
| 400 | [`invalid_webhook_url`](https://moleman.io/docs/errors#invalid_webhook_url) | The webhook URL is not a public https address (private networks and other schemes are refused). |
| 403 | [`wrong_key_mode`](https://moleman.io/docs/errors#wrong_key_mode) | A test key asked for something made with a live key, or the other way round. Test and live data never mix. |

---

# Find businesses in an area, with emails

`POST /api/v1/search`

Finds businesses of one kind in a place (for example dentists in Austin, TX) and the best email for each: the business email or the owner's, checked. It runs in rounds until it has `target` emails that work (or searched the whole area). Poll `GET /api/v1/lists/{listId}` or set `webhookUrl` (event `search.completed`).

Owner emails: only owners the business names on its own website. For businesses in Vermont, Oregon and Connecticut we deliver owner emails only when the website prints them.

**Credits:** 1 credit per delivered email that Works. Businesses without a website, and emails we could not confirm, are free. With `brief: true`, plus 0.5 credits per summary.

## Body (JSON)

| Name | Type | Required | What it is |
| --- | --- | --- | --- |
| `category` | string | yes | What kind of business, e.g. "dentist", "roofing contractor", "yoga studio". |
| `location` | string | yes | City, county, state or ZIP / post code. |
| `country` | `"US"` · `"CA"` · `"GB"` · `"AU"` · `"NZ"` · `"IE"` · `"DE"` · `"AT"` · `"CH"` · `"FR"` · `"ES"` · `"IT"` · `"NL"` · `"BE"` · `"LU"` · `"PT"` · `"DK"` · `"SE"` · `"FI"` · `"PL"` · `"CZ"` · `"MX"` | yes | Country of the location. |
| `mode` | `"target"` · `"all"` |  | target: stop at `target` emails that work. all: search the whole area (up to creditLimit). default `"target"` |
| `target` | integer |  | How many emails that work you want (mode "target"). default `25` |
| `creditLimit` | number |  | mode "all": stop when this many credits are used (default: until your credits run out). |
| `role` | `"any"` · `"best"` · `"owner"` · `"marketing"` · `"sales"` |  | any: the business email (info@, office@ …). best: the owner when the business names one on its own website, else the business email. owner: only owners named on the business's own website. default `"any"` |
| `brief` | boolean |  | Also write a short company summary and a first line for each business. default `false` |
| `webhookUrl` | string |  | Public https URL we POST the result to when the job is done (see the webhooks guide). |
| `webhookSecret` | string |  | Optional: sign this webhook with your own secret instead of the account signing secret. |

## Example request

```bash
curl -X POST "https://moleman.io/api/v1/search" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"category":"dentist","location":"Austin, TX","country":"US","target":25}'
```

## Example answer

```json
{
  "runId": 55,
  "listId": 380,
  "jobId": 984,
  "livemode": true,
  "run": {
    "status": "running",
    "delivered": 0,
    "creditsUsed": 0
  },
  "pollUrl": "/api/v1/lists/380"
}
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |
| 400 | [`invalid_request`](https://moleman.io/docs/errors#invalid_request) | A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path. |
| 400 | [`invalid_json`](https://moleman.io/docs/errors#invalid_json) | The body is not valid JSON. |
| 409 | [`idempotency_in_progress`](https://moleman.io/docs/errors#idempotency_in_progress) | A request with the same Idempotency-Key is still running. |
| 422 | [`idempotency_key_reused`](https://moleman.io/docs/errors#idempotency_key_reused) | The Idempotency-Key was already used in the last 24 hours with a different request. |
| 402 | [`insufficient_credits`](https://moleman.io/docs/errors#insufficient_credits) | The account has fewer credits than this request could use at most. We hold the maximum while the job runs and give back what it does not use. |
| 404 | [`place_not_found`](https://moleman.io/docs/errors#place_not_found) | We could not find the place you searched in. |
| 503 | [`search_unavailable`](https://moleman.io/docs/errors#search_unavailable) | Business search is not available right now. |
| 400 | [`invalid_webhook_url`](https://moleman.io/docs/errors#invalid_webhook_url) | The webhook URL is not a public https address (private networks and other schemes are refused). |

---

# Your jobs, newest first

`GET /api/v1/jobs`

All jobs of your account (made with keys of the same kind), newest first, without their results. Page with `cursor`.

**Credits:** Free.

## Parameters

| Name | Type | Required | What it is |
| --- | --- | --- | --- |
| `limit` | integer | yes | Rows per page, 1–1000. default `100`, 1–1000 |
| `cursor` | string |  | The `nextCursor` of the page before. Leave out for the first page. |

## Example request

```bash
curl "https://moleman.io/api/v1/jobs" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY"
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |
| 400 | [`invalid_request`](https://moleman.io/docs/errors#invalid_request) | A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path. |
| 400 | [`invalid_cursor`](https://moleman.io/docs/errors#invalid_cursor) | The cursor is not one we gave out (they are opaque; do not build them yourself). |

---

# A job's status and results

`GET /api/v1/jobs/{id}`

Status (`queued`, `running`, `paused`, `done`, `cancelled`, `failed`), progress and the results so far. Poll every 5–10 seconds until `status` is `done`, or use a webhook. More than 1,000 results: page with `cursor`.

**Credits:** Free.

## Parameters

| Name | Type | Required | What it is |
| --- | --- | --- | --- |
| `id` | integer | yes | The id. |
| `limit` | integer | yes | Rows per page, 1–1000. default `1000`, 1–1000 |
| `cursor` | string |  | The `nextCursor` of the page before. Leave out for the first page. |

## Example request

```bash
curl "https://moleman.io/api/v1/jobs/981" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY"
```

## Example answer

```json
{
  "jobId": 981,
  "listId": 377,
  "action": "findEmail",
  "status": "done",
  "livemode": true,
  "total": 1,
  "done": 1,
  "failed": 0,
  "creditsCharged": 1,
  "creditsHeld": 0,
  "message": null,
  "createdAt": 1762344000000,
  "finishedAt": 1762344014000,
  "results": [
    {
      "id": 4102,
      "company": "Bright Smile Dental",
      "domain": "brightsmiledental.com",
      "firstName": "Sarah",
      "lastName": "Miller",
      "fullName": "Sarah Miller",
      "jobTitle": null,
      "email": "sarah.miller@brightsmiledental.com",
      "result": "Works",
      "resultCode": "verified",
      "resultMeaning": "We checked it. This inbox is real, so you can send to it.",
      "emailStatus": "valid",
      "emailStatusText": "Works",
      "emailScore": 96,
      "resolveStatus": null,
      "resolveStatusText": null,
      "findStatus": "found",
      "findStatusText": "Found and checked",
      "emailType": null,
      "emailTypeText": null,
      "briefText": null,
      "briefLine": null,
      "briefRelevance": null,
      "extra": {}
    }
  ],
  "nextCursor": null
}
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |
| 400 | [`invalid_request`](https://moleman.io/docs/errors#invalid_request) | A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path. |
| 400 | [`invalid_cursor`](https://moleman.io/docs/errors#invalid_cursor) | The cursor is not one we gave out (they are opaque; do not build them yourself). |
| 404 | [`not_found`](https://moleman.io/docs/errors#not_found) | The job or list does not exist, belongs to another account, or was deleted. |
| 403 | [`wrong_key_mode`](https://moleman.io/docs/errors#wrong_key_mode) | A test key asked for something made with a live key, or the other way round. Test and live data never mix. |

---

# Stop a job

`POST /api/v1/jobs/{id}/cancel`

Stops a running job. Results already delivered stay (and stay charged); the credits held for the rest go back at once.

**Credits:** Free.

## Parameters

| Name | Type | Required | What it is |
| --- | --- | --- | --- |
| `id` | integer | yes | The id. |

## Example request

```bash
curl -X POST "https://moleman.io/api/v1/jobs/123/cancel" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY"
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |
| 400 | [`invalid_request`](https://moleman.io/docs/errors#invalid_request) | A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path. |
| 400 | [`invalid_json`](https://moleman.io/docs/errors#invalid_json) | The body is not valid JSON. |
| 409 | [`idempotency_in_progress`](https://moleman.io/docs/errors#idempotency_in_progress) | A request with the same Idempotency-Key is still running. |
| 422 | [`idempotency_key_reused`](https://moleman.io/docs/errors#idempotency_key_reused) | The Idempotency-Key was already used in the last 24 hours with a different request. |
| 404 | [`not_found`](https://moleman.io/docs/errors#not_found) | The job or list does not exist, belongs to another account, or was deleted. |
| 403 | [`wrong_key_mode`](https://moleman.io/docs/errors#wrong_key_mode) | A test key asked for something made with a live key, or the other way round. Test and live data never mix. |

---

# Upload a list

`POST /api/v1/lists`

Stores rows as a list (it shows up in the app). Then run find-contacts, verify or deep-search on it with `listId`. Most integrations skip this and send `rows` directly.

**Credits:** Free.

## Body (JSON)

| Name | Type | Required | What it is |
| --- | --- | --- | --- |
| `name` | string |  | default `"API List"` |
| `rows` | array of objects | yes | up to 50,000 |
| `rows[].company` | string |  | Business name. |
| `rows[].website` | string |  | Website (any form: https://acme.com, acme.com/contact, www.acme.com). |
| `rows[].domain` | string |  | Domain, if you have it instead of a website. |
| `rows[].firstName` | string |  |  |
| `rows[].lastName` | string |  |  |
| `rows[].fullName` | string |  | Used when first and last name are not given separately. |
| `rows[].jobTitle` | string |  |  |
| `rows[].email` | string |  | The email to check (verify). |
| `rows[].phone` | string |  |  |
| `rows[].city` | string |  |  |
| `rows[].state` | string |  |  |
| `rows[].country` | string |  | Two-letter country code, e.g. US, CA, GB. |
| `rows[].address` | string |  | Business address. For US businesses it decides which state rules apply. |
| `rows[].category` | string |  |  |

## Example request

```bash
curl -X POST "https://moleman.io/api/v1/lists" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name":"Austin dentists","rows":[{"company":"Bright Smile Dental","website":"brightsmiledental.com","firstName":"Sarah","lastName":"Miller"}]}'
```

## Example answer

```json
{
  "listId": 381,
  "rowCount": 1,
  "contactIds": [
    4110
  ],
  "livemode": true
}
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |
| 400 | [`invalid_request`](https://moleman.io/docs/errors#invalid_request) | A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path. |
| 400 | [`invalid_json`](https://moleman.io/docs/errors#invalid_json) | The body is not valid JSON. |
| 409 | [`idempotency_in_progress`](https://moleman.io/docs/errors#idempotency_in_progress) | A request with the same Idempotency-Key is still running. |
| 422 | [`idempotency_key_reused`](https://moleman.io/docs/errors#idempotency_key_reused) | The Idempotency-Key was already used in the last 24 hours with a different request. |

---

# Your lists, newest first

`GET /api/v1/lists`

All lists of your account (uploads, API jobs and business searches), newest first.

**Credits:** Free.

## Parameters

| Name | Type | Required | What it is |
| --- | --- | --- | --- |
| `limit` | integer | yes | Rows per page, 1–1000. default `100`, 1–1000 |
| `cursor` | string |  | The `nextCursor` of the page before. Leave out for the first page. |

## Example request

```bash
curl "https://moleman.io/api/v1/lists" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY"
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |
| 400 | [`invalid_request`](https://moleman.io/docs/errors#invalid_request) | A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path. |
| 400 | [`invalid_cursor`](https://moleman.io/docs/errors#invalid_cursor) | The cursor is not one we gave out (they are opaque; do not build them yourself). |

---

# A list with its results

`GET /api/v1/lists/{id}`

The list, the progress of its business search (if it is one) and up to 1,000 rows per page.

**Credits:** Free.

## Parameters

| Name | Type | Required | What it is |
| --- | --- | --- | --- |
| `id` | integer | yes | The id. |
| `limit` | integer | yes | Rows per page, 1–1000. default `1000`, 1–1000 |
| `cursor` | string |  | The `nextCursor` of the page before. Leave out for the first page. |

## Example request

```bash
curl "https://moleman.io/api/v1/lists/123" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY"
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |
| 400 | [`invalid_request`](https://moleman.io/docs/errors#invalid_request) | A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path. |
| 400 | [`invalid_cursor`](https://moleman.io/docs/errors#invalid_cursor) | The cursor is not one we gave out (they are opaque; do not build them yourself). |
| 404 | [`not_found`](https://moleman.io/docs/errors#not_found) | The job or list does not exist, belongs to another account, or was deleted. |
| 403 | [`wrong_key_mode`](https://moleman.io/docs/errors#wrong_key_mode) | A test key asked for something made with a live key, or the other way round. Test and live data never mix. |

---

# Rows as JSON pages or one CSV file

`GET /api/v1/lists/{id}/rows`

The rows of a list. `format=json` (default): pages of `limit` rows with `nextCursor`. `format=csv`: the whole list as one CSV file with plain column names (email, first_name, last_name, company_name, website, personalization …) that Instantly, Smartlead and most CRMs map without help. `only=works` keeps only emails that Work.

**Credits:** Free.

## Parameters

| Name | Type | Required | What it is |
| --- | --- | --- | --- |
| `id` | integer | yes | The id. |
| `limit` | integer | yes | Rows per page, 1–1000. default `100`, 1–1000 |
| `cursor` | string |  | The `nextCursor` of the page before. Leave out for the first page. |
| `format` | `"json"` · `"csv"` | yes | csv: the whole list as one CSV file (no paging), ready for Instantly, Smartlead or a CRM. default `"json"` |
| `only` | `"all"` · `"works"` | yes | works: only rows whose email Works. default `"all"` |

## Example request

```bash
curl "https://moleman.io/api/v1/lists/123/rows" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY"
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |
| 400 | [`invalid_request`](https://moleman.io/docs/errors#invalid_request) | A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path. |
| 400 | [`invalid_cursor`](https://moleman.io/docs/errors#invalid_cursor) | The cursor is not one we gave out (they are opaque; do not build them yourself). |
| 404 | [`not_found`](https://moleman.io/docs/errors#not_found) | The job or list does not exist, belongs to another account, or was deleted. |
| 403 | [`wrong_key_mode`](https://moleman.io/docs/errors#wrong_key_mode) | A test key asked for something made with a live key, or the other way round. Test and live data never mix. |

---

# Delete a list

`DELETE /api/v1/lists/{id}`

Deletes the list and all its rows and results for good (running jobs on it stop). The same as deleting it in the app.

**Credits:** Free.

## Parameters

| Name | Type | Required | What it is |
| --- | --- | --- | --- |
| `id` | integer | yes | The id. |

## Example request

```bash
curl -X DELETE "https://moleman.io/api/v1/lists/123" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY"
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |
| 400 | [`invalid_request`](https://moleman.io/docs/errors#invalid_request) | A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path. |
| 404 | [`not_found`](https://moleman.io/docs/errors#not_found) | The job or list does not exist, belongs to another account, or was deleted. |
| 403 | [`wrong_key_mode`](https://moleman.io/docs/errors#wrong_key_mode) | A test key asked for something made with a live key, or the other way round. Test and live data never mix. |

---

# Send a signed test event

`POST /api/v1/webhooks/test`

Sends a signed `ping` event to your URL right now and tells you what your server answered. Use it to set up Make, Zapier or n8n, and to test your signature check.

**Credits:** Free.

## Body (JSON)

| Name | Type | Required | What it is |
| --- | --- | --- | --- |
| `url` | string | yes | The https URL to send a signed test event ("ping") to. |
| `webhookSecret` | string |  | Optional: sign this webhook with your own secret instead of the account signing secret. |

## Example request

```bash
curl -X POST "https://moleman.io/api/v1/webhooks/test" \
  -H "Authorization: Bearer $MOLEMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"url":"https://hook.eu2.make.com/abc123"}'
```

## Example answer

```json
{
  "delivered": true,
  "statusCode": 200,
  "error": null,
  "signature": "t=1762344000,v1=5f2c…"
}
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | [`missing_api_key`](https://moleman.io/docs/errors#missing_api_key) | The request has no API key. |
| 401 | [`invalid_api_key`](https://moleman.io/docs/errors#invalid_api_key) | The API key is not one of ours (a typo, a cut-off copy, or a key from another account that was deleted). |
| 401 | [`revoked_api_key`](https://moleman.io/docs/errors#revoked_api_key) | The API key was revoked on the API page. Revoking takes effect at once. |
| 429 | [`rate_limited`](https://moleman.io/docs/errors#rate_limited) | More than 120 requests in one minute with this key. |
| 500 | [`server_error`](https://moleman.io/docs/errors#server_error) | Something broke on our side. Nothing was charged for it. |
| 400 | [`invalid_request`](https://moleman.io/docs/errors#invalid_request) | A parameter is missing, has the wrong type or is out of range. `details` lists every problem with its path. |
| 400 | [`invalid_json`](https://moleman.io/docs/errors#invalid_json) | The body is not valid JSON. |
| 409 | [`idempotency_in_progress`](https://moleman.io/docs/errors#idempotency_in_progress) | A request with the same Idempotency-Key is still running. |
| 422 | [`idempotency_key_reused`](https://moleman.io/docs/errors#idempotency_key_reused) | The Idempotency-Key was already used in the last 24 hours with a different request. |
| 400 | [`invalid_webhook_url`](https://moleman.io/docs/errors#invalid_webhook_url) | The webhook URL is not a public https address (private networks and other schemes are refused). |

---

# This API as OpenAPI 3.1

`GET /api/v1/openapi.json`

The OpenAPI 3.1 description of every endpoint, built from the same schemas the server checks requests with. No key needed.

**Credits:** Free.

No API key needed.

## Get it

```bash
curl "https://moleman.io/api/v1/openapi.json"
```

---

# All docs as one Markdown file

`GET /api/v1/docs.md`

Every guide and endpoint in one Markdown file, for reading offline or giving to an AI agent. No key needed.

**Credits:** Free.

No API key needed.

## Get it

```bash
curl "https://moleman.io/api/v1/docs.md"
```

---

# Clay HTTP API column settings

`GET /api/v1/clay-template.json`

Ready settings for Clay's "HTTP API" column (find email, verify, Deep Search). No key needed. See the Clay guide.

**Credits:** Free.

No API key needed.

## Get it

```bash
curl "https://moleman.io/api/v1/clay-template.json"
```
