Webhooks
Every AOA webhook event type: order.paid, order.failed, order.refunded, event.cancelled, event.updated, attendee.registered and attendee.checked_in.
Subscribe only to what you handle: extra events create noise and use up your handler’s capacity. The list of events is set when you register the URL and changed with PATCH.
| Type | When it’s sent |
|---|---|
order.paid | A ticket was paid for. This is where the event attendee appears |
order.failed | The payment didn’t go through or the time for it ran out |
order.refunded | The money was refunded, the ticket voided |
event.cancelled | The event was cancelled: all its tickets are void |
event.updated | The time or place changed |
attendee.registered | An attendee was confirmed without payment: free registration, an approved application, a guest added by the organizer |
attendee.checked_in | A guest checked in at the door |
The main event of the money flow. It arrives after the bank confirms the payment, that is when the money has really been charged, not when the buyer pressed “Pay”.
{
"id": "3d9f7c21-…",
"type": "order.paid",
"createdAt": "2026-07-30T12:02:31.000Z",
"data": {
"paymentId": "d3f1a2b4-…",
"eventId": "a1b2c3",
"status": "SUCCESS",
"amountMinor": 90000,
"currency": "UAH",
"attendeeId": "clx9attendee01"
}
}amountMinor is kopiyky (hundredths of a hryvnia) as an integer. attendeeId appears together with a successful payment; you can match it against the guest list.
The payment won’t be completed and there will be no ticket for it. It arrives once per payment, with the same paymentId that creating the order through the API returns.
{
"id": "5c1e8a47-…",
"type": "order.failed",
"createdAt": "2026-07-30T12:14:05.000Z",
"data": {
"paymentId": "e7a4c9d2-…",
"eventId": "a1b2c3",
"status": "EXPIRED",
"amountMinor": 90000,
"currency": "UAH"
}
}status shows the reason: FAILED when the bank declined the card or invoice, and EXPIRED when the time to pay ran out or the buyer abandoned the payment or replaced it with a new one (for example, changed the number of tickets and got a new link). There’s no attendeeId field here: an attendee only appears with a successful payment.
The event webhooks exist for. It can arrive a week after the purchase, and you won’t see it by polling the order status.
{
"id": "7a2b9c14-…",
"type": "event.cancelled",
"createdAt": "2026-08-01T09:15:00.000Z",
"data": {
"eventId": "a1b2c3",
"title": "Джаз-вечір",
"startAt": "2026-08-15T18:00:00.000Z"
}
}Cancellation doesn’t mean a refund
These are two different events. Cancelling an event voids the tickets, and the organizer refunds the money: each refund arrives as a separateorder.refunded. Don’t treat the money as refunded until you’ve received exactly that.{
"id": "b5e8d033-…",
"type": "order.refunded",
"createdAt": "2026-08-01T09:16:12.000Z",
"data": {
"paymentId": "d3f1a2b4-…",
"eventId": "a1b2c3",
"status": "REFUNDED",
"amountMinor": 90000
}
}amountMinor is the amount actually refunded. It can be less than the original payment: if the buyer paid the service fee, it isn’t refunded.
The webhook doesn’t pass on the refund reason: it’s the organizer’s internal note, and it can contain personal data.
The time or place of a published event changed. Editing the title, description or cover doesn’t trigger this webhook.
{
"id": "9e4f2b10-…",
"type": "event.updated",
"createdAt": "2026-08-02T10:20:00.000Z",
"data": {
"eventId": "a1b2c3",
"title": "Джаз-вечір",
"changed": ["time"],
"startAt": "2026-08-15T19:00:00.000Z",
"endAt": "2026-08-15T23:00:00.000Z",
"allDay": false,
"dates": [
{
"startAt": "2026-08-15T19:00:00.000Z",
"endAt": "2026-08-15T23:00:00.000Z"
}
],
"location": {
"name": "Coffee Lab",
"address": "вул. Хрещатик, 1",
"city": "Київ"
}
}
}changed says what exactly changed: time, place or both. The other fields carry the current values in the same shape as GET/api/v1/events/{eventId}: startAt, endAt, allDay and location (null if no place is set), so you can simply overwrite your record.
dates lists the days or time slots in chronological order; a regular event has one item. It matters for multi-day events: when a day in the middle moves, the overall startAt and endAt stay the same. A cancellation arrives as a separate event.cancelled.
An attendee was confirmed without payment: a free registration, a ticket with a 100% coupon, a guest added by the organizer or an approved application. Paid registrations arrive as order.paid, so they don’t come here.
{
"id": "4b7d1e93-…",
"type": "attendee.registered",
"createdAt": "2026-08-03T15:40:12.000Z",
"data": {
"attendeeId": "clx9attendee01",
"eventId": "a1b2c3",
"status": "confirmed",
"tickets": [
{ "ticketTypeId": "clx9ticket001", "quantity": 1 }
]
}
}If the event requires approving applications, the webhook arrives when the application is approved, not when it’s submitted. A guest from the waitlist comes here when they’re let in. One attendee gets one webhook: cancelling and registering again doesn’t send a second one.
The body has no name, email or phone number, only identifiers. tickets shows how many seats of which type the attendee got; ticketTypeId matches the id in the event’s list of ticket types.
A ticket was checked in: a QR scan, a search in the guest list or being let in at the door.
{
"id": "e2a9c6f4-…",
"type": "attendee.checked_in",
"createdAt": "2026-08-15T18:07:44.000Z",
"data": {
"attendeeId": "clx9attendee01",
"eventId": "a1b2c3",
"ticketTypeId": "clx9ticket001",
"quantity": 1,
"method": "qr",
"checkedInAt": "2026-08-15T18:07:43.000Z"
}
}Check-in is about the ticket, not the person: at a multi-day event an attendee has several tickets, and each one arrives as a separate webhook. quantity above 1 means a group ticket that let several people in. method takes the values qr, nfc or manual. Undoing a check-in doesn’t send a webhook, and checking in the same ticket again doesn’t send a second one.
Events aren’t guaranteed to arrive in the order they happened: each delivery is retried independently, so a retried event can arrive after a later one. Go by createdAt in the body, not by the time you received it.
In practice: your handler has to cope with events arriving out of order. For example, an order.refunded that arrives before order.paid shouldn’t create a record from scratch: check the current state on your side.