# Pinata Webhooks

Pinata delivers account webhooks (private and public IPFS file events, pin-by-CID and CAR-import
events) through Svix, which implements Standard Webhooks. Every delivery
is an HTTP `POST` with a JSON body and three signature headers: `webhook-id`, `webhook-timestamp`,
and `webhook-signature`. The body is `{event_type, timestamp, workspace_id, data}`. The file sits
under `data.object` (`data.new_file` for `ipfs.public.pin_by_cid_succeeded`). There is no event id in
the body; the `webhook-id` header identifies the message. The two CAR-import events name the field
`event` instead of `event_type`, so read `event_type ?? event`.

## When to Use This Skill

* How do I receive Pinata webhooks?
* How do I verify Pinata webhook signatures (`webhook-signature`, `whsec_` secret)?
* How do I handle `ipfs.public.file_added` or `ipfs.private.file_deleted` events?
* How do I react to `ipfs.public.pin_by_cid_succeeded` or `ipfs.public.pin_by_cid_failed`?
* Why is my Pinata webhook signature verification failing, or rejecting Hookdeck retries?

## Verification (core)

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`. Svix signs it with HMAC-SHA256
using the base64-decoded secret (the part after `whsec_`), base64-encodes the result, and sends one
or more space-separated `v1,<signature>` entries. Use the official `svix` library, which compares in
constant time. Read the `webhook-*` headers, and when `webhook-id` is absent fall back to the `svix-*`
headers (`svix-id`, `svix-timestamp`, `svix-signature`), as Hookdeck does.

```javascript
import { Webhook } from 'svix'; // svix 2.x is ESM-only

const prefix = req.headers['webhook-id'] ? 'webhook' : 'svix';
const id = req.headers[`${prefix}-id`];
const timestamp = req.headers[`${prefix}-timestamp`];
const signature = req.headers[`${prefix}-signature`];
// rawBody: the raw Buffer/string, never re-serialized JSON
new Webhook(process.env.PINATA_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': id, 'svix-timestamp': timestamp, 'svix-signature': signature,
  'webhook-id': id, 'webhook-timestamp': timestamp, 'webhook-signature': signature,
}); // throws on a bad signature or a timestamp outside the window
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes

```

```python
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["PINATA_WEBHOOK_SECRET"])  # "whsec_..."
wh.verify(raw_body, {"svix-id": id, "svix-timestamp": ts, "svix-signature": sig,
                     "webhook-id": id, "webhook-timestamp": ts, "webhook-signature": sig})

```

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

The secret is on the webhook dashboard's right sidebar in the Pinata App (click the reveal icon)
and starts with `whsec_`. Each endpoint has its own. Keep the full `whsec_...` value in
`PINATA_WEBHOOK_SECRET`. Handlers fail closed: with the variable unset they return 500 instead of
accepting anything. A missing header or a bad signature returns 400. Express must read the body with
`express.raw({ type: '*/*' })` so any content type works and the exact bytes are verified.

### Hookdeck retries and replays hit the 5-minute window

The `svix` and `standardwebhooks` libraries always enforce a fixed 5-minute timestamp window, and
it can't be configured. Hookdeck retries and replays resend the **original** `webhook-timestamp`, so
the library rejects any retry or replay that happens more than 5 minutes after the original
delivery. If you verify behind Hookdeck (or replay old events), use the manual verifier in
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/pinata-webhooks/references/verification.md), where the window can be dropped or
widened. Deduplicate on `webhook-id` instead of relying on the timestamp.

## Common Event Types

11 events are documented (the list may not be exhaustive). The name is `event_type` in the JSON
body (`event` for the CAR-import events). The examples handle all of these:

| Event | Triggered When |
| --- | --- |
| `ipfs.private.file_added` | A file is added to private IPFS |
| `ipfs.private.file_updated` | A private file's metadata is updated |
| `ipfs.private.file_deleted` | A file is deleted from private IPFS |
| `ipfs.public.file_added` | A file is added to public IPFS |
| `ipfs.public.file_updated` | A public file's metadata is updated |
| `ipfs.public.file_deleted` | A file is deleted from public IPFS |
| `ipfs.public.car_import_succeeded` | A CAR file imported successfully after upload |
| `ipfs.public.car_import_failed` | A CAR file failed to import after upload |
| `ipfs.public.pin_by_cid_added` | A pin by CID task is queued |
| `ipfs.public.pin_by_cid_succeeded` | Pinata pinned the content from the public IPFS network |
| `ipfs.public.pin_by_cid_failed` | Pinata couldn't retrieve the content for a pin by CID task |

See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/pinata-webhooks/references/overview.md) for payload shapes per event.

## Environment Variables

```bash
# Signing secret of the webhook (Svix format, starts with whsec_)
PINATA_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 pinata --path /webhooks/pinata

```

In the Pinata App, add the tunnel URL on the Webhooks tab, then trigger an event (for example,
upload a file) to see it arrive.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/pinata-webhooks/references/overview.md) - Pinata webhook concepts, events, payload shapes
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/pinata-webhooks/references/setup.md) - Register an endpoint, get the secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/pinata-webhooks/references/verification.md) - Svix verification details, header fallback, manual verifier, timestamp window