# 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](/docs/credits).

## Next

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

