Webhooks
Get a message in Slack, Discord or your own server when an update goes live or fails, new reviews arrive, or StoreFast can't reach Partner Center.
- 1
Add the webhook
In Settings, under Slack, Discord and webhooks, click Add. Pick Slack, Discord or Webhook, paste the URL and choose what to send.
- 2
Copy the signing secret
A generic webhook gets a signing secret that starts with
whsec_. It's shown once, so keep it with your server's secrets. - 3
Send a test
Click Send test. The webhook shows how the last delivery went.
Which URLs work
- Slack: an incoming webhook URL, starting with
https://hooks.slack.com/services/. - Discord: a webhook URL, starting with
https://discord.com/api/webhooks/. - Webhook: any
https://URL on a public domain name, without a port, user name or password. IP addresses and local names aren't allowed. - Up to 5 webhooks per account.
Events
update.live- An update or listing change is live in the Store.
update.failed- A submission failed certification or Partner Center couldn't process it.
reviews.new- New reviews arrived, at most once a day per app, with up to 5 of them.
connection.problem- Syncing has failed for about a day, or a Partner Center key expires soon or has expired.
test- Sent by Send test, with only a
url.
Events go out after the sync that finds them: the nightly sync, Sync now, or the checks StoreFast makes after you submit. Slack and Discord get a formatted message; a generic webhook gets the JSON below.
Payload
Every delivery is a POST with a JSON body: a unique id per delivery, the event type, createdAt and the event's data. url in data opens the page on StoreFast.
POST /your/endpoint
Content-Type: application/json
User-Agent: StoreFast-Webhooks/1.0
X-StoreFast-Event: update.live
X-StoreFast-Delivery: 5b0e7d6a-3c1f-4f8e-9a2b-7d4c1e9f0a36
X-StoreFast-Timestamp: 1791763200
X-StoreFast-Signature: sha256=8f3c...
{
"id": "5b0e7d6a-3c1f-4f8e-9a2b-7d4c1e9f0a36",
"type": "update.live",
"createdAt": "2026-10-12T07:00:00.000Z",
"data": {
"app": { "id": "3f2b7c1e-8a4d-4e6b-9c2f-5d1a0b7e6c94", "name": "My App" },
"version": "1.4.0.0",
"listingOnly": false,
"storeUrl": "https://apps.microsoft.com/detail/9NBLGGH4R315",
"url": "https://storefast.app/app/3f2b7c1e-8a4d-4e6b-9c2f-5d1a0b7e6c94"
}
}{
"app": { "id": "3f2b7c1e-8a4d-4e6b-9c2f-5d1a0b7e6c94", "name": "My App" },
"version": "1.4.0.0",
"status": "CertificationFailed",
"errors": ["The app crashed on launch."],
"url": "https://storefast.app/app/3f2b7c1e-8a4d-4e6b-9c2f-5d1a0b7e6c94"
}{
"app": { "id": "3f2b7c1e-8a4d-4e6b-9c2f-5d1a0b7e6c94", "name": "My App" },
"count": 2,
"average": 4.5,
"reviews": [
{ "rating": 5, "title": "Great", "text": "Does what it says.", "language": null, "version": "1.4.0.0" },
{ "rating": 4, "title": "Good", "text": "Fast, but needs a dark mode.", "language": "de", "version": "1.3.0.0" }
],
"url": "https://storefast.app/app/3f2b7c1e-8a4d-4e6b-9c2f-5d1a0b7e6c94"
}Review title and text are in English; language is the original language when StoreFast translated it, otherwise null. Review text is written by your app's users, so treat it as untrusted.
{
"problem": "sync_failing",
"message": "Syncing failed two nights in a row.",
"url": "https://storefast.app/app/settings"
}problem is sync_failing, key_expiring or key_expired.
Check the signature
X-StoreFast-Signature is sha256= and the hex HMAC-SHA256 of the X-StoreFast-Timestamp header, a dot and the raw body, keyed with your signing secret. Compare it in constant time, and refuse old timestamps so a captured request can't be replayed.
import { createHmac, timingSafeEqual } from 'node:crypto'
// body is the raw request body as a string, before any JSON parsing.
export function verify(body, headers, secret) {
const timestamp = headers['x-storefast-timestamp']
const signature = headers['x-storefast-signature'] ?? ''
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
const expected = 'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex')
return signature.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}import hashlib, hmac, time
# body is the raw request body as bytes, before any JSON parsing.
def verify(body: bytes, headers, secret: str) -> bool:
timestamp = headers.get("X-StoreFast-Timestamp", "")
signature = headers.get("X-StoreFast-Signature", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
message = timestamp.encode() + b"." + body
expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)Delivery
- Your server has 5 seconds to answer with a 2xx status.
- Redirects aren't followed, so give the final URL.
- Each event is sent once, with no retries. Use
idorX-StoreFast-Deliveryto drop repeats. - Settings shows the last delivery's status code or a short reason, never your server's response body.
Stuck on something? Email hello@storefast.app and we'll help you get it working.