Payment webhooks
It is the only place from which an online payment confirms a booking (together with reconciliation, which uses the
same internal path). It lives outside /api/: no session, no CSRF, no per-IP limits shared with the public API. For
Stripe: /webhooks/payments/stripe/<establishment-id>; the panel shows the exact URL in Configuration → Online
payments.
What happens on each call
Section titled “What happens on each call”- The body is read raw (up to 256 KB): the signature is verified on the exact bytes received.
- Arenile loads the credentials of the establishment in the URL, decrypts them and hands the request to the
provider’s driver, which verifies the signature. Missing or wrong signature:
400 webhook_signature_invalid. - In a single transaction:
- it records the event in the de-duplication table (unique per provider + event id): a duplicate answers
200with no effect; - it updates the provider payment;
- on success, it records the payment, updates the paid amount and confirms the booking through the booking
service (hold → stable occupation, activity log,
booking.confirmedevent); for zones with automatic assignment it also assigns the spot; - on failure, it marks the attempt as failed and emits
payment.failed: the booking stays pending payment while the hold is valid and the customer can retry; - on refund, it records the refund and emits
payment.refunded.
- it records the event in the de-duplication table (unique per provider + event id): a duplicate answers
- It answers
200with a summary (WebhookAck). Authentic but unhandled events answer200and are ignored.
Stripe
Section titled “Stripe”The signature is in the Stripe-Signature header (t=…,v1=…): HMAC-SHA256 of "<t>.<body>" with the webhook secret
(whsec_…), constant-time comparison, 5-minute tolerance on the clock; several v1 values are accepted (secret
rotation).
| Stripe event | Effect in Arenile |
|---|---|
checkout.session.completed |
Payment succeeded → confirmation |
payment_intent.succeeded |
Payment succeeded → confirmation |
payment_intent.payment_failed |
Payment failed (reason: error code only, never the message) |
charge.refunded |
Refund (amount refunded with that event) |
| others | Ignored (200) |
Checkout Sessions created by Arenile carry establishment_id, booking_id and booking_code in their metadata and
expire after 30 minutes. The Stripe driver is written with Go’s standard library (no SDK) and a pinned Stripe API
version.
Reconciliation
Section titled “Reconciliation”A job every 15 minutes asks the provider for the outcome of payments pending for more than 10 minutes and
applies it with the same code as the webhook, using a synthetic identifier (reconcile:<reference>:<status>): if the
real webhook arrives later, it is still de-duplicated.
- GET
/api/admin/v1/establishments/{establishmentId}/payments/reconciliationRiconciliazione dei pagamenti onlineSession or API key - POST
/api/admin/v1/establishments/{establishmentId}/payments/reconciliationEsegue subito la riconciliazione (come il job ogni 15 minuti)Session or API key
The report is also available through the API (GET /api/admin/v1/establishments/{id}/payments/reconciliation, with
from, to and issues_only) and can be run on demand. Flagged issues: pending for too long, different outcome,
payment without ledger entry, different amount, provider unreachable.
Trying it locally
Section titled “Trying it locally”With the Stripe CLI you can forward test webhooks to your development instance:
stripe listen --forward-to localhost:8080/webhooks/payments/stripe/<establishment-id>The CLI prints a whsec_… secret to save as the webhook secret in test mode.

