Gareth Wilson Gareth Wilson

Guide to Yapily Webhooks: Features and Best Practices

Published


Yapily webhooks tell your application when something changes on the Yapily platform: a payment you initiated reaches COMPLETED or FAILED, a hosted payment finishes, or an asynchronous Data Plus categorisation is ready to collect. Yapily is an open-banking API platform, and without webhooks you would poll payment and data endpoints and spend your rate limit doing it.

This guide covers how Yapily webhooks are registered and signed, how secret rotation works, the events you can subscribe to and their envelope, and the best practices for running a Yapily receiver in production. Yapily webhooks are in private beta, so ask your Customer Success Manager for access first.

What are Yapily webhooks?

A Yapily webhook is a subscription to one or more event categories, registered through the API with a callback URL. When an event in one of those categories fires, Yapily sends an HTTPS POST with a JSON body to the URL.

Every request carries a webhook-signature header: the lowercase hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret. While you rotate the secret with a delay, Yapily also sends webhook-rotation-signature, signed with the other secret.

Despite the header name, this is not Standard Webhooks. There is no webhook-id or webhook-timestamp, the value has no v1, prefix, and only the body is signed. A Standard Webhooks library will reject every Yapily request.

Yapily webhook features

FeatureDetails
AvailabilityPrivate beta
ConfigurationAPI only: GET /webhook/events/categories, then POST /webhook/events
Subscription modelOne or more event categories per webhook, up to 10 webhooks per application
Signature headerwebhook-signature, plus webhook-rotation-signature during a delayed secret reset
SchemeHMAC-SHA256, hex digest, no prefix
Signed contentThe raw request body only
SecretOne per webhook, shown only in the registration response. Reset with POST /webhook/secrets/{webhook_id}
Replay windowNone. No timestamp is sent or signed
Response windowRespond 200 OK within 1 second
RetriesMentioned but no schedule is published
Callback URLPublic HTTPS with a valid certificate. No query parameters. Self-signed certificates are rejected
MetadataOptional object under 10 kB, echoed back in every event as metadata.user
Source IPsNot published

Common events

Yapily lists the categories available to your application at runtime with GET /webhook/events/categories. Its product pages document these event types:

ProductEvent typeFires when
Paymentssingle_payment.status.updatedA single payment's status moves to COMPLETED or FAILED
single_payment.status.completedA single payment's status moves to COMPLETED
single_payment.iso_status.updatedA single payment's ISO 20022 status changes, which can happen several times
Hosted Pageshosted_pis.payment_status.completed.v1A hosted payment completes successfully
hosted_pis.payment_status.failed.v1A hosted payment fails and an error is received
Data Plustransactions.categorisation.completedA categorisation request finishes and the data is ready to collect
transactions.categorisation.failedA categorisation request fails

Yapily's introduction also mentions consent and data-access notifications that no page names yet, so treat the list as incomplete and acknowledge types you don't recognise. Note that the hosted payment types end in .v1 and the others don't.

Every event shares one envelope. Here is Yapily's hosted_pis.payment_status.failed.v1 example:

{
  "type": "hosted_pis.payment_status.failed.v1",
  "applicationId": "a75620ec-8a74-408d-b13f-84227036cf43",
  "id": "b59e9533-24a0-4fd0-afc8-0c0c1a186f34",
  "event": {
    "consentId": "2427faa9-08e2-404e-90f1-4a312dc5f80d",
    "institutionId": "mock-sandbox",
    "paymentId": "9c5f7701-5275-442f-9d86-80d3ddc5cccc",
    "paymentRequestId": "dedcc4f9-7d6d-451c-accc-8a322f153a56",
    "isoStatus": { "code": "RJCT", "name": "Rejected" },
    "status": "FAILED"
  },
  "metadata": {
    "tracingId": "6cf5ef06-9f96-4f24-8bab-32ab09128010",
    "user": { "key": "value" }
  }
}
  • type is the event type. Route on it.
  • applicationId is the application, or sub-application, that registered the webhook.
  • event is the product-specific object. Its shape depends on type.
  • metadata.tracingId is Yapily's tracing ID. Quote it when you contact Yapily support.
  • metadata.user is the metadata you supplied at registration.
  • id is described as "the Webhook ID that triggered this event" on the introduction page, and as "the webhook delivery ID" on the Hosted Pages page. The completed and failed hosted payment examples carry the same id, which suggests the first reading. Don't rely on it as a per-event ID until you have seen real deliveries.

Setting up Yapily webhooks

  1. Get a Yapily access token for your application.
  2. Call GET /webhook/events/categories and choose the categories you need.
  3. Call POST /webhook/events with those categories, your callback URL and, optionally, a metadata object. The URL must be public HTTPS with a valid certificate and must not contain query parameters, or Yapily returns a 400.
  4. Copy the secret from the response and store it server-side, for example as YAPILY_WEBHOOK_SECRET. Yapily says this is the only call that ever shows it.

GET /webhook/events lists your registered webhooks and DELETE /webhook/events/{webhook_id} removes one. If you use Yapily's application management for resellers and merchants, send a sub-application header with the sub-application's ID on each of these calls.

If you use Hookdeck, create a source with the Yapily source type, set the secret on it, and register the Hookdeck source URL as the callback URL.

Securing Yapily webhooks

Compute an HMAC-SHA256 of the raw request body with the secret as a UTF-8 string, encode it as hex, and compare it in constant time with webhook-signature. Yapily's own samples compare case-insensitively, so lowercase the header first. During a delayed secret reset, also check webhook-rotation-signature and accept the request if either one matches.

const crypto = require('crypto');
const express = require('express');

function verifyYapilySignature(rawBody, signatureHeaders, secret) {
  if (!secret) return false;
  const expected = Buffer.from(crypto.createHmac('sha256', secret).update(rawBody).digest('hex'), 'utf8');

  return signatureHeaders.some((header) => {
    if (!header) return false;
    const received = Buffer.from(header.trim().toLowerCase(), 'utf8');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

const app = express();

app.post('/webhooks/yapily', express.raw({ type: '*/*' }), (req, res) => {
  const signatures = [req.get('webhook-signature'), req.get('webhook-rotation-signature')];
  if (!verifyYapilySignature(req.body, signatures, process.env.YAPILY_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Route on event.type. Persist first, respond within 1 second, process async.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac
import os


def verify_yapily_signature(raw_body: bytes, signature_headers, secret: str) -> bool:
    if not secret:
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest().encode("utf-8")
    return any(
        hmac.compare_digest(header.strip().lower().encode("utf-8"), expected)
        for header in signature_headers
        if header
    )


# In FastAPI: raw_body = await request.body()
# verify_yapily_signature(raw_body,
#                         [request.headers.get("webhook-signature"),
#                          request.headers.get("webhook-rotation-signature")],
#                         os.environ["YAPILY_WEBHOOK_SECRET"])

To check your verifier, use the secret yapily-test-secret and this exact body:

{"type":"single_payment.status.completed","applicationId":"e6d4bd6c-d7a2-4389-9ff7-4b8f95b6eed0","id":"14178946-c67c-417f-a773-f4f593d479a3","event":{"status":"COMPLETED"}}

The webhook-signature header should be:

b3b32eeca44e76e14f5a332852d989add056a96cfae88e3f79d0373fbf1ff868

Make Yapily webhooks production-ready. Hookdeck Event Gateway verifies the webhook-signature header on every Yapily delivery, answers well inside the 1-second window, and durably queues payment and categorisation events before your service sees them.

Yapily webhook limitations and pain points

A 1-second response window

The Problem: Deliveries count as failed even though your handler works.

Why It Happens: Yapily asks you to respond 200 OK within 1 second. Database writes, calls to other services, or a cold start easily take longer. The same page also says you would normally respond after processing, which doesn't fit in a second.

Workarounds:

  • Verify, write the raw event to a queue or table, and return 200 immediately.
  • Do all processing asynchronously.
  • Keep the webhook route warm if you run on serverless.

How Hookdeck Can Help: Hookdeck acknowledges Yapily as soon as the request arrives and stores it, then delivers to your service without Yapily's 1-second limit, so slow processing never fails a Yapily delivery.

Rotation and the second signature header

The Problem: After you reset the secret, verification starts failing on some or all deliveries.

Why It Happens: POST /webhook/secrets/{webhook_id} replaces the secret and returns the new one. With a delay, Yapily sends both webhook-signature and webhook-rotation-signature for that period, each signed with one of the two secrets. Yapily doesn't say which header carries which secret, so a verifier that reads only webhook-signature may fail as soon as the reset starts.

Workarounds:

  • Always reset with a delay, and check both headers during the overlap, as in the code above.
  • Update your stored secret before the delay ends.

How Hookdeck Can Help: Hookdeck's Yapily source type checks webhook-signature only, and Yapily doesn't say which secret signs that header during the overlap. Reset with a delay, watch the first deliveries in Hookdeck, and if they fail verification with the old secret, switch the source to the new one. Switch before the delay ends at the latest, and retry any deliveries that failed in between from Hookdeck.

A Standard Webhooks verifier rejects everything

The Problem: You verify with a Standard Webhooks library because of the header name, and nothing passes.

Why It Happens: Yapily's webhook-signature is a bare hex HMAC of the body. Standard Webhooks expects a base64 v1, signature over an ID, a timestamp and the body.

Workarounds:

  • Use a plain HMAC-SHA256 of the raw body, encoded as hex, as above.

How Hookdeck Can Help: Hookdeck's Yapily source type uses Yapily's own scheme, so you don't have to work around the naming collision.

No retry schedule, and a 24-hour cut-off for payments

The Problem: You can't tell how long Yapily keeps trying, and some payments never produce a webhook.

Why It Happens: Yapily says a payment webhook might not arrive if it "fails to deliver to you after retrying", but doesn't publish how many retries or over what period. Separately, Yapily times out payment webhooks at 24 hours: if the institution hasn't updated the payment's status by then, no webhook is sent. Some institutions don't provide status updates at all.

Workarounds:

  • Poll Get Payment Details for payments that have no webhook after a reasonable period, and always after 24 hours.
  • Reconcile after any outage on your side, since you can't rely on retries covering it.

How Hookdeck Can Help: Hookdeck receives and stores every delivery even while your service is down, so an outage on your side doesn't depend on Yapily's unpublished retry policy.

Best practices

Verify against the raw body

Yapily's docs warn not to map the body into any structure before signing it. Read the raw bytes (express.raw(), await request.body()), verify, then parse.

Route on type and expect versioned names

Switch on type, including the .v1 suffix on hosted payment events. Log and acknowledge unknown types, because new categories appear through GET /webhook/events/categories.

Use metadata to carry your own context

Whatever you put in metadata at registration comes back as metadata.user on every event. Use it to tag the environment or tenant a webhook belongs to, and keep it under 10 kB.

Build your own idempotency key

Because id may identify the webhook rather than the event, combine type with an ID from event, such as the payment ID and status, and store it before processing. See our guide to webhook idempotency.

Treat the webhook as a signal and confirm state

For payments, re-read the payment from the API before releasing goods or funds, and keep polling as a fallback for institutions that don't report status changes.

Acknowledge fast and process asynchronously

The 1-second window leaves no room for work in the request. See why to process webhooks asynchronously.

Conclusion

Yapily signs every webhook with a hex HMAC-SHA256 of the raw body in webhook-signature, keyed with a secret you only see once, and adds webhook-rotation-signature while a delayed reset is in progress. Verify against the raw bytes, check both headers during rotation, respond within a second, and keep a polling fallback for payments, because Yapily doesn't publish its retry policy and stops waiting for status updates after 24 hours.

Hookdeck Event Gateway sits in front of your receiver, verifies every Yapily signature, and answers inside the 1-second window while it queues each event for your service.

Get started with Hookdeck for free and handle Yapily webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.