- Auth: API key (Authentication)
- 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).
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
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.
{
"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). |
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 and Rate limits.
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.