# Orders

An order issues tickets. Send it in one request after payment, or hold the tickets with a reservation while the buyer pays.

There are two ways to sell a ticket.

- **One request after payment.** The buyer paid on your site, your site sends [`POST /v1/orders`](#create) and gets the tickets. This fits when there are plenty of tickets and you're ready to refund if they sell out while the buyer pays.
- **A reservation during payment.** Your site reserves the tickets before payment ([`POST /v1/reservations`](#reserve)), the buyer pays, your site confirms the reservation ([`POST /v1/reservations/{id}/confirm`](#confirm)). If payment fails, you cancel the reservation ([`DELETE /v1/reservations/{id}`](#cancel)) or it expires on its own. Use this for assigned seats and for tiers that may sell out.

Either way, the tickets are issued and the guest's email goes out right after confirmation.

## Order object {#order-object}

| Field | Type | Description |
|---|---|---|
| `id` | string | Order UUID |
| `org_id` | string | Organization UUID |
| `livemode` | boolean | A live order or a test one |
| `event_id` | string | Event UUID |
| `status` | string | Status, see [below](#statuses) |
| `buyer` | object | Buyer: `email`, `first_name`, `last_name`, `phone`. Also the ticket holder |
| `items` | array | Contents: `{tariff_id, qty, seat}`. Tickets without seats are grouped by tier, each seat is its own item with `qty: 1` and `seat` (the seat id) |
| `hold_expires_at` | string | When the reservation runs out, RFC 3339 |
| `payment_ref` | string | Your payment number, if you passed one |
| `tickets` | array | Tickets: `{id, url, tariff_id, holder_first_name, holder_last_name, status, seat}`. Empty until the tickets are issued |
| `seats` | array | The order's seats `{id, sector, row, number, label}` in item order |
| `reason` | string | Why the order can't be confirmed. `seat_taken`: a seat went to another order, refund the buyer |
| `source` | string | Where the order came from: `api`, `manual` (issued by hand) or `import` (from a guest list) |

`tickets[].url` is the link to the ticket page: `https://budu.events/t/…`. You can show it to the buyer right away. More in [tickets](https://budu.events/en/docs/tickets/#links).

## Statuses {#statuses}

| Status | Meaning |
|---|---|
| `reserved` | Tickets are held until `hold_expires_at`, no payment yet |
| `confirming` | Confirmation is in progress. The order can no longer be cancelled |
| `paid` | The sale is recorded and tickets are being issued. This usually takes a fraction of a second |
| `issued` | Tickets are issued and the email is sent |
| `expired` | The reservation expired. Confirming is still possible if tickets are left |
| `cancelled` | The reservation was cancelled |
| `failed` | Confirmation failed: the contents didn't match the reservation. Create a new order |

An order in `paid` or `issued` never becomes `expired` or `cancelled`. You can undo a sale only ticket by ticket: [`POST /v1/tickets/{id}/cancel`](https://budu.events/en/docs/tickets/#cancel).

## Create an order {#create}

`POST /v1/orders`

Reservation and confirmation in one request. Send it after payment.

| Field | Type | Required | Description |
|---|---|---|---|
| `event_id` | string | yes | UUID of a published event |
| `items` | array | yes | Items: `{"tariff_id", "qty"}` or `{"seat"}` for a [seat](https://budu.events/en/docs/seats/#order). Up to 20 tickets in total |
| `buyer.email` | string | yes | Email: the ticket email goes there |
| `buyer.first_name`, `buyer.last_name` | string | yes | First and last name: printed on the ticket |
| `buyer.phone` | string | no | Phone, stored with the order |
| `payment_ref` | string | no | Your payment number, returned in the order |

```bash Request
curl https://api.budu.events/v1/orders \
  -H "Authorization: Bearer $BUDU_KEY" \
  -H "Idempotency-Key: order-1042" \
  -H "Content-Type: application/json" \
  -d '{
    "event_id": "0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60",
    "items": [{"tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c71", "qty": 2}],
    "buyer": {
      "email": "maria@example.ru",
      "first_name": "Мария",
      "last_name": "Соколова"
    },
    "payment_ref": "pay-77812"
  }'
```

```json Response 201
{
  "id": "0193e0c5-1f20-7c3e-b6a1-9d4e2f7a8b10",
  "org_id": "0193d9a1-2b3c-7d4e-8f50-61728394a5b6",
  "livemode": false,
  "event_id": "0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60",
  "status": "issued",
  "buyer": {
    "email": "maria@example.ru",
    "first_name": "Мария",
    "last_name": "Соколова",
    "phone": ""
  },
  "items": [
    {"tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c71", "qty": 2, "seat": ""}
  ],
  "hold_expires_at": "2026-10-22T12:15:04Z",
  "payment_ref": "pay-77812",
  "tickets": [
    {
      "id": "0193e0c5-1f6b-7e02-a3c9-5b8d1e4f9f0a",
      "url": "https://budu.events/t/Vw3kqP7nX2bLmR9tYc4sHa",
      "tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c71",
      "holder_first_name": "Мария",
      "holder_last_name": "Соколова",
      "status": "valid",
      "seat": null
    },
    {
      "id": "0193e0c5-1f6b-7e02-a3c9-5b8d1e4fa21c",
      "url": "https://budu.events/t/9pLmZ2xQw8RkT4vNb6CyJe",
      "tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c71",
      "holder_first_name": "Мария",
      "holder_last_name": "Соколова",
      "status": "valid",
      "seat": null
    }
  ],
  "seats": [],
  "reason": "",
  "source": "api"
}
```

If the tickets take longer than a couple of seconds to issue, the response is `202`:

```json Response 202
{"status": "processing", "order_id": "0193e0c5-1f20-7c3e-b6a1-9d4e2f7a8b10"}
```

The sale is already recorded, the tickets get issued on their own and the email goes out as usual. The ticket links appear in [`GET /v1/orders/{id}`](#get): read the order once a second until the status turns `issued`.

| Error | When |
|---|---|
| `400 invalid_request` | No buyer, a bad email, no first or last name, empty contents, no such tier (`details.tariff_id`) |
| `404 not_found` | No such event or seat |
| `409 event_not_on_sale` | The event isn't published, or sales are still opening after publishing |
| `409 sold_out` | The tier is sold out (`details.tariff_id`) |
| `409 seat_taken` | The seat is already taken (`details.seat_id`, `details.seat`) |
| `422 limit_exceeded` | More than 20 tickets, or more than the tier's `per_order_limit` (`details.tariff_id`) |
| `500 internal` with `details.order_id` | The connection to the stock store broke off in the middle of the sale. We finish the order ourselves: check it with `GET /v1/orders/{id}` |

## Reserve {#reserve}

`POST /v1/reservations`

The same fields as an [order](#create), plus one more:

| Field | Type | Required | Description |
|---|---|---|---|
| `hold_ttl_seconds` | integer | no | How long to hold the reservation, 60 to 1,800 seconds. 900 by default, that is 15 minutes |

A reservation doesn't need `payment_ref`: you pass it when you confirm.

```bash Request
curl https://api.budu.events/v1/reservations \
  -H "Authorization: Bearer $BUDU_KEY" \
  -H "Idempotency-Key: cart-5531" \
  -H "Content-Type: application/json" \
  -d '{
    "event_id": "0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60",
    "items": [{"seat": "П-1-1"}, {"seat": "П-1-2"}],
    "buyer": {
      "email": "anna@example.ru",
      "first_name": "Анна",
      "last_name": "Петрова"
    },
    "hold_ttl_seconds": 600
  }'
```

You get `201` with the [order](#order-object) in `reserved` status and `hold_expires_at`. While the reservation lasts, nobody else can buy these tickets and seats. The errors are the same as for an order.

## Confirm a reservation {#confirm}

`POST /v1/reservations/{id}/confirm`

Call it after payment. The body is optional: `{"payment_ref": "pay-77813"}`.

```bash Request
curl https://api.budu.events/v1/reservations/0193e0c5-2a40-7b11-9c3d-4e5f6a7b8c90/confirm \
  -H "Authorization: Bearer $BUDU_KEY" \
  -H "Idempotency-Key: pay-77813" \
  -H "Content-Type: application/json" \
  -d '{"payment_ref": "pay-77813"}'
```

| Situation | Response |
|---|---|
| The reservation is alive, tickets are issued | `200`, the order in `issued` status with tickets |
| The sale is recorded, tickets are still being issued | `202 {"status": "processing", "order_id"}`. The tickets appear on their own |
| The reservation expired, but tickets are still left | the hold is taken again, then as in the first two rows |
| The reservation expired and the tickets are gone | `409 reservation_expired`: refund the buyer |
| A seat from the order went to another order | `409 seat_taken` with `details.order_id`, `details.seat_id`, `details.seat`. This is final: the order becomes `expired` with `reason: "seat_taken"`, refund the buyer |
| The reservation was cancelled | `400 invalid_request` «Резерв отменён — создайте новый» (reservation cancelled, create a new one) |
| The order is already confirmed with a different `payment_ref` | `409 idempotency_conflict`, the earlier payment in `details.payment_ref` |

Confirming is safe to repeat: a repeat returns the same order and the same tickets.

> **A successful confirm wins.** If two confirms, or a confirm and the reservation's expiry, overlap and the sale got recorded, the order becomes `paid` and the tickets get issued. We don't roll back a sale. So don't cancel the payment because your HTTP client timed out: repeat the confirm with the same `Idempotency-Key` or read the order. `409 reservation_expired` and `409 seat_taken` come only when the sale didn't happen.

## Cancel a reservation {#cancel}

`DELETE /v1/reservations/{id}`

Frees the reservation's tickets and seats when payment fails. A confirmed order can't be cancelled this way.

```bash Request
curl -X DELETE https://api.budu.events/v1/reservations/0193e0c5-2a40-7b11-9c3d-4e5f6a7b8c90 \
  -H "Authorization: Bearer $BUDU_KEY"
```

You get `200` with the order in `cancelled` status. Cancelling again answers the same. Errors: `400 invalid_request` «Заказ уже оплачивается — отменить резерв нельзя» (payment is in progress, status `confirming`) and «Заказ уже оплачен — отменяйте билеты по отдельности» (already paid, cancel tickets one by one; `paid`, `issued`).

## Get an order {#get}

`GET /v1/orders/{id}`

```bash Request
curl https://api.budu.events/v1/orders/0193e0c5-1f20-7c3e-b6a1-9d4e2f7a8b10 \
  -H "Authorization: Bearer $BUDU_KEY"
```

You get `200` with the [order](#order-object). This is always `200`, even while tickets are being issued: then the status is `paid` and `tickets` doesn't hold every ticket yet.
