--- url: https://docs.youcan.shop/store-admin/resthooks/overview.md --- # REST Hooks ## Introduction REST Hooks itself is not a specification, but rather a collection of patterns that treat webhooks like subscriptions. the REST Hook subscriptions are created, updated or deleted using a REST API. With REST Hooks, the REST API is able to communicate with other apps in real time, via webhooks, without a complicated setup. ### The REST Hooks pattern has four basic requirements * Mechanism to store subscriptions * Mechanism to modify subscriptions via API * List of event types & implementation of events * Mechanism to send hooks ## Available events | Event | Description | | --- | --- | | `order.created` | Triggered when a new order is created. | | `order.updated` | Triggered when an existing order is updated. | | `order.paid` | Triggered when an order is marked as paid. | | `product.inventory.low` | Triggered when a product inventory becomes low. | | `cart.abandoned` | Triggered when a cart is flagged as abandoned. | | `upsell.accepted` | Triggered when a customer accepts an upsell. | | `app.uninstalled` | Triggered when a seller uninstalls your app. | | `app.charge_updated` | Triggered when the status of one of your app charges changes. | The events `order.create`, `inventory.low`, and `upsell.accept` are deprecated aliases. Do not subscribe to them in new code. ## Payload format Every delivery is a `POST` request with a JSON body in this shape: ```json { "event_name": "order.created", "event_happened_at": "2026-08-08T21:30:00.000000Z", "data": {} } ``` `data` holds the event object: the order for `order.*` events, the [abandoned cart](/store-admin/entities/abandoned-cart) for `cart.abandoned`, the charge for `app.charge_updated`, and so on. Read your event data from `data`. A trimmed `order.created` delivery: ```json { "event_name": "order.created", "event_happened_at": "2026-08-08T21:30:00.000000Z", "data": { "id": "00000000-0000-0000-0000-000000000000", "ref": "10245", "status": 1, "total": 249.9, "currency": "MAD", "customer": { "id": "...", "first_name": "...", "email": "..." }, "store_id": "00000000-0000-0000-0000-000000000000" } } ``` An `app.uninstalled` delivery: ```json { "event_name": "app.uninstalled", "event_happened_at": "2026-08-08T21:30:00.000000Z", "data": { "store_id": "00000000-0000-0000-0000-000000000000", "store_slug": "my-store", "seller_id": "00000000-0000-0000-0000-000000000000", "app_id": "app_xxxxxxxxxxxxxxxxxxxxxxxxxxx" } } ``` ## Security & Verification ### YouCan Signature Every delivery carries a `X-YOUCAN-SIGNATURE` header: an [hmac](https://en.wikipedia.org/wiki/HMAC) `sha256` hash of the raw request body, signed with the secret of the OAuth client the subscription belongs to. Compute the hash over the raw request body, before any JSON parsing. Re-encoding a parsed body does not reproduce the same bytes. Validating the signature in PHP: ```PHP function isValidYouCanSignature(string $signature, string $rawBody, string $signingKey): bool { $expectedSignature = hash_hmac( 'sha256', // Hashing Algorithm $rawBody, // Raw request body $signingKey // OAuth Client Secret Key ); return hash_equals($expectedSignature, $signature); } ``` The same validation in TypeScript, as used by the [app template](https://github.com/youcan-shop/shop-app-template-nuxt): ```ts import { Buffer } from 'node:buffer'; import crypto from 'node:crypto'; function isValidYouCanSignature(signature: string, rawBody: string, signingKey: string): boolean { const expected = crypto.createHmac('sha256', signingKey).update(rawBody).digest('hex'); const received = Buffer.from(signature); const computed = Buffer.from(expected); return received.length === computed.length && crypto.timingSafeEqual(received, computed); } ``` ### Delivery headers Each delivery also carries these headers: | Header | Content | | --- | --- | | `X-YOUCAN-SIGNATURE` | The [signature](#youcan-signature) of the payload. | | `X-YOUCAN-TOPIC` | The event name. | | `X-YOUCAN-DELIVERY-ID` | A unique id for this delivery. Retries of the same delivery keep the same id, use it to deduplicate. | | `X-YOUCAN-API-VERSION` | The payload version. Currently `v1`. | ## Delivery and retries * Your endpoint must respond with a `2xx` status code. Respond quickly and do the work after. * If your endpoint responds with a `5xx` status code, or the request does not complete, the delivery is retried up to 5 times with an increasing delay, over approximately 4 hours. * If your endpoint responds with a `4xx` status code, the delivery is not retried. * If your endpoint responds with `410 Gone`, the subscription is deactivated. * Subscriptions that fail for a long period are deactivated. We email you when that happens. * To restore a deactivated subscription, make sure the endpoint accepts deliveries again, then [subscribe](/store-admin/resthooks/subscribe) to the same event and address again. For webhooks [declared in an app manifest](/apps/webhooks), releasing a version of the app activates its subscriptions again. * Each app (OAuth client) holds at most 7 active subscriptions per event per store. Deactivated subscriptions do not count.