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

update.live
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"
  }
}
update.failed data
{
  "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"
}
reviews.new data
{
  "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.

connection.problem data
{
  "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.

Node
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))
}
Python
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 id or X-StoreFast-Delivery to 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.