LeadModule
Back to blog
Tutorial

LeadModule API Quickstart: Enrich Your First 1,000 Contacts

·5 min read·Marco Kwak, Founder

Use the LeadModule API to enrich contacts programmatically. This quickstart covers single lookups, async polling, batch processing, and webhook callbacks in Python and TypeScript.

The LeadModule API lets you enrich contacts programmatically without a dashboard. This guide covers single-contact lookups, async polling, batch processing, and webhook callbacks.

The API is asynchronous: you submit a contact, receive a runId, and poll for the result (or use webhooks for push-based delivery). This design lets the waterfall engine query multiple providers without blocking your application.

Get Your API Key

Sign up at app.leadmodule.ai — the free tier includes credits to get started. Once logged in, navigate to Settings → API Keys → Create Key.

Your key will have the format lm_live_ followed by a random string. It's shown once on creation — store it in your environment variables, not in your code.

The default rate limit is 60 requests per minute per API key.

Single Contact Enrichment

The basic flow: POST to submit → receive runId → poll GET until status: "completed".

Python:

import requests
import time
 
API_KEY = "lm_live_xxxxxxxxxxxxxxxx"
BASE_URL = "https://app.leadmodule.ai/api/v1/enrich"
 
# Step 1: Submit enrichment request
response = requests.post(BASE_URL, json={
    "firstName": "Jane",
    "lastName": "Smith",
    "companyDomain": "acme.com",
    "linkedInUrl": "https://linkedin.com/in/janesmith",
    "enrichmentType": "email"
}, headers={"Authorization": f"Bearer {API_KEY}"})
 
run = response.json()  # { "runId": "...", "status": "pending" }
 
# Step 2: Poll for result
while True:
    result = requests.get(f"{BASE_URL}/{run['runId']}",
                          headers={"Authorization": f"Bearer {API_KEY}"}).json()
    if result["status"] in ("completed", "failed"):
        break
    time.sleep(5)
 
# Step 3: Use the result
if result.get("resultStatus") == "found":
    print(f"Email: {result['result']['email']}")
    print(f"Confidence: {result['result']['confidence']}")
    print(f"Provider: {result['result']['provider']}")

TypeScript:

const API_KEY = "lm_live_xxxxxxxxxxxxxxxx"
const BASE_URL = "https://app.leadmodule.ai/api/v1/enrich"
 
// Step 1: Submit enrichment request
const submitRes = await fetch(BASE_URL, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    firstName: "Jane",
    lastName: "Smith",
    companyDomain: "acme.com",
    linkedInUrl: "https://linkedin.com/in/janesmith",
    enrichmentType: "email",
  }),
})
 
const { runId } = await submitRes.json()
 
// Step 2: Poll for result
let result
while (true) {
  const pollRes = await fetch(`${BASE_URL}/${runId}`, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  })
  result = await pollRes.json()
  if (result.status === "completed" || result.status === "failed") break
  await new Promise((r) => setTimeout(r, 5000))
}
 
// Step 3: Use the result
if (result.resultStatus === "found") {
  console.log(`Email: ${result.result.email}`)
  console.log(`Confidence: ${result.result.confidence}`)
}

Understanding the Response

A completed enrichment response looks like this:

{
  "runId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "resultStatus": "found",
  "creditsUsed": 1,
  "result": {
    "email": "jane.smith@acme.com",
    "confidence": 95,
    "provider": "prospeo",
    "validation": {
      "status": "valid",
      "score": 100
    }
  }
}

Key fields to check:

  • resultStatus: "found" | "not_found" | "error" — whether enrichment succeeded
  • result.confidence: 0–100, the provider's confidence in the result
  • result.validation.status: "valid" | "invalid" | "risky" | "unknown" — email deliverability
  • creditsUsed: credits consumed for this request

Duplicate requests return immediately with HTTP 200 (not 202), cached: true, and cost zero credits. The cache lasts 30 days. Use forceRefresh: true to bypass it.

Batch Processing (1,000 Contacts)

There is no batch endpoint — submit one contact at a time with rate limiting. At 60 requests per minute, 1,000 contacts takes approximately 17 minutes to submit. Processing time is additional.

import time
 
contacts = [...]  # List of dicts with firstName, lastName, companyDomain
 
results = []
for i, contact in enumerate(contacts):
    response = requests.post(BASE_URL, json={
        **contact,
        "enrichmentType": "email"
    }, headers={"Authorization": f"Bearer {API_KEY}"})
 
    if response.status_code == 429:
        retry_after = int(response.headers.get("Retry-After", 60))
        time.sleep(retry_after)
        continue  # Retry this contact
 
    results.append(response.json())
 
    # Respect rate limit: 60 req/min = 1 req/sec
    time.sleep(1)

For high-volume processing, the webhook approach below is more efficient — you fire all requests and handle results as they arrive, rather than polling each one.

Using Webhooks Instead of Polling

Add webhookUrl to your request body. LeadModule will POST the result to your URL when enrichment completes, with no polling required on your end.

requests.post(BASE_URL, json={
    "firstName": "Jane",
    "lastName": "Smith",
    "companyDomain": "acme.com",
    "enrichmentType": "email",
    "webhookUrl": "https://your-app.com/webhooks/enrichment"
}, headers={"Authorization": f"Bearer {API_KEY}"})

The webhook payload includes runId, status, result, and creditsUsed. The request header X-LeadModule-Event: enrichment.completed identifies the event type. Delivery is retried up to 3 times with exponential backoff if your endpoint returns a non-2xx response.

Error Handling

StatusMeaningAction
202Request acceptedPoll for result or wait for webhook
200Cache hitResult already available, zero credits
400Invalid inputCheck required fields (need at least one of firstName, lastName, companyDomain, linkedInUrl)
401Bad API keyVerify key starts with lm_live_ and is active
402No creditsTop up credits or upgrade plan
429Rate limitedWait for Retry-After seconds, then retry

Get Your API Key

Sign up free. No credit card required. Start enriching contacts in minutes.

Create Free Account

Frequently Asked Questions

Is there a batch enrichment endpoint?

Not currently. Submit one contact per request and respect the rate limit. For high-volume processing, use the webhook approach to avoid polling overhead.

What is the rate limit?

Default is 60 requests per minute per API key. Rate-limited requests return 429 with a Retry-After header.

How much does each API call cost?

Each enrichment costs 1 credit. Cached (duplicate) requests cost zero credits. The free tier includes credits to start.

What programming languages can I use?

Any language with HTTP support. The API is standard REST — POST to submit, GET to poll. This guide shows Python and TypeScript.

How do I handle enrichment failures?

Check resultStatus in the response. 'not_found' means no provider returned a result. 'error' means something went wrong. Neither is charged differently from a 'found' result.

Can I enrich phone numbers too?

Yes. Set enrichmentType to 'phone' or 'all' (email + phone). Phone results appear in result.phone.