Agent skill
Pinata Webhooks Skill
Receive and verify Pinata webhooks (IPFS file storage and pinning, delivered via Svix with Standard Webhooks signatures). Use when setting up Pinata webhook handlers, debugging webhook-signature verification, or handling events like ipfs.public.file_added, ipfs.private.file_deleted, and ipfs.public.pin_by_cid_succeeded.
Install this skill
npx skills add hookdeck/webhook-skills --skill 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_. The file sits under data.object (data.new_file for ipfs.public.pin_). 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_addedoripfs.private.file_events?deleted - How do I react to
ipfs.public.pin_orby_ cid_ succeeded 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.
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
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/, examples/nextjs/, 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, 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_ | A private file's metadata is updated |
ipfs.private.file_ | 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_ | A CAR file imported successfully after upload |
ipfs.public.car_ | A CAR file failed to import after upload |
ipfs.public.pin_ | A pin by CID task is queued |
ipfs.public.pin_ | Pinata pinned the content from the public IPFS network |
ipfs.public.pin_ | Pinata couldn't retrieve the content for a pin by CID task |
See references/overview.md for payload shapes per event.
Environment Variables
# Signing secret of the webhook (Svix format, starts with whsec_)
PINATA_WEBHOOK_SECRET=whsec_xxxxx
Local Development
# 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 - Pinata webhook concepts, events, payload shapes
- references/setup.md - Register an endpoint, get the secret
- references/verification.md - Svix verification details, header fallback, manual verifier, timestamp window