# Gopuff Webhooks

Gopuff's partner API (Powered by Gopuff) sends webhooks about order fulfillment: order status changes, driver location and delivery ETA. Endpoints are registered in the **Svix App Portal**, which signs every delivery. Gopuff's own pages have no signature section; the scheme comes from Svix's manual verification guide.

## When to Use This Skill

* How do I receive Gopuff webhooks?
* How do I verify Gopuff webhook signatures (`svix-signature`)?
* How do I handle `order.status.v2` or `order.delivered` events?
* How do I get the Svix App Portal link for my Gopuff partner account?
* Why is my Gopuff webhook signature verification failing?

## Verification (core)

Each request carries `svix-id`, `svix-timestamp` (Unix seconds) and `svix-signature` (space-delimited `v1,<base64>` entries). The signed content is `{svix-id}.{svix-timestamp}.{raw body}`, HMAC-SHA256 with the base64-decoded part of the `whsec_` secret, base64-encoded. Use the raw body, never re-serialized JSON.

```javascript
import crypto from 'node:crypto';

// toleranceSec = 0 (default) skips the timestamp window, because Hookdeck retries and replays
// keep the original svix-timestamp. A positive value opts in.
function verify(rawBody, { id, timestamp, signature }, secret, toleranceSec = 0) {
  if (!/^\d+$/.test(timestamp ?? '')) return false;
  if (toleranceSec > 0 && Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSec) return false;
  const key = Buffer.from(secret.split('_')[1], 'base64'); // decode after "whsec_"
  const expected = crypto.createHmac('sha256', key)
    .update(`${id}.${timestamp}.`).update(rawBody).digest();
  return signature.split(' ').some((entry) => {
    const [version, sig] = entry.split(',');
    const got = Buffer.from(sig ?? '', 'base64');
    return version === 'v1' && got.length === expected.length &&
      crypto.timingSafeEqual(got, expected);
  });
}

```

The examples verify the same way and also accept the white-labeled `webhook-*` headers.

**Why not the `svix` SDK?** `new Webhook(secret).verify(...)` (npm) and `Webhook(secret).verify(...)` (PyPI) check the same signature, but they always enforce a 5-minute timestamp window with no way to turn it off. Hookdeck retries and replays carry the original `svix-timestamp`, so the SDK rejects them. The examples verify manually and make the window opt-in with `GOPUFF_TIMESTAMP_TOLERANCE_SECONDS` (default `0`; a positive number of seconds turns it on; anything that isn't a whole number is treated as `0`, with a warning at startup). If you never retry or replay late, the SDK is fine.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/gopuff-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/gopuff-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/gopuff-webhooks/examples/fastapi/).

## Common Event Types

Event types you select in the Svix App Portal (from Gopuff's [webhook docs](https://devportaldocs.poweredbygopuff.com/#/webhooks/introduction)):

| Event Type | Description |
| --- | --- |
| `order.status.v2` | Order status changed (recommended; slim payload) |
| `order.status` | Order status changed (full order object; end of support 08/15/2025) |
| `order.delivered` | Order successfully delivered |

Order statuses: `Pending` (no event sent), `Packing_Now`, `Packed`, `Delivery`, `Finished`, `Cancelled`. Gopuff also documents Delivery Status (driver location) and Delivery ETA payloads but does not list event-type strings for them.

**The payloads contain no event-type field**, and Svix sends no event-type header. The examples tell payloads apart by shape (`order`, `driver`, `expected_delivery_time`). Always keep a default branch for unknown shapes.

## Environment Variables

```bash
GOPUFF_WEBHOOK_SECRET=whsec_xxxxx   # Svix App Portal > Endpoints > Signing Secret
GOPUFF_TIMESTAMP_TOLERANCE_SECONDS=0   # Optional replay window; a positive value opts in

```

## Local Development

```bash
npx hookdeck-cli listen 3000 gopuff --path /webhooks/gopuff

```

No account required. Use the printed URL as the endpoint URL in the App Portal. (FastAPI example: port 8000.)

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/gopuff-webhooks/references/overview.md) - Events, payloads, order status flow
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/gopuff-webhooks/references/setup.md) - App Portal link, endpoint, secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/gopuff-webhooks/references/verification.md) - Signature details and debugging