Webhooks
Webhooks allow your app to receive real-time notifications when specific events occur within a SitePack store. Instead of polling the API for changes, SitePack will "push" data to your server via an HTTPS POST request.
How Webhooks Work
- Subscription: You define the events (topics) you want to listen to in your
app.json. - Event Occurs: A merchant performs an action (e.g., a customer submits a form).
- Delivery: SitePack sends a signed JSON payload to your configured endpoint.
- Acknowledgement: Your server must respond with a
200 OKstatus code to acknowledge receipt.
The "Thin Payload" Strategy
To ensure security and data privacy, SitePack webhooks use a Thin Payload architecture. Instead of sending all the resource data (which might contain sensitive PII), the webhook contains only the topic, the site and the uuid of the record that changed.
Example Payload
{
"uuid": "019eca47-221d-73c1-9c6c-c556a2b02cc2",
"test_mode": true,
"topic": "lead.created",
"site": "019eca47-21a4-7935-8636-641e7370897b",
"created_at": "2026-06-15T07:55:19Z",
"arguments": {
"test": true,
"timestamp": 1781510118,
"message": "This is a test webhook message from the admin panel."
}
}
After receiving this notification, your app should call the API using its credentials to fetch the full data. This ensures that:
- Sensitive data is never sent over the wire unnecessarily.
- The App's Scopes are re-validated before data access.
Webhook topics
Every event carries the uuid of the record that changed in arguments.uuid. Read the record with the endpoint in the last column, using your own scopes. See API Endpoints for a worked example.
Leads and customers
| Topic | When it fires | Read it with |
|---|---|---|
lead.created | A visitor submits a form on the site. | GET /api/public/v1/leads/{uuid} (leads:read) |
lead.updated | A lead's status, rating or notes change. | GET /api/public/v1/leads/{uuid} (leads:read) |
lead.completed | A lead is closed as won or lost. Fires after lead.updated. | GET /api/public/v1/leads/{uuid} (leads:read) |
lead.deleted | A lead is deleted. | Nothing to read: remove your copy. |
customer.created | A customer record is created, at checkout or through the API. | GET /api/public/v1/customers/{uuid} (customers:read) |
customer.updated | A customer record changes. | GET /api/public/v1/customers/{uuid} (customers:read) |
newsletter.subscribed | Someone signs up for the newsletter. | No endpoint yet. |
newsletter.confirmed | A newsletter sign-up is confirmed. | No endpoint yet. |
newsletter.unsubscribed | Someone unsubscribes. | No endpoint yet. |
comment.updated | A comment or review is moderated. | GET /api/public/v1/comments (comments:read) |
comment.deleted | A comment or review is deleted. | Nothing to read: remove your copy. |
Webshop
| Topic | When it fires | Read it with |
|---|---|---|
order.created | An order is placed, before online payment completes. | GET /api/public/v1/orders/{uuid} (orders:read) |
order.paid | An order's payment reaches "completed". | GET /api/public/v1/orders/{uuid} (orders:read) |
product.created | A product is created. | GET /api/public/v1/products/{uuid} (products:read) |
product.updated | A product's details change. | GET /api/public/v1/products/{uuid} (products:read) |
product.deleted | A product is deleted. | Nothing to read: remove your copy. |
product.stock_adjusted | A product's stock is adjusted. | GET /api/public/v1/products/{uuid}/stock (inventory:read) |
product.option_updated | A product's options (such as size or colour) change. | GET /api/public/v1/products/{uuid}/options (products:read) |
product.variant_updated | A product's variants change (sku, ean, price, stock, image, properties). | GET /api/public/v1/products/{uuid}/variants (products:read) |
product.variant_deleted | A product variant is removed. | GET /api/public/v1/products/{uuid}/variants (products:read) |
collection.created | A collection is created. | GET /api/public/v1/collections/{uuid} (collections:read) |
collection.updated | A collection or its rules change. | GET /api/public/v1/collections/{uuid} (collections:read) |
collection.deleted | A collection is deleted. | Nothing to read: remove your copy. |
For every product.* topic, arguments.uuid is the uuid of the product, also when a variant or option changed. See Product Options & Variants.
Content
| Topic | When it fires | Read it with |
|---|---|---|
content.created, content.updated | A page is created or changed. | GET /api/public/v1/content/{uuid} (content:read) |
content.published, content.unpublished | A page goes online or offline. | GET /api/public/v1/content/{uuid} (content:read) |
content.deleted | A page is deleted. | Nothing to read: remove your copy. |
blog.created, blog.updated | A blog post is created or changed. | GET /api/public/v1/blogs/{uuid} (blogs:read) |
blog.deleted | A blog post is deleted. | Nothing to read: remove your copy. |
category.created, category.updated | A category is created or changed. | GET /api/public/v1/categories/{uuid} (categories:read) |
category.deleted | A category is deleted. | Nothing to read: remove your copy. |
tag.created, tag.updated | A tag is created or changed. | GET /api/public/v1/tags/{uuid} (tags:read) |
tag.deleted | A tag is deleted. | Nothing to read: remove your copy. |
media.updated | A media item's details change. | GET /api/public/v1/media/{uuid} (media:read) |
media.deleted | A media item is deleted. | Nothing to read: remove your copy. |
redirect.created, redirect.updated | A redirect is created or changed. | GET /api/public/v1/redirects/{uuid} (redirects:read) |
redirect.deleted | A redirect is deleted. | Nothing to read: remove your copy. |
translation.updated, translation.deleted | A translation is saved or removed. arguments.uuid is the translation's own uuid. | Translations (translations:read) |
elements.updated | The homepage, footer or sidebar elements change. arguments.uuid is the site uuid. | GET /api/public/v1/elements/{area} (elements:read) |
usp.updated | A USP is created or changed. | GET /api/public/v1/usps/{uuid} (usp:read) |
topbar.updated | The topbar changes. | No endpoint yet. |
event.created, event.updated, event.deleted | An agenda event is created, changed or deleted. | No endpoint yet. |
Site
| Topic | When it fires | Read it with |
|---|---|---|
site.updated | The site settings change. | GET /api/public/v1/site (sites:read) |
module.settings_updated | A module's settings change. arguments.uuid is the site uuid. | GET /api/public/v1/modules (modules:read) |
app.installed, app.uninstalled, app.settings_updated | Your own app is installed, removed or reconfigured. See below. | GET /api/public/v1/app/settings (app:settings:read) |
Topics that do not fire yet
GET /api/public/v1/webhooks lists a few more topics as available. You can subscribe to them, but SitePack does not send them yet: order.updated, product.option_created, product.option_deleted, product.variant_created, discount.created, discount.archived, navigation.updated, shipping.label_created, mailbox.created, mailbox.deleted and domain.ordered. Until they do, poll the matching list endpoint, or use a related topic: a new variant also fires product.variant_updated.
App lifecycle topics
The app.* topics are about your app itself rather than about store data. Unlike the other topics, which go to every active app subscribed to them, an app.* event is delivered only to the app it concerns. Subscribe to them in the webhooks array of your app.json like any other topic; they are signed with your App Secret in the same way.
| Topic | When it fires |
|---|---|
app.installed | On a fresh install of your app on a site. Re-approving an app that is already active (for example after a new version asks for more scopes) does not fire it again. |
app.uninstalled | After the merchant uninstalled your app. It is still delivered to you even though your app is no longer active on the site, so you can clean up. |
app.settings_updated | Every time the merchant saves your app's settings page in the admin. |
The payload is thin, like every other webhook: site is the site uuid and arguments.uuid is your app's uuid.
{
"uuid": "019f4a1c-5e2b-7d10-8a3e-2f6c9b1d0e47",
"test_mode": false,
"topic": "app.settings_updated",
"site": "019eca47-21a4-7935-8636-641e7370897b",
"created_at": "2026-10-01T09:12:44Z",
"arguments": {
"uuid": "019e8b20-3c4d-7a51-9f62-7d8e1a2b3c4d"
}
}
A typical use of app.installed and app.settings_updated is to read the merchant's settings back with GET /api/public/v1/app/settings (merchant_settings, scope app:settings:read), so a changed connection or password setting is picked up right away.
Security and Verification
HTTPS Requirement
All webhook addresses must use the https:// protocol.
Signing (HMAC)
Every webhook request is signed with your Webhook signing secret using HMAC-SHA384. You find it in the Partner Portal on your app's page, in the OAuth & install card, where you can also rotate it. Verify it to be sure the request truly came from SitePack and was not altered.
Each request carries two headers:
| Header | Value |
|---|---|
X-SitePack-Webhook-Timestamp | The unix time SitePack signed the request. |
X-SitePack-Webhook-Signature | v1= followed by the hex HMAC-SHA384 digest. |
The signature is computed over "{timestamp}.{raw_body}". To verify, recompute the HMAC over the raw request body exactly as received (do not re-serialize the JSON) and compare in constant time. Reject a request whose timestamp is too old to blunt replay attacks.
import hmac, hashlib
def is_valid(secret, timestamp, raw_body, header):
expected = "v1=" + hmac.new(
secret.encode(),
f"{timestamp}.{raw_body}".encode(),
hashlib.sha384,
).hexdigest()
return hmac.compare_digest(expected, header)
Delivery and retries
- SitePack posts each event once, with a 5 second connect timeout. Answer with a
2xxquickly and do the real work afterwards, for example in a queue. - When your server does not answer with a
2xx, or cannot be reached, SitePack retries once, about 5 seconds later. After that the event is dropped. - A retry is a new delivery with a new event
uuid. Make your handler safe to run twice: key your work ontopicplusarguments.uuid, not on the eventuuid. - Webhooks are a signal, not a log. If you must not miss anything, also sync from the list endpoints now and then, for example
GET /api/public/v1/leads, which lists the most recently changed leads first.
Configuring Webhooks
Webhooks are part of an API integration: your server needs the signing secret to verify them and an access token to read the data behind them.
Add the webhooks array to your app.json and run sitepack app:publish:
{
"webhooks": [
{
"topic": "lead.created",
"address": "https://your-app.com/webhooks/leads"
}
]
}
Once published, your app's page in the Partner Portal lists the subscriptions under App Webhooks. Click Test next to a topic to send a test message (with "test_mode": true) to your address.
Next Steps
- API Endpoints: the endpoint to call for each topic.
- Authentication (OAuth): How to obtain the tokens needed to fetch data from webhooks.
- Scopes & Permissions: Learn how scopes limit the data you can access via the API.
- App Manifest (app.json): Configure your webhook topics and addresses.
- API Integrations: How webhooks fit in the whole flow.