BootLeads API
Search and retrieve store profiles over HTTPS with JSON. The API comes with the Enterprise plan and returns the same store data as the search and CSV export.
https://bootleads.com/api/v2Authorization: Bearer <key>- Enterprise
- +50,000 a day
- 3 requests a second
- Up to 100 stores
Quick start
Copy your key from Account settings and send it as a Bearer token. The key works while your own Enterprise subscription is active.
curl https://bootleads.com/api/v2/stores/search \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"platform": ["shopify"], "country": ["US"], "page": {"size": 100}, "fields": ["title", "contact"]}'
import requests response = requests.post( "https://bootleads.com/api/v2/stores/search", headers={"Authorization": "Bearer YOUR_API_KEY"}, json={"platform": ["shopify"], "country": ["US"], "page": {"size": 100}, "fields": ["title", "contact"]}, timeout=60, ) data = response.json() print(response.headers["X-Rows-Remaining"], [store["id"] for store in data["results"]])
const response = await fetch("https://bootleads.com/api/v2/stores/search", { method: "POST", headers: {"Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json"}, body: JSON.stringify({platform: ["shopify"], country: ["US"], page: {size: 100}, fields: ["title", "contact"]}), }); const data = await response.json(); console.log(response.headers.get("X-Rows-Remaining"), data.results.map((store) => store.id));
Rows and rate limits
-
Every store returned by
stores/searchorstores/mgetis one row off your balance. The balance grows by 50,000 rows at 00:00 UTC each day and holds up to 3,000,000 rows. See how the balance adds up. -
A request runs while the balance is above zero, so the last page can take it a little below zero. After that, requests return
429with the coderow_quota_exhausteduntil the next refill. -
Each API key can make 3 requests per second. Faster requests return
429with the coderate_limit_exceeded.
Only the stores you get back use rows. Missing ids, empty pages, errors and GET /filters are free.
| Response header | Meaning |
|---|---|
X-Rows-Remaining | Rows left after this request. |
X-Rows-Refill-At | When the next refill happens, in UTC (ISO 8601). |
Retry-After | On a 429: seconds to wait before trying again. |
How the balance adds up
The balance is not a monthly allowance. It grows a little every day you are subscribed, and your billing date does not change it.
- First purchase: your first Enterprise subscription starts the balance full, at 3,000,000 rows.
- Every day: at 00:00 UTC the balance grows by 50,000 rows. Rows you don't use carry over until the balance reaches 3,000,000, which is 60 days of refills.
- Renewals: renewing does not reset the balance or top it up. It keeps growing by the day, as before.
- If the subscription ends: the balance stops growing after the last day you paid for and keeps the rows you had. When you subscribe again, it grows again from the next day. The days in between add nothing.
For example, one account over four months:
| Date | What happens | Balance after |
|---|---|---|
| Oct 1 | You buy Enterprise. The balance starts full. | 3,000,000 |
| Oct 1 | Your first sync downloads 2,800,000 stores. | 200,000 |
| Oct 2–31 | 30 refills of 50,000. Your daily sync uses 5,000 a day. | 1,550,000 |
| Nov 1 | The subscription renews. The balance is not reset or topped up; the daily refill carries on. | 1,595,000 |
| Dec 1 | The subscription ends. Its last day still gets a refill, and the rows you have left are kept. | 2,945,000 |
| Jan 15 | You buy Enterprise again. The 45 days without a subscription add nothing. | 2,945,000 |
| Jan 16 | The daily refill starts again. | 2,995,000 |
Pagination and syncing
Search results come in pages of up to 100 stores, ordered by created_at (newest first unless you set sort.created_at to asc). While has_more is true, pass next_cursor back in page.cursor for the next page; on the last page it is an empty string. A cursor keeps the filters, sort and fields of the request that started it, so send it without filters or sort. Cursors do not expire: you can stop and continue days later. A cursor issued before a change to our filters is rejected with cursor_invalid_or_expired; start a new search.
Keeping a copy in sync
Every page also returns resume_cursor, a bookmark just after the last store on the page, and the last page has one too. What it brings back depends on the sort order:
-
Oldest first (
sort.created_atset toasc): it is a sync bookmark. Pass it later and you get the stores added since that match the cursor's filters. Use this to keep a copy of the data. -
Newest first (the default): it carries on towards older stores. It picks up a run you stopped halfway, but it never brings newer stores. To check for new stores in this order, set
created_at.minto the newestcreated_atyou already have, and drop the ids you have seen.
Stores that match your filters later are not returned
The bookmark follows the date a store was added (created_at), while filters are checked against each store as it is now. A password-protected store that opens, or a store whose category, followers or Shopify registration country we fill in later, sits behind your bookmark. Sync with filters that do not change once a store is added, such as platform, include password_protected in status, and apply the rest of your filters on your side.
- First run: send
platformandstatuswithsort.created_atset toasc, page untilhas_moreis false, and saveresume_cursorwith each page you store. - A whole platform can hold more stores than your row balance. When a request returns
row_quota_exhausted, stop and continue from the saved cursor after the next refill. - Every day after: send the saved cursor, page to the end and save the new
resume_cursor. - Store results by
id: a store can come back more than once. - Every few days, send the ids of stores you hold as
password_protectedto /stores/mget to see which have opened.
import time import requests API = "https://bootleads.com/api/v2/stores/search" HEADERS = {"Authorization": "Bearer YOUR_API_KEY"} FIRST_RUN = { "platform": ["shopify"], "status": ["active", "password_protected"], "sort": {"created_at": "asc"}, "page": {"size": 100}, } def sync(bookmark=None): """Page from the bookmark to the end; returns the bookmark for the next run.""" body = {"page": {"size": 100, "cursor": bookmark}} if bookmark else FIRST_RUN while True: resp = requests.post(API, json=body, headers=HEADERS, timeout=60) if resp.status_code == 429: if resp.json()["error"]["code"] == "row_quota_exhausted": return bookmark # out of rows: run again after the refill time.sleep(int(resp.headers.get("Retry-After", 1))) # too fast: wait, then retry continue resp.raise_for_status() data = resp.json() bookmark = data["resume_cursor"] save(data["results"], bookmark) # upsert by id and keep the bookmark together if not data["has_more"]: return bookmark body = {"page": {"size": 100, "cursor": data["next_cursor"]}}
Other ways a store can slip past a bookmark
Pages are not a frozen snapshot. Stores that close while you page drop out of later pages, and stores added meanwhile show up at the end of an ascending run. A store reaches search a few minutes after it is added, and when we merge two addresses of one store or a store moves to another platform it keeps the earlier created_at; either way it can land just before your bookmark. Rerun the full sync now and then if you need every one.
Endpoints
POST/stores/search
One row per store returned
Search stores. The body takes any of the filters as top-level keys, plus page (size 1–100, default 50, and cursor), sort ({"created_at": "asc"} or "desc") and fields. Unknown keys are rejected.
curl https://bootleads.com/api/v2/stores/search \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": ["shopify"], "country": ["US", "CA"], "technologies": ["klaviyo"], "product_count": {"min": 20}, "page": {"size": 100}, "fields": ["title", "country", "contact", "social"] }'
{ "has_more": true, "next_cursor": "eyJ2IjoyLCJhZnRlciI6WzE3NjA5NDc5NjQwMDAsImV4YW1wbGUtc3RvcmUuY29tIl0s...", "resume_cursor": "eyJ2IjoyLCJhZnRlciI6WzE3NjA5NDc5NjQwMDAsImV4YW1wbGUtc3RvcmUuY29tIl0s...", "results": [ { "id": "example-store.com", "created_at": "2026-10-20T08:12:44+00:00", "title": "Example Store", "country": "US", "contact": {"email": ["hello@example-store.com"], "phone": ["+1 555 123 4567"]}, "social": {"platforms": ["instagram"], "total_followers": 15200} } ] }
A full store object, without fields
Keys without a value are left out or come back empty. Each key is described under Response fields.
{ "id": "example-store.com", "platform": "shopify", "title": "Example Store", "description": "Handmade leather shoes, shipped from New York.", "categories": [ "Apparel & Accessories / Shoes & Footwear" ], "country": "US", "language": "en", "status": "active", "is_cod": false, "has_ios_app": false, "has_android_app": false, "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-03-20T14:00:00.000Z", "product": { "count": 128, "avg_price": 39.99, "avg_price_usd": 39.99, "currency": "USD" }, "apps": { "active": [ { "slug": "example-app", "name": "Example Reviews App", "image": "https://cdn.shopify.com/app-store/listing_images/xxx/icon/xxx.png", "url": "https://apps.shopify.com/example-app" } ] }, "technologies": { "active": [ { "id": "klaviyo", "name": "Klaviyo", "category": "email_sms_marketing", "website": "https://www.klaviyo.com/" } ], "checked_at": "2025-03-20T14:00:00.000Z" }, "contact": { "email": [ "hello@example-store.com" ], "phone": [ "+12125550147" ], "whatsapp": [ "12125550147" ] }, "social": { "platforms": [ "instagram", "tiktok" ], "total_followers": 18400, "link": { "instagram": "https://www.instagram.com/examplestore", "tiktok": "https://www.tiktok.com/@examplestore" }, "followers": { "instagram": 15200, "tiktok": 3200 } }, "fb_ads": { "count": 12, "last_start_date": "2025-03-18T00:00:00+00:00" }, "shopify": { "myshopify": "example-store.myshopify.com", "theme_schema_name": "Dawn", "registration_country": "US", "city": "New York", "province": "New York", "ships_to_countries": [ "US", "CA", "GB" ], "offers_installments": false, "payment_methods": [ "SHOP_PAY", "APPLE_PAY" ] } }
POST/stores/mget
One row per store found
Get up to 50 stores by domain, in the order you ask for them. Domains we do not have come back in missing_ids and do not use rows. Takes fields too.
curl https://bootleads.com/api/v2/stores/mget \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"ids": ["example-store.com", "unknown-store.com"], "fields": ["platform", "product"]}'
{ "missing_ids": ["unknown-store.com"], "results": [ { "id": "example-store.com", "created_at": "2025-01-15T10:30:00+00:00", "platform": "shopify", "product": {"count": 128, "avg_price": 39.99, "avg_price_usd": 39.99, "currency": "USD"} } ] }
GET/filters
Free
Every filter with how to write it, and for list filters the values you can send, with approximate store counts refreshed hourly. Send the key of a value, not its label: labels can change. Add ?key=apps to get one filter.
curl "https://bootleads.com/api/v2/filters?key=country" -H "Authorization: Bearer YOUR_API_KEY"
{ "filters": [ { "key": "country", "type": "array", "description": "Where the store is based, not where it sells ...", "docs_url": "https://bootleads.com/data/filters/#filter-country", "values": [{"key": "US", "label": "United States", "count": 2154021}, ...] } ] }
Filters
Filters are top-level keys of the search body. Each one narrows the results; several values in one filter match a store that has any of them. Ranges take {"min": .., "max": ..}, both inclusive and either optional; dates are ISO 8601 and use UTC. The descriptions are the ones in the store search, so the filter reference shows the same text.
Only active stores come back unless you set status. List filters take the value keys from GET /filters. Filters with a fixed set of lowercase values, such as status, reject a value they don't know with 422. In the others case does not matter, except in theme and province names; UK works as GB, and a value we don't have, such as a misspelt country, matches no store. Where a description mentions Unknown, send unknown for stores without a value.
Basics
Matches stores whose title or description contains any of the words.
In the filter referenceThe ecommerce platform the store runs on. Smaller platforms are recorded under their own name, such as `opencart`, but only those with enough stores are listed as values; `custom` means storefront software built in-house.
In the filter referenceWhere the store is based, not where it sells: many stores ship worldwide. Shopify uses the registered country; other platforms are inferred from store settings, phone, domain and currency. Send `unknown` for stores with no detected country.
In the filter referenceprovince
Shopify only
The state, province or region the merchant registered with Shopify, such as California or Ontario. Shopify only asks for one in some countries. Write each value as country code and name, e.g. US:California.
In the filter referenceA top-level category; it also matches stores in any of its subcategories. Use `subcategory` for a single subcategory. Assigned by an AI model; send `unknown` for stores it has not classified yet. Category and subcategory values combine: a store in any of them matches.
In the filter referenceA subcategory. Assigned by an AI model; send `unknown` for stores it has not classified yet. Category and subcategory values combine: a store in any of them matches.
In the filter referenceThe main language of the storefront.
In the filter referenceWhether visitors can open the store. Password-protected stores are usually not launched yet or temporarily closed. Only active stores are returned unless you set it.
activepassword_protected
When we first found the store. Dates use UTC.
In the filter referenceProducts
Number of products in the catalog. A minimum of 0 includes stores with an empty catalog.
In the filter referenceAverage product price in US dollars.
In the filter referenceThe currency the store sells in.
In the filter referenceships_to
Shopify only
Countries the store delivers to, from its Shopify shipping settings. `*` (Worldwide) matches stores that ship everywhere.
In the filter referencelatest_product_at
Shopify & WooCommerce only
When the store last added a product.
In the filter referenceContact & social
Stores that link to a profile on any of the chosen networks. Send `unknown` for stores with no social profile.
In the filter referenceFollowers across all of the store's social profiles.
In the filter referenceAdvertising
Whether the store advertises on Facebook and Instagram: started an ad in the last 30 days, has run ads at some point, or never seen advertising.
activeevernever
Number of Meta ads the store started in the last 30 days.
In the filter referenceStores with a tracking pixel from any of the chosen vendors. Send `unknown` for stores with no pixel at all.
In the filter referenceBusiness & traits
How the store fulfils orders: dropshipping, print on demand or cash on delivery. Dropshipping is read from a store's category links, Shopify apps and product vendors, print on demand from its Shopify apps and product vendors, and cash on delivery from page content.
dropshippingpodcod
Stores that match every chosen trait. Shopify Plus and Offers Installments (Shop Pay Installments) apply to Shopify stores only.
shopify_plushas_mobile_appplatform_changedoffers_installments
The platform a store ran on before its current one. Only switches seen since we started tracking the store are known.
In the filter referenceStores that link to the chosen page types, such as wholesale, affiliate or shipping.
In the filter referenceTechnologies & apps
Stores running any of the chosen tools, detected on the store's homepage across all platforms: email, reviews, subscriptions, support, search, ads and more.
In the filter referenceStores running any tool in the chosen categories. Combined with technologies, a store must match both.
In the filter referenceapps
Shopify only
Stores running any of the chosen apps.
In the filter referencetheme
Shopify only
The theme the storefront is built on, such as Dawn, Prestige or the retired Debut. Custom themes keep whatever name their developer gave them.
In the filter referenceDomain
A custom domain, or a platform address such as myshopify.com.
customplatform
The domain ending, such as .com or .co.uk.
In the filter referenceWhen the domain was registered.
In the filter referenceResponse fields
Each store is a JSON object keyed by its domain in id; fields without a value are left out or come back empty. To get less, pass fields with any of the top-level keys below; id and created_at always come back.
platformtitledescriptioncountrylanguagestatuscategoriesis_dropshippingis_codis_print_on_demandhas_ios_apphas_android_appupdated_atproductappstechnologiescontactsocialfb_adsshopify
Store basics
id
The store's website address. Each domain is one store.
platform
The ecommerce platform the store runs on. Besides the 6 main platforms, stores on 53 smaller platforms such as OpenCart, Shopware and Cafe24 are recorded under their own name. Custom-built means the store runs storefront software built in-house, or on a platform we don't recognise yet.
status
Whether the store is open or behind a password page.
title
The store name shown in the browser tab.
description
The short description the store gives search engines.
shopify.myshopify
The store's permanent myshopify.com address.
Geography & classification
country
Where the store is based, not where it sells.
shopify.registration_countryshopify.provinceshopify.city
City, state or province, and country the store registered with Shopify.
language
The main language of the storefront.
categories
Category and subcategory from a taxonomy of 29 categories and 159 subcategories.
is_dropshippingis_print_on_demandis_codhas_ios_apphas_android_appshopify.offers_installments
Yes/no flags for dropshipping, print on demand, cash on delivery, an iOS or Android app, and installment payments such as Shop Pay Installments (Shopify only).
shopify.ships_to_countries
Countries the store delivers to. Stores that ship everywhere show Worldwide.
Products & pricing
product.count
Number of products in the catalog.
product.avg_price_usd
Average product price, converted to US dollars.
product.avg_price
Average product price in the store's own currency.
product.currency
The currency the store sells in.
shopify.payment_methods
Wallets offered at checkout: Shop Pay, PayPal, Apple Pay, Google Pay and Amazon Pay.
Contact
contact.email
Email addresses on the store's own domain.
contact.phone
Phone numbers in international format.
contact.whatsapp
WhatsApp numbers from chat links and buttons.
Social reach
social.total_followers
Followers across the store's Facebook, Instagram, YouTube, Pinterest and TikTok profiles.
social.followers.facebook
Followers of the store's Facebook page.
social.followers.instagram
Followers of the store's Instagram account.
social.followers.tiktok
Followers of the store's TikTok account.
social.followers.youtube
Subscribers to the store's YouTube channel.
social.followers.pinterest
Followers of the store's Pinterest profile.
social.link.facebooksocial.platforms
The store's Facebook page.
social.link.instagramsocial.platforms
The store's Instagram profile.
social.link.tiktoksocial.platforms
The store's TikTok profile.
social.link.youtubesocial.platforms
The store's YouTube channel.
social.link.twittersocial.platforms
The store's X (Twitter) profile.
social.link.pinterestsocial.platforms
The store's Pinterest profile.
social.link.linkedinsocial.platforms
The store's LinkedIn page.
Ads & technology
fb_ads.count
Meta (Facebook and Instagram) ads the store started in the last 30 days.
fb_ads.last_start_date
Start date of the store's most recent Meta ad.
apps.active
Shopify apps running on the store, with name, icon and App Store link.
technologies.activetechnologies.checked_at
Third-party tools running on the store, in 13 categories, and when the store was last checked for them.
shopify.theme_schema_name
The Shopify theme the store uses.
Dates
created_at
When we first found the store.
updated_at
When we last checked the store.
Errors
Errors return a status code and a JSON body with a stable code and a readable message:
{"error": {"code": "row_quota_exhausted", "msg": "Row balance is used up. It refills at 00:00 UTC."}}
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_parameters | The request cannot be run, for example a cursor sent together with filters. | Fix the request |
| 400 | cursor_invalid_or_expired | The cursor is not one we issued, or was issued before a change to our filters. | Start a new search |
| 401 | authentication_failed | Missing or unknown API key. | Check the key |
| 402 | subscription_expired | The Enterprise subscription has ended. | Renew the plan. If it renews automatically, retry in a few minutes: the renewal can take a moment to arrive. |
| 403 | api_permission_denied | The account's own plan does not include the API. | Use an Enterprise account's key |
| 403 | account_disabled | The account has been disabled. | Contact us |
| 404 | not_found | No such endpoint. | Check the path |
| 422 | validation_error | A field is unknown or has the wrong type; the message names it. | Fix the field |
| 429 | rate_limit_exceeded | More than the per-second limit. | Retry after Retry-After |
| 429 | row_quota_exhausted | The row balance is used up until the next refill. | Retry after Retry-After |
| 500 | internal_server_error | Something failed on our side. | Retry with backoff |
Terms of use
The API is covered by our terms of service. You may build our data into your own products, but you may not resell the raw data or rebuild a service like BootLeads. Keep your API key private: do not share it or ship it in software other people use.
Get an API key
The API is included with the Enterprise plan. Your key is on the Account settings page.