API documentation
A simple REST API. Send a request, get JSON back.
Authentication
Every call needs your API key, except /v1/sample. Send it in the X-API-Key header, or in an Authorization: Bearer header. Your key is on your Account page.
Keys start with eldb_live_. Keep yours secret: never put it in the code of a web page.
curl https://api.ecommerce-leads-database.com/v1/me \ -H "X-API-Key: eldb_live_…" # Or, if you prefer: curl https://api.ecommerce-leads-database.com/v1/me \ -H "Authorization: Bearer eldb_live_…"
Endpoints
All URLs start with https://api.ecommerce-leads-database.com.
/v1/sampleFree sample (no key)
Returns a few stores: 10 by default, 20 at most. Use it to see the data shape before subscribing. Does not count against any quota.
curl "https://api.ecommerce-leads-database.com/v1/sample?limit=10"
/v1/meYour plan and quota
Returns your plan, requests and stores used, your databases and the reset date. Free: uses no request and no store.
curl https://api.ecommerce-leads-database.com/v1/me \
-H "X-API-Key: eldb_live_…"
{
"plan": "api_pro",
"planLabel": "Pro",
"requests": { "limit": 25000, "used": … },
"stores": { "limit": 10000, "used": … },
"databases": { "slots": 3, "selected": ["shopify", "woocommerce", "prestashop"] },
"exports": true,
"resetAt": "…"
}/v1/stores/countCount stores
Returns how many stores match the filters, and how many have a Google profile. Costs 1 request and 0 stores.
Parameters: platform, tld, country, hasGmb, hasEmail, hasPhone, app, currency, lang, operatorCountry, activeOnly, genericEmailsOnly, includeUnverified, includeAliases, maxChromeRank, mailProvider, dmarcPolicy, trackingId, hasDecisionMaker, hasDecisionMakerEmail
curl "https://api.ecommerce-leads-database.com/v1/stores/count?platform=shopify" \
-H "X-API-Key: eldb_live_…"
{ "count": 29331, "gmbCount": 6511 }Real figures for verified Shopify stores.
/v1/stores/searchSearch stores
Filters the database and returns one page of results (items) with a cursor (nextCursor).
Parameters: platform, tld, country, minProducts, hasGmb, hasEmail, hasPhone, app, currency, lang, operatorCountry, activeOnly, genericEmailsOnly, includeUnverified, includeAliases, maxChromeRank, mailProvider, dmarcPolicy, trackingId, hasDecisionMaker, hasDecisionMakerEmail, cursor, limit
- limit: 1 to 100 results per page. 50 by default.
- Order: highest traffic first. Stores with no known traffic come last.
- By default, search and count return verified stores only: a real online shop, re-checked on its own site. Add includeUnverified=true to also include brochure sites and unreachable sites.
- activeOnly is true by default: stores scored inactive are left out. Add activeOnly=false to include them.
- Filters: hasEmail (email collected in the last 12 months), hasPhone, app (a detected app, e.g. Klaviyo), currency (EUR), lang (fr), operatorCountry (FR), genericEmailsOnly (with hasEmail: generic mailboxes only). A bad value returns 400 invalid_parameter and the request is not charged.
- maxChromeRank (highest Chrome UX Report ranking bucket: 1000, 5000, 10000, 50000, 100000, 500000, 1000000, 5000000, 10000000 or 50000000). Any other value returns 400 invalid_parameter. The audience field contains Chrome UX Report (Google) data, under the CC BY-SA 4.0 license.
- mailProvider (email provider worked out from the MX: google-workspace, microsoft-365, ovh, infomaniak, ionos, gandi, zoho, proton, o2switch, hostinger, other, none) and dmarcPolicy (none, quarantine, reject or missing). Any other value returns 400 invalid_parameter. The app filter also finds tools read from DNS (for example Klaviyo through SPF). App slugs that DNS can add: klaviyo, mailchimp, brevo, shopify email, mailjet, sendgrid, hubspot, salesforce marketing cloud, omnisend, mandrill, amazon ses, zendesk, mailgun, meta business, google site verification, pinterest, apple, atlassian, zoom, stripe, shopify.
- Public tracking IDs: use trackingId to find the other stores that share one code. Pass a bare value such as GTM-ABC123, AW-123456789, G-ABCDEF1234, UA-1234567-1 or ca-pub-1234567890123456. Numeric IDs need their type first: meta:123456789012345, tiktok:…, pinterest:…, microsoftAds:…, linkedin:…, hotjar:…, criteo:…, snapchat:…, clarity:…, klaviyo:… (Klaviyo is case-sensitive). Any other value returns 400 invalid_parameter. Each result also holds trackingIds (null until the store has been read).
- hasDecisionMaker=true|false (at least one known decision maker, or none) and hasDecisionMakerEmail=true (at least one decision maker whose email was found on the site or accepted by the mail server). Any other value returns 400 invalid_parameter. Each result also holds hasDecisionMaker.
- Next page: send the nextCursor value back in the cursor parameter. On the last page, nextCursor is null.
- Every store returned counts against your store budget. A page of 50 results uses 50 stores.
- Each result is a store summary: domain, platform, country, theme, product count, apps, traffic, merchant name, Google rating and category, hasEmail, hasPhone, hasDecisionMaker, currency, lang, operatorCountry, isActiveShop. Each result also holds audience (null when the store is not in the Chrome UX Report ranking). Each result also holds dns (null until the store has been read). For the full record, call /v1/stores/:domain.
curl -G https://api.ecommerce-leads-database.com/v1/stores/search \
-H "X-API-Key: eldb_live_…" \
-d platform=shopify \
-d minProducts=50 \
-d hasGmb=true \
-d limit=50
{
"items": [
{ "domain": "…", "platform": "shopify", "productCount": …, "merchantName": "…", … }
],
"nextCursor": "18450:01J…"
}/v1/stores/:domainStore record
Returns the full record of one store. Costs 1 request and 1 store. The catalog is not included: use /catalog. Every email is returned with its type; add genericEmailsOnly=true to receive generic mailboxes only. genericEmailsOnly=true also hides decision-maker emails.
| Field | Content |
|---|---|
domain | The store's domain name. |
platform | Detected ecommerce platform (shopify, woocommerce…). |
tld | Domain extension. |
country | Country (ISO code). |
theme | Detected theme. |
productCount | Number of products detected. |
verificationStatus | Outcome of the last site check. |
lastSuccessAt | Date of the last successful check. |
firstSeenAt | Date the store entered the database. |
detectedApps | Apps and tools detected on the site. |
traffic | Monthly traffic and organic keyword estimates. |
audience | Chrome audience: { chromeRank, class, country, month, source }. chromeRank is the Chrome UX Report (Google) ranking bucket for that country: 1000, 5000, 10000, up to 50000000. null if the store is not ranked. This field is shared under the CC BY-SA 4.0 license: credit the source and keep the same license. class values: top_1k, top_5k, top_10k, top_50k, top_100k, top_500k, top_1m, top_5m, top_10m, top_50m. Chrome UX Report source: https://developer.chrome.com/docs/crux. License: https://creativecommons.org/licenses/by-sa/4.0/. Data adapted: ranks matched to stores and grouped by country by us. |
dns | DNS: { mailProvider, dmarcPolicy, apps, checkedAt }. mailProvider is the email provider worked out from the MX (google-workspace, microsoft-365, ovh, infomaniak, ionos, gandi, zoho, proton, o2switch, hostinger, other, none). dmarcPolicy is none, quarantine, reject or missing (no record). apps lists the email sending and domain verification tools read from the SPF and TXT records. checkedAt is the date of the reading. null until the store has been read. No verification token is exposed. |
trackingIds | trackingIds: public tracking and ad IDs grouped by type, for example { "gtm": ["GTM-ABC123"], "googleAds": ["AW-123456789"], "meta": ["123456789012345"] }. Types: ga4, ua, gtm, googleAds, floodlight, adsense, meta, tiktok, pinterest, microsoftAds, linkedin, snapchat, criteo, clarity, hotjar, klaviyo. {} when nothing was found, null until the store has been read. Only codes that any visitor can see in the page source. |
platformVersion | Platform version (PrestaShop, Magento, Odoo) when the site shows it. |
operator | Country of the business behind the store ({ country, source }): from its VAT number, Shopify settings, legal-notice address, phone or TLD, in that order. Can differ from the TLD. |
isActiveShop | false when the store looks inactive (empty catalog, demo products, default currency), with the reasons in activeShopReasons. null when not scored yet. |
legal | Legal-notice page: pageUrl, companyIds (SIREN, VAT, BE enterprise number, CH UID, checked by their check digits), legalForm. |
shopify | Shopify stores: shopId, isPlus, shipsToCountries, shopPayInstallments, collectionsCount, themeStoreId. |
catalogStats | Catalog figures when the platform exposes them: average price and weight, variants, images, vendors, product types, categories, brands, newest product date. |
metadata | merchantName; contactEmail with contactEmailType (generic, personal or unknown) and emailDomainMatch; telephone (E.164) with phoneType (mobile, fixed_line…); address; socialLinks; sources (page and date each contact was collected); title, metaDescription, lang, currency, hreflangs, features, paymentMethods, shippingCarriers, salesChannels, copyrightYear, contactPageUrl, canonicalUrl. A contact collected more than 12 months ago, or an email whose owner opted out, is null with the reason in contactEmailWithheld, telephoneWithheld or addressWithheld. |
emails | Every email the store publishes: [{ email, type, sourceUrl }]. type is generic, personal or unknown; sourceUrl is the page the email was read on. At most 10, best first. An email read more than 12 months ago, or removed by its owner, is not returned. With genericEmailsOnly=true, only generic emails remain. |
decisionMakers | Decision makers: [{ firstName, lastName, role, source, sourceUrl, since, email, emailStatus }], primary decision maker first. role: owner (owner, sole trader), president, managing_director, manager, publication_director (person legally responsible for the site), partner, administrator (board member), other. source: rne (national business register, France), zefix (commercial register, Switzerland) or legal_page (the store's legal notice). sourceUrl: the company's public registry page, or the legal notice page. since: start date in the role when the registry gives it, else null. emailStatus: found_on_site (address published on the site), valid (accepted by the domain's mail server), catch_all (the server accepts every address: most likely format, not confirmed), pattern (worked out from the domain's address format, not checked), not_found (no address accepted by the server), null (no known address). email is null when its owner removed it, when it is more than 12 months old, or with genericEmailsOnly=true. A decision maker read more than 12 months ago is no longer returned. Never a birth date, nationality or home address. |
company | Company registry record: { registry, registryId, officialName, activityCode, headcountBand, createdOn, category, legalFormCode, active, hq, checkedAt }. registry is fr, be or ch; registryId is the SIREN, the BE enterprise number or the CH UID. officialName: registered name. activityCode: activity code (NAF or NACE). headcountBand: employee headcount band, for example "10-19". createdOn: creation date. category: company category (France). legalFormCode: the registry's legal form code. active: false when the company is closed. hq: postcode and town of the head office. checkedAt: date the registry was last read. A field the registry does not give is null. company is null when the store has no company number, when the registry does not know it, or when the reading is more than 12 months old. |
gmb | Google profile: placeId, category, additionalCategories, rating, ratingDistribution, isClaimed, totalPhotos, workTime, address, addressInfo, latitude, longitude, phone, website. null when there is none. |
curl https://api.ecommerce-leads-database.com/v1/stores/probikeshop.fr \
-H "X-API-Key: eldb_live_…"
{
"store": {
"domain": "probikeshop.fr",
"platform": "shopify",
"tld": "fr",
"country": "FR",
"theme": "…",
"productCount": 5000,
"verificationStatus": "…",
"lastSuccessAt": "…",
"detectedApps": ["…"],
"trackingIds": { "gtm": ["GTM-ABC123"], "googleAds": ["AW-123456789"] },
"operator": { "country": "FR", "source": "vat" },
"isActiveShop": true,
"legal": { "pageUrl": "…", "companyIds": { "siren": "…", "vat": "…" }, "legalForm": "SAS" },
"metadata": {
"merchantName": "…",
"contactEmail": "…",
"contactEmailType": "generic",
"contactEmailWithheld": null,
"telephone": "+33…",
"phoneType": "fixed_line",
"address": { … },
"sources": {
"contactEmail": { "url": "https://…/mentions-legales", "at": "2026-10-19T08:00:00.000Z" }
},
"socialLinks": { "instagram": "…" },
"currency": "EUR",
"lang": "fr"
},
"emails": [
{ "email": "contact@…", "type": "generic", "sourceUrl": "https://…/contact" }
],
"decisionMakers": [
{
"firstName": "Marie", "lastName": "Durand", "role": "president",
"source": "rne", "sourceUrl": "https://annuaire-entreprises.data.gouv.fr/entreprise/…",
"since": "2019-03-01", "email": "marie.durand@…", "emailStatus": "valid"
}
],
"company": {
"registry": "fr", "registryId": "…", "officialName": "…",
"activityCode": "47.91B", "headcountBand": "10-19", "createdOn": "…",
"category": "PME", "legalFormCode": "5710", "active": true,
"hq": "…", "checkedAt": "…"
},
"gmb": {
"placeId": "…", "category": "…", "rating": 4.5, "votes": 594,
"address": "…", "phone": "…", "website": "…"
}
}
}/v1/stores/:domain/catalogPro and ScaleProduct catalog
Returns the store's products, up to 5,000. Available for part of the Shopify and WooCommerce stores. Otherwise you get a 404 no_catalog, which is not billed. Costs 10 requests.
curl https://api.ecommerce-leads-database.com/v1/stores/probikeshop.fr/catalog \
-H "X-API-Key: eldb_live_…"
{
"domain": "probikeshop.fr",
"catalog": [
{
"id": "…", "title": "…", "handle": "…", "vendor": "…",
"productType": "…", "tags": ["…"],
"price": …, "compareAtPrice": …, "currency": "EUR",
"available": true, "imageUrl": "…", "imageCount": …, "variantCount": …,
"createdAt": "…", "updatedAt": "…"
}
]
}/v1/stores/:domain/trendPro and ScaleTraffic trend
Returns the monthly history of organic traffic and positions. 404 no_trend when there is no data, not billed. Costs 2 requests.
curl https://api.ecommerce-leads-database.com/v1/stores/probikeshop.fr/trend \
-H "X-API-Key: eldb_live_…"
{
"domain": "probikeshop.fr",
"trend": [
{ "date": "2026-07-01", "organicTraffic": 197021, "organicPositions": 19377 }
],
"savedAt": "2026-07-01T04:12:08.441Z"
}Enrich with your keys
Find the email and phone of a store's decision makers with your own Apollo, FullEnrich, Kaspr or Dropcontact account. The call uses your credits at the provider. The results are yours: no other customer sees them.
Save your provider keys in your account, on the My provider keys page. They never go through the API: a stolen ELDB key can neither read nor change them.
Per request: 25 stores and 3 decision makers per store. Per customer: 3 running enrichments, and 500 people per provider per day (UTC). A result is served for 12 months, then erased at 13 months.
/v1/enrichment/providersYour providers
Lists the providers with a saved key, with their status and the credits left when the provider reports them. Free.
curl https://api.ecommerce-leads-database.com/v1/enrichment/providers \
-H "X-API-Key: eldb_live_…"
{
"providers": [
{ "provider": "dropcontact", "status": "valid", "creditsRemaining": 250 }
]
}/v1/enrichmentsStart an enrichment
Send provider and domain (one store) or domains (25 at most). maxPeoplePerStore: 1 to 3, 2 by default. phones: true to ask for phone numbers too (more credits). force: true to run again a person enriched less than 30 days ago. Costs 1 request and 1 store per store with decision makers. Answers 200 with the results when the provider answers at once, otherwise 202: follow the job.
curl -X POST https://api.ecommerce-leads-database.com/v1/enrichments \
-H "X-API-Key: eldb_live_…" \
-H "Content-Type: application/json" \
-d '{ "provider": "dropcontact", "domains": ["probikeshop.fr"], "maxPeoplePerStore": 2 }'
HTTP/1.1 202 Accepted
{
"job": {
"id": "…", "provider": "dropcontact", "status": "queued", "phones": false,
"storeCount": 1, "personCount": 2, "creditsUsed": null, "errorCode": null,
"createdAt": "…", "updatedAt": "…"
},
"results": [
{ "domain": "probikeshop.fr", "status": "ok", "people": [ … ] }
]
}/v1/enrichments/:jobIdFollow an enrichment
Returns the job status (queued, submitted, done, partial, failed, canceled) and the results by store. Free. Wait at least 20 seconds between two calls.
curl https://api.ecommerce-leads-database.com/v1/enrichments/<jobId> \ -H "X-API-Key: eldb_live_…"
/v1/stores/:domain/enrichmentA store's results
Returns the platform, the merchant name, the decision makers, your providers and your results. Costs 1 request and 1 store. DELETE on the same URL erases your results for this store.
curl https://api.ecommerce-leads-database.com/v1/stores/probikeshop.fr/enrichment \
-H "X-API-Key: eldb_live_…"
{
"domain": "probikeshop.fr",
"platform": "…",
"merchantName": "…",
"providers": ["dropcontact"],
"decisionMakers": [{ "firstName": "…", "lastName": "…", "role": "president" }],
"results": [
{
"firstName": "…", "lastName": "…", "role": "president", "provider": "dropcontact",
"status": "found", "skipReason": null,
"email": "…", "emailStatus": "valid", "emailStatusRaw": "nominative@pro", "emailKind": "professional",
"phone": null, "phoneType": null, "linkedinUrl": "…", "jobTitle": "…",
"creditsUsed": 1, "errorCode": null, "enrichedAt": "…", "reused": false
}
]
}Each result holds firstName, lastName, role, provider, status (pending, found, not_found, skipped, error), skipReason, email, emailStatus (valid, risky, catch_all, invalid, unknown), emailStatusRaw (the provider's own label), emailKind (professional, generic, personal), phone, phoneType, linkedinUrl, jobTitle, creditsUsed, errorCode, enrichedAt and reused (true: a result already found, no new credits).
GET /v1/stores/:domain also returns enrichment: your own results for this store, next to decisionMakers. Empty array if you have none.
Quotas and costs
Each plan has two counters: requests and stores. Both reset each time your subscription renews.
We email you when you have used 80% of a counter.
- Starter (€49 excl. VAT): 5,000 requests and 2,500 stores per month. 1 platform database of your choice.
- Pro (€149 excl. VAT): 25,000 requests and 10,000 stores per month. 3 databases of your choice. Catalog and trends.
- Scale (€449 excl. VAT): 150,000 requests and 40,000 stores per month. All databases. Catalog and trends.
The store budget counts every store returned by /v1/stores/search, and 1 store per /v1/stores/:domain call. /v1/stores/count and /v1/me use no stores.
404, 403 and 409 errors are not billed.
| Endpoint | Requests | Stores |
|---|---|---|
/v1/sample | 0 | 0 |
/v1/me | 0 | 0 |
/v1/stores/count | 1 | 0 |
/v1/stores/search | 1 | 1 per store returned |
/v1/stores/:domain | 1 | 1 |
/v1/stores/:domain/catalog | 10 | 0 |
/v1/stores/:domain/trend | 2 | 0 |
/v1/enrichment/providers | 0 | 0 |
POST /v1/enrichments | 1 | 1 per store with decision makers |
/v1/enrichments/:jobId | 0 | 0 |
/v1/stores/:domain/enrichment | 1 | 1 |
Response headers
X-RateLimit-Limit: requests included this month.X-RateLimit-Remaining: requests left.X-RateLimit-Reset: when the counters reset, at your subscription renewal.X-Stores-Limit: stores included this month.X-Stores-Remaining: stores left.
Errors
An error returns an HTTP status and a JSON body with an error field.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_parameter | A parameter is invalid, e.g. limit outside 1 to 100. |
| 401 | missing_api_key | No key was sent. |
| 401 | invalid_api_key | Unknown or revoked key. |
| 402 | active_subscription_required | No active subscription on this account. |
| 402 | plan_upgrade_required | Endpoint not in your plan (reason: exports_not_in_plan). Catalog and trends need Pro or Scale. |
| 403 | plan_restriction | Platform outside your databases (reason: platform_locked). The allowed field lists your databases. |
| 404 | not_found | Unknown domain. |
| 404 | no_catalog | No catalog for this store. |
| 404 | no_trend | No trend for this store. |
| 409 | no_database_selected | Pick your databases in your account first. |
| 429 | quota_exceeded | Request quota used up until resetAt. |
| 429 | store_quota_exceeded | Store budget used up until resetAt. |
| 429 | rate_limited | Too many calls in a short time. Wait for the Retry-After delay. |
| 429 | platform_change_limited | Database changes are limited to 1 every 30 days. |
| 400 | too_many_stores | More than 25 stores in one enrichment. |
| 402 | provider_out_of_credits | Your account at the provider has no credits left. |
| 404 | provider_not_available | This provider is not open for enrichment. |
| 409 | provider_key_missing | No key saved for this provider. |
| 422 | provider_key_invalid | The provider refused your key. Replace it in your account. |
| 422 | provider_plan_has_no_api | Your plan at the provider does not include API access. |
| 429 | byok_open_jobs | 3 enrichments are already running. Wait for one to finish. |
| 429 | byok_daily_cap | 500 people sent to this provider today. Try again tomorrow. |
| 429 | provider_rate_limited | The provider asks to slow down. The job resumes on its own. |
| 502 | provider_unavailable | The provider is not responding. The job retries on its own. |
| 503 | opt_out_unavailable | A pause of a few minutes: the objection check is not responding. Nothing is sent. |
Example response when the store budget is used up:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 25000
X-RateLimit-Remaining: 0
X-Stores-Limit: 10000
X-Stores-Remaining: 0
{
"error": "store_quota_exceeded",
"plan": "api_pro",
"quotaMonthly": 25000,
"quotaUsed": …,
"storesMonthly": 10000,
"storesUsed": 10000,
"resetAt": "…"
}MCP server
Connect your AI agent (Claude, Cursor…) to the database. Same key, same quotas. The server offers 8 tools:
search_stores: search stores with filters.count_stores: count stores.get_store: read one store's record.get_store_catalog: read the catalog (Pro and Scale).get_store_trend: read the traffic trend (Pro and Scale).enrich_store: enrich the decision makers of one or more stores with your provider key.get_enrichment: follow an enrichment, read a store's results or list your providers.get_quota: see your plan and remaining quota.
Claude Code
claude mcp add eldb --transport http https://mcp.ecommerce-leads-database.com/mcp \ --header "X-API-Key: eldb_live_…"
Cursor and compatible clients (JSON)
For a client that accepts a remote HTTP server with headers, such as Cursor (mcp.json):
{
"mcpServers": {
"eldb": {
"url": "https://mcp.ecommerce-leads-database.com/mcp",
"headers": {
"Authorization": "Bearer eldb_live_…"
}
}
}
}Claude Desktop: use the mcp-remote bridge, in claude_desktop_config.json:
{
"mcpServers": {
"eldb": {
"command": "npx",
"args": [
"-y", "mcp-remote", "https://mcp.ecommerce-leads-database.com/mcp",
"--header", "Authorization: Bearer eldb_live_…"
]
}
}
}The key always goes in a header. Never put it in the URL.
Code examples
List every Shopify store with a Google profile, page by page.
JavaScript (fetch)
const API = "https://api.ecommerce-leads-database.com";
const headers = { "X-API-Key": process.env.ELDB_API_KEY };
let cursor = null;
do {
const url = new URL("/v1/stores/search", API);
url.searchParams.set("platform", "shopify");
url.searchParams.set("hasGmb", "true");
url.searchParams.set("limit", "50");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers });
if (res.status === 429) {
console.log(await res.json()); // quota or budget used up: see resetAt
break;
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { items, nextCursor } = await res.json();
for (const store of items) console.log(store.domain);
console.log("Stores left:", res.headers.get("X-Stores-Remaining"));
cursor = nextCursor;
} while (cursor);Python (requests)
import os
import requests
API = "https://api.ecommerce-leads-database.com"
headers = {"X-API-Key": os.environ["ELDB_API_KEY"]}
params = {"platform": "shopify", "hasGmb": "true", "limit": 50}
while True:
r = requests.get(f"{API}/v1/stores/search", headers=headers, params=params)
if r.status_code == 429:
print(r.json()) # quota or budget used up: see resetAt
break
r.raise_for_status()
data = r.json()
for store in data["items"]:
print(store["domain"])
print("Stores left:", r.headers.get("X-Stores-Remaining"))
if not data["nextCursor"]:
break
params["cursor"] = data["nextCursor"]