Webhooks
Payylo sends outbound webhooks so your application is notified the moment a payment changes state — a booking engine can confirm a reservation without polling. Every delivery is signed so you can verify it came from Payylo.
Event types
| Event | When it’s sent |
|---|---|
payment_intent.preauthorised | A payment intent has been authorised (hold placed), not yet captured. |
payment_intent.captured | A payment intent has been captured (funds taken). |
payment_intent.refunded | A captured payment intent has been refunded. |
payment_intent.failed | A payment intent failed. |
group_payment.partially_paid | Some — but not all — of a group’s money has been captured. Holds alone never trigger this (use payment_intent.preauthorised). |
group_payment.paid | All slots of a group payment have been paid. |
group_payment.partially_refunded | Some captured intents of a group payment have been refunded. |
group_payment.refunded | All captured intents of a group payment have been refunded. |
Events fire regardless of the payment provider (Stripe, Rede or Redsys).
Registering an endpoint
Register endpoints via POST /v1/webhooks. The signing secret is returned
exactly once, at creation — store it immediately, it cannot be retrieved later.
Omit event_types (or pass []) to receive all events.
curl https://api.payylo.com/v1/webhooks \
-H "Authorization: Bearer sk_test_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/payylo-webhook",
"event_types": ["group_payment.paid", "group_payment.refunded"]
}'
# Response includes "secret": "whsec_…" — save it now, shown only once.Manage endpoints with GET /v1/webhooks, GET/PATCH/DELETE /v1/webhooks/{id} —
full request/response shapes are in the
API Reference .
Event payload
Payylo POSTs a JSON body to your URL:
{
"id": "f1e2d3c4-...",
"type": "group_payment.paid",
"created": "2026-06-20T16:57:23.000Z",
"data": {
"group_payment_id": "eda0165c-...",
"state": "paid"
}
}Headers sent with every delivery:
| Header | Value |
|---|---|
Payylo-Signature | t=<unixSeconds>,v1=<hex HMAC-SHA256> |
Payylo-Event-Id | Logical event id (shared per event) |
Payylo-Event-Type | The event type |
Verifying the signature
The signature is HMAC-SHA256(secret, "{timestamp}.{rawBody}"), hex-encoded.
Verify it against the raw request body (never re-serialize). Also reject old
timestamps to prevent replay. Any language with an HMAC library can do this —
here in Node:
import { createHmac } from 'node:crypto';
const [, t, v1] = header.match(/t=(\d+),v1=([0-9a-f]+)/) ?? [];
const expected = createHmac('sha256', secret)
.update(`${t}.${rawBody}`) // rawBody = the exact bytes received
.digest('hex');
const valid = expected === v1; // and check Math.abs(now - t) is smallWire it into your handler, verifying before parsing the body:
app.post(
'/payylo-webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const rawBody = req.body.toString('utf8');
const header = req.header('Payylo-Signature') ?? '';
const secret = process.env.PAYYLO_WEBHOOK_SECRET!; // whsec_…
const [, t, v1] = header.match(/t=(\d+),v1=([0-9a-f]+)/) ?? [];
const expected = createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
if (expected !== v1) return res.status(400).end();
const event = JSON.parse(rawBody);
switch (event.type) {
case 'group_payment.paid':
// fulfil the reservation
break;
case 'group_payment.refunded':
// handle the refund
break;
}
res.json({ received: true });
}
);Retries
If your endpoint doesn’t return a 2xx, Payylo retries with exponential backoff
(1m, 5m, 30m, 2h, 5h, 10h — 6 attempts) before marking the delivery failed.
Make your handler idempotent: the same event may be delivered more than once.