Agent skill

Yapily Webhooks Skill

Receive and verify Yapily webhooks. Use when setting up Yapily open banking webhook handlers, debugging webhook-signature verification, or handling payment events like single_payment.status.completed and hosted_pis.payment_status.completed.v1.

Install this skill

npx skills add hookdeck/webhook-skills --skill yapily-webhooks


When to Use This Skill

  • How do I receive Yapily webhooks?
  • How do I verify the Yapily webhook-signature header?
  • Why is my Yapily webhook signature verification failing?
  • How do I handle single_payment.status.completed or hosted_pis.payment_status.failed.v1 events?
  • How do I rotate a Yapily webhook secret without downtime (webhook-rotation-signature)?

Verification (core)

Yapily signs the raw request body with HMAC-SHA256, keyed with the webhook secret as a UTF-8 string, and sends the plain lowercase hex digest in webhook-signature. There is no prefix, timestamp or message ID: this is not Standard Webhooks, despite the header name. During a delayed secret reset Yapily also sends webhook-rotation-signature; accept a match on either header. Compare case-insensitively and timing-safe.

const crypto = require('crypto');

function verify(rawBody, signatures, secret) {
  const expected = Buffer.from(
    crypto.createHmac('sha256', Buffer.from(secret, 'utf-8')).update(rawBody).digest('hex')
  );
  return signatures.some((sig) => {
    if (!sig) return false;
    const received = Buffer.from(sig.trim().toLowerCase());
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

// signatures = [req.headers['webhook-signature'], req.headers['webhook-rotation-signature']]
import hmac, hashlib

def verify(raw_body: bytes, signatures: list, secret: str) -> bool:
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(s.strip().lower().encode(), expected.encode()) for s in signatures if s)

In Express, read the body with express.raw({ type: '*/*' }) so every content type reaches the verifier as a Buffer. Never JSON.parse and re-serialise before verifying.

For complete handlers with route wiring, event dispatch, and tests, see:

Payload Shape

{ "id": "<webhook id>", "applicationId": "<app id>", "type": "<event type>", "event": { }, "metadata": { "tracingId": "...", "user": { } } }

event varies by type. metadata.user echoes the metadata you supplied when registering the webhook.

Common Event Types

Subscribe by event category with POST /webhook/events; list the categories available to you with GET /webhook/events/categories. The type values in the documented payloads are:

typeDescription
single_payment.status.completedSingle payment status moved to COMPLETED
single_payment.status.updatedSingle payment status moved to COMPLETED or FAILED
single_payment.iso_status.updatedSingle payment ISO 20022 status changed (can transition several times)
hosted_pis.payment_status.completed.v1Hosted payment completed successfully
hosted_pis.payment_status.failed.v1Hosted payment failed

Async data-access completion and other categories also use this envelope; handlers should log and return 200 for types they don't recognise.

Important Headers

HeaderDescription
webhook-signatureHex HMAC-SHA256 of the raw body
webhook-rotation-signatureOnly present during a delayed secret reset; signed with the other secret

Environment Variables

YAPILY_WEBHOOK_SECRET=your_webhook_secret   # webhookSecret from POST /webhook/events; shown only once

Local Development

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

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 4, 2026

View on GitHub →