Agent skill
Pleo Webhooks Skill
Receive and verify Pleo webhooks. Use when setting up Pleo webhook subscriptions with the Subscriptions API, fetching the whsec_ subscription secret, debugging Standard Webhooks / Svix signature verification (webhook-id, webhook-timestamp, webhook-signature), or handling Pleo spend management events like v1.export-job.created and v1.vendor.created for accounting and ERP integrations.
Install this skill
npx skills add hookdeck/webhook-skills --skill pleo-webhooks
Pleo is a spend management platform. Its webhooks notify accounting and ERP integrations when, for example, a user exports expenses or creates a vendor. You subscribe through the Subscriptions API (POST /v1/subscriptions), and Pleo delivers each event through Svix with Standard Webhooks headers.
When to Use This Skill
- How do I receive Pleo webhooks?
- How do I verify a Pleo webhook signature?
- Where do I get my Pleo webhook secret?
- How do I handle
v1.export-job.createdorv1.vendor.created? - Why is my Pleo
webhook-signatureverification failing? - How do I create a Pleo webhook subscription?
Signature at a Glance
| Property | Value |
|---|---|
| Headers | webhook-id, webhook-timestamp, webhook-signature |
| Signature header format | Space-delimited list of v1,<base64> |
| Algorithm | HMAC-SHA256, base64 (inferred from Svix delivery; Pleo's docs don't state it) |
| Signed content | {webhook-id}.{webhook-timestamp}.{raw body} (Svix scheme) |
| Secret | Per subscription, whsec_..., from GET /v1/subscriptions/{id}/secret → data.secretKey |
| Key | Base64-decode the part after whsec_ |
| Timestamp | Reject if more than ~5 minutes off |
| Alternatives | Basic auth or custom headers, set on the subscription (fallbacks only) |
Verification (core)
Pleo has no webhook SDK. Use the Standard Webhooks library (standardwebhooks on npm and PyPI), which checks the timestamp, recomputes the HMAC and compares in constant time against every v1, entry. Pass the raw body, not re-serialised JSON.
const { Webhook } = require('standardwebhooks');
// rawBody: express.raw() buffer as string, or await request.text() in Next.js
function verifyPleoWebhook(rawBody, headers) {
const wh = new Webhook(process.env.PLEO_WEBHOOK_SECRET); // full "whsec_..." value
return wh.verify(rawBody, {
'webhook-id': headers['webhook-id'],
'webhook-timestamp': headers['webhook-timestamp'],
'webhook-signature': headers['webhook-signature'],
}); // returns { data, eventId, eventType }; throws -> respond 400
}
Python: from standardwebhooks.webhooks import Webhook, then Webhook(secret).verify(await request.body(), headers). For a dependency-free version, see references/verification.md.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Payload
{
"data": { "id": "b4c4d068-...", "companyId": "b5fdaec1-...", "numberOfItems": 1, "status": "pending", "...": "..." },
"eventId": "a257a31e-e8a6-4c36-a98d-451250e5b5c4",
"eventType": "v1.export-job.created"
}
data is the resource as it was when the event fired. Switch on eventType.
Common Event Types
| Event | Triggered When |
|---|---|
v1.export-job.created | A user clicks Export in Pleo; the export job starts in pending |
v1.vendor.created | A vendor is added in Pleo that isn't in the accounting system yet (state: "DRAFT") |
Pleo's events page prose spells the first one v1.export.job-created, but payloads and the Subscriptions API use v1.export-job.created. See references/overview.md for payload fields.
Delivery Semantics
- Return a
2xxto acknowledge. Anything else, including3xx, is a failure. - Retries back off over about 27.5 hours (immediately, 5s, 5m, 30m, 2h, 5h, 10h, 10h), then the message is dropped. Pleo suggests a backstop poll.
- Retries keep the same
webhook-id. Dedupe on it and return200for duplicates. - Firewalled endpoints should allowlist the Svix source IPs.
Environment Variables
PLEO_WEBHOOK_SECRET=whsec_xxxxx # from GET /v1/subscriptions/{id}/secret; handlers fail closed without it
Local Development
Receive webhooks locally with the Hookdeck CLI. No install or account required:
npx hookdeck-cli listen 3000 pleo --path /webhooks/pleo
Use the printed URL as endpointUrl when creating the subscription, then trigger an export or create a vendor in Pleo. FastAPI examples use port 8000.
Reference Materials
- references/overview.md - Event types, payload fields, delivery and retries
- references/setup.md - Create a subscription, fetch the secret, Basic auth and custom header fallbacks, IPs
- references/verification.md - Library and manual verification, gotchas, debugging