API reference

Locomint API

Base URL https://api.locomint.io/v1. JSON in, JSON out, UTF-8 everywhere. One header to authenticate. Every response carries a request ID, and every authenticated response carries your rate-limit headers.

Quickstart

Search for dentists in Lisbon, then fetch any record in full, enrichment included, by the place_id a search returned. The ID below is a restaurant in Dubai:

curl "https://api.locomint.io/v1/places/search?q=dentist&location=Lisbon,%20Portugal&limit=5" \
  -H "X-API-Key: $LOCOMINT_KEY"

curl "https://api.locomint.io/v1/places/ChIJjxN4B_tDXz4RLrBrwzc9Qew" \
  -H "X-API-Key: $LOCOMINT_KEY"

Always add the country to the location. “Warsaw” alone can come back with Warsaw, Indiana mixed in with Warsaw, Poland. “Warsaw, Poland” cannot.

Authentication

Send your key in the X-API-Key header on every request, or as Authorization: Bearer if your HTTP client sends keys that way. Create a free key on the signup page: confirm the address from the link we email you and the key appears on that page with 75 credits a month. Keys look like lm_live_…. The prefix is part of the key, not a plan marker, and older keys beginning lm_free_ keep working unchanged. Store it when you receive it. We keep only a one-way hash of your key, so we cannot show it to you a second time and nobody reading our database can use it; if you lose it, issue a new one from your dashboard and the old one stops working. A missing or unknown key returns 401.

Programmatic signup: POST/v1/signup with { "email", "name", "company" } answers 201 with "api_key": null and emails a confirmation link that lasts 24 hours; the key is issued on the page that link opens, once the address is proved. One account per email; three signups per network per day.

What the Google Maps Scraper API does, in plain language

A keyword and a location in, a page of places with the core fields out.

ParameterTypeNotes
qstring, requiredKeyword or category: dentist, coffee shop, car rental
locationstringFree text, geocoded server-side, in any language or script (東京, Москва, Россия). Write it as "City, Country": a bare city name resolves to whichever city the provider picks, and "Warsaw" alone returned Warsaw, Poland and Warsaw, Indiana in one answer. A location that names no place, such as an emoji or punctuation, is refused with a 400 and costs nothing
lat, lng, radiusnumber, number, int (m)Coordinate search instead of location. Default radius 5000 m
limitint, default 20, max 100Places per page. Credits are reserved for limit and settled to the number actually returned
pageint, default 1Next page of the same query. Consecutive pages overlap slightly, so deduplicate on place_id; deep pages can also drift to a same-named city elsewhere, and those places are returned and charged like any other, so check each place's address.country_code
languageBCP-47, default enNames and categories come back localised

Response: { "data": [PlaceSummary…], "pagination": { "page", "limit", "returned", "total_estimate", "next_page" }, "meta": { "request_id", "credits_used", "latency_ms", "source", "geocoded_location", "location_note" } }. next_page is null on the last page; total_estimate is always null, because the source publishes no total. geocoded_location is the centre of the map view the search resolved to, labelled with the location you sent. location_note is set when the location has no comma - a bare city name - and names the country most of the results came from. A query that names one place, such as Louvre Museum, returns just that place; a query that names an area, such as Soho, returns no businesses and costs nothing. Costs one credit per place returned.

Business Details API and Business Email Finder GET/places/{place_id}

What the Business Details API does, in plain language

What the Business Email Finder API does, in plain language

The full record: everything in the summary plus description, all seven days of opening hours, plus code, timezone, photos, attributes — and enrichment, fetched from the business website on first request and cached per site for 30 days.

ParameterTypeNotes
place_idpathThe place_id from search (ChIJ…). The numeric cid is returned for matching with other tools but is not accepted here
refreshbool, default falseRe-crawl the website now (+1 credit)
languageBCP-47

Every record carries hours_status, which says whether the weekly schedule could be read: ok means all seven days are in hours, not_published means the listing publishes none, and unavailable means the listing does publish hours and we failed to read them. Read the field rather than guessing from an empty hours object.

The enrichment block is what the business publishes on its own website: emails (every address the business publishes on its own website: company mailboxes such as info@ or bookings@ first, including a company address on a sister domain, then addresses that name a member of staff; a named address on another company's domain, such as the web designer credited in the footer, is left out), named_emails (the addresses in emails that name a person, so you can keep company mailboxes only), socials (the business's own profiles, one per network), named_profiles (personal profile links the site publishes for its people, such as linkedin.com/in/…; we return the link and never open it), whatsapp from a click-to-chat link, phones (the numbers the site marks as numbers to call - its tel: links and the telephone in its schema.org markup - in E.164 when the place's country is known, as printed otherwise; the listing's own number stays in phone), contact_form_url, tech_stack, and the site's own title, description and language. website_status says why a record has no contacts (ok, unreachable, parked, redirect_social, no_website) and crawled_at says when the site was read.

Costs one credit; unknown IDs return 404 and cost nothing, and so does a record that comes back unavailable — you are not charged for a schedule we could not read.

Reviews API GET/places/{place_id}/reviews

The first page of reviews the listing shows: usually five to eight, chosen by the listing as the most relevant rather than the most recent, and returned to you newest first. The newest review on the page can be weeks old for a busy place, so this is a sample of what customers say, not a feed of new reviews. Reviewer identity is off by default; pass include_author=true to get the public display name. limit up to 100; five credits per call. Reviews are priced apart from the rest because one call reads about forty times the data a search does. A place that has reviews but returns none is a failed read, not an empty answer: it is retried once and then reported as a 503, free of charge. Only this first page is available — paging further would require defeating a protection measure, which we do not do.

pagination carries returned, next_cursor (always null) and total — the number of reviews the business's own record states. Compare the two: returned: 8, total: 8 is every review there is, and returned: 8, total: 478 is the first page of a busy listing. total is null when the record states no count.

Free Business Count API GET/places/count

What the Free Business Count API does, in plain language

How many places match a query, before you pay for them. Same parameters as search except limit and page. Free: the answer is cached for 24 hours per query and each key may ask 50 fresh questions a day (then 429 until tomorrow).

curl "https://api.locomint.io/v1/places/count" -G \
  --data-urlencode "q=dentist" --data-urlencode "location=Lisbon, Portugal" \
  -H "X-API-Key: $LOCOMINT_KEY"

Response: { "data": { "query", "location", "count", "exact", "at_least", "cached" }, "meta": {…} }. count is the size of the first result page (up to 50). exact: true means the page was not full, so that is the whole answer; at_least: true means 50 came back and a full search will find more.

LLM Grounding API GET/ground/local

What the LLM Grounding API does, in plain language

The same business data as search, shaped for a model's context window instead of a database. Every record is trimmed to the fields an assistant needs to answer a "near me" question, each one carries a citation with the place id and the time it was read, and the response includes a text block you can drop straight into a prompt. Built for agents and for tool calls; the browsable version of the same data is search.

ParameterTypeNotes
qstring, requiredWhat to look for: dentist, coffee shop
locationstringAlways "City, Country". Or use lat, lng and radius
limitint, default 5, max 10Kept small on purpose: context windows are the constraint
detailbool, default falseAlso read each result's full record and its website: emails, named_emails, whatsapp, socials, named_profiles and contact_form_url. Without it those fields are null rather than empty, because nothing was looked up. The record keeps hours_today either way; it carries no weekly schedule. Two credits each instead of one
languageBCP-47, default en
curl "https://api.locomint.io/v1/ground/local" -G \
  --data-urlencode "q=dentist" --data-urlencode "location=Lisbon, Portugal" \
  --data-urlencode "limit=3" -H "X-API-Key: $LOCOMINT_KEY"

Response: { "data": [GroundedPlace…], "text", "query", "location", "meta" }. Each GroundedPlace carries name, up to two categories, address, city, country_code, phone, website, rating, review_count, status, open_now, hours_today, plus emails, named_emails, whatsapp, socials, named_profiles and contact_form_url, which are null until detail=true looks the website up — null means "not checked", an empty list would mean "checked and found none". The citation on every record is { place_id, source, fetched_at }, so an answer built from this data can be traced.

Why a text block as well as JSON. Passing raw JSON into a prompt wastes tokens on punctuation and field names. The text field is one line per business with the same facts, which is what most agents end up building by hand anyway.

Costs one credit per business returned, two with detail=true. Credits are reserved for limit and settled to the number actually returned, so a thin result costs less.

AI assistant tools (MCP) POST/mcp

What the MCP Server does, in plain language

Locomint speaks the Model Context Protocol, so an AI assistant can call it directly as a set of tools instead of you writing glue code. It is a hosted server: nothing to install, no package to keep updated. Authenticate with the same API key you already have, sent as a bearer token.

Add it to Claude Code with one command:

claude mcp add --transport http locomint https://api.locomint.io/mcp \
  --header "Authorization: Bearer $LOCOMINT_KEY"

Or paste this into any client that takes a config file:

{
  "mcpServers": {
    "locomint": {
      "type": "http",
      "url": "https://api.locomint.io/mcp",
      "headers": { "Authorization": "Bearer ${LOCOMINT_API_KEY}" }
    }
  }
}

Nine tools, each calling the REST endpoint that does the same job:

ToolWhat it doesCredits
local_searchBusinesses for a category in a city. Answers with the compact text block by default, which is what you quote; pass output="json" for the full records and citations1 each, 2 with detail
count_localHow many businesses match, before spending anythingfree
place_detailsOne full record: hours for all seven days, contacts, technologies1
company_by_domainA company record from its domain: what the site says it is, role mailboxes, socials, WhatsApp, technologies1
read_pageAny public page as clean markdown or text, with contacts found on it1
verify_emailsWhether addresses can receive mail, without sending anything1 per address that gets an answer
crawl_accessWhat a site's robots.txt allows each named AI agent to do, with the deciding line quoted1
check_schemaThe schema.org types a page declares and the required fields it is missing1
account_usageCredits used and left this monthfree

The same limits apply. Tools run through the same key, credits, quota and rate limit as the REST API, and a refusal comes back to the model in words it can act on, so it retries sensibly instead of failing the conversation.

That is one of two ways to reach the same engine from an assistant. The other is your own Apify account: Apify runs an MCP gateway at https://mcp.apify.com and any actor in its Store can be loaded into it as a tool, so naming the Locomint listings in its tools parameter makes them callable with no Locomint key, billed by Apify per result. The MCP Server page carries that config block and the price of each listing.

Email Verification API POST/emails/verify

What the Email Verification API does, in plain language

Whether each address can receive mail, checked without sending a message. Up to 100 addresses per call; the same address written twice, in any capitalisation, is checked and charged once and answered with one row, under the spelling you sent first.

curl -X POST "https://api.locomint.io/v1/emails/verify" \
  -H "X-API-Key: $LOCOMINT_KEY" -H "Content-Type: application/json" \
  -d '{"emails": ["info@locomint.io", "sales@example.com"]}'

Every distinct address you send gets a row, in the order you sent them. Each carries status (deliverable, undeliverable, invalid, disposable, risky for catch-all domains, unknown when the mail server would not answer, or valid when smtp_check is false or the mailbox could not be asked and only the domain was checked), a reason, a 0–100 score, and every check that ran: syntax_valid, mx_found, mx_host, disposable, disposable_service (the throwaway service, when we recognise its mail server), free_provider, role_account, catch_all, smtp_checked, smtp_code. Set "smtp_check": false for domain-level checks only.

One credit per address that got an answer, and invalid, disposable and catch-all rows are answers. A row whose mail server did not answer, or that the call ran out of time for, comes back unknown and costs nothing. The call gives itself 90 seconds and answers 504 only when no address answered at all.

Website Content Extraction API POST/websites/extract

What the Website Content Extraction API does, in plain language

Readable text plus contact details for up to 20 page URLs per call. One credit per page read. A URL we could not reach, one that answered with an error or a challenge, one our safety guard refused (a private or non-web address, status: refused), one whose path the site’s robots.txt disallows and a file that is not a web page (a PDF or an image) are each marked in status and cost nothing. A site whose owner asked to be left out comes back removed, unread and free. A page with little text, usually one built by JavaScript, comes back thin and is a credit, because it was read; so is a parked domain. A bare domain such as example.org is read as https://example.org/; anything that is not an http or https address with a real host name is a 400 for the whole call.

curl -X POST "https://api.locomint.io/v1/websites/extract" \
  -H "X-API-Key: $LOCOMINT_KEY" -H "Content-Type: application/json" \
  -d '{"urls": ["https://www.python.org/about/"], "output": "markdown", "max_chars": 20000}'

Each row: status (ok, thin for client-side rendered pages, parked, http_error, not_html, unreachable, blocked, refused, robots_disallowed, removed), title, description, language, headings, content, word_count, and with include_contacts (default true) the emails, named_emails, socials, named_profiles, whatsapp, phones, contact_form_url and tech_stack. Pass "include_links": true for up to 500 outgoing links per page.

Every URL gets its own deadline, so one site that accepts a connection and then goes quiet cannot cost you the rest of the batch: it comes back unreachable with a note and the others answer normally. You get one row per distinct URL you sent, in the same order (a URL sent twice is read once), and a URL we never reached is not charged for.

We read each site's robots.txt before fetching it and honour what it says. A path the site disallows comes back as robots_disallowed with the rule quoted in note, and costs nothing. This is the same reading the AI Crawler Checker does, applied to ourselves.

Company Enrichment API POST/domains/enrich

What the Company Enrichment API does, in plain language

A company record from a domain name. We read the homepage, and the contact page when the homepage gave no way in, and return what the business publishes about itself. Every address the site publishes comes back, company mailboxes first; the ones that name a member of staff are repeated in named_emails, and personal profile links are in named_profiles. Cached per host for thirty days and read from a datacenter address, with no residential proxy traffic.

Technology detection matches each page against an open fingerprint set maintained by the enthec/webappanalyzer project, used under the GPL-3.0 and pinned to a specific commit. Locomint runs those fingerprints on its own servers and returns the results; the fingerprint data itself is not redistributed.

curl -X POST "https://api.locomint.io/v1/domains/enrich" \
  -H "X-API-Key: $LOCOMINT_KEY" -H "Content-Type: application/json" \
  -d '{"domains": ["python.org", "https://www.djangoproject.com/foundation/"]}'
FieldMeaning
domain, websiteNormalised host; www., paths and schemes are all accepted on the way in
statusok, unreachable, parked, redirect_social or no_website
title, description, languageWhat the site says it is
emails, named_emails, socials, named_profiles, whatsapp, phones, contact_form_urlHow to reach them. named_emails and named_profiles are the ones that name a person. phones are the numbers the site links to call or puts in its schema.org markup, in E.164 when the site's country domain says where it is (.pt, .co.uk) and as printed otherwise
tech_stackThe technologies the site runs, by slug: CMS, ecommerce, analytics, chat widgets, payment, hosting
technologies[]The same findings with name, categories and, where the page gives it away, version
crawled_atWhen the site was last read, or last tried: an unreachable row carries the time of the failed attempt. Pass "refresh": true to read it now
noteOnly on a row the call ran out of time for, which comes back unreachable and free

Up to 20 domains per call. Each is normalised before it is counted, so acme.example, www.acme.example and https://acme.example/about are one domain, charged one credit. The site is read at the bare domain, or at www. when the bare domain does not answer. The call reads five sites at a time within 90 seconds; a site it ran out of time for comes back marked and free, and the call answers 504 only when no site answered at all. GET/domains/{domain} returns the same record for a single domain.

Tech Stack Checker API POST/tech/detect

What the Tech Stack Checker API does, in plain language

What a website is built with, for up to 50 URLs in one call. Each URL is fetched once from a datacenter address and matched against an open fingerprint set of 7,610 technologies in 107 categories: content management systems, ecommerce platforms, analytics, tag managers, JavaScript frameworks, payment processors, chat widgets, web servers, CDNs, and WordPress plugins and themes. 5,834 of them can be recognised from what a server sends, and that is what this endpoint reads — the HTML, the response headers, the cookies the server sets, the meta tags and the script URLs on the page. No browser is started and no JavaScript is executed.

Technology detection matches each page against an open fingerprint set maintained by the enthec/webappanalyzer project, used under the GPL-3.0 and pinned to a specific commit. Locomint runs those fingerprints on its own servers and returns the results; the fingerprint data itself is not redistributed.

curl -X POST "https://api.locomint.io/v1/tech/detect" \
  -H "X-API-Key: $LOCOMINT_KEY" -H "Content-Type: application/json" \
  -d '{"urls": ["techcrunch.com", "https://www.allbirds.com/"], "versions": true}'
ParameterTypeNotes
urlsarray, required1 to 50 entries. A bare domain or a full URL to any page: acme.com and https://acme.com/pricing are both accepted, and the second reads that page rather than the homepage
versionsbooleanFill version wherever the page states one. On by default; set it to false for names and categories only
categoriesarrayKeep only technologies in these categories, spelled as the list below spells them. Omit it to get everything found. Free: a page with nothing in them comes back as filtered and costs no credit
{
  "data": [
    {
      "url": "https://techcrunch.com/",
      "final_url": "https://techcrunch.com/",
      "status": "ok",
      "http_status": 200,
      "technologies": [
        { "name": "WordPress", "categories": ["CMS", "Blogs"], "version": "6.9.7" },
        { "name": "PHP", "categories": ["Programming languages"], "version": null },
        { "name": "MySQL", "categories": ["Databases"], "version": null },
        { "name": "Nginx", "categories": ["Web servers", "Reverse proxies"], "version": null },
        { "name": "Yoast SEO", "categories": ["SEO", "WordPress plugins"], "version": "25.1" },
        { "name": "Google Tag Manager", "categories": ["Tag managers"], "version": null }
      ],
      "tech_names": ["WordPress", "PHP", "MySQL", "Nginx", "WordPress VIP", "Yoast SEO", "…"],
      "tech_slugs": ["wordpress", "php", "mysql", "nginx", "wordpress-vip", "yoast-seo", "…"],
      "tech_count": 11,
      "categories": ["Blogs", "CMS", "Databases", "Email", "Marketing automation", "PaaS",
                     "Personalisation", "Programming languages", "Reverse proxies", "SEO",
                     "Security", "Tag managers", "Web servers", "WordPress plugins"],
      "scan_time_ms": 885
    }
  ],
  "meta": { "request_id": "req_0d4a17c9b2e5", "credits_used": 2, "latency_ms": 1904, "source": "websites" }
}

Every row carries url, final_url, status, http_status, technologies[], tech_names[], tech_slugs[], tech_count, categories[] and scan_time_ms. tech_names holds the proper names a person reads in a spreadsheet and tech_slugs the same findings in the form the tech_stack field uses elsewhere in Locomint, so a batch can be joined against records you already hold. You get one row per distinct URL you sent, in the order you sent them; a page given twice, or given as a bare domain and a full URL, is read and charged once. A technology that another one implies is returned in its own right, which is why WordPress brings PHP and MySQL with it on a page that names neither, and version is null unless the site printed a version somewhere we can read it.

One credit per URL actually read and answered. A URL we could not reach, one our own safety guard refused (a private or non-web address), one whose path the site’s robots.txt disallows, one that was not a web page at all (a PDF or an image) and one that ran out of time are each marked in status and cost nothing — you are charged for pages that were read, not for URLs you sent. So is a page whose every finding your own categories filter removed: it comes back as filtered, so a list of fifty still lines up against your sheet, and it costs no credit. URLs must be http or https with a real host name; anything else is a 400 for the whole call and nothing is charged. A site that answers with a challenge or a rate limit ends that row there and is never retried through a different address.

The 107 categories, spelled as the fingerprint set spells them: A/B Testing, Accessibility, Accounting, Advertising, Affiliate programs, Analytics, Appointment scheduling, Artificial Intelligence, Augmented reality, Authentication, Blogs, Browser fingerprinting, Buy now pay later, CDN, CI, CMS, CRM, Caching, Cart abandonment, Comment systems, Containers, Content curation, Control systems, Cookie compliance, Cross border ecommerce, Cryptominers, Customer data platform, DMS, Database managers, Databases, Development, Digital asset management, Documentation, Domain parking, Ecommerce, Ecommerce frontends, Editors, Email, Feature management, Feed readers, Font scripts, Form builders, Fulfilment, Fundraising & donations, Geolocation, Hosting, Hosting panels, IaaS, Issue trackers, JavaScript frameworks, JavaScript graphics, JavaScript libraries, LMS, Live chat, Livestreaming, Load balancers, Loyalty & rewards, Maps, Marketing automation, Media servers, Message boards, Miscellaneous, Mobile frameworks, Network devices, Network storage, Operating systems, PaaS, Page builders, Payment processors, Performance, Personalisation, Photo galleries, Programming languages, RUM, Recruitment & staffing, Referral marketing, Remote access, Reservations & delivery, Retargeting, Returns, Reverse proxies, Reviews, Rich text editors, SEO, SSL/TLS certificate authorities, Search engines, Security, Segmentation, Shipping carriers, Shopify apps, Shopify themes, Static site generator, Surveys, Tag managers, Ticket booking, Translation, UI frameworks, User onboarding, Video players, Web frameworks, Web server extensions, Web servers, Webmail, Widgets, Wikis, WordPress plugins, WordPress themes.

What it will not see. Anything a site reveals only once JavaScript has run. About a quarter of the fingerprint set is written that way — it looks for a variable a script creates in the browser — and we do not run a browser, so those are not claimed. A single-page app that renders everything client-side returns less than a server-rendered site of the same size.

Status & usage

GET/status — public, no key. The daily canary result: status (operational / degraded / major_outage / unknown), success_rate, latencies, checked_at, and age_hours saying how old the reading is. If the checks themselves stop running, the status becomes unknown with stale: true rather than repeating the last good result for ever — a status page that cannot go stale silently is the only kind worth reading. Shown live at /status.

GET/usage — credits used and remaining in the current billing month for your key. Free, does not count as usage.

AI Crawler Checker GET/domains/{domain}/access

What the AI Crawler Checker does, in plain language

What a site allows an automated visitor to do, before you fetch anything. Reads only the site's own public files: robots.txt, llms.txt, ai.txt and the homepage. Every verdict quotes the line that decided it, or says that no line covered the path, so you can show why you fetched, or why you did not.

curl "https://api.locomint.io/v1/domains/nytimes.com/access?agent=GPTBot" \
  -H "X-API-Key: $LOCOMINT_KEY"
FieldMeaning
agents[]One verdict per named AI agent: allowed, the rule that decided it, whether the site named that agent explicitly or fell back to *, and any crawl_delay
blocked_agents, blocks_any_ai_agent, ai_training_allowedThe short answer
robots, llms_txt, ai_txtWhether each file exists, its status and size in bytes. robots.truncated is true only if the file was too large to read in full — the whole file is read, which matters because sites such as figma.com put the group that names AI agents at the end of a 380 KB file
path, agent_requestedThe question you asked, echoed back, so the answer can be filed as a dated record
warnings[]Empty in the normal case. Non-empty when something could make a verdict here incomplete, such as a rules file we could not read
sitemaps, content_signalsSitemaps declared in robots.txt, and any content-signal line
pay_per_crawlTrue when the site answers 402 Payment Required
text_without_javascript, javascript_dependentWords of readable text in the raw HTML, and whether the page needs scripts to say anything: true when the homepage carries scripts and shows almost no text without them, null when the homepage could not be read

Pass agent=ClaudeBot to check one agent, and path=/pricing to ask about a path rather than the site root; a missing leading slash is added, and a full URL is reduced to its path. When no rule covers the path the verdict is allowed and rule is empty; matched_by then names the group of rules that applied, or says no matching group when the site names neither the agent nor *. One credit. A site whose robots.txt we could not read answers allowed: null for every agent, never a guess, and costs nothing.

Why this exists. Crawling rules are now written per AI agent and change often, and the answer matters legally, not just technically. This endpoint gives you a dated, quotable record of what a site said on the day you fetched it.

Schema Validator API POST/schema/validate

What the Schema Validator API does, in plain language

Read and check the schema.org markup on a page: every typed object it declares, and the required fields that are missing for the types search engines document rich results for. Google retired the API behind its own testing tool and the public validator is rate limited, so this fills a gap for anyone who wants the check in a build pipeline.

curl -X POST "https://api.locomint.io/v1/schema/validate" \
  -H "X-API-Key: $LOCOMINT_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://www.python.org/"}'

Send exactly one of url, html or jsonld. jsonld takes a JSON-encoded string, a single object, or an array of objects — all three are read the same way, so you can pass the document straight out of your code. Checking a url costs one credit; checking html or a jsonld document you already hold is free.

Response: { "data": { "blocks", "types", "items", "errors", "valid", "checked", "checked_types", "unchecked_types", "source_url" }, "meta" }. Each item names its type, the ruleset it was checked_against (a Dentist is judged as a LocalBusiness, a Corporation as an Organization), and the missing_required and missing_recommended fields. openingHoursSpecification and the older openingHours string each satisfy the hours recommendation.

A type we hold no rules for is reported as unchecked, not as a failure: those items come back "checked": false and "valid": null, and unchecked_types lists every type that was skipped. When nothing on the page matched a ruleset, the top-level valid is null rather than true — a page we could not judge never reads as a clean pass, so treat valid as three-valued: true, false, or null for "no opinion".

POST/schema/generate writes the markup instead of judging it. Pass a place_id and it builds LocalBusiness markup from the real record, including opening hours, coordinates, rating and the social profiles from the website, and picks the closest type: a dentist becomes Dentist, a cafe becomes CafeOrCoffeeShop. Pass a url instead to build Organization markup from what a page says about itself. The answer has four keys at the top level: data, the JSON-LD document; script, the same document already wrapped in a <script type="application/ld+json"> tag and escaped so it is safe to paste into a page; validation, the validator run over exactly that document; and meta. check is kept as an alias of validation for older integrations. Fields the record does not carry are omitted rather than invented — a place with no postal code gets an address without postalCode, and validation reports it under missing_recommended. One credit.

An honest limit. We report conformance with the documented requirements. Whether a search engine grants a rich result is its decision, and no API can promise it.

Career Site Jobs API GET/jobs/board

What the Career Site Jobs API does, in plain language

The roles a company is advertising, read from the job board feed its own careers page is built on. Six applicant tracking systems are covered: greenhouse, lever, ashby, workable, recruitee and personio. All six publish these feeds for any visitor to read, and Greenhouse documents its job board API as needing no authentication, so there is no account, no session and no aggregator in between. You ask for one employer at a time; there is no pool of postings to search across companies.

A board is named by two values. The provider is the system it runs on, and the board is the token in the careers URL that names the employer: boards.greenhouse.io/acme, jobs.lever.co/acme, jobs.ashbyhq.com/Acme, acme.workable.com, acme.recruitee.com and acme.jobs.personio.de all give acme. When all you have is a link to a careers page, the detect endpoint below works both of them out for you.

What this does not cover. Indeed, LinkedIn, Glassdoor and ZipRecruiter are not in here and will not be. Those sites refuse automated requests, and we do not route around a refusal. What you get instead is the opening as the employer published it, from the same feed the aggregators are themselves reading.

curl "https://api.locomint.io/v1/jobs/board?provider=ashby&board=Ramp&limit=3&descriptions=false" \
  -H "X-API-Key: $LOCOMINT_KEY"
ParameterTypeNotes
providerstring, requiredOne of greenhouse, lever, ashby, workable, recruitee, personio
boardstring, requiredThe employer's board token, taken from the careers URL as above. A board that does not exist answers 404 and costs nothing
limitintegerJobs per page, 1 to 100, default 20
pageinteger1-based. pagination.next_page is null on the last page
descriptionsbooleanFill description with the whole posting text. On by default; set it to false for a far smaller answer at the same price

Every job carries job_id, title, company, department, location, locations[], remote, employment_type, description, apply_url, posted_at, updated_at, compensation, provider, board, source_url and fetched_at. A field the employer does not publish comes back null rather than guessed: compensation is null unless the board carries a structured range, and updated_at is null on feeds that record only a publication date.

{
  "data": [
    {
      "job_id": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
      "title": "Security Engineer, Cloud",
      "company": "Ramp",
      "department": "Engineering",
      "location": "New York, NY (HQ)",
      "locations": ["New York, NY (HQ)", "Remote (US)", "Remote (Canada)", "Miami, FL"],
      "remote": true,
      "employment_type": "Full-time",
      "description": null,
      "apply_url": "https://jobs.ashbyhq.com/Ramp/34413f8d-.../application",
      "posted_at": "2026-04-07T17:12:35.753000+00:00",
      "updated_at": null,
      "compensation": { "min": 211400, "max": 290600, "currency": "USD", "interval": "year" },
      "provider": "ashby",
      "board": "Ramp",
      "source_url": "https://api.ashbyhq.com/posting-api/job-board/Ramp?includeCompensation=true",
      "fetched_at": "2026-09-12T09:41:06Z"
    }
  ],
  "pagination": { "page": 1, "limit": 3, "returned": 3, "total": 145, "next_page": 2 }
}

GET/jobs/detect?url=… turns a careers page into the two values above. Give it any URL a company uses for hiring and it answers with the provider and board to ask for, how many roles are open on it, and both the board_url a person can open and the feed_url we read. A page with no feed behind it says so and costs nothing.

curl "https://api.locomint.io/v1/jobs/detect?url=https://linear.app/careers" \
  -H "X-API-Key: $LOCOMINT_KEY"

One credit per job returned, settled to the number the page actually contained, so a board with three openings costs three credits however high you set limit. Detection costs one credit when a feed is found and nothing when there is none. Feeds are read live on every call and fetched from a datacenter address; nothing is served from a stored index of postings, and a feed that answers with a rate limit or a challenge ends the request rather than being retried through another address.

Google Ads Transparency API GET/ads/search

What the Google Ads Transparency API does, in plain language

The ads a business runs on Google Search, YouTube, Shopping, Maps and Play, from the public Ads Transparency Center: who ran each one, whether it is a text, image or video ad, the day it was first and last shown, how many days it ran, and a link to the ad itself. It is read the way the Transparency Center's own pages read it, logged out, with no account and no key of anybody else's.

q says whose ads. A website such as nike.com returns every advertiser that promotes it, and it is the way to get a brand: for nike.com that is Nike, Inc., its Japanese company and the resellers, which no single advertiser would show. An advertiser ID (AR and digits) or a Transparency Center link is used as given. An advertiser's name has to match exactly, ignoring case and punctuation, because the Transparency Center matches loosely: its first answer for "nike" is a one-ad account in Kenya called "Nike". A name that matches nobody returns no ads, costs nothing, and lists in suggestions the advertisers whose names start that way, so you can ask again by ID. Where several accounts share the exact name, all of them are searched.

curl "https://api.locomint.io/v1/ads/search?q=nike.com&region=US&format=text&limit=5" \
  -H "X-API-Key: $LOCOMINT_KEY"
ParameterTypeNotes
qstring, requiredA website, an advertiser ID, a Transparency Center link, or an advertiser's exact name
regionstringOnly ads shown in this country: a two-letter ISO code such as US. Default: anywhere
platformstringsearch, youtube, shopping, maps or play
formatstringtext, image or video
start_date, end_datedateOnly ads shown within these days, YYYY-MM-DD, both inclusive
limitintegerAds per page, 1 to 100, default 40
cursorstringpagination.next_cursor from the previous page. It is null on the last page

Every ad carries creative_id, advertiser_id, advertiser_name, format, first_shown, last_shown, days_shown, domain (the website it promotes, on a website search), image_url with image_width and image_height where the listing carries a static image, preview_url (the script the Transparency Center draws the ad with) and ad_url, the ad's own page. The words of a text or video ad are not decoded: they sit in a renderer of about 170 KB per ad, and ad_url shows them. searched says how q was read, and pagination.total_min and total_max are the Transparency Center's own count of everything that matches, on every page.

{
  "data": [
    {
      "creative_id": "CR05867015907196272641",
      "advertiser_id": "AR16735076323512287233",
      "advertiser_name": "Nike, Inc.",
      "format": "text",
      "first_shown": "2026-05-05T16:30:04Z",
      "last_shown": "2026-09-30T04:32:54Z",
      "days_shown": 120,
      "domain": "nike.com",
      "image_url": null,
      "image_width": null,
      "image_height": null,
      "preview_url": "https://displayads-formats.googleusercontent.com/ads/preview/content.js?...",
      "ad_url": "https://adstransparency.google.com/advertiser/AR16735076323512287233/creative/CR05867015907196272641?region=US"
    }
  ],
  "searched": { "kind": "domain", "value": "nike.com", "advertisers": [] },
  "suggestions": [],
  "pagination": { "limit": 5, "returned": 5, "total_min": 700, "total_max": 800, "next_cursor": "CgoAP7zmjZ7PZA..." }
}

One credit per ad returned, settled to the number the page actually held, so a search with three ads costs three credits however high you set limit. A name that matches no advertiser costs nothing. The Transparency Center is read live on every call, through the same residential network our map search uses; if it answers with a CAPTCHA page the request ends with 503 and it is never retried through another address. We then send it nothing for about fifteen minutes, and Retry-After says how many seconds of that are left.

The Place record

FieldDescription
place_idStable identifier, shared with official sources. Use it to diff runs.
name, categories[]As listed, localised by language
addressfull, street, city, region, postal_code, country_code (ISO 3166-1)
locationlat, lng
phone, phone_e164, websitephone as displayed locally, phone_e164 normalised (+351…); website as listed
rating, review_countThe listing's star rating and how many reviews it has, the number the listing prints as "918 reviews". null when the listing states no count
price_range, price_levelprice_range is the price band the listing prints, as printed ("$10–20", "AED 100–350"); price_level is 1–4 for the older $$ form and rarely set now
statusopen, temporarily_closed, permanently_closed
hoursPer weekday, list of { open, close } in HH:MM; 24:00 allowed
service_area_businessTrue when the business has no storefront address
description, plus_code, timezone, photo_urls[], photos_count, attributes{}Details only. photos_count is how many photos the listing has; photo_urls are the first few. attributes is a map of attribute name to true/false
enrichmentemails[] (every address the business publishes, company mailboxes first), named_emails[] (the ones that name a person), socials{} (the business's own profiles), named_profiles[] (its people's profile links), whatsapp, phones[] (numbers the site links to call), contact_form_url, tech_stack[], website_status, title, description, language, crawled_at
source, fetched_atWhich data path answered, and when

Errors

Every error is { "error": { "code", "message", "request_id" } }.

StatusCodeMeaning
400, 405invalid_requestMissing or invalid parameter or body; 405 when the method is wrong for the endpoint
401unauthorizedMissing or unknown API key
402quota_exceededThe request would exceed your monthly quota. Nothing charged. More volume runs on Apify: see the pricing page
404not_foundUnknown place ID, endpoint or job board, or no job feed behind a careers page. Nothing charged
409email_takenSignup: this email already has a key
429rate_limited, signup_limitedToo many requests this minute (honour Retry-After), the daily count cap, or too many signups from one address
504timeoutA verify or enrich call in which nothing answered within 90 seconds, or a crawl-rule check whose site did not answer in time. Nothing charged; send fewer items
503source_unavailableThe data path is degraded, or a source asked us to slow down and we are pausing requests to it; retry after Retry-After, which gives the real wait. Check /status.

Limits & credits

  • Rate limit: 60 requests per minute on a free key. Every authenticated request counts, including ones that fail validation. Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
  • Quota: the request’s worst-case cost is reserved before any work and settled to the real cost afterwards; a request that would exceed the quota is refused with 402 and costs nothing. GET/usage returns { "data": { "plan", "period", "resets_at", "monthly_quota", "credits_used", "credits_reserved", "credits_remaining", "rate_per_minute" }, "meta" } for your key, free. credits_used is what has settled; credits_reserved is what is held for requests still running, and is already subtracted from credits_remaining, so the two never double-count. At the free plan's 75 credits a month the API answers 402 until the reset date; more volume runs on Apify, and the pricing page compares the two.
  • Credits: 1 per place in search, 1 per details call, +1 for refresh=true, 5 per reviews call, 1 per email address that gets an answer, 1 per page extracted, 1 per domain enriched, 1 per URL read for its tech stack, 1 per domain crawl-rule check, 1 per markup check of a URL, 1 per job returned, 1 per careers page with a feed behind it, 1 per ad returned. count, usage and status are free.
  • Request ID: every response carries X-Request-Id. Quote it when you write to us.

Run the same engine on Apify

The hosted API's free plan is for trying things and for small jobs. Past it, the same engine runs as pay-per-result listings on the Apify Store: Apify bills you per result, there is no subscription, nothing is paid to us directly, and Apify's own free plan includes $5 of usage a month with no card. Start a listing from the Apify console, on a schedule, or with one call:

curl -X POST "https://api.apify.com/v2/acts/locomint~google-maps-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"searchStringsArray": ["dentist"], "locationQuery": "Lisbon, Portugal", "maxCrawledPlacesPerSearch": 50, "includeDetails": true, "includeEnrichment": true}'

The answer is the run's dataset as a JSON array, one item per place, in the same record layout as the API. Apify holds a synchronous call open for up to five minutes; for a bigger job, start the run with POST https://api.apify.com/v2/acts/locomint~google-maps-scraper/runs and read its dataset when it finishes. The token also works as an Authorization: Bearer header, which keeps it out of shell history and server logs.

Input fieldWhat it does
searchStringsArrayWhat to look for: one or more categories or business names, each searched separately, up to 25 per run
locationQueryCity, area or address. Write it as "City, Country"
maxCrawledPlacesPerSearchPlaces to collect per search term, 1 to 500, default 20. A large number sweeps the whole city on a grid
placeIdsPlace IDs (ChIJ…) to fetch in full instead of searching, up to 1,000 per run
includeDetails, includeEnrichmentThe full record with weekly hours, and the website contacts. Both default to true and are included in the price
placeMinimumStars, websiteFilter, skipClosedPlaces, categoryFilterWordsFilters applied before anything else is fetched, so a place that fails one is not charged
languageLanguage for names and categories, default en

Each listing bills per result at its own price. The pricing page sets them beside the free plan.

ListingActorPrice
Google Maps Scraper & Email Extractorlocomint~google-maps-scraper$3.00 per 1,000 places
Google Maps Scraper – Multi-City Lead Listslocomint~google-maps-multi-city-scraper$3.00 per 1,000 leads
Google Maps Place Details Scraperlocomint~google-maps-place-details-scraper$2.50 per 1,000 records
Website Email Scraper – Contact Details & Socialslocomint~website-content-contact-extractor$1.50 per 1,000 pages
Website Content Crawlerlocomint~website-crawler-content-contacts$1.50 per 1,000 pages
Bulk Email Verifier & Validatorlocomint~bulk-email-verifier$0.90 per 1,000 addresses
Company Enrichment API – Domain to Emails & Techlocomint~company-enrichment$5.00 per 1,000 domains
AI Crawler Checkerlocomint~ai-crawler-checker$3.00 per 1,000 domains
Schema Markup Validator & Generatorlocomint~schema-markup-validator$0.50 per 1,000 URLs
Career Site Job Scraperlocomint~career-site-jobs-scraper$6.00 per 1,000 jobs
Tech Stack Detectorlocomint~tech-stack-detector$10.00 per 1,000 pages
Google Maps Email Extractorlocomint~google-maps-email-extractor$5.00 per 1,000 leads
Google Ads Transparency Scraperlocomint~google-ads-transparency-scraper$1.00 per 1,000 ads

Removal requests

Any business can ask to be removed from our results via the removal form or POST/removal-requests (public, no key) with { "business_name", "contact_email", "place_id"?, "address"?, "website"?, "reason"? }; the answer is 202 with a ticket id. Removals take effect within 7 days and are honoured across the API and the actors.