Why use one
Without a webhook, finding who started or stopped using an app means pulling its list (or GET /v1/events) 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
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. 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: 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 to send a free sample and check your endpoint.
What you receive
{
"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 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=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of <t>.<raw request body>, 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.
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);
}
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 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_requiredotherwise). Up to 25 active webhooks per account (409 webhook_limit_reached); for more, talk to us.