Guide to Yapily Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Availability | Private beta |
| Configuration | API only: GET /webhook/events/categories, then POST /webhook/events |
| Subscription model | One or more event categories per webhook, up to 10 webhooks per application |
| Signature header | webhook-signature, plus webhook-rotation-signature during a delayed secret reset |
| Scheme | HMAC-SHA256, hex digest, no prefix |
| Signed content | The raw request body only |
| Secret | One per webhook, shown only in the registration response. Reset with POST /webhook/secrets/{webhook_ |
| Replay window | None. No timestamp is sent or signed |
| Response window | Respond 200 OK within 1 second |
| Retries | Mentioned but no schedule is published |
| Callback URL | Public HTTPS with a valid certificate. No query parameters. Self-signed certificates are rejected |
| Metadata | Optional object under 10 kB, echoed back in every event as metadata.user |
| Source IPs | Not 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:
| Product | Event type | Fires when |
|---|---|---|
| Payments | single_ | A single payment's status moves to COMPLETED or FAILED |
single_ | A single payment's status moves to COMPLETED | |
single_ | A single payment's ISO 20022 status changes, which can happen several times | |
| Hosted Pages | hosted_ | A hosted payment completes successfully |
hosted_ | A hosted payment fails and an error is received | |
| Data Plus | transactions.categorisation.completed | A categorisation request finishes and the data is ready to collect |
transactions.categorisation.failed | A 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_ 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" }
}
}
typeis the event type. Route on it.applicationIdis the application, or sub-application, that registered the webhook.eventis the product-specific object. Its shape depends ontype.metadata.tracingIdis Yapily's tracing ID. Quote it when you contact Yapily support.metadata.useris the metadata you supplied at registration.idis 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 sameid, 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
- Get a Yapily access token for your application.
- Call
GET /webhook/events/categoriesand choose the categories you need. - Call
POST /webhook/eventswith those categories, your callback URL and, optionally, ametadataobject. The URL must be public HTTPS with a valid certificate and must not contain query parameters, or Yapily returns a400. - 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_ 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-signatureheader 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
200immediately. - 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_ 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.