Skip to Content
Webhooks

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

EventWhen it’s sent
payment_intent.preauthorisedA payment intent has been authorised (hold placed), not yet captured.
payment_intent.capturedA payment intent has been captured (funds taken).
payment_intent.refundedA captured payment intent has been refunded.
payment_intent.failedA payment intent failed.
group_payment.partially_paidSome — but not all — of a group’s money has been captured. Holds alone never trigger this (use payment_intent.preauthorised).
group_payment.paidAll slots of a group payment have been paid.
group_payment.partially_refundedSome captured intents of a group payment have been refunded.
group_payment.refundedAll 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:

HeaderValue
Payylo-Signaturet=<unixSeconds>,v1=<hex HMAC-SHA256>
Payylo-Event-IdLogical event id (shared per event)
Payylo-Event-TypeThe 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 small

Wire 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.

Last updated on