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.created or v1.vendor.created?
  • Why is my Pleo webhook-signature verification failing?
  • How do I create a Pleo webhook subscription?

Signature at a Glance

PropertyValue
Headerswebhook-id, webhook-timestamp, webhook-signature
Signature header formatSpace-delimited list of v1,<base64>
AlgorithmHMAC-SHA256, base64 (inferred from Svix delivery; Pleo's docs don't state it)
Signed content{webhook-id}.{webhook-timestamp}.{raw body} (Svix scheme)
SecretPer subscription, whsec_..., from GET /v1/subscriptions/{id}/secret → data.secretKey
KeyBase64-decode the part after whsec_
TimestampReject if more than ~5 minutes off
AlternativesBasic 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

EventTriggered When
v1.export-job.createdA user clicks Export in Pleo; the export job starts in pending
v1.vendor.createdA 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 2xx to acknowledge. Anything else, including 3xx, 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 return 200 for 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Oct 28, 2026

View on GitHub →