Webhooks
Mealio tells your server when a user's cart is filled. Set your URL in the dashboard.
cart.completed
POST to your webhook URL
{
"type": "cart.completed",
"created": "2026-11-02T18:04:11.000Z",
"livemode": true,
"data": {
"cartId": "c2a1...",
"externalUserId": "u_123",
"recipeId": "chicken-tinga",
"store": "heb",
"added": [{ "key": "yellow onion", "productId": "2412", "name": "H-E-B Yellow Onion", "quantity": 1 }],
"skipped": [{ "key": "salt", "reason": "user_skipped" }],
"failed": [{ "key": "cilantro", "productId": "331", "name": "Fresh Cilantro", "reason": "out_of_stock" }]
}
}Check the signature
Every webhook carries a Mealio-Signature header: t=<unix seconds>,v1=<hex>. The hex is an HMAC-SHA256 of "<t>.<raw body>" with your webhook secret. Reject anything older than five minutes.
Node
import crypto from 'node:crypto';
function verifyMealio(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false;
const want = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1));
}Answer with any 2xx within three seconds. Mealio sends each event once today. Use the dashboard's Send test button to check your endpoint with a signed ping.