API.

Nine tools, one API. Every tool has a synchronous endpoint for live one-off lookups and an asynchronous bulk endpoint for batches up to 5 000 rows. JSON in, JSON out, except for bulk uploads (multipart) and downloads (CSV).

Last updated: August 6, 2026

Generate keys in Settings → API. Each key is shown once. Store it like a password. You can disable or rotate keys at any time.

Base URL
https://mozartemail.com
One rule holds everywhere: you are charged for results, not for attempts. Credits are held before the call and refunded automatically whenever the work produces nothing we can prove: a miss, a timeout, a rate-limited upstream, a skipped row. Every response carries billable and charged so your own ledger can mirror ours.

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.

ToolSynchronousBulk uploadCostBilled when
Email finder/api/find-email/api/bulk/upload1an address is verified
Email verifier/api/verify/api/bulk-verify/upload0.1a verdict is produced
Domain finder/api/find-domain/api/bulk-domain/upload0.1a domain clears the confidence gate
Domain scraper/api/scrape-domain/api/bulk-domain-scraper/upload1the domain is scanned
LinkedIn profile/api/linkedin-profile/api/bulk-linkedin/upload1.5the profile is found
LinkedIn company/api/linkedin-company/api/bulk-linkedin-company/upload0.5the page is scraped
LinkedIn URL finder/api/linkedin-finder/api/bulk-linkedin-finder/upload1.5a profile is delivered
Google Maps scraper/api/google-maps/search
/api/scrape-google-maps
/api/bulk-google-maps/upload1a listing is delivered
Reverse lookup/api/reverse-lookup/api/bulk-reverse-lookup/upload2a profile is delivered
Reading a synchronous response. /api/find-email and /api/find-domain put the verdict in ok and status. The seven other tools always return HTTP 200 for a call that ran, and put the verdict in the payload. Branch on billable (authoritative: it is exactly what you paid for) or on the tool's own signal (result, found, linkedin_url…). Transport failures are non-2xx with { ok: false, reason } and are never billed.
POST

/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

FieldTypeRequiredNotes
firstNamestringyesUp to 80 chars.
lastNamestringyesUp to 80 chars.
domainOrCompanystringyesEither 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
}
Catch-all domains. When a domain accepts every address, no individual mailbox can be proven, so this endpoint returns not-found and charges nothing, rather than selling you a guess. There is no third status and no emailGuess field. If what you need is the catch-all verdict itself, call /api/verify, which reports catch_all explicitly.

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);
POST

/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

FieldTypeRequiredNotes
emailstringyesLower-cased and trimmed before checking.

Verdicts

validThe mailbox answered for itself, out-of-band or over SMTP. Confidence 99. Billable.
invalidBad syntax, no MX, or the provider says the mailbox is absent. Confidence 0. Billable.
catch_allThe domain accepts every address, so no individual mailbox is provable. Confidence 50. Billable.
unknownThe 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.

POST

/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

FieldTypeRequiredNotes
companystringyesLegal name or brand, as it appears in your files.
countrystringnoISO-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
}
Nothing is returned below the confidence gate. A prediction the evidence does not support comes back as status: "not-found" with domain: null, and costs nothing, even though the engine had a candidate. That gate is the whole point of the tool: it is what turns a coin-flip guess into a domain you can send from.
POST

/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.

POST

/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.

POST

/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
}
POST

/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.

POST

/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

FieldTypeRequiredNotes
emailstringyesLower-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.

The photo URL is a signed media.licdn.com link and expires. Download the image if you need to keep it; do not store the URL as a permanent reference.

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.

POST

/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.

FieldTypeRequiredNotes
querystringyesOne trade per sweep gives the best phone fill rates.
lat / lngnumberyesCentre of the disc.
radius_mnumbernoMetres. Default 25 000; clamped to 1 000–200 000.
citystringnoExact 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.

POST

/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.

FieldTypeRequiredNotes
querystringyesUp to 120 chars.
lat / lngnumberyesCentre of the disc.
radiusKmnumbernoKilometres here, not metres. Default 25; clamped to 1–200.
limitnumbernoHow many listings you want. Default 300, max 5 000. You are charged for what is delivered, which can be less.
quoteIdstringnoDeliver the frozen set from a quote taken within the last 15 minutes for the same parameters.
citystringnoExact mode, as in the quote. Must match the quote's value to reuse it.
locationLabelstringnoHuman 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.

POST

/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

  1. Upload a file to the endpoint for your tool. We parse it, hold the per-row cost for every row, and return a jobId.
  2. Status moves queuedprocessingdone (or error / cancelled). Poll every 5–10 seconds.
  3. 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 job5 000
Max file size3 MB
Concurrent jobs per account3 (across all tools)
Result retentionIndefinite (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.

POST

/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 columnAccepted headers (case-insensitive)
First namefirst_name, firstname, first name, prenom
Last namelast_name, lastname, last name, nom
Targetdomain, 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.

HTTPreasonMeaning
400no_fileMultipart had no file field.
400file_too_largeLarger than 3 MB.
400empty_csvFewer than 2 rows (no data).
400invalid_fileCouldn't parse the file (corrupt .xlsx, malformed CSV, etc.).
400missing_columnsHeader didn't expose the columns this tool needs.
400no_valid_rowsAll rows blank after filtering.
400too_many_rowsOver the 5 000-row limit. Response includes got and limit.
402insufficient_creditsBalance can't cover the full batch. Response includes balance and required.
429too_many_active_jobsAlready 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 }.

EndpointInput column(s)Override fieldskindCost/row
/api/bulk-verify/uploademailinputColverify0.1
/api/bulk-domain/uploadcompany (+ optional country)companyCol, countryColdomain0.1
/api/bulk-domain-scraper/uploaddomaininputColdomain_roles1
/api/bulk-linkedin/uploadprofile URLinputCollinkedin_profile1.5
/api/bulk-linkedin-company/uploadcompany URLinputCollinkedin_company0.5
/api/bulk-linkedin-finder/uploadfirst + last names, plus optional company, job title, locationfirstCol, lastCol, companyCol, titleCol, locationCollinkedin_find1.5
/api/bulk-google-maps/uploadbusiness name|location, or a listing URLinputColgoogle_maps1
/api/bulk-reverse-lookup/uploademailinputColreverse_lookup2

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.

GET

/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

queuedAccepted, waiting for the worker.
processingWorker is running the rows.
doneAll rows processed. Results ready to download.
errorJob failed before finishing. errorMessage is non-null. Held credits are refunded automatically.
cancelledJob was cancelled (delete or admin action). Unused credits refunded.
GET

/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"
    }
  ]
}
GET

/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:

kindAppended columns (all prefixed mozart_)
emailemail, status, confidence, provider
domaindomain, status, confidence
verifyresult, reason, provider, has_mx, confidence
domain_rolesstatus, n_role, role_emails, other_emails
linkedin_profilefull_name, prenom, nom, societe, poste, headline, role, location, pays, company_url, followers, connections, about, error
linkedin_companynom, site_web, secteur, taille, siege, fondee_en, type_societe, specialites, abonnes, effectif_linkedin, slogan, description, error
linkedin_findlinkedin_url, confidence, verified, full_name, headline, company_on_profile, location, pays, company_url, followers, connections, score, error
google_mapsfound, name, category, address, city, phone, website, website_domain, rating, reviews_count, latitude, longitude, maps_url, place_id, data_id
reverse_lookuplinkedin_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
DELETE

/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

ToolStatuses
Email finderverified 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 verifiervalid · invalid · catch_all, all three are verdicts, all billed · unknown no verdict, free.
Domain finderfound above the evidence gate, billed · not-found below it or nothing at all. Free.
Domain scraperok · blocked · unreachable · timeout, the scan ran, all billed · error the scan could not run, free.
LinkedIn & reverse lookupNo status string: a hit carries the data (and linkedin_url for the two finders), a miss carries error. Branch on billable.
Google Mapsfound a listing was resolved, billed · anything else is free.
There is no billable guess anywhere in this API. Whenever the evidence does not support an answer (a catch-all mailbox, a domain below the gate, two people with the same name), the tool returns nothing and charges nothing. That is a deliberate trade: a miss costs you one row, a wrong result costs you a prospect.

Confidence scales

Two scales, depending on the tool. Both are comparable across calls, so a threshold you pick once keeps meaning the same thing.

ToolScaleReading
Email finder0–100100 two or more strong out-of-band channels, or SMTP-confirmed · 85–99 one strong channel · 0 not found.
Email verifier0–10099 valid · 50 catch-all · 0 invalid.
Domain finder0–100Evidence score. An address on the site itself weighs most, a parked page weighs against. Nothing under the gate is returned.
LinkedIn URL finder, Reverse lookup0–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:

HTTPreasonWhen
400invalid_jsonBody wasn't valid JSON.
400missing_fieldsRequired fields absent.
400invalid_emailMalformed address (/api/reverse-lookup).
400invalid_formMultipart body could not be parsed (bulk upload).
401unauthorized / not_authenticatedMissing or revoked API key.
402insufficient_creditsTop up to continue. Bulk includes required; single returns balance.
404not_foundUnknown job ID, or job belongs to another account.
409cancelledThe Maps batch was deleted while its sweep was still running.
429rate_limitedHit one of the rate gates below. The Retry-After header gives the back-off in seconds.
429too_many_active_jobsAlready 3 bulk jobs queued or processing.
500server_exceptionInternal error. Contact support if persistent.
503upstream_rate_limitedThe engine rate-limited us. Back off using Retry-After (5s by default).
503engine_timeout / engine_busyThe 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.

EndpointBurstPer accountGlobal
/api/find-email3 / sec40 / min40 / min
/api/find-domain3 / sec60 / min60 / min
Verifier, scrapers, reverse lookup, /api/scrape-google-maps3 / sec40 / min60 / min
/api/google-maps/quote1 / 2 sec8 / min24 / min
/api/google-maps/search1 / 2 sec6 / min20 / min
Bulk uploads3 concurrent jobs per account, all tools combined. Queue more by waiting for one to finish.
Status / list / downloadUnlimited. 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