Shoplens.dev
Get an API key
Menu

Getting started

ShopLens API

Data on Shopify stores: store metrics, best-selling products, similar shops, ads on Meta, Google and TikTok, and the people who work at each store's company. Monitors watch shops for you and send what changed to a webhook. Use it from your code through the REST API or from an AI assistant through the MCP server.

Everything is JSON over HTTPS. Reads use GET. POST, PATCH and DELETE reveal a contact and manage monitors. The OpenAPI 3.1 spec is the source of truth for every field.

The API is versioned in the path. New fields and endpoints ship in v1, so ignore fields you don't know. Removing or renaming a field would ship as /api/v2, and v1 would keep working.

Base URL
https://data.shoplens.dev/api/v1
MCP server
https://data.shoplens.dev/mcp

Getting started

Authentication

Every REST endpoint needs an API key, sent in the Authorization header as Bearer <key>. Create keys in the ShopLens dashboard. Keep them on your server, never in browser code.

A missing, invalid or revoked key answers 401. A revoked key stops working at once. Only the documentation (/llms.txt, /llms-full.txt, /api/v1/openapi.json) and /api/v1/health are public.

The MCP server does not take API keys. It signs you in with OAuth instead.

Request
curl "https://data.shoplens.dev/api/v1/shops/example-shop.com" \
  --header "Authorization: Bearer $SHOPLENS_API_KEY"
{} Response401 UnauthorizedJSON
{
  "error": "Missing or invalid API key: send it as `Authorization: Bearer <key>`"
}

Getting started

Credits

Calls are paid with the credits of your plan. The REST API and the MCP server spend the same credits, and each endpoint below shows what it costs next to its path. See the pricing page for plans.

  • Only successful calls (200 or 201) spend credits. Errors, and a shop that is still being looked up (202), are free.
  • A call costs the same whether its data was already stored or fetched for it. Revealing a person again costs the reveal again, so keep the contact.
  • With fewer credits than a call can cost, it answers 402 and doesn't run, so your balance never goes below zero. A search needs its limit in credits available (25 by default) and is charged only for the shops it returns: with a low balance, lower limit.
  • Plan credits renew each billing period. Credits from recharges don't expire and are spent after your plan's.
{} Response402 Payment RequiredJSON
{
  "error": "Not enough credits: upgrade your plan or turn on auto recharge at https://app.shoplens.dev/billing"
}

Getting started

Rate limits

Each account has a limit of requests per minute that depends on its plan: 120 on Starter, 600 on Growth and 1,800 on Scale. All your API keys and the MCP server share it.

  • Above the limit, calls answer 429 with a Retry-After header in seconds. They don't run and spend no credits.
  • The count is approximate. Spread bursts out instead of aiming at the exact limit.
{} Response429 Too Many RequestsJSON
{
  "error": "Too many requests for your plan: retry after the seconds in the Retry-After header"
}

Getting started

Identifying a shop

Shops are addressed by domain, not by ID. In paths, use the bare domain: /shops/example-shop.com works, /shops/https://example-shop.com does not, because its slashes are read as extra path segments.

A leading www. is ignored, and the shop's .myshopify.com domain also works. The q search parameter does accept a full URL.

In responses, a shop's id is a Shopify global ID, gid://shopify/Shop/<number>. A best-selling product's id is gid://shopify/Product/<number>, or null for shops whose products have not been refreshed yet.

GET Same shop
/api/v1/shops/example-shop.com
/api/v1/shops/www.example-shop.com
/api/v1/shops/example-shop.myshopify.com

Getting started

Shops not indexed yet

When you ask Get a shop for a domain ShopLens has never seen, it queues the domain and answers 202 Accepted. A background job checks that the domain is a public Shopify store and enriches it, which takes a few minutes. The Retry-After header carries the same number of seconds as retryAfter. Poll with that delay, not right away.

  • 200: the shop is ready.
  • 202: still being looked up. Try again after Retry-After seconds.
  • 404: the domain is not a public Shopify store (not Shopify, password-protected, or unreachable for more than a day). This is final.

Search, ads and people only work with shops that already exist, and answer 404 for unknown ones.

{} Response202 AcceptedJSON
{
  "status": "pending",
  "message": "Shop not indexed yet: it is being looked up, try again in a few minutes",
  "retryAfter": 300
}

Getting started

Pagination

Lists are paginated with cursors. A paginated response has hasNextPage and nextCursor. While hasNextPage is true, repeat the same request with cursor=<nextCursor>.

  • Cursors are opaque: don't parse or build them. There are no offsets.
  • limit is 1 to 50, 25 by default. Ads on one platform have no limit: the page size is the platform's.
  • Keep the filters, sort and order the same while paginating.
Next page
GET /api/v1/shops?country=US&limit=25
→ hasNextPage: true, nextCursor: "eyJzIjoxMjUwMDB9"

GET /api/v1/shops?country=US&limit=25&cursor=eyJzIjoxMjUwMDB9

Getting started

Data conventions

  • Null means unknown. Most fields are nullable because a shop may not be fully analysed yet. A missing value is not zero.
  • Money is a decimal string, never a float. estimatedMonthlySales and estimatedMonthlyAppSpend are { "value": "125000.00", "currency": "USD" }, or null when there is no estimate.
  • Dates: a shop's createdAt is a calendar date, YYYY-MM-DD, with no time. Other timestamps, such as an ad's firstShownAt, are ISO 8601 date-times in UTC.

Getting started

Errors

Errors have the shape { "error": "message" }. Invalid parameters answer 400 with an issues list, where path names the invalid field.

Without enough credits for a call, it answers 402. Above your plan's rate limit, it answers 429.

Calling a path with a method it doesn't support answers 405 with an Allow header.

{} Response400 Bad RequestJSON
{
  "error": "Invalid request",
  "issues": [
    { "path": "limit", "message": "…" }
  ]
}

Endpoints

Search shops

GET/api/v1/shops1 credit per shop returned

Filters and ranks the shops ShopLens has indexed. Returns summaries. Call Get a shop for best sellers and product types.

  • Range filters are inclusive and named min<Metric> / max<Metric>. The metrics are EstimatedMonthlyVisits, EstimatedMonthlySales (USD), ProductsCount, VariantsCount and CollectionsCount.
  • minCreatedAt and maxCreatedAt filter by the date the shop was created. With either one set, shops with no known creation date are left out.
  • Shops without a value for the sort field come last. With createdAt, those are shops with no known creation date. Without sort the order is stable but arbitrary.
  • A cursor only works with the sort and order it was issued for, otherwise the API answers 400. Keep the filters the same while paginating.

Query parameters

  • qstringoptional

    Text to search in the shop name or domain.

  • countrystringoptional

    ISO 3166-1 alpha-2 code, such as US or ES.

  • categoryenumoptional

    One of:APPARELSHOESJEWELRY_WATCHESBAGS_ACCESSORIESBEAUTY_PERSONAL_CAREHEALTH_SUPPLEMENTSSPORTS_FITNESSHOME_DECORFURNITUREKITCHEN_DININGGARDEN_OUTDOORTOOLS_HARDWAREFOOD_BEVERAGESPETSBABY_KIDSTOYS_GAMESELECTRONICSCOMPUTERS_GAMINGBOOKS_MEDIAARTS_CRAFTS_COLLECTIBLESOFFICE_STATIONERYAUTOMOTIVEBUSINESS_INDUSTRIALDIGITAL_PRODUCTSGIFTS_PARTYSMOKING_VAPINGADULTWEAPONS_HUNTINGMASS_MERCHANTSOTHER

  • statusenumoptional

    ACTIVE, PASSWORD, CLOSED, NOT_FOUND or UNKNOWN.

  • shopifyPlusbooleanoptional

    true: only Shopify Plus shops. false: only shops known not to be on Plus.

  • minEstimatedMonthlyVisitsnumberoptional

    Minimum estimated monthly visits, inclusive.

  • maxEstimatedMonthlyVisitsnumberoptional

    Maximum estimated monthly visits, inclusive.

  • minEstimatedMonthlySalesnumberoptional

    Minimum estimated monthly sales in USD, inclusive.

  • maxEstimatedMonthlySalesnumberoptional

    Maximum estimated monthly sales in USD, inclusive.

  • minProductsCountnumberoptional

    Minimum number of products, inclusive.

  • maxProductsCountnumberoptional

    Maximum number of products, inclusive.

  • minVariantsCountnumberoptional

    Minimum number of product variants, inclusive.

  • maxVariantsCountnumberoptional

    Maximum number of product variants, inclusive.

  • minCollectionsCountnumberoptional

    Minimum number of collections, inclusive.

  • maxCollectionsCountnumberoptional

    Maximum number of collections, inclusive.

  • minCreatedAtdateoptional

    Earliest date the shop was created, YYYY-MM-DD, inclusive.

  • maxCreatedAtdateoptional

    Latest date the shop was created, YYYY-MM-DD, inclusive.

  • sortenumoptional

    estimatedMonthlyVisits, estimatedMonthlySales, productsCount, variantsCount, collectionsCount or createdAt (the date the shop was created).

  • orderenumoptional

    desc (default, highest or newest first) or asc.

  • limitintegeroptional

    Number of results, 1 to 50. Defaults to 25.

  • cursorstringoptional

    nextCursor from the previous response, as is.

Responses

  • 200A page of shops
    • shopsobject[]

      19 fields
      • idstring

        gid://shopify/Shop/<number>

      • createdAtstring | null

        Date the shop was created, YYYY-MM-DD.

      • namestring | null

      • domainstring | null

        Primary domain.

      • myshopifyDomainstring

      • countryCodestring | null

        ISO 3166-1 alpha-2.

      • citystring | null

      • regionstring | null

        State or province.

      • currencystring | null

        The shop's base currency, ISO 4217.

      • primaryLanguagestring | null

        Language code, such as en.

      • categoryenum | null

        One of the values of the category filter in Search shops.

      • statusenum | null

        ACTIVE, PASSWORD, CLOSED, NOT_FOUND or UNKNOWN.

      • productsCountinteger | null

      • variantsCountinteger | null

      • collectionsCountinteger | null

      • estimatedMonthlyVisitsinteger | null

      • estimatedMonthlySalesobject | null

        Null when there is no estimate.

        2 fields
        • valuestring

          Decimal amount, such as 125000.00.

        • currencystring

          Always USD.

      • estimatedMonthlyAppSpendobject | null

        Monthly spend on Shopify apps. Null when there is no estimate.

        2 fields
        • valuestring

          Decimal amount, such as 125000.00.

        • currencystring

          Always USD.

      • isShopifyPlusboolean | null

        true: on Shopify Plus. false: known not to be on Plus. null: no signal.

    • hasNextPageboolean

    • nextCursorstring | null

      Pass it as cursor for the next page. Null on the last page.

  • 400A parameter is invalid
    • errorstring

      Always Invalid request.

    • issuesobject[]

      2 fields
      • pathstring

        The invalid parameter, such as limit.

      • messagestring

  • 401Missing, invalid or revoked API key
    • errorstring

      What went wrong.

  • 402Not enough credits for this call
    • errorstring

      What went wrong.

Request
curl --request GET \
  --url "https://data.shoplens.dev/api/v1/shops?country=US&category=APPAREL&sort=estimatedMonthlySales&limit=10" \
  --header "Authorization: Bearer $SHOPLENS_API_KEY"
{} Response
JSON
{
  "shops": [
    {
      "id": "gid://shopify/Shop/48213790",
      "createdAt": "2019-03-14",
      "name": "Example Shop",
      "domain": "example-shop.com",
      "myshopifyDomain": "example-shop.myshopify.com",
      "countryCode": "US",
      "city": "Austin",
      "region": "Texas",
      "currency": "USD",
      "primaryLanguage": "en",
      "category": "APPAREL",
      "status": "ACTIVE",
      "productsCount": 412,
      "variantsCount": 2380,
      "collectionsCount": 36,
      "estimatedMonthlyVisits": 184000,
      "estimatedMonthlySales": { "value": "125000.00", "currency": "USD" },
      "estimatedMonthlyAppSpend": { "value": "1240.00", "currency": "USD" },
      "isShopifyPlus": true
    }
  ],
  "hasNextPage": true,
  "nextCursor": "eyJzIjoxMjUwMDB9"
}

Endpoints

Get a shop

GET/api/v1/shops/{domain}1 credit

Returns the shop, its best-selling products (topProducts, best first), its product types (productTypes, most products first) and the shops most similar to it (similarShops). It is the only endpoint that looks up domains ShopLens has not seen yet. See Shops not indexed yet.

  • similarShops lists the domains of the 5 to 10 shops most similar to this one, to find competitors or alternatives. It is empty until the comparison has been computed. Pass any of them back to this endpoint.
  • taxonomyCategory, on best sellers and product types, is a path from the Shopify Product Taxonomy in English, such as Apparel & Accessories > Clothing.
  • merchantProductType is the type as the merchant wrote it. It is free text, so two shops can name the same thing differently.

Path parameters

  • domainstringrequired

    Bare shop domain, such as example-shop.com, without https:// or slashes. A leading www. is ignored and the .myshopify.com domain also works.

Responses

  • 200The shop. If the domain redirects to another store's main domain, that store is returned
    • idstring

      gid://shopify/Shop/<number>

    • createdAtstring | null

      Date the shop was created, YYYY-MM-DD.

    • namestring | null

    • domainstring | null

      Primary domain.

    • myshopifyDomainstring

    • countryCodestring | null

      ISO 3166-1 alpha-2.

    • citystring | null

    • regionstring | null

      State or province.

    • currencystring | null

      The shop's base currency, ISO 4217.

    • primaryLanguagestring | null

      Language code, such as en.

    • categoryenum | null

      One of the values of the category filter in Search shops.

    • statusenum | null

      ACTIVE, PASSWORD, CLOSED, NOT_FOUND or UNKNOWN.

    • productsCountinteger | null

    • variantsCountinteger | null

    • collectionsCountinteger | null

    • estimatedMonthlyVisitsinteger | null

    • estimatedMonthlySalesobject | null

      Null when there is no estimate.

      2 fields
      • valuestring

        Decimal amount, such as 125000.00.

      • currencystring

        Always USD.

    • estimatedMonthlyAppSpendobject | null

      Monthly spend on Shopify apps. Null when there is no estimate.

      2 fields
      • valuestring

        Decimal amount, such as 125000.00.

      • currencystring

        Always USD.

    • isShopifyPlusboolean | null

      true: on Shopify Plus. false: known not to be on Plus. null: no signal.

    • languagesstring[]

      Published languages.

    • shopifyThemestring | null

      Only set for themes from the Shopify Theme Store.

    • emailsstring[]

    • phonesstring[]

    • similarShopsstring[]

      Domains of the 5 to 10 most similar shops. Empty when not computed yet.

    • topProductsobject[]

      Best sellers, best first.

      10 fields
      • idstring | null

        gid://shopify/Product/<number>. Null for shops whose products have not been refreshed yet.

      • rankinteger

        1 is the best seller.

      • titlestring

      • handlestring

      • merchantProductTypestring | null

      • taxonomyCategorystring | null

      • shopUrlstring | null

      • featuredImagestring | null

      • pricestring | null

        Lowest variant price, as a decimal string.

      • currencystring | null

    • productTypesobject[]

      Most products first.

      3 fields
      • merchantProductTypestring | null

      • taxonomyCategorystring | null

      • productsCountinteger

  • 202Not indexed yet, being looked up
    • statusstring

      Always pending.

    • messagestring

    • retryAfterinteger

      Seconds to wait before asking again, same as the Retry-After header.

  • 400A parameter is invalid
    • errorstring

      Always Invalid request.

    • issuesobject[]

      2 fields
      • pathstring

        The invalid parameter, such as limit.

      • messagestring

  • 401Missing, invalid or revoked API key
    • errorstring

      What went wrong.

  • 402Not enough credits for this call
    • errorstring

      What went wrong.

  • 404No public Shopify store at this domain
    • errorstring

      What went wrong.

Request
curl --request GET \
  --url "https://data.shoplens.dev/api/v1/shops/example-shop.com" \
  --header "Authorization: Bearer $SHOPLENS_API_KEY"
{} Response
JSON
{
  "id": "gid://shopify/Shop/48213790",
  "createdAt": "2019-03-14",
  "name": "Example Shop",
  "domain": "example-shop.com",
  "myshopifyDomain": "example-shop.myshopify.com",
  "countryCode": "US",
  "city": "Austin",
  "region": "Texas",
  "currency": "USD",
  "primaryLanguage": "en",
  "category": "APPAREL",
  "status": "ACTIVE",
  "productsCount": 412,
  "variantsCount": 2380,
  "collectionsCount": 36,
  "estimatedMonthlyVisits": 184000,
  "estimatedMonthlySales": { "value": "125000.00", "currency": "USD" },
  "estimatedMonthlyAppSpend": { "value": "1240.00", "currency": "USD" },
  "isShopifyPlus": true,
  "languages": ["en", "es"],
  "shopifyTheme": "Dawn",
  "emails": ["hello@example-shop.com"],
  "phones": [],
  "similarShops": ["another-example.com", "example-store.co"],
  "topProducts": [
    {
      "id": "gid://shopify/Product/7790214",
      "rank": 1,
      "title": "Everyday Tee",
      "handle": "everyday-tee",
      "merchantProductType": "T-Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing",
      "shopUrl": "https://example-shop.com/products/everyday-tee",
      "featuredImage": "https://cdn.shopify.com/…/everyday-tee.jpg",
      "price": "28.00",
      "currency": "USD"
    }
  ],
  "productTypes": [
    {
      "merchantProductType": "T-Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing",
      "productsCount": 64
    }
  ]
}

Endpoints

Get a shop's ads

GET/api/v1/shops/{domain}/ads40 credits

Returns, for each of meta, google and tiktok, the advertiser found for the shop, a link to its public ad library, the total number of ads and its 20 most recent ads, newest first (fewer if the advertiser has fewer).

  • Meta and TikTok are searched by name, and only an advertiser whose name matches the shop is accepted. When none matches, advertiser is null and total is 0. ShopLens would rather return no advertiser than another company's ads.
  • total is the count the platform reports, approximate on Meta and Google, and null when unknown. To page through every ad, use List every ad on one platform.
  • placements says where an ad runs inside the platform, such as facebook or instagram. Only Meta fills it.

Path parameters

  • domainstringrequired

    Bare shop domain, such as example-shop.com, without https:// or slashes. A leading www. is ignored and the .myshopify.com domain also works.

Responses

  • 200Ads per platform
    • platformsobject

      1 field
      • meta, google, tiktokobject | null

        One key per platform, each with these fields.

        4 fields
        • advertiserobject | null

          Null when no advertiser matched the shop.

          2 fields
          • idstring | null

          • namestring | null

        • libraryUrlstring | null

          All of the advertiser's ads in the ad library.

        • totalinteger | null

          Null when unknown.

        • adsobject[]

          Up to 20, newest first.

          14 fields
          • idstring

            The platform's own ad ID.

          • placementsstring[]

            Empty outside Meta.

          • formatstring | null

            image, video or text.

          • titlestring | null

          • textstring | null

          • ctastring | null

          • imageUrlstring | null

          • videoUrlstring | null

          • landingUrlstring | null

          • libraryUrlstring | null

            This ad in the ad library.

          • isActiveboolean | null

          • firstShownAtstring | null

            ISO 8601 date-time, UTC.

          • lastShownAtstring | null

            ISO 8601 date-time, UTC.

          • daysRunninginteger | null

            Calendar days (UTC) the ad has run, counting the first and last day: from firstShownAt to lastShownAt, or to today for an active ad with no lastShownAt. Null when the dates are unknown.

  • 400A parameter is invalid
    • errorstring

      Always Invalid request.

    • issuesobject[]

      2 fields
      • pathstring

        The invalid parameter, such as limit.

      • messagestring

  • 401Missing, invalid or revoked API key
    • errorstring

      What went wrong.

  • 402Not enough credits for this call
    • errorstring

      What went wrong.

  • 404No shop with this domain
    • errorstring

      What went wrong.

Request
curl --request GET \
  --url "https://data.shoplens.dev/api/v1/shops/example-shop.com/ads" \
  --header "Authorization: Bearer $SHOPLENS_API_KEY"
{} Response
JSON
{
  "platforms": {
    "meta": {
      "advertiser": { "id": "104829310", "name": "Example Shop" },
      "libraryUrl": "https://www.facebook.com/ads/library/?active_status=active&ad_type=all&country=ALL&view_all_page_id=104829310",
      "total": 7415,
      "ads": [
        {
          "id": "1209384756",
          "placements": ["facebook", "instagram"],
          "format": "video",
          "title": null,
          "text": "Our best sellers, as always, with love",
          "cta": "Shop now",
          "imageUrl": "https://scontent.xx.fbcdn.net/v/t39.35426-6/…_n.jpg",
          "videoUrl": "https://video.xx.fbcdn.net/o1/v/t2/f2/m366/….mp4",
          "landingUrl": "https://example-shop.com/collections/best-sellers",
          "libraryUrl": "https://www.facebook.com/ads/library/?id=1209384756",
          "isActive": true,
          "firstShownAt": "2026-10-03T00:00:00.000Z",
          "lastShownAt": "2026-10-27T00:00:00.000Z",
          "daysRunning": 25
        }
      ]
    },
    "google": {
      "advertiser": { "id": "AR00000000000000000001", "name": null },
      "libraryUrl": "https://adstransparency.google.com/advertiser/AR00000000000000000001",
      "total": 1000,
      "ads": [
        {
          "id": "CR00000000000000000001",
          "placements": [],
          "format": "video",
          "title": null,
          "text": null,
          "cta": null,
          "imageUrl": null,
          "videoUrl": null,
          "landingUrl": null,
          "libraryUrl": "https://adstransparency.google.com/advertiser/AR00000000000000000001/creative/CR00000000000000000001",
          "isActive": null,
          "firstShownAt": "2026-09-10T00:00:00.000Z",
          "lastShownAt": "2026-10-27T09:21:53.000Z",
          "daysRunning": 48
        }
      ]
    },
    "tiktok": {
      "advertiser": { "id": "7000000000000000001", "name": "EXAMPLE SHOP SL" },
      "libraryUrl": null,
      "total": 57539,
      "ads": [
        {
          "id": "1800000000000000001",
          "placements": [],
          "format": "video",
          "title": "As always, with love",
          "text": null,
          "cta": null,
          "imageUrl": "https://p16-common-sign.tiktokcdn.com/…~tplv-tiktokx-origin.jpeg",
          "videoUrl": "https://library.tiktok.com/api/v1/cdn/…",
          "landingUrl": null,
          "libraryUrl": "https://library.tiktok.com/ads/detail/?ad_id=1800000000000000001",
          "isActive": null,
          "firstShownAt": "2026-10-26T00:00:00.000Z",
          "lastShownAt": "2026-10-26T00:00:00.000Z",
          "daysRunning": 1
        }
      ]
    }
  }
}

Endpoints

List every ad on one platform

GET/api/v1/shops/{domain}/ads/{platform}40 credits per page

Pages through all of the shop's ads on one platform, active and inactive, with the same advertiser, libraryUrl and total as Get a shop's ads. Use that endpoint when the 20 most recent ads are enough.

  • Each page is read from the platform's ad library when you ask for it, so its size is the platform's, about 10 to 30 ads, and there is no limit. Meta returns the newest first. Google and TikTok use their own default order.
  • The filters are applied by the platform's library, so each platform supports a different set: Meta all but placement, Google startDate, endDate, format, country and placement, TikTok none. A filter the platform does not support answers 400 instead of being ignored.
  • A cursor only works with the platform, advertiser and filters it was issued for, otherwise the API answers 400. Keep the same filters while paginating.
  • When no advertiser matched the shop, advertiser is null and ads is empty. If the platform fails, the API answers 502. Retry with the same cursor.

Path parameters

  • domainstringrequired

    Bare shop domain, such as example-shop.com, without https:// or slashes. A leading www. is ignored and the .myshopify.com domain also works.

  • platformenumrequired

    meta, google or tiktok.

Query parameters

  • startDatedateoptional

    Only ads shown on or after this date, YYYY-MM-DD. Meta and Google.

  • endDatedateoptional

    Only ads shown on or before this date, YYYY-MM-DD. Meta and Google.

  • formatenumoptional

    image, video or text. Meta and Google.

  • countrystringoptional

    Only ads shown in this country, ISO 3166-1 alpha-2 such as US. Meta and Google.

  • statusenumoptional

    all (default), active or inactive. Meta only.

  • languagestringoptional

    Ad language, ISO 639-1 such as en. Meta only.

  • placementenumoptional

    search, youtube, shopping, maps or play. Google only.

  • sortenumoptional

    recent (default, newest first) or impressions (most impressions first). Meta only.

  • cursorstringoptional

    nextCursor from the previous response, as is.

Responses

  • 200A page of ads
    • advertiserobject | null

      Null when no advertiser matched the shop.

      2 fields
      • idstring | null

      • namestring | null

    • libraryUrlstring | null

      All of the advertiser's ads in the ad library.

    • totalinteger | null

      Null when unknown.

    • adsobject[]

      One page, in the platform's order.

      14 fields
      • idstring

        The platform's own ad ID.

      • placementsstring[]

        Empty outside Meta.

      • formatstring | null

        image, video or text.

      • titlestring | null

      • textstring | null

      • ctastring | null

      • imageUrlstring | null

      • videoUrlstring | null

      • landingUrlstring | null

      • libraryUrlstring | null

        This ad in the ad library.

      • isActiveboolean | null

      • firstShownAtstring | null

        ISO 8601 date-time, UTC.

      • lastShownAtstring | null

        ISO 8601 date-time, UTC.

      • daysRunninginteger | null

        Calendar days (UTC) the ad has run, counting the first and last day: from firstShownAt to lastShownAt, or to today for an active ad with no lastShownAt. Null when the dates are unknown.

    • hasNextPageboolean

    • nextCursorstring | null

      Pass it as cursor for the next page. Null on the last page.

  • 400A parameter is invalid
    • errorstring

      Always Invalid request.

    • issuesobject[]

      2 fields
      • pathstring

        The invalid parameter, such as limit.

      • messagestring

  • 401Missing, invalid or revoked API key
    • errorstring

      What went wrong.

  • 402Not enough credits for this call
    • errorstring

      What went wrong.

  • 404No shop with this domain
    • errorstring

      What went wrong.

  • 502The platform failed. Retry with the same cursor
    • errorstring

      What went wrong.

Request
curl --request GET \
  --url "https://data.shoplens.dev/api/v1/shops/example-shop.com/ads/meta?format=video&country=US&sort=impressions" \
  --header "Authorization: Bearer $SHOPLENS_API_KEY"
{} Response
JSON
{
  "advertiser": { "id": "104829310", "name": "Example Shop" },
  "libraryUrl": "https://www.facebook.com/ads/library/?active_status=active&ad_type=all&country=ALL&view_all_page_id=104829310",
  "total": 7415,
  "ads": [
    {
      "id": "1209384756",
      "placements": ["facebook", "instagram"],
      "format": "video",
      "title": null,
      "text": "Our best sellers, as always, with love",
      "cta": "Shop now",
      "imageUrl": "https://scontent.xx.fbcdn.net/v/t39.35426-6/…_n.jpg",
      "videoUrl": "https://video.xx.fbcdn.net/o1/v/t2/f2/m366/….mp4",
      "landingUrl": "https://example-shop.com/collections/best-sellers",
      "libraryUrl": "https://www.facebook.com/ads/library/?id=1209384756",
      "isActive": true,
      "firstShownAt": "2026-10-03T00:00:00.000Z",
      "lastShownAt": "2026-10-27T00:00:00.000Z",
      "daysRunning": 25
    }
  ],
  "hasNextPage": true,
  "nextCursor": "eyJwbGF0Zm9ybSI6Ik1FVEEifQ"
}

Endpoints

List people at a shop

GET/api/v1/shops/{domain}/people5 credits

Lists people who work at the shop's company, with masked last names. hasEmail and hasPhone say what is available to reveal.

  • The list never includes contacts, not even for people already revealed. email, phone and linkedinUrl only come from Reveal a person's contact.
  • total is the number of people reported for the company, or null.

Path parameters

  • domainstringrequired

    Bare shop domain, such as example-shop.com, without https:// or slashes. A leading www. is ignored and the .myshopify.com domain also works.

Query parameters

  • limitintegeroptional

    Number of results, 1 to 50. Defaults to 25.

  • cursorstringoptional

    nextCursor from the previous response, as is.

Responses

  • 200A page of people
    • totalinteger | null

      Null when unknown.

    • peopleobject[]

      6 fields
      • idstring

        Use it to reveal the contact.

      • firstNamestring | null

      • lastNamestring | null

        Masked, such as Sm***h, until the contact is revealed.

      • titlestring | null

        Job title.

      • hasEmailboolean | null

        Whether an email is available to reveal.

      • hasPhoneboolean | null

        Whether a direct phone is available to reveal.

    • hasNextPageboolean

    • nextCursorstring | null

      Pass it as cursor for the next page. Null on the last page.

  • 400A parameter is invalid
    • errorstring

      Always Invalid request.

    • issuesobject[]

      2 fields
      • pathstring

        The invalid parameter, such as limit.

      • messagestring

  • 401Missing, invalid or revoked API key
    • errorstring

      What went wrong.

  • 402Not enough credits for this call
    • errorstring

      What went wrong.

  • 404No shop with this domain
    • errorstring

      What went wrong.

Request
curl --request GET \
  --url "https://data.shoplens.dev/api/v1/shops/example-shop.com/people?limit=25" \
  --header "Authorization: Bearer $SHOPLENS_API_KEY"
{} Response
JSON
{
  "total": 38,
  "people": [
    {
      "id": "p_8f2a91c4",
      "firstName": "Jordan",
      "lastName": "Sm***h",
      "title": "Head of Ecommerce",
      "hasEmail": true,
      "hasPhone": false
    }
  ],
  "hasNextPage": true,
  "nextCursor": "eyJwIjoyNX0"
}

Endpoints

Reveal a person's contact

POST/api/v1/shops/{domain}/people/{id}/reveal25 credits, 150 with a phone

Reveals one person's work email and LinkedIn URL and returns the person with email, phone and linkedinUrl, each null when not available. phone is only returned with ?phone=true, which also reveals the phone number. Without it, phone is always null. It is the only way to get a contact. Call it only when a user asks for that person's contact, never over a whole list.

  • Use the id from the people list. Only people listed for that shop can be revealed.
  • The contact is stored, so revealing the same person again returns it right away, but it costs the same credits (150 with phone=true when a phone is returned). Keep the contact instead of revealing it again. Asking for the phone later, for someone revealed without it, does one more lookup.
  • Phone lookups often fail at the provider. On a first reveal the email and LinkedIn are then returned without the phone, so check whether phone is null.

Path parameters

  • domainstringrequired

    Bare shop domain, such as example-shop.com, without https:// or slashes. A leading www. is ignored and the .myshopify.com domain also works.

  • idstringrequired

    The person's id from the people list.

Query parameters

  • phonebooleanoptional

    true to also reveal the phone number. Without it the phone is never returned. Defaults to false.

Responses

  • 200The person with their revealed contact
    • idstring

      Use it to reveal the contact.

    • firstNamestring | null

    • lastNamestring | null

      Unmasked.

    • titlestring | null

      Job title.

    • hasEmailboolean | null

      Whether an email is available to reveal.

    • hasPhoneboolean | null

      Whether a direct phone is available to reveal.

    • emailstring | null

      Work email. Null when the provider has none.

    • phonestring | null

      Only returned when asked for with phone=true. Null otherwise, or when not available.

    • linkedinUrlstring | null

      Null when the provider has none.

  • 400A parameter is invalid
    • errorstring

      Always Invalid request.

    • issuesobject[]

      2 fields
      • pathstring

        The invalid parameter, such as limit.

      • messagestring

  • 401Missing, invalid or revoked API key
    • errorstring

      What went wrong.

  • 402Not enough credits for this call
    • errorstring

      What went wrong.

  • 404Shop or person not found, or the provider could not reveal this person
    • errorstring

      What went wrong.

  • 422hasPhone is false. Retry without phone=true
    • errorstring

      What went wrong.

  • 502The provider failed. Try again later
    • errorstring

      What went wrong.

Request
curl --request POST \
  --url "https://data.shoplens.dev/api/v1/shops/example-shop.com/people/p_8f2a91c4/reveal" \
  --header "Authorization: Bearer $SHOPLENS_API_KEY"
{} Response
JSON
{
  "id": "p_8f2a91c4",
  "firstName": "Jordan",
  "lastName": "Smith",
  "title": "Head of Ecommerce",
  "hasEmail": true,
  "hasPhone": false,
  "email": "jordan@example-shop.com",
  "phone": null,
  "linkedinUrl": "https://www.linkedin.com/in/…"
}

Monitors

Monitors and webhooks

A monitor watches a set of shops for the changes you pick (its topics) and sends them to your webhookUrl. Monitors belong to the user of the API key, and only that user can see or change them. ShopLens checks for changes about once a day.

Some topics are checked per market, because a shop can sell different products, at different prices and with different stock in each country. They are checked in each country of markets, or only in each shop's primary market when markets is empty. The other topics ignore it.

Each webhook is signed with the monitor's secret, so your endpoint can check it comes from ShopLens. You get the secret when you create the monitor and from Get a monitor. See Receiving webhooks for the headers, the signature and what each event sends.

Topics

TopicFires whenPer market
product.createdA product is publishedYes
product.deletedA product is unpublished or deletedYes
product.title_changedA product's title changesNo
product.description_changedA product's description changesNo
product.images_changedA product's images are added, removed or reorderedNo
product.price_changedThe price of any variant changesYes
product.compare_at_price_changedThe compare-at price of any variant changesYes
product.sale_startedA variant's compare-at price becomes higher than its priceYes
product.sale_endedA variant stops being on saleYes
product.out_of_stockA variant sells outYes
product.back_in_stockA sold-out variant is available againYes
variant.createdA product gets a new variant, such as a size or colorNo
variant.deletedA variant is removed from a productNo
collection.createdA collection is publishedNo
POST /api/v1/monitors
{
  "webhookUrl": "https://example-app.com/webhooks/shoplens",
  "topics": ["product.price_changed", "product.out_of_stock"],
  "markets": ["US", "ES"],
  "shops": ["example-shop.com"]
}

Endpoints

Create a monitor

POST/api/v1/monitors1 credit

Subscribes to changes in up to 100 shops and sends them to your webhookUrl. The response includes the secret the webhooks are signed with.

  • Every shop must be one ShopLens already has. An unknown domain answers 400 with an issue at shops.<position>. Request it first with Get a shop and add it once it is ready.
  • Several monitors watching the same shop share one check of it, about once a day.

Body parameters

  • namestring | nulloptional

    Up to 100 characters, to recognise the monitor.

  • webhookUrlstringrequired

    HTTPS URL on a public domain that receives the webhooks, on the default port and without credentials. IP addresses, localhost and internal names such as .local or .internal are rejected. It is stored normalized, and a domain that resolves to a private address is not delivered to.

  • topicsstring[]required

    Changes to watch, at least one. See Monitors and webhooks for the list.

  • marketsstring[]optional

    Up to 50 countries, ISO 3166-1 alpha-2 such as US, where the per-market topics are checked. Empty or omitted: each shop's primary market only.

  • shopsstring[]required

    1 to 100 shop domains, written as in Get a shop. Each must be a shop ShopLens already has.

  • activebooleanoptional

    Defaults to true. false creates it paused.

Responses

  • 201The new monitor, with its secret
    • idstring

      Monitor ID, a UUID.

    • namestring | null

    • webhookUrlstring

    • topicsstring[]

      Changes watched.

    • marketsstring[]

      Countries where per-market topics are checked. Empty: each shop's primary market only.

    • activeboolean

      false: paused.

    • shopsobject[]

      Shops watched, in the order they were added.

      4 fields
      • idstring

        gid://shopify/Shop/<number>

      • namestring | null

      • domainstring | null

        Primary domain, without www.

      • myshopifyDomainstring

    • createdAtstring

      ISO 8601 date-time.

    • updatedAtstring

      ISO 8601 date-time.

    • secretstring

      Key the monitor's webhooks are signed with (Standard Webhooks). Keep it private.

  • 400A field is invalid, or a shop is not one ShopLens has
    • errorstring

      Always Invalid request.

    • issuesobject[]

      2 fields
      • pathstring

        The invalid parameter, such as limit.

      • messagestring

  • 401Missing, invalid or revoked API key
    • errorstring

      What went wrong.

  • 402Not enough credits for this call
    • errorstring

      What went wrong.

Request
curl --request POST \
  --url "https://data.shoplens.dev/api/v1/monitors" \
  --header "Authorization: Bearer $SHOPLENS_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Competitors",
    "webhookUrl": "https://example-app.com/webhooks/shoplens",
    "topics": [
      "product.created",
      "product.price_changed",
      "product.out_of_stock"
    ],
    "markets": [
      "US",
      "ES"
    ],
    "shops": [
      "example-shop.com",
      "another-example.com"
    ]
  }'
{} Response
JSON
{
  "id": "3f6b2c1e-8a4d-4f7e-9b2a-6c1d0e5f7a90",
  "name": "Competitors",
  "webhookUrl": "https://example-app.com/webhooks/shoplens",
  "topics": [
    "product.created",
    "product.price_changed",
    "product.out_of_stock"
  ],
  "markets": ["US", "ES"],
  "active": true,
  "shops": [
    {
      "id": "gid://shopify/Shop/48213790",
      "name": "Example Shop",
      "domain": "example-shop.com",
      "myshopifyDomain": "example-shop.myshopify.com"
    },
    {
      "id": "gid://shopify/Shop/61029384",
      "name": "Another Example",
      "domain": "another-example.com",
      "myshopifyDomain": "another-example.myshopify.com"
    }
  ],
  "createdAt": "2026-10-30T09:12:44.000Z",
  "updatedAt": "2026-10-30T09:12:44.000Z",
  "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw…"
}

Endpoints

List your monitors

GET/api/v1/monitorsFree

Your monitors, paginated. Secrets are not included: get one monitor to see its secret.

Query parameters

  • limitintegeroptional

    Number of results, 1 to 50. Defaults to 25.

  • cursorstringoptional

    nextCursor from the previous response, as is.

Responses

  • 200A page of monitors
    • monitorsobject[]

      9 fields
      • idstring

        Monitor ID, a UUID.

      • namestring | null

      • webhookUrlstring

      • topicsstring[]

        Changes watched.

      • marketsstring[]

        Countries where per-market topics are checked. Empty: each shop's primary market only.

      • activeboolean

        false: paused.

      • shopsobject[]

        Shops watched, in the order they were added.

        4 fields
        • idstring

          gid://shopify/Shop/<number>

        • namestring | null

        • domainstring | null

          Primary domain, without www.

        • myshopifyDomainstring

      • createdAtstring

        ISO 8601 date-time.

      • updatedAtstring

        ISO 8601 date-time.

    • hasNextPageboolean

    • nextCursorstring | null

      Pass it as cursor for the next page. Null on the last page.

  • 400A parameter is invalid
    • errorstring

      Always Invalid request.

    • issuesobject[]

      2 fields
      • pathstring

        The invalid parameter, such as limit.

      • messagestring

  • 401Missing, invalid or revoked API key
    • errorstring

      What went wrong.

Request
curl --request GET \
  --url "https://data.shoplens.dev/api/v1/monitors?limit=25" \
  --header "Authorization: Bearer $SHOPLENS_API_KEY"
{} Response
JSON
{
  "monitors": [
    {
      "id": "3f6b2c1e-8a4d-4f7e-9b2a-6c1d0e5f7a90",
      "name": "Competitors",
      "webhookUrl": "https://example-app.com/webhooks/shoplens",
      "topics": [
        "product.created",
        "product.price_changed",
        "product.out_of_stock"
      ],
      "markets": ["US", "ES"],
      "active": true,
      "shops": [
        {
          "id": "gid://shopify/Shop/48213790",
          "name": "Example Shop",
          "domain": "example-shop.com",
          "myshopifyDomain": "example-shop.myshopify.com"
        },
        {
          "id": "gid://shopify/Shop/61029384",
          "name": "Another Example",
          "domain": "another-example.com",
          "myshopifyDomain": "another-example.myshopify.com"
        }
      ],
      "createdAt": "2026-10-30T09:12:44.000Z",
      "updatedAt": "2026-10-30T09:12:44.000Z"
    }
  ],
  "hasNextPage": false,
  "nextCursor": null
}

Endpoints

Get a monitor

GET/api/v1/monitors/{id}Free

One of your monitors, with the secret its webhooks are signed with.

Path parameters

  • idstringrequired

    The monitor's id.

Responses

  • 200The monitor
    • idstring

      Monitor ID, a UUID.

    • namestring | null

    • webhookUrlstring

    • topicsstring[]

      Changes watched.

    • marketsstring[]

      Countries where per-market topics are checked. Empty: each shop's primary market only.

    • activeboolean

      false: paused.

    • shopsobject[]

      Shops watched, in the order they were added.

      4 fields
      • idstring

        gid://shopify/Shop/<number>

      • namestring | null

      • domainstring | null

        Primary domain, without www.

      • myshopifyDomainstring

    • createdAtstring

      ISO 8601 date-time.

    • updatedAtstring

      ISO 8601 date-time.

    • secretstring

      Key the monitor's webhooks are signed with (Standard Webhooks). Keep it private.

  • 400A parameter is invalid
    • errorstring

      Always Invalid request.

    • issuesobject[]

      2 fields
      • pathstring

        The invalid parameter, such as limit.

      • messagestring

  • 401Missing, invalid or revoked API key
    • errorstring

      What went wrong.

  • 404No monitor with this id among yours
    • errorstring

      What went wrong.

Request
curl --request GET \
  --url "https://data.shoplens.dev/api/v1/monitors/3f6b2c1e-8a4d-4f7e-9b2a-6c1d0e5f7a90" \
  --header "Authorization: Bearer $SHOPLENS_API_KEY"
{} Response
JSON
{
  "id": "3f6b2c1e-8a4d-4f7e-9b2a-6c1d0e5f7a90",
  "name": "Competitors",
  "webhookUrl": "https://example-app.com/webhooks/shoplens",
  "topics": [
    "product.created",
    "product.price_changed",
    "product.out_of_stock"
  ],
  "markets": ["US", "ES"],
  "active": true,
  "shops": [
    {
      "id": "gid://shopify/Shop/48213790",
      "name": "Example Shop",
      "domain": "example-shop.com",
      "myshopifyDomain": "example-shop.myshopify.com"
    },
    {
      "id": "gid://shopify/Shop/61029384",
      "name": "Another Example",
      "domain": "another-example.com",
      "myshopifyDomain": "another-example.myshopify.com"
    }
  ],
  "createdAt": "2026-10-30T09:12:44.000Z",
  "updatedAt": "2026-10-30T09:12:44.000Z",
  "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw…"
}

Endpoints

Update a monitor

PATCH/api/v1/monitors/{id}Free

Changes only the fields you send and keeps the rest. Send at least one.

  • topics, markets and shops replace the whole list.
  • "active": false pauses the monitor and "name": null clears its name.

Path parameters

  • idstringrequired

    The monitor's id.

Body parameters

  • namestring | nulloptional

    Up to 100 characters, to recognise the monitor. null clears it.

  • webhookUrlstringoptional

    HTTPS URL on a public domain that receives the webhooks, on the default port and without credentials. IP addresses, localhost and internal names such as .local or .internal are rejected. It is stored normalized, and a domain that resolves to a private address is not delivered to.

  • topicsstring[]optional

    Changes to watch, at least one. See Monitors and webhooks for the list.

  • marketsstring[]optional

    Up to 50 countries, ISO 3166-1 alpha-2 such as US, where the per-market topics are checked. Empty or omitted: each shop's primary market only.

  • shopsstring[]optional

    1 to 100 shop domains, written as in Get a shop. Each must be a shop ShopLens already has.

  • activebooleanoptional

    false pauses the monitor without deleting it.

Responses

  • 200The updated monitor
    • idstring

      Monitor ID, a UUID.

    • namestring | null

    • webhookUrlstring

    • topicsstring[]

      Changes watched.

    • marketsstring[]

      Countries where per-market topics are checked. Empty: each shop's primary market only.

    • activeboolean

      false: paused.

    • shopsobject[]

      Shops watched, in the order they were added.

      4 fields
      • idstring

        gid://shopify/Shop/<number>

      • namestring | null

      • domainstring | null

        Primary domain, without www.

      • myshopifyDomainstring

    • createdAtstring

      ISO 8601 date-time.

    • updatedAtstring

      ISO 8601 date-time.

    • secretstring

      Key the monitor's webhooks are signed with (Standard Webhooks). Keep it private.

  • 400A field is invalid, or a shop is not one ShopLens has
    • errorstring

      Always Invalid request.

    • issuesobject[]

      2 fields
      • pathstring

        The invalid parameter, such as limit.

      • messagestring

  • 401Missing, invalid or revoked API key
    • errorstring

      What went wrong.

  • 404No monitor with this id among yours
    • errorstring

      What went wrong.

Request
curl --request PATCH \
  --url "https://data.shoplens.dev/api/v1/monitors/3f6b2c1e-8a4d-4f7e-9b2a-6c1d0e5f7a90" \
  --header "Authorization: Bearer $SHOPLENS_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "active": false
  }'
{} Response
JSON
{
  "id": "3f6b2c1e-8a4d-4f7e-9b2a-6c1d0e5f7a90",
  "name": "Competitors",
  "webhookUrl": "https://example-app.com/webhooks/shoplens",
  "topics": [
    "product.created",
    "product.price_changed",
    "product.out_of_stock"
  ],
  "markets": ["US", "ES"],
  "active": false,
  "shops": [
    {
      "id": "gid://shopify/Shop/48213790",
      "name": "Example Shop",
      "domain": "example-shop.com",
      "myshopifyDomain": "example-shop.myshopify.com"
    },
    {
      "id": "gid://shopify/Shop/61029384",
      "name": "Another Example",
      "domain": "another-example.com",
      "myshopifyDomain": "another-example.myshopify.com"
    }
  ],
  "createdAt": "2026-10-30T09:12:44.000Z",
  "updatedAt": "2026-10-30T11:40:02.000Z",
  "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw…"
}

Endpoints

Delete a monitor

DELETE/api/v1/monitors/{id}Free

Deletes the monitor and stops its webhooks. To stop it for a while, set active to false instead.

Path parameters

  • idstringrequired

    The monitor's id.

Responses

  • 204Deleted

  • 400A parameter is invalid
    • errorstring

      Always Invalid request.

    • issuesobject[]

      2 fields
      • pathstring

        The invalid parameter, such as limit.

      • messagestring

  • 401Missing, invalid or revoked API key
    • errorstring

      What went wrong.

  • 404No monitor with this id among yours
    • errorstring

      What went wrong.

Request
curl --request DELETE \
  --url "https://data.shoplens.dev/api/v1/monitors/3f6b2c1e-8a4d-4f7e-9b2a-6c1d0e5f7a90" \
  --header "Authorization: Bearer $SHOPLENS_API_KEY"
{} Response
JSON
No body

Webhooks

Receiving webhooks

Webhooks follow the Standard Webhooks specification. Each change is one POST to the monitor's webhookUrl with a JSON body, signed with the monitor's secret. The Standard Webhooks libraries check the signature in one call. Compute it over the raw body, before you parse it.

Headers

  • webhook-id

    The event's id. It stays the same if the event is sent again, so use it to skip events you already handled.

  • webhook-timestamp

    When it was sent, in Unix seconds. Reject events older than a few minutes.

  • webhook-signature

    v1, and the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the base64-decoded part of the secret after whsec_.

Delivery

  • A webhook counts as delivered when your endpoint answers any 2xx within 10 seconds. Redirects are not followed.
  • One event covers one product or one collection. When several variants of a product change in the same check, they arrive together in data.variants.
  • One change can fire several topics. Putting a variant on sale fires product.sale_started, product.price_changed and product.compare_at_price_changed when the monitor watches all three, each as its own webhook. Watch only the topics you need.

Body

  • idstring

    Event ID, the same as the webhook-id header.

  • eventTypestring

    One of the topics. See each event below.

  • createdAtstring

    ISO 8601.

  • monitorobject

    The monitor that sent it.

    2 fields
    • idstring

    • namestring | null

  • shopobject

    The shop that changed.

    3 fields
    • idstring

      gid://shopify/Shop/<id>

    • domainstring | null

    • namestring | null

  • marketstring | null

    Country where the change was seen, for per-market topics; null for the others.

  • dataobject

    Depends on eventType. See each event below.

Product

  • idstring

    gid://shopify/Product/<id>

  • titlestring

  • handlestring

  • descriptionstring

    Plain text, without HTML.

  • shopUrlstring | null

    Public product page; null if the product is not on the online store.

  • featuredImagestring | null

    URL of the featured image.

  • merchantProductTypestring | null

    Product type as the merchant wrote it.

  • taxonomyCategorystring | null

    Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

Variant

Priced in the event's market, or in the shop's primary market when market is null.

  • idstring

    gid://shopify/ProductVariant/<id>

  • titlestring

    Such as M / White.

  • skustring | null

  • barcodestring | null

    Such as the UPC, EAN or ISBN.

  • pricestring

    Decimal, such as 39.00.

  • compareAtPricestring | null

    Decimal; null if the variant has none.

  • currencystring

    ISO 4217 currency of both prices.

  • availableForSaleboolean

    Whether it can be bought in the market.

Collection

  • idstring

    gid://shopify/Collection/<id>

  • titlestring

  • handlestring

  • shopUrlstring | null

    Public collection page; null if the collection is not on the online store.

Request
POST https://example-app.com/webhooks/shoplens
Content-Type: application/json
User-Agent: Shoplens-Webhooks/1.0
webhook-id: evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c
webhook-timestamp: 1762171200
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
Verify in Node.js
import { Webhook } from "standardwebhooks";

const webhook = new Webhook(process.env.SHOPLENS_WEBHOOK_SECRET);

// Throws if the signature or the timestamp is not valid
const event = webhook.verify(rawBody, headers);

Webhook event

product.created

A product is published in a market.

Checked in each of the monitor's markets.

data

  • productProduct

    The new product.

    8 fields
    • idstring

      gid://shopify/Product/<id>

    • titlestring

    • handlestring

    • descriptionstring

      Plain text, without HTML.

    • shopUrlstring | null

      Public product page; null if the product is not on the online store.

    • featuredImagestring | null

      URL of the featured image.

    • merchantProductTypestring | null

      Product type as the merchant wrote it.

    • taxonomyCategorystring | null

      Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

  • variantsVariant[]

    Every variant of the product.

    8 fields
    • idstring

      gid://shopify/ProductVariant/<id>

    • titlestring

      Such as M / White.

    • skustring | null

    • barcodestring | null

      Such as the UPC, EAN or ISBN.

    • pricestring

      Decimal, such as 39.00.

    • compareAtPricestring | null

      Decimal; null if the variant has none.

    • currencystring

      ISO 4217 currency of both prices.

    • availableForSaleboolean

      Whether it can be bought in the market.

product.createdSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "product.created",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": "US",
  "data": {
    "product": {
      "id": "gid://shopify/Product/8123456789012",
      "title": "Linen Shirt",
      "handle": "linen-shirt",
      "description": "Breathable linen shirt with a regular fit.",
      "shopUrl": "https://example-shop.com/products/linen-shirt",
      "featuredImage": "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
      "merchantProductType": "Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing > Clothing Tops > Shirts"
    },
    "variants": [
      {
        "id": "gid://shopify/ProductVariant/4412345678001",
        "title": "M / White",
        "sku": "LS-M-WH",
        "barcode": "840123456001",
        "price": "45.00",
        "compareAtPrice": null,
        "currency": "USD",
        "availableForSale": true
      },
      {
        "id": "gid://shopify/ProductVariant/4412345678002",
        "title": "L / White",
        "sku": "LS-L-WH",
        "barcode": "840123456002",
        "price": "45.00",
        "compareAtPrice": null,
        "currency": "USD",
        "availableForSale": true
      }
    ]
  }
}

Webhook event

product.deleted

A product is unpublished from a market or deleted.

Checked in each of the monitor's markets.

data

  • productProduct

    The product as it was last seen.

    8 fields
    • idstring

      gid://shopify/Product/<id>

    • titlestring

    • handlestring

    • descriptionstring

      Plain text, without HTML.

    • shopUrlstring | null

      Public product page; null if the product is not on the online store.

    • featuredImagestring | null

      URL of the featured image.

    • merchantProductTypestring | null

      Product type as the merchant wrote it.

    • taxonomyCategorystring | null

      Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

product.deletedSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "product.deleted",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": "US",
  "data": {
    "product": {
      "id": "gid://shopify/Product/8123456789012",
      "title": "Linen Shirt",
      "handle": "linen-shirt",
      "description": "Breathable linen shirt with a regular fit.",
      "shopUrl": "https://example-shop.com/products/linen-shirt",
      "featuredImage": "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
      "merchantProductType": "Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing > Clothing Tops > Shirts"
    }
  }
}

Webhook event

product.title_changed

A product's title changes.

The same in every market, so market is null.

data

  • productProduct

    The product, with its new title.

    8 fields
    • idstring

      gid://shopify/Product/<id>

    • titlestring

    • handlestring

    • descriptionstring

      Plain text, without HTML.

    • shopUrlstring | null

      Public product page; null if the product is not on the online store.

    • featuredImagestring | null

      URL of the featured image.

    • merchantProductTypestring | null

      Product type as the merchant wrote it.

    • taxonomyCategorystring | null

      Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

  • previousobject

    The title before the change.

    1 field
    • titlestring

  • currentobject

    The title now.

    1 field
    • titlestring

product.title_changedSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "product.title_changed",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": null,
  "data": {
    "product": {
      "id": "gid://shopify/Product/8123456789012",
      "title": "Relaxed Linen Shirt",
      "handle": "linen-shirt",
      "description": "Breathable linen shirt with a regular fit.",
      "shopUrl": "https://example-shop.com/products/linen-shirt",
      "featuredImage": "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
      "merchantProductType": "Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing > Clothing Tops > Shirts"
    },
    "previous": { "title": "Linen Shirt" },
    "current": { "title": "Relaxed Linen Shirt" }
  }
}

Webhook event

product.description_changed

A product's description changes.

The same in every market, so market is null.

data

  • productProduct

    The product, with its new description.

    8 fields
    • idstring

      gid://shopify/Product/<id>

    • titlestring

    • handlestring

    • descriptionstring

      Plain text, without HTML.

    • shopUrlstring | null

      Public product page; null if the product is not on the online store.

    • featuredImagestring | null

      URL of the featured image.

    • merchantProductTypestring | null

      Product type as the merchant wrote it.

    • taxonomyCategorystring | null

      Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

  • previousobject

    The description before the change, as plain text.

    1 field
    • descriptionstring

  • currentobject

    The description now, as plain text.

    1 field
    • descriptionstring

product.description_changedSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "product.description_changed",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": null,
  "data": {
    "product": {
      "id": "gid://shopify/Product/8123456789012",
      "title": "Linen Shirt",
      "handle": "linen-shirt",
      "description": "Breathable linen shirt with a relaxed fit. Made in Portugal.",
      "shopUrl": "https://example-shop.com/products/linen-shirt",
      "featuredImage": "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
      "merchantProductType": "Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing > Clothing Tops > Shirts"
    },
    "previous": {
      "description": "Breathable linen shirt with a regular fit."
    },
    "current": {
      "description": "Breathable linen shirt with a relaxed fit. Made in Portugal."
    }
  }
}

Webhook event

product.images_changed

A product's images are added, removed or reordered.

The same in every market, so market is null.

data

  • productProduct

    The product, with its new featured image.

    8 fields
    • idstring

      gid://shopify/Product/<id>

    • titlestring

    • handlestring

    • descriptionstring

      Plain text, without HTML.

    • shopUrlstring | null

      Public product page; null if the product is not on the online store.

    • featuredImagestring | null

      URL of the featured image.

    • merchantProductTypestring | null

      Product type as the merchant wrote it.

    • taxonomyCategorystring | null

      Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

  • previousobject

    The images before the change.

    1 field
    • imagesstring[]

      Image URLs, in order.

  • currentobject

    The images now.

    1 field
    • imagesstring[]

      Image URLs, in order.

product.images_changedSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "product.images_changed",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": null,
  "data": {
    "product": {
      "id": "gid://shopify/Product/8123456789012",
      "title": "Linen Shirt",
      "handle": "linen-shirt",
      "description": "Breathable linen shirt with a regular fit.",
      "shopUrl": "https://example-shop.com/products/linen-shirt",
      "featuredImage": "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
      "merchantProductType": "Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing > Clothing Tops > Shirts"
    },
    "previous": {
      "images": [
        "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
        "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-back.jpg"
      ]
    },
    "current": {
      "images": [
        "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
        "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-detail.jpg",
        "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-back.jpg"
      ]
    }
  }
}

Webhook event

product.price_changed

The price of one or more of a product's variants changes in a market.

Checked in each of the monitor's markets.

data

  • productProduct

    The product.

    8 fields
    • idstring

      gid://shopify/Product/<id>

    • titlestring

    • handlestring

    • descriptionstring

      Plain text, without HTML.

    • shopUrlstring | null

      Public product page; null if the product is not on the online store.

    • featuredImagestring | null

      URL of the featured image.

    • merchantProductTypestring | null

      Product type as the merchant wrote it.

    • taxonomyCategorystring | null

      Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

  • variantsVariant[]

    The variants whose price changed, as they are now. Each one adds previous.price, its price before the change.

    9 fields
    • idstring

      gid://shopify/ProductVariant/<id>

    • titlestring

      Such as M / White.

    • skustring | null

    • barcodestring | null

      Such as the UPC, EAN or ISBN.

    • pricestring

      Decimal, such as 39.00.

    • compareAtPricestring | null

      Decimal; null if the variant has none.

    • currencystring

      ISO 4217 currency of both prices.

    • availableForSaleboolean

      Whether it can be bought in the market.

    • previousobject

      1 field
      • pricestring

        Price before the change.

product.price_changedSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "product.price_changed",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": "US",
  "data": {
    "product": {
      "id": "gid://shopify/Product/8123456789012",
      "title": "Linen Shirt",
      "handle": "linen-shirt",
      "description": "Breathable linen shirt with a regular fit.",
      "shopUrl": "https://example-shop.com/products/linen-shirt",
      "featuredImage": "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
      "merchantProductType": "Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing > Clothing Tops > Shirts"
    },
    "variants": [
      {
        "id": "gid://shopify/ProductVariant/4412345678001",
        "title": "M / White",
        "sku": "LS-M-WH",
        "barcode": "840123456001",
        "price": "39.00",
        "compareAtPrice": null,
        "currency": "USD",
        "availableForSale": true,
        "previous": { "price": "45.00" }
      }
    ]
  }
}

Webhook event

product.compare_at_price_changed

The compare-at price of one or more of a product's variants changes in a market.

Checked in each of the monitor's markets.

data

  • productProduct

    The product.

    8 fields
    • idstring

      gid://shopify/Product/<id>

    • titlestring

    • handlestring

    • descriptionstring

      Plain text, without HTML.

    • shopUrlstring | null

      Public product page; null if the product is not on the online store.

    • featuredImagestring | null

      URL of the featured image.

    • merchantProductTypestring | null

      Product type as the merchant wrote it.

    • taxonomyCategorystring | null

      Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

  • variantsVariant[]

    The variants whose compare-at price changed, as they are now. Each one adds previous.compareAtPrice, its compare-at price before the change.

    9 fields
    • idstring

      gid://shopify/ProductVariant/<id>

    • titlestring

      Such as M / White.

    • skustring | null

    • barcodestring | null

      Such as the UPC, EAN or ISBN.

    • pricestring

      Decimal, such as 39.00.

    • compareAtPricestring | null

      Decimal; null if the variant has none.

    • currencystring

      ISO 4217 currency of both prices.

    • availableForSaleboolean

      Whether it can be bought in the market.

    • previousobject

      1 field
      • compareAtPricestring | null

        Compare-at price before the change.

product.compare_at_price_changedSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "product.compare_at_price_changed",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": "US",
  "data": {
    "product": {
      "id": "gid://shopify/Product/8123456789012",
      "title": "Linen Shirt",
      "handle": "linen-shirt",
      "description": "Breathable linen shirt with a regular fit.",
      "shopUrl": "https://example-shop.com/products/linen-shirt",
      "featuredImage": "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
      "merchantProductType": "Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing > Clothing Tops > Shirts"
    },
    "variants": [
      {
        "id": "gid://shopify/ProductVariant/4412345678001",
        "title": "M / White",
        "sku": "LS-M-WH",
        "barcode": "840123456001",
        "price": "39.00",
        "compareAtPrice": "59.00",
        "currency": "USD",
        "availableForSale": true,
        "previous": { "compareAtPrice": "49.00" }
      }
    ]
  }
}

Webhook event

product.sale_started

One or more of a product's variants go on sale in a market: their compare-at price becomes higher than their price.

Checked in each of the monitor's markets.

data

  • productProduct

    The product.

    8 fields
    • idstring

      gid://shopify/Product/<id>

    • titlestring

    • handlestring

    • descriptionstring

      Plain text, without HTML.

    • shopUrlstring | null

      Public product page; null if the product is not on the online store.

    • featuredImagestring | null

      URL of the featured image.

    • merchantProductTypestring | null

      Product type as the merchant wrote it.

    • taxonomyCategorystring | null

      Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

  • variantsVariant[]

    The variants now on sale, as they are now. Each one adds previous, its prices before the sale, so you can tell a real price drop from a raised compare-at price.

    9 fields
    • idstring

      gid://shopify/ProductVariant/<id>

    • titlestring

      Such as M / White.

    • skustring | null

    • barcodestring | null

      Such as the UPC, EAN or ISBN.

    • pricestring

      Decimal, such as 39.00.

    • compareAtPricestring | null

      Decimal; null if the variant has none.

    • currencystring

      ISO 4217 currency of both prices.

    • availableForSaleboolean

      Whether it can be bought in the market.

    • previousobject

      2 fields
      • pricestring

        Price before the change.

      • compareAtPricestring | null

        Compare-at price before the change.

product.sale_startedSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "product.sale_started",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": "US",
  "data": {
    "product": {
      "id": "gid://shopify/Product/8123456789012",
      "title": "Linen Shirt",
      "handle": "linen-shirt",
      "description": "Breathable linen shirt with a regular fit.",
      "shopUrl": "https://example-shop.com/products/linen-shirt",
      "featuredImage": "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
      "merchantProductType": "Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing > Clothing Tops > Shirts"
    },
    "variants": [
      {
        "id": "gid://shopify/ProductVariant/4412345678001",
        "title": "M / White",
        "sku": "LS-M-WH",
        "barcode": "840123456001",
        "price": "39.00",
        "compareAtPrice": "45.00",
        "currency": "USD",
        "availableForSale": true,
        "previous": { "price": "45.00", "compareAtPrice": null }
      }
    ]
  }
}

Webhook event

product.sale_ended

One or more of a product's variants stop being on sale in a market.

Checked in each of the monitor's markets.

data

  • productProduct

    The product.

    8 fields
    • idstring

      gid://shopify/Product/<id>

    • titlestring

    • handlestring

    • descriptionstring

      Plain text, without HTML.

    • shopUrlstring | null

      Public product page; null if the product is not on the online store.

    • featuredImagestring | null

      URL of the featured image.

    • merchantProductTypestring | null

      Product type as the merchant wrote it.

    • taxonomyCategorystring | null

      Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

  • variantsVariant[]

    The variants no longer on sale, as they are now. Each one adds previous, its prices during the sale.

    9 fields
    • idstring

      gid://shopify/ProductVariant/<id>

    • titlestring

      Such as M / White.

    • skustring | null

    • barcodestring | null

      Such as the UPC, EAN or ISBN.

    • pricestring

      Decimal, such as 39.00.

    • compareAtPricestring | null

      Decimal; null if the variant has none.

    • currencystring

      ISO 4217 currency of both prices.

    • availableForSaleboolean

      Whether it can be bought in the market.

    • previousobject

      2 fields
      • pricestring

        Price before the change.

      • compareAtPricestring | null

        Compare-at price before the change.

product.sale_endedSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "product.sale_ended",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": "US",
  "data": {
    "product": {
      "id": "gid://shopify/Product/8123456789012",
      "title": "Linen Shirt",
      "handle": "linen-shirt",
      "description": "Breathable linen shirt with a regular fit.",
      "shopUrl": "https://example-shop.com/products/linen-shirt",
      "featuredImage": "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
      "merchantProductType": "Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing > Clothing Tops > Shirts"
    },
    "variants": [
      {
        "id": "gid://shopify/ProductVariant/4412345678001",
        "title": "M / White",
        "sku": "LS-M-WH",
        "barcode": "840123456001",
        "price": "45.00",
        "compareAtPrice": null,
        "currency": "USD",
        "availableForSale": true,
        "previous": { "price": "39.00", "compareAtPrice": "45.00" }
      }
    ]
  }
}

Webhook event

product.out_of_stock

One or more of a product's variants sell out in a market.

Checked in each of the monitor's markets.

data

  • productProduct

    The product.

    8 fields
    • idstring

      gid://shopify/Product/<id>

    • titlestring

    • handlestring

    • descriptionstring

      Plain text, without HTML.

    • shopUrlstring | null

      Public product page; null if the product is not on the online store.

    • featuredImagestring | null

      URL of the featured image.

    • merchantProductTypestring | null

      Product type as the merchant wrote it.

    • taxonomyCategorystring | null

      Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

  • variantsVariant[]

    The variants that sold out.

    8 fields
    • idstring

      gid://shopify/ProductVariant/<id>

    • titlestring

      Such as M / White.

    • skustring | null

    • barcodestring | null

      Such as the UPC, EAN or ISBN.

    • pricestring

      Decimal, such as 39.00.

    • compareAtPricestring | null

      Decimal; null if the variant has none.

    • currencystring

      ISO 4217 currency of both prices.

    • availableForSaleboolean

      Whether it can be bought in the market.

product.out_of_stockSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "product.out_of_stock",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": "US",
  "data": {
    "product": {
      "id": "gid://shopify/Product/8123456789012",
      "title": "Linen Shirt",
      "handle": "linen-shirt",
      "description": "Breathable linen shirt with a regular fit.",
      "shopUrl": "https://example-shop.com/products/linen-shirt",
      "featuredImage": "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
      "merchantProductType": "Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing > Clothing Tops > Shirts"
    },
    "variants": [
      {
        "id": "gid://shopify/ProductVariant/4412345678002",
        "title": "L / White",
        "sku": "LS-L-WH",
        "barcode": "840123456002",
        "price": "45.00",
        "compareAtPrice": null,
        "currency": "USD",
        "availableForSale": false
      }
    ]
  }
}

Webhook event

product.back_in_stock

One or more sold-out variants of a product can be bought again in a market.

Checked in each of the monitor's markets.

data

  • productProduct

    The product.

    8 fields
    • idstring

      gid://shopify/Product/<id>

    • titlestring

    • handlestring

    • descriptionstring

      Plain text, without HTML.

    • shopUrlstring | null

      Public product page; null if the product is not on the online store.

    • featuredImagestring | null

      URL of the featured image.

    • merchantProductTypestring | null

      Product type as the merchant wrote it.

    • taxonomyCategorystring | null

      Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

  • variantsVariant[]

    The variants available again.

    8 fields
    • idstring

      gid://shopify/ProductVariant/<id>

    • titlestring

      Such as M / White.

    • skustring | null

    • barcodestring | null

      Such as the UPC, EAN or ISBN.

    • pricestring

      Decimal, such as 39.00.

    • compareAtPricestring | null

      Decimal; null if the variant has none.

    • currencystring

      ISO 4217 currency of both prices.

    • availableForSaleboolean

      Whether it can be bought in the market.

product.back_in_stockSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "product.back_in_stock",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": "US",
  "data": {
    "product": {
      "id": "gid://shopify/Product/8123456789012",
      "title": "Linen Shirt",
      "handle": "linen-shirt",
      "description": "Breathable linen shirt with a regular fit.",
      "shopUrl": "https://example-shop.com/products/linen-shirt",
      "featuredImage": "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
      "merchantProductType": "Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing > Clothing Tops > Shirts"
    },
    "variants": [
      {
        "id": "gid://shopify/ProductVariant/4412345678002",
        "title": "L / White",
        "sku": "LS-L-WH",
        "barcode": "840123456002",
        "price": "45.00",
        "compareAtPrice": null,
        "currency": "USD",
        "availableForSale": true
      }
    ]
  }
}

Webhook event

variant.created

A product gets one or more new variants, such as a new size or color.

The same in every market, so market is null.

data

  • productProduct

    The product.

    8 fields
    • idstring

      gid://shopify/Product/<id>

    • titlestring

    • handlestring

    • descriptionstring

      Plain text, without HTML.

    • shopUrlstring | null

      Public product page; null if the product is not on the online store.

    • featuredImagestring | null

      URL of the featured image.

    • merchantProductTypestring | null

      Product type as the merchant wrote it.

    • taxonomyCategorystring | null

      Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

  • variantsVariant[]

    The new variants, priced in the shop's primary market.

    8 fields
    • idstring

      gid://shopify/ProductVariant/<id>

    • titlestring

      Such as M / White.

    • skustring | null

    • barcodestring | null

      Such as the UPC, EAN or ISBN.

    • pricestring

      Decimal, such as 39.00.

    • compareAtPricestring | null

      Decimal; null if the variant has none.

    • currencystring

      ISO 4217 currency of both prices.

    • availableForSaleboolean

      Whether it can be bought in the market.

variant.createdSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "variant.created",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": null,
  "data": {
    "product": {
      "id": "gid://shopify/Product/8123456789012",
      "title": "Linen Shirt",
      "handle": "linen-shirt",
      "description": "Breathable linen shirt with a regular fit.",
      "shopUrl": "https://example-shop.com/products/linen-shirt",
      "featuredImage": "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
      "merchantProductType": "Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing > Clothing Tops > Shirts"
    },
    "variants": [
      {
        "id": "gid://shopify/ProductVariant/4412345678003",
        "title": "XL / White",
        "sku": "LS-XL-WH",
        "barcode": "840123456003",
        "price": "45.00",
        "compareAtPrice": null,
        "currency": "USD",
        "availableForSale": true
      }
    ]
  }
}

Webhook event

variant.deleted

One or more variants are removed from a product.

The same in every market, so market is null.

data

  • productProduct

    The product.

    8 fields
    • idstring

      gid://shopify/Product/<id>

    • titlestring

    • handlestring

    • descriptionstring

      Plain text, without HTML.

    • shopUrlstring | null

      Public product page; null if the product is not on the online store.

    • featuredImagestring | null

      URL of the featured image.

    • merchantProductTypestring | null

      Product type as the merchant wrote it.

    • taxonomyCategorystring | null

      Readable Shopify Product Taxonomy path, such as Apparel & Accessories > Clothing.

  • variantsVariant[]

    The removed variants, as they were last seen.

    8 fields
    • idstring

      gid://shopify/ProductVariant/<id>

    • titlestring

      Such as M / White.

    • skustring | null

    • barcodestring | null

      Such as the UPC, EAN or ISBN.

    • pricestring

      Decimal, such as 39.00.

    • compareAtPricestring | null

      Decimal; null if the variant has none.

    • currencystring

      ISO 4217 currency of both prices.

    • availableForSaleboolean

      Whether it can be bought in the market.

variant.deletedSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "variant.deleted",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": null,
  "data": {
    "product": {
      "id": "gid://shopify/Product/8123456789012",
      "title": "Linen Shirt",
      "handle": "linen-shirt",
      "description": "Breathable linen shirt with a regular fit.",
      "shopUrl": "https://example-shop.com/products/linen-shirt",
      "featuredImage": "https://cdn.shopify.com/s/files/1/0612/3456/7890/files/linen-shirt-white.jpg",
      "merchantProductType": "Shirts",
      "taxonomyCategory": "Apparel & Accessories > Clothing > Clothing Tops > Shirts"
    },
    "variants": [
      {
        "id": "gid://shopify/ProductVariant/4412345678004",
        "title": "S / Sand",
        "sku": "LS-S-SD",
        "barcode": "840123456004",
        "price": "45.00",
        "compareAtPrice": null,
        "currency": "USD",
        "availableForSale": true
      }
    ]
  }
}

Webhook event

collection.created

A collection is published.

The same in every market, so market is null.

data

  • collectionCollection

    The new collection.

    4 fields
    • idstring

      gid://shopify/Collection/<id>

    • titlestring

    • handlestring

    • shopUrlstring | null

      Public collection page; null if the collection is not on the online store.

collection.createdSample payload
{
  "id": "evt_3f9a1c0b7d2e4a5f8b6c1d0e9f2a3b4c",
  "eventType": "collection.created",
  "createdAt": "2026-11-03T08:00:00.000Z",
  "monitor": {
    "id": "6f1c2a4e-8b3d-4c5e-9a7f-1d2e3f4a5b6c",
    "name": "Competitors"
  },
  "shop": {
    "id": "gid://shopify/Shop/61234567890",
    "domain": "example-shop.com",
    "name": "Example Shop"
  },
  "market": null,
  "data": {
    "collection": {
      "id": "gid://shopify/Collection/301234567890",
      "title": "Summer Linen",
      "handle": "summer-linen",
      "shopUrl": "https://example-shop.com/collections/summer-linen"
    }
  }
}

AI agents

MCP server

AI assistants get the same data from a remote MCP server (streamable HTTP, stateless). It exposes a few tools built around tasks, rather than one tool per endpoint. Tool calls spend credits from the same balance as the REST API, at the same price as the endpoint each tool calls, and follow the same rules: only successful calls are charged.

It signs you in with OAuth, following the MCP authorization spec. Add the server URL to your MCP client and it opens the ShopLens dashboard for you to sign in. No API key goes in the client's config.

A request without a valid access token answers 401 with a WWW-Authenticate header that points to /.well-known/oauth-protected-resource/mcp, which names the authorization server.

Tools

  • search_shops1 credit per shop returned

    Search and rank shops, with the same filters as GET /shops.

  • get_shop1 credit

    A shop's details, best sellers, product types and similar shops. For a shop not indexed yet, it asks to retry in a few minutes.

  • get_shop_ads40 credits

    A shop's ads on Meta, Google and TikTok, the 20 most recent per platform.

  • list_shop_platform_ads40 credits per page

    Every ad of a shop on one platform, paginated, with the same filters as GET /shops/{domain}/ads/{platform}.

  • get_shop_people5 credits

    People at the shop's company, paginated.

  • reveal_shop_person25 credits, 150 with a phone

    Reveal one person's email, LinkedIn and optionally phone. Only for explicit requests about one person.

Server URL
https://data.shoplens.dev/mcp