# Installgraph API documentation > Every page of https://installgraph.com/docs in one file (25 pages, generated 2026-11-10). OpenAPI 3.1: https://installgraph.com/openapi.json --- # Installgraph API The Installgraph API tells you which apps a Shopify store runs and which stores run an app, over **16** JSON endpoints at `https://api.installgraph.com`; 1 credit unlocks 1 store, and a company email gets up to **50** free credits. ## Quickstart You need a work email address. A browser is enough; a terminal is optional. This takes about two minutes. ### 1. Get a key [Start free](https://installgraph.com/login) with your email and open the sign-in link we send. Your key is step 1 on your [dashboard](https://installgraph.com/account); copy it then, as it is shown once. With a company email you start with 25 free credits, and 25 more once the link is clicked (once per company domain). Keep the key in an environment variable: ```bash export INSTALLGRAPH_API_KEY="sfs_…" ``` ### 2. Count before you pay Counts are free. How many UK stores run Klaviyo? ```bash curl "https://api.installgraph.com/v1/apps/klaviyo/companies/count?country=GB" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` ```json { "data": { "app": "klaviyo", "count": 1284 }, "meta": { "creditsUsed": 0, "creditsRemaining": 50 } } ``` ### 3. Pull the rows you need Each store in the page costs 1 credit. `limit` caps the spend. ```bash curl "https://api.installgraph.com/v1/apps/klaviyo/companies?country=GB&limit=10" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` The same call in JavaScript (Node 18+) and Python: ```js const res = await fetch("https://api.installgraph.com/v1/apps/klaviyo/companies?country=GB&limit=10", { headers: { Authorization: `Bearer ${process.env.INSTALLGRAPH_API_KEY}` }, }); const { data, pagination, meta } = await res.json(); console.log(data.map((s) => s.domain), meta.creditsRemaining); ``` ```python import os, requests res = requests.get( "https://api.installgraph.com/v1/apps/klaviyo/companies", params={"country": "GB", "limit": 10}, headers={"Authorization": f"Bearer {os.environ['INSTALLGRAPH_API_KEY']}"}, ) body = res.json() print([s["domain"] for s in body["data"]], body["meta"]["creditsRemaining"]) ``` ### No code: a CSV in your browser Add `.csv` to any endpoint and your key as `?key=`, and open the URL. The browser downloads a spreadsheet, charged the same as the JSON: ``` https://api.installgraph.com/v1/apps/klaviyo/companies.csv?country=GB&limit=10&key=sfs_… ``` Nested fields become columns (`industry.category`), and lists are joined with "; ". `limit` (up to 500) still caps the spend; the next page's cursor is in the `X-Next-Cursor` response header. To look up one store instead: [`GET /v1/companies/{domain}`](https://installgraph.com/docs/reference/get-company.md). ## Conventions - **Base URL:** `https://api.installgraph.com`. Every path starts with `/v1`. HTTPS only. - **Auth:** `Authorization: Bearer sfs_…` (or `X-API-Key`) from code, or `?key=sfs_…` on a URL in a browser. See [Authentication](https://installgraph.com/docs/authentication.md). - **CSV:** add `.csv` to any endpoint's path for a spreadsheet instead of JSON. - **Responses:** JSON. A success is `{ "data", "meta" }`; lists add `"pagination"`. An error is `{ "error": { "code", "message", "requestId" } }`. See [Errors](https://installgraph.com/docs/errors.md). - **Times** are ISO 8601 in UTC (`2026-11-02T03:40:12.000Z`). **Countries** are ISO 3166-1 alpha-2 (`GB`). - **Missing values** are `null`, never omitted: every documented field is always present. - **Freshness:** each store's homepage is scanned once a week. See [Data model](https://installgraph.com/docs/data-model.md). - **Stability:** within `v1`, fields and error codes are added but never renamed or removed. Ignore fields you do not recognise. ## Endpoints | Endpoint | What it returns | Cost | | --- | --- | --- | | [`GET /v1/companies/{domain}`](https://installgraph.com/docs/reference/get-company.md) | A store's current stack | 1 credit | | [`GET /v1/apps/{slug}/companies`](https://installgraph.com/docs/reference/list-app-companies.md) | Stores running an app | 1 credit per store | | [`GET /v1/apps/{slug}/companies/count`](https://installgraph.com/docs/reference/count-app-companies.md) | How many stores run an app (free) | Free | | [`GET /v1/events`](https://installgraph.com/docs/reference/list-events.md) | Installs and removals | 1 credit per store | | [`POST /v1/webhooks`](https://installgraph.com/docs/reference/create-webhook.md) | Subscribe to an app's installs or removals | 10 credits per change delivered | | [`GET /v1/webhooks`](https://installgraph.com/docs/reference/list-webhooks.md) | Your webhooks | Free | | [`GET /v1/webhooks/{id}`](https://installgraph.com/docs/reference/get-webhook.md) | One webhook | Free | | [`DELETE /v1/webhooks/{id}`](https://installgraph.com/docs/reference/delete-webhook.md) | Delete a webhook | Free | | [`GET /v1/webhooks/{id}/deliveries`](https://installgraph.com/docs/reference/list-webhook-deliveries.md) | Recent deliveries | Free | | [`POST /v1/webhooks/{id}/test`](https://installgraph.com/docs/reference/test-webhook.md) | Send a sample delivery now | Free | | [`GET /v1/apps`](https://installgraph.com/docs/reference/list-apps.md) | The app catalogue | Free | | [`GET /v1/industries`](https://installgraph.com/docs/reference/list-industries.md) | The industry taxonomy, regions and order-value bands | Free | | [`GET /v1/public/stats`](https://installgraph.com/docs/reference/get-public-stats.md) | App popularity by category, region and industry | Free | | [`GET /v1/usage`](https://installgraph.com/docs/reference/get-usage.md) | Your balance and recent usage | Free | | [`GET /v1/pricing`](https://installgraph.com/docs/reference/get-pricing.md) | Prices, packs and rules (no key) | Free | | [`POST /v1/billing/purchase`](https://installgraph.com/docs/reference/agent-purchase.md) | Buy a credit pack on the saved card | Pack price | ## Guides - [Authentication](https://installgraph.com/docs/authentication.md): getting a key, sending it, scopes. - [Credits and billing](https://installgraph.com/docs/credits.md): what costs what, free re-pulls, running out. - [Pagination](https://installgraph.com/docs/pagination.md): cursors and a loop that reads a whole list. - [Rate limits](https://installgraph.com/docs/rate-limits.md): 60 requests a minute, headers, retrying. - [Errors](https://installgraph.com/docs/errors.md): every code, and what to do about it. - [Data model](https://installgraph.com/docs/data-model.md): how detection works, store status, industries, regions. - [Using the API from an AI agent](https://installgraph.com/docs/agents.md): machine-readable docs and a safe spending loop. ## Machine-readable - [OpenAPI 3.1 spec](https://installgraph.com/openapi.json): every public endpoint, schema and example. - [llms.txt](https://installgraph.com/llms.txt) and [llms-full.txt](https://installgraph.com/llms-full.txt): an index, and every docs page in one Markdown file. - Every docs page as Markdown: add `.md` to its URL, for example [/docs/authentication.md](https://installgraph.com/docs/authentication.md). ## Support Quote the `requestId` from the response (also in the `X-Request-Id` header). For more than $1,000 or 40,000 stores in 30 days, the full dataset, or a weekly changes feed: [Talk to us](https://installgraph.com/talk-to-us). Source: https://installgraph.com/docs/quickstart --- # Authentication Every request except two keyless ones sends an API key, `sfs_` followed by **32** letters and digits, as `Authorization: Bearer sfs_…`. ## Get a key Sign in at [installgraph.com/login](https://installgraph.com/login) (an emailed link, no password). Your key is on your [dashboard](https://installgraph.com/account). New accounts are made in the browser only: at most 3 per IP per day, and disposable addresses are refused. Free credits need a company email: 25 at sign-up and 25 more once the link is clicked (once per company domain); webmail gets none. A key is shown once. Every key on an account shares the account's credit balance. Lost it? Regenerate it on the dashboard: that revokes the old one. ## Send it ```bash curl "https://api.installgraph.com/v1/usage" -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` `X-API-Key: sfs_…` works too. In a browser, add the key to the URL instead: `https://api.installgraph.com/v1/usage?key=sfs_…`. A header wins if both are sent. ## Keep it secret - A key in a URL (`?key=`) is fine for your own browser, but it stays in your browser history: don't share those links. - Never put the key in a web page or app you publish: anyone can read it there and spend your credits. From code, use the header. - Keep it in an environment variable or secret store, not in source control. The `sfs_` prefix lets secret scanners spot a leaked key. - If a key leaks, revoke it on your account page and make a new one. Revocation takes effect on the next request. ## Scopes | Scope | Endpoints | | --- | --- | | `companies:read` | [`GET /v1/companies/{domain}`](https://installgraph.com/docs/reference/get-company.md), [`GET /v1/apps/{slug}/companies`](https://installgraph.com/docs/reference/list-app-companies.md), [`GET /v1/apps/{slug}/companies/count`](https://installgraph.com/docs/reference/count-app-companies.md) | | `apps:read` | [`GET /v1/apps`](https://installgraph.com/docs/reference/list-apps.md) | | `events:read` | [`GET /v1/events`](https://installgraph.com/docs/reference/list-events.md) | | none | [`GET /v1/usage`](https://installgraph.com/docs/reference/get-usage.md), [`GET /v1/industries`](https://installgraph.com/docs/reference/list-industries.md), [`POST /v1/billing/purchase`](https://installgraph.com/docs/reference/agent-purchase.md) | Keys made at sign-up have all three scopes. [`GET /v1/usage`](https://installgraph.com/docs/reference/get-usage.md) shows a key's scopes. ## No key needed [`GET /v1/pricing`](https://installgraph.com/docs/reference/get-pricing.md) and [`GET /v1/public/stats`](https://installgraph.com/docs/reference/get-public-stats.md) take no key. ## Errors | Status | Code | Meaning | | --- | --- | --- | | 401 | `missing_api_key` | No key was sent. | | 401 | `invalid_api_key` | The key is not one we issued. | | 401 | `api_key_revoked` | The key was revoked. | | 403 | `insufficient_scope` | The key lacks this endpoint's scope. | | 403 | `api_key_suspended` | The key is suspended. | Source: https://installgraph.com/docs/authentication --- # Credits and billing **1 credit** unlocks 1 store: a row in an app's list, or a store's full app stack; counts are free, re-pulls within **7 days** are free, and you pay only for stores found. ## What costs what | Request | Credits | | --- | --- | | [`GET /v1/apps/{slug}/companies`](https://installgraph.com/docs/reference/list-app-companies.md) | 1 per store in the page | | [`GET /v1/companies/{domain}`](https://installgraph.com/docs/reference/get-company.md) | 1 per store found; `include=history` adds 1 | | [`GET /v1/events`](https://installgraph.com/docs/reference/list-events.md) | 1 per distinct store in the page | | [`POST /v1/webhooks`](https://installgraph.com/docs/reference/create-webhook.md) | 10 per change delivered; creating, listing and testing are free ([Webhooks](https://installgraph.com/docs/webhooks.md)) | | [`GET /v1/apps/{slug}/companies/count`](https://installgraph.com/docs/reference/count-app-companies.md) | Free | | [`GET /v1/apps`](https://installgraph.com/docs/reference/list-apps.md), [`GET /v1/industries`](https://installgraph.com/docs/reference/list-industries.md), [`GET /v1/usage`](https://installgraph.com/docs/reference/get-usage.md), [`GET /v1/pricing`](https://installgraph.com/docs/reference/get-pricing.md) | Free | [`GET /v1/pricing`](https://installgraph.com/docs/reference/get-pricing.md) returns this table as JSON, with no key. ## Rules 1. **Charged only for stores found.** An empty page or an error costs nothing. 2. **Re-pulls are free for 7 days.** The same store for the same kind of request (a row from the same app's list, a lookup, or history) is free again within 7 days; `meta.freeRepulls` counts them. Scans are weekly, so a re-pull inside that window returns the same data. 3. **Counts are free and unlimited**, so narrow a query before you pay for rows. 4. **Store lookups that miss** (`404`) are free up to 2 × your lookups that found a store in the last 30 days + 100, then cost 1 credit each. 5. **A page the balance cannot fully cover is cut** to the rows you can pay for; `pagination.nextCursor` resumes after the last one. 6. **Credits expire after 12 months.** Each credit expires 12 months after it was added, and the oldest are spent first. They are non-refundable and not transferable. ## Seeing what you spent Every response carries: | Header | Body | Meaning | | --- | --- | --- | | `X-Credits-Charged` | `meta.creditsUsed` | Credits this request cost | | `X-Credits-Remaining` | `meta.creditsRemaining` | Balance afterwards (`null` for unlimited keys) | | | `meta.freeRepulls` | Stores served free as re-pulls | [`GET /v1/usage`](https://installgraph.com/docs/reference/get-usage.md) shows the balance and the last 30 days by endpoint. ## Packs | Credits | Price | $ per 1,000 stores | Pack id (API) | | ---: | ---: | ---: | --- | | 500 | $50 | $100 | `starter` | | 2,500 | $150 | $60 | `growth` | | 10,000 | $400 | $40 | `pro` | | 40,000 | $1,000 | $25 | `scale` | A company email gets up to 50 free credits: 25 at sign-up, 25 when the email is verified (once per company domain). Webmail gets none. Buy on your [account page](https://installgraph.com/account) (Stripe Checkout). Auto top-up, off by default, rebuys a chosen pack on the saved card when the balance runs low. Full detail: [Pricing](https://installgraph.com/pricing). ## Limits Per account per rolling 30 days: 40,000 distinct stores (`402 usage_cap_reached`) and $1,000 of purchases (`402 spend_cap_reached`). For more, [talk to us](https://installgraph.com/talk-to-us). ## Running out A billed request the balance cannot cover answers `402 insufficient_credits` and charges nothing. The error carries `offers`, so a script or agent can act on it: ```json { "error": { "code": "insufficient_credits", "message": "Not enough credits for this request (balance 0). Buy a credit pack, or turn on auto top-up.", "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "balance": 0, "offers": { "pack": { "id": "starter", "name": "Starter", "priceUsd": 50, "credits": 500, "buyUrl": "https://installgraph.com/account?buy=starter" }, "talkToUs": { "url": "https://installgraph.com/talk-to-us", "for": "More than $1,000 or 40,000 stores in 30 days, the full dataset, or a changes feed." }, "pricing": "https://installgraph.com/pricing" } } } ``` Either send a person to `offers.pack.buyUrl`, or, if the account owner has turned on agent purchases, buy with [`POST /v1/billing/purchase`](https://installgraph.com/docs/reference/agent-purchase.md). Source: https://installgraph.com/docs/credits --- # Pagination List endpoints return up to **500** rows a page (default **100**) and a `pagination.nextCursor` you pass back to get the next page, until it is `null`. ## How it works [`GET /v1/apps`](https://installgraph.com/docs/reference/list-apps.md), [`GET /v1/apps/{slug}/companies`](https://installgraph.com/docs/reference/list-app-companies.md) and [`GET /v1/events`](https://installgraph.com/docs/reference/list-events.md) take: | Parameter | Meaning | | --- | --- | | `limit` | Rows per page, 1–500, default 100. On billed lists it is also your spend cap per page. | | `cursor` | `pagination.nextCursor` from the previous page, unchanged. | and return: ```json "pagination": { "nextCursor": "eyJrIjoi…", "hasMore": true } ``` - Send the **same filters** with every page. A cursor is a position, not an offset: rows added while you page do not shift or repeat earlier rows. - A page can hold **fewer rows than `limit` while `hasMore` is true** (filters applied after the scan, or a balance that covered only part of the page). Keep going until `nextCursor` is `null`. - Cursors are opaque. Do not build or edit one; an edited cursor gets `400 invalid_cursor`. - [`GET /v1/events`](https://installgraph.com/docs/reference/list-events.md) runs oldest first. Save the last cursor and resume from it to read only new events next time. ## Read a whole list JavaScript: ```js async function* pages(path, params) { let cursor; do { const qs = new URLSearchParams({ ...params, ...(cursor ? { cursor } : {}) }); const res = await fetch(`https://api.installgraph.com${path}?${qs}`, { headers: { Authorization: `Bearer ${process.env.INSTALLGRAPH_API_KEY}` }, }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); yield body.data; cursor = body.pagination.nextCursor; } while (cursor); } for await (const rows of pages("/v1/apps", { category: "reviews", limit: "500" })) { for (const app of rows) console.log(app.slug); } ``` Python: ```python import os, requests def pages(path, params): headers = {"Authorization": f"Bearer {os.environ['INSTALLGRAPH_API_KEY']}"} cursor = None while True: query = {**params, **({"cursor": cursor} if cursor else {})} body = requests.get(f"https://api.installgraph.com{path}", params=query, headers=headers).json() if "error" in body: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") yield body["data"] cursor = body["pagination"]["nextCursor"] if not cursor: break for rows in pages("/v1/apps", {"category": "reviews", "limit": 500}): for app in rows: print(app["slug"]) ``` On a billed list, count first ([`GET /v1/apps/{slug}/companies/count`](https://installgraph.com/docs/reference/count-app-companies.md), free) and stop once you have the rows you need. Source: https://installgraph.com/docs/pagination --- # Rate limits Each key may make **60** requests a minute unless agreed otherwise; over that, the API answers `429 rate_limited` with a `Retry-After` header in seconds. ## Headers Every response carries: | Header | Meaning | | --- | --- | | `X-RateLimit-Limit` | Requests allowed per minute for this key | | `X-RateLimit-Remaining` | Requests left in the current minute | | `X-RateLimit-Reset` | When the minute resets, in epoch seconds | | `Retry-After` | On a `429` only: seconds to wait | The window is a fixed minute per key. A `429` costs no credits. ## Other limits | What | Limit | | --- | --- | | [`GET /v1/public/stats`](https://installgraph.com/docs/reference/get-public-stats.md) (no key) | 30 requests a minute per IP; cacheable for an hour | | New accounts ([installgraph.com/login](https://installgraph.com/login)) | 3 per IP per day | | Stores unlocked | 40,000 per account per 30 days ([Credits](https://installgraph.com/docs/credits.md#limits)) | Need a higher rate? [Talk to us](https://installgraph.com/talk-to-us). ## Retrying Retry `429` after `Retry-After` seconds, and `5xx` with exponential backoff. Do not retry other `4xx`: they fail the same way again. ```js async function call(url, init = {}, attempt = 0) { const res = await fetch(url, { ...init, headers: { Authorization: `Bearer ${process.env.INSTALLGRAPH_API_KEY}`, ...init.headers }, }); if ((res.status === 429 || res.status >= 500) && attempt < 5) { const wait = Number(res.headers.get("Retry-After")) || 2 ** attempt; await new Promise((r) => setTimeout(r, wait * 1000)); return call(url, init, attempt + 1); } const body = await res.json(); if (!res.ok) throw Object.assign(new Error(body.error.message), body.error); return body; } ``` Billed requests are safe to retry: an error charges nothing, and a store you already paid for is free again for 7 days. Source: https://installgraph.com/docs/rate-limits --- # Errors Every error is `{ "error": { "code", "message", "requestId" } }` with one of **23** stable codes; branch on `code`, show `message` to people, and quote `requestId` to support. ## Shape ```json { "error": { "code": "not_found", "message": "No app `klavyio` in the catalogue. GET /v1/apps lists them.", "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14" } } ``` - `code` is the contract: codes are added, never renamed. `message` is for people and may change. - `requestId` is also in the `X-Request-Id` header of every response. - Some errors add fields: a `402` carries `offers` (see [Credits](https://installgraph.com/docs/credits.md#running-out)), `purchase_not_allowed` adds `settings`, a `405` sends an `Allow` header. - **Errors never cost credits**, except store lookups past the free-miss allowance (`404` on [`GET /v1/companies/{domain}`](https://installgraph.com/docs/reference/get-company.md), 1 credit, shown in `X-Credits-Charged`). ## Codes | Status | Code | Meaning | What to do | Retry | | --- | --- | --- | --- | --- | | 400 | `invalid_request` | A parameter or body field is missing or malformed. | Fix the request; `message` names the field. Do not retry unchanged. | No | | 400 | `invalid_cursor` | The `cursor` is not one this endpoint issued (edited, truncated, or from another list). | Pass `pagination.nextCursor` back unchanged, or start again without a cursor. | No | | 400 | `invalid_email` | Sign-up refused the address (malformed or disposable). | Use a real work email. | No | | 401 | `missing_api_key` | No key was sent. | Send `Authorization: Bearer sfs_…` or `X-API-Key: sfs_…`. | No | | 401 | `invalid_api_key` | The key is not one we issued. | Check for truncation or whitespace; keys are `sfs_` plus 32 letters and digits. | No | | 401 | `api_key_revoked` | The key was revoked. | Make a new key at installgraph.com/account. | No | | 402 | `insufficient_credits` | The balance cannot cover one row or lookup. Nothing was charged. | Buy credits: `error.offers.pack.buyUrl`, or `POST /v1/billing/purchase` if agent purchases are on. | No | | 402 | `usage_cap_reached` | The account unlocked 40,000 distinct stores in the last 30 days. | Wait for the window to roll, or Talk to us: `error.offers.talkToUs.url`. | No | | 402 | `spend_cap_reached` | Purchases would pass $1,000 in 30 days. | Talk to us for more: `error.offers.talkToUs.url`. | No | | 402 | `paid_plan_required` | Webhooks need an account that has bought a credit pack. | Buy any pack: `error.offers.pack.buyUrl`. Then create the webhook again. | No | | 402 | `payment_failed` | The saved card was declined. | Ask the account owner to update the card at installgraph.com/account. | No | | 403 | `insufficient_scope` | The key lacks this endpoint's scope. | Use a key with the scope the endpoint lists (`GET /v1/usage` shows yours). | No | | 403 | `api_key_suspended` | The key is suspended. | Talk to us, quoting `requestId`. | No | | 403 | `purchase_not_allowed` | Agent purchases are off for this account, the pack is over the owner's limits, or the key has no account. | Hand `error.offers.pack.buyUrl` to a person; the owner sets limits at `error.settings`. | No | | 404 | `not_found` | No such endpoint, app, or store (or not scanned yet). | Check the slug or domain. Store 404s are free within the lookup-miss allowance. | No | | 405 | `method_not_allowed` | Wrong HTTP method for this path. | Use the method in the `Allow` header. | No | | 409 | `account_exists` | That email already has an account. | Sign in at installgraph.com/login to make more keys. | No | | 409 | `no_payment_method` | No card is saved, so nothing can be bought off-session. | Buy a pack once at installgraph.com/account to save a card. | No | | 409 | `webhook_limit_reached` | The account already has 25 active webhooks. | Delete one with `DELETE /v1/webhooks/{id}`, or Talk to us: `error.offers.talkToUs.url`. | No | | 429 | `rate_limited` | Too many requests this minute. | Wait `Retry-After` seconds, then retry. | Yes | | 500 | `internal_error` | Something failed on our side. Nothing was charged. | Retry with backoff; quote `requestId` if it persists. | Yes | | 503 | `not_configured` | A feature this request needs (such as billing) is unavailable. | Retry later. | Yes | | 503 | `service_unavailable` | The API is briefly unavailable. | Retry with backoff. | Yes | ## Handling them ```js async function call(url, init = {}, attempt = 0) { const res = await fetch(url, { ...init, headers: { Authorization: `Bearer ${process.env.INSTALLGRAPH_API_KEY}`, ...init.headers }, }); if ((res.status === 429 || res.status >= 500) && attempt < 5) { const wait = Number(res.headers.get("Retry-After")) || 2 ** attempt; await new Promise((r) => setTimeout(r, wait * 1000)); return call(url, init, attempt + 1); } const body = await res.json(); if (!res.ok) throw Object.assign(new Error(body.error.message), body.error); return body; } try { const { data } = await call("https://api.installgraph.com/v1/companies/example.com"); } catch (err) { if (err.code === "not_found") { /* not a store we know: skip it */ } else if (err.code === "insufficient_credits") { /* show err.offers.pack.buyUrl */ } else throw err; } ``` Source: https://installgraph.com/docs/errors --- # Change webhooks Subscribe to one app's installs or removals and we POST each change to your URL as our scans detect it, for **10 credits** per change delivered. ## Why use one Without a webhook, finding who started or stopped using an app means pulling its list (or [`GET /v1/events`](https://installgraph.com/docs/reference/list-events.md)) on a schedule and comparing. A webhook does the comparing for you: each store that changes reaches you once, a minute or so after the scan that saw it. Every store is scanned weekly, so a change reaches you within about a week of happening. ## Subscribe ```bash curl -X POST "https://api.installgraph.com/v1/webhooks" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"app": "klaviyo", "url": "https://example.com/hooks/installgraph", "types": ["removed"], "country": "US"}' ``` | Field | Meaning | | --- | --- | | `app` | An app slug from [`GET /v1/apps`](https://installgraph.com/docs/reference/list-apps.md). One app per webhook. | | `url` | Where we POST. https on the standard port, at a public address. | | `types` | `installed`, `removed` or both (the default). | | `country`, `region`, `industry`, `aov` | Optional, as on [`GET /v1/events`](https://installgraph.com/docs/reference/list-events.md): only changes at matching stores are sent and charged. | The response carries a `secret` (`whsec_…`). Store it: it is shown only once, and it signs every delivery. Then call [`POST /v1/webhooks/{id}/test`](https://installgraph.com/docs/reference/test-webhook.md) to send a free sample and check your endpoint. ## What you receive ```json { "id": "6733d1a07be42c19f5a8e301", "type": "technology.removed", "subscriptionId": "6731b2e4c9a1f20d8e4b7c15", "createdAt": "2026-11-12T04:21:07.000Z", "data": { "id": "evt_6720a4c3f1b2", "type": "removed", "domain": "example.com", "app": { "slug": "klaviyo", "name": "Klaviyo", "category": "email-sms" }, "detectedAt": "2026-11-12T04:20:31.000Z", "previousScanAt": "2026-11-05T03:58:10.000Z", "lastSeenAt": "2026-11-05T03:58:10.000Z", "country": "US", "region": "north-america", "industry": null, "pricePoint": null } } ``` `data` is the same row [`GET /v1/events`](https://installgraph.com/docs/reference/list-events.md) returns. The change happened between `previousScanAt` and `detectedAt`. A store's first scan reports no installs, because we cannot say when they happened. Headers: `Installgraph-Webhook-Id` (the payload's `id`, the same on every retry) and `Installgraph-Signature`. ## Verify the signature `Installgraph-Signature: t=,v1=`, where `v1` is HMAC-SHA256 of `.`, keyed by your secret. Check it against the raw body, before parsing, compare in constant time, and refuse a `t` more than 5 minutes old. ```js import { createHmac, timingSafeEqual } from "node:crypto"; function verify(secret, rawBody, header) { const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2))); const t = Number(parts.t); if (!parts.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false; const expected = Buffer.from(createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")); const given = Buffer.from(parts.v1); return expected.length === given.length && timingSafeEqual(expected, given); } ``` ```python import hashlib, hmac, time def verify(secret: str, raw_body: bytes, header: str) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) t = int(parts.get("t", "0")) if "v1" not in parts or abs(time.time() - t) > 300: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts["v1"]) ``` ## Answer quickly, deduplicate Reply with any 2xx within 10 seconds, then do your work. Anything else (an error status, a timeout, a redirect) is retried after 1, 5 and 30 minutes, then 2, 6, 12 and 24 hours. Deliveries can arrive out of order and, rarely, twice, so deduplicate on `Installgraph-Webhook-Id`. [`GET /v1/webhooks/{id}/deliveries`](https://installgraph.com/docs/reference/list-webhook-deliveries.md) shows each delivery's attempts and your endpoint's last answer. After 3 changes in a row fail every retry, the webhook is `disabled`; fix the endpoint and create a new one. ## Price - **10 credits per change delivered**, one store installing or removing the app. Creating, listing, testing and deleting webhooks is free. - Charged when a change is first sent, and **refunded** if every retry fails or you delete the webhook first. - If your balance cannot cover a change, it waits and is sent once you top up (or auto top-up runs), for up to 7 days; after that it is dropped, uncharged. - Webhooks need an account that has bought a credit pack (`402 paid_plan_required` otherwise). Up to 25 active webhooks per account (`409 webhook_limit_reached`); for more, [talk to us](https://installgraph.com/talk-to-us). Source: https://installgraph.com/docs/webhooks --- # Data model Each Shopify store's homepage is read **once a week** with one plain GET (no browser), and an app counts as present when its web pixel, theme app embed, script or markup is found there. ## Detection | Evidence | What we found | Strength | | --- | --- | --- | | `webPixel` | The app's Shopify web pixel, identified by its Partner app id | Strongest | | `themeAppExtension` | An app embed in the theme | Strong | | `script` | A ScriptTag or third-party script on the page | Good | | `html` | Markup the app writes into the page | Good | Each app on a store lists its evidence (`evidence`), so you can check it in the page source. Apps a tag manager injects only at runtime are not seen. ## Store status A store's `status` is the latest scan's verdict. Apps come from the latest `live` scan (`lastHealthyScanAt`), so a store that is briefly unreachable keeps its last known stack. | Status | Meaning | | --- | --- | | `live` | Read the storefront | | `password` | Behind a password page | | `closed` | The store is unavailable | | `not_shopify` | The domain no longer serves a Shopify store | | `unreachable` | We could not read the page this time | ## Dates - `firstSeenAt` / `lastSeenAt` on an app: the first and latest scans that found it. - An **event** ([`GET /v1/events`](https://installgraph.com/docs/reference/list-events.md)) happened between `previousScanAt` and `detectedAt`. A store's first scan reports no installs (we cannot say when they happened). Removals are inferred only from a scan that read the whole live page. - `theme.since`: the first scan on the current theme. ## Store attributes | Field | What it is | | --- | --- | | `country` | ISO 3166-1 alpha-2 country the store sells from (Shopify's `Shopify.country`) | | `region` | `north-america`, `latin-america`, `europe`, `middle-east-africa` or `asia-pacific` | | `industry` | Our reading of the homepage, in the Store Leads taxonomy: 26 categories, 182 subcategories, with `confidence` | | `pricePoint` | Typical order value as a USD band (`under-25`, `25-50`, `50-100`, `100-200`, `200-500`, `500-1000`, `1000-plus`), from homepage product prices | | `theme` | Theme name, version, Theme Store id, and the merchant's name for their copy | | `customPixelCount` | Custom pixels in Shopify's Customer events | [`GET /v1/industries`](https://installgraph.com/docs/reference/list-industries.md) lists every industry, region (with its countries) and order-value band the filters accept. ## Filters `country`, `region`, `industry` and `aov` work on [`GET /v1/apps/{slug}/companies`](https://installgraph.com/docs/reference/list-app-companies.md), its count, and [`GET /v1/events`](https://installgraph.com/docs/reference/list-events.md). Combined filters must all hold. - `industry=Apparel` matches the category and every subcategory; `industry=Apparel/Footwear` matches one subcategory. - `aov` takes one or more bands, comma-separated: `aov=100-200,200-500`. ## App categories Our own categories for apps (not the App Store's), used by [`GET /v1/apps`](https://installgraph.com/docs/reference/list-apps.md) and [`GET /v1/public/stats`](https://installgraph.com/docs/reference/get-public-stats.md): `ads-pixel`, `analytics`, `tag-management`, `server-side-tracking`, `attribution`, `email-sms`, `reviews`, `loyalty`, `subscriptions`, `search`, `upsell-personalisation`, `popups`, `support-chat`, `consent`, `accessibility`, `shipping-protection`, `returns-tracking`, `fraud`, `payments-bnpl`, `affiliate`, `wishlist`, `back-in-stock`, `translation`, `page-builder`, `ab-testing`, `cross-border`, `product-feeds`, `other`. Source: https://installgraph.com/docs/data-model --- # Using the API from an AI agent An agent can learn the whole API from [/llms-full.txt](https://installgraph.com/llms-full.txt) or [/openapi.json](https://installgraph.com/openapi.json), get a key with one POST, and spend safely because counts are **free** and every response reports the remaining balance. ## Point the agent at the docs | Resource | Use | | --- | --- | | [/llms.txt](https://installgraph.com/llms.txt) | A short index of the site and docs | | [/llms-full.txt](https://installgraph.com/llms-full.txt) | Every docs page in one Markdown file: paste or fetch into context | | [/openapi.json](https://installgraph.com/openapi.json) | OpenAPI 3.1 with examples: generate tools or a client from it | | `/docs/….md` | Any single page as Markdown (add `.md` to its URL) | | [`GET /v1/pricing`](https://installgraph.com/docs/reference/get-pricing.md) | Live prices as JSON, no key | ## A safe loop 1. **Know the prices.** `GET /v1/pricing` (no key) rather than prices from memory. 2. **Get a key from the person.** Accounts are made in the browser: they sign up at installgraph.com/login and copy the key from their dashboard. Never ask for it in a chat log; read it from an environment variable. 3. **Count first.** [`GET /v1/apps/{slug}/companies/count`](https://installgraph.com/docs/reference/count-app-companies.md) is free. The count is the most a full pull can cost. 4. **Confirm big spends.** If the count is more than the person asked for, or more than `meta.creditsRemaining`, ask before pulling. 5. **Pull with `limit`** set to the rows you need, and read `meta.creditsRemaining` after each page. 6. **On `402`**, stop. Show the person `error.offers.pack.buyUrl`, or, only if they have turned on agent purchases, call [`POST /v1/billing/purchase`](https://installgraph.com/docs/reference/agent-purchase.md). 7. **Branch on `error.code`**, never on `message`. Wait `Retry-After` seconds on `429`. See [Errors](https://installgraph.com/docs/errors.md). ## Instructions to give an agent ```text You can call the Installgraph API (base URL https://api.installgraph.com) with the key in INSTALLGRAPH_API_KEY, sent as "Authorization: Bearer ". Docs: https://installgraph.com/llms-full.txt 1 credit unlocks 1 store. Counts are free: always call /v1/apps/{slug}/companies/count before /v1/apps/{slug}/companies, and ask me before any pull over 100 credits. Use limit to cap each page. Report meta.creditsRemaining when you finish. On a 402, stop and show me error.offers.pack.buyUrl. Never put the key in a URL, a file you write, or your reply. ``` ## Agent purchases Off by default. The account owner turns them on at [installgraph.com/account](https://installgraph.com/account) and sets a per-purchase and a 30-day limit. Then [`POST /v1/billing/purchase`](https://installgraph.com/docs/reference/agent-purchase.md) with `{"pack": "starter"}` buys on the card saved at the account's first checkout. Outside the owner's limits it answers `403 purchase_not_allowed`; past $1,000 in 30 days, `402 spend_cap_reached`. Source: https://installgraph.com/docs/agents --- # A store's current stack `GET /v1/companies/{domain}` Returns one Shopify store's current app stack, theme, country, industry and order-value band for **1 credit**. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** `companies:read` - **Cost:** 1 credit per store found; free again within 7 days. `include=history` costs 1 more. Use it when you have a domain and want everything we know about that store. To find stores, start from an app with `GET /v1/apps/{slug}/companies`. The apps a store ran at its latest healthy scan, with its theme, country and currency. `domain` is normalized (scheme, `www.`, path and port are dropped), so `https://www.allbirds.com/` works. Costs 1 credit, free again within 7 days. A 404 (no store found) is free up to 2 × your lookups that found a store in the last 30 days + 100, then costs 1 credit (`X-Credits-Charged` on the 404; a 402 when out of credits). Until your account has bought credits, a domain we know of but have not scanned yet answers with the same 404 as one we have never heard of. `include=history` adds the store's install and removal events (`history`, newest first, up to 500) for 1 credit more. Scope `companies:read`. ## Request ```bash curl "https://api.installgraph.com/v1/companies/example.com" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` ### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `domain` | string | Yes | Example: `allbirds.com`. | ### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `include` | string | No | One of `history`. | ## Response `200`. The example's stores are fictional. ```json { "data": { "domain": "example.com", "name": "Example Apparel Co.", "myshopifyDomain": "example-apparel.myshopify.com", "country": "US", "region": "north-america", "industry": { "category": "Apparel", "subcategory": "Footwear", "path": "/Apparel/Footwear", "confidence": "high" }, "pricePoint": { "aovBand": "100-200", "label": "$100–200", "confidence": "medium", "medianProductPriceUsd": 128, "productPriceRangeUsd": [ 95, 165 ], "productPricesSeen": 24 }, "currency": "USD", "locale": "en", "status": "live", "theme": { "name": "Dawn", "version": "15.2.0", "themeStoreId": 887, "customName": "Dawn - spring", "since": "2026-03-02T04:11:09.000Z" }, "lastScannedAt": "2026-11-02T03:40:12.000Z", "lastHealthyScanAt": "2026-11-02T03:40:12.000Z", "customPixelCount": 2, "apps": [ { "slug": "klaviyo", "name": "Klaviyo", "category": "email-sms", "firstSeenAt": "2026-01-12T05:02:44.000Z", "lastSeenAt": "2026-11-02T03:40:12.000Z", "evidence": [ "webPixel", "themeAppExtension" ] }, { "slug": "judge-me", "name": "Judge.me", "category": "reviews", "firstSeenAt": "2026-01-12T05:02:44.000Z", "lastSeenAt": "2026-11-02T03:40:12.000Z", "evidence": [ "themeAppExtension", "script" ] } ] }, "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "creditsUsed": 1, "creditsCharged": 1, "creditsRemaining": 49, "freeRepulls": 0 } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data.domain` | string | Example: `allbirds.com`. | | `data.name` | string \| null | | | `data.myshopifyDomain` | string \| null | Example: `allbirds.myshopify.com`. | | `data.country` | string \| null | Example: `US`. | | `data.region` | Region \| null | One of `north-america`, `latin-america`, `europe`, `middle-east-africa`, `asia-pacific`. | | `data.industry` | Industry \| null | Our reading of what the store sells, from its homepage, in the Store Leads taxonomy. | | `data.industry.category` | string | Example: `Apparel`. | | `data.industry.subcategory` | string \| null | Null when the store fits a category but none of its subcategories (a general bookshop). Example: `Footwear`. | | `data.industry.path` | string | `/Books & Literature` alone when there is no subcategory. Example: `/Apparel/Footwear`. | | `data.industry.confidence` | string \| null | One of `high`, `medium`, `low`. | | `data.pricePoint` | PricePoint \| null | The store's typical order value, in USD bands so it reads the same in every country. Our own reading of the homepage: the product prices shown there (converted from the store's currency), and what is sold. | | `data.pricePoint.aovBand` | string | One of `under-25`, `25-50`, `50-100`, `100-200`, `200-500`, `500-1000`, `1000-plus`. | | `data.pricePoint.label` | string | Example: `$100–200`. | | `data.pricePoint.confidence` | string \| null | One of `high`, `medium`, `low`. | | `data.pricePoint.medianProductPriceUsd` | integer \| null | Median product price on the homepage, in USD. | | `data.pricePoint.productPriceRangeUsd` | integer[] \| null | Interquartile range of homepage product prices, in USD. | | `data.pricePoint.productPricesSeen` | integer | | | `data.currency` | string \| null | Example: `USD`. | | `data.locale` | string \| null | Example: `en`. | | `data.status` | Status | The latest scan's verdict: `live`, `password` (behind a password page), `closed` (store unavailable), `not_shopify` (the domain no longer serves a Shopify store), or `unreachable` (we could not read the page this time). Apps are from the latest `live` scan (`lastHealthyScanAt`). One of `live`, `password`, `closed`, `not_shopify`, `unreachable`. | | `data.theme` | object \| null | | | `data.theme.name` | string | The theme's own name (schema name), e.g. Dawn. | | `data.theme.version` | string \| null | | | `data.theme.themeStoreId` | integer \| null | Shopify Theme Store id, for a store theme. | | `data.theme.customName` | string \| null | What the merchant named their copy. | | `data.theme.since` | string (date-time) \| null | First scan on this theme. | | `data.lastScannedAt` | string (date-time) | | | `data.lastHealthyScanAt` | string (date-time) \| null | | | `data.customPixelCount` | integer \| null | Custom pixels in Shopify's Customer events. | | `data.apps` | CompanyApp[] | | | `data.apps[].slug` | string | Example: `klaviyo`. | | `data.apps[].name` | string | The catalogue's name: our own for apps we curate, else the App Store listing title as written. Example: `Klaviyo`. | | `data.apps[].displayName` | string | What a merchant calls the app, read from its keyword-padded App Store title ("Preorder, Back In Stock ‑ STOQ" is `STOQ`). A developer's listings that share a brand keep the words that tell them apart (`Swym Wishlist Plus`). Equal to `name` until derived. Example: `Klaviyo`. | | `data.apps[].category` | Category | One of 28 values (see [Data model](https://installgraph.com/docs/data-model.md)). | | `data.apps[].kind` | string | `shopify-app`: an app installed from Shopify. `web`: a website technology installed outside Shopify, by pasting its tag into the theme or through a tag manager (Google Tag Manager, Meta Pixel, Hotjar). A vendor with both has one entry of each (`meta-pixel` and `meta-facebook-instagram-app`). One of `shopify-app`, `web`. | | `data.apps[].firstSeenAt` | string (date-time) | First scan that found it. | | `data.apps[].lastSeenAt` | string (date-time) | Latest scan that found it. | | `data.apps[].evidence` | Evidence[] | One of `webPixel`, `themeAppExtension`, `appProxy`, `customPixel`, `script`, `html`. | Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 400 | `invalid_request` | A parameter or body field is missing or malformed. | Fix the request; `message` names the field. Do not retry unchanged. | | 401 | `missing_api_key` | No key was sent. | Send `Authorization: Bearer sfs_…` or `X-API-Key: sfs_…`. | | 401 | `invalid_api_key` | The key is not one we issued. | Check for truncation or whitespace; keys are `sfs_` plus 32 letters and digits. | | 401 | `api_key_revoked` | The key was revoked. | Make a new key at installgraph.com/account. | | 402 | `insufficient_credits` | The balance cannot cover one row or lookup. Nothing was charged. | Buy credits: `error.offers.pack.buyUrl`, or `POST /v1/billing/purchase` if agent purchases are on. | | 402 | `usage_cap_reached` | The account unlocked 40,000 distinct stores in the last 30 days. | Wait for the window to roll, or Talk to us: `error.offers.talkToUs.url`. | | 403 | `insufficient_scope` | The key lacks this endpoint's scope. | Use a key with the scope the endpoint lists (`GET /v1/usage` shows yours). | | 403 | `api_key_suspended` | The key is suspended. | Talk to us, quoting `requestId`. | | 404 | `not_found` | No such endpoint, app, or store (or not scanned yet). | Check the slug or domain. Store 404s are free within the lookup-miss allowance. | | 429 | `rate_limited` | Too many requests this minute. | Wait `Retry-After` seconds, then retry. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/get-company --- # Stores running an app `GET /v1/apps/{slug}/companies` Lists the Shopify stores running an app, filtered by country, region, industry or order value, at **1 credit per store**. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** `companies:read` - **Cost:** 1 credit per store in the page; stores you unlocked from this app's list in the last 7 days are free. Count first with `GET /v1/apps/{slug}/companies/count` (free), then page through only as many rows as you need with `limit`. Stores whose latest healthy scan found the app, by domain. 1 credit per row. Scope `companies:read`. ## Request ```bash curl "https://api.installgraph.com/v1/apps/klaviyo/companies?country=GB&industry=Apparel&limit=2" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` ### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | Yes | Example: `klaviyo`. | ### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `country` | string | No | ISO 3166-1 alpha-2 country the store sells from (Shopify's `Shopify.country`). Example: `US`. | | `region` | Region | No | A world region, i.e. its countries. Combined with `country`, both must hold. One of `north-america`, `latin-america`, `europe`, `middle-east-africa`, `asia-pacific`. | | `industry` | string | No | A top-level industry (`Apparel`, which also matches stores placed at that level only) or a subcategory (`Apparel/Footwear`). `GET /v1/industries` lists them. Example: `Apparel/Footwear`. | | `aov` | string | No | One or more typical order-value bands in USD, comma-separated (`100-200,200-500`). `GET /v1/industries` lists them. Example: `100-200`. | | `limit` | integer | No | 1–500. Default `100`. | | `cursor` | string | No | `pagination.nextCursor` from the previous page, unchanged, within 24 hours, with the same key's account and the same filters. | ## Response `200`. The example's stores are fictional. ```json { "data": [ { "domain": "example.net", "name": "Example Knitwear", "country": "GB", "region": "europe", "industry": { "category": "Apparel", "subcategory": "Knitwear", "path": "/Apparel/Knitwear", "confidence": "medium" }, "pricePoint": { "aovBand": "50-100", "label": "$50–100", "confidence": "medium", "medianProductPriceUsd": 72, "productPriceRangeUsd": [ 58, 94 ], "productPricesSeen": 18 }, "currency": "GBP", "status": "live", "lastScannedAt": "2026-11-01T22:15:03.000Z", "firstSeenAt": "2026-02-08T09:30:00.000Z", "lastSeenAt": "2026-11-01T22:15:03.000Z", "evidence": [ "webPixel" ] }, { "domain": "example.org", "name": "Example Outdoor", "country": "GB", "region": "europe", "industry": { "category": "Apparel", "subcategory": null, "path": "/Apparel", "confidence": "low" }, "pricePoint": null, "currency": "GBP", "status": "live", "lastScannedAt": "2026-10-31T18:02:51.000Z", "firstSeenAt": "2026-05-19T11:47:20.000Z", "lastSeenAt": "2026-10-31T18:02:51.000Z", "evidence": [ "themeAppExtension", "script" ] } ], "pagination": { "nextCursor": "eyJrIjoiYXBwLWNvbXBhbmllcyIsImQiOiJleGFtcGxlLm9yZyJ9", "hasMore": true }, "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "creditsUsed": 2, "creditsCharged": 2, "creditsRemaining": 47, "freeRepulls": 0 } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data[].domain` | string | | | `data[].name` | string \| null | | | `data[].country` | string \| null | | | `data[].region` | Region \| null | One of `north-america`, `latin-america`, `europe`, `middle-east-africa`, `asia-pacific`. | | `data[].industry` | Industry \| null | Our reading of what the store sells, from its homepage, in the Store Leads taxonomy. | | `data[].industry.category` | string | Example: `Apparel`. | | `data[].industry.subcategory` | string \| null | Null when the store fits a category but none of its subcategories (a general bookshop). Example: `Footwear`. | | `data[].industry.path` | string | `/Books & Literature` alone when there is no subcategory. Example: `/Apparel/Footwear`. | | `data[].industry.confidence` | string \| null | One of `high`, `medium`, `low`. | | `data[].pricePoint` | PricePoint \| null | The store's typical order value, in USD bands so it reads the same in every country. Our own reading of the homepage: the product prices shown there (converted from the store's currency), and what is sold. | | `data[].pricePoint.aovBand` | string | One of `under-25`, `25-50`, `50-100`, `100-200`, `200-500`, `500-1000`, `1000-plus`. | | `data[].pricePoint.label` | string | Example: `$100–200`. | | `data[].pricePoint.confidence` | string \| null | One of `high`, `medium`, `low`. | | `data[].pricePoint.medianProductPriceUsd` | integer \| null | Median product price on the homepage, in USD. | | `data[].pricePoint.productPriceRangeUsd` | integer[] \| null | Interquartile range of homepage product prices, in USD. | | `data[].pricePoint.productPricesSeen` | integer | | | `data[].currency` | string \| null | | | `data[].status` | Status | The latest scan's verdict: `live`, `password` (behind a password page), `closed` (store unavailable), `not_shopify` (the domain no longer serves a Shopify store), or `unreachable` (we could not read the page this time). Apps are from the latest `live` scan (`lastHealthyScanAt`). One of `live`, `password`, `closed`, `not_shopify`, `unreachable`. | | `data[].lastScannedAt` | string (date-time) \| null | | | `data[].firstSeenAt` | string (date-time) \| null | | | `data[].lastSeenAt` | string (date-time) \| null | | | `data[].evidence` | Evidence[] | One of `webPixel`, `themeAppExtension`, `appProxy`, `customPixel`, `script`, `html`. | Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). Lists page with `limit` and `cursor`. See [Pagination](https://installgraph.com/docs/pagination.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 400 | `invalid_request` | A parameter or body field is missing or malformed. | Fix the request; `message` names the field. Do not retry unchanged. | | 400 | `invalid_cursor` | The `cursor` is not one this endpoint issued (edited, truncated, or from another list). | Pass `pagination.nextCursor` back unchanged, or start again without a cursor. | | 401 | `missing_api_key` | No key was sent. | Send `Authorization: Bearer sfs_…` or `X-API-Key: sfs_…`. | | 401 | `invalid_api_key` | The key is not one we issued. | Check for truncation or whitespace; keys are `sfs_` plus 32 letters and digits. | | 401 | `api_key_revoked` | The key was revoked. | Make a new key at installgraph.com/account. | | 402 | `insufficient_credits` | The balance cannot cover one row or lookup. Nothing was charged. | Buy credits: `error.offers.pack.buyUrl`, or `POST /v1/billing/purchase` if agent purchases are on. | | 402 | `usage_cap_reached` | The account unlocked 40,000 distinct stores in the last 30 days. | Wait for the window to roll, or Talk to us: `error.offers.talkToUs.url`. | | 403 | `insufficient_scope` | The key lacks this endpoint's scope. | Use a key with the scope the endpoint lists (`GET /v1/usage` shows yours). | | 403 | `api_key_suspended` | The key is suspended. | Talk to us, quoting `requestId`. | | 404 | `not_found` | No such endpoint, app, or store (or not scanned yet). | Check the slug or domain. Store 404s are free within the lookup-miss allowance. | | 429 | `rate_limited` | Too many requests this minute. | Wait `Retry-After` seconds, then retry. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/list-app-companies --- # How many stores run an app (free) `GET /v1/apps/{slug}/companies/count` Counts the Shopify stores running an app with the same filters as the list, **free**, so you know the cost before you pull rows. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** `companies:read` - **Cost:** Free and unlimited. Call it before every `GET /v1/apps/{slug}/companies` pull: the count is the most that pull can cost. The number of stores `/v1/apps/{slug}/companies` would list with the same filters. Free and unlimited, so you can narrow a query before paying for rows. Scope `companies:read`. ## Request ```bash curl "https://api.installgraph.com/v1/apps/klaviyo/companies/count?country=GB&industry=Apparel" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` ### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | Yes | Example: `klaviyo`. | ### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `country` | string | No | ISO 3166-1 alpha-2 country the store sells from (Shopify's `Shopify.country`). Example: `US`. | | `region` | Region | No | A world region, i.e. its countries. Combined with `country`, both must hold. One of `north-america`, `latin-america`, `europe`, `middle-east-africa`, `asia-pacific`. | | `industry` | string | No | A top-level industry (`Apparel`, which also matches stores placed at that level only) or a subcategory (`Apparel/Footwear`). `GET /v1/industries` lists them. Example: `Apparel/Footwear`. | | `aov` | string | No | One or more typical order-value bands in USD, comma-separated (`100-200,200-500`). `GET /v1/industries` lists them. Example: `100-200`. | ## Response `200`. The example's stores are fictional. ```json { "data": { "app": "klaviyo", "count": 1284 }, "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "creditsUsed": 0, "creditsCharged": 0, "creditsRemaining": 47, "freeRepulls": 0 } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data.app` | string | | | `data.count` | integer | | Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 404 | `not_found` | No such endpoint, app, or store (or not scanned yet). | Check the slug or domain. Store 404s are free within the lookup-miss allowance. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/count-app-companies --- # Installs and removals `GET /v1/events` Streams app installs and removals across Shopify stores, oldest first, at **1 credit per store** in each page. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** `events:read` - **Cost:** 1 credit per distinct store in the page. Use it to watch for changes: who started or stopped running an app since your last read. Save `pagination.nextCursor` and resume from it next time. The change stream, oldest first, so you can read it forward: page with the cursor, and resume a later read with `since` set to the last `detectedAt` you have (cursors last 24 hours). An event is reported by the scan that saw the change: the change happened between `previousScanAt` and `detectedAt`. A store's first scan reports no installs (we cannot say when they happened), and removals are only inferred from a scan that read the whole live page. Each event carries the store's current country, region and industry. With `country`, `region` or `industry`, a page can hold fewer than `limit` events while `hasMore` is true. 1 credit per row. Scope `events:read`. ## Request ```bash curl "https://api.installgraph.com/v1/events?app=klaviyo&type=removed&since=2026-10-01T00%3A00%3A00Z&limit=1" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` ### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `app` | string | No | Example: `klaviyo`. | | `type` | string | No | One of `installed`, `removed`. | | `since` | string (date-time) | No | Events detected at or after this time (ISO 8601). | | `until` | string (date-time) | No | Events detected before this time (ISO 8601). | | `country` | string | No | ISO 3166-1 alpha-2 country the store sells from (Shopify's `Shopify.country`). Example: `US`. | | `region` | Region | No | A world region, i.e. its countries. Combined with `country`, both must hold. One of `north-america`, `latin-america`, `europe`, `middle-east-africa`, `asia-pacific`. | | `industry` | string | No | A top-level industry (`Apparel`, which also matches stores placed at that level only) or a subcategory (`Apparel/Footwear`). `GET /v1/industries` lists them. Example: `Apparel/Footwear`. | | `aov` | string | No | One or more typical order-value bands in USD, comma-separated (`100-200,200-500`). `GET /v1/industries` lists them. Example: `100-200`. | | `limit` | integer | No | 1–500. Default `100`. | | `cursor` | string | No | `pagination.nextCursor` from the previous page, unchanged, within 24 hours, with the same key's account and the same filters. | ## Response `200`. The example's stores are fictional. ```json { "data": [ { "id": "evt_6720a4c3f1b2", "type": "removed", "domain": "example.com", "app": { "slug": "klaviyo", "name": "Klaviyo", "category": "email-sms" }, "detectedAt": "2026-10-12T04:20:31.000Z", "previousScanAt": "2026-10-05T03:58:10.000Z", "lastSeenAt": "2026-10-05T03:58:10.000Z", "country": "US", "region": "north-america", "industry": { "category": "Apparel", "subcategory": "Footwear", "path": "/Apparel/Footwear", "confidence": "high" }, "pricePoint": { "aovBand": "100-200", "label": "$100–200", "confidence": "medium", "medianProductPriceUsd": 128, "productPriceRangeUsd": [ 95, 165 ], "productPricesSeen": 24 } } ], "pagination": { "nextCursor": "eyJrIjoiZXZlbnRzIiwiaWQiOiJldnRfNjcyMGE0YzNmMWIyIn0", "hasMore": true }, "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "creditsUsed": 1, "creditsCharged": 1, "creditsRemaining": 46, "freeRepulls": 0 } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data[].id` | string | | | `data[].type` | string | One of `installed`, `removed`. | | `data[].domain` | string | | | `data[].app` | AppSummary | | | `data[].app.slug` | string | Example: `klaviyo`. | | `data[].app.name` | string | The catalogue's name: our own for apps we curate, else the App Store listing title as written. Example: `Klaviyo`. | | `data[].app.displayName` | string | What a merchant calls the app, read from its keyword-padded App Store title ("Preorder, Back In Stock ‑ STOQ" is `STOQ`). A developer's listings that share a brand keep the words that tell them apart (`Swym Wishlist Plus`). Equal to `name` until derived. Example: `Klaviyo`. | | `data[].app.category` | Category | One of 28 values (see [Data model](https://installgraph.com/docs/data-model.md)). | | `data[].app.kind` | string | `shopify-app`: an app installed from Shopify. `web`: a website technology installed outside Shopify, by pasting its tag into the theme or through a tag manager (Google Tag Manager, Meta Pixel, Hotjar). A vendor with both has one entry of each (`meta-pixel` and `meta-facebook-instagram-app`). One of `shopify-app`, `web`. | | `data[].detectedAt` | string (date-time) | The scan that saw the change. | | `data[].previousScanAt` | string (date-time) \| null | The healthy scan before it. | | `data[].lastSeenAt` | string (date-time) \| null | For a removal, the last scan that still saw the app. | | `data[].country` | string \| null | The store's current country. | | `data[].region` | Region \| null | One of `north-america`, `latin-america`, `europe`, `middle-east-africa`, `asia-pacific`. | | `data[].industry` | Industry \| null | Our reading of what the store sells, from its homepage, in the Store Leads taxonomy. | | `data[].industry.category` | string | Example: `Apparel`. | | `data[].industry.subcategory` | string \| null | Null when the store fits a category but none of its subcategories (a general bookshop). Example: `Footwear`. | | `data[].industry.path` | string | `/Books & Literature` alone when there is no subcategory. Example: `/Apparel/Footwear`. | | `data[].industry.confidence` | string \| null | One of `high`, `medium`, `low`. | | `data[].pricePoint` | PricePoint \| null | The store's typical order value, in USD bands so it reads the same in every country. Our own reading of the homepage: the product prices shown there (converted from the store's currency), and what is sold. | | `data[].pricePoint.aovBand` | string | One of `under-25`, `25-50`, `50-100`, `100-200`, `200-500`, `500-1000`, `1000-plus`. | | `data[].pricePoint.label` | string | Example: `$100–200`. | | `data[].pricePoint.confidence` | string \| null | One of `high`, `medium`, `low`. | | `data[].pricePoint.medianProductPriceUsd` | integer \| null | Median product price on the homepage, in USD. | | `data[].pricePoint.productPriceRangeUsd` | integer[] \| null | Interquartile range of homepage product prices, in USD. | | `data[].pricePoint.productPricesSeen` | integer | | Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). Lists page with `limit` and `cursor`. See [Pagination](https://installgraph.com/docs/pagination.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 400 | `invalid_request` | A parameter or body field is missing or malformed. | Fix the request; `message` names the field. Do not retry unchanged. | | 400 | `invalid_cursor` | The `cursor` is not one this endpoint issued (edited, truncated, or from another list). | Pass `pagination.nextCursor` back unchanged, or start again without a cursor. | | 401 | `missing_api_key` | No key was sent. | Send `Authorization: Bearer sfs_…` or `X-API-Key: sfs_…`. | | 401 | `invalid_api_key` | The key is not one we issued. | Check for truncation or whitespace; keys are `sfs_` plus 32 letters and digits. | | 401 | `api_key_revoked` | The key was revoked. | Make a new key at installgraph.com/account. | | 402 | `insufficient_credits` | The balance cannot cover one row or lookup. Nothing was charged. | Buy credits: `error.offers.pack.buyUrl`, or `POST /v1/billing/purchase` if agent purchases are on. | | 402 | `usage_cap_reached` | The account unlocked 40,000 distinct stores in the last 30 days. | Wait for the window to roll, or Talk to us: `error.offers.talkToUs.url`. | | 403 | `insufficient_scope` | The key lacks this endpoint's scope. | Use a key with the scope the endpoint lists (`GET /v1/usage` shows yours). | | 403 | `api_key_suspended` | The key is suspended. | Talk to us, quoting `requestId`. | | 429 | `rate_limited` | Too many requests this minute. | Wait `Retry-After` seconds, then retry. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/list-events --- # Subscribe to an app's installs or removals `POST /v1/webhooks` Subscribes your URL to one app's installs or removals; each change our scans detect is POSTed to you for **10 credits**. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** `events:read` - **Cost:** Free to create; 10 credits per change delivered, refunded if delivery fails. Use it instead of pulling an app's list or `GET /v1/events` on a schedule and diffing. Keep the `secret` from the response: it is shown once and signs every delivery (see [Webhooks](https://installgraph.com/docs/webhooks.md)). From now on, each time a scan detects a store installing or removing `app` (filtered by `types` and the optional company filters), we POST it to `url` within about a minute of the scan, as a `WebhookPayload` whose `data` is the same row `GET /v1/events` returns. Scans are weekly per store, so a change reaches you within a week of happening rather than whenever you next pull and diff a list. **Price:** free to create; **10 credits per change delivered** (one store, one install or removal), taken before the first send and refunded if every retry fails. A change your balance cannot cover waits (`awaiting_credits`) and is sent once you top up, for up to 7 days. Needs an account that has bought a credit pack (`paid_plan_required` otherwise). At most 25 active webhooks per account. **Delivery:** `url` must be https on port 443 at a public address. Any 2xx is success; anything else, a timeout (10s) or a redirect is retried after 1, 5 and 30 minutes, 2, 6, 12 and 24 hours. After 3 deliveries in a row fail every retry, the webhook is `disabled`. Deliveries may arrive out of order and, rarely, more than once: deduplicate on the `Installgraph-Webhook-Id` header (the payload's `id`). **Verifying:** `Installgraph-Signature: t=,v1=`, where `v1` is HMAC-SHA256 of `.` keyed by the `secret` returned here (shown once). Compare in constant time and refuse a `t` more than 5 minutes old. Scope `events:read`. ## Request ```bash curl -X POST "https://api.installgraph.com/v1/webhooks" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"app":"klaviyo","url":"https://example.com/hooks/installgraph","types":["removed"],"country":"US"}' ``` ### Body JSON. | Field | Type | Description | | --- | --- | --- | | `app` | string | An app slug from `GET /v1/apps`. Example: `klaviyo`. | | `url` | string (uri) | Example: `https://example.com/hooks/installgraph`. | | `types` | string[] | One of `installed`, `removed`. | | `country` | string | As on `GET /v1/events`. | | `region` | Region | One of `north-america`, `latin-america`, `europe`, `middle-east-africa`, `asia-pacific`. | | `industry` | string | As on `GET /v1/events`. | | `aov` | string | As on `GET /v1/events`. | | `description` | string | | ## Response `200`. The example's stores are fictional. ```json { "data": { "id": "6731b2e4c9a1f20d8e4b7c15", "app": { "slug": "klaviyo", "name": "Klaviyo", "category": "email-sms" }, "types": [ "removed" ], "filters": { "country": "US" }, "url": "https://example.com/hooks/installgraph", "description": null, "status": "active", "disabledReason": null, "creditsPerDelivery": 10, "createdAt": "2026-11-10T12:00:00.000Z", "lastDeliveredAt": null, "secret": "whsec_3q2-7wEXAMPLEc9Lr0m4NnT1vYbZk8sFhGdJpQ6uXaW0" }, "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "creditsUsed": 0, "creditsCharged": 0, "creditsRemaining": 2490, "freeRepulls": 0 } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data.id` | string | | | `data.app` | AppSummary | | | `data.app.slug` | string | Example: `klaviyo`. | | `data.app.name` | string | The catalogue's name: our own for apps we curate, else the App Store listing title as written. Example: `Klaviyo`. | | `data.app.displayName` | string | What a merchant calls the app, read from its keyword-padded App Store title ("Preorder, Back In Stock ‑ STOQ" is `STOQ`). A developer's listings that share a brand keep the words that tell them apart (`Swym Wishlist Plus`). Equal to `name` until derived. Example: `Klaviyo`. | | `data.app.category` | Category | One of 28 values (see [Data model](https://installgraph.com/docs/data-model.md)). | | `data.app.kind` | string | `shopify-app`: an app installed from Shopify. `web`: a website technology installed outside Shopify, by pasting its tag into the theme or through a tag manager (Google Tag Manager, Meta Pixel, Hotjar). A vendor with both has one entry of each (`meta-pixel` and `meta-facebook-instagram-app`). One of `shopify-app`, `web`. | | `data.types` | string[] | One of `installed`, `removed`. | | `data.filters` | object | The company filters it was created with. | | `data.filters.country` | string | | | `data.filters.region` | string | | | `data.filters.industry` | string | | | `data.filters.aov` | string | | | `data.url` | string | | | `data.description` | string \| null | | | `data.status` | string | `disabled` after 3 deliveries in a row failed every retry; create a new one. One of `active`, `disabled`. | | `data.disabledReason` | string \| null | | | `data.creditsPerDelivery` | integer | Example: `10`. | | `data.createdAt` | string (date-time) | | | `data.lastDeliveredAt` | string (date-time) \| null | | | `data.secret` | string | Example: `whsec_…`. | Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 400 | `invalid_request` | A parameter or body field is missing or malformed. | Fix the request; `message` names the field. Do not retry unchanged. | | 401 | `missing_api_key` | No key was sent. | Send `Authorization: Bearer sfs_…` or `X-API-Key: sfs_…`. | | 401 | `invalid_api_key` | The key is not one we issued. | Check for truncation or whitespace; keys are `sfs_` plus 32 letters and digits. | | 401 | `api_key_revoked` | The key was revoked. | Make a new key at installgraph.com/account. | | 402 | `paid_plan_required` | Webhooks need an account that has bought a credit pack. | Buy any pack: `error.offers.pack.buyUrl`. Then create the webhook again. | | 403 | `insufficient_scope` | The key lacks this endpoint's scope. | Use a key with the scope the endpoint lists (`GET /v1/usage` shows yours). | | 403 | `api_key_suspended` | The key is suspended. | Talk to us, quoting `requestId`. | | 404 | `not_found` | No such endpoint, app, or store (or not scanned yet). | Check the slug or domain. Store 404s are free within the lookup-miss allowance. | | 409 | `webhook_limit_reached` | The account already has 25 active webhooks. | Delete one with `DELETE /v1/webhooks/{id}`, or Talk to us: `error.offers.talkToUs.url`. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/create-webhook --- # Your webhooks `GET /v1/webhooks` Lists every webhook on your account, active or disabled, newest first, **free**. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** `events:read` - **Cost:** Free. Use it to check which subscriptions are running and whether any was disabled after repeated failed deliveries (`disabledReason`). Every webhook on your account, active or disabled, newest first. Free. Scope `events:read`. ## Request ```bash curl "https://api.installgraph.com/v1/webhooks" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` ## Response `200`. The example's stores are fictional. ```json { "data": [ { "id": "6731b2e4c9a1f20d8e4b7c15", "app": { "slug": "klaviyo", "name": "Klaviyo", "category": "email-sms" }, "types": [ "removed" ], "filters": { "country": "US" }, "url": "https://example.com/hooks/installgraph", "description": null, "status": "active", "disabledReason": null, "creditsPerDelivery": 10, "createdAt": "2026-11-10T12:00:00.000Z", "lastDeliveredAt": "2026-11-12T04:21:07.000Z" } ], "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "creditsUsed": 0, "creditsCharged": 0, "creditsRemaining": 2480, "freeRepulls": 0 } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data[].id` | string | | | `data[].app` | AppSummary | | | `data[].app.slug` | string | Example: `klaviyo`. | | `data[].app.name` | string | The catalogue's name: our own for apps we curate, else the App Store listing title as written. Example: `Klaviyo`. | | `data[].app.displayName` | string | What a merchant calls the app, read from its keyword-padded App Store title ("Preorder, Back In Stock ‑ STOQ" is `STOQ`). A developer's listings that share a brand keep the words that tell them apart (`Swym Wishlist Plus`). Equal to `name` until derived. Example: `Klaviyo`. | | `data[].app.category` | Category | One of 28 values (see [Data model](https://installgraph.com/docs/data-model.md)). | | `data[].app.kind` | string | `shopify-app`: an app installed from Shopify. `web`: a website technology installed outside Shopify, by pasting its tag into the theme or through a tag manager (Google Tag Manager, Meta Pixel, Hotjar). A vendor with both has one entry of each (`meta-pixel` and `meta-facebook-instagram-app`). One of `shopify-app`, `web`. | | `data[].types` | string[] | One of `installed`, `removed`. | | `data[].filters` | object | The company filters it was created with. | | `data[].filters.country` | string | | | `data[].filters.region` | string | | | `data[].filters.industry` | string | | | `data[].filters.aov` | string | | | `data[].url` | string | | | `data[].description` | string \| null | | | `data[].status` | string | `disabled` after 3 deliveries in a row failed every retry; create a new one. One of `active`, `disabled`. | | `data[].disabledReason` | string \| null | | | `data[].creditsPerDelivery` | integer | Example: `10`. | | `data[].createdAt` | string (date-time) | | | `data[].lastDeliveredAt` | string (date-time) \| null | | Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 401 | `missing_api_key` | No key was sent. | Send `Authorization: Bearer sfs_…` or `X-API-Key: sfs_…`. | | 401 | `invalid_api_key` | The key is not one we issued. | Check for truncation or whitespace; keys are `sfs_` plus 32 letters and digits. | | 401 | `api_key_revoked` | The key was revoked. | Make a new key at installgraph.com/account. | | 403 | `insufficient_scope` | The key lacks this endpoint's scope. | Use a key with the scope the endpoint lists (`GET /v1/usage` shows yours). | | 403 | `api_key_suspended` | The key is suspended. | Talk to us, quoting `requestId`. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/list-webhooks --- # One webhook `GET /v1/webhooks/{id}` Returns one webhook's app, event types, filters, URL and status, **free**. The secret is never shown again. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** `events:read` - **Cost:** Free. Use it to check a single subscription's status; `GET /v1/webhooks/{id}/deliveries` shows what was sent. Free. Scope `events:read`. ## Request ```bash curl "https://api.installgraph.com/v1/webhooks/6731b2e4c9a1f20d8e4b7c15" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` ### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | | ## Response `200`. The example's stores are fictional. ```json { "data": { "id": "6731b2e4c9a1f20d8e4b7c15", "app": { "slug": "klaviyo", "name": "Klaviyo", "category": "email-sms" }, "types": [ "removed" ], "filters": { "country": "US" }, "url": "https://example.com/hooks/installgraph", "description": null, "status": "active", "disabledReason": null, "creditsPerDelivery": 10, "createdAt": "2026-11-10T12:00:00.000Z", "lastDeliveredAt": "2026-11-12T04:21:07.000Z" }, "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "creditsUsed": 0, "creditsCharged": 0, "creditsRemaining": 2480, "freeRepulls": 0 } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data.id` | string | | | `data.app` | AppSummary | | | `data.app.slug` | string | Example: `klaviyo`. | | `data.app.name` | string | The catalogue's name: our own for apps we curate, else the App Store listing title as written. Example: `Klaviyo`. | | `data.app.displayName` | string | What a merchant calls the app, read from its keyword-padded App Store title ("Preorder, Back In Stock ‑ STOQ" is `STOQ`). A developer's listings that share a brand keep the words that tell them apart (`Swym Wishlist Plus`). Equal to `name` until derived. Example: `Klaviyo`. | | `data.app.category` | Category | One of 28 values (see [Data model](https://installgraph.com/docs/data-model.md)). | | `data.app.kind` | string | `shopify-app`: an app installed from Shopify. `web`: a website technology installed outside Shopify, by pasting its tag into the theme or through a tag manager (Google Tag Manager, Meta Pixel, Hotjar). A vendor with both has one entry of each (`meta-pixel` and `meta-facebook-instagram-app`). One of `shopify-app`, `web`. | | `data.types` | string[] | One of `installed`, `removed`. | | `data.filters` | object | The company filters it was created with. | | `data.filters.country` | string | | | `data.filters.region` | string | | | `data.filters.industry` | string | | | `data.filters.aov` | string | | | `data.url` | string | | | `data.description` | string \| null | | | `data.status` | string | `disabled` after 3 deliveries in a row failed every retry; create a new one. One of `active`, `disabled`. | | `data.disabledReason` | string \| null | | | `data.creditsPerDelivery` | integer | Example: `10`. | | `data.createdAt` | string (date-time) | | | `data.lastDeliveredAt` | string (date-time) \| null | | Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 404 | `not_found` | No such endpoint, app, or store (or not scanned yet). | Check the slug or domain. Store 404s are free within the lookup-miss allowance. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/get-webhook --- # Delete a webhook `DELETE /v1/webhooks/{id}` Stops a webhook at once, **free**, cancelling queued changes and refunding any already charged. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** `events:read` - **Cost:** Free; changes still queued are cancelled and any already paid for are refunded. Use it to stop a subscription, or to change one: delete it and create a new one with the new app, filters or URL. Stops it at once. Changes still queued are cancelled, and any already paid for are refunded. Free. ## Request ```bash curl -X DELETE "https://api.installgraph.com/v1/webhooks/6731b2e4c9a1f20d8e4b7c15" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` ### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | | ## Response `200`. The example's stores are fictional. ```json { "data": { "id": "6731b2e4c9a1f20d8e4b7c15", "deleted": true, "cancelledDeliveries": 0 }, "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "creditsUsed": 0, "creditsCharged": 0, "creditsRemaining": 2480, "freeRepulls": 0 } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data.id` | string | | | `data.deleted` | boolean | | | `data.cancelledDeliveries` | integer | | Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 404 | `not_found` | No such endpoint, app, or store (or not scanned yet). | Check the slug or domain. Store 404s are free within the lookup-miss allowance. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/delete-webhook --- # Recent deliveries `GET /v1/webhooks/{id}/deliveries` Lists a webhook's latest deliveries (up to **100**, kept 90 days) with each one's status, attempts and your endpoint's last response, **free**. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** `events:read` - **Cost:** Free. Use it to debug your endpoint: `lastStatus` and `lastError` show what it answered, and `nextAttemptAt` when we will retry. The webhook's latest deliveries, newest first, kept 90 days. Free. Scope `events:read`. ## Request ```bash curl "https://api.installgraph.com/v1/webhooks/6731b2e4c9a1f20d8e4b7c15/deliveries?limit=1" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` ### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | | ### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | No | 1–100. Default `50`. | ## Response `200`. The example's stores are fictional. ```json { "data": [ { "id": "6733d1a07be42c19f5a8e301", "eventId": "evt_6720a4c3f1b2", "type": "removed", "domain": "example.com", "status": "delivered", "attempts": 1, "lastStatus": 204, "lastError": null, "credits": 10, "refunded": false, "createdAt": "2026-11-12T04:21:05.000Z", "lastAttemptAt": "2026-11-12T04:21:07.000Z", "nextAttemptAt": null, "deliveredAt": "2026-11-12T04:21:07.000Z" } ], "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "creditsUsed": 0, "creditsCharged": 0, "creditsRemaining": 2480, "freeRepulls": 0 } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data[].id` | string | The payload's `id` and the `Installgraph-Webhook-Id` header. | | `data[].eventId` | string | The event's id in `GET /v1/events`. | | `data[].type` | string | One of `installed`, `removed`. | | `data[].domain` | string | | | `data[].status` | string | `failed`: every retry failed (refunded). `expired`: your balance did not cover it within 7 days (never charged). `cancelled`: the webhook was deleted or disabled first. One of `pending`, `awaiting_credits`, `delivered`, `failed`, `expired`, `cancelled`. | | `data[].attempts` | integer | | | `data[].lastStatus` | integer \| null | Your endpoint's HTTP status on the last attempt. | | `data[].lastError` | string \| null | | | `data[].credits` | integer | | | `data[].refunded` | boolean | | | `data[].createdAt` | string (date-time) | | | `data[].lastAttemptAt` | string (date-time) \| null | | | `data[].nextAttemptAt` | string (date-time) \| null | | | `data[].deliveredAt` | string (date-time) \| null | | Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 400 | `invalid_request` | A parameter or body field is missing or malformed. | Fix the request; `message` names the field. Do not retry unchanged. | | 404 | `not_found` | No such endpoint, app, or store (or not scanned yet). | Check the slug or domain. Store 404s are free within the lookup-miss allowance. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/list-webhook-deliveries --- # Send a sample delivery now `POST /v1/webhooks/{id}/test` Sends a signed sample change to your webhook's URL now and reports what your endpoint answered, **free**. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** `events:read` - **Cost:** Free; never charged or retried. Use it after creating a webhook to check your endpoint is reachable and your signature check passes. The sample has `data.test: true` and the domain `example.com`. POSTs a signed sample (`data.domain` `example.com`, `data.test: true`) to the webhook's URL now and reports what came back, so you can check your endpoint and signature check. Free, never retried. ## Request ```bash curl -X POST "https://api.installgraph.com/v1/webhooks/6731b2e4c9a1f20d8e4b7c15/test" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` ### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | | ## Response `200`. The example's stores are fictional. ```json { "data": { "delivered": true, "status": 204, "error": null, "body": { "id": "test_6733d2f17be42c19f5a8e3a9", "type": "technology.removed", "subscriptionId": "6731b2e4c9a1f20d8e4b7c15", "createdAt": "2026-11-10T12:01:00.000Z", "data": { "id": "test", "type": "removed", "domain": "example.com", "app": { "slug": "klaviyo", "name": "Klaviyo", "category": "email-sms" }, "detectedAt": "2026-11-10T12:01:00.000Z", "previousScanAt": "2026-11-03T12:01:00.000Z", "lastSeenAt": null, "country": "US", "region": "north-america", "industry": null, "pricePoint": null, "test": true } } }, "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "creditsUsed": 0, "creditsCharged": 0, "creditsRemaining": 2480, "freeRepulls": 0 } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data.delivered` | boolean | | | `data.status` | integer \| null | | | `data.error` | string \| null | | | `data.body` | WebhookPayload | | | `data.body.id` | string | The delivery id; the same on every retry. Deduplicate on it. | | `data.body.type` | string | One of `technology.installed`, `technology.removed`. | | `data.body.subscriptionId` | string | | | `data.body.createdAt` | string (date-time) | When this attempt was sent. | | `data.body.data` | Event | | | `data.body.data.id` | string | | | `data.body.data.type` | string | One of `installed`, `removed`. | | `data.body.data.domain` | string | | | `data.body.data.app` | AppSummary | | | `data.body.data.detectedAt` | string (date-time) | The scan that saw the change. | | `data.body.data.previousScanAt` | string (date-time) \| null | The healthy scan before it. | | `data.body.data.lastSeenAt` | string (date-time) \| null | For a removal, the last scan that still saw the app. | | `data.body.data.country` | string \| null | The store's current country. | | `data.body.data.region` | Region \| null | One of `north-america`, `latin-america`, `europe`, `middle-east-africa`, `asia-pacific`. | | `data.body.data.industry` | Industry \| null | Our reading of what the store sells, from its homepage, in the Store Leads taxonomy. | | `data.body.data.pricePoint` | PricePoint \| null | The store's typical order value, in USD bands so it reads the same in every country. Our own reading of the homepage: the product prices shown there (converted from the store's currency), and what is sold. | Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 404 | `not_found` | No such endpoint, app, or store (or not scanned yet). | Check the slug or domain. Store 404s are free within the lookup-miss allowance. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/test-webhook --- # The app catalogue `GET /v1/apps` Lists every app we recognise by slug, with its category, App Store link and rating, **free**. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** `apps:read` - **Cost:** Free. Use it to find an app's `slug` for the other endpoints, or to list one category with `category`. Every app we recognise, by slug. Free. Scope `apps:read`. With `q`, apps by name as merchants know them, ranked: exact `displayName`, then exact `brand`, exact alias, `displayName` by prefix, `brand` or alias by prefix, and last a word of the listing title; within each, most installed first. `q` is matched by prefix, ignoring case, spaces and punctuation (`judge.me`, `Judge Me` and `judgeme` are the same). Pages follow the ranking (pass back `nextCursor` with the same `q`). ## Request ```bash curl "https://api.installgraph.com/v1/apps?category=email-sms&limit=1" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` ### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `q` | string | No | An app's name, brand, former title or abbreviation, or the start of one: `klaviyo`, `stoq`, `swym wish`, `revenuehunt`. At least 2 letters or digits. Example: `stoq`. | | `category` | Category | No | One of 28 values (see [Data model](https://installgraph.com/docs/data-model.md)). | | `limit` | integer | No | 1–500. Default `100`. | | `cursor` | string | No | `pagination.nextCursor` from the previous page, unchanged, within 24 hours, with the same key's account and the same filters. | ## Response `200`. The example's stores are fictional. ```json { "data": [ { "slug": "klaviyo", "name": "Klaviyo", "category": "email-sms", "website": "klaviyo.com", "appStoreUrl": "https://apps.shopify.com/klaviyo-email-marketing", "iconUrl": "https://cdn.shopify.com/app-store/listing_images/klaviyo/icon.png", "rating": 4.6, "reviewCount": 2730 } ], "pagination": { "nextCursor": "eyJrIjoiYXBwcyIsInMiOiJrbGF2aXlvIn0", "hasMore": true }, "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "creditsUsed": 0, "creditsCharged": 0, "creditsRemaining": 46, "freeRepulls": 0 } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data[].slug` | string | Example: `klaviyo`. | | `data[].name` | string | The catalogue's name: our own for apps we curate, else the App Store listing title as written. Example: `Klaviyo`. | | `data[].displayName` | string | What a merchant calls the app, read from its keyword-padded App Store title ("Preorder, Back In Stock ‑ STOQ" is `STOQ`). A developer's listings that share a brand keep the words that tell them apart (`Swym Wishlist Plus`). Equal to `name` until derived. Example: `Klaviyo`. | | `data[].category` | Category | One of 28 values (see [Data model](https://installgraph.com/docs/data-model.md)). | | `data[].kind` | string | `shopify-app`: an app installed from Shopify. `web`: a website technology installed outside Shopify, by pasting its tag into the theme or through a tag manager (Google Tag Manager, Meta Pixel, Hotjar). A vendor with both has one entry of each (`meta-pixel` and `meta-facebook-instagram-app`). One of `shopify-app`, `web`. | | `data[].website` | string \| null | Example: `klaviyo.com`. | | `data[].appStoreUrl` | string \| null | Example: `https://apps.shopify.com/klaviyo-email-marketing`. | | `data[].iconUrl` | string \| null | App Store icon (Shopify CDN); append ?width=72&height=72 to resize. | | `data[].rating` | number \| null | Average App Store rating, 1–5. | | `data[].reviewCount` | integer \| null | | | `data[].brand` | string \| null | The vendor merchants know; null until derived. Example: `Swym`. | | `data[].aliases` | string[] | Other names the app goes by: the listing title, former titles, Partner handles, abbreviations in the title (`RH`). | Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). Lists page with `limit` and `cursor`. See [Pagination](https://installgraph.com/docs/pagination.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 400 | `invalid_request` | A parameter or body field is missing or malformed. | Fix the request; `message` names the field. Do not retry unchanged. | | 400 | `invalid_cursor` | The `cursor` is not one this endpoint issued (edited, truncated, or from another list). | Pass `pagination.nextCursor` back unchanged, or start again without a cursor. | | 401 | `missing_api_key` | No key was sent. | Send `Authorization: Bearer sfs_…` or `X-API-Key: sfs_…`. | | 401 | `invalid_api_key` | The key is not one we issued. | Check for truncation or whitespace; keys are `sfs_` plus 32 letters and digits. | | 401 | `api_key_revoked` | The key was revoked. | Make a new key at installgraph.com/account. | | 403 | `insufficient_scope` | The key lacks this endpoint's scope. | Use a key with the scope the endpoint lists (`GET /v1/usage` shows yours). | | 403 | `api_key_suspended` | The key is suspended. | Talk to us, quoting `requestId`. | | 429 | `rate_limited` | Too many requests this minute. | Wait `Retry-After` seconds, then retry. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/list-apps --- # The industry taxonomy, regions and order-value bands `GET /v1/industries` Lists the values the `industry`, `region` and `aov` filters accept: **26** industries with **182** subcategories, 5 regions and 7 order-value bands, **free**. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** none - **Cost:** Free. Read it once and cache it: it changes rarely. The values `industry` and `region` accept. Industries are the Store Leads category taxonomy at two levels (26 categories, 182 subcategories); each store's industry is our own reading of its homepage. Regions group the countries stores sell from. Free; any key. ## Request ```bash curl "https://api.installgraph.com/v1/industries" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` ## Response `200`. The example's stores are fictional. ```json { "data": { "industries": [ { "name": "Apparel", "path": "/Apparel", "subcategories": [ { "name": "Footwear", "path": "/Apparel/Footwear" } ] } ], "regions": [ { "id": "north-america", "name": "North America", "countries": [ "US", "CA" ] } ], "aovBands": [ { "id": "100-200", "label": "$100–200" } ] }, "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "creditsUsed": 0, "creditsCharged": 0, "creditsRemaining": 46, "freeRepulls": 0 } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data.industries` | object[] | | | `data.industries[].name` | string | Example: `Apparel`. | | `data.industries[].path` | string | Example: `/Apparel`. | | `data.industries[].subcategories` | object[] | | | `data.industries[].subcategories[].name` | string | Example: `Footwear`. | | `data.industries[].subcategories[].path` | string | Example: `/Apparel/Footwear`. | | `data.regions` | object[] | | | `data.regions[].id` | Region | One of `north-america`, `latin-america`, `europe`, `middle-east-africa`, `asia-pacific`. | | `data.regions[].name` | string | Example: `North America`. | | `data.regions[].countries` | string[] | | | `data.aovBands` | object[] | | | `data.aovBands[].id` | string | Example: `100-200`. | | `data.aovBands[].label` | string | Example: `$100–200`. | Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 401 | `missing_api_key` | No key was sent. | Send `Authorization: Bearer sfs_…` or `X-API-Key: sfs_…`. | | 401 | `invalid_api_key` | The key is not one we issued. | Check for truncation or whitespace; keys are `sfs_` plus 32 letters and digits. | | 401 | `api_key_revoked` | The key was revoked. | Make a new key at installgraph.com/account. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/list-industries --- # App popularity by category, region and industry `GET /v1/public/stats` Returns, with **no key**, how many stores run each app category and its ten most installed apps, broken down by region and industry. - **Auth:** none - **Cost:** Free; no key. 30 requests a minute per IP. Use it for aggregate charts and landing pages. It never names a store; for stores, use a key. No key. Recomputed daily over every store we scanned. Per app category: how many stores run any app in it, and its ten most installed apps, each broken down by region and top-level industry (`unknown` for stores without a country or an industry reading yet). 30 requests a minute per IP; cacheable for an hour. ## Request ```bash curl "https://api.installgraph.com/v1/public/stats?category=email-sms" ``` ### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `category` | Category | No | One of 28 values (see [Data model](https://installgraph.com/docs/data-model.md)). | ## Response `200`. The example's stores are fictional. ```json { "data": { "computedAt": "2026-11-06T02:00:00.000Z", "companies": 412806, "byRegion": { "north-america": 221034, "europe": 109877, "asia-pacific": 52410, "latin-america": 14988, "middle-east-africa": 9877, "unknown": 4620 }, "byIndustry": { "Apparel": 88312, "Beauty & Fitness": 51207, "unknown": 30114 }, "regions": { "north-america": "North America", "europe": "Europe" }, "categories": [ { "category": "email-sms", "companies": 198440, "byRegion": { "north-america": 112307, "europe": 50122 }, "byIndustry": { "Apparel": 47210 }, "apps": [ { "slug": "klaviyo", "name": "Klaviyo", "category": "email-sms", "companies": 96120, "share": 0.2328, "byRegion": { "north-america": 58014 }, "byIndustry": { "Apparel": 24877 } } ] } ] }, "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14" } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data.computedAt` | string (date-time) | | | `data.companies` | integer | | | `data.byRegion` | Breakdown | Store counts by region id or industry name, with `unknown` for stores lacking one. | | `data.byIndustry` | Breakdown | Store counts by region id or industry name, with `unknown` for stores lacking one. | | `data.regions` | object | Region id to name. | | `data.categories` | object[] | | | `data.categories[].category` | Category | One of 28 values (see [Data model](https://installgraph.com/docs/data-model.md)). | | `data.categories[].companies` | integer | | | `data.categories[].byRegion` | Breakdown | Store counts by region id or industry name, with `unknown` for stores lacking one. | | `data.categories[].byIndustry` | Breakdown | Store counts by region id or industry name, with `unknown` for stores lacking one. | | `data.categories[].apps` | object[] | | | `data.categories[].apps[].slug` | string | | | `data.categories[].apps[].name` | string | | | `data.categories[].apps[].kind` | string | One of `shopify-app`, `web`. | | `data.categories[].apps[].companies` | integer | | | `data.categories[].apps[].share` | number | Share of all stores counted, 0–1. | | `data.categories[].apps[].byRegion` | Breakdown | Store counts by region id or industry name, with `unknown` for stores lacking one. | | `data.categories[].apps[].byIndustry` | Breakdown | Store counts by region id or industry name, with `unknown` for stores lacking one. | ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 404 | `not_found` | No such endpoint, app, or store (or not scanned yet). | Check the slug or domain. Store 404s are free within the lookup-miss allowance. | | 429 | `rate_limited` | Too many requests this minute. | Wait `Retry-After` seconds, then retry. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/get-public-stats --- # Your balance and recent usage `GET /v1/usage` Returns your key's credit balance, rate limit, prices and the last **30 days** of usage, **free**. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** none - **Cost:** Free. Check it before a large pull, or to reconcile what a job spent. Free, and needs no scope. ## Request ```bash curl "https://api.installgraph.com/v1/usage" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" ``` ## Response `200`. The example's stores are fictional. ```json { "data": { "key": { "prefix": "sfs_01234567", "owner": "you@company.com", "name": "Default", "scopes": [ "companies:read", "apps:read", "events:read" ] }, "credits": { "balance": 46, "unlimited": false }, "rateLimit": { "perMinute": 60, "remaining": 59, "resetAt": "2026-11-06T10:15:00.000Z" }, "prices": { "GET /v1/companies/{domain}": { "perRequest": 1 }, "GET /v1/apps/{slug}/companies": { "perRow": 1 } }, "last30Days": { "since": "2026-10-07T10:14:12.000Z", "requests": 3, "credits": 4, "byEndpoint": [ { "endpoint": "GET /v1/apps/{slug}/companies", "requests": 1, "rows": 2, "credits": 2 }, { "endpoint": "GET /v1/companies/{domain}", "requests": 1, "rows": 1, "credits": 1 }, { "endpoint": "GET /v1/events", "requests": 1, "rows": 1, "credits": 1 } ] }, "recent": [ { "at": "2026-11-06T10:14:02.000Z", "type": "usage", "amount": -1, "credits": 1, "balanceAfter": 46, "endpoint": "GET /v1/events", "rows": 1, "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "note": null }, { "at": "2026-11-05T16:20:41.000Z", "type": "grant", "amount": 25, "credits": 25, "balanceAfter": 50, "endpoint": null, "rows": null, "requestId": null, "note": "Verified email" } ] }, "meta": { "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14", "creditsUsed": 0, "creditsCharged": 0, "creditsRemaining": 46, "freeRepulls": 0 } } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | `data.key` | object | | | `data.key.prefix` | string | Example: `sfs_Ab12Cd34`. | | `data.key.owner` | string | | | `data.key.name` | string \| null | | | `data.key.scopes` | string[] | One of `companies:read`, `apps:read`, `events:read`. | | `data.credits` | object | | | `data.credits.balance` | integer \| null | Null for unlimited keys. | | `data.credits.unlimited` | boolean | | | `data.rateLimit` | object | | | `data.rateLimit.perMinute` | integer | | | `data.rateLimit.remaining` | integer | | | `data.rateLimit.resetAt` | string (date-time) | | | `data.prices` | object | Credits per endpoint, `{ perRequest }` or `{ perRow }`. | | `data.last30Days` | object | | | `data.last30Days.since` | string (date-time) | | | `data.last30Days.requests` | integer | Billed requests (free ones are not recorded). | | `data.last30Days.credits` | integer | | | `data.last30Days.byEndpoint` | object[] | | | `data.last30Days.byEndpoint[].endpoint` | string | | | `data.last30Days.byEndpoint[].requests` | integer | | | `data.last30Days.byEndpoint[].rows` | integer | | | `data.last30Days.byEndpoint[].credits` | integer | | | `data.recent` | object[] | The latest 50 ledger entries, newest first. | | `data.recent[].at` | string (date-time) | | | `data.recent[].type` | string | One of `usage`, `grant`, `purchase`, `refund`, `adjustment`. | | `data.recent[].amount` | integer | Change to the balance. | | `data.recent[].credits` | integer | | | `data.recent[].balanceAfter` | integer \| null | | | `data.recent[].endpoint` | string \| null | | | `data.recent[].rows` | integer \| null | | | `data.recent[].requestId` | string \| null | | | `data.recent[].note` | string \| null | | Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 401 | `missing_api_key` | No key was sent. | Send `Authorization: Bearer sfs_…` or `X-API-Key: sfs_…`. | | 401 | `invalid_api_key` | The key is not one we issued. | Check for truncation or whitespace; keys are `sfs_` plus 32 letters and digits. | | 401 | `api_key_revoked` | The key was revoked. | Make a new key at installgraph.com/account. | | 403 | `insufficient_scope` | The key lacks this endpoint's scope. | Use a key with the scope the endpoint lists (`GET /v1/usage` shows yours). | | 403 | `api_key_suspended` | The key is suspended. | Talk to us, quoting `requestId`. | | 429 | `rate_limited` | Too many requests this minute. | Wait `Retry-After` seconds, then retry. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/get-usage --- # Prices, packs and rules (no key) `GET /v1/pricing` Returns prices, credit packs, free credits and limits as JSON with **no key**: the same table as installgraph.com/pricing. - **Auth:** none - **Cost:** Free; no key. CORS open. Read it before spending, so a script or agent quotes prices from the source rather than from memory. Machine-readable pricing, the same as installgraph.com/pricing. CORS open. ## Request ```bash curl "https://api.installgraph.com/v1/pricing" ``` ## Response `200`. The example's stores are fictional. ```json { "data": { "unit": "1 credit unlocks 1 store: a row in an app's list, or a store's full app stack from a domain lookup.", "prices": { "GET /v1/companies/{domain}": { "creditsPerStore": 1, "note": "Full app stack. `include=history` adds 1." } }, "rules": { "chargedOnlyForStoresFound": true, "freeRepullDays": 7, "countsFree": true, "creditsExpire": true, "creditExpiryDays": 365, "refundable": false }, "free": { "instantCredits": 25, "verifiedEmailCredits": 25, "note": "The verified-email credits are granted once per company email domain." }, "packs": [ { "id": "starter", "name": "Starter", "priceUsd": 50, "credits": 500, "usdPer1000Stores": 100, "buyUrl": "https://installgraph.com/account?buy=starter" } ], "autoTopUp": { "default": "off", "spendCapUsd": 1000, "spendCapDays": 30 }, "limits": { "storesPerWindow": 40000, "windowDays": 30 }, "talkToUs": "https://installgraph.com/talk-to-us", "signUp": { "endpoint": "POST /v1/keys", "body": { "email": "you@company.com" } } } } ``` Source: https://installgraph.com/docs/reference/get-pricing --- # Buy a credit pack on the saved card `POST /v1/billing/purchase` Buys a credit pack on the account's saved card, from **$50** for 500 credits, only if the account owner has turned agent purchases on. - **Auth:** API key ([Authentication](https://installgraph.com/docs/authentication.md)) - **Scope:** none - **Cost:** Charges the account's saved card the pack price ($50 to $1,000). Use it when a job runs out of credits (`402 insufficient_credits`) and the owner has allowed it. Otherwise hand `error.offers.pack.buyUrl` to a person. Buys `starter` ($50, 500 credits), `growth` ($150, 2,500), `pro` ($400, 10,000) or `scale` ($1,000, 40,000) on the card saved at the account's first checkout. Off until the account owner turns on agent purchases and sets per-purchase and 30-day limits (`403 purchase_not_allowed`). ## Request ```bash curl -X POST "https://api.installgraph.com/v1/billing/purchase" \ -H "Authorization: Bearer $INSTALLGRAPH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"pack":"starter"}' ``` ### Body JSON. | Field | Type | Description | | --- | --- | --- | | `pack` | string | One of `starter`, `growth`, `pro`, `scale`. | ## Response `200`. The example's stores are fictional. ```json { "data": { "pack": "starter", "credits": 500, "priceUsd": 50, "balance": 546 } } ``` Every keyed response also carries `meta` (`requestId`, `creditsUsed`, `creditsRemaining`, `freeRepulls`) and the `X-Credits-*` and `X-RateLimit-*` headers. See [Credits](https://installgraph.com/docs/credits.md) and [Rate limits](https://installgraph.com/docs/rate-limits.md). ## Errors | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 402 | `spend_cap_reached` | Purchases would pass $1,000 in 30 days. | Talk to us for more: `error.offers.talkToUs.url`. | | 402 | `payment_failed` | The saved card was declined. | Ask the account owner to update the card at installgraph.com/account. | | 403 | `purchase_not_allowed` | Agent purchases are off for this account, the pack is over the owner's limits, or the key has no account. | Hand `error.offers.pack.buyUrl` to a person; the owner sets limits at `error.settings`. | | 409 | `no_payment_method` | No card is saved, so nothing can be bought off-session. | Buy a pack once at installgraph.com/account to save a card. | All codes: [Errors](https://installgraph.com/docs/errors.md). Source: https://installgraph.com/docs/reference/agent-purchase