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.