MyLeadFox

Developer API

The complete filter surface, not a reduced subset

Five filter kinds, 19 operators, and/or connectors, per-filter negation, global search and eight sort keys. Anything you can build in the interface you can express in a request.

Quickstart

Base URL https://go.myleadfox.com/api/v1. Authenticate with a bearer token; an X-API-Key header is accepted for tools that expose a custom-header field more easily than an Authorization builder.

curl -X POST https://go.myleadfox.com/api/v1/search \
  -H 'Authorization: Bearer mlf_live_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"filters":[{"kind":"dropdown","col":"country","values":["US"]}]}'

Never ship a key in client-side code.

Anything in a browser bundle is public however it is obfuscated. Call the API from your server. Entitlement is re-checked on every call, so a revoked key stops working on the very next request.

Playground

Build a real request

Compose a filter expression, pick columns, and take the generated cURL straight to your terminal.

Counts and a summary for a filter set. It never returns rows. Returns a search_id you can page through afterwards, so you size a segment before spending download credits on it.

Held in memory for this page only, never saved to browser storage, never sent anywhere except the API. Reloading clears it.

Filters

Request

curl -X POST https://go.myleadfox.com/api/v1/search \
  -H 'Authorization: Bearer mlf_live_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "filters": [
    {
      "kind": "dropdown",
      "col": "country",
      "values": [
        "US"
      ]
    }
  ]
}'

Reference

Four endpoints

POST/api/v1/rows

Rows, one page of shops

The only endpoint that returns row data. Any page is directly addressable: page 500 needs no walk through 1–499. If your balance is smaller than the page you receive as many rows as you can afford and truncated_by_credits is true: a partial balance is spent, never stranded.

Scope download1 lead per row DELIVERED, never rounded up to a page

Body fields

FieldTypeMeaning
search_idstringPage an existing search. Wins if you also send filters.
filters / searcharray / stringOr select inline, exactly as for /search.
pageinteger1-based. Any page directly addressable.
limitintegerRows to return, 1–100. You are charged what you receive.
columnsarrayWhich of the 98 columns to return. Omit or ["all"] for every one.
include_totalbooleanOpt-in live total. Costs the heavier metering weight.
sortarrayUp to 8 sort keys.

Request

curl -X POST https://go.myleadfox.com/api/v1/rows \
  -H 'Authorization: Bearer mlf_live_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "filters": [
    { "kind": "dropdown", "col": "country", "values": ["US"] }
  ],
  "limit": 3,
  "columns": ["domain", "name", "country", "products"]
}'

200 response

{
  "search_id": "de11cac33c3da5df",
  "page": 1,
  "page_size": 100,
  "limit": 3,
  "returned": 3,
  "columns": ["domain", "name", "country", "products"],
  "rows": [
    {
      "domain": "a-1batteries.com",
      "name": "A-1 Accredited Batteries",
      "products": 65,
      "country": "US"
    }
  ],
  "truncated_by_credits": false,
  "credits": {
    "search_remaining": 4997,
    "download_remaining": 199997,
    "enrich_remaining": 500,
    "charged": 3,
    "charged_unit": "leads"
  }
}
POST/api/v1/lookup

Lookup: fetch shops by domain

Fetch rows for up to 100 domains per call. Three lists come back and they mean different things: rows were found and charged; missing are not in MyLeadFox and will still be missing tomorrow; withheld_for_credits are in MyLeadFox but past your balance: free, so top up and ask again.

Scope downloadcharged per domain FOUND: misses are free and named

Body fields

FieldTypeMeaning
domainsarray | stringUp to 100, as a JSON array or a comma-separated string.
columnsarrayWhich columns to return.

Request

curl -X POST https://go.myleadfox.com/api/v1/lookup \
  -H 'Authorization: Bearer mlf_live_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "domains": ["gymshark.com", "not-a-real-shop.example"],
  "columns": ["domain", "name", "country", "products"]
}'

200 response

{
  "returned": 1,
  "rows": [
    {
      "domain": "gymshark.com",
      "name": "Gymshark",
      "country": "GB",
      "products": 1284
    }
  ],
  "missing": ["not-a-real-shop.example"],
  "withheld_for_credits": [],
  "credits": {
    "download_remaining": 199996,
    "charged": 1,
    "charged_unit": "leads"
  }
}
POST/api/v1/enrich

Enrich: find contacts

Find contacts for domains already in the dataset. Every domain returns in exactly one of four buckets and only the first charges. Enrichment is synchronous: each domain is checked in real time as the request runs, so keep batches small.

Scope enrichonly the `enriched` outcome charges: the other five are free

Body fields

FieldTypeMeaning
domainsarrayUp to 100 per call. Sustain roughly one 100-domain batch per minute.

Request

curl -X POST https://go.myleadfox.com/api/v1/enrich \
  -H 'Authorization: Bearer mlf_live_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "domains": ["gymshark.com"]
}'

200 response

{
  "results": [
    {
      "domain": "gymshark.com",
      "outcome": "enriched",
      "emails": ["hello@gymshark.com"],
      "phone": null,
      "mailbox_provider": "Google Workspace",
      "mailbox_category": "business"
    }
  ],
  "credits": {
    "enrich_remaining": 499,
    "charged": 1,
    "charged_unit": "enrichments"
  }
}

The six outcomes: only the first charges

outcomeMeaningCharged
enrichedWe checked in real time and found contacts.Yes: 1 credit
alreadyYou enriched this domain recently and still hold it; contacts come from cache.No
not_in_gridThe domain is not in the MyLeadFox dataset, so we will not enrich it.No
no_contact_foundIt is in the dataset, we checked, and there was nobody to return.No
skipped_no_creditYour balance ran out mid-batch.No
not_attemptedThe call reached its time budget: send those domains again.No

Filters

Every operator, with live match counts

Each row below was executed against the real dataset. The count is what it returned.

Combining filters

{"filters": [
  {"kind": "dropdown", "col": "country", "values": ["US"], "conn": "or"},
  {"kind": "dropdown", "col": "country", "values": ["GB"], "conn": "or"},
  {"kind": "numeric",  "col": "products", "op": "gte", "min": 50},
  {"kind": "text",     "col": "tagline",  "mode": "contains", "value": "coffee", "not": true}
]}
  • conn is and (default) or or. The result is (every and matches) AND (at least one or matches).
  • not: true inverts a single filter.
  • Position is not meaning. Reordering the list cannot change the result.
  • search is not a filter and has no connector: it always ANDs onto the whole expression, so it can never be widened by someone else’s or.
kindoperatorexamplematches
textcontains{"kind":"text","col":"name","mode":"contains","value":"coffee"}18,766
textstarts{"kind":"text","col":"domain","mode":"starts","value":"shop"}140,335
textends{"kind":"text","col":"domain","mode":"ends","value":".shop"}83,831
textexact{"kind":"text","col":"domain","mode":"exact","value":"gymshark.com"}1
textempty{"kind":"text","col":"tagline","mode":"empty"}4,087,868
textnot_empty{"kind":"text","col":"tagline","mode":"not_empty"}4,832,067
textregex{"kind":"text","col":"domain","mode":"regex","parts":[{"term":"coffee","conn":"not"},{"term":"shop"}]}24,284
numericbetween{"kind":"numeric","col":"products","min":10,"max":100,"op":"between"}1,539,598
numericgte{"kind":"numeric","col":"products","op":"gte","min":500}504,984
numericlte{"kind":"numeric","col":"products","op":"lte","max":5}816,032
numericeq{"kind":"numeric","col":"products","op":"eq","min":1}223,909
numericneq{"kind":"numeric","col":"products","op":"neq","min":0}3,696,424
numericempty{"kind":"numeric","col":"products","op":"empty"}5,030,260
numericnot_empty{"kind":"numeric","col":"products","op":"not_empty"}3,889,675
dropdownvalues{"kind":"dropdown","col":"country","values":["US","GB"]}1,645,791
tagsany{"kind":"tags","col":"categories","mode":"any","values":["Shopping"]}136,474
tagsall{"kind":"tags","col":"categories","mode":"all","values":["Apparel & Accessories > Clothing"]}273
tagsnone{"kind":"tags","col":"categories","mode":"none","values":["Apparel & Accessories > Clothing > T-Shirts"]}8,754,419
datefrom/to{"kind":"date","col":"registered","from":"2020-01-01","to":"2024-12-31"}2,427,890

Global text search

{"search": "coffee"} matches a substring, case-insensitively, across six columns: domain, name, tagline, description_preview, emails, categories. Deliberately a substring match, so coffee finds coffeehouse.

Sorting

Up to 8 keys. An unknown column is a 400 unknown_column, unlike the app which ignores it, because a machine that misspells a column would otherwise receive a correct-looking 200 sorted by something else entirely.

Limits

Published, and returned on every response

Breaching a limit returns 429 with a Retry-After header, and is never charged.

Rate limits

Per key
60 weighted units / minute
Per key, concurrent
2 requests

What a call costs

Not every call costs the same unit. A plain filtered query is the cheapest; a global text search costs more. You never have to guess: every response carries your remaining allowance in its headers, and a 429 carries Retry-After.

Typical speed

Filtered page (dropdown filters)
Fast
Filtered page, deep (page 100)
Fast
Global text search: first call
Slower
Global text search: later pages
Quick

Give your client a generous timeout rather than a tight one.

Pagination: 100 rows per page maximum, and any page is directly addressable: page 500 needs no walk through 1–499. Ask for fewer with limit and you are charged fewer.

Errors

Seventeen codes, none of them charged

Branch on the error code. It is stable; the message is not.

{
  "error": "insufficient_credits",
  "message": "Not enough download credits for this page.",
  "pool": "download",
  "needed": 100,
  "remaining": 42
}
CodeHTTPMeaning
missing_api_key401No key was sent.
invalid_api_key401Unknown, malformed, rotated or revoked: deliberately indistinguishable.
ip_not_allowed403The key has an IP allowlist and you are not on it.
insufficient_scope403The key lacks the scope this call needs.
free_trial_no_api403The API is not on the Free Trial.
account_inactive403The account is closed or locked.
account_frozen402A subscription payment failed. Settle the invoice.
rate_limited429Too fast, or the server is busy. Honour Retry-After.
invalid_request400The request body is wrong.
unknown_column400You asked for a column that does not exist.
search_not_found404No saved search has that id.
page_out_of_range404That page does not exist; `pages` tells you the real count.
too_many_domains400Over the per-call domain ceiling.
insufficient_credits402Not enough credits in that pool.
feature_disabled403Your team owner turned this off for your account.
key_management_forbidden403Only the account owner manages keys.
server_error500Our fault. You were not charged. Retry.

Extra fields worth branching on

CodeExtra fields
rate_limitedretry_after (seconds), reason (per_key · server_busy · fair_share · too_many_concurrent)
page_out_of_rangepages, requested
too_many_domainslimit, given
insufficient_creditspool, needed, remaining
unknown_columncolumn
insufficient_scoperequired_scope
account_frozensince, invoice_id

No error is ever charged.

A mistake in your request is a 4xx and costs nothing. A failure on our side is a 500 and costs nothing. A rate-limit breach is a 429 and costs nothing. Abuse is stopped by the rate limits, not by billing you for mistakes.

Columns

All 98 queryable columns

The columns field accepts any name below, or ["all"] for every one. Asking for a name that is not on this list is a 400 unknown_column: nothing is silently dropped.

Identity & status (9)

  • domaintext
  • statusdropdown
  • status_reasondropdown
  • actual_platformdropdown
  • shop_activedropdown
  • nametext
  • taglinetext
  • description_previewtext
  • myshopifytext

Catalogue (4)

  • productsnumeric
  • collectionsnumeric
  • categoriestags
  • shop_collectionstext

Location & market (7)

  • citytext
  • statetext
  • countrydropdown
  • ships_tonumeric
  • agenumeric
  • registereddate
  • currencydropdown

Language (7)

  • primary_langdropdown
  • primary_lang_namedropdown
  • languagetags
  • language_namestags
  • hreflang_countnumeric
  • hreflang_listtags
  • is_multilingualdropdown

Contact (5)

  • emailstext
  • has_emaildropdown
  • phonetext
  • has_phonedropdown
  • contact_urltext

Technology (3)

  • themetext
  • appstags
  • other_techtags

Social (9)

  • has_any_socialdropdown
  • social_countnumeric
  • instagramtext
  • facebooktext
  • tiktoktext
  • twittertext
  • youtubetext
  • pinteresttext
  • linkedintext

Performance (lab) (18)

  • lh_perfnumeric
  • lh_seonumeric
  • lh_a11ynumeric
  • lh_bpnumeric
  • lcpnumeric
  • fcpnumeric
  • sinumeric
  • tbtnumeric
  • clsnumeric
  • lh_perf_desknumeric
  • lh_seo_desknumeric
  • lh_a11y_desknumeric
  • lh_bp_desknumeric
  • lcp_desknumeric
  • fcp_desknumeric
  • si_desknumeric
  • tbt_desknumeric
  • cls_desknumeric

Performance (field / CrUX) (7)

  • crux_lcpnumeric
  • crux_inpnumeric
  • crux_fcpnumeric
  • crux_ttfbnumeric
  • crux_clsnumeric
  • crux_verdictdropdown
  • psi_statusdropdown

SEO (10)

  • seo_titletext
  • favicontext
  • h1_countnumeric
  • og_titletext
  • og_descriptiontext
  • og_imagetext
  • og_typedropdown
  • og_site_nametext
  • og_urltext
  • seo_completenessnumeric

Record metadata (1)

  • http_codedropdown

Other (18)

  • mail_providerdropdown
  • mail_categorydropdown
  • mail_hosttext
  • ship_intldropdown
  • ship_partnerstags
  • ship_policy_urltext
  • pixelstags
  • videotags
  • crux_has_datadropdown
  • seo_title_lennumeric
  • seo_metatext
  • seo_meta_lennumeric
  • canonicaltext
  • robotstext
  • h1_firsttext
  • sd_typestags
  • data_completenessnumeric
  • has_lighthouse_datadropdown

Questions

API frequently asked

How do I authenticate?

Send your key as a bearer token: `Authorization: Bearer mlf_live_…`. An `X-API-Key` header is accepted as an alternative for tools that expose a custom-header field more easily than an Authorization builder. Entitlement is re-checked on every call, so a plan change or an unpaid invoice stops a key working on the very next request.

What are the rate limits, exactly?

60 weighted units per minute per key, and 2 concurrent requests per key. Not every call costs the same unit: a plain filtered query is the cheapest and a global text search costs more. You never have to guess, because every response returns your remaining allowance in its headers. Breaching a limit returns 429 with a Retry-After header and is never charged.

Am I ever charged for an error?

No. A mistake in your request is a 4xx and costs nothing, a failure on our side is a 500 and costs nothing, and a rate-limit breach is a 429 and costs nothing. Abuse is stopped by the rate limits, not by billing you for mistakes.

Does the API expose the whole filter surface, or a reduced subset?

The whole surface. Five filter kinds, nineteen operators, and/or connectors, per-filter negation with not: true, global search, and sorting on up to eight keys: everything the application offers. Anything you can build in the interface you can express in a request.

How does pagination work?

Up to 100 rows per page, and any page is directly addressable: page 500 needs no walk through pages 1 to 499. Ask for fewer with limit and you are charged fewer. A search_id lets you page a result set without re-sending the filters.

How fast is it?

Filtered queries are fast, including deep pages: asking for page 500 costs no more than page 1. A global text search is slower on its first call and quicker on the pages after it, because the first call does the work the rest reuse. Give your client a generous timeout rather than a tight one.

What happens if my balance runs out mid-page?

You receive as many rows as you can afford and truncated_by_credits comes back true. A partial balance is spent, never stranded. On lookup, domains past your balance return in withheld_for_credits: free, and still there when you top up.

Is the API available on the Free Trial?

No: it is available on every paid plan. A Free Trial key returns free_trial_no_api with a 403.

Get an API key

Included on Starter and Pro at no extra cost, drawing from the same allowance as the web app.