# Ticket Management API

**Author**: Josh S. Sakweli, Backend Lead Team
**Last Updated**: 2026-02-20
**Version**: v1.2

**Base URL**: `https://your-api-domain.com/api/v1/e-events/tickets`

**Short Description**: The Ticket Management API allows event organizers to define and manage ticket types for their events on NextGate. Organizers can create multiple ticket tiers (e.g. VIP, Early Bird, General Admission), control pricing, capacity, sales periods, visibility, and sales channels. Tickets are scoped to a specific event and follow the event's lifecycle from DRAFT through to PUBLISHED.

**Hints**:
- Tickets can be created while the event is in **DRAFT or PUBLISHED** status. Full edits (name, price, type, etc.) are only allowed in **DRAFT**. After publishing, use the dedicated published-ticket endpoint for limited updates.
- For **HYBRID** events, you must create at least one `IN_PERSON` ticket and one `ONLINE` ticket before the event can be published.
- Ticket names must be unique per event per attendance mode — you cannot have two `IN_PERSON` tickets both named "VIP Pass" on the same event.
- **DONATION** tickets are restricted to `ONLINE_ONLY` sales channel and a maximum of 1 ticket per order and per user. They have no fixed price — the buyer freely chooses their donation amount at checkout.
- The ticket sales window must fall **within the event's registration window**. Sales cannot start before registration opens or end after registration closes.
- The minimum gap between `salesStartDateTime` and `salesEndDateTime` is **30 minutes**.
- Soft deletion is used — a ticket can only be deleted if zero tickets have been sold. Otherwise, close it using the status endpoint.
- All datetimes must be in **ISO 8601 / ZonedDateTime format** (e.g. `2025-08-10T09:00:00+03:00`).

---

## User Journey

```
  [Organizer creates event in DRAFT status]
                    |
                    | (Event must be DRAFT or PUBLISHED for ticket work)
                    v
  . . . . . . . . . . . . . . . . . . . . . . .
  .                                             .
  .   TICKET SETUP PHASE (DRAFT or PUBLISHED)  .
  .                                             .
  .   [Create ticket types]                     .
  .      |-- General Admission (PAID)           .
  .      |-- VIP Pass (PAID)                    .
  .      |-- Student Discount (PAID)            .
  .      |-- Free Entry (FREE)                  .
  .      '-- Support the Artist (DONATION)      .
  .                                             .
  . . . . . . . . . . . . . . . . . . . . . . .
                    |
                    v
  [Review all ticket types via Get All Tickets]
                    |
                    v
  . . . . . . . . . . . . . . . . . . . . . . .
  .                                             .
  .   ADJUSTMENTS PHASE (DRAFT only)            .
  .                                             .
  .   Need to fix details?                      .
  .      --> Update Ticket (name, price, etc.)  .
  .                                             .
  .   Wrong capacity?                           .
  .      --> Update Ticket Capacity             .
  .                                             .
  .   Ticket no longer needed?                  .
  .      --> Delete Ticket (only if 0 sold)     .
  .                                             .
  . . . . . . . . . . . . . . . . . . . . . . .
                    |
                    v
  [Event published — tickets go live for buyers]
                    |
                    v
  . . . . . . . . . . . . . . . . . . . . . . .
  .                                             .
  .   LIVE EVENT PHASE (PUBLISHED)              .
  .                                             .
  .   Need a new ticket tier?                   .
  .      --> Create Ticket (allowed on PUBLISHED).
  .                                             .
  .   Need to adjust visibility/status?         .
  .      --> Update Published Ticket            .
  .                                             .
  .   Need to shift the sales window?           .
  .      --> Update Sales Window                .
  .                                             .
  .   Ticket sells out?                         .
  .      --> System auto-sets SOLD_OUT          .
  .      --> Organizer can increase capacity    .
  .          to reactivate it                   .
  .                                             .
  .   Want to pause sales temporarily?          .
  .      --> Update Status to INACTIVE          .
  .                                             .
  .   Want to stop sales permanently?           .
  .      --> Update Status to CLOSED            .
  .                                             .
  . . . . . . . . . . . . . . . . . . . . . . .
```

---

## Sales Window Rules

Ticket sales must respect three nested time windows:

```
Event:          [eventStartDateTime ─────────────── eventEndDateTime]
Registration:         [registrationOpensAt ──── registrationClosesAt]
Ticket Sales:              [salesStartDateTime ── salesEndDateTime]
```

**Rules enforced:**
- `salesStartDateTime` cannot be in the past
- `salesEndDateTime` cannot be in the past
- `salesEndDateTime` must be after `salesStartDateTime`
- Minimum gap between sales start and end is **30 minutes**
- `salesStartDateTime` must be on or after `registrationOpensAt`
- `salesStartDateTime` cannot be after `registrationClosesAt`
- `salesEndDateTime` cannot be after `registrationClosesAt`
- Neither date can be after `eventEndDateTime`

---

## Standard Response Format

All API responses follow a consistent structure:

### Success Response Structure
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-09-23T10:30:45",
  "data": {}
}
```

### Error Response Structure
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-09-23T10:30:45",
  "data": "Error description"
}
```

### Standard Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | `true` for successful operations, `false` for errors |
| `httpStatus` | string | HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) |
| `message` | string | Human-readable description of the operation result |
| `action_time` | string | ISO 8601 timestamp of when the response was generated |
| `data` | object/string | Response payload on success; error detail on failure |

---

## HTTP Method Badge Standards

- **GET** — Green (`#28a745`) — Safe, read-only operations
- **POST** — Blue (`#007bff`) — Create new resources
- **PUT** — Yellow (`#ffc107`, black text) — Replace entire resource
- **PATCH** — Orange (`#fd7e14`) — Partial update
- **DELETE** — Red (`#dc3545`) — Remove resource

---

## Enum Reference

### TicketPricingType
| Value | Description |
|-------|-------------|
| `PAID` | Standard paid ticket. Price must be greater than `0.00` |
| `FREE` | Free entry. Price must be exactly `0.00` |
| `DONATION` | Supporter ticket. No fixed price — buyer freely enters their donation amount at checkout. Restricted to `ONLINE_ONLY` channel, max 1 per order and per user. `price` is `null` in responses |

### SalesChannel
| Value | Description |
|-------|-------------|
| `EVERYWHERE` | Available both online and at the door |
| `ONLINE_ONLY` | Available for purchase online only |
| `AT_DOOR_ONLY` | Available for purchase at the venue door only |

### AttendanceMode
| Value | Description |
|-------|-------------|
| `IN_PERSON` | Ticket grants physical entry to the venue |
| `ONLINE` | Ticket grants access to the online/virtual stream |

> For `IN_PERSON` events, only `IN_PERSON` tickets are allowed. For `ONLINE` events, only `ONLINE` tickets are allowed. For `HYBRID` events, both are permitted and at least one of each is required before publishing.

### TicketVisibility
| Value | Description |
|-------|-------------|
| `VISIBLE` | Always shown to the public |
| `HIDDEN` | Never shown to buyers (organizer use only) |
| `HIDDEN_WHEN_NOT_ON_SALE` | Only visible while the ticket is actively on sale |
| `CUSTOM_SCHEDULE` | Shown only within a defined date/time window. Requires `visibilityStartDate` and `visibilityEndDate` |

### TicketStatus
| Value | Description |
|-------|-------------|
| `ACTIVE` | Ticket is live and available for purchase |
| `INACTIVE` | Temporarily paused. Organizer can reactivate |
| `CLOSED` | Permanently stopped. Cannot be reopened |
| `SOLD_OUT` | System-managed. Set automatically when `ticketsSold >= totalTickets`. Reverts to `ACTIVE` if capacity is increased |
| `DELETED` | Soft-deleted. Only possible if zero tickets were sold. Use the Delete endpoint — cannot be set via status update |

---

## Endpoints

---

## 1. Create Ticket

**Purpose**: Creates a new ticket type for a specific event. The event must be in `DRAFT` or `PUBLISHED` status. The authenticated user must be the event organizer.

**Endpoint**: `POST` `{base_url}/{eventId}`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to add the ticket to | Must be a valid UUID |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `name` | string | Yes | Ticket name (e.g. "VIP Pass", "Early Bird") | Min: 2, Max: 100 characters. Must be unique per event per attendance mode |
| `description` | string | No | Optional description of what the ticket includes | Max: 500 characters |
| `price` | decimal | Conditional | Ticket price. Use `0.00` for FREE tickets. Omit or send `0.00` for DONATION tickets | Min: 0.00. PAID → must be > 0.00. FREE → must be 0.00. DONATION → ignored |
| `ticketPricingType` | string | Yes | Pricing model | Enum: `PAID`, `FREE`, `DONATION` |
| `salesChannel` | string | Yes | Where the ticket can be purchased. DONATION must use `ONLINE_ONLY` | Enum: `EVERYWHERE`, `ONLINE_ONLY`, `AT_DOOR_ONLY`. Defaults to `EVERYWHERE` |
| `totalQuantity` | integer | Yes | Total number of tickets available | Min: 1, Max: 1,000,000 |
| `salesStartDateTime` | datetime | No | When sales open. Must be within registration window | ISO 8601 ZonedDateTime. Cannot be before `registrationOpensAt` or after `registrationClosesAt` |
| `salesEndDateTime` | datetime | No | When sales close. Must be within registration window | ISO 8601 ZonedDateTime. Must be after `salesStartDateTime` with at least 30 minutes gap |
| `minQuantityPerOrder` | integer | No | Minimum tickets per order | Min: 1. Defaults to 1 |
| `maxQuantityPerOrder` | integer | No | Maximum tickets per order. DONATION tickets must be 1 | Min: 1, Max: 100. Must be ≥ `minQuantityPerOrder` |
| `maxQuantityPerUser` | integer | No | Maximum tickets a single user can purchase across all orders. DONATION tickets must be 1 | Min: 1, Max: 1000. Must be ≥ `maxQuantityPerOrder` |
| `visibility` | string | Yes | Controls whether the ticket is shown to buyers | Enum: `VISIBLE`, `HIDDEN`, `HIDDEN_WHEN_NOT_ON_SALE`, `CUSTOM_SCHEDULE`. Defaults to `VISIBLE` |
| `visibilityStartDate` | datetime | No | When the ticket becomes visible. Required if `visibility` is `CUSTOM_SCHEDULE` | ISO 8601 ZonedDateTime |
| `visibilityEndDate` | datetime | No | When the ticket stops being visible. Required if `visibility` is `CUSTOM_SCHEDULE` | ISO 8601 ZonedDateTime. Must be after `visibilityStartDate` |
| `attendanceMode` | string | Yes | Whether this ticket is for physical or online attendance | Enum: `IN_PERSON`, `ONLINE`. Must match the event format |
| `inclusiveItems` | array of strings | No | List of perks included with this ticket (e.g. "Free T-Shirt", "Meet & Greet") | Max: 50 items. Each item: max 200 characters, cannot be blank |

**Request JSON Sample (PAID)**:
```json
{
  "name": "VIP Pass",
  "description": "Full weekend access with backstage entry and a complimentary gift bag.",
  "price": 150.00,
  "ticketPricingType": "PAID",
  "salesChannel": "EVERYWHERE",
  "totalQuantity": 200,
  "salesStartDateTime": "2026-03-18T08:00:00+03:00",
  "salesEndDateTime": "2026-04-17T23:59:00+03:00",
  "minQuantityPerOrder": 1,
  "maxQuantityPerOrder": 4,
  "maxQuantityPerUser": 4,
  "visibility": "VISIBLE",
  "attendanceMode": "IN_PERSON",
  "inclusiveItems": [
    "Backstage access",
    "Complimentary gift bag",
    "Priority seating"
  ]
}
```

**Request JSON Sample (DONATION)**:
```json
{
  "name": "Support the Artist",
  "description": "Show your support — donate any amount you choose at checkout.",
  "price": 0.00,
  "ticketPricingType": "DONATION",
  "salesChannel": "ONLINE_ONLY",
  "totalQuantity": 500,
  "salesStartDateTime": "2026-03-18T08:00:00+03:00",
  "salesEndDateTime": "2026-04-17T23:59:00+03:00",
  "minQuantityPerOrder": 1,
  "maxQuantityPerOrder": 1,
  "maxQuantityPerUser": 1,
  "visibility": "VISIBLE",
  "attendanceMode": "IN_PERSON"
}
```

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Ticket created successfully",
  "action_time": "2025-02-18T10:30:45",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "eventId": "f1e2d3c4-b5a6-7890-fedc-ba9876543210",
    "name": "VIP Pass",
    "description": "Full weekend access with backstage entry and a complimentary gift bag.",
    "price": 150.00,
    "ticketPricingType": "PAID",
    "salesChannel": "EVERYWHERE",
    "totalTickets": 200,
    "ticketsSold": 0,
    "ticketsRemaining": 200,
    "ticketsAvailable": 200,
    "isSoldOut": false,
    "salesStartDateTime": "2026-03-18T05:00:00Z",
    "salesEndDateTime": "2026-04-17T20:59:00Z",
    "isOnSale": false,
    "minQuantityPerOrder": 1,
    "maxQuantityPerOrder": 4,
    "maxQuantityPerUser": 4,
    "visibility": "VISIBLE",
    "visibilityStartDate": null,
    "visibilityEndDate": null,
    "isCurrentlyVisible": true,
    "attendanceMode": "IN_PERSON",
    "inclusiveItems": [
      "Backstage access",
      "Complimentary gift bag",
      "Priority seating"
    ],
    "status": "ACTIVE",
    "saleStatusMessage": "On sale until Apr 17, 2026",
    "createdAt": "2025-02-18T10:30:45+03:00",
    "updatedAt": null,
    "createdBy": "john_organizer",
    "updatedBy": null
  }
}
```

> **Note on DONATION response**: For `DONATION` tickets, the `price` field is `null` in the response. The buyer enters their own amount at checkout.

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| `id` | Unique identifier for this ticket type |
| `eventId` | The event this ticket belongs to |
| `name` | Ticket name |
| `description` | Ticket description |
| `price` | Ticket price. `null` for DONATION tickets |
| `ticketPricingType` | Pricing model: `PAID`, `FREE`, or `DONATION` |
| `salesChannel` | Where the ticket can be purchased |
| `totalTickets` | Total number of tickets created |
| `ticketsSold` | Number of tickets purchased so far |
| `ticketsRemaining` | `totalTickets - ticketsSold` |
| `ticketsAvailable` | Same as `ticketsRemaining` |
| `isSoldOut` | `true` if `ticketsSold >= totalTickets` |
| `salesStartDateTime` | When ticket sales open |
| `salesEndDateTime` | When ticket sales close |
| `isOnSale` | `true` if ticket is ACTIVE and currently within the sales window |
| `saleStatusMessage` | Human-readable message describing the current sale state (e.g. `"On sale until Apr 17, 2026"`, `"Sales start Mar 18, 2026"`, `"Sales ended"`, `"Sold out"`) |
| `minQuantityPerOrder` | Minimum per order |
| `maxQuantityPerOrder` | Maximum per order (`null` = no limit) |
| `maxQuantityPerUser` | Maximum per user across all orders (`null` = no limit) |
| `visibility` | Visibility setting |
| `visibilityStartDate` | Start of custom visibility window |
| `visibilityEndDate` | End of custom visibility window |
| `isCurrentlyVisible` | Whether the ticket is currently visible to buyers |
| `attendanceMode` | `IN_PERSON` or `ONLINE` |
| `inclusiveItems` | List of perks included with the ticket |
| `status` | Ticket status: `ACTIVE`, `INACTIVE`, `CLOSED`, `SOLD_OUT`, `DELETED` |
| `createdAt` | Timestamp when the ticket was created |
| `updatedAt` | Timestamp of last update (`null` if never updated) |
| `createdBy` | Username of the organizer who created the ticket |
| `updatedBy` | Username of last person who updated the ticket |

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Authenticated user is not the event organizer |
| `404` | Event not found |
| `400` | Event is not in DRAFT or PUBLISHED status |
| `400` | Ticket name already exists for this event and attendance mode |
| `422` | Validation errors (missing required fields, invalid price, sales window outside registration window, gap less than 30 minutes, etc.) |

**Error Response Examples**:

*Event in invalid status (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Tickets can only be created for DRAFT or PUBLISHED events. Current status: CANCELLED",
  "action_time": "2025-02-18T10:30:45",
  "data": "Tickets can only be created for DRAFT or PUBLISHED events. Current status: CANCELLED"
}
```

*Duplicate Ticket Name (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "A ticket with name 'VIP Pass' and attendance mode 'IN_PERSON' already exists for this event",
  "action_time": "2025-02-18T10:30:45",
  "data": "A ticket with name 'VIP Pass' and attendance mode 'IN_PERSON' already exists for this event"
}
```

---

## 2. Update Ticket

**Purpose**: Updates the full details of an existing ticket type. The event must still be in `DRAFT` status. All fields are optional — only the fields you send will be updated.

**Endpoint**: `PUT` `{base_url}/{ticketId}`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `ticketId` | UUID | Yes | The ticket to update | Must be a valid UUID |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `name` | string | No | Updated ticket name | Min: 2, Max: 100 characters. Must remain unique per event per attendance mode |
| `description` | string | No | Updated description | Max: 500 characters |
| `price` | decimal | No | Updated price. Ignored for DONATION tickets | Min: 0.00. Must be consistent with `ticketPricingType` |
| `ticketPricingType` | string | No | Updated pricing model | Enum: `PAID`, `FREE`, `DONATION` |
| `salesChannel` | string | No | Updated sales channel. DONATION tickets must be `ONLINE_ONLY` | Enum: `EVERYWHERE`, `ONLINE_ONLY`, `AT_DOOR_ONLY` |
| `totalQuantity` | integer | No | Updated total capacity | Min: 1, Max: 1,000,000 |
| `salesStartDateTime` | datetime | No | Updated sales open time | ISO 8601 ZonedDateTime. Cannot be before `registrationOpensAt` |
| `salesEndDateTime` | datetime | No | Updated sales close time | ISO 8601 ZonedDateTime. At least 30 minutes after `salesStartDateTime` |
| `minQuantityPerOrder` | integer | No | Updated minimum per order | Min: 1 |
| `maxQuantityPerOrder` | integer | No | Updated maximum per order. DONATION must be 1 | Min: 1, Max: 100 |
| `maxQuantityPerUser` | integer | No | Updated maximum per user. DONATION must be 1 | Min: 1, Max: 1000 |
| `attendanceMode` | string | No | Updated attendance mode | Enum: `IN_PERSON`, `ONLINE`. Must match event format rules |
| `inclusiveItems` | array of strings | No | Full replacement list of inclusive perks | Max: 50 items. Each: max 200 characters, cannot be blank |
| `visibility` | string | No | Updated visibility | Enum: `VISIBLE`, `HIDDEN`, `HIDDEN_WHEN_NOT_ON_SALE`, `CUSTOM_SCHEDULE` |
| `visibilityStartDate` | datetime | No | Updated visibility start | Required if changing to `CUSTOM_SCHEDULE` |
| `visibilityEndDate` | datetime | No | Updated visibility end | Required if changing to `CUSTOM_SCHEDULE`. Must be after start |

**Request JSON Sample**:
```json
{
  "name": "VIP Weekend Pass",
  "price": 175.00,
  "maxQuantityPerOrder": 2,
  "inclusiveItems": [
    "Backstage access",
    "Complimentary gift bag",
    "Priority seating",
    "Artist meet & greet"
  ]
}
```

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket updated successfully",
  "action_time": "2025-02-18T11:00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "name": "VIP Weekend Pass",
    "price": 175.00,
    "maxQuantityPerOrder": 2,
    "status": "ACTIVE",
    "updatedAt": "2025-02-18T11:00:00+03:00",
    "updatedBy": "john_organizer"
  }
}
```

**Success Response Fields**: Same as [Create Ticket response fields](#1-create-ticket).

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Ticket not found |
| `400` | Event is not in DRAFT status |
| `400` | Updated name conflicts with an existing ticket on the same event |
| `422` | Validation errors (invalid price, quantity inconsistencies, sales window violations, etc.) |

---

## 3. Get All Tickets for Event

**Purpose**: Retrieves a lightweight summary list of all active (non-deleted) tickets for a given event, ordered by creation date ascending.

**Endpoint**: `GET` `{base_url}/{eventId}`

**Access Level**: 🌐 Public

**Authentication**: None required

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to retrieve tickets for | Must be a valid UUID |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Tickets retrieved successfully",
  "action_time": "2025-02-18T10:30:45",
  "data": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "General Admission",
      "price": 25.00,
      "ticketPricingType": "PAID",
      "salesChannel": "EVERYWHERE",
      "visibility": "VISIBLE",
      "totalTickets": 1000,
      "ticketsSold": 342,
      "ticketsAvailable": 658,
      "isSoldOut": false,
      "attendanceMode": "IN_PERSON",
      "status": "ACTIVE",
      "isOnSale": true
    },
    {
      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "name": "VIP Pass",
      "price": 150.00,
      "ticketPricingType": "PAID",
      "salesChannel": "EVERYWHERE",
      "visibility": "VISIBLE",
      "totalTickets": 200,
      "ticketsSold": 200,
      "ticketsAvailable": 0,
      "isSoldOut": true,
      "attendanceMode": "IN_PERSON",
      "status": "SOLD_OUT",
      "isOnSale": false
    }
  ]
}
```

**Success Response Fields** (per item):
| Field | Description |
|-------|-------------|
| `id` | Unique identifier for the ticket type |
| `name` | Ticket name |
| `price` | Ticket price. `null` for DONATION tickets |
| `ticketPricingType` | Pricing model |
| `salesChannel` | Where it can be purchased |
| `visibility` | Visibility setting |
| `totalTickets` | Total quantity created |
| `ticketsSold` | Quantity sold so far |
| `ticketsAvailable` | Quantity still available |
| `isSoldOut` | Whether the ticket is sold out |
| `attendanceMode` | `IN_PERSON` or `ONLINE` |
| `status` | Current ticket status |
| `isOnSale` | Whether the ticket is currently purchasable |
| `saleStatusMessage` | Human-readable message describing the current sale state |

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `404` | Event not found |

---

## 4. Get Ticket by ID

**Purpose**: Retrieves the full details of a single ticket type by its ID.

**Endpoint**: `GET` `{base_url}/{eventId}/{ticketId}`

**Access Level**: 🌐 Public

**Authentication**: None required

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event the ticket belongs to | Must be a valid UUID |
| `ticketId` | UUID | Yes | The specific ticket to retrieve | Must be a valid UUID |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket retrieved successfully",
  "action_time": "2025-02-18T10:30:45",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "eventId": "f1e2d3c4-b5a6-7890-fedc-ba9876543210",
    "name": "VIP Pass",
    "description": "Full weekend access with backstage entry and a complimentary gift bag.",
    "price": 150.00,
    "ticketPricingType": "PAID",
    "salesChannel": "EVERYWHERE",
    "totalTickets": 200,
    "ticketsSold": 45,
    "ticketsRemaining": 155,
    "ticketsAvailable": 155,
    "isSoldOut": false,
    "salesStartDateTime": "2026-03-18T05:00:00Z",
    "salesEndDateTime": "2026-04-17T20:59:00Z",
    "isOnSale": true,
    "saleStatusMessage": "On sale until Apr 17, 2026",
    "minQuantityPerOrder": 1,
    "maxQuantityPerOrder": 4,
    "maxQuantityPerUser": 4,
    "visibility": "VISIBLE",
    "visibilityStartDate": null,
    "visibilityEndDate": null,
    "isCurrentlyVisible": true,
    "attendanceMode": "IN_PERSON",
    "inclusiveItems": [
      "Backstage access",
      "Complimentary gift bag",
      "Priority seating"
    ],
    "status": "ACTIVE",
    "createdAt": "2025-02-18T10:30:45+03:00",
    "updatedAt": "2025-02-18T11:00:00+03:00",
    "createdBy": "john_organizer",
    "updatedBy": "john_organizer"
  }
}
```

**Success Response Fields**: Same as [Create Ticket response fields](#1-create-ticket).

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `404` | Event not found or ticket not found |

---

## 5. Update Ticket Capacity

**Purpose**: Updates the total quantity (capacity) of a ticket. Allowed on both DRAFT and PUBLISHED events. The new capacity must be greater than or equal to the number of tickets already sold.

**Endpoint**: `PATCH` `{base_url}/{eventId}/{ticketId}/capacity`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `ticketId` | UUID | Yes | The ticket to update capacity for | Must be a valid UUID |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `newTotalQuantity` | integer | Yes | The new total capacity | Min: 1, Max: 1,000,000. Must be ≥ `ticketsSold` |

**Request JSON Sample**:
```json
{
  "newTotalQuantity": 300
}
```

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket capacity updated successfully",
  "action_time": "2025-02-18T12:00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "name": "VIP Pass",
    "totalTickets": 300,
    "ticketsSold": 200,
    "ticketsRemaining": 100,
    "ticketsAvailable": 100,
    "isSoldOut": false,
    "status": "ACTIVE",
    "updatedAt": "2025-02-18T12:00:00+03:00",
    "updatedBy": "john_organizer"
  }
}
```

> **Note:** If a ticket was previously `SOLD_OUT` and the new capacity exceeds tickets sold, the status is automatically reset to `ACTIVE`.

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Ticket not found |
| `400` | New capacity is less than the number of tickets already sold |
| `422` | `newTotalQuantity` is missing or below minimum |

*Capacity Below Sold Count (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot reduce capacity to 100 because 200 tickets have already been sold",
  "action_time": "2025-02-18T12:00:00",
  "data": "Cannot reduce capacity to 100 because 200 tickets have already been sold"
}
```

---

## 6. Update Ticket Status

**Purpose**: Manually changes the status of a ticket type. Use this to pause sales (`INACTIVE`), permanently stop sales (`CLOSED`), or reactivate a paused ticket (`ACTIVE`). Works on both DRAFT and PUBLISHED events. The system automatically manages `SOLD_OUT` status — it cannot be set manually.

**Endpoint**: `PATCH` `{base_url}/{eventId}/{ticketId}/status`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `ticketId` | UUID | Yes | The ticket to update status for | Must be a valid UUID |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `status` | string | Yes | The new status to set | Enum: `ACTIVE`, `INACTIVE`, `CLOSED`. Cannot set `SOLD_OUT` or `DELETED` manually |

**Status Transition Rules**:
| Current Status | Allowed Transitions | Notes |
|----------------|---------------------|-------|
| `ACTIVE` | `INACTIVE`, `CLOSED` | Normal operations |
| `INACTIVE` | `ACTIVE`, `CLOSED` | Can be reactivated |
| `SOLD_OUT` | `ACTIVE`, `CLOSED` | `ACTIVE` only if capacity was increased first |
| `CLOSED` | None | Permanent. Cannot be changed |
| `DELETED` | None | Permanent. Cannot be changed |

**Request JSON Sample**:
```json
{
  "status": "INACTIVE"
}
```

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket status updated successfully",
  "action_time": "2025-02-18T13:00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "name": "VIP Pass",
    "status": "INACTIVE",
    "isOnSale": false,
    "updatedAt": "2025-02-18T13:00:00+03:00",
    "updatedBy": "john_organizer"
  }
}
```

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Ticket not found |
| `400` | Attempted to set `SOLD_OUT` or `DELETED` manually |
| `400` | Attempted to change status of a `CLOSED` or `DELETED` ticket |
| `422` | `status` field is missing |

---

## 7. Delete Ticket

**Purpose**: Soft-deletes a ticket type. The ticket is marked as deleted and hidden from all listings. **Deletion is only allowed if zero tickets have been sold.** If tickets have already been sold, close the ticket using the status endpoint instead.

**Endpoint**: `DELETE` `{base_url}/{eventId}/{ticketId}`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `ticketId` | UUID | Yes | The ticket to delete | Must be a valid UUID |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket deleted successfully",
  "action_time": "2025-02-18T14:00:00",
  "data": null
}
```

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Ticket not found |
| `400` | Ticket cannot be deleted because tickets have already been sold |

*Tickets Already Sold (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot delete ticket 'VIP Pass' because 45 tickets have been sold. You can close the ticket instead to stop sales.",
  "action_time": "2025-02-18T14:00:00",
  "data": "Cannot delete ticket 'VIP Pass' because 45 tickets have been sold. You can close the ticket instead to stop sales."
}
```

---

## 8. Update Sales Window

**Purpose**: Updates the sales start and/or end datetime of a ticket on a PUBLISHED event. Both fields are optional — omitting one preserves its current value. All existing sales period rules apply (30-minute minimum gap, must fall within registration window, etc.).

**Endpoint**: `PATCH` `{base_url}/{ticketId}/sales-window`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `ticketId` | UUID | Yes | The ticket to update the sales window for | Must be a valid UUID |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `salesStartDateTime` | datetime | No | New sales open time | ISO 8601 ZonedDateTime. Must be on or after `registrationOpensAt`. Cannot be after `registrationClosesAt` |
| `salesEndDateTime` | datetime | No | New sales close time | ISO 8601 ZonedDateTime. Must be after `salesStartDateTime` with at least 30 minutes gap. Cannot be after `registrationClosesAt` |

> **At least one of `salesStartDateTime` or `salesEndDateTime` must be provided.**

**Request JSON Sample**:
```json
{
  "salesEndDateTime": "2026-04-25T23:59:00+03:00"
}
```

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket sales window updated successfully",
  "action_time": "2026-02-20T10:00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "eventId": "f1e2d3c4-b5a6-7890-fedc-ba9876543210",
    "name": "VIP Pass",
    "salesStartDateTime": "2026-03-18T05:00:00Z",
    "salesEndDateTime": "2026-04-25T20:59:00Z",
    "isOnSale": true,
    "status": "ACTIVE",
    "updatedAt": "2026-02-20T10:00:00+03:00",
    "updatedBy": "john_organizer"
  }
}
```

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Ticket not found |
| `400` | Ticket is deleted or closed |
| `422` | Sales window violates registration window, gap less than 30 minutes, or no fields provided |

*Sales Window Outside Registration Window (422):*
```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Sales end date cannot be after registration closes (2026-04-17T20:59:00Z)",
  "action_time": "2026-02-20T10:00:00",
  "data": {
    "stage": "TICKETS",
    "message": "Sales end date cannot be after registration closes (2026-04-17T20:59:00Z)"
  }
}
```

---

## 9. Update Published Ticket

**Purpose**: Performs a limited update on a ticket belonging to a PUBLISHED event. Only three fields are allowed: `visibility` (and its schedule dates), `status` (ACTIVE, INACTIVE, or CLOSED), and `inclusiveItems`. All other fields must be updated while the event is still in DRAFT.

**Endpoint**: `PATCH` `{base_url}/{ticketId}/published`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `ticketId` | UUID | Yes | The published ticket to update | Must be a valid UUID |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `visibility` | string | No | Updated visibility setting | Enum: `VISIBLE`, `HIDDEN`, `HIDDEN_WHEN_NOT_ON_SALE`, `CUSTOM_SCHEDULE` |
| `visibilityStartDate` | datetime | No | Start of custom visibility window | Required if `visibility` is `CUSTOM_SCHEDULE`. ISO 8601 ZonedDateTime |
| `visibilityEndDate` | datetime | No | End of custom visibility window | Required if `visibility` is `CUSTOM_SCHEDULE`. Must be after `visibilityStartDate`. ISO 8601 ZonedDateTime |
| `status` | string | No | Updated ticket status | Enum: `ACTIVE`, `INACTIVE`, `CLOSED`. Cannot set `SOLD_OUT` or `DELETED` |
| `inclusiveItems` | array of strings | No | Full replacement list of perks | Max: 50 items. Each: max 200 characters, cannot be blank |

> **This endpoint is specifically for PUBLISHED events.** For DRAFT events, use the full `PUT /{ticketId}` endpoint instead.

**Request JSON Sample**:
```json
{
  "visibility": "CUSTOM_SCHEDULE",
  "visibilityStartDate": "2026-03-01T00:00:00+03:00",
  "visibilityEndDate": "2026-04-17T23:59:00+03:00",
  "inclusiveItems": [
    "Backstage access",
    "Complimentary gift bag",
    "Priority seating",
    "Artist meet & greet",
    "Exclusive after-party entry"
  ]
}
```

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket updated successfully",
  "action_time": "2026-02-20T10:00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "eventId": "f1e2d3c4-b5a6-7890-fedc-ba9876543210",
    "name": "VIP Pass",
    "description": "Full weekend access with backstage entry and a complimentary gift bag.",
    "price": 150.00,
    "ticketPricingType": "PAID",
    "salesChannel": "EVERYWHERE",
    "totalTickets": 200,
    "ticketsSold": 87,
    "ticketsRemaining": 113,
    "ticketsAvailable": 113,
    "isSoldOut": false,
    "salesStartDateTime": "2026-03-18T05:00:00Z",
    "salesEndDateTime": "2026-04-17T20:59:00Z",
    "isOnSale": true,
    "minQuantityPerOrder": 1,
    "maxQuantityPerOrder": 4,
    "maxQuantityPerUser": 4,
    "visibility": "CUSTOM_SCHEDULE",
    "visibilityStartDate": "2026-03-01T00:00:00+03:00",
    "visibilityEndDate": "2026-04-17T23:59:00+03:00",
    "isCurrentlyVisible": false,
    "attendanceMode": "IN_PERSON",
    "inclusiveItems": [
      "Backstage access",
      "Complimentary gift bag",
      "Priority seating",
      "Artist meet & greet",
      "Exclusive after-party entry"
    ],
    "status": "ACTIVE",
    "createdAt": "2025-02-18T10:30:45+03:00",
    "updatedAt": "2026-02-20T10:00:00+03:00",
    "createdBy": "john_organizer",
    "updatedBy": "john_organizer"
  }
}
```

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Ticket not found |
| `400` | Event is not in PUBLISHED status — use the draft update endpoint instead |
| `400` | Ticket is deleted |
| `422` | Invalid visibility schedule dates, attempted to set `SOLD_OUT` or `DELETED`, or `CUSTOM_SCHEDULE` missing required date fields |

*Event Not Published (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "This endpoint is only for published events. Use the draft ticket update endpoint instead.",
  "action_time": "2026-02-20T10:00:00",
  "data": "This endpoint is only for published events. Use the draft ticket update endpoint instead."
}
```

---

## Quick Reference

### Endpoint Summary
| # | Method | Path | Description | Auth | Event Status |
|---|--------|------|-------------|------|--------------|
| 1 | POST | `/{eventId}` | Create a ticket type | 🔒 Organizer | DRAFT or PUBLISHED |
| 2 | PUT | `/{ticketId}` | Full update of ticket details | 🔒 Organizer | DRAFT only |
| 3 | GET | `/{eventId}` | Get all tickets for an event | 🌐 Public | Any |
| 4 | GET | `/{eventId}/{ticketId}` | Get a single ticket by ID | 🌐 Public | Any |
| 5 | PATCH | `/{eventId}/{ticketId}/capacity` | Update ticket capacity | 🔒 Organizer | DRAFT or PUBLISHED |
| 6 | PATCH | `/{eventId}/{ticketId}/status` | Update ticket status | 🔒 Organizer | DRAFT or PUBLISHED |
| 7 | DELETE | `/{eventId}/{ticketId}` | Delete a ticket | 🔒 Organizer | DRAFT or PUBLISHED (0 sold) |
| 8 | PATCH | `/{ticketId}/sales-window` | Update sales window dates | 🔒 Organizer | PUBLISHED |
| 9 | PATCH | `/{ticketId}/published` | Limited update (visibility, status, perks) | 🔒 Organizer | PUBLISHED only |

### Common HTTP Status Codes
| Code | Meaning |
|------|---------|
| `200 OK` | Successful GET, PATCH, PUT, or DELETE |
| `201 Created` | Successful POST (ticket created) |
| `400 Bad Request` | Business rule violated (wrong status, already sold, duplicate name) |
| `401 Unauthorized` | Missing or invalid token |
| `403 Forbidden` | Authenticated but not the event organizer |
| `404 Not Found` | Event or ticket does not exist |
| `422 Unprocessable Entity` | Validation errors on request fields |

### Business Rule Cheat Sheet
| Rule | Detail |
|------|--------|
| Create ticket | Allowed on DRAFT and PUBLISHED events |
| Full update (`PUT`) | DRAFT events only |
| Published update (`PATCH /published`) | PUBLISHED events only. Visibility, status, and inclusiveItems only |
| Sales window update | PUBLISHED events. Validates against registration window |
| Capacity update | DRAFT and PUBLISHED events |
| Status update | DRAFT and PUBLISHED events |
| FREE ticket | Price must be exactly `0.00` |
| PAID ticket | Price must be greater than `0.00` |
| DONATION ticket | `ONLINE_ONLY` channel. Max 1 per order and per user. No fixed price — buyer sets amount at checkout. `price` is `null` in response |
| Sales window | Must fall within the event's registration window |
| Sales period gap | Minimum 30 minutes between `salesStartDateTime` and `salesEndDateTime` |
| HYBRID event | Must have ≥ 1 `IN_PERSON` ticket and ≥ 1 `ONLINE` ticket to publish |
| Delete | Only allowed if `ticketsSold = 0` |
| SOLD_OUT | System-managed. Cannot be set manually. Auto-cleared when capacity is increased |
| CLOSED | Permanent. Cannot be reversed |