# 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).
