New to the API? See the plans and how records are counted, or get your API key in about a minute.

etailinsights API v1

Read-only access to the etailinsights database of eCommerce companies, websites and contacts. Base URL: https://mcp.etailinsights.com. Every path below is relative to it. Reference revision 2026-11 (records metering).

Authentication

Send your API key in the X-ETI-API-Key header on every request except GET /v1/health. Keys look like eti_ followed by 32 lowercase hex characters. A missing, malformed, unknown or revoked key returns 401 unauthorized; the body never says which. The MCP endpoint, POST /mcp, takes the same key as Authorization: Bearer instead — and only there: a REST route ignores an Authorization header (see MCP and connectors).

curl -H "X-ETI-API-Key: eti_0123456789abcdef0123456789abcdef" \
     "https://mcp.etailinsights.com/v1/companies/by-domain?domain=glossier.com"

Response envelope

Every response — success or error — has the same shape. Exactly one of data and error is non-null.

{
  "data":  { ... },                       // the record(s), or null on error
  "error": null,                          // or {"code": "...", "message": "..."}
  "meta":  {
    "request_id": "6f1c0e2a-…",           // server-generated; quote it when reporting an issue
    "latency_ms": 41,                     // server-side processing time
    "records_returned": 1                 // records this response counted against your allotment (0 on errors)
  }
}

error.code is stable and machine-readable; error.message is for people and may change. Timestamps are UTC ISO-8601 with a Z suffix.

Endpoints

GET /v1/health

No authentication. Returns 200 with {"status": "ok", "checks": {"mysql": "ok", "dynamodb": "ok"}, "version": "…"}, or 503 with "status": "degraded" when a dependency check fails. Not wrapped in the envelope.

GET /robots.txt

No authentication. Plain text that asks crawlers to stay off this host, apart from the paths MCP clients call before signing in. Not wrapped in the envelope, not counted.

GET /v1/companies/by-domain

Look up one company by its web domain. Returns a CompanyRecord. One record.

Query parameterTypeNotes
domainstring, required3–253 characters. Protocol, www. and a trailing slash are stripped; matching is case-insensitive. https://www.glossier.com/ and glossier.com are the same lookup.

Errors: 400 invalid_request (missing or malformed domain), 404 not_found.

GET /v1/companies/{company_id}

Look up one company by its public ID. Returns a CompanyRecord. One record.

Path parameterTypeNotes
company_idstring, requiredPublic company identifier, eti_co_{uuid}, as returned by every other endpoint. Any other prefix is 400 invalid_request.

Errors: 400 invalid_request, 404 not_found.

GET /v1/companies/{company_id}/contacts

The company's published contacts, C-suite first, paged. Each contact returned counts as one record (see Records and limits).

ParameterTypeNotes
company_idpath, requiredeti_co_{uuid}.
limitquery, integerPage size requested, 1–50; default 10. The page you receive may be shorter when it would cross your monthly allotment — limit in the response is the size actually served.
offsetquery, integerZero-based; default 0. Use next_offset from the previous page.
seniorityquery, enumOne of c_suite, director, manager, other. Any other value is 400.
{
  "data": {
    "company_id": "eti_co_abc12345-1111-2222-3333-444455556666",
    "contacts": [ { ...ContactRecord... }, ... ],
    "total": 14,            // contacts matching the filter
    "offset": 0,
    "limit": 10,            // the page size served
    "returned": 10,         // len(contacts) — the records this response counted
    "has_more": true,
    "next_offset": 10       // present only when has_more
  },
  "error": null,
  "meta": { "request_id": "…", "latency_ms": 63, "records_returned": 10 }
}

Errors: 400 invalid_request, 403 regulatory_restriction (the company is in a GDPR/CASL-restricted country; contacts are withheld, the company itself is still readable), 404 not_found. A company with no matching contacts is 200 with an empty list, not 404.

GET /v1/contacts/by-email

Look up one contact by email address. Returns the ContactRecord plus a CompanySummary of their company. One record (the summary is part of it).

Query parameterTypeNotes
emailstring, requiredSyntactically validated, then lowercased before the lookup. A malformed address is 400 invalid_request.
{ "data": { "contact": { ...ContactRecord... }, "company": { ...CompanySummary... } }, "error": null, "meta": { ... } }

Errors: 400 invalid_request, 403 regulatory_restriction, 404 not_found.

GET /v1/website-info/by-domain

Website metadata and the technology platforms detected on it. Returns a WebsiteInfoRecord. One record (the platforms list is part of it).

Query parameterTypeNotes
domainstring, required3–253 characters. Protocol, www. and a trailing slash are stripped; other subdomains are kept (shop.example.com queries as-is).

Errors: 400 invalid_request, 404 not_found.

Application surface — POST /v1/search/websites, POST /v1/search/companies, POST /v1/search/executives

Criteria search is reserved for the etailinsights application. These paths are not part of the customer API: a key without the search:advanced scope receives 404 not_found, exactly as for a path that does not exist. Search results are not records and never count against an allotment.

Data models

All fields are present in every response; a value that is unknown is null (or an empty list for list fields). No field is hidden by plan — one call returns the full record.

CompanyRecord

FieldTypeNotes
idstringPublic identifier, eti_co_{uuid}. Stable; use it for /v1/companies/{company_id}.
namestringCanonical company name.
domainstring | nullPrimary web domain, bare (no protocol or www.).
business_typestring | nulleCommerce model, e.g. eCommerce Pure Play, Brick and Mortar.
descriptionstring | null
profile_typeenumCertified (researched by etailinsights) or Community (auto-generated).
revenue_yearintegerThe year revenue_current refers to; identical across all records in a response.
revenue_currentinteger | nullEstimated online revenue for revenue_year, USD.
revenue_priorinteger | nullEstimated online revenue for revenue_year − 1, USD.
revenue_historyobjectYear (string) → estimated online revenue, USD, for 2020–2024. Years without data are omitted.
employee_rangestring | nullBanded, e.g. 51-200.
employee_countinteger | nullPrecise headcount where known.
countrystring | nullISO 3166-1 alpha-2.
statestring | nullUS state abbreviation or non-US region.
citystring | null
founded_yearinteger | null
ownershipstring | nullPublic or Private.
tickerstring | null
is_b2b, is_b2cboolean | null
store_countinteger | nullPhysical retail locations.
phonestring | nullCorporate phone.
linkedin_urlstring | null
contact_countinteger | nullPublished contacts available through /contacts.
all_websitesstring[]Every published domain; the primary domain first, the rest alphabetical.
industry_verticalstring | nulle.g. Apparel, Accessories, and Shoes. Distinct from business_type.
shipping_destinationsstring[]ISO alpha-2 codes the company's websites ship to, de-duplicated, sorted.
enrichmentobjectAdditive, best-effort enrichment fields (social and traffic data, sales series). Empty when none is available; a lookup never fails because of it.

CompanySummary

The four identity fields of a CompanyRecord — id, name, domain, business_type — embedded in /v1/contacts/by-email responses. Fetch the full record with /v1/companies/{company_id}.

ContactRecord

FieldTypeNotes
idstringPublic identifier, eti_ct_{uuid}.
company_idstringeti_co_{uuid} of the contact's company.
first_name, last_namestringAlways present.
titlestring | nullFree-text job title.
rolestring | nullNormalized role, e.g. CEO/President/Owner.
seniorityenum | nullc_suite, director, manager, other.
is_head_of_ecommerce, is_head_of_fulfillmentboolean | null
emailstring | nullNull when no verified email exists.
phonestring | nullBest available: direct line, then alternate, then the company's corporate line.
linkedin_urlstring | null
countrystring | nullISO alpha-2; may differ from the company's.

WebsiteInfoRecord

FieldTypeNotes
domainstringNormalized (lowercased, www. stripped).
company_idstringeti_co_{uuid}.
company_namestring
is_primarybooleanWhether this is the company's primary website.
brand_namestring | nullWhen different from the company name.
descriptionstring | null
launch_yearinteger | null
product_countinteger | null
orders_per_dayinteger | nullEstimated.
ships_internationally, has_warehouseboolean | nullnull = unknown, false = known no.
twitter, facebook, instagram, tiktok, blogstring | nullProfile / blog URLs.
platformsPlatformRecord[]Technology detected on the site, ordered by category then name. Each item: name (string) and category (string — commerce, analytics, esp, payment, shipping, … extensible; handle unknown values).

Records and limits

Your plan buys records per month. A record is one company, one website, or one contact returned in a successful (2xx) response. Errors return no records and count nothing. Calls are separately rate-limited per minute (and, on some keys, per day) so one runaway loop cannot flood the service.

Records per endpoint

EndpointRecords per 2xx response
GET /v1/companies/by-domain1
GET /v1/companies/{company_id}1
GET /v1/companies/{company_id}/contactsOne per contact returned — the page's returned count (up to limit)
GET /v1/contacts/by-email1
GET /v1/website-info/by-domain1

meta.records_returned on every response is exactly this number.

Plans

TierPlan limits
Explore5,000 records/month · up to 17 calls/minute
Scale10,000 records/month · up to 167 calls/minute
Summit25,000 records/month · up to 833 calls/minute
Enterprisecustom records/month, set per key · up to 1,000 calls/minute

The month is the UTC calendar month; your count resets at 00:00 UTC on the 1st. Need more this month, or a bigger plan? Contact support@etailinsights.com.

Limit headers

Every authenticated response carries the state of each limit including that response, so you can pace a client without a separate call.

HeaderMeaning
X-Records-LimitRecords your key may return this month.
X-Records-UsedRecords counted so far this month, including this response.
X-Records-Remainingmax(0, limit − used).
X-Records-ResetUnix epoch seconds of the next month's start (00:00 UTC on the 1st).
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-ResetCalls per minute for your tier, calls left in the current minute, epoch seconds when the minute window resets.
X-Daily-Limit, X-Daily-Remaining, X-Daily-ResetEvery key has a daily call cap equal to its monthly records limit (X-Daily-Limit equals X-Records-Limit unless support has set a fixed cap by hand): calls per day, calls left today, epoch seconds of the next midnight UTC. Present on every counted response; absent when there is nothing to count (a records limit of 0, where every lookup is refused with record_limit_exceeded) and on a response the counter could not record. The cap follows an upgrade, a new allotment or a grant on the next request, and today's count carries.
Retry-AfterOn a 429 only: seconds until the limit that fired resets.
In rare degraded conditions the service answers without having been able to count the request; the response then carries X-Records-Limit and X-Records-Reset only. The request still counts; the next normal response shows the true X-Records-Used.

Reaching the limit

A request that would take you past your monthly allotment is refused with 429 and error.code = "record_limit_exceeded". There is no overage: the key simply stops until the month resets, a grant is added, or the plan changes.

HTTP/1.1 429 Too Many Requests
Retry-After: 864000
X-Records-Limit: 10000
X-Records-Used: 10000
X-Records-Remaining: 0
X-Records-Reset: 1796083200

{
  "data": null,
  "error": {
    "code": "record_limit_exceeded",
    "message": "You've hit your limit for the month; it resets 2026-12-01T00:00:00Z. Contact support@etailinsights.com to increase your limit."
  },
  "meta": { "request_id": "…", "latency_ms": 3, "records_returned": 0 }
}

MCP and connectors

The same records are available to AI assistants through the Model Context Protocol (MCP), at one endpoint.

POST /mcp

Streamable HTTP, answered with one JSON response per POST: no event streams and no sessions. Protocol revisions 2025-06-18, 2025-11-25 and 2026-07-28 are served. GET and DELETE on /mcp return 405.

ToolSame asCost
lookup_company_by_domainGET /v1/companies/by-domain1 record
get_companyGET /v1/companies/{company_id}1 record
get_company_contactsGET /v1/companies/{company_id}/contacts1 record per contact returned
lookup_contact_by_emailGET /v1/contacts/by-email1 record
lookup_website_infoGET /v1/website-info/by-domain1 record

Each tool takes the parameters of the REST endpoint it matches, with the same bounds, and returns that endpoint's data object unchanged, with the limit values beside it.

Connecting by sign-in (OAuth)

Assistants that cannot send a key header — claude.ai, Claude Desktop and ChatGPT — connect through OAuth 2.1. The assistant finds the server from the 401 challenge, registers itself, and opens a consent page where you paste your API key once. The page shows which key it is — its label, plan and prefix — and nothing connects until you choose Connect. The assistant then calls /mcp with an access token that stands for that key: the same limits, the same allotment and the same records as the key itself.

EndpointWhat it does
GET /.well-known/oauth-protected-resource
GET /.well-known/oauth-protected-resource/mcp
Protected-resource metadata (RFC 9728): the resource is /mcp and there is one authorization server, this one.
GET /.well-known/oauth-authorization-server Authorization-server metadata (RFC 8414): PKCE with S256 only, public clients only (none), no scopes.
POST /oauth/register Dynamic client registration (RFC 7591), with a JSON body. Redirect URIs are https and matched exactly; http://localhost and http://127.0.0.1 are matched with any port. A registration whose redirect is local, or on a host other than Claude's or ChatGPT's, is limited per network: past the limit, 429 with temporarily_unavailable and Retry-After.
GET /oauth/authorize
POST /oauth/authorize
The consent page: paste the key, see which key it is, then Connect or use a different key. It warns when the app sends you back to an address we don't recognise, or to an app on your own computer. The answer carries code, state and iss (RFC 9207).
POST /oauth/token Form-encoded. authorization_code with the PKCE verifier, and refresh_token. A bad, expired or already-used code or refresh token is invalid_grant.
POST /oauth/revoke Token revocation (RFC 7009), form-encoded. Revoking a refresh token ends its connection.

Errors

HTTPerror.codeWhen
400invalid_requestMissing or malformed parameter, bad company_id prefix, limit outside 1–50, unknown seniority.
401unauthorizedMissing, malformed, unknown or revoked API key.
403regulatory_restrictionContact data withheld for a company in a GDPR/CASL-restricted country.
403forbiddenAccess not allowed from the source IP, or a capability the key lacks.
404not_foundNo such company, contact or website — or a path that does not exist for your key.
429rate_limitedPer-minute call limit for your tier. Back off Retry-After seconds.
429daily_limit_exceededThe key's daily call cap: as many calls a day as it has records a month. Resets at midnight UTC.
429record_limit_exceededMonthly record allotment reached. See Records and limits.
500internal_errorUnexpected server error; quote meta.request_id when reporting it.
503service_unavailableA backing service is unavailable; retry with backoff.