# 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=<unix seconds>,v1=<hex>`, where `v1` is HMAC-SHA256 of
`<t>.<raw body>` 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).
