Generate keys in Settings → API. Each key is shown once. Store it like a password. You can disable or rotate keys at any time.
Authentication
Every request must carry an Authorization header:
Authorization: Bearer mz_live_<your_key>
Keys are scoped to a single account and counted against that account's credit balance. There is no separate API quota. What you see in your dashboard is what you can spend. Keys can be disabled or deleted at any time; revoked keys return 401 unauthorized.
Endpoint catalogue
The nine tools, their synchronous endpoint, their bulk sibling, and what a credit buys. Bulk jobs of every kind share the same status, list, download, and delete endpoints.
| Tool | Synchronous | Bulk upload | Cost | Billed when |
|---|---|---|---|---|
| Email finder | /api/find-email | /api/bulk/upload | 1 | an address is verified |
| Email verifier | /api/verify | /api/bulk-verify/upload | 0.1 | a verdict is produced |
| Domain finder | /api/find-domain | /api/bulk-domain/upload | 0.1 | a domain clears the confidence gate |
| Domain scraper | /api/scrape-domain | /api/bulk-domain-scraper/upload | 1 | the domain is scanned |
| LinkedIn profile | /api/linkedin-profile | /api/bulk-linkedin/upload | 1.5 | the profile is found |
| LinkedIn company | /api/linkedin-company | /api/bulk-linkedin-company/upload | 0.5 | the page is scraped |
| LinkedIn URL finder | /api/linkedin-finder | /api/bulk-linkedin-finder/upload | 1.5 | a profile is delivered |
| Google Maps scraper | /api/google-maps/search /api/scrape-google-maps | /api/bulk-google-maps/upload | 1 | a listing is delivered |
| Reverse lookup | /api/reverse-lookup | /api/bulk-reverse-lookup/upload | 2 | a profile is delivered |
/api/find-email
Find a verified email from first name, last name, and a company domain or name. The call is synchronous: half of all lookups return in under 3 seconds and roughly seven in ten in under 8, but a hard target that needs the SMTP-confirmation step can run to the 25-second ceiling. Past that ceiling the request comes back as not-found, free. One credit is charged only on a verified result.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| firstName | string | yes | Up to 80 chars. |
| lastName | string | yes | Up to 80 chars. |
| domainOrCompany | string | yes | Either a domain (acme.com) or a company name (Acme Inc). Domains yield the most precise results. |
Example: curl
curl -X POST https://mozartemail.com/api/find-email \
-H "Authorization: Bearer mz_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Clara",
"lastName": "Weber",
"domainOrCompany": "deutschebank.com"
}'Response: verified
{
"ok": true,
"status": "verified",
"email": "clara.weber@deutschebank.com",
"domain": "deutschebank.com",
"confidence": 95,
"provider": "Microsoft 365",
"rawStatus": "VALID",
"elapsed": 1.4
}Response: not found
{
"ok": false,
"status": "not-found",
"email": null,
"domain": "deutschebank.com",
"confidence": 0,
"provider": null,
"rawStatus": "NOT_FOUND",
"elapsed": 2.1
}Example: Python
import requests
r = requests.post(
"https://mozartemail.com/api/find-email",
headers={"Authorization": "Bearer mz_live_xxx"},
json={
"firstName": "Clara",
"lastName": "Weber",
"domainOrCompany": "deutschebank.com",
},
timeout=30,
)
data = r.json()
if data["ok"]:
print(data["email"], "·", data["confidence"])Example: Node
const r = await fetch("https://mozartemail.com/api/find-email", {
method: "POST",
headers: {
"Authorization": "Bearer mz_live_xxx",
"Content-Type": "application/json",
},
body: JSON.stringify({
firstName: "Clara",
lastName: "Weber",
domainOrCompany: "deutschebank.com",
}),
});
const data = await r.json();
if (data.ok) console.log(data.email, "·", data.confidence);/api/verify
Verify a single email address, reusing the finder's verification path (MX classification, Microsoft 365 out-of-band, SMTP RCPT with catch-all detection). 0.1 credit per produced verdict: valid, invalid and catch_all are all billable, because each one is an answer. Only unknown is free.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| string | yes | Lower-cased and trimmed before checking. |
Verdicts
| valid | The mailbox answered for itself, out-of-band or over SMTP. Confidence 99. Billable. |
| invalid | Bad syntax, no MX, or the provider says the mailbox is absent. Confidence 0. Billable. |
| catch_all | The domain accepts every address, so no individual mailbox is provable. Confidence 50. Billable. |
| unknown | The mail server would not say: greylisting, a block, a timeout. No verdict, no charge. |
Request & response
curl -X POST https://mozartemail.com/api/verify \
-H "Authorization: Bearer mz_live_xxx" -H "Content-Type: application/json" \
-d '{ "email": "clara.weber@deutschebank.com" }'
{
"ok": true, "billable": true, "charged": 0.1,
"email": "clara.weber@deutschebank.com",
"domain": "deutschebank.com",
"result": "valid", // valid | invalid | catch_all | unknown
"reason": "smtp_rcpt_exists",
"provider": "m365", "has_mx": true, "catch_all": false,
"confidence": 99, "sla_status": "valid"
}reason names the signal behind the verdict: syntax, no_mx, oob_E3, oob_E2, oob_absent, oob_accept_all, smtp_rcpt_exists, smtp_rcpt_absent, smtp_catch_all, smtp_blocked. Log it: it is what lets you tell a hard bounce apart from an unreachable server.
/api/find-domain
Turn a company name into the domain it actually uses for email. Candidates are built from the name, filtered on live MX records, then probed on the company's own website; the evidence is scored out of 100. 0.1 credit, charged only when a domain clears the confidence gate.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| company | string | yes | Legal name or brand, as it appears in your files. |
| country | string | no | ISO-2 hint (fr, de, gb…). It promotes the matching ccTLD; omit it to search worldwide. An unrecognised value is ignored, never an error. |
Request & response
curl -X POST https://mozartemail.com/api/find-domain \
-H "Authorization: Bearer mz_live_xxx" -H "Content-Type: application/json" \
-d '{ "company": "Dassault Aviation", "country": "fr" }'
{
"ok": true,
"status": "found", // found | not-found
"company": "Dassault Aviation",
"domain": "dassault-aviation.com",
"confidence": 85, // 0–100 evidence score
"elapsed": 2.6
}/api/scrape-domain
Scan a domain's website for role-based emails (contact@, info@, support@…) and list the other addresses found on the way. Pass a domain, not a company name. 1 credit per domain scanned. Roughly a third of domains expose a role-based email publicly.
curl -X POST https://mozartemail.com/api/scrape-domain \
-H "Authorization: Bearer mz_live_xxx" -H "Content-Type: application/json" \
-d '{ "domain": "acme.com" }'
{
"ok": true, "billable": true, "charged": 1,
"domain": "acme.com", "status": "ok",
"role_emails": ["contact@acme.com", "info@acme.com"],
"other_emails": ["jean.dupont@acme.com"],
"n_role": 2, "n_own": 3, "pages": 3
}This is the one tool billed for the work rather than the catch: status ok, blocked, unreachable and timeout all mean the scan ran, and all are billable: a site that publishes no address is a real answer. Only error (a scan that could not start) is free.
/api/linkedin-profile
Scrape a public LinkedIn profile from its URL (name, current company, title, headline, decision-maker role, location, and public extras). 1.5 credits, charged only when the profile is found.
curl -X POST https://mozartemail.com/api/linkedin-profile \
-H "Authorization: Bearer mz_live_xxx" -H "Content-Type: application/json" \
-d '{ "url": "https://www.linkedin.com/in/xavier-lepingle" }'
{
"ok": true, "billable": true, "charged": 1.5,
"full_name": "Xavier Lépingle", "prenom": "Xavier", "nom": "Lépingle",
"societe": "Hermès", "poste": "Head of Digital", "headline": "…",
"role": "founder", "location": "Paris", "pays": "FR",
"company_url": "https://www.linkedin.com/company/hermes",
"followers": "1200", "source": "og_whatsapp"
}The URL must contain /in/. Deleted profiles and profiles behind LinkedIn's anti-bot wall come back with an error field, billable: false, and cost nothing.
/api/linkedin-company
Scrape a public LinkedIn company page from its URL (name, website, industry, size, headquarters, founding year, followers, specialties, description). 0.5 credit per successfully scraped page, the cheapest call in the catalogue.
curl -X POST https://mozartemail.com/api/linkedin-company \
-H "Authorization: Bearer mz_live_xxx" -H "Content-Type: application/json" \
-d '{ "url": "https://www.linkedin.com/company/hermes" }'
{
"ok": true, "billable": true, "charged": 0.5,
"nom": "Hermès", "site_web": "https://www.hermes.com",
"secteur": "Luxury Goods & Jewelry", "taille": "10001+",
"siege": "Paris, Île-de-France", "fondee_en": "1837",
"abonnes": "44,725", "effectif_linkedin": 12000
}/api/linkedin-finder
Find a person's public LinkedIn profile from their name and company. It mirrors the profile scraper, which consumes the URL this one produces. firstName and lastName are required; company is what separates your person from their homonyms, and jobTitle / location sharpen the match further. 1.5 credits per profile delivered.
Nothing is returned below the anti-homonym threshold: a miss costs you nothing, a wrong profile would cost you a prospect. confidence (0–1) ships with every hit; it only reaches 0.8 when verified is true, meaning the profile page itself gave back the name you searched for.
curl -X POST https://mozartemail.com/api/linkedin-finder \
-H "Authorization: Bearer mz_live_xxx" -H "Content-Type: application/json" \
-d '{ "firstName": "Patrick", "lastName": "Castaings",
"company": "Thales Angenieux", "jobTitle": "COO" }'
{
"ok": true, "billable": true, "charged": 1.5, "found": true,
"linkedin_url": "https://www.linkedin.com/in/patrick-c-a9121219",
"confidence": 0.85, "verified": true,
"full_name": "Patrick Castaings", "headline": "COO chez Thales Angenieux",
"societe_linkedin": "Thales Angenieux", "location": "Saint-Étienne"
}Misses name their cause in error: no_candidate (nothing indexed for that name), low_score (candidates came back but none could be confirmed as your person), anomaly (the search engines throttled us; retry shortly). This call can take up to 120 seconds under load; set your client timeout accordingly.
/api/reverse-lookup
Put a face on an address: one email in, the person's public LinkedIn profile out. Works on professional and personal addresses alike. 2 credits per profile delivered, and nothing when the person cannot be identified with confidence.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| string | yes | Lower-cased before lookup. A malformed address is rejected with 400 invalid_email and never held. |
Request & response
curl -X POST https://mozartemail.com/api/reverse-lookup \
-H "Authorization: Bearer mz_live_xxx" -H "Content-Type: application/json" \
-d '{ "email": "clara.weber@deutschebank.com" }'
{
"ok": true, "billable": true, "charged": 2, "found": true,
"email": "clara.weber@deutschebank.com",
"linkedin_url": "https://www.linkedin.com/in/clara-weber-8a12b34",
"confidence": 0.87, // 0–1
"verified": true, "to_verify": false,
"full_name": "Clara Weber", "prenom": "Clara", "nom": "Weber",
"societe": "Deutsche Bank", "headline": "Head of Structured Finance",
"location": "Frankfurt", "pays": "DE",
"company_url": "https://www.linkedin.com/company/deutsche-bank",
"followers": "3400", "connections": "500+",
"method": "broker_page", "photo": "https://media.licdn.com/…"
}Reading the confidence
A URL is only returned above the engine's threshold, and confidence tells you how firmly. At ≥ 0.8 the profile page itself corroborated the identity: to_verify is false and the match is safe to use unattended. Between the threshold and 0.8, to_verify is true: the evidence points one way but the profile did not confirm it, so give it a glance before you act on it.
Misses carry an error code and cost nothing: rien_trouve, homonyme_ou_non_verifie, og_bloque, moteurs_bloques. Like the LinkedIn finder, this call can run to 120 seconds under load.
Google Maps
Two ways in. Discovery harvests every business matching a trade inside a map disc. Quote the zone for free, then harvest what you want from it. Enrichment resolves businesses you already know, one listing per input. Both bill 1 credit per delivered listing.
Listing fields
Every listing, whichever route produced it, carries: name, category, address, city, phone, website, website_domain, rating, reviews_count, lat, lng, place_id, data_id.
/api/google-maps/quote
Count what exists in a zone before spending anything. This runs the real sweep, the same tile footprint a harvest would use, and is never billed. It returns the true count, the actual fill rates for that zone, map pins, and an eight-listing sample.
| Field | Type | Required | Notes |
|---|---|---|---|
| query | string | yes | One trade per sweep gives the best phone fill rates. |
| lat / lng | number | yes | Centre of the disc. |
| radius_m | number | no | Metres. Default 25 000; clamped to 1 000–200 000. |
| city | string | no | Exact mode: keep only listings whose town matches this commune, instead of a ring. |
curl -X POST https://mozartemail.com/api/google-maps/quote \
-H "Authorization: Bearer mz_live_xxx" -H "Content-Type: application/json" \
-d '{ "query": "boilermaking", "lat": 45.44, "lng": 4.39, "radius_m": 30000 }'
{
"ok": true, "status": "ok",
"available": 412, // listings that really exist in the zone
"quoteId": "7f0b…", // frozen for 15 minutes
"fill": { "phone": 94, "website": 71, "rating": 66 }, // % for THIS zone
"pins": [[45.44, 4.39], …], // up to 400 coordinates
"sample": [ { "name": "…", "phone": "…", … } ] // 8 listings
}Pass the quoteId to the harvest and you get exactly the set you were shown. Google's results drift between sweeps; freezing the quote is what makes the announced number the delivered number. The freeze lasts 15 minutes, after which the harvest simply re-sweeps.
/api/google-maps/search
Harvest the zone. Unlike the other synchronous endpoints this one lands a completed bulk job: it returns a jobId you download through /api/bulk/{id}/download, like any other batch. Credits are held for the requested volume and the unused remainder is refunded on the spot.
| Field | Type | Required | Notes |
|---|---|---|---|
| query | string | yes | Up to 120 chars. |
| lat / lng | number | yes | Centre of the disc. |
| radiusKm | number | no | Kilometres here, not metres. Default 25; clamped to 1–200. |
| limit | number | no | How many listings you want. Default 300, max 5 000. You are charged for what is delivered, which can be less. |
| quoteId | string | no | Deliver the frozen set from a quote taken within the last 15 minutes for the same parameters. |
| city | string | no | Exact mode, as in the quote. Must match the quote's value to reuse it. |
| locationLabel | string | no | Human label for the batch name. Commas become dashes. |
curl -X POST https://mozartemail.com/api/google-maps/search \
-H "Authorization: Bearer mz_live_xxx" -H "Content-Type: application/json" \
-d '{ "query": "boilermaking", "lat": 45.44, "lng": 4.39,
"radiusKm": 30, "limit": 300, "quoteId": "7f0b…",
"locationLabel": "Saint-Étienne" }'
{
"ok": true,
"jobId": "b18e2c80-3f7d-4a1d-9d1e-c4ad3e2c5b71",
"delivered": 300, "charged": 300, "held": 300,
"label": "boilermaking · Saint-Étienne · 30 km"
}A 5 000-listing sweep stays under about a minute, but budget for it: the call is synchronous and holds the connection open until the job is stored.
/api/scrape-google-maps
Enrichment, one business at a time: give a name and optionally a location, get back the single matching listing. 1 credit when a listing is found, free otherwise.
curl -X POST https://mozartemail.com/api/scrape-google-maps \
-H "Authorization: Bearer mz_live_xxx" -H "Content-Type: application/json" \
-d '{ "name": "Boulangerie Utopie", "location": "Paris 11" }'
{
"ok": true, "billable": true, "charged": 1,
"status": "found", "found": true,
"name": "Boulangerie Utopie", "category": "Bakery",
"address": "20 Rue Jean-Pierre Timbaud, 75011 Paris", "city": "Paris",
"phone": "+33 9 82 50 74 48", "website": "https://…",
"website_domain": "boulangerie-utopie.com",
"rating": 4.6, "reviews_count": 2841,
"lat": 48.8657, "lng": 2.3721,
"place_id": "ChIJ…", "data_id": "0x47e66…:0x8f1c…"
}Its bulk sibling, /api/bulk-google-maps/upload, takes one input cell per row shaped business name|location (the location half is optional), or a Google Maps listing URL.
Bulk search
Upload a CSV or .xlsx, poll for status, download a results CSV. Jobs run on our worker and survive network drops on your side. You can disconnect after uploading and come back later for the results.
Lifecycle
- Upload a file to the endpoint for your tool. We parse it, hold the per-row cost for every row, and return a jobId.
- Status moves queued → processing → done (or error / cancelled). Poll every 5–10 seconds.
- Download the enriched CSV when status is done. Partial results are available earlier.
Pricing
The per-row cost is the tool's unit cost from the catalogue. We pre-hold cost × rows when you upload, charge only the rows that produced a result, and refund the difference when the job finishes. Cancelling a running job refunds everything not yet charged. If your balance can't cover the batch, the upload is rejected with insufficient_credits and nothing is deducted.
Limits
| Max rows per job | 5 000 |
| Max file size | 3 MB |
| Concurrent jobs per account | 3 (across all tools) |
| Result retention | Indefinite (delete with DELETE /api/bulk/{id}) |
File format
UTF-8 CSV or .xlsx (first worksheet) with a header row. Delimiters , ; and tab are auto-detected, so French Excel exports work as-is. Column names are case-insensitive and matched against common variants; a single-column file needs no header at all. Blank cells are skipped silently, never held and never billed. Extra columns are ignored for matching but preserved, unchanged and in their original order, in the results CSV.
/api/bulk/upload
The email-finder batch. Upload a CSV or .xlsx as multipart/form-data with a single field named file. The endpoint validates the file, holds the credits, and returns a job ID immediately. Processing happens asynchronously.
Columns
| Required column | Accepted headers (case-insensitive) |
|---|---|
| First name | first_name, firstname, first name, prenom |
| Last name | last_name, lastname, last name, nom |
| Target | domain, company, company_name, website, societe |
A row needs a first name, a last name, and either a domain or a company name. To override auto-detection, pass 0-based column indices as extra multipart fields: firstCol, lastCol, domainCol, companyCol.
Example: curl
curl -X POST https://mozartemail.com/api/bulk/upload \ -H "Authorization: Bearer mz_live_xxx" \ -F "file=@./prospects.csv" # .xlsx works the same way curl -X POST https://mozartemail.com/api/bulk/upload \ -H "Authorization: Bearer mz_live_xxx" \ -F "file=@./prospects.xlsx"
Response: accepted
{
"ok": true,
"jobId": "b18e2c80-3f7d-4a1d-9d1e-c4ad3e2c5b71",
"total": 1248
}Common rejections
Shared by every bulk upload endpoint.
| HTTP | reason | Meaning |
|---|---|---|
| 400 | no_file | Multipart had no file field. |
| 400 | file_too_large | Larger than 3 MB. |
| 400 | empty_csv | Fewer than 2 rows (no data). |
| 400 | invalid_file | Couldn't parse the file (corrupt .xlsx, malformed CSV, etc.). |
| 400 | missing_columns | Header didn't expose the columns this tool needs. |
| 400 | no_valid_rows | All rows blank after filtering. |
| 400 | too_many_rows | Over the 5 000-row limit. Response includes got and limit. |
| 402 | insufficient_credits | Balance can't cover the full batch. Response includes balance and required. |
| 429 | too_many_active_jobs | Already 3 jobs in queued or processing. Wait for one to finish. |
The other eight uploads
Same multipart shape, same file field, same limits and rejections as /api/bulk/upload. They differ only in which columns they read and what a row costs. Each returns { ok, jobId, total }.
| Endpoint | Input column(s) | Override fields | kind | Cost/row |
|---|---|---|---|---|
| /api/bulk-verify/upload | inputCol | verify | 0.1 | |
| /api/bulk-domain/upload | company (+ optional country) | companyCol, countryCol | domain | 0.1 |
| /api/bulk-domain-scraper/upload | domain | inputCol | domain_roles | 1 |
| /api/bulk-linkedin/upload | profile URL | inputCol | linkedin_profile | 1.5 |
| /api/bulk-linkedin-company/upload | company URL | inputCol | linkedin_company | 0.5 |
| /api/bulk-linkedin-finder/upload | first + last names, plus optional company, job title, location | firstCol, lastCol, companyCol, titleCol, locationCol | linkedin_find | 1.5 |
| /api/bulk-google-maps/upload | business name|location, or a listing URL | inputCol | google_maps | 1 |
| /api/bulk-reverse-lookup/upload | inputCol | reverse_lookup | 2 |
Override fields are 0-based column indices passed as extra multipart fields. Omit them and the columns are detected from your headers; a single-column file is taken as-is, header or not.
The LinkedIn finder is the one multi-column scraper: firstCol and lastCol are required, and a row missing either name is skipped, never held and never billed. Its ninth sibling, the Maps discovery harvest, has no file upload: it creates its batch through /api/google-maps/search.
/api/bulk/{id}
Returns the job header: status, progress counters, and credit accounting. Poll every 5–10 seconds while queued or processing. Works for jobs of every kind.
Example: curl
curl https://mozartemail.com/api/bulk/b18e2c80-3f7d-4a1d-9d1e-c4ad3e2c5b71 \ -H "Authorization: Bearer mz_live_xxx"
Response
{
"ok": true,
"job": {
"id": "b18e2c80-3f7d-4a1d-9d1e-c4ad3e2c5b71",
"filename": "prospects.csv",
"status": "processing",
"total": 1248,
"processed": 412,
"found": 358,
"notFound": 54,
"creditsHeld": 1248,
"creditsCharged": 358,
"errorMessage": null,
"createdAt": "2026-05-11T17:24:08.221Z",
"startedAt": "2026-05-11T17:24:09.804Z",
"finishedAt": null
}
}found counts billable rows, not successful ones in the everyday sense. On a verifier batch that means every row that produced a verdict, including an invalid.
Status values
| queued | Accepted, waiting for the worker. |
| processing | Worker is running the rows. |
| done | All rows processed. Results ready to download. |
| error | Job failed before finishing. errorMessage is non-null. Held credits are refunded automatically. |
| cancelled | Job was cancelled (delete or admin action). Unused credits refunded. |
/api/bulk/list
List the most-recent 50 jobs for the authed account, newest first. Jobs are listed one kind at a time: pass ?kind= with one of email (the default), domain, verify, domain_roles, linkedin_profile, linkedin_company, linkedin_find, google_maps, reverse_lookup. An unknown value falls back to email.
Response
curl "https://mozartemail.com/api/bulk/list?kind=reverse_lookup" \
-H "Authorization: Bearer mz_live_xxx"
{
"ok": true,
"jobs": [
{
"id": "b18e2c80-...",
"filename": "prospects.csv",
"status": "done",
"total": 1248,
"processed": 1248,
"found": 1041,
"notFound": 207,
"creditsHeld": 0,
"creditsCharged": 1041,
"createdAt": "2026-05-11T17:24:08.221Z",
"finishedAt": "2026-05-11T17:31:52.118Z"
}
]
}/api/bulk/{id}/download
Returns the enriched results as a CSV download. Available for any job. Partial results stream as soon as rows are processed. Response is text/csv; charset=utf-8, with a UTF-8 BOM and CRLF line endings so Excel opens it correctly on both platforms.
Output columns
Every column from your uploaded file comes back verbatim, in its original order, followed by the Mozart columns appended on the right. Your own columns are never renamed or reordered, so the file drops straight back into your pipeline. Which Mozart columns you get depends on the job's kind:
| kind | Appended columns (all prefixed mozart_) |
|---|---|
| email, status, confidence, provider | |
| domain | domain, status, confidence |
| verify | result, reason, provider, has_mx, confidence |
| domain_roles | status, n_role, role_emails, other_emails |
| linkedin_profile | full_name, prenom, nom, societe, poste, headline, role, location, pays, company_url, followers, connections, about, error |
| linkedin_company | nom, site_web, secteur, taille, siege, fondee_en, type_societe, specialites, abonnes, effectif_linkedin, slogan, description, error |
| linkedin_find | linkedin_url, confidence, verified, full_name, headline, company_on_profile, location, pays, company_url, followers, connections, score, error |
| google_maps | found, name, category, address, city, phone, website, website_domain, rating, reviews_count, latitude, longitude, maps_url, place_id, data_id |
| reverse_lookup | linkedin_url, confidence, full_name, first_name, last_name, company, headline, location, pays, company_url, followers, connections, verified, method, error |
List fields (role_emails, specialites…) are joined with | ; booleans render as yes / no.
Example: curl
curl https://mozartemail.com/api/bulk/b18e2c80-.../download \ -H "Authorization: Bearer mz_live_xxx" \ -o results.csv
/api/bulk/{id}
Remove a job and its rows. Permanent. There is no undo. Deleting a job that is still running cancels it and refunds everything held but not yet charged. Finished results are already in any CSV you downloaded.
curl -X DELETE https://mozartemail.com/api/bulk/b18e2c80-... \ -H "Authorization: Bearer mz_live_xxx"
Responses: { "ok": true } on success, 404 not_found if the ID doesn't belong to your account.
Statuses & confidence
Each tool answers in its own vocabulary, but the shape is always the same: a status you branch on, and a confidence you threshold on. Single and bulk agree: the CSV column carries the same value the JSON does.
Result statuses by tool
| Tool | Statuses |
|---|---|
| Email finder | verified confirmed out-of-band or over SMTP, safe to send, billed · not-found no provable mailbox: a miss, a timeout, or a catch-all domain. Free. |
| Email verifier | valid · invalid · catch_all, all three are verdicts, all billed · unknown no verdict, free. |
| Domain finder | found above the evidence gate, billed · not-found below it or nothing at all. Free. |
| Domain scraper | ok · blocked · unreachable · timeout, the scan ran, all billed · error the scan could not run, free. |
| LinkedIn & reverse lookup | No status string: a hit carries the data (and linkedin_url for the two finders), a miss carries error. Branch on billable. |
| Google Maps | found a listing was resolved, billed · anything else is free. |
Confidence scales
Two scales, depending on the tool. Both are comparable across calls, so a threshold you pick once keeps meaning the same thing.
| Tool | Scale | Reading |
|---|---|---|
| Email finder | 0–100 | 100 two or more strong out-of-band channels, or SMTP-confirmed · 85–99 one strong channel · 0 not found. |
| Email verifier | 0–100 | 99 valid · 50 catch-all · 0 invalid. |
| Domain finder | 0–100 | Evidence score. An address on the site itself weighs most, a parked page weighs against. Nothing under the gate is returned. |
| LinkedIn URL finder, Reverse lookup | 0–1 | ≥ 0.8 the profile page itself corroborated the identity: use unattended · below that, the search evidence points one way but was not confirmed: worth an eye. |
Errors
All non-2xx responses include { "ok": false, "reason": "<code>" }, and none of them are billed. Common codes:
| HTTP | reason | When |
|---|---|---|
| 400 | invalid_json | Body wasn't valid JSON. |
| 400 | missing_fields | Required fields absent. |
| 400 | invalid_email | Malformed address (/api/reverse-lookup). |
| 400 | invalid_form | Multipart body could not be parsed (bulk upload). |
| 401 | unauthorized / not_authenticated | Missing or revoked API key. |
| 402 | insufficient_credits | Top up to continue. Bulk includes required; single returns balance. |
| 404 | not_found | Unknown job ID, or job belongs to another account. |
| 409 | cancelled | The Maps batch was deleted while its sweep was still running. |
| 429 | rate_limited | Hit one of the rate gates below. The Retry-After header gives the back-off in seconds. |
| 429 | too_many_active_jobs | Already 3 bulk jobs queued or processing. |
| 500 | server_exception | Internal error. Contact support if persistent. |
| 503 | upstream_rate_limited | The engine rate-limited us. Back off using Retry-After (5s by default). |
| 503 | engine_timeout / engine_busy | The engine didn't answer in time: 30s for the finders and verifier, 45s for the scrapers, 120s for the LinkedIn finder and reverse lookup. Safe to retry; nothing was charged. |
Rate limits
Three gates guard each synchronous endpoint: a per-account burst, a per-account minute, and a shared minute across all accounts protecting the engine. Any of them returns 429 rate_limited with a Retry-After header. Back off on the indicated seconds and retry.
| Endpoint | Burst | Per account | Global |
|---|---|---|---|
| /api/find-email | 3 / sec | 40 / min | 40 / min |
| /api/find-domain | 3 / sec | 60 / min | 60 / min |
| Verifier, scrapers, reverse lookup, /api/scrape-google-maps | 3 / sec | 40 / min | 60 / min |
| /api/google-maps/quote | 1 / 2 sec | 8 / min | 24 / min |
| /api/google-maps/search | 1 / 2 sec | 6 / min | 20 / min |
| Bulk uploads | 3 concurrent jobs per account, all tools combined. Queue more by waiting for one to finish. | ||
| Status / list / download | Unlimited. Poll every 5–10 seconds; longer intervals are kinder to our cache. | ||
Past a few hundred lookups, use the bulk endpoints rather than looping on the synchronous ones: a batch runs far faster than the per-account minute gate allows, and it survives a network drop on your side.
Need something else? Back home · Manage keys · concierge@mozartemail.com