External Webhook Endpoint Integration Guide
Overview
Demi sends outbound webhooks when product data changes. Events are batched and delivered to your endpoint as JSON. Each delivery includes a signature header (X-Webhook-Signature) that you must verify to ensure authenticity. Our team will work with you to align on a unique key signature.
Event Summary
- Event type:
product.sync - Delivery format: batch payload
- Actions:
created,updated,deleted
Endpoint Requirements
- HTTPS required for all endpoints
- Respond with 2xx to acknowledge receipt
- Non-2xx responses may trigger retries
- Timeouts are enforced server-side (default 30s)
Request Headers
Every webhook request includes:
Content-Type: application/jsonX-Webhook-Signature: t=timestamp,v1=signatureX-Webhook-ID: <event_id>X-Webhook-Timestamp: <unix_seconds>User-Agent: UpMeals-Webhooks/1.0
Signature Verification (Required)
Use the X-Webhook-Signature header to verify authenticity.
Signature Format
t=timestamp,v1=signature
How to Validate
- Extract timestamp and signature from
X-Webhook-Signature - Create
signedPayloadas:${timestamp}.${rawBody}Note: rawBody must be the exact raw request payload string (not re-serialized JSON). - Compute HMAC-SHA256 using the webhook secret:
signature = HMAC_SHA256(signedPayload, secret) - Compare the computed signature with
v1using a timing-safe comparison - Reject requests if:
- Signature does not match
- Timestamp is outside a 300-second tolerance window
Pseudocode Example
const header = req.headers["x-webhook-signature"];
// Parse: "t=...,v1=..."
const timestamp = ...;
const signature = ...;
const signedPayload = `${timestamp}.${rawBody}`;
const expected = hmacSha256Hex(signedPayload, secret);
if (!timingSafeEqual(signature, expected)) reject();
Payload Format
{
"id": "evt_<uuid>",
"type": "product.sync",
"created": 1700000000,
"data": [
{
"action": "created|updated|deleted",
"object": { ... }
}
]
}
data[].object by Action
- created / updated: full product payload (public product DTO)
- deleted:
{ "id": <productId> }
Sample Webhook Responses
Product Updated
{
"id": "evt_3b2a7bea-d694-427c-8cf9-8c25ce1ff60f",
"type": "product.sync",
"created": 1767045021,
"data": [
{
"action": "updated",
"object": {
"id": 3279,
"status": "active",
"upc": 62809000724,
"yield": 1,
"shelfLifeDays": 1,
"priceRetail": 22.32,
"priceWholesale": 18.26,
"method": "Mix aiol",
"name": "Aioli mix (Copy) test",
"recipeCategoryId": 106,
"yieldUnitId": 5,
"portionUnitId": 5,
"weightUnitId": null,
"yieldUnit": {
"id": 5,
"symbol": "ea",
"type": "each",
"conversionFactor": 1,
"name": "Each"
},
"portionUnit": {
"id": 5,
"symbol": "ea",
"type": "each",
"conversionFactor": 1,
"name": "Each"
},
"subRecipes": [
{
"position": 0,
"quantity": 13,
"parentRecipeId": 3279,
"childRecipeId": 1901
},
{
"position": 1,
"quantity": 100,
"parentRecipeId": 3279,
"childRecipeId": 1895
}
],
"recipeTags": [
{
"recipeId": 3279,
"tagId": 192,
"relationType": "kitchenStation"
}
],
"createdOn": "2023-05-25T23:23:30.000Z",
"modifiedOn": "2025-12-29T21:50:12.000Z"
}
}
]
}
Product Created
{
"id": "evt_0065cfda-3ac1-4baf-a77c-f6ce8f672db0",
"type": "product.sync",
"created": 1767049977,
"data": [
{
"action": "created",
"object": {
"id": 6267,
"status": "active",
"upc": 62809000803,
"yield": 1,
"shelfLifeDays": 1,
"priceRetail": 22.32,
"priceWholesale": 18.26,
"method": "Mix aiol",
"name": "Aioli mix (Copy) test (Copy test)",
"recipeCategoryId": 106,
"yieldUnitId": 5,
"portionUnitId": 5,
"weightUnitId": null,
"yieldUnit": {
"id": 5,
"symbol": "ea",
"type": "each",
"conversionFactor": 1,
"name": "Each"
},
"portionUnit": {
"id": 5,
"symbol": "ea",
"type": "each",
"conversionFactor": 1,
"name": "Each"
},
"subRecipes": [
{
"position": 0,
"quantity": 12,
"parentRecipeId": 6267,
"childRecipeId": 1901
},
{
"position": 1,
"quantity": 100,
"parentRecipeId": 6267,
"childRecipeId": 1895
}
],
"recipeTags": [
{
"recipeId": 6267,
"tagId": 192,
"relationType": "kitchenStation"
}
],
"createdOn": "2025-12-29T23:12:47.000Z",
"modifiedOn": "2025-12-29T23:12:47.000Z"
}
}
]
}
Product Deleted
{
"id": "evt_2e914fa0-9092-469d-9b9a-373aeeb78f86",
"type": "product.sync",
"created": 1767123306,
"data": [
{
"action": "deleted",
"object": {
"id": 1088
}
}
]
}
Delivery, retries, and duplicates
Treat webhook delivery as at-least-once. Store X-Webhook-ID and ignore an event ID that your integration has already processed successfully.
- Demi makes up to six attempts: immediately, then after approximately 1 minute, 5 minutes, 30 minutes, 2 hours, and 8 hours.
- Network failures, timeouts, and retryable non-2xx responses are retried.
400,401,403,404,405,410, and422are not retried.- Endpoints are automatically disabled after 50 consecutive failed deliveries.
- No cross-event ordering guarantee is published. Use each payload's latest object state and make processing idempotent.
Operational checklist
- Read and verify the exact raw request body before parsing JSON.
- Reject signatures older than five minutes.
- Return a 2xx quickly and process expensive work asynchronously.
- Log the event ID, event type, action, attempt result, and processing outcome.
- Keep the current and next secret available during coordinated secret rotation.
- Contact Demi support to register, rotate, test, disable, or remove an endpoint.
Compatibility
The current event type is product.sync. Consumers should ignore unknown additive fields and unknown future event types. Breaking payload changes will require a versioned contract and migration notice.
Need Help?
If you have questions about webhook integration:
- Contact us at support@getdemi.co