Events API
- Events Management API
- Ticket Management API
- Event Checkout & Payment API
- Event Booking Orders API
- Event Check-In System API
- Organizer Analytics API
- Events Applicant Form API
- Attendance Analytics API
- Event Feedback API
- Event Fund Claims
Events Management API
Base URL: https://your-api-domain.com/api/v1/e-events
Short Description: The Event Core API provides full lifecycle management for events on the NextGate platform — from creating drafts and configuring schedules, to publishing, managing live events, and discovery. It is used by event organizers to build, publish, and manage events step-by-step, and by all authenticated users to browse, search, and filter published events.
Hints:
- All write operations (create, update, publish) require a valid Bearer token. Read operations on published events are public.
- Dates and times must always use ISO 8601 format with timezone offset (e.g.,
2025-06-15T09:00:00+03:00). The API stores and returnsZonedDateTime. - Event creation follows a staged workflow:
BASIC_INFO → SCHEDULE → LOCATION_DETAILS → REGISTRATION_SETUPS → TICKETS → REVIEW. All required stages must be completed before publishing. - Slugs are auto-generated from the event title with a UUID suffix to guarantee uniqueness — do not pass a slug manually.
- Pagination uses 1-based page numbers (page=1 is the first page).
- Category seeding and management live in the Event Categories API (base:
/api/v1/e-events/categories), documented separately.
Event Creation User Journey
[Organizer]
│
│ POST /draft
▼
┌─────────────┐
│ BASIC INFO │ title, category, format, description, media
└──────┬──────┘
│ PATCH /drafts/{id}/basic-info (optional update)
│
│ PATCH /draft/{id}/schedule
▼
┌──────────────┐
│ SCHEDULE │ days[ date, startTime, endTime ], timezone
└──────┬───────┘
│
│ PATCH /draft/{id}/location
▼
┌──────────────────┐
│ LOCATION DETAILS │ venue (IN_PERSON/HYBRID) or virtualDetails (ONLINE/HYBRID)
└──────┬───────────┘ or skip entirely (TBA format)
│
│ PATCH /drafts/{id}/registration
▼
┌───────────────────────┐
│ REGISTRATION SETUPS │ registrationOpensAt, registrationClosesAt
└──────┬────────────────┘
│
│ (Ticket creation via Tickets API — required before publish)
▼
┌──────────────┐
│ TICKETS │ at least one active ticket required
└──────┬───────┘
│
│ (Optional enrichment)
│ PATCH /drafts/{id}/highlights
│ PATCH /drafts/{id}/faqs
│ PATCH /drafts/{id}/lineup
│ PATCH /drafts/{id}/agenda
│ POST /draft/{id}/products/{productId}
│ POST /draft/{id}/shops/{shopId}
│
│ PATCH /{eventId}/publish
▼
┌───────────────┐
│ PUBLISHED │ RSA keys generated, category count incremented,
│ │ event visible in public feed
└───────────────┘
Event Status Flow
DRAFT ◄──────────────────────► PUBLISHED
│ (unpublish │
│ if 0 tickets sold) │ (unpublish — only if no tickets sold)
│ │
│ (discardDraft) │
▼ ▼
[deleted] CANCELLED ◄─── (cancel from any non-terminal status)
│
[terminal state]
PUBLISHED ──► (system / scheduled job) ──► HAPPENING ──► COMPLETED
Status Rules:
DRAFT ↔ PUBLISHED— Free movement. Unpublish is only allowed if zero tickets have been sold.CANCELLED— Terminal. Can be triggered from any non-terminal status. Triggers bulk refund if tickets were sold.HAPPENING/COMPLETED— System-managed via scheduled jobs.
Standard Response Format
All API responses follow a consistent structure using the Globe Response Builder pattern.
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-09-23T10:30:45",
"data": { }
}
Error Response Structure
{
"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, CREATED, BAD_REQUEST, etc.) |
message |
string | Human-readable description of the result |
action_time |
string | ISO 8601 timestamp of when the response was generated |
data |
object / string | Response payload on success; error detail on failure |
Shared Response Object Definitions
The following objects are returned by multiple endpoints. They are defined once here and referenced throughout.
A. EventResponse (Full Event Object)
Returned by all draft and event management endpoints.
| Field | Type | Description |
|---|---|---|
id |
UUID | Unique event identifier |
title |
string | Event title |
slug |
string | URL-friendly identifier, auto-generated |
description |
string | Full event description |
category.categoryId |
UUID | Category identifier |
category.categoryName |
string | Category display name |
category.categorySlug |
string | Category slug |
eventFormat |
string | IN_PERSON, ONLINE, HYBRID, or TBA |
eventVisibility |
string | PUBLIC, PRIVATE, or UNLISTED |
status |
string | DRAFT, PUBLISHED, HAPPENING, CANCELLED, COMPLETED |
schedule.startDateTime |
ZonedDateTime | Event start (ISO 8601 with offset) |
schedule.endDateTime |
ZonedDateTime | Event end (ISO 8601 with offset) |
schedule.timezone |
string | IANA timezone string (e.g., Africa/Dar_es_Salaam) |
schedule.days[] |
array | Day-level schedule entries (see EventDayInfo below) |
venue.name |
string | Venue name (IN_PERSON / HYBRID only) |
venue.address |
string | Venue address |
venue.coordinates.latitude |
string | Latitude decimal string |
venue.coordinates.longitude |
string | Longitude decimal string |
virtualDetails.meetingLink |
string | Meeting URL (ONLINE / HYBRID only) |
virtualDetails.meetingId |
string | Platform meeting ID |
virtualDetails.passcode |
string | Meeting passcode |
media.banner |
string | Banner image URL |
media.thumbnail |
string | Thumbnail image URL |
media.gallery[] |
array | List of gallery image URLs |
highlights[] |
array | See HighlightEntry definition below |
faqs[] |
array | See FaqEntry definition below |
lineup[] |
array | See LineupEntry definition below |
agenda[] |
array | See AgendaDay definition below |
linkedProducts[] |
array | { productId, productName, productSlug } |
linkedShops[] |
array | { shopId, shopName, shopSlug } |
tickets[] |
array | See TicketSummaryInfo definition below |
organizer.organizerId |
UUID | Organizer account ID |
organizer.organizerName |
string | Organizer full name |
organizer.organizerUsername |
string | Organizer system username |
currentStage |
string | Current event creation stage |
completedStages[] |
array | List of completed stage names |
completionPercentage |
integer | 0–100 completion percentage |
canPublish |
boolean | Whether all required stages are completed |
createdAt |
ZonedDateTime | Creation timestamp |
updatedAt |
ZonedDateTime | Last update timestamp |
createdBy |
string | Username of creator |
updatedBy |
string | Username of last editor |
EventDayInfo
| Field | Type | Description |
|---|---|---|
id |
UUID | Day entity ID |
date |
string | Date in YYYY-MM-DD format |
startTime |
string | Start time in HH:mm:ss |
endTime |
string | End time in HH:mm:ss |
description |
string | Optional day description |
dayOrder |
integer | Display order (1-based) |
HighlightEntry
| Field | Type | Description |
|---|---|---|
type |
string | AGE_RESTRICTION, CHECK_IN_TIME, PARKING, DRESS_CODE, FOOD_DRINKS, ACCESSIBILITY, REFUND_POLICY, WHAT_TO_BRING, PROHIBITED_ITEMS, WEATHER_INFO, CUSTOM |
title |
string | Display title for this highlight |
value |
string | Short value (e.g., "18+") |
description |
string | Longer explanation |
FaqEntry
| Field | Type | Description |
|---|---|---|
question |
string | The FAQ question |
answer |
string | The answer |
order |
integer | Display order |
LineupEntry
| Field | Type | Description |
|---|---|---|
entryType |
string | PLATFORM_USER or CUSTOM |
userId |
UUID | Platform user ID (only when entryType=PLATFORM_USER) |
name |
string | Display name (auto-enriched from user profile when PLATFORM_USER) |
role |
string | HEADLINER, PERFORMER, SPEAKER, DJ, HOST, PANELIST, MODERATOR, GUEST |
title |
string | Professional title (e.g., "Lead Vocalist") |
bio |
string | Short biography |
image |
string | Profile/headshot image URL |
performanceDay |
integer | Which day number of the event they perform |
performanceTime |
string | Time of performance |
order |
integer | Display order |
AgendaDay
| Field | Type | Description |
|---|---|---|
dayNumber |
integer | Day number (1-based) |
date |
string | Date in YYYY-MM-DD format |
sessions[] |
array | See AgendaSession definition below |
AgendaSession
| Field | Type | Description |
|---|---|---|
startTime |
string | Session start time (HH:mm) |
endTime |
string | Session end time (HH:mm) |
title |
string | Session title |
description |
string | Session description |
type |
string | GENERAL, PERFORMANCE, CEREMONY, PANEL, WORKSHOP, NETWORKING, MEAL, BREAK |
location |
string | Sub-location within the event venue |
presenterType |
string | PLATFORM_USER or CUSTOM |
presenterId |
UUID | Platform user ID (when presenterType=PLATFORM_USER) |
presenterName |
string | Presenter name (auto-enriched from user profile when PLATFORM_USER) |
presenterTitle |
string | Professional title |
presenterBio |
string | Short biography |
presenterImage |
string | Headshot image URL |
TicketSummaryInfo
| Field | Type | Description |
|---|---|---|
id |
UUID | Ticket type ID |
name |
string | Ticket type name (e.g., "VIP", "General Admission") |
price |
BigDecimal | Ticket price (0.00 for free) |
totalTickets |
integer | Total slots allocated |
ticketsSold |
integer | Number sold |
ticketsAvailable |
integer | Remaining slots |
isSoldOut |
boolean | True when available = 0 |
attendanceMode |
string | Ticket attendance mode enum value |
status |
string | Ticket status enum value |
isOnSale |
boolean | Whether the ticket is currently on sale |
B. EventSummaryResponse (Lightweight List Object)
Returned by all paginated list and search endpoints.
| Field | Type | Description |
|---|---|---|
id |
UUID | Event ID |
title |
string | Event title |
slug |
string | URL slug |
shortDescription |
string | First 150 characters of description |
categoryId |
UUID | Category ID |
categoryName |
string | Category display name |
eventFormat |
string | IN_PERSON, ONLINE, HYBRID, TBA |
eventVisibility |
string | PUBLIC, PRIVATE, UNLISTED |
status |
string | Event status |
startDateTime |
ZonedDateTime | Start date/time with offset |
endDateTime |
ZonedDateTime | End date/time with offset |
timezone |
string | IANA timezone |
locationSummary |
string | Human-readable location (e.g., "Dar es Salaam, TZ", "Online Event", "Location To Be Announced") |
thumbnail |
string | Thumbnail URL |
pricing.minPrice |
BigDecimal | Lowest available ticket price |
pricing.maxPrice |
BigDecimal | Highest available ticket price |
pricing.isFree |
boolean | True when all tickets are free |
pricing.hasPaidTickets |
boolean | True when at least one paid ticket exists |
organizerId |
UUID | Organizer ID |
organizerName |
string | Organizer full name |
organizerUsername |
string | Organizer username |
stats.totalTickets |
integer | Sum of all ticket slots |
stats.ticketsSold |
integer | Total tickets sold |
stats.ticketsAvailable |
integer | Remaining tickets |
stats.isSoldOut |
boolean | True when no tickets remain |
stats.attendeeCount |
integer | Same as ticketsSold |
createdAt |
ZonedDateTime | Creation timestamp |
C. Standard Paginated Response Wrapper
All list endpoints return data inside a Spring Page wrapper.
{
"success": true,
"httpStatus": "OK",
"message": "Events retrieved successfully",
"action_time": "2025-02-17T10:30:45",
"data": {
"content": [ ],
"pageable": {
"pageNumber": 0,
"pageSize": 10
},
"totalElements": 42,
"totalPages": 5,
"last": false,
"first": true,
"empty": false
}
}
Note: All list endpoints accept
page(1-based, default1) andsize(default10) as query parameters. Spring internally converts to 0-based before querying.
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
Standard Error Types
Application-Level Exceptions (400–499)
| Code | Name | When it occurs |
|---|---|---|
400 |
BAD_REQUEST |
Invalid request data, already published event, duplicate product/shop, unpublish blocked due to ticket sales |
401 |
UNAUTHORIZED |
Missing, expired, or malformed Bearer token |
403 |
FORBIDDEN |
Authenticated but not the event organizer, or accessing a draft belonging to another user |
404 |
NOT_FOUND |
Event, category, product, or shop ID not found |
422 |
UNPROCESSABLE_ENTITY |
Bean validation failures with per-field detail |
Server-Level Exceptions (500+)
| Code | Name | When it occurs |
|---|---|---|
500 |
INTERNAL_SERVER_ERROR |
RSA key generation failure, unexpected runtime error |
Shared Error Response Examples
All endpoints may return these error shapes. Each endpoint section references them rather than repeating the full JSON.
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Token has expired",
"action_time": "2025-02-17T10:30:45",
"data": "Token has expired"
}
403 — Forbidden:
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "Access denied: Insufficient permissions",
"action_time": "2025-02-17T10:30:45",
"data": "Access denied: Insufficient permissions"
}
404 — Not Found:
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Event not found with ID: 3fa85f64-5717-4562-b3fc-2c963f66afa6",
"action_time": "2025-02-17T10:30:45",
"data": "Event not found with ID: 3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
422 — Validation Error:
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Validation failed",
"action_time": "2025-02-17T10:30:45",
"data": {
"title": "size must be between 3 and 200",
"categoryId": "must not be null",
"eventFormat": "must not be null"
}
}
Endpoints
1. Create Event Draft
Purpose: Creates a new event in DRAFT status as the first step of the event creation workflow. Marks BASIC_INFO stage as completed automatically.
Endpoint: POST /api/v1/e-events/drafts
Access Level: 🔒 Protected (Any authenticated user — becomes event organizer)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Content-Type |
string | Yes | application/json |
Request JSON Sample:
{
"title": "Dar es Salaam Jazz Festival 2025",
"categoryId": "8e3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"eventFormat": "IN_PERSON",
"eventVisibility": "PUBLIC",
"description": "An annual celebration of jazz and live music at the heart of the city.",
"media": {
"banner": "https://cdn.example.com/banners/jazz-2025.jpg",
"thumbnail": "https://cdn.example.com/thumbs/jazz-2025.jpg",
"gallery": []
}
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
title |
string | Yes | Event title | Min: 3, Max: 200 characters |
categoryId |
UUID | Yes | ID of an active event category | Must exist and be active |
eventFormat |
string | Yes | Event format | Enum: IN_PERSON, ONLINE, HYBRID, TBA |
eventVisibility |
string | No | Who can see the event | Enum: PUBLIC, PRIVATE, UNLISTED. Defaults to PUBLIC |
description |
string | No | Full event description | Max: 5000 characters |
media |
object | No | Media URLs for the event | See MediaRequest below |
media.banner |
string | No | Banner image URL | Max: 500 characters |
media.thumbnail |
string | No | Thumbnail image URL | Max: 500 characters |
media.gallery |
array | No | List of gallery image URLs | — |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "CREATED",
"message": "Event draft created",
"action_time": "2025-02-17T10:30:45",
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"title": "Dar es Salaam Jazz Festival 2025",
"slug": "dar-es-salaam-jazz-festival-2025-a1b2c3d4",
"description": "An annual celebration of jazz...",
"category": {
"categoryId": "8e3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"categoryName": "Music & Concerts",
"categorySlug": "music-concerts"
},
"eventFormat": "IN_PERSON",
"eventVisibility": "PUBLIC",
"status": "DRAFT",
"currentStage": "BASIC_INFO",
"completedStages": ["BASIC_INFO"],
"completionPercentage": 20,
"canPublish": false,
"schedule": null,
"venue": null,
"virtualDetails": null,
"media": {
"banner": "https://cdn.example.com/banners/jazz-2025.jpg",
"thumbnail": "https://cdn.example.com/thumbs/jazz-2025.jpg",
"gallery": []
},
"highlights": null,
"faqs": null,
"lineup": null,
"agenda": null,
"linkedProducts": [],
"linkedShops": [],
"tickets": [],
"organizer": {
"organizerId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"organizerName": "Amina Hassan",
"organizerUsername": "amina.hassan"
},
"createdAt": "2025-02-17T10:30:45+03:00",
"updatedAt": null,
"createdBy": "amina.hassan",
"updatedBy": null
}
}
Success Response Fields: See Shared Response Object A — EventResponse.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token — see Shared Error: 401 |
404 |
Category ID not found |
422 |
Missing required fields (title, categoryId, eventFormat) — see Shared Error: 422 |
2. Get My Drafts
Purpose: Returns a paginated list of all DRAFT events owned by the authenticated organizer.
Endpoint: GET /api/v1/e-events/drafts
Access Level: 🔒 Protected (Organizer — own drafts only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
page |
integer | No | Page number (1-based) | Min: 1 | 1 |
size |
integer | No | Items per page | Min: 1 | 10 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Drafts retrieved",
"action_time": "2025-02-17T10:30:45",
"data": {
"content": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"title": "Dar es Salaam Jazz Festival 2025",
"slug": "dar-es-salaam-jazz-festival-2025-a1b2c3d4",
"shortDescription": "An annual celebration of jazz and live music...",
"categoryId": "8e3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"categoryName": "Music & Concerts",
"eventFormat": "IN_PERSON",
"eventVisibility": "PUBLIC",
"status": "DRAFT",
"startDateTime": null,
"endDateTime": null,
"timezone": null,
"locationSummary": null,
"thumbnail": "https://cdn.example.com/thumbs/jazz-2025.jpg",
"pricing": { "isFree": true, "hasPaidTickets": false },
"organizerName": "Amina Hassan",
"organizerUsername": "amina.hassan",
"stats": { "totalTickets": 0, "ticketsSold": 0, "ticketsAvailable": 0, "isSoldOut": false },
"createdAt": "2025-02-17T10:30:45+03:00"
}
],
"totalElements": 3,
"totalPages": 1,
"first": true,
"last": true,
"empty": false
}
}
Success Response Fields: data.content[] contains EventSummaryResponse objects. Pagination fields follow Standard Paginated Response Wrapper.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token — see Shared Error: 401 |
3. Get Draft by ID
Purpose: Retrieves the full detail of a specific draft. Only the draft's organizer can access it.
Endpoint: GET /api/v1/e-events/drafts/{draftId}
Access Level: 🔒 Protected (Organizer — own drafts only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
draftId |
UUID | Yes | The ID of the draft event | Must be a valid UUID |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Draft retrieved",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Authenticated but not the owner of this draft |
404 |
Draft not found with given ID |
4. Discard Draft
Purpose: Permanently deletes a draft event and all its associated day schedules, linked products, and linked shops. This action is irreversible.
Endpoint: DELETE /api/v1/e-events/drafts/{draftId}
Access Level: 🔒 Protected (Organizer — own drafts only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
draftId |
UUID | Yes | The ID of the draft to discard | Must be a valid UUID |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Draft discarded",
"action_time": "2025-02-17T10:30:45",
"data": null
}
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Authenticated but not the owner of this draft |
404 |
Draft not found with given ID |
5. Update Draft — Basic Info
Purpose: Updates the basic information of a draft. All fields are optional — only provided fields are updated. Advances currentStage to SCHEDULE upon completion.
Endpoint: PATCH /api/v1/e-events/drafts/{draftId}/basic-info
Access Level: 🔒 Protected (Organizer — own drafts 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 |
|---|---|---|---|---|
draftId |
UUID | Yes | The draft to update | Must be a valid UUID |
Request JSON Sample:
{
"title": "Dar es Salaam Jazz Festival 2025 — Updated",
"description": "The biggest jazz event in East Africa returns for its 10th edition with over 30 artists.",
"categoryId": "8e3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"eventVisibility": "PUBLIC",
"eventFormat": "IN_PERSON",
"media": {
"banner": "https://cdn.example.com/banners/jazz-2025-v2.jpg",
"thumbnail": "https://cdn.example.com/thumbs/jazz-2025-v2.jpg",
"gallery": ["https://cdn.example.com/gallery/img1.jpg"]
}
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
title |
string | No | Updated event title | Min: 3, Max: 200 characters |
description |
string | No | Updated description | Min: 15, Max: 5000 characters |
categoryId |
UUID | No | New category | Must exist and be active |
eventVisibility |
string | No | Visibility change | Enum: PUBLIC, PRIVATE, UNLISTED |
eventFormat |
string | No | Format change | Enum: IN_PERSON, ONLINE, HYBRID, TBA |
media |
object | No | Updated media | See media fields in endpoint 1 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Basic info updated",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. Note that completedStages will now include "BASIC_INFO" and currentStage will be "SCHEDULE".
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the draft owner |
404 |
Draft or category not found |
422 |
Validation failure on provided fields |
6. Update Draft — Schedule
Purpose: Sets the event's day-by-day schedule. Supports multi-day events. Each day must have a unique date in chronological order. Advances currentStage to LOCATION_DETAILS. The overall startDateTime and endDateTime on the event are derived automatically from the first and last day.
Endpoint: PATCH /api/v1/e-events/drafts/{draftId}/schedule
Access Level: 🔒 Protected (Organizer — own drafts 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 |
|---|---|---|---|---|
draftId |
UUID | Yes | The draft to update | Must be a valid UUID |
Request JSON Sample:
{
"timezone": "Africa/Dar_es_Salaam",
"days": [
{
"date": "2025-07-18",
"startTime": "18:00:00",
"endTime": "23:00:00",
"description": "Opening Night",
"dayOrder": 1
},
{
"date": "2025-07-19",
"startTime": "16:00:00",
"endTime": "23:59:00",
"description": "Main Concert Day",
"dayOrder": 2
}
]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
timezone |
string | No | IANA timezone identifier | Must be a valid IANA zone ID (e.g., Africa/Dar_es_Salaam). Defaults to UTC |
days |
array | Yes | List of event days | Min 1 day required |
days[].date |
string | Yes | Date of the day | YYYY-MM-DD format; must not be in the past |
days[].startTime |
string | Yes | Day start time | HH:mm:ss format |
days[].endTime |
string | Yes | Day end time | HH:mm:ss; must be after startTime |
days[].description |
string | No | Optional day description | — |
days[].dayOrder |
integer | No | Display order | Defaults to position in array if omitted |
Notes:
- All existing days are replaced on each call. To update the schedule, resend the full days array.
- Dates must be unique — duplicate dates in the same request are rejected with
422.- Days must be provided in chronological order (sorted ascending by date).
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Schedule updated",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. schedule.days will be populated, and schedule.startDateTime / schedule.endDateTime will be set from the first and last day.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the draft owner |
404 |
Draft not found |
422 |
Days out of order, duplicate dates, date in the past, missing time fields |
7. Update Draft — Location
Purpose: Sets the physical venue and/or virtual meeting details for the event. Required fields depend on eventFormat. For TBA format, this endpoint can be called but no fields are required — the stage is automatically marked complete.
Endpoint: PATCH /api/v1/e-events/drafts/{draftId}/location
Access Level: 🔒 Protected (Organizer — own drafts 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 |
|---|---|---|---|---|
draftId |
UUID | Yes | The draft to update | Must be a valid UUID |
Request JSON Sample (IN_PERSON):
{
"venue": {
"name": "Mlimani City Arena",
"address": "Sam Nujoma Road, Dar es Salaam",
"coordinates": {
"latitude": -6.7724,
"longitude": 39.2083
}
}
}
Request JSON Sample (ONLINE):
{
"virtualDetails": {
"meetingLink": "https://zoom.us/j/123456789",
"meetingId": "123 456 789",
"passcode": "jazz2025"
}
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
venue |
object | Conditional | Required when format is IN_PERSON or HYBRID |
— |
venue.name |
string | Yes (if venue) | Venue name | Max: 200 characters |
venue.address |
string | No | Full address | Max: 500 characters |
venue.coordinates.latitude |
BigDecimal | No | GPS latitude | — |
venue.coordinates.longitude |
BigDecimal | No | GPS longitude | — |
virtualDetails |
object | Conditional | Required when format is ONLINE or HYBRID |
— |
virtualDetails.meetingLink |
string | Yes (if virtualDetails) | Meeting URL | Max: 500 characters |
virtualDetails.meetingId |
string | No | Platform meeting ID | Max: 100 characters |
virtualDetails.passcode |
string | No | Meeting passcode | Max: 100 characters |
Format-based rules:
IN_PERSON→venue.nameis required;virtualDetailsis ignoredONLINE→virtualDetails.meetingLinkis required;venueis ignoredHYBRID→ bothvenue.nameandvirtualDetails.meetingLinkare requiredTBA→ no fields required; stage is immediately marked complete
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Location updated",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. completedStages will include "LOCATION_DETAILS" when requirements are met.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the draft owner |
404 |
Draft not found |
422 |
Missing required venue/virtual fields for the event format |
8. Update Draft — Registration Config
Purpose: Sets the registration window (open and close dates) for the event. The window must be valid relative to itself and cannot close after the event ends. Advances currentStage to TICKETS.
Endpoint: PATCH /api/v1/e-events/drafts/{draftId}/registration
Access Level: 🔒 Protected (Organizer — own drafts 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 |
|---|---|---|---|---|
draftId |
UUID | Yes | The draft to update | Must be a valid UUID |
Request JSON Sample:
{
"registrationOpensAt": "2025-05-01T08:00:00+03:00",
"registrationClosesAt": "2025-07-17T23:59:00+03:00",
"ctaLabel":"Get ticket"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
registrationOpensAt |
ZonedDateTime | Yes | When registration opens | ISO 8601 with timezone offset; must be before registrationClosesAt |
registrationClosesAt |
ZonedDateTime | Yes | When registration closes | ISO 8601 with timezone offset; must not be after the event's endDateTime |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Registration config updated",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. completedStages will include "REGISTRATION_SETUPS" and currentStage will advance to "TICKETS".
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the draft owner |
404 |
Draft not found |
422 |
registrationOpensAt is after registrationClosesAt, or registrationClosesAt is after event end |
9. Update Draft — Highlights
Purpose: Replaces the full list of event highlights (key attendee-facing info such as age restriction, dress code, parking, etc.). Sending an empty array clears all highlights.
Endpoint: PATCH /api/v1/e-events/drafts/{draftId}/highlights
Access Level: 🔒 Protected (Organizer — own drafts 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 |
|---|---|---|---|---|
draftId |
UUID | Yes | The draft to update | Must be a valid UUID |
Request JSON Sample:
{
"highlights": [
{
"type": "AGE_RESTRICTION",
"title": "Age Limit",
"value": "18+",
"description": "This event is strictly for adults aged 18 and above."
},
{
"type": "DRESS_CODE",
"title": "Dress Code",
"value": "Smart Casual",
"description": "No sportswear or flip flops allowed."
}
]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
highlights |
array | Yes | Full list of highlights | Empty array is valid (clears existing) |
highlights[].type |
string | Yes | Highlight category | Enum: AGE_RESTRICTION, CHECK_IN_TIME, PARKING, DRESS_CODE, FOOD_DRINKS, ACCESSIBILITY, REFUND_POLICY, WHAT_TO_BRING, PROHIBITED_ITEMS, WEATHER_INFO, CUSTOM |
highlights[].title |
string | Yes | Display title | — |
highlights[].value |
string | No | Short value summary | — |
highlights[].description |
string | No | Longer explanation | — |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Highlights updated",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. data.highlights will reflect the new list.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the draft owner |
404 |
Draft not found |
10. Update Draft — FAQs
Purpose: Replaces the full list of frequently asked questions for the event. Sending an empty array clears all FAQs.
Endpoint: PATCH /api/v1/e-events/drafts/{draftId}/faqs
Access Level: 🔒 Protected (Organizer — own drafts 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 |
|---|---|---|---|---|
draftId |
UUID | Yes | The draft to update | Must be a valid UUID |
Request JSON Sample:
{
"faqs": [
{
"question": "Is this event suitable for children?",
"answer": "No. This event is strictly 18+ only.",
"order": 1
},
{
"question": "Is parking available?",
"answer": "Yes, free parking is available on site.",
"order": 2
}
]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
faqs |
array | Yes | Full list of FAQs | Empty array is valid (clears existing) |
faqs[].question |
string | Yes | FAQ question text | — |
faqs[].answer |
string | Yes | FAQ answer text | — |
faqs[].order |
integer | No | Display order | — |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "FAQs updated",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. data.faqs will reflect the new list.
Possible Error Responses: Same as Endpoint 9 (401, 403, 404).
11. Update Draft — Lineup
Purpose: Replaces the full event lineup. Supports both platform users (whose profile data is auto-enriched) and custom entries for external performers or speakers. Sending an empty array clears the lineup.
Endpoint: PATCH /api/v1/e-events/drafts/{draftId}/lineup
Access Level: 🔒 Protected (Organizer — own drafts 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 |
|---|---|---|---|---|
draftId |
UUID | Yes | The draft to update | Must be a valid UUID |
Request JSON Sample:
{
"lineup": [
{
"entryType": "PLATFORM_USER",
"userId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"role": "HEADLINER",
"performanceDay": 1,
"performanceTime": "21:00",
"order": 1
},
{
"entryType": "CUSTOM",
"name": "DJ Afrobeat",
"role": "DJ",
"title": "International Guest DJ",
"bio": "Award-winning DJ from Lagos with 10 years of experience.",
"image": "https://cdn.example.com/artists/dj-afrobeat.jpg",
"performanceDay": 2,
"performanceTime": "22:00",
"order": 2
}
]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
lineup |
array | Yes | Full lineup list | Empty array is valid |
lineup[].entryType |
string | Yes | Entry source | Enum: PLATFORM_USER, CUSTOM |
lineup[].userId |
UUID | Conditional | Platform user ID | Required when entryType=PLATFORM_USER; must exist in the system |
lineup[].name |
string | Conditional | Display name | Required when entryType=CUSTOM; auto-set from user profile when PLATFORM_USER |
lineup[].role |
string | No | Lineup role | Enum: HEADLINER, PERFORMER, SPEAKER, DJ, HOST, PANELIST, MODERATOR, GUEST |
lineup[].title |
string | No | Professional title | — |
lineup[].bio |
string | No | Biography text | Auto-set from user profile when PLATFORM_USER |
lineup[].image |
string | No | Headshot URL | Auto-set from user profile when PLATFORM_USER |
lineup[].performanceDay |
integer | No | Day number of performance | — |
lineup[].performanceTime |
string | No | Performance start time (HH:mm) |
— |
lineup[].order |
integer | No | Display order | — |
Auto-enrichment: When
entryType=PLATFORM_USER, the system automatically fetches and populatesname,bio, andimagefrom the user's profile. These are also refreshed on everyGETof the event.
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Lineup updated",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. data.lineup will contain enriched entries.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the draft owner |
404 |
Draft not found, or a userId in lineup does not exist |
12. Update Draft — Agenda
Purpose: Replaces the full event agenda organized by day and session. Supports both platform users and custom entries as session presenters. Sending an empty array clears the agenda.
Endpoint: PATCH /api/v1/e-events/drafts/{draftId}/agenda
Access Level: 🔒 Protected (Organizer — own drafts 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 |
|---|---|---|---|---|
draftId |
UUID | Yes | The draft to update | Must be a valid UUID |
Request JSON Sample:
{
"agenda": [
{
"dayNumber": 1,
"date": "2025-07-18",
"sessions": [
{
"startTime": "18:00",
"endTime": "19:00",
"title": "Welcome & Opening Ceremony",
"type": "CEREMONY",
"location": "Main Stage",
"presenterType": "PLATFORM_USER",
"presenterId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
},
{
"startTime": "20:00",
"endTime": "22:00",
"title": "Headliner Performance",
"type": "PERFORMANCE",
"presenterType": "CUSTOM",
"presenterName": "The Sauti Sol Band",
"presenterTitle": "Headline Act",
"presenterBio": "East Africa's most celebrated group."
}
]
}
]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
agenda |
array | Yes | Full agenda list (by day) | Empty array is valid |
agenda[].dayNumber |
integer | Yes | Day number (1-based) | — |
agenda[].date |
string | No | Date string (YYYY-MM-DD) |
— |
agenda[].sessions |
array | Yes | Sessions for this day | — |
sessions[].startTime |
string | Yes | Session start (HH:mm) |
— |
sessions[].endTime |
string | Yes | Session end (HH:mm) |
— |
sessions[].title |
string | Yes | Session name | — |
sessions[].description |
string | No | Session description | — |
sessions[].type |
string | No | Session type | Enum: GENERAL, PERFORMANCE, CEREMONY, PANEL, WORKSHOP, NETWORKING, MEAL, BREAK |
sessions[].location |
string | No | Sub-location within venue | — |
sessions[].presenterType |
string | No | Presenter source | Enum: PLATFORM_USER, CUSTOM |
sessions[].presenterId |
UUID | Conditional | Platform user ID | Required when presenterType=PLATFORM_USER |
sessions[].presenterName |
string | Conditional | Presenter name | Required when presenterType=CUSTOM; auto-set when PLATFORM_USER |
sessions[].presenterTitle |
string | No | Professional title | Auto-enriched for platform users |
sessions[].presenterBio |
string | No | Biography | Auto-enriched for platform users |
sessions[].presenterImage |
string | No | Image URL | Auto-enriched for platform users |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Agenda updated",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. data.agenda will contain enriched sessions.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the draft owner |
404 |
Draft not found, or a presenterId does not exist |
13. Attach Product to Event
Purpose: Links an existing active product from the PRODUCT domain to an event. Allowed on both DRAFT and PUBLISHED events. Marks the LINKS stage as completed.
Endpoint: POST /api/v1/e-events/draft/{eventId}/products/{productId}
Access Level: 🔒 Protected (Organizer — own events 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 (DRAFT or PUBLISHED) | Must be a valid UUID |
productId |
UUID | Yes | The product to attach | Must exist and have status ACTIVE |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Product attached",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. data.linkedProducts will include the newly attached product.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event or product not found |
400 |
Product is not active, already attached, or event is not in DRAFT/PUBLISHED status |
14. Remove Product from Event
Purpose: Detaches a previously linked product from the event. Allowed on both DRAFT and PUBLISHED events.
Endpoint: DELETE /api/v1/e-events/draft/{eventId}/products/{productId}
Access Level: 🔒 Protected (Organizer — own events 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 (DRAFT or PUBLISHED) | Must be a valid UUID |
productId |
UUID | Yes | The product to remove | Must be a valid UUID |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Product removed",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found, or product not currently attached |
15. Attach Shop to Event
Purpose: Links an existing active shop from the PRODUCT domain to an event. Allowed on both DRAFT and PUBLISHED events. Marks the LINKS stage as completed.
Endpoint: POST /api/v1/e-events/draft/{eventId}/shops/{shopId}
Access Level: 🔒 Protected (Organizer — own events 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 (DRAFT or PUBLISHED) | Must be a valid UUID |
shopId |
UUID | Yes | The shop to attach | Must exist and have status ACTIVE |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Shop attached",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. data.linkedShops will include the newly attached shop.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event or shop not found |
400 |
Shop is not active, already attached, or event is not in DRAFT/PUBLISHED status |
16. Remove Shop from Event
Purpose: Detaches a previously linked shop from the event. Allowed on both DRAFT and PUBLISHED events.
Endpoint: DELETE /api/v1/e-events/draft/{eventId}/shops/{shopId}
Access Level: 🔒 Protected (Organizer — own events 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 (DRAFT or PUBLISHED) | Must be a valid UUID |
shopId |
UUID | Yes | The shop to remove | Must be a valid UUID |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Shop removed",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse.
Possible Error Responses: Same as Endpoint 14 (401, 403, 404).
17. Publish Event
Purpose: Transitions a draft event to PUBLISHED status. This is the final step of the creation workflow. The system validates all required stages are complete, performs a duplicate event check against existing published events, and generates an RSA key pair used for secure ticket QR code signing.
Endpoint: PATCH /api/v1/e-events/{eventId}/publish
Access Level: 🔒 Protected (Organizer — own events 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 to publish | Must be a valid UUID |
Pre-publish checklist (all must pass or the request is rejected with
422):
BASIC_INFOstage completed ✓SCHEDULEstage completed (at least one day, not in the past) ✓LOCATION_DETAILSstage completed ✓REGISTRATION_SETUPSstage completed ✓- At least one active ticket exists for the event ✓
registrationOpensAtis beforeregistrationClosesAt✓registrationClosesAtis not after event end ✓- Event start date is not in the past ✓
- No duplicate event detected with ≥85% similarity score to another organizer's public event ✓
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Event published successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. data.status will be "PUBLISHED".
Error Response Sample (duplicate detected):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "This event appears to be a duplicate of 'Dar es Salaam Jazz Night' by user123. Please make the title, date, or location more distinct.",
"action_time": "2025-02-17T10:30:45",
"data": "This event appears to be a duplicate..."
}
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found |
400 |
Event is already published, or duplicate event detected |
422 |
One or more required stages are incomplete, or date/registration validation fails |
500 |
RSA key generation failed |
18. Unpublish Event
Purpose: Reverts a PUBLISHED event back to DRAFT status. Only allowed if zero tickets have been sold across all ticket types. If tickets have been sold, the organizer must cancel the event instead.
Endpoint: PATCH /api/v1/e-events/{eventId}/unpublish
Access Level: 🔒 Protected (Organizer — own events 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 to unpublish | Must be a valid UUID |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Event unpublished successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. data.status will be "DRAFT".
Error Response Sample (tickets sold):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Cannot unpublish: tickets have already been sold. Please cancel the event instead.",
"action_time": "2025-02-17T10:30:45",
"data": "Cannot unpublish: tickets have already been sold. Please cancel the event instead."
}
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found |
400 |
Event is not currently PUBLISHED, or tickets have been sold |
19. Cancel Event
Purpose: Cancels an event. Can be triggered from any non-terminal status (DRAFT, PUBLISHED, HAPPENING). This action is irreversible. If the event was published and tickets were sold, a bulk refund process is triggered.
Endpoint: PATCH /api/v1/e-events/{eventId}/cancel
Access Level: 🔒 Protected (Organizer — own events 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 to cancel | Must be a valid UUID |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Event cancelled successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. data.status will be "CANCELLED".
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found |
400 |
Event is already CANCELLED or COMPLETED |
20. Update Published Event — Basic Info
Purpose: Updates the description and/or media of a published event. Title, category, and format changes are blocked on published events as the slug has already been shared publicly.
Endpoint: PATCH /api/v1/e-events/{eventId}/published/basic-info
Access Level: 🔒 Protected (Organizer — own events 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 published event to update | Must be a valid UUID |
Request JSON Sample:
{
"description": "Updated: The biggest jazz event in East Africa. Now featuring 35 artists across 3 stages.",
"media": {
"banner": "https://cdn.example.com/banners/jazz-2025-v3.jpg",
"thumbnail": "https://cdn.example.com/thumbs/jazz-2025-v3.jpg",
"gallery": ["https://cdn.example.com/gallery/img1.jpg", "https://cdn.example.com/gallery/img2.jpg"]
}
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
description |
string | No | Updated event description | Min: 15, Max: 5000 characters |
media |
object | No | Updated media URLs | See media fields in Endpoint 1 |
media.banner |
string | No | Banner image URL | Max: 500 characters |
media.thumbnail |
string | No | Thumbnail image URL | Max: 500 characters |
media.gallery |
array | No | Gallery image URLs | — |
Blocked fields:
title,categoryId,eventFormat, andeventVisibilitycannot be changed on a published event. Providing them will have no effect.
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Published event updated successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found |
400 |
Event is not in PUBLISHED status |
422 |
Validation failure on provided fields |
21. Update Published Event — Registration Window
Purpose: Extends the registration close date for a published event. The new registrationClosesAt must be later than the current value — shortening the registration window is not allowed.
Endpoint: PATCH /api/v1/e-events/{eventId}/published/registration
Access Level: 🔒 Protected (Organizer — own events 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 published event to update | Must be a valid UUID |
Request JSON Sample:
{
"registrationClosesAt": "2025-07-20T23:59:00+03:00"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
registrationClosesAt |
ZonedDateTime | Yes | New registration close date | Must be after the current registrationClosesAt; must not be after event end |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Registration window extended successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found |
400 |
Event is not PUBLISHED |
422 |
New close date is not after current close date, or is after event end |
22. Update Published Event — Highlights
Purpose: Replaces the full list of highlights for a published event. Behavior is identical to the draft equivalent — sending an empty array clears all highlights.
Endpoint: PATCH /api/v1/e-events/{eventId}/published/highlights
Access Level: 🔒 Protected (Organizer — own events 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 published event to update | Must be a valid UUID |
Request Body: Same format as Endpoint 9 — Update Draft Highlights.
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Highlights updated successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found |
400 |
Event is not PUBLISHED |
23. Update Published Event — FAQs
Purpose: Replaces the full list of FAQs for a published event. Sending an empty array clears all FAQs.
Endpoint: PATCH /api/v1/e-events/{eventId}/published/faqs
Access Level: 🔒 Protected (Organizer — own events 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 published event to update | Must be a valid UUID |
Request Body: Same format as Endpoint 10 — Update Draft FAQs.
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "FAQs updated successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Possible Error Responses: Same as Endpoint 22 (401, 403, 404, 400).
24. Update Published Event — Lineup
Purpose: Replaces the full event lineup on a published event. Supports the same platform user enrichment as the draft equivalent.
Endpoint: PATCH /api/v1/e-events/{eventId}/published/lineup
Access Level: 🔒 Protected (Organizer — own events 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 published event to update | Must be a valid UUID |
Request Body: Same format as Endpoint 11 — Update Draft Lineup.
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Lineup updated successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found, or a userId in lineup does not exist |
400 |
Event is not PUBLISHED |
25. Update Published Event — Agenda
Purpose: Replaces the full event agenda on a published event. Supports the same platform user enrichment as the draft equivalent.
Endpoint: PATCH /api/v1/e-events/{eventId}/published/agenda
Access Level: 🔒 Protected (Organizer — own events 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 published event to update | Must be a valid UUID |
Request Body: Same format as Endpoint 12 — Update Draft Agenda.
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Agenda updated successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found, or a presenterId does not exist |
400 |
Event is not PUBLISHED |
26. Reveal Location (TBA → Actual)
Purpose: Reveals the actual location for an event that was originally published with eventFormat=TBA. The new format cannot be TBA. Required fields depend on the new format chosen.
Endpoint: PATCH /api/v1/e-events/{eventId}/published/reveal-location
Access Level: 🔒 Protected (Organizer — own events 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 published TBA event | Must be a valid UUID |
Request JSON Sample (revealing as IN_PERSON):
{
"eventFormat": "IN_PERSON",
"venue": {
"name": "Mlimani City Arena",
"address": "Sam Nujoma Road, Dar es Salaam",
"coordinates": {
"latitude": -6.7724,
"longitude": 39.2083
}
}
}
Request JSON Sample (revealing as ONLINE):
{
"eventFormat": "ONLINE",
"virtualDetails": {
"meetingLink": "https://zoom.us/j/123456789",
"meetingId": "123 456 789",
"passcode": "jazz2025"
}
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
eventFormat |
string | Yes | The actual event format | Enum: IN_PERSON, ONLINE, HYBRID — cannot be TBA |
venue |
object | Conditional | Physical venue details | Required when new format is IN_PERSON or HYBRID |
venue.name |
string | Yes (if venue) | Venue name | Max: 200 characters |
venue.address |
string | No | Full address | Max: 500 characters |
venue.coordinates.latitude |
BigDecimal | No | GPS latitude | — |
venue.coordinates.longitude |
BigDecimal | No | GPS longitude | — |
virtualDetails |
object | Conditional | Virtual meeting details | Required when new format is ONLINE or HYBRID |
virtualDetails.meetingLink |
string | Yes (if virtualDetails) | Meeting URL | Max: 500 characters |
virtualDetails.meetingId |
string | No | Platform meeting ID | Max: 100 characters |
virtualDetails.passcode |
string | No | Meeting passcode | Max: 100 characters |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Location revealed successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found |
400 |
Event is not PUBLISHED, current format is not TBA, or new format is TBA |
422 |
Missing required venue/virtual fields for the chosen format |
27. Get Event by ID
Purpose: Retrieves full event details. Published events are publicly accessible without authentication. Draft events can only be viewed by their organizer.
Endpoint: GET /api/v1/e-events/{eventId}
Access Level: 🌐 Public (for PUBLISHED events) | 🔒 Protected (for DRAFT events — organizer only)
Authentication: Bearer Token (required only for DRAFT access)
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Conditional | Required when accessing a DRAFT event |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
eventId |
UUID | Yes | The event to retrieve | Must be a valid UUID |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Event retrieved successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse.
Possible Error Responses:
| Status | Scenario |
|---|---|
403 |
Attempting to access a DRAFT event without being its organizer |
404 |
Event not found or is soft-deleted |
28. Get My Events
Purpose: Returns a paginated list of all events (any status) created by the authenticated organizer.
Endpoint: GET /api/v1/e-events/my-events
Access Level: 🔒 Protected (Organizer — own events only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
page |
integer | No | Page number (1-based) | Min: 1 | 1 |
size |
integer | No | Items per page | Min: 1 | 10 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Events retrieved successfully",
"action_time": "2025-02-17T10:30:45",
"data": {
"content": [ "[ EventSummaryResponse objects ]" ],
"totalElements": 12,
"totalPages": 2
}
}
Success Response Fields: data.content[] contains EventSummaryResponse objects. Pagination follows Standard Paginated Response Wrapper.
Possible Error Responses: 401 — see Shared Error: 401.
29. Get My Events by Status
Purpose: Returns a paginated list of the authenticated organizer's events filtered by a specific status.
Endpoint: GET /api/v1/e-events/my-events/status/{status}
Access Level: 🔒 Protected (Organizer — own events only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
status |
string | Yes | Event status filter | Enum: DRAFT, PUBLISHED, HAPPENING, CANCELLED, COMPLETED |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
page |
integer | No | Page number (1-based) | Min: 1 | 1 |
size |
integer | No | Items per page | Min: 1 | 10 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Events retrieved successfully",
"action_time": "2025-02-17T10:30:45",
"data": {
"content": [ "[ EventSummaryResponse objects ]" ],
"totalElements": 5,
"totalPages": 1
}
}
Success Response Fields: Same as Endpoint 28.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
400 |
Invalid status enum value |
30. Get Events Feed
Purpose: Returns a paginated list of all published events for the public discovery feed. Results are ordered by creation date descending. This endpoint will eventually incorporate a recommendation algorithm.
Endpoint: GET /api/v1/e-events/events-feed
Access Level: 🌐 Public
Authentication: None required
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
page |
integer | No | Page number (1-based) | Min: 1 | 1 |
size |
integer | No | Items per page | Min: 1 | 10 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Events feed retrieved successfully",
"action_time": "2025-02-17T10:30:45",
"data": {
"content": [ "[ EventSummaryResponse objects — PUBLISHED only ]" ],
"totalElements": 87,
"totalPages": 9
}
}
Success Response Fields: data.content[] contains EventSummaryResponse objects (PUBLISHED status only).
Possible Error Responses: None expected (no authentication required).
31. Search Events
Purpose: Full-text search across published event titles. Results are ordered by start date ascending.
Endpoint: GET /api/v1/e-events/search
Access Level: 🌐 Public
Authentication: None required
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
query |
string | Yes | Search keyword(s) | Non-empty string | — |
page |
integer | No | Page number (1-based) | Min: 1 | 1 |
size |
integer | No | Items per page | Min: 1 | 10 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Search results retrieved successfully",
"action_time": "2025-02-17T10:30:45",
"data": {
"content": [ "[ EventSummaryResponse objects ]" ],
"totalElements": 4,
"totalPages": 1
}
}
Success Response Fields: data.content[] contains EventSummaryResponse objects.
Possible Error Responses: None expected for normal queries (empty results return an empty content array, not a 404).
32. Filter Events by Date Range
Purpose: Returns published events whose schedule overlaps with the provided date range. An event is included if it starts before endDate AND ends after startDate (overlap logic, not exact range match).
Endpoint: GET /api/v1/e-events/filter/date
Access Level: 🌐 Public
Authentication: None required
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
startDate |
ZonedDateTime | Yes | Range start | ISO 8601 with offset (e.g., 2025-07-01T00:00:00+03:00) |
— |
endDate |
ZonedDateTime | Yes | Range end | ISO 8601 with offset; must be after startDate |
— |
page |
integer | No | Page number (1-based) | Min: 1 | 1 |
size |
integer | No | Items per page | Min: 1 | 10 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Filtered events retrieved successfully",
"action_time": "2025-02-17T10:30:45",
"data": {
"content": [ "[ EventSummaryResponse objects ]" ],
"totalElements": 7,
"totalPages": 1
}
}
Possible Error Responses:
| Status | Scenario |
|---|---|
400 |
startDate is after endDate |
33. Search and Filter Events (Combined)
Purpose: Combines keyword search with optional date range filtering in a single call. All parameters are optional — calling with no parameters is equivalent to getting the full published feed.
Endpoint: GET /api/v1/e-events/filter
Access Level: 🌐 Public
Authentication: None required
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
query |
string | No | Title keyword search | — | — |
startDate |
ZonedDateTime | No | Date range start | ISO 8601 with offset | — |
endDate |
ZonedDateTime | No | Date range end | ISO 8601 with offset; must be after startDate if both provided |
— |
page |
integer | No | Page number (1-based) | Min: 1 | 1 |
size |
integer | No | Items per page | Min: 1 | 10 |
Possible Error Responses:
| Status | Scenario |
|---|---|
400 |
startDate is after endDate (when both are provided) |
34. Search My Events (Organizer)
Purpose: Allows an organizer to search and filter within their own events across all statuses. Supports keyword, status filter, and date range simultaneously.
Endpoint: GET /api/v1/e-events/my-events/search
Access Level: 🔒 Protected (Organizer — own events only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer <token> |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
query |
string | No | Keyword search on title | — | — |
status |
string | No | Filter by status | Enum: DRAFT, PUBLISHED, HAPPENING, CANCELLED, COMPLETED |
— |
startDate |
ZonedDateTime | No | Date range start | ISO 8601 with offset | — |
endDate |
ZonedDateTime | No | Date range end | ISO 8601 with offset; must be after startDate if both provided |
— |
page |
integer | No | Page number (1-based) | Min: 1 | 1 |
size |
integer | No | Items per page | Min: 1 | 10 |
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
400 |
startDate is after endDate |
Quick Reference — Endpoint Summary
| # | Method | Path | Auth | Description |
|---|---|---|---|---|
| 1 | POST | /e-events/drafts |
🔒 | Create event draft |
| 2 | GET | /e-events/drafts |
🔒 | List my drafts (paginated) |
| 3 | GET | /e-events/drafts/{draftId} |
🔒 | Get draft by ID |
| 4 | DELETE | /e-events/drafts/{draftId} |
🔒 | Discard a draft |
| 5 | PATCH | /e-events/drafts/{draftId}/basic-info |
🔒 | Update basic info |
| 6 | PATCH | /e-events/drafts/{draftId}/schedule |
🔒 | Update schedule / days |
| 7 | PATCH | /e-events/drafts/{draftId}/location |
🔒 | Update venue / virtual details |
| 8 | PATCH | /e-events/drafts/{draftId}/registration |
🔒 | Update registration window |
| 9 | PATCH | /e-events/drafts/{draftId}/highlights |
🔒 | Update highlights |
| 10 | PATCH | /e-events/drafts/{draftId}/faqs |
🔒 | Update FAQs |
| 11 | PATCH | /e-events/drafts/{draftId}/lineup |
🔒 | Update lineup |
| 12 | PATCH | /e-events/drafts/{draftId}/agenda |
🔒 | Update agenda |
| 13 | POST | /e-events/draft/{eventId}/products/{productId} |
🔒 | Attach product (DRAFT or PUBLISHED) |
| 14 | DELETE | /e-events/draft/{eventId}/products/{productId} |
🔒 | Remove product (DRAFT or PUBLISHED) |
| 15 | POST | /e-events/draft/{eventId}/shops/{shopId} |
🔒 | Attach shop (DRAFT or PUBLISHED) |
| 16 | DELETE | /e-events/draft/{eventId}/shops/{shopId} |
🔒 | Remove shop (DRAFT or PUBLISHED) |
| 17 | PATCH | /e-events/{eventId}/publish |
🔒 | Publish event |
| 18 | PATCH | /e-events/{eventId}/unpublish |
🔒 | Unpublish event (0 sales only) |
| 19 | PATCH | /e-events/{eventId}/cancel |
🔒 | Cancel event (terminal) |
| 20 | PATCH | /e-events/{eventId}/published/basic-info |
🔒 | Update description/media on published event |
| 21 | PATCH | /e-events/{eventId}/published/registration |
🔒 | Extend registration close date |
| 22 | PATCH | /e-events/{eventId}/published/highlights |
🔒 | Update highlights on published event |
| 23 | PATCH | /e-events/{eventId}/published/faqs |
🔒 | Update FAQs on published event |
| 24 | PATCH | /e-events/{eventId}/published/lineup |
🔒 | Update lineup on published event |
| 25 | PATCH | /e-events/{eventId}/published/agenda |
🔒 | Update agenda on published event |
| 26 | PATCH | /e-events/{eventId}/published/reveal-location |
🔒 | Reveal TBA location |
| 27 | GET | /e-events/{eventId} |
🌐/🔒 | Get event by ID |
| 28 | GET | /e-events/my-events |
🔒 | List my events |
| 29 | GET | /e-events/my-events/status/{status} |
🔒 | My events by status |
| 30 | GET | /e-events/events-feed |
🌐 | Public events feed |
| 31 | GET | /e-events/search |
🌐 | Search events by keyword |
| 32 | GET | /e-events/filter/date |
🌐 | Filter by date range |
| 33 | GET | /e-events/filter |
🌐 | Combined search + filter |
| 34 | GET | /e-events/my-events/search |
🔒 | Search within my events |
Note on path inconsistency: Endpoints 5–12 use the path prefix
/drafts/{id}/...(plural), while endpoints 6, 7, 13–16 use/draft/{id}/...(singular). This reflects the actual controller routing in the current codebase — use the exact paths shown in the table above.
Data Format Standards
| Concern | Standard |
|---|---|
| Timestamps | ISO 8601 with timezone offset: 2025-07-18T18:00:00+03:00 |
| Dates | YYYY-MM-DD format: 2025-07-18 |
| Times | HH:mm:ss (24-hour): 18:00:00 |
| IDs | UUID v4: 3fa85f64-5717-4562-b3fc-2c963f66afa6 |
| Pagination | 1-based page query parameter, Spring Page wrapper in response |
| Prices | BigDecimal with 2 decimal places; 0.00 for free |
| Enums | Uppercase strings as defined (e.g., IN_PERSON, PUBLISHED) |
Ticket Management API
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_PERSONticket and oneONLINEticket before the event can be published. - Ticket names must be unique per event per attendance mode — you cannot have two
IN_PERSONtickets both named "VIP Pass" on the same event. - DONATION tickets are restricted to
ONLINE_ONLYsales 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
salesStartDateTimeandsalesEndDateTimeis 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:
salesStartDateTimecannot be in the pastsalesEndDateTimecannot be in the pastsalesEndDateTimemust be aftersalesStartDateTime- Minimum gap between sales start and end is 30 minutes
salesStartDateTimemust be on or afterregistrationOpensAtsalesStartDateTimecannot be afterregistrationClosesAtsalesEndDateTimecannot be afterregistrationClosesAt- Neither date can be after
eventEndDateTime
Standard Response Format
All API responses follow a consistent structure:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-09-23T10:30:45",
"data": {}
}
Error Response Structure
{
"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_PERSONevents, onlyIN_PERSONtickets are allowed. ForONLINEevents, onlyONLINEtickets are allowed. ForHYBRIDevents, 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):
{
"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):
{
"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:
{
"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
DONATIONtickets, thepricefield isnullin 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):
{
"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):
{
"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:
{
"name": "VIP Weekend Pass",
"price": 175.00,
"maxQuantityPerOrder": 2,
"inclusiveItems": [
"Backstage access",
"Complimentary gift bag",
"Priority seating",
"Artist meet & greet"
]
}
Success Response JSON Sample:
{
"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.
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:
{
"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:
{
"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.
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:
{
"newTotalQuantity": 300
}
Success Response JSON Sample:
{
"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_OUTand the new capacity exceeds tickets sold, the status is automatically reset toACTIVE.
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):
{
"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:
{
"status": "INACTIVE"
}
Success Response JSON Sample:
{
"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:
{
"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):
{
"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
salesStartDateTimeorsalesEndDateTimemust be provided.
Request JSON Sample:
{
"salesEndDateTime": "2026-04-25T23:59:00+03:00"
}
Success Response JSON Sample:
{
"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):
{
"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:
{
"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:
{
"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):
{
"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 |
Event Checkout & Payment API
Base URL: https://api.nextgate.co.tz/api/v1
Short Description: The NextGate Checkout API manages the complete ticket purchasing lifecycle on the NextGate event platform. It supports two distinct checkout flows: online checkout for registered attendees and at-door ticket sales for event organizers and scanner devices. This API should be used by frontend clients, mobile applications, and authorized scanner hardware integrations.
Hints:
- All monetary values are in TZS (Tanzanian Shilling) unless otherwise stated
- Checkout sessions expire after 15 minutes for online checkout and 1 hour for at-door sales — always check
expiresAtbefore processing payment - Bearer token authentication is required for all endpoints
- For FREE tickets, payment is auto-processed immediately upon session creation — no separate payment step is needed
- For DONATION tickets, maximum 1 ticket per order and cannot be purchased for other attendees
- Ticket holds are applied immediately on session creation; cancelling the session releases the hold
- Scanner devices must have the
SELL_TICKETSpermission assigned and a validdeviceFingerprintregistered before calling the scanner sale endpoint
Standard Response Format
All API responses follow a consistent structure using our Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-09-23T10:30:45",
"data": {}
}
Error Response Structure
{
"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 | Always true for successful operations, false for errors |
httpStatus |
string | HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) |
message |
string | Human-readable message describing the operation result |
action_time |
string | ISO 8601 timestamp of when the response was generated |
data |
object/string | Response payload for success, error details for failures |
HTTP Method Badge Standards
- GET — Green: Safe, read-only operations
- POST — Blue: Create new resources
- PUT — Yellow: Update/replace entire resource
- PATCH — Orange: Partial updates
- DELETE — Red: Remove resources
User Journey Flows
Flow A — Online Checkout (Registered Attendee)
[ Attendee browses event & selects ticket ]
|
v
[ POST /checkout — Create checkout session ]
|
............|............
. .
v v
[ FREE / DONATION ticket ] [ PAID ticket ]
| |
v |
[ Auto-processed immediately ] |
| v
| [ GET /checkout/{sessionId} ]
| [ — Review session details ]
| |
| v
| [ POST /checkout/{sessionId}/payment ]
| [ — Deduct from wallet, create escrow ]
| |
`----------->-----------'
|
v
[ Booking order created asynchronously ]
[ QR codes generated & sent to attendees ]
|
v
[ Session status = COMPLETED ]
At any point before payment:
[ POST /checkout/{sessionId}/cancel ]
[ — Releases ticket hold ]
Flow B — At-Door Sale via Scanner Device
[ Customer arrives at event venue ]
|
v
[ Scanner device sends sale request ]
[ POST /checkout/sell-at-door-ticket/scanner ]
|
............|............
. .
v v
[ Validate scanner ID ] [ Validate device fingerprint ]
[ & permissions ] [ & SELL_TICKETS permission ]
. .
`..........v............'
|
v
[ Validate ticket type belongs to event ]
[ Check sales channel = AT_DOOR or BOTH ]
|
v
[ Cash payment processed (no wallet deduction) ]
[ Booking order created immediately ]
|
v
[ QR codes returned in response ]
[ immediateCheckIn = true → ticket marked as checked-in ]
Flow C — At-Door Sale via Organizer
[ Organizer is authenticated & accesses event ]
|
v
[ POST /checkout/{eventId}/organizer ]
[ — Organizer sells ticket at their counter ]
|
v
[ System verifies organizer owns the event ]
|
v
[ Validate ticket type & attendee count ]
|
v
[ Cash payment recorded (NEUTRAL transaction) ]
[ Booking order created ]
|
v
[ QR codes returned in response ]
[ immediateCheckIn flag respected ]
Endpoints
1. Create Checkout Session
Purpose: Creates a new online checkout session for a registered attendee purchasing event tickets, holding the requested quantity and initializing the payment intent.
Endpoint: POST /api/v1/e-events/checkout
Access Level: 🔒 Protected (Requires valid Bearer token — authenticated attendee)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer token of the authenticated attendee |
Content-Type |
string | Yes | Must be application/json |
Request JSON Sample:
{
"eventId": "b3f1a2c4-1234-5678-abcd-000000000001",
"ticketTypeId": "d4e5f6a7-1234-5678-abcd-000000000002",
"ticketsForMe": 2,
"donationAmount": null,
"otherAttendees": [
{
"name": "Jane Doe",
"email": "jane.doe@example.com",
"phone": "+255712345678",
"quantity": 1
}
],
"sendTicketsToAttendees": true,
"paymentMethodId": null
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
eventId |
UUID | Yes | The ID of the event being booked | Must be a valid published event that has not yet started |
ticketTypeId |
UUID | Yes | The ID of the ticket type being purchased | Must belong to the specified event and be active and on sale |
ticketsForMe |
integer | Yes | Number of tickets for the buyer themselves. Use 0 if the buyer is not attending |
Min: 0 |
donationAmount |
decimal | No | Donation amount in TZS. Only applicable for DONATION type tickets |
Only used when ticket pricing type is DONATION |
otherAttendees |
array | No | List of other attendees to purchase tickets for | Each attendee must have valid name, email, and Tanzanian phone number |
otherAttendees[].name |
string | Yes (if array provided) | Full name of the attendee | Min: 2, Max: 100 characters |
otherAttendees[].email |
string | Yes (if array provided) | Email address of the attendee | Valid email format; no duplicate emails in the array |
otherAttendees[].phone |
string | Yes (if array provided) | Phone number of the attendee | Must match Tanzania format: +255[67]XXXXXXXX |
otherAttendees[].quantity |
integer | Yes (if array provided) | Number of tickets for this attendee | Min: 1 |
sendTicketsToAttendees |
boolean | No | If true, QR tickets are sent to each attendee's email. If false, all QR codes are sent to the buyer only |
Default: true |
paymentMethodId |
UUID | No | ID of a saved payment method. Defaults to wallet if not provided | Optional |
Business Rules:
- Total quantity =
ticketsForMe+ sum of allotherAttendees[].quantity— must be at least 1 DONATIONtickets: maximum 1 per order, cannot be bought for other attendees, online onlyAT_DOOR_ONLYtickets cannot be purchased through this endpoint- Wallet balance is validated upfront for
PAIDtickets - If the event has a required questionnaire set to
BEFORE_CHECKOUT, it must be submitted before calling this endpoint FREEtickets are auto-processed immediately — the response will already showPAYMENT_COMPLETED
Success Response JSON Sample:
{
"success": true,
"httpStatus": "CREATED",
"message": "Checkout session created successfully",
"action_time": "2025-09-23T10:30:45",
"data": {
"sessionId": "a1b2c3d4-0000-0000-0000-000000000001",
"status": "PENDING_PAYMENT",
"customerId": "f1e2d3c4-0000-0000-0000-000000000010",
"customerUserName": "john_doe",
"eventId": "b3f1a2c4-1234-5678-abcd-000000000001",
"eventTitle": "Kilimanjaro Jazz Night 2025",
"ticketDetails": {
"ticketTypeId": "d4e5f6a7-1234-5678-abcd-000000000002",
"ticketTypeName": "VIP",
"unitPrice": 50000.00,
"ticketsForBuyer": 2,
"otherAttendees": [
{
"name": "Jane Doe",
"email": "jane.doe@example.com",
"phone": "+255712345678",
"quantity": 1
}
],
"sendTicketsToAttendees": true,
"totalQuantity": 3,
"subtotal": 150000.00
},
"pricing": {
"subtotal": 150000.00,
"total": 150000.00
},
"paymentIntent": {
"provider": "WALLET",
"clientSecret": null,
"paymentMethods": ["WALLET"],
"status": "PENDING"
},
"ticketsHeld": true,
"ticketHoldExpiresAt": "2025-09-23T10:45:45",
"expiresAt": "2025-09-23T10:45:45",
"createdAt": "2025-09-23T10:30:45",
"updatedAt": "2025-09-23T10:30:45",
"completedAt": null,
"createdBookingOrderId": null,
"isExpired": false,
"canRetryPayment": false
}
}
Success Response Fields:
| Field | Description |
|---|---|
sessionId |
Unique identifier for this checkout session. Use it for all subsequent actions |
status |
Current session status. Values: PENDING_PAYMENT, PAYMENT_COMPLETED, COMPLETED, PAYMENT_FAILED, CANCELLED, EXPIRED |
customerId |
Account ID of the buyer |
customerUserName |
Username of the buyer |
eventId |
ID of the event being booked |
eventTitle |
Human-readable event title |
ticketDetails.ticketTypeId |
ID of the chosen ticket type |
ticketDetails.ticketTypeName |
Name of the ticket type (e.g., VIP, Regular) |
ticketDetails.unitPrice |
Price per single ticket in TZS |
ticketDetails.ticketsForBuyer |
Number of tickets allocated to the buyer |
ticketDetails.otherAttendees |
List of other attendees and their ticket quantities |
ticketDetails.sendTicketsToAttendees |
Whether QR codes will be emailed to each attendee |
ticketDetails.totalQuantity |
Total tickets across buyer and all attendees |
ticketDetails.subtotal |
Total price before any discounts (TZS) |
pricing.subtotal |
Subtotal amount in TZS |
pricing.total |
Final payable amount in TZS |
paymentIntent.provider |
Payment provider (e.g., WALLET) |
paymentIntent.paymentMethods |
Available payment methods for this session |
paymentIntent.status |
Payment intent status (PENDING, COMPLETED) |
ticketsHeld |
Whether the tickets are currently being held in reserve |
ticketHoldExpiresAt |
Timestamp when the ticket hold expires |
expiresAt |
Timestamp when the entire session expires |
createdBookingOrderId |
Populated after payment is completed — the resulting booking order ID |
isExpired |
Computed flag indicating whether the session has passed its expiry time |
canRetryPayment |
Whether the session allows another payment attempt (true if status is PAYMENT_FAILED and attempts < 5) |
Error Response JSON Sample:
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Please complete the event questionnaire before purchasing tickets",
"action_time": "2025-09-23T10:30:45",
"data": "Please complete the event questionnaire before purchasing tickets"
}
Possible Errors:
| HTTP Status | Scenario |
|---|---|
400 BAD_REQUEST |
Event is not published, event has already started, ticket not on sale, insufficient wallet balance, AT_DOOR_ONLY ticket purchased online, DONATION rules violated, questionnaire not submitted |
401 UNAUTHORIZED |
Missing, expired, or invalid Bearer token |
404 NOT_FOUND |
Event or ticket type not found |
422 UNPROCESSABLE_ENTITY |
Validation failed on request fields (missing eventId, invalid email format, invalid phone format, etc.) |
500 INTERNAL_SERVER_ERROR |
Unexpected server error |
2. Get Checkout Session
Purpose: Retrieves the current state of an existing checkout session belonging to the authenticated user.
Endpoint: GET /api/v1/e-events/checkout/{sessionId}
Access Level: 🔒 Protected (Authenticated attendee — only the session owner can retrieve it)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer token of the authenticated attendee |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
sessionId |
UUID | Yes | The ID of the checkout session to retrieve | Must be a valid UUID belonging to the authenticated user |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Checkout session retrieved successfully",
"action_time": "2025-09-23T10:35:00",
"data": {
"sessionId": "a1b2c3d4-0000-0000-0000-000000000001",
"status": "PENDING_PAYMENT",
"customerId": "f1e2d3c4-0000-0000-0000-000000000010",
"customerUserName": "john_doe",
"eventId": "b3f1a2c4-1234-5678-abcd-000000000001",
"eventTitle": "Kilimanjaro Jazz Night 2025",
"ticketDetails": {
"ticketTypeId": "d4e5f6a7-1234-5678-abcd-000000000002",
"ticketTypeName": "VIP",
"unitPrice": 50000.00,
"ticketsForBuyer": 2,
"otherAttendees": [
{
"name": "Jane Doe",
"email": "jane.doe@example.com",
"phone": "+255712345678",
"quantity": 1
}
],
"sendTicketsToAttendees": true,
"totalQuantity": 3,
"subtotal": 150000.00
},
"pricing": {
"subtotal": 150000.00,
"total": 150000.00
},
"paymentIntent": {
"provider": "WALLET",
"clientSecret": null,
"paymentMethods": ["WALLET"],
"status": "PENDING"
},
"paymentAttempts": [],
"ticketsHeld": true,
"ticketHoldExpiresAt": "2025-09-23T10:45:45",
"expiresAt": "2025-09-23T10:45:45",
"createdAt": "2025-09-23T10:30:45",
"updatedAt": "2025-09-23T10:30:45",
"completedAt": null,
"createdBookingOrderId": null,
"isExpired": false,
"canRetryPayment": false
}
}
Success Response Fields:
| Field | Description |
|---|---|
sessionId |
Unique session identifier |
status |
Current session status |
paymentAttempts |
List of all payment attempts made on this session, including failures |
paymentAttempts[].attemptNumber |
Sequential attempt number (1-indexed) |
paymentAttempts[].paymentMethod |
Payment method used for this attempt |
paymentAttempts[].status |
Result of the attempt: SUCCESS or FAILED |
paymentAttempts[].errorMessage |
Failure reason if the attempt failed |
paymentAttempts[].attemptedAt |
Timestamp of the attempt |
paymentAttempts[].transactionId |
External or internal transaction reference |
| All other fields | Same as Create Checkout Session response |
Possible Errors:
| HTTP Status | Scenario |
|---|---|
401 UNAUTHORIZED |
Missing, expired, or invalid Bearer token |
404 NOT_FOUND |
Session not found or does not belong to the authenticated user |
500 INTERNAL_SERVER_ERROR |
Unexpected server error |
3. Process Payment
Purpose: Initiates wallet payment for a pending checkout session, creating an escrow account and triggering asynchronous booking order creation upon success.
Endpoint: POST /api/v1/e-events/checkout/{sessionId}/payment
Access Level: 🔒 Protected (Session owner only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer token of the authenticated attendee |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
sessionId |
UUID | Yes | The ID of the checkout session to pay for | Must be a valid UUID; session must be in PENDING_PAYMENT status and not expired |
Business Rules:
- Session must be in
PENDING_PAYMENTstatus - Session must not be expired
- Cannot call this on FREE tickets (auto-processed on creation)
- Wallet must have sufficient balance at time of payment call
- Maximum 5 payment attempts per session; after that
canRetryPaymentbecomesfalse - On success, escrow is created, session moves to
PAYMENT_COMPLETED, and a booking order is created asynchronously
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Payment completed successfully. Your booking is being processed.",
"action_time": "2025-09-23T10:38:00",
"data": {
"success": true,
"status": "SUCCESS",
"message": "Payment completed successfully. Your booking is being processed.",
"checkoutSessionId": "a1b2c3d4-0000-0000-0000-000000000001",
"escrowId": "e9f8a7b6-0000-0000-0000-000000000020",
"escrowNumber": "ESC-2025-000001",
"orderId": null,
"orderNumber": null,
"paymentMethod": "WALLET",
"amountPaid": 150000.00,
"platformFee": 7500.00,
"sellerAmount": 142500.00,
"currency": "TZS"
}
}
Success Response Fields:
| Field | Description |
|---|---|
status |
Payment result status: SUCCESS, FAILED, or PENDING |
checkoutSessionId |
The checkout session this payment belongs to |
escrowId |
ID of the escrow account holding the funds |
escrowNumber |
Human-readable escrow reference number (format: ESC-YYYY-NNNNNN) |
orderId |
Booking order ID — may be null immediately after payment as booking is created asynchronously |
orderNumber |
Human-readable order reference — null until order is created |
paymentMethod |
Payment method used: WALLET |
amountPaid |
Total amount deducted from buyer's wallet in TZS |
platformFee |
Platform fee amount (5% of total) in TZS |
sellerAmount |
Amount that will be released to the event organizer in TZS |
currency |
Currency code, always TZS |
Possible Errors:
| HTTP Status | Scenario |
|---|---|
400 BAD_REQUEST |
Session is not in PENDING_PAYMENT status, session is expired, or attempting payment on a FREE ticket |
401 UNAUTHORIZED |
Missing, expired, or invalid Bearer token |
404 NOT_FOUND |
Session not found or does not belong to the authenticated user |
500 INTERNAL_SERVER_ERROR |
Payment processing error, insufficient balance at time of processing |
4. Cancel Checkout Session
Purpose: Cancels an active checkout session and releases any held tickets back to available inventory.
Endpoint: POST /api/v1/e-events/checkout/{sessionId}/cancel
Access Level: 🔒 Protected (Session owner only)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer token of the authenticated attendee |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
sessionId |
UUID | Yes | The ID of the checkout session to cancel | Must be a valid UUID belonging to the authenticated user |
Business Rules:
- Cannot cancel a session that is in
COMPLETEDstatus - Cannot cancel a session that is in
PAYMENT_COMPLETEDstatus (payment already processed) - Cancelling releases the ticket hold immediately
- Cancellation does not trigger a refund — refunds are handled separately through the escrow system
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Checkout session cancelled successfully",
"action_time": "2025-09-23T10:40:00",
"data": null
}
Possible Errors:
| HTTP Status | Scenario |
|---|---|
400 BAD_REQUEST |
Session is already completed or payment has been completed |
401 UNAUTHORIZED |
Missing, expired, or invalid Bearer token |
404 NOT_FOUND |
Session not found or does not belong to the authenticated user |
500 INTERNAL_SERVER_ERROR |
Unexpected server error |
5. Scanner — Sell Ticket at Door
Purpose: Allows an authorized scanner device to sell tickets at the venue entrance, processing a cash payment and optionally checking in the attendee immediately.
Endpoint: POST /api/v1/e-events/checkout/sell-at-door-ticket/scanner
Access Level: 🔒 Protected (Scanner device authentication via scannerId + deviceFingerprint)
Authentication: Bearer Token (of scanner's linked account) + Scanner credentials in body
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer token linked to the scanner's registered account |
Content-Type |
string | Yes | Must be application/json |
Request JSON Sample:
{
"scannerId": "SCN-2025-001",
"deviceFingerprint": "a3f1b2c4d5e6f7890abc1234def56789",
"ticketTypeId": "d4e5f6a7-1234-5678-abcd-000000000002",
"quantity": 2,
"attendees": [
{
"fullName": "John Mbeki",
"email": "john.mbeki@example.com",
"phoneNumber": "+255789123456"
},
{
"fullName": "Amina Hassan",
"email": "amina.hassan@example.com",
"phoneNumber": "+255754321987"
}
],
"immediateCheckIn": true
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
scannerId |
string | Yes | Unique identifier of the registered scanner device | Must match a registered, active scanner with SELL_TICKETS permission |
deviceFingerprint |
string | Yes | Hardware fingerprint of the scanner device | Must match the fingerprint registered for this scanner |
ticketTypeId |
UUID | Yes | ID of the ticket type to sell | Must belong to the event this scanner is assigned to; must not be ONLINE_ONLY; must be on sale |
quantity |
integer | Yes | Total number of tickets to sell | Min: 1; must equal the number of attendees in the attendees array |
attendees |
array | Yes | List of attendee details, one entry per ticket | Min: 1 entry; count must match quantity |
attendees[].fullName |
string | No | Full name of the attendee | Optional — if blank, a generated name like ATTENDEE-XXXX is assigned |
attendees[].email |
string | No | Email address of the attendee | Valid email format if provided |
attendees[].phoneNumber |
string | No | Phone number of the attendee | Optional |
immediateCheckIn |
boolean | Yes | If true, the ticket is marked as checked-in immediately upon sale |
Required |
Business Rules:
- The scanner must be active, not expired, and have the
SELL_TICKETSpermission - The
deviceFingerprintmust exactly match the registered fingerprint for this scanner - The number of attendees must equal
quantity— a 1-to-1 mapping is enforced - Payment method is always
CASH— no wallet deduction occurs - The ticket type must not be
ONLINE_ONLY immediateCheckIn = trueautomatically marks each generated ticket as checked-in
Success Response JSON Sample:
{
"success": true,
"httpStatus": "CREATED",
"message": "Tickets sold successfully at door",
"action_time": "2025-09-23T18:00:00",
"data": {
"bookingId": "c9d8e7f6-0000-0000-0000-000000000030",
"bookingReference": "BK-2025-000042",
"eventId": "b3f1a2c4-1234-5678-abcd-000000000001",
"eventName": "Kilimanjaro Jazz Night 2025",
"tickets": [
{
"ticketInstanceId": "aa11bb22-0000-0000-0000-000000000050",
"ticketSeries": "VIP-0042-A",
"ticketTypeName": "VIP",
"attendeeName": "John Mbeki",
"attendeeEmail": "john.mbeki@example.com",
"checkedIn": true,
"checkInTime": "2025-09-23T18:00:05Z",
"qrCode": "eyJhbGciOiJIUzI1NiJ9..."
},
{
"ticketInstanceId": "cc33dd44-0000-0000-0000-000000000051",
"ticketSeries": "VIP-0042-B",
"ticketTypeName": "VIP",
"attendeeName": "Amina Hassan",
"attendeeEmail": "amina.hassan@example.com",
"checkedIn": true,
"checkInTime": "2025-09-23T18:00:05Z",
"qrCode": "eyJhbGciOiJIUzI1NiJ9..."
}
],
"totalAmount": 100000.00,
"currency": "TZS",
"paymentMethod": "CASH",
"soldBy": "Gate-A Scanner",
"soldAt": "Main Entrance",
"saleTime": "2025-09-23T18:00:05Z"
}
}
Success Response Fields:
| Field | Description |
|---|---|
bookingId |
UUID of the created booking order |
bookingReference |
Human-readable booking reference number |
eventId |
ID of the event |
eventName |
Name of the event |
tickets |
Array of issued ticket instances — one per attendee |
tickets[].ticketInstanceId |
Unique ID of this specific ticket instance |
tickets[].ticketSeries |
Ticket serial number (e.g., VIP-0042-A) |
tickets[].ticketTypeName |
The type of the sold ticket |
tickets[].attendeeName |
Name of the attendee this ticket is assigned to |
tickets[].attendeeEmail |
Email of the attendee |
tickets[].checkedIn |
Whether the attendee has been checked in |
tickets[].checkInTime |
Timestamp of check-in if immediateCheckIn was true |
tickets[].qrCode |
JWT-encoded QR code string for this ticket |
totalAmount |
Total cash amount collected in TZS |
currency |
Always TZS |
paymentMethod |
Always CASH for at-door sales |
soldBy |
Name of the scanner that processed the sale |
soldAt |
Location label of the scanner (e.g., "Main Entrance") |
saleTime |
ISO 8601 timestamp of when the sale occurred |
Possible Errors:
| HTTP Status | Scenario |
|---|---|
400 BAD_REQUEST |
Attendee count does not match quantity, ticket is ONLINE_ONLY, ticket not on sale |
401 UNAUTHORIZED |
Missing or invalid Bearer token |
403 FORBIDDEN |
Scanner does not have SELL_TICKETS permission, device fingerprint mismatch, scanner is inactive or expired |
404 NOT_FOUND |
Scanner ID not found, ticket type not found |
422 UNPROCESSABLE_ENTITY |
Validation errors on request fields |
500 INTERNAL_SERVER_ERROR |
Payment processing or booking creation failure |
6. Organizer — Sell Ticket at Door
Purpose: Allows the authenticated event organizer to sell tickets directly at their event counter, processing a cash payment and optionally checking in the attendee immediately.
Endpoint: POST /api/v1/e-events/checkout/sell-at-door-ticket/{eventId}/organizer
Access Level: 🔒 Protected (Must be the organizer of the specified event)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer token of the authenticated event organizer |
Content-Type |
string | Yes | Must be application/json |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
eventId |
UUID | Yes | The ID of the event to sell tickets for | Must be an existing, non-deleted event; authenticated user must be the organizer |
Request JSON Sample:
{
"ticketTypeId": "d4e5f6a7-1234-5678-abcd-000000000002",
"quantity": 2,
"attendees": [
{
"fullName": "Peter Salim",
"email": "peter.salim@example.com",
"phoneNumber": "+255711223344"
},
{
"fullName": "Grace Mwangi",
"email": "grace.mwangi@example.com",
"phoneNumber": null
}
],
"immediateCheckIn": false,
"location": "VIP Gate"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
ticketTypeId |
UUID | Yes | ID of the ticket type to sell | Must belong to the event in the path; must not be ONLINE_ONLY; must be on sale |
quantity |
integer | Yes | Total number of tickets to sell | Min: 1; must equal the number of attendees in the attendees array |
attendees |
array | Yes | List of attendee details — one entry per ticket | Min: 1 entry; count must match quantity |
attendees[].fullName |
string | No | Full name of the attendee | Optional — auto-generated if blank |
attendees[].email |
string | No | Email of the attendee | Valid email format if provided |
attendees[].phoneNumber |
string | No | Phone number of the attendee | Optional |
immediateCheckIn |
boolean | Yes | Whether to mark attendees as checked-in immediately | Required |
location |
string | No | Description of the sale point, e.g., "VIP Gate", "Main Counter" | Max: 200 characters; defaults to "Organizer Counter" if not provided |
Business Rules:
- Only the event organizer (the user who created the event) can call this endpoint
- Number of entries in
attendeesmust equalquantity - Payment is always
CASH— no wallet or ledger deduction - Ticket type must not be
ONLINE_ONLY immediateCheckIn = truemarks each ticket as checked-in at time of sale
Success Response JSON Sample:
{
"success": true,
"httpStatus": "CREATED",
"message": "Tickets sold successfully at door",
"action_time": "2025-09-23T17:30:00",
"data": {
"bookingId": "d7e6f5a4-0000-0000-0000-000000000035",
"bookingReference": "BK-2025-000043",
"eventId": "b3f1a2c4-1234-5678-abcd-000000000001",
"eventName": "Kilimanjaro Jazz Night 2025",
"tickets": [
{
"ticketInstanceId": "ee55ff66-0000-0000-0000-000000000060",
"ticketSeries": "VIP-0043-A",
"ticketTypeName": "VIP",
"attendeeName": "Peter Salim",
"attendeeEmail": "peter.salim@example.com",
"checkedIn": false,
"checkInTime": null,
"qrCode": "eyJhbGciOiJIUzI1NiJ9..."
},
{
"ticketInstanceId": "gg77hh88-0000-0000-0000-000000000061",
"ticketSeries": "VIP-0043-B",
"ticketTypeName": "VIP",
"attendeeName": "Grace Mwangi",
"attendeeEmail": "grace.mwangi@example.com",
"checkedIn": false,
"checkInTime": null,
"qrCode": "eyJhbGciOiJIUzI1NiJ9..."
}
],
"totalAmount": 100000.00,
"currency": "TZS",
"paymentMethod": "CASH",
"soldBy": "organizer_username",
"soldAt": "VIP Gate",
"saleTime": "2025-09-23T17:30:05Z"
}
}
Success Response Fields:
| Field | Description |
|---|---|
bookingId |
UUID of the created booking order |
bookingReference |
Human-readable booking reference number |
eventId |
ID of the event |
eventName |
Name of the event |
tickets |
Array of issued ticket instances — one per attendee |
tickets[].ticketInstanceId |
Unique ID of this specific ticket instance |
tickets[].ticketSeries |
Ticket serial number |
tickets[].ticketTypeName |
The type of ticket sold |
tickets[].attendeeName |
Assigned attendee name |
tickets[].attendeeEmail |
Attendee email |
tickets[].checkedIn |
Whether immediately checked in |
tickets[].checkInTime |
Check-in timestamp, null if not checked in |
tickets[].qrCode |
JWT-encoded QR code string for this ticket |
totalAmount |
Total cash amount in TZS |
currency |
Always TZS |
paymentMethod |
Always CASH |
soldBy |
Username of the organizer who made the sale |
soldAt |
Location label provided in the request |
saleTime |
ISO 8601 timestamp of the sale |
Possible Errors:
| HTTP Status | Scenario |
|---|---|
400 BAD_REQUEST |
Attendee count does not match quantity, ticket is ONLINE_ONLY, ticket not on sale |
401 UNAUTHORIZED |
Missing or invalid Bearer token |
403 FORBIDDEN |
Authenticated user is not the organizer of the specified event |
404 NOT_FOUND |
Event not found, ticket type not found |
422 UNPROCESSABLE_ENTITY |
Validation errors on request fields |
500 INTERNAL_SERVER_ERROR |
Payment processing or booking creation failure |
Standard Error Response Examples
Bad Request — General (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Ticket is not currently on sale",
"action_time": "2025-09-23T10:30:45",
"data": "Ticket is not currently on sale"
}
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Token has expired",
"action_time": "2025-09-23T10:30:45",
"data": "Token has expired"
}
Forbidden — Access Denied (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "Only the event organizer can sell tickets at door",
"action_time": "2025-09-23T10:30:45",
"data": "Only the event organizer can sell tickets at door"
}
Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Event not found",
"action_time": "2025-09-23T10:30:45",
"data": "Event not found"
}
Validation Error (422):
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Validation failed",
"action_time": "2025-09-23T10:30:45",
"data": {
"ticketTypeId": "must not be null",
"quantity": "must be greater than or equal to 1",
"immediateCheckIn": "must not be null"
}
}
Standard Error Types Reference
Application-Level Exceptions (400–499)
400 BAD_REQUEST: General invalid request, business rule violations, or item already exists401 UNAUTHORIZED: Authentication issues (missing, invalid, expired, or malformed token)403 FORBIDDEN: Access denied, scanner permission issues, organizer mismatch404 NOT_FOUND: Event, ticket type, session, or scanner not found422 UNPROCESSABLE_ENTITY: Bean validation errors with per-field details429 TOO_MANY_REQUESTS: Rate limit exceeded
Server-Level Exceptions (500+)
500 INTERNAL_SERVER_ERROR: Unexpected server errors, payment orchestration failures
Ticket Pricing Types — Detailed Behaviour
This section explains exactly how the system handles each pricing type end-to-end: from checkout creation through payment, booking order creation, and ticket serial assignment. Understanding this is critical for integrating correctly with the checkout API.
FREE Tickets
What they are: Tickets with a price of 0 TZS. No money changes hands.
Checkout flow:
[ POST /checkout — session created ]
|
v
[ System detects price = 0 TZS ]
|
v
[ Payment auto-processed immediately ]
[ No wallet deduction ]
[ No escrow created ]
|
v
[ PaymentCompletedEvent published (escrow = null) ]
|
v
[ Booking order created asynchronously ]
[ Ticket serials assigned ]
|
v
[ Session status = COMPLETED ]
[ Response returned to caller ]
Key rules:
- The caller does not need to call
POST /{sessionId}/payment— this is skipped entirely - The session is returned from the create endpoint already in
PAYMENT_COMPLETEDstatus (may shift toCOMPLETEDonce the booking is written) - No escrow record exists for this transaction;
escrowIdwill benullin all responses - A
NEUTRALtransaction history entry is recorded for audit purposes - Ticket holds are still applied on creation and released naturally upon booking completion
- FREE tickets can be
ONLINE_ONLYorBOTHdepending on the sales channel configuration —AT_DOOR_ONLYFREE tickets go through the scanner/organizer at-door flows instead
PAID Tickets
What they are: Tickets with a price greater than 0 TZS. Wallet payment is required.
Checkout flow:
[ POST /checkout — session created ]
[ Wallet balance validated upfront ]
[ Ticket hold applied ]
|
v
[ Session status = PENDING_PAYMENT ]
[ paymentIntent.provider = WALLET ]
|
v
[ POST /{sessionId}/payment called by client ]
|
v
[ Wallet deducted via double-entry ledger ]
[ Escrow account created (ESC-YYYY-NNNNNN)]
[ platformFee = 5% of total ]
[ sellerAmount = total - platformFee ]
|
v
[ PaymentCompletedEvent published (escrow != null) ]
|
v
[ Booking order created asynchronously ]
[ Ticket serials assigned ]
[ QR codes generated ]
|
v
[ Session status = COMPLETED ]
[ Escrow status = HELD ]
[ (Released to organizer on event completion) ]
Key rules:
- Wallet balance is checked twice: once during session creation (upfront validation) and once at actual payment time — a window exists between the two checks where balance could change
- Maximum 5 payment attempts per session; after that
canRetryPayment = falseand a new session must be created - If a payment attempt fails, the session moves to
PAYMENT_FAILEDbut the ticket hold remains active until the session expires - Escrow holds funds in a separate ledger account — the organizer does not receive the money until the platform releases it after the event
orderIdin the payment response may benullimmediately after payment since booking creation is asynchronous — pollGET /checkout/{sessionId}and checkcreatedBookingOrderIdto confirm
DONATION Tickets
What they are: Tickets where the attendee voluntarily chooses the amount they pay. A minimum may or may not be set by the organizer.
Checkout flow:
[ POST /checkout — session created ]
[ donationAmount provided in body ]
|
v
[ System validates DONATION rules ]
.....................................
. totalQuantity must be exactly 1 .
. otherAttendees must be empty .
. salesChannel must be ONLINE_ONLY .
.....................................
|
v
[ Treated as PAID internally ]
[ donationAmount used as the ticket price ]
[ Wallet deducted for the donation amount ]
[ Escrow created for the donation amount ]
|
v
[ Booking order created asynchronously ]
[ Ticket serial assigned ]
[ QR code generated ]
|
v
[ Session status = COMPLETED ]
Key rules:
- Strictly 1 ticket per order — the system rejects any request with
ticketsForMe > 1or anyotherAttendees - Online-only — DONATION tickets cannot be sold at the door through any channel
- The
donationAmountin the request body is the amount that will be charged; the system uses it as the effective unit price - Despite being a donation, the standard 5% platform fee still applies and an escrow account is created
- If
donationAmountisnullor0, the system may treat it as a FREE ticket depending on the ticket's configured minimum — confirm with the organizer's ticket setup
Ticket Serials — How They Are Assigned
Every ticket instance issued by the system receives a unique ticket serial (also called ticketSeries in the response). This serial is the human-readable identifier printed on physical tickets, displayed in QR codes, and used for manual verification at the door.
Serial Format
[TICKET_TYPE_CODE]-[BOOKING_NUMBER]-[POSITION_LETTER]
Examples:
VIP-0042-A ← First VIP ticket in booking #42
VIP-0042-B ← Second VIP ticket in booking #42
REG-0199-A ← First Regular ticket in booking #199
GENERAL-0001-A ← First General Admission ticket in booking #1
How Serials Are Generated
[ Booking order created ]
|
v
[ System reads totalQuantity from checkout session ]
|
v
[ For each ticket in the order: ]
.......................................
. ticketSeries = TYPE_CODE .
. + "-" .
. + BOOKING_NUMBER . ← zero-padded (e.g., 0042)
. + "-" .
. + POSITION_LETTER . ← A, B, C, D ... per ticket
.......................................
|
v
[ Each serial stored on the TicketInstance entity ]
[ JWT-encoded QR token generated per ticket ]
[ QR token embeds: ticketSeries + ticketInstanceId ]
|
v
[ Serials returned in: ]
. POST /sell-at-door-ticket response (tickets[].ticketSeries) ]
. Booking order details endpoint (separate booking API) ]
Serial Assignment per Pricing Type
| Pricing Type | When Serials Are Assigned | Who Appears in attendeeName |
|---|---|---|
FREE |
Asynchronously after PaymentCompletedEvent |
Buyer for ticketsForBuyer tickets; each named attendee for their tickets |
PAID |
Asynchronously after payment escrow is created | Buyer for ticketsForBuyer tickets; each named attendee for their tickets |
DONATION |
Asynchronously after payment (always 1 ticket) | Always the buyer only |
| At-Door (any type) | Synchronously — returned immediately in the sale response | Each attendee in the attendees array; auto-generated name if blank |
Attendee-to-Serial Mapping
When a buyer purchases tickets for themselves and other attendees, each serial maps to exactly one person:
Buyer purchases:
ticketsForMe = 2
otherAttendees = [ { name: "Jane", quantity: 1 } ]
totalQuantity = 3
Serials assigned:
VIP-0042-A → Buyer (ticket 1 of 2 for buyer)
VIP-0042-B → Buyer (ticket 2 of 2 for buyer)
VIP-0042-C → Jane (her 1 ticket)
QR Code & Serial Relationship
Each ticket's qrCode field in the response is a JWT token that encodes the ticket serial and instance ID. Scanners decode this JWT at check-in time to verify the ticket. The serial alone is readable by humans; the JWT is what the scanner hardware validates cryptographically.
QR Code (JWT) decodes to:
...........................................
. ticketInstanceId (UUID) .
. ticketSeries (e.g. VIP-0042-A) .
. eventId (UUID) .
. issuedAt (timestamp) .
...........................................
|
v
[ Scanner validates JWT signature ]
[ Marks ticket as CHECKED_IN ]
[ Returns check-in confirmation ]
Quick Reference
Endpoint Summary
| # | Method | Path | Description |
|---|---|---|---|
| 1 | POST | /api/v1/e-events/checkout |
Create online checkout session |
| 2 | GET | /api/v1/e-events/checkout/{sessionId} |
Get checkout session details |
| 3 | POST | /api/v1/e-events/checkout/{sessionId}/payment |
Process wallet payment |
| 4 | POST | /api/v1/e-events/checkout/{sessionId}/cancel |
Cancel checkout session |
| 5 | POST | /api/v1/e-events/checkout/sell-at-door-ticket/scanner |
Scanner at-door sale |
| 6 | POST | /api/v1/e-events/checkout/sell-at-door-ticket/{eventId}/organizer |
Organizer at-door sale |
Session Status Reference
| Status | Meaning |
|---|---|
PENDING_PAYMENT |
Session created, awaiting payment |
PAYMENT_PROCESSING |
External payment initiated, awaiting confirmation |
PAYMENT_COMPLETED |
Payment succeeded, booking being created |
PAYMENT_FAILED |
Payment attempt failed (retry may be possible) |
COMPLETED |
Booking fully created and confirmed |
CANCELLED |
Session cancelled by user |
EXPIRED |
Session timed out before payment |
Ticket Pricing Type Behaviour
| Pricing Type | Payment Required | At-Door Allowed | Notes |
|---|---|---|---|
FREE |
No | Depends on sales channel | Auto-processed on session creation |
PAID |
Yes (Wallet) | Yes | Escrow created on payment |
DONATION |
Optional amount | No (Online only) | Max 1 ticket per order; no other attendees |
Sales Channel Rules
| Sales Channel | Online Checkout | At-Door (Scanner) | At-Door (Organizer) |
|---|---|---|---|
ONLINE_ONLY |
✅ Allowed | ❌ Blocked | ❌ Blocked |
AT_DOOR_ONLY |
❌ Blocked | ✅ Allowed | ✅ Allowed |
BOTH |
✅ Allowed | ✅ Allowed | ✅ Allowed |
Authentication Reference
- Bearer Token: Include
Authorization: Bearer <token>in all request headers - All endpoints require authentication
- For scanner endpoints, the Bearer token must belong to the account linked to the scanner device
Data Format Standards
- Dates: ISO 8601 format (
2025-09-23T10:30:45) - Currency: TZS (Tanzanian Shilling) — decimal values with 2 decimal places
- UUIDs: Standard UUID v4 format (
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) - Phone Numbers: Tanzania format only —
+255[67]XXXXXXXX
Event Booking Orders API
Base URL: https://api.nexgate.com/api/v1
Short Description: The Event Booking Orders API provides comprehensive booking management functionality for confirmed event ticket purchases in the Nexgate platform. This API enables users to view their booking details including JWT-signed QR codes for secure event entry, track multi-day event check-ins, retrieve complete booking history, and access event snapshots at time of booking. The system supports both single-day and multi-day events with per-day check-in tracking, automated ticket series generation, and comprehensive booking references.
Hints:
- Auto-Creation: Bookings created automatically after successful checkout payment
- JWT Security: QR codes are RSA-signed JWTs containing ticket and event data
- Multi-Day Support: Full support for multi-day events with per-day check-in tracking
- Ticket Series: Auto-generated unique series (e.g., "VIP-0001", "GENER-0042")
- Event Snapshots: Event details captured at booking time (immutable)
- Booking Reference: Short readable codes (e.g., "EVT-2025-A3F4")
- Check-In Tracking: Complete history with location, staff, and day information
- Access Control: Customers see own bookings, organizers see event bookings, admins see all
- Notification Integration: Auto-emails tickets to buyer and optionally to attendees
- Virtual Events: Includes virtual meeting links for online/hybrid events
- Attendee Management: Track tickets for buyer and other attendees separately
Standard Response Format
All API responses follow a consistent structure using our Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-12-11T10:30:45",
"data": {
// Actual response data goes here
}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2025-12-11T10:30:45",
"data": "Error description"
}
HTTP Method Badge Standards
- GET - GET - Green (Read operations)
BookingOrderResponse Structure
This is the comprehensive response structure returned by booking endpoints:
{
"bookingId": "550e8400-e29b-41d4-a716-446655440000",
"bookingReference": "EVT-A3F4B21C",
"status": "CONFIRMED",
"event": {
"eventId": "770e8400-e29b-41d4-a716-446655440002",
"title": "East African Tech Summit 2025",
"startDateTime": "2025-12-15T09:00:00",
"endDateTime": "2025-12-17T18:00:00",
"timezone": "Africa/Nairobi",
"location": "KICC Nairobi, Harambee Avenue, Nairobi",
"format": "HYBRID",
"virtualDetails": {
"platform": "ZOOM",
"meetingUrl": "https://zoom.us/j/123456789",
"meetingId": "123 456 789",
"passcode": "summit2025",
"additionalInstructions": "Join 5 minutes early for networking"
}
},
"organizer": {
"name": "TechEvents Kenya",
"email": "organizer@techevents.ke",
"phone": "+254712345678"
},
"customer": {
"customerId": "660e8400-e29b-41d4-a716-446655440001",
"name": "johndoe",
"email": "john@example.com"
},
"tickets": [
{
"ticketInstanceId": "880e8400-e29b-41d4-a716-446655440010",
"ticketTypeName": "VIP Pass",
"ticketSeries": "VIP-0001",
"price": 150.00,
"qrCode": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"attendanceMode": "IN_PERSON",
"attendee": {
"name": "John Doe",
"email": "john@example.com",
"phone": "+255712345678"
},
"buyer": {
"name": "John Doe",
"email": "john@example.com"
},
"checkIns": [
{
"checkInTime": "2025-12-15T09:15:00+03:00",
"checkInLocation": "Main Gate",
"checkedInBy": "Scanner Operator 1",
"dayName": "Day 1 - Opening Day",
"scannerId": "SCANNER-001",
"checkInMethod": "QR_SCAN"
},
{
"checkInTime": "2025-12-16T08:45:00+03:00",
"checkInLocation": "VIP Entrance",
"checkedInBy": "Scanner Operator 2",
"dayName": "Day 2 - Conference Day",
"scannerId": "SCANNER-003",
"checkInMethod": "QR_SCAN"
}
],
"hasBeenCheckedIn": true,
"lastCheckedInAt": "2025-12-16T08:45:00+03:00",
"lastCheckedInBy": "Scanner Operator 2",
"lastCheckInLocation": "VIP Entrance",
"lastCheckInDayName": "Day 2 - Conference Day",
"status": "USED",
"validFrom": "2025-12-15T09:00:00+03:00",
"validUntil": "2025-12-17T18:00:00+03:00"
},
{
"ticketInstanceId": "880e8400-e29b-41d4-a716-446655440011",
"ticketTypeName": "General Admission",
"ticketSeries": "GENER-0042",
"price": 50.00,
"qrCode": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"attendanceMode": "IN_PERSON",
"attendee": {
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+255723456789"
},
"buyer": {
"name": "John Doe",
"email": "john@example.com"
},
"checkIns": [],
"hasBeenCheckedIn": false,
"lastCheckedInAt": null,
"lastCheckedInBy": null,
"lastCheckInLocation": null,
"lastCheckInDayName": null,
"status": "ACTIVE",
"validFrom": "2025-12-15T09:00:00+03:00",
"validUntil": "2025-12-17T18:00:00+03:00"
}
],
"totalTickets": 2,
"checkedInTicketsCount": 1,
"subtotal": 200.00,
"total": 200.00,
"bookedAt": "2025-12-11T10:30:45",
"cancelledAt": null
}
Response Field Descriptions
Root Level Fields
| Field | Type | Description |
|---|---|---|
| bookingId | string (UUID) | Unique booking identifier |
| bookingReference | string | Short readable code (e.g., "EVT-A3F4B21C") |
| status | enum | Booking status: CONFIRMED, CANCELLED |
| event | object | Event snapshot at time of booking |
| organizer | object | Organizer details snapshot |
| customer | object | Customer (buyer) information |
| tickets | array | List of all booked tickets with check-in history |
| totalTickets | integer | Total number of tickets in booking |
| checkedInTicketsCount | integer | Count of tickets with at least one check-in |
| subtotal | decimal | Subtotal amount |
| total | decimal | Total amount paid |
| bookedAt | string (LocalDateTime) | When booking was created |
| cancelledAt | string (LocalDateTime) | When booking was cancelled (null if active) |
Event Snapshot Object
| Field | Type | Description |
|---|---|---|
| eventId | string (UUID) | Event identifier |
| title | string | Event title at time of booking |
| startDateTime | string (LocalDateTime) | Event start date/time |
| endDateTime | string (LocalDateTime) | Event end date/time |
| timezone | string | IANA timezone (e.g., "Africa/Nairobi") |
| location | string | Full venue location (name + address) |
| format | string | Event format: IN_PERSON, ONLINE, HYBRID |
| virtualDetails | object | Virtual meeting details (for ONLINE/HYBRID) |
Virtual Details Object
| Field | Type | Description |
|---|---|---|
| platform | string | Platform: ZOOM, GOOGLE_MEET, MS_TEAMS, CUSTOM |
| meetingUrl | string | Full meeting URL |
| meetingId | string | Meeting ID (optional) |
| passcode | string | Meeting passcode (optional) |
| additionalInstructions | string | Extra instructions (optional) |
Organizer Snapshot Object
| Field | Type | Description |
|---|---|---|
| name | string | Organizer name at time of booking |
| string | Organizer email | |
| phone | string | Organizer phone |
Customer Info Object
| Field | Type | Description |
|---|---|---|
| customerId | string (UUID) | Customer account ID |
| name | string | Customer username |
| string | Customer email |
Booked Ticket Response Object
| Field | Type | Description |
|---|---|---|
| ticketInstanceId | string (UUID) | Unique ticket instance ID |
| ticketTypeName | string | Ticket type name (e.g., "VIP Pass") |
| ticketSeries | string | Auto-generated series (e.g., "VIP-0001") |
| price | decimal | Ticket price |
| qrCode | string | JWT-signed QR code (very long string) |
| attendanceMode | string | IN_PERSON or ONLINE (for hybrid events) |
| attendee | object | Attendee information |
| buyer | object | Buyer information (who paid) |
| checkIns | array | Complete check-in history (all days) |
| hasBeenCheckedIn | boolean | True if checked in at least once |
| lastCheckedInAt | string (ZonedDateTime) | Most recent check-in time |
| lastCheckedInBy | string | Who performed last check-in |
| lastCheckInLocation | string | Location of last check-in |
| lastCheckInDayName | string | Day name of last check-in |
| status | enum | ACTIVE, USED, CANCELLED |
| validFrom | string (ZonedDateTime) | Ticket valid from |
| validUntil | string (ZonedDateTime) | Ticket valid until |
Attendee/Buyer Info Object
| Field | Type | Description |
|---|---|---|
| name | string | Person's name |
| string | Person's email | |
| phone | string | Person's phone (attendee only) |
Check-In Record Object
| Field | Type | Description |
|---|---|---|
| checkInTime | string (ZonedDateTime) | When check-in occurred |
| checkInLocation | string | Where (e.g., "Main Gate", "VIP Entrance") |
| checkedInBy | string | Staff/scanner operator name |
| dayName | string | Event day (e.g., "Day 1", "Day 2 - Saturday") |
| scannerId | string | Scanner device ID |
| checkInMethod | string | Method: QR_SCAN (default), MANUAL, NFC |
BookingOrderSummaryResponse Structure
Lightweight response for listing bookings:
{
"bookingId": "550e8400-e29b-41d4-a716-446655440000",
"bookingReference": "EVT-A3F4B21C",
"status": "CONFIRMED",
"eventTitle": "East African Tech Summit 2025",
"eventStartDateTime": "2025-12-15T09:00:00",
"eventLocation": "KICC Nairobi, Harambee Avenue, Nairobi",
"totalTickets": 2,
"checkedInTickets": 1,
"total": 200.00,
"bookedAt": "2025-12-11T10:30:45"
}
Endpoints
1. Get Booking by ID
Purpose: Retrieve complete booking details including all tickets and check-in history
Endpoint: GET {base_url}/e-events/booking-orders/{bookingId}
Access Level: 🔒 Protected (Booking Owner, Event Organizer, or Admin)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token (format: Bearer <token>) |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| bookingId | string (UUID) | Yes | Booking order ID |
Success Response: Returns complete BookingOrderResponse structure
Success Response Message: "Booking retrieved successfully"
HTTP Status Code: 200 OK
Access Rules:
- Customers: Can view their own bookings
- Organizers: Can view bookings for their events
- Admins (SUPER_ADMIN, STAFF_ADMIN): Can view any booking
Behavior:
- Returns complete booking details with all tickets
- Includes event snapshot (as it was at booking time)
- Shows full check-in history for multi-day events
- Includes JWT-signed QR codes for all tickets
- Virtual details included for ONLINE/HYBRID events
Use Cases:
- Customer viewing booking confirmation
- Customer retrieving QR codes for event entry
- Organizer checking booking details
- Admin reviewing booking for support
Standard Error Types:
Error Response Examples:
Booking Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Booking not found: 550e8400-e29b-41d4-a716-446655440000",
"action_time": "2025-12-11T10:30:45",
"data": "Booking not found: 550e8400-e29b-41d4-a716-446655440000"
}
Access Denied (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "You don't have permission to view this booking",
"action_time": "2025-12-11T10:30:45",
"data": "You don't have permission to view this booking"
}
2. Get My Bookings
Purpose: Retrieve all bookings for the authenticated user
Endpoint: GET {base_url}/e-events/booking-orders/my-bookings
Access Level: 🔒 Protected (Authenticated Users)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token (format: Bearer <token>) |
Success Response: Returns array of BookingOrderSummaryResponse
Success Response Message: "Bookings retrieved successfully"
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Bookings retrieved successfully",
"action_time": "2025-12-11T10:30:45",
"data": [
{
"bookingId": "550e8400-e29b-41d4-a716-446655440000",
"bookingReference": "EVT-A3F4B21C",
"status": "CONFIRMED",
"eventTitle": "East African Tech Summit 2025",
"eventStartDateTime": "2025-12-15T09:00:00",
"eventLocation": "KICC Nairobi, Harambee Avenue, Nairobi",
"totalTickets": 2,
"checkedInTickets": 1,
"total": 200.00,
"bookedAt": "2025-12-11T10:30:45"
},
{
"bookingId": "550e8400-e29b-41d4-a716-446655440001",
"bookingReference": "EVT-B5D2E12F",
"status": "CONFIRMED",
"eventTitle": "Dar es Salaam Food Festival",
"eventStartDateTime": "2025-12-20T11:00:00",
"eventLocation": "Mlimani City, Sam Nujoma Road, Dar es Salaam",
"totalTickets": 4,
"checkedInTickets": 0,
"total": 150.00,
"bookedAt": "2025-12-10T14:20:30"
}
]
}
HTTP Status Code: 200 OK
Sorting: Bookings sorted by bookedAt descending (newest first)
Behavior:
- Returns lightweight summary for all user's bookings
- Sorted newest first
- Includes check-in progress (checkedInTickets / totalTickets)
- Shows upcoming and past events
Use Cases:
- User viewing booking history
- Dashboard showing all purchases
- Finding booking for upcoming event
- Checking past event attendance
Standard Error Types:
3. Download Ticket PDF
Purpose: Download the ticket PDF for a specific ticket belonging to the authenticated user
Endpoint: GET {base_url}/e-events/booking-orders/tickets/{ticketId}/pdf
Access Level: 🔒 Protected (Authenticated Users)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token (format: Bearer <token>) |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| ticketId | UUID | Yes | The unique ID of the ticket to download |
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| mode | string | No | download |
Controls how the PDF is served. download forces a file save dialog. inline opens the PDF directly in the browser. |
Mode Values:
| Value | Content-Disposition | Behaviour |
|---|---|---|
download |
attachment; filename="ticket-{series}.pdf" |
Browser prompts user to save the file |
inline |
inline; filename="ticket-{series}.pdf" |
PDF opens directly in the browser tab |
Success Response: Returns a binary PDF file
Success Response Headers:
| Header | Value |
|---|---|
| Content-Type | application/pdf |
| Content-Disposition | attachment; filename="ticket-{series}.pdf" (or inline depending on mode) |
HTTP Status Code: 200 OK
Behavior:
- Verifies the ticket belongs to the authenticated user before generating the PDF
- Generates the ticket PDF on-the-fly using the event's SVG template
- The PDF contains event details, attendee name, seat/series, QR code for check-in, and ticket number
- QR code encodes a signed JWT token used by gate staff to verify the ticket at entry
- File name in the
Content-Dispositionheader uses the ticket's series (e.g.ticket-VIP-0033.pdf)
Use Cases:
- User downloading their ticket after booking
- Re-downloading a lost or deleted ticket
- Saving ticket to device for offline access at the event
Standard Error Types:
| Code | Type | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
Missing or invalid Bearer token |
| 403 | FORBIDDEN |
Ticket does not belong to the authenticated user |
| 404 | NOT_FOUND |
Ticket with the given ID not found |
| 500 | INTERNAL_SERVER_ERROR |
PDF generation failed |
Booking Status Lifecycle
Status Descriptions
| Status | Description | Can Cancel? | Tickets Valid? |
|---|---|---|---|
| CONFIRMED | Booking active and tickets valid | Future (not implemented) | Yes |
| CANCELLED | Booking cancelled (placeholder) | N/A | No |
Current Implementation:
- All bookings created as CONFIRMED
- Cancellation not yet implemented (status exists for future)
- Tickets remain ACTIVE after booking until checked in
Ticket Instance Status
Status Lifecycle
| Status | Description | Can Check In? |
|---|---|---|
| ACTIVE | Ticket ready for use | Yes |
| USED | Ticket checked in | Yes (for multi-day) |
| CANCELLED | Ticket cancelled | No |
Multi-Day Events:
- Ticket status remains ACTIVE even after first check-in
- Can check in multiple times (once per day)
- Status changes to USED after all expected check-ins
- Each check-in tracked separately in checkIns array
Ticket Series Generation
How Ticket Series Work
Format: {TICKET_CODE}-{COUNTER}
Examples:
- VIP Pass →
VIP-0001,VIP-0002,VIP-0003 - General Admission →
GENER-0001,GENER-0002 - Early Bird →
EARLY-0001,EARLY-0002
Ticket Code Extraction:
- Take first 5 characters of ticket type name
- Remove spaces
- Convert to uppercase
- Fallback to "TICK" if name empty
Counter System:
- Separate counter per ticket type (stored in DB)
- Pessimistic locking prevents duplicates
- Counter never resets (unique forever)
- Increments with each ticket created
Example Flow:
Ticket Type: "VIP Pass"
Counter: 0
First Booking (3 tickets):
- VIP-0001 (counter now 1)
- VIP-0002 (counter now 2)
- VIP-0003 (counter now 3)
Second Booking (2 tickets):
- VIP-0004 (counter now 4)
- VIP-0005 (counter now 5)
Why Series Numbers?
- Easy reference for staff
- Quick ticket identification
- Helps track capacity sold
- Professional appearance on PDF tickets
JWT-Signed QR Codes
Security Architecture
What is the QR Code?
- Not a random string
- Full JWT (JSON Web Token)
- Signed with event's RSA private key
- Contains complete ticket and event data
- Cannot be forged or tampered with
JWT Structure:
Header.Payload.Signature
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ0aWNrZXRJbnN0YW5jZUlkIjoiODgw...
[RSA signature]
JWT Payload Contains:
- ticketInstanceId (UUID)
- ticketTypeId (UUID)
- ticketTypeName (string)
- ticketSeries (string)
- eventId (UUID)
- eventName (string)
- eventStartDateTime (timestamp)
- attendeeName (string)
- attendeeEmail (string)
- attendeePhone (string)
- attendanceMode (IN_PERSON/ONLINE)
- bookingReference (string)
- eventSchedules (array of days for multi-day events)
- validFrom (timestamp)
- validUntil (timestamp)
Multi-Day Event Schedules:
"eventSchedules": [
{
"dayName": "Day 1 - Opening Day",
"startDateTime": "2025-12-15T09:00:00+03:00",
"endDateTime": "2025-12-15T18:00:00+03:00",
"description": "Conference Opening & Keynotes"
},
{
"dayName": "Day 2 - Conference Day",
"startDateTime": "2025-12-16T09:00:00+03:00",
"endDateTime": "2025-12-16T18:00:00+03:00",
"description": "Technical Sessions & Workshops"
},
{
"dayName": "Day 3 - Closing Day",
"startDateTime": "2025-12-17T09:00:00+03:00",
"endDateTime": "2025-12-17T18:00:00+03:00",
"description": "Finals & Networking"
}
]
RSA Key Pair:
- Generated when event is published (2048-bit)
- Private key: Stored securely in database (for signing)
- Public key: Available to scanners (for verification)
- Cannot derive private key from public key
Verification Process (Scanner App):
- Scanner reads QR code
- Extracts JWT token
- Fetches event's public key
- Verifies JWT signature
- Checks expiry and validity
- Checks if already checked in for this day
- Records check-in if valid
Why JWT over Simple Codes?
- Cannot be forged: Requires private key
- Contains all data: No database lookup needed
- Offline capable: Scanner can verify without internet
- Tamper-proof: Any modification breaks signature
- Time-limited: validFrom/validUntil built in
- Day tracking: Multi-day schedules embedded
Multi-Day Event Check-Ins
How Multi-Day Check-Ins Work
Single-Day Event:
- One check-in expected
- After check-in, status changes to USED
- Simple entry tracking
Multi-Day Event (e.g., 3-day festival):
- Multiple check-ins allowed (one per day)
- Each day tracked separately
- Ticket remains ACTIVE after first day
- Full history stored in
checkInsarray
Check-In Record Fields:
- checkInTime: When check-in happened
- checkInLocation: Where (e.g., "Main Gate", "VIP Entrance")
- checkedInBy: Staff/scanner operator name
- dayName: Which day (must match eventSchedules)
- scannerId: Scanner device ID
- checkInMethod: QR_SCAN, MANUAL, NFC (default: QR_SCAN)
Example: 3-Day Festival
Day 1 (Friday):
{
"checkInTime": "2025-12-15T18:30:00+03:00",
"checkInLocation": "Main Gate",
"checkedInBy": "Staff Member 1",
"dayName": "Day 1 - Friday Night",
"scannerId": "SCANNER-001",
"checkInMethod": "QR_SCAN"
}
Day 2 (Saturday):
{
"checkInTime": "2025-12-16T14:15:00+03:00",
"checkInLocation": "VIP Entrance",
"checkedInBy": "Staff Member 2",
"dayName": "Day 2 - Saturday",
"scannerId": "SCANNER-003",
"checkInMethod": "QR_SCAN"
}
Day 3 (Sunday):
{
"checkInTime": "2025-12-17T12:00:00+03:00",
"checkInLocation": "Main Gate",
"checkedInBy": "Staff Member 1",
"dayName": "Day 3 - Sunday",
"scannerId": "SCANNER-001",
"checkInMethod": "QR_SCAN"
}
Validation Rules:
dayNamemust match one from JWT's eventSchedules- Cannot check in twice on same day
- Can check in on different days
- Check-in time must be within day's start/end
Benefits:
- Accurate attendance tracking per day
- Prevents duplicate same-day entries
- Allows organizers to see daily attendance
- Supports flexible entry (don't need all days)
Event Snapshots
Why Snapshots?
Problem: Event details can change after booking
- Event renamed
- Time changed
- Venue moved
- Organizer updated contact
Solution: Capture event state at booking time
Snapshot Fields (Immutable after booking):
- eventTitle
- eventStartDateTime
- eventEndDateTime
- eventTimezone
- eventLocation (full venue details)
- eventFormat
- virtualDetails (for online/hybrid)
- organizerName
- organizerEmail
- organizerPhone
Benefits:
- PDF Tickets: Generate correct ticket with original details
- Legal Record: What customer actually purchased
- Email Confirmations: Show accurate booking details
- Refund Logic: Reference original event details
- Audit Trail: Track what changed vs. what was booked
Example Scenario:
- User books ticket for "Tech Summit" at KICC
- Snapshot created: "Tech Summit", "KICC Nairobi"
- Organizer renames event to "African Tech Summit"
- Organizer moves venue to "Safari Park Hotel"
- User's booking still shows original: "Tech Summit at KICC"
- This is what they paid for (legal protection)
Booking Creation Flow
Automatic Creation After Payment
Trigger: EventPaymentCompletedListener fires after successful payment
Steps:
-
Fetch Entities:
- Get EventCheckoutSession
- Get Event details
- Get TicketType details
-
Generate Booking Reference:
- Format:
EVT-{8-char-UUID} - Example:
EVT-A3F4B21C - Unique and readable
- Format:
-
Create Ticket Instances:
- For buyer's tickets (ticketsForMe count)
- For each other attendee (their quantity)
- Each ticket gets unique series number
- Each ticket assigned to correct attendee
-
Generate JWT QR Codes:
- Create JWT payload with ticket data
- Include event schedules for multi-day
- Sign with event's RSA private key
- Set as ticket's qrCode field
-
Snapshot Event Details:
- Capture current event title, times, location
- Capture organizer details
- Store in booking (immutable)
-
Save Booking:
- Create EventBookingOrderEntity
- Save to database with all tickets
- Link to checkout session
-
Update Checkout Session:
- Set createdBookingOrderId
- Mark as COMPLETED
-
Publish Event:
- Fire BookingCreatedEvent
- Triggers notifications
-
Send Notifications:
- Email buyer confirmation with tickets
- Optionally email other attendees their tickets
- Notify event organizer of new booking
Transaction Safety:
- Entire flow in single transaction
- If any step fails, everything rolls back
- No partial bookings created
Timing:
- Happens immediately after payment success
- Usually completes in < 2 seconds
- User gets instant confirmation
Notification System Integration
Who Gets Notified?
1. Buyer (ALWAYS):
- Receives booking confirmation
- Gets ALL tickets (if sendTicketsToAttendees = false)
- Gets only THEIR tickets (if sendTicketsToAttendees = true)
- Channels: EMAIL, SMS, PUSH, IN_APP
- Priority: HIGH
2. Other Attendees (CONDITIONAL):
- Only if sendTicketsToAttendees = true
- Each attendee gets ONLY their tickets
- Channels: EMAIL, SMS
- Priority: HIGH
- Grouped by email (all tickets for same person)
3. Event Organizer (ALWAYS):
- Notified of new booking
- Gets booking summary (not tickets)
- Channels: EMAIL, IN_APP
- Priority: NORMAL
Notification Content
Buyer Confirmation:
- Booking reference
- Event title and date
- Number of tickets
- QR codes attached
- Event location/virtual link
- Instructions for entry
Attendee Ticket Email:
- "You've received tickets from [Buyer Name]"
- Event details
- Their specific QR codes
- Entry instructions
Organizer Alert:
- New booking notification
- Buyer name and email
- Number of tickets sold
- Total amount
- Booking reference for lookup
QR Code Delivery
sendTicketsToAttendees = false:
Buyer receives:
- Ticket 1 QR (for self)
- Ticket 2 QR (for self)
- Ticket 3 QR (for Jane)
- Ticket 4 QR (for Jane)
- Ticket 5 QR (for Bob)
Buyer must forward to Jane and Bob manually
sendTicketsToAttendees = true:
Buyer receives:
- Ticket 1 QR (for self)
- Ticket 2 QR (for self)
Jane receives:
- Ticket 3 QR (for Jane)
- Ticket 4 QR (for Jane)
Bob receives:
- Ticket 5 QR (for Bob)
Everyone gets their own tickets directly
Access Control Rules
Who Can View Bookings?
1. Booking Owner (Customer):
- Can view all their own bookings
- GET /booking-orders/my-bookings (all)
- GET /booking-orders/{bookingId} (specific)
2. Event Organizer:
- Can view bookings for their events
- Useful for checking attendance
- Verifying purchases
- Customer support
3. Platform Admins:
- Roles: SUPER_ADMIN, STAFF_ADMIN
- Can view any booking
- For support and dispute resolution
4. Others:
- Cannot view bookings they don't own
- Cannot view other users' bookings
- 403 Forbidden error
Virtual Event Support
Virtual Details Structure
Included for:
- ONLINE events (only virtual)
- HYBRID events (both physical + virtual)
Fields:
- platform: ZOOM, GOOGLE_MEET, MS_TEAMS, CUSTOM
- meetingUrl: Full meeting URL (required)
- meetingId: Meeting ID if applicable
- passcode: Meeting passcode if required
- additionalInstructions: Extra guidance
Example (Zoom):
{
"platform": "ZOOM",
"meetingUrl": "https://zoom.us/j/123456789?pwd=abc123",
"meetingId": "123 456 789",
"passcode": "summit2025",
"additionalInstructions": "Please join 5 minutes early for a smooth start"
}
Example (Custom):
{
"platform": "CUSTOM",
"meetingUrl": "https://customplatform.com/event/12345",
"meetingId": null,
"passcode": null,
"additionalInstructions": "Use your booking email to log in"
}
Delivery:
- Included in booking response
- Sent in confirmation email
- Sent in ticket emails
- Available before event starts
Hybrid Events:
- Virtual details + physical venue
- Attendees choose format via ticket type
- IN_PERSON tickets: Physical entry
- ONLINE tickets: Virtual access
- Both in same booking possible
Date/Time Formats
DateTime Standards
LocalDateTime (No timezone):
- Format:
YYYY-MM-DDTHH:mm:ss - Example:
2025-12-15T09:00:00 - Used for: bookedAt, cancelledAt, event snapshots
ZonedDateTime (With timezone):
- Format:
YYYY-MM-DDTHH:mm:ss±HH:mm - Example:
2025-12-15T09:00:00+03:00 - Used for: validFrom, validUntil, checkInTime
Why Different Formats?
LocalDateTime:
- Server-local time
- Audit timestamps
- No conversion needed
- Simple comparison
ZonedDateTime:
- User-aware time
- Ticket validity
- Check-in times
- Handles timezone correctly
Example:
Event in Nairobi (UTC+3):
startDateTime: 2025-12-15T09:00:00+03:00
Ticket validFrom: 2025-12-15T09:00:00+03:00
User in London sees: 2025-12-15T06:00:00+00:00 ✓
User in New York sees: 2025-12-15T01:00:00-05:00 ✓
Validation Rules Summary
Booking Retrieval Validation
Access Validation:
- ✅ User must be authenticated
- ✅ Booking must exist
- ✅ User must be: owner OR organizer OR admin
- ❌ Other users cannot access
Data Validation:
- ✅ bookingId must be valid UUID
- ✅ Booking must not be deleted (soft delete support)
Quick Reference Guide
Common HTTP Status Codes
200 OK: Successful request401 Unauthorized: Authentication required/failed403 Forbidden: User doesn't have permission404 Not Found: Booking not found
Booking Reference Format
- Pattern:
EVT-{8-char-UUID} - Examples:
EVT-A3F4B21C,EVT-F2D5E891 - Unique and readable
- Easy for customer service
Ticket Series Format
- Pattern:
{CODE}-{COUNTER} - Examples:
VIP-0001,GENER-0042,EARLY-0123 - Code: First 5 chars of ticket name
- Counter: Unique incrementing number
QR Code Format
- Type: RSA-signed JWT
- Length: ~1000-2000 characters
- Contains: Complete ticket + event data
- Cannot be: Forged, tampered, reused (tracked)
Data Format Standards
- Dates: LocalDateTime for timestamps, ZonedDateTime for validity
- IDs: UUID format
- Money: Decimal with 2 decimals (150.00)
- Currency: TZS (Tanzanian Shilling)
- Timezone: IANA format (Africa/Nairobi)
Booking Flow Checklist
- ✅ User completes checkout payment
- ✅ Payment listener triggers
- ✅ System generates booking reference
- ✅ System creates ticket instances
- ✅ System generates JWT QR codes
- ✅ System snapshots event details
- ✅ System saves booking order
- ✅ System publishes booking event
- ✅ System sends email notifications
- ✅ User receives confirmation + QR codes
Best Practices
For Users:
- Save QR codes to phone immediately
- Screenshot confirmation email
- Arrive early for check-in
- One QR per person at gate
For Developers:
- Always show booking reference prominently
- Display QR codes large enough to scan
- Cache booking details locally
- Handle offline QR display
- Show check-in history clearly
- Support PDF ticket generation
For Organizers:
- Provide clear entry instructions
- Test scanner apps before event
- Have backup manual verification
- Track attendance by day
- Monitor check-in progress live
Error Handling Tips
- 403 Forbidden: Show "This booking belongs to another user"
- 404 Not Found: Suggest "Check your email for correct reference"
- Network Error: Show cached booking if available
- QR Load Failure: Provide booking reference as backup
Common Mistakes to Avoid
❌ Showing other users' bookings
❌ Not displaying virtual details for ONLINE events
❌ Forgetting to show multi-day check-in history
❌ Not caching QR codes for offline use
❌ Displaying mutable event data instead of snapshot
❌ Not grouping attendees' tickets properly
❌ Ignoring timezone in validity checks
Additional Notes
Future Enhancements (Not Yet Implemented)
Booking Cancellation:
- Status: CANCELLED exists but not functional
- Future: Refund logic + ticket release
- Future: Cancellation deadline rules
- Future: Partial cancellations
Ticket Transfers:
- Currently: Tickets assigned at booking
- Future: Transfer ticket to another person
- Future: Resale marketplace
- Future: Name change requests
Check-In API:
- Currently: Manual check-ins not via API
- Future: Scanner app endpoints
- Future: QR verification endpoint
- Future: Manual check-in endpoint
Booking Modifications:
- Currently: Cannot modify after creation
- Future: Add more tickets
- Future: Change attendee names
- Future: Upgrade ticket types
Integration Points
Payment System:
- Booking created after payment success
- Linked via checkoutSessionId
- Payment status determines creation
Notification System:
- BookingCreatedEvent triggers emails
- Async processing via listeners
- Multiple notification channels
Escrow System:
- Payments held until event completion
- Booking tracks payment via checkout session
- Organizer paid after event
Scanner Apps:
- Will consume booking data
- Verify JWT QR codes
- Record check-ins
- Update ticket status
Conclusion
The Event Booking Orders API provides comprehensive booking management with:
✅ Security: JWT-signed QR codes prevent fraud
✅ Flexibility: Multi-day events with per-day tracking
✅ Reliability: Event snapshots preserve booking details
✅ Scalability: Unique ticket series with DB counters
✅ User Experience: Clear references and notifications
✅ Multi-Attendee: Support for group bookings
✅ Virtual Events: Full online/hybrid event support
✅ Access Control: Proper permissions for all parties
Event Check-In System API
Base URL: https://api.nexgate.com/api/v1
Short Description: The Event Check-In System API provides secure ticket validation and scanner management for event entry. This API enables organizers to generate registration tokens (like WhatsApp's "Link Device"), scanners to register for events, and perform real-time ticket validation with JWT signature verification. The system supports multi-day events with per-day check-in tracking, device fingerprinting for security, automatic scanner revocation, and comprehensive duplicate detection.
Hints:
- Registration Flow: Organizer generates token → Scanner scans QR → Scanner registers → Automatic revocation of old scanners
- Device Security: Fingerprint validation prevents credential theft
- JWT Validation: RSA-signed tickets verified offline-capable
- Multi-Day Events: Separate check-in per event day
- One Device Rule: One ACTIVE scanner per device across all events
- Check-In Strategies: 5 strategies (HOURS_BEFORE, SPECIFIC_TIME, ALL_DAY, EXACT_TIME, AS_DAY_START)
- Duplicate Detection: Prevents same-day re-entry per event day
- Scanner Stats: Tracks successful/failed scans automatically
- Auto-Revocation: Old scanners revoked when device registers for new event
- Organizer Only: Only event organizers can generate tokens and manage scanners
API Overview
Registration Token Endpoints
- POST
/check-in/tokens/generate- Generate registration token (organizer) - GET
/check-in/tokens/validate/{token}- Validate registration token
Scanner Management Endpoints
- POST
/check-in/scanners/register- Register scanner device - GET
/check-in/scanners/event/{eventId}- Get all scanners for event - GET
/check-in/scanners/event/{eventId}/active- Get active scanners - POST
/check-in/scanners/{scannerId}/revoke- Revoke scanner
Ticket Validation Endpoint
- POST
/check-in/validate- Validate ticket and check-in
Response Structures
RegistrationTokenResponse
{
"tokenId": "uuid",
"token": "REG-ABC12345-XYZ67890",
"eventId": "uuid",
"eventName": "East African Tech Summit 2025",
"scannerName": "Gate A - Main Entrance",
"expiresAt": "2025-12-11T10:35:00Z",
"validityMinutes": 5,
"remainingSeconds": 240,
"qrCodeData": "scannerapp://register?token=REG-ABC12345-XYZ67890",
"isValid": true,
"used": false
}
ScannerResponse
{
"scannerId": "uuid-string",
"name": "Gate A - Main Entrance",
"eventId": "uuid",
"eventName": "East African Tech Summit 2025",
"status": "ACTIVE",
"deviceFingerprint": "abc123def456...",
"createdAt": "2025-12-11T10:30:00Z",
"credentials": "eyJhbGc...",
"publicKey": "MIIBIjANBgkqhkiG9w0...",
"revocationReason": null
}
ValidateTicketResponse
{
"valid": true,
"status": "VALID",
"message": "✅ Entry granted for Day 1 - Opening Day. Welcome!",
"ticketInstanceId": "uuid",
"ticketTypeName": "VIP Pass",
"ticketSeries": "VIP-0001",
"attendeeName": "John Doe",
"attendeeEmail": "john@example.com",
"eventName": "East African Tech Summit 2025",
"bookingReference": "EVT-A3F4B21C",
"alreadyCheckedIn": false,
"previousCheckInTime": null,
"previousCheckInLocation": null,
"currentCheckInTime": "2025-12-15T09:15:00+03:00",
"validationMode": "ONLINE",
"scannerName": "Gate A - Main Entrance",
"dayName": "Day 1 - Opening Day"
}
Endpoints
1. Generate Registration Token
Endpoint: POST /check-in/tokens/generate
Access: 🔒 Event Organizer Only
Request:
{
"eventId": "uuid",
"scannerName": "Gate A - Main Entrance"
}
Success Response: Returns RegistrationTokenResponse
Behavior:
- Validates user is event organizer
- Validates event has RSA keys (must be published)
- Generates token:
REG-{8-UUID}-{8-UUID} - Sets expiry: 5 minutes (configurable)
- Returns QR code data for scanner app
Errors:
403 FORBIDDEN: Not event organizer404 NOT_FOUND: Event not found422: Event not published or no RSA keys
2. Validate Registration Token
Endpoint: GET /check-in/tokens/validate/{token}
Access: 🔓 Public (for scanner apps)
Success Response: Returns RegistrationTokenResponse with validity status
Use Case: Scanner app validates token before registration
3. Register Scanner
Endpoint: POST /check-in/scanners/register
Access: 🔓 Public (uses registration token)
Request:
{
"registrationToken": "REG-ABC12345-XYZ67890",
"deviceFingerprint": "abc123def456hash",
"scannerName": "Gate A - Main Entrance",
"deviceInfo": "{\"model\":\"iPhone 13\",\"os\":\"iOS 15\"}"
}
Success Response: Returns ScannerResponse with credentials
Behavior:
- Validates device fingerprint (10-255 chars)
- Validates token (not used, not expired)
- Auto-revokes any ACTIVE scanner with same device fingerprint
- Generates scanner ID (UUID)
- Generates JWT credentials (1 year validity)
- Marks token as used (one-time use)
Key Rule: One device → One ACTIVE scanner (across all events)
Errors:
400 BAD_REQUEST: Invalid fingerprint, token used/expired404 NOT_FOUND: Token not found
4. Get Scanners for Event
Endpoint: GET /check-in/scanners/event/{eventId}
Access: 🔒 Event Organizer Only
Success Response: Returns array of ScannerResponse
Includes: All scanners (ACTIVE + REVOKED)
5. Get Active Scanners
Endpoint: GET /check-in/scanners/event/{eventId}/active
Access: 🔒 Event Organizer Only
Success Response: Returns array of ScannerResponse (ACTIVE only)
6. Revoke Scanner
Endpoint: POST /check-in/scanners/{scannerId}/revoke?reason=Suspicious activity
Access: 🔒 Event Organizer Only
Success Response: 200 OK with confirmation message
Behavior:
- Changes status to REVOKED (permanent)
- Records revocation reason and timestamp
- Scanner can no longer validate tickets
7. Validate Ticket and Check-In
Endpoint: POST /check-in/validate
Access: 🔒 Scanner credentials required
Request:
{
"jwtToken": "eyJhbGc...",
"scannerId": "uuid-string",
"deviceFingerprint": "abc123def456hash",
"checkInLocation": "Gate A"
}
Success Response: Returns ValidateTicketResponse
Validation Flow:
- Validate Scanner: Active status, fingerprint match
- Verify JWT: RSA signature with event's public key
- Find Current Day: Match current time to event schedules
- Find Booking: Locate ticket in database
- Check Duplicate: Already checked in for this day?
- Create Check-In: Add check-in record for current day
- Update Stats: Increment scanner counters
Check-In Strategies:
- HOURS_BEFORE: X hours before event + late grace period
- SPECIFIC_TIME: Daily window (e.g., 08:00-23:00)
- ALL_DAY: Anytime on event date (00:00-23:59)
- EXACT_TIME: Only during event start-end
- AS_DAY_START: From day start (00:00) until event end + grace
Validation Statuses:
- VALID: ✅ Entry granted
- DUPLICATE: ❌ Already checked in for this day
- INVALID_SIGNATURE: ❌ JWT signature failed
- EXPIRED: ❌ Ticket validity expired
- NOT_FOUND: ❌ Ticket not in database
- REVOKED: ❌ Scanner revoked
Response Examples:
Success (VALID):
{
"success": true,
"message": "✅ Entry granted for Day 1 - Opening Day. Welcome!",
"data": {
"valid": true,
"status": "VALID",
"attendeeName": "John Doe",
"dayName": "Day 1 - Opening Day",
"currentCheckInTime": "2025-12-15T09:15:00+03:00"
}
}
Duplicate Check-In:
{
"success": false,
"message": "❌ Ticket already used for Day 1 - Opening Day. Entry denied.",
"data": {
"valid": false,
"status": "DUPLICATE",
"alreadyCheckedIn": true,
"previousCheckInTime": "2025-12-15T09:00:00+03:00",
"previousCheckInLocation": "Gate A"
}
}
System Flows
Registration Flow
Step 1: Organizer Generates Token
Organizer → POST /tokens/generate
← Returns: Token + QR code data
Step 2: Scanner Scans QR Code
Scanner App → Scans QR code
Extracts: "scannerapp://register?token=REG-..."
Step 3: Scanner Validates Token (Optional)
Scanner App → GET /tokens/validate/{token}
← Confirms: Token valid, event details
Step 4: Scanner Registers
Scanner App → POST /scanners/register
Sends: Token, device fingerprint, name
← Receives: Scanner credentials (JWT)
Step 5: Scanner Stores Credentials
Scanner App → Saves: credentials, publicKey
Ready to validate tickets offline
Ticket Validation Flow
Step 1: Scanner Scans Ticket QR
Scanner → Reads JWT from QR code
Step 2: Offline Validation (Optional)
Scanner → Verifies JWT signature with public key
Checks: Expiry, event match
Step 3: Online Check-In
Scanner → POST /validate
Sends: JWT, scannerId, fingerprint
Step 4: System Validates
System → Validates scanner status
System → Verifies JWT signature
System → Finds current event day
System → Checks duplicate
System → Records check-in
Step 5: Response to Scanner
System → Returns validation result
Scanner → Shows success/error to staff
Multi-Day Event Support
How It Works
Single-Day Event:
- One check-in expected
- Status: ACTIVE → USED after check-in
Multi-Day Event (e.g., 3-day festival):
- Multiple check-ins allowed (one per day)
- Each day tracked separately
- Ticket stays ACTIVE throughout
Example: 3-Day Festival
JWT contains schedules:
{
"eventSchedules": [
{
"dayName": "Day 1 - Friday Night",
"startDateTime": "2025-12-15T18:00:00+03:00",
"endDateTime": "2025-12-15T23:59:00+03:00"
},
{
"dayName": "Day 2 - Saturday",
"startDateTime": "2025-12-16T10:00:00+03:00",
"endDateTime": "2025-12-16T23:59:00+03:00"
},
{
"dayName": "Day 3 - Sunday",
"startDateTime": "2025-12-17T10:00:00+03:00",
"endDateTime": "2025-12-17T20:00:00+03:00"
}
]
}
Check-In Timeline:
Friday 18:30 → Check-in for "Day 1 - Friday Night" ✅
Friday 19:00 → Duplicate for "Day 1" ❌
Saturday 11:00 → Check-in for "Day 2 - Saturday" ✅
Saturday 15:00 → Duplicate for "Day 2" ❌
Sunday 12:00 → Check-in for "Day 3 - Sunday" ✅
Validation Logic:
- System finds current time: Saturday 11:00
- Matches to event schedule: "Day 2 - Saturday"
- Checks if ticket already checked in for "Day 2"
- If no → Allow check-in
- If yes → Reject as duplicate
Check-In Window Strategies
1. HOURS_BEFORE (Default)
Configuration:
- earlyCheckInHours: 2 (default)
- lateCheckInMinutes: 30 (default)
Window: 2 hours before event until 30 minutes after event ends
Example:
Event: 09:00 - 18:00
Check-in allowed: 07:00 - 18:30
Use Case: Conferences, concerts
2. SPECIFIC_TIME
Configuration:
- checkInOpensAt: "08:00"
- checkInClosesAt: "23:00"
Window: Same time window each event day
Example:
3-day event (Dec 15-17)
Check-in allowed: 08:00-23:00 each day
Use Case: Multi-day festivals with consistent entry hours
3. ALL_DAY
Configuration: None needed
Window: Entire event date (00:00 - 23:59)
Example:
Event date: Dec 15
Check-in allowed: Dec 15 00:00 - Dec 15 23:59
Use Case: All-day events, exhibitions
4. EXACT_TIME
Configuration: None
Window: Only during event start-end times
Example:
Event: 14:00 - 17:00
Check-in allowed: 14:00 - 17:00 only
Use Case: Strict timing events
5. AS_DAY_START
Configuration:
- lateCheckInMinutes: 30 (default)
Window: From start of event day (00:00) until event end + grace
Example:
Event: Dec 15 18:00 - 23:00
Check-in allowed: Dec 15 00:00 - 23:30
Use Case: Evening events with early arrival
Security Features
Device Fingerprinting
Purpose: Prevent credential theft
How It Works:
- Scanner generates fingerprint from device hardware
- Fingerprint sent with every request
- System validates fingerprint matches registered device
- Mismatch → Reject (credentials stolen)
Fingerprint Components (example):
const fingerprint = SHA256(
deviceModel +
osVersion +
hardwareId +
appInstallId
);
JWT Credentials
Scanner Credentials (1 year validity):
{
"scannerId": "uuid",
"eventId": "uuid",
"type": "scanner_credential",
"iat": 1702300000,
"exp": 1733836000
}
Signed with: Event's RSA private key
Used for: Scanner authentication to API
Ticket JWT Verification
Process:
- Scanner receives ticket JWT (QR code)
- Scanner verifies signature with event's public key
- Scanner can validate offline (no internet needed)
- Scanner sends to API for check-in recording
Benefits:
- Offline validation capability
- Cannot forge tickets
- Cannot reuse tokens (duplicate tracking)
- Tamper-proof (signature validation)
Auto-Revocation System
The Rule
One device → One ACTIVE scanner at a time (across ALL events)
Scenarios
Scenario 1: Device Registers for Different Event
Device ABC has ACTIVE scanner for Event 1
Device ABC registers for Event 2
→ System revokes Event 1 scanner automatically
→ Creates new scanner for Event 2
Scenario 2: Same Event Re-Registration
Device ABC has ACTIVE scanner for Event 1
Device ABC registers again for Event 1
→ System revokes old scanner
→ Creates new scanner (new credentials)
Revocation Message:
"Automatically revoked: Device registered as new scanner for event 'East African Tech Summit 2025'"
Why This Rule?
Prevents:
- Device scanning tickets for multiple events simultaneously
- Confusion about which event device is working
- Credential misuse across events
Allows:
- Device switching between events (auto-handled)
- Fresh start for each event
- Clean scanner sessions
Scanner Statistics
Auto-Tracked Metrics
- totalScans: All scan attempts
- successfulScans: Valid entries
- failedScans: Duplicates, invalid tickets
- lastScanAt: Most recent scan timestamp
- lastSyncedAt: Last API contact
Success Rate Calculation
successRate = (successfulScans / totalScans) * 100
Updated Automatically
- Each validation attempt updates counters
- Successful → successfulScans++, totalScans++
- Failed → failedScans++, totalScans++
Error Codes Summary
Registration Token Errors
- 400: Token expired/used, invalid format
- 403: Not event organizer
- 404: Token/event not found
- 422: Event not published, no RSA keys
Scanner Registration Errors
- 400: Invalid fingerprint (too short/long)
- 400: Token expired/used
- 404: Token not found
- 422: Invalid scanner name
Ticket Validation Errors
- INVALID_SIGNATURE: JWT signature failed
- DUPLICATE: Already checked in for this day
- EXPIRED: Ticket validity expired
- NOT_FOUND: Ticket not in database
- REVOKED: Scanner revoked
Best Practices
For Organizers
✅ Generate tokens right before scanner setup
✅ Use descriptive scanner names (e.g., "Gate A - Main")
✅ Monitor scanner activity via dashboard
✅ Revoke suspicious scanners immediately
✅ Test scanner before event starts
For Scanner App Developers
✅ Generate stable device fingerprint
✅ Store credentials securely (encrypted)
✅ Implement offline validation first
✅ Sync check-ins when online
✅ Show clear success/error messages
✅ Handle device fingerprint mismatch gracefully
For Event Staff
✅ Keep scanners charged
✅ Verify scanner name matches gate
✅ Watch for duplicate warnings
✅ Report technical issues immediately
✅ Use backup manual verification if needed
Quick Reference
Token Lifetime
- Registration token: 5 minutes
- Scanner credentials: 1 year
- Ticket JWT: Event validity period
Device Fingerprint
- Min length: 10 characters
- Max length: 255 characters
- Format: Stable hash of device properties
Scanner Name
- Min length: 3 characters
- Max length: 200 characters
- Examples: "Gate A", "VIP Entrance", "Main Hall Scanner"
Status Flow
Registration Token: unused → used (one-time)
Scanner: ACTIVE → REVOKED (permanent)
Validation: VALID | DUPLICATE | INVALID_SIGNATURE | EXPIRED | NOT_FOUND | REVOKED
Integration Guide
Organizer Dashboard Integration
// 1. Generate token
POST /check-in/tokens/generate
{
eventId: "uuid",
scannerName: "Gate A"
}
// 2. Display QR code
<QRCode value={response.qrCodeData} />
// 3. Show token expiry countdown
remainingTime = response.remainingSeconds
// 4. List active scanners
GET /check-in/scanners/event/{eventId}/active
Scanner App Integration
// 1. Scan registration QR code
const token = extractTokenFromQR(qrData);
// 2. Register device
POST /check-in/scanners/register
{
registrationToken: token,
deviceFingerprint: generateFingerprint(),
scannerName: "Gate A",
deviceInfo: JSON.stringify(deviceDetails)
}
// 3. Store credentials
secureStorage.save('credentials', response.credentials);
secureStorage.save('publicKey', response.publicKey);
secureStorage.save('scannerId', response.scannerId);
// 4. Validate tickets
while (eventActive) {
const ticketJWT = scanTicketQR();
// Offline validation
const offlineValid = verifyJWT(ticketJWT, publicKey);
if (offlineValid) {
// Online check-in
POST /check-in/validate
{
jwtToken: ticketJWT,
scannerId: scannerId,
deviceFingerprint: generateFingerprint(),
checkInLocation: "Gate A"
}
}
}
Conclusion
The Event Check-In System provides enterprise-grade security with:
✅ WhatsApp-Style Registration: Scan QR to link device
✅ Device Security: Fingerprint validation prevents theft
✅ Offline Capable: JWT verification without internet
✅ Multi-Day Support: Per-day check-in tracking
✅ Auto-Revocation: Clean scanner sessions per event
✅ Flexible Timing: 5 check-in window strategies
✅ Duplicate Prevention: Same-day re-entry blocked
✅ Real-Time Stats: Automatic success/fail tracking
Organizer Analytics API
Base URL: https://api.nexgate.com/api/v1
Short Description: The Organizer Analytics API provides comprehensive financial and performance insights for event organizers. This API enables organizers to track total collections across all events, analyze revenue by event with filters, monitor individual event performance with detailed metrics, and visualize revenue trends over time with monthly/yearly breakdowns. The system calculates escrow holdings, released payments, ticket sales, attendance rates, and sell-out percentages automatically.
Hints:
- Organizer Only: All endpoints restricted to event organizers
- Auto-Calculations: Revenue, attendance, sell-out rates computed automatically
- Escrow Tracking: Separate tracking for held vs released funds
- Multi-Event: Aggregates data across all organizer's events
- Time Filters: Filter by status, date range, year
- Pagination: Event lists paginated (default 20 per page)
- Top Performer: Identifies highest revenue event
- Trend Analysis: Monthly/yearly revenue patterns
- Real-Time: Updates reflect latest bookings and check-ins
Response Structures
CollectionSummaryResponse
{
"eventMetrics": {
"totalEvents": 15,
"upcomingEvents": 5,
"ongoingEvents": 1,
"completedEvents": 8,
"cancelledEvents": 1
},
"collectionMetrics": {
"totalTicketsSold": 2500,
"totalRevenue": 5000000.00,
"inEscrow": 1200000.00,
"released": 3800000.00,
"refunded": 0.00,
"pendingRefunds": 0.00
},
"topEvent": {
"eventId": "uuid",
"eventTitle": "East African Tech Summit 2025",
"revenue": 1500000.00,
"ticketsSold": 500,
"attendanceRate": 92.5
}
}
EventRevenueResponse
{
"events": [
{
"eventId": "uuid",
"eventTitle": "East African Tech Summit 2025",
"eventDate": "2025-12-15T09:00:00",
"status": "PUBLISHED",
"ticketsSold": 500,
"totalRevenue": 1500000.00,
"inEscrow": 1500000.00,
"released": 0.00,
"refunded": 0.00,
"attendanceRate": 0.0,
"totalCapacity": 1000,
"sellOutPercentage": 50.0
}
],
"pagination": {
"currentPage": 0,
"pageSize": 20,
"totalPages": 3,
"totalElements": 45,
"hasNext": true,
"hasPrevious": false
}
}
EventPerformanceResponse
{
"eventId": "uuid",
"eventTitle": "East African Tech Summit 2025",
"eventDate": "2025-12-15T09:00:00",
"status": "COMPLETED",
"financials": {
"totalRevenue": 1500000.00,
"inEscrow": 0.00,
"released": 1500000.00,
"refunded": 0.00,
"averageTicketPrice": 3000.00
},
"ticketMetrics": {
"totalCapacity": 1000,
"totalSold": 500,
"totalRemaining": 500,
"sellOutPercentage": 50.0
},
"attendanceMetrics": {
"totalTickets": 500,
"checkedIn": 462,
"noShows": 38,
"attendanceRate": 92.4
},
"timeline": {
"createdAt": "2025-10-01T10:00:00",
"publishedAt": "2025-10-05T14:30:00",
"firstSaleAt": "2025-10-06T09:15:00",
"eventDate": "2025-12-15T09:00:00",
"completedAt": "2025-12-15T18:00:00"
}
}
RevenueTrendResponse
{
"period": "MONTHLY",
"totalEvents": 12,
"trends": [
{
"label": "JAN",
"year": 2025,
"month": 1,
"eventsCount": 2,
"ticketsSold": 350,
"revenue": 875000.00,
"inEscrow": 0.00,
"released": 875000.00,
"averageAttendanceRate": 88.5,
"averageSellOutRate": 62.0
}
]
}
Endpoints
1. Get Collection Summary
Endpoint: GET /analytics/collections/summary
Access: 🔒 Organizer Only
Success Response: Returns CollectionSummaryResponse
Success Response Message: "Collection summary retrieved"
Behavior:
- Aggregates ALL organizer's events
- Calculates escrow (PUBLISHED, HAPPENING events)
- Calculates released (COMPLETED events)
- Identifies top performer by revenue
- Real-time metrics from latest data
Metrics Included:
- Event counts by status
- Total tickets sold
- Total revenue (all time)
- Money in escrow (upcoming/ongoing)
- Money released (completed)
- Refunds (placeholder)
- Top performing event
2. Get Event Revenue
Endpoint: GET /analytics/collections/by-event?status=PUBLISHED&startDate=2025-01-01&endDate=2025-12-31&page=0&size=20
Access: 🔒 Organizer Only
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| status | string | No | Filter by event status (PUBLISHED, COMPLETED, etc.) |
| startDate | date (ISO) | No | Filter events from this date (2025-01-01) |
| endDate | date (ISO) | No | Filter events until this date (2025-12-31) |
| page | integer | No | Page number (0-indexed, default: 0) |
| size | integer | No | Items per page (default: 20) |
Success Response: Returns EventRevenueResponse with pagination
Success Response Message: "Event revenue retrieved"
Behavior:
- Lists organizer's events with revenue details
- Filters by status and date range
- Sorted by event date (newest first)
- Paginated results
- Includes capacity and sell-out metrics
3. Get Event Performance
Endpoint: GET /analytics/performance/{eventId}
Access: 🔒 Event Organizer Only
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| eventId | string (UUID) | Yes | Event identifier |
Success Response: Returns EventPerformanceResponse
Success Response Message: "Event performance retrieved"
Behavior:
- Validates organizer owns event
- Calculates comprehensive metrics
- Shows financial breakdown
- Displays ticket sales vs capacity
- Tracks attendance from check-ins
- Provides event timeline
Metrics Sections:
- Financials: Revenue, escrow, released, average price
- Tickets: Capacity, sold, remaining, sell-out %
- Attendance: Total, checked-in, no-shows, rate
- Timeline: Key dates from creation to completion
Errors:
403 FORBIDDEN: Not event organizer404 NOT_FOUND: Event not found
4. Get Revenue Trends
Endpoint: GET /analytics/trends?period=MONTHLY&year=2025
Access: 🔒 Organizer Only
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| period | string | No | MONTHLY or YEARLY (default: MONTHLY) |
| year | integer | No | Year to analyze (default: current year) |
Success Response: Returns RevenueTrendResponse
Success Response Message: "Revenue trends retrieved"
Behavior:
MONTHLY Period:
- Shows all 12 months for specified year
- Data per month: events, tickets, revenue, escrow, released
- Calculates average attendance and sell-out rates
YEARLY Period:
- Shows all years organizer has events
- Data per year: total events, tickets, revenue
- Year-over-year comparison
Each Period Includes:
- Event count
- Tickets sold
- Total revenue
- Money in escrow
- Money released
- Average attendance rate
- Average sell-out rate
Key Calculations
Escrow vs Released
In Escrow (Not yet paid to organizer):
- PUBLISHED events (upcoming)
- HAPPENING events (ongoing)
- Formula: Sum of booking totals for non-completed events
Released (Paid to organizer):
- COMPLETED events only
- Formula: Sum of booking totals for completed events
Example:
Event A: PUBLISHED, Revenue: 500,000 TZS → In Escrow
Event B: HAPPENING, Revenue: 300,000 TZS → In Escrow
Event C: COMPLETED, Revenue: 1,200,000 TZS → Released
Total Revenue: 2,000,000 TZS
In Escrow: 800,000 TZS
Released: 1,200,000 TZS
Attendance Rate
Attendance Rate = (Checked-In Tickets / Total Tickets) × 100
Example:
- Total tickets: 500
- Checked-in: 462
- Attendance: 92.4%
Sell-Out Percentage
Sell-Out % = (Tickets Sold / Total Capacity) × 100
Example:
- Capacity: 1000
- Sold: 500
- Sell-out: 50%
Average Ticket Price
Average Price = Total Revenue / Total Tickets
Example:
- Revenue: 1,500,000 TZS
- Tickets: 500
- Average: 3,000 TZS
Event Status Flow
DRAFT → PUBLISHED → HAPPENING → COMPLETED
↓ ↓
CANCELLED (Revenue Released)
Financial Impact by Status:
- DRAFT: No bookings, no revenue
- PUBLISHED: Bookings allowed, revenue in escrow
- HAPPENING: Ongoing, revenue still in escrow
- COMPLETED: Revenue released to organizer
- CANCELLED: Refunds processed (if applicable)
Top Performer Logic
Selection Criteria: Highest total revenue
Metrics Included:
- Event ID and title
- Total revenue (highest wins)
- Tickets sold
- Attendance rate
Use Case: Dashboard highlight showing best-performing event
Use Cases
Dashboard Overview
GET /analytics/collections/summary
Shows:
- Total events across all statuses
- Total revenue (all time)
- Current escrow balance
- Released payments
- Top performing event
Event List with Filters
GET /analytics/collections/by-event?status=COMPLETED&page=0&size=20
Shows:
- All completed events
- Revenue details per event
- Attendance and sell-out rates
- Paginated for easy navigation
Deep Dive on Specific Event
GET /analytics/performance/550e8400-e29b-41d4-a716-446655440000
Shows:
- Complete financial breakdown
- Ticket sales metrics
- Attendance statistics
- Event timeline
Monthly Revenue Analysis
GET /analytics/trends?period=MONTHLY&year=2025
Shows:
- Revenue per month in 2025
- Event count trends
- Attendance patterns
- Sell-out trends
Year-Over-Year Comparison
GET /analytics/trends?period=YEARLY
Shows:
- Revenue by year
- Growth trends
- Performance evolution
Best Practices
For Organizers
✅ Check collection summary regularly
✅ Monitor escrow balance (upcoming payouts)
✅ Track attendance rates to improve future events
✅ Use trends to identify peak seasons
✅ Review individual event performance post-event
For Developers
✅ Cache collection summary (refresh hourly)
✅ Paginate event lists (default 20 items)
✅ Display financial amounts clearly (currency formatting)
✅ Show percentage metrics with 1 decimal (92.4%)
✅ Provide export functionality for trends
Quick Reference
HTTP Status Codes
200 OK: Successful request401 UNAUTHORIZED: Authentication required403 FORBIDDEN: Not event organizer404 NOT_FOUND: Event not found
Date Formats
- Event Date: LocalDateTime (2025-12-15T09:00:00)
- Query Params: ISO Date (2025-01-01)
Currency
- All amounts in TZS (Tanzanian Shilling)
- Format: 1500000.00 (2 decimals)
Percentage Format
- Format: 92.4 (1 decimal)
- Range: 0.0 - 100.0
Pagination
- Zero-indexed pages (0, 1, 2...)
- Default size: 20
- Max size: 100 (recommended)
Conclusion
The Organizer Analytics API provides comprehensive insights with:
✅ Collection Summary: Total revenue, escrow, and top performers
✅ Event Revenue: Filterable, paginated event list
✅ Event Performance: Deep dive into individual event metrics
✅ Revenue Trends: Monthly/yearly pattern analysis
✅ Real-Time: Auto-calculated from latest bookings
✅ Organizer-Focused: Restricted to event owners only
Events Applicant Form API
Base URL: https://your-api-domain.com/api/v1/e-events/applicant-form
Short Description: The Applicant Form API allows event organizers to attach an optional custom multi-page form to their event. When enabled, attendees are prompted to fill in the form as part of the registration flow. The form is fully scoped to the event — all endpoints use eventId as the primary key, so the organizer never needs to track a separate formId. The entire feature is optional; if never enabled, the event registration proceeds with no form step.
Hints:
- All endpoints require a valid Bearer token.
- Form setup and management (enable, pages, fields, options) require the authenticated user to be the event organizer. Any other user gets
403. - Attendee submission endpoints (
/start,/pages/{pageId}/save,/submit,/my-response) require the event to be PUBLISHED and within its registration window (registrationOpensAt→registrationClosesAt). enableFormautomatically creates the underlying form and a default first page — you do not create them separately.startSubmissionis idempotent — if the attendee already has a draft response it returns the existing one instead of creating a new one.savePagesaves answers for a page without advancing the page index — it is a save-in-place operation. Callsubmitwhen all pages are done.- Delete endpoints accept a
?hard=falsequery parameter. Soft delete (hard=false, default) preserves historical response data. Hard delete (hard=true) permanently removes the record. DROPDOWN,RADIO, andCHECKBOXfield types require options to be added separately after the field is created.- The
HEADERfield type is a display-only section divider — it has norequiredflag and stores no answer data.
Form Structure — Layers
┌─────────────────────────────────────────────────────────────────────────┐
│ FORM │
│ title · description · settings · cover page │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ PAGE 1 │ │
│ │ title · description · action button text │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ FIELD (e.g. TEXT, EMAIL, DATE, FILE, RATING …) │ │ │
│ │ │ label · placeholder · required · validation rules │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ FIELD (DROPDOWN / RADIO / CHECKBOX) │ │ │
│ │ │ label · required │ │ │
│ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │
│ │ │ │ Option 1 │ │ Option 2 │ │ Option 3 │ … │ │ │
│ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ FIELD (HEADER — display only, no answer stored) │ │ │
│ │ │ label (section divider text) │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ PAGE 2 │ │
│ │ ┌──────────────────────────┐ ┌──────────────────────────┐ │ │
│ │ │ FIELD │ │ FIELD │ … │ │
│ │ └──────────────────────────┘ └──────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ PAGE N … │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Rules at a glance:
- A form has one or more pages displayed in
displayOrder. - Each page has one or more fields displayed in
displayOrder. - Fields of type
DROPDOWN,RADIO, orCHECKBOXmust have options added after the field is created — no other field type has options. HEADERis the only field type that stores no answer and cannot be marked required. It acts purely as a section label between other fields.- Every other field type (
TEXT,TEXTAREA,EMAIL,PHONE,NUMBER,URL,DATE,TIME,DATETIME,RATING,FILE) stores exactly one answer per attendee.
Organizer Workflow
[Organizer]
│
│ POST /events/{eventId}/enable
▼
┌─────────────────────────────────────┐
│ Form ENABLED │
│ - Form auto-created internally │
│ - Default page auto-created │
│ - Config saved (displayTime etc.) │
└──────────────┬──────────────────────┘
│
│ Build the form structure:
│
│ POST /events/{eventId}/pages
│ POST /events/{eventId}/pages/{pageId}/fields
│ POST /events/{eventId}/fields/{fieldId}/options
│ (DROPDOWN / RADIO / CHECKBOX only)
│
│ (Optional bulk / clone shortcuts)
│ POST /events/{eventId}/pages/bulk
│ POST /events/{eventId}/pages/{pageId}/fields/bulk
│ POST /events/{eventId}/pages/{pageId}/clone
│ POST /events/{eventId}/fields/{fieldId}/clone
│
│ Preview before publishing:
│ GET /events/{eventId}/preview/metadata
│ GET /events/{eventId}/preview/pages/{pageNumber}
│ POST /events/{eventId}/preview/pages/{pageNumber}/validate
▼
┌─────────────────────────────────────┐
│ Form ready — event can publish │
└─────────────────────────────────────┘
Attendee Submission Journey
[Attendee — event must be PUBLISHED and within registration window]
│
│ POST /events/{eventId}/start
▼
┌──────────────────────────────────┐
│ Response created (DRAFT) │ Returns existing draft if one
│ or existing draft returned │ already exists (idempotent)
└──────────────┬───────────────────┘
│
│ For each page:
│ PUT /events/{eventId}/pages/{pageId}/save
│ Body: { fieldId: { value, fileUrl, ... }, ... }
▼
┌──────────────────────────────────┐
│ Page answers saved │ Save is in-place, does NOT
│ (repeat per page) │ advance page index automatically
└──────────────┬───────────────────┘
│
│ POST /events/{eventId}/submit
▼
┌──────────────────────────────────┐
│ Response SUBMITTED │ completionTimeSeconds recorded
└──────────────────────────────────┘
Response Status Flow
DRAFT ──────────────────────────────► SUBMITTED
│ │
│ (withdraw — not in this API) │ (organizer action — outside scope)
▼ ▼
WITHDRAWN UNDER_REVIEW → APPROVED / REJECTED
Standard Response Format
All API responses follow a consistent structure using the Globe Response Builder pattern.
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-02-17T10:30:45",
"data": { }
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2025-02-17T10: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, CREATED, BAD_REQUEST, etc.) |
message |
string | Human-readable description of the 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) — Update / replace - PATCH — Orange (
#fd7e14) — Partial update - DELETE — Red (
#dc3545) — Remove resource
Shared Response Object Definitions
A. ApplicantFormSetupResponse
Returned only by the Enable Form endpoint (Endpoint 1).
| Field | Type | Description |
|---|---|---|
config |
object | The saved EventApplicantFormEntity — see config fields below |
config.id |
UUID | Config record ID |
config.formId |
UUID | Internal Form Builder form ID (not needed for subsequent API calls) |
config.displayTime |
string | BEFORE_CHECKOUT or AFTER_CHECKOUT |
config.isRequiredOnline |
boolean | Whether online buyers must complete the form |
config.applyToAtDoor |
boolean | Whether walk-in attendees see the form |
config.createdAt |
ZonedDateTime | Config creation timestamp |
config.updatedAt |
ZonedDateTime | Last update timestamp |
form |
object | Full FormResponse — see FormResponse definition below |
B. EventApplicantFormEntity (Config Object)
Returned by the Update Settings endpoint.
| Field | Type | Description |
|---|---|---|
id |
UUID | Config record ID |
formId |
UUID | Internal Form Builder form ID |
displayTime |
string | BEFORE_CHECKOUT or AFTER_CHECKOUT |
isRequiredOnline |
boolean | Whether online buyers must complete the form |
applyToAtDoor |
boolean | Whether walk-in attendees see the form |
createdAt |
ZonedDateTime | ISO 8601 with offset |
updatedAt |
ZonedDateTime | ISO 8601 with offset |
C. FormResponse (Full Form Object)
| Field | Type | Description |
|---|---|---|
formId |
UUID | Form identifier (Form Builder internal ID) |
title |
string | Auto-set to "Attendee Questions - {event title}" on enable |
description |
string | Form description |
settings |
object | Form open/close settings (see FormSettings below) |
settings.acceptResponses |
boolean | Whether the form is accepting responses |
settings.allowMultipleSubmissions |
boolean | Whether a user can submit more than once |
settings.responseStartTime |
Instant | UTC datetime when responses open |
settings.responseDeadline |
Instant | UTC datetime when responses close |
settings.allowSaveDraft |
boolean | Whether respondents can save progress |
coverPage |
object | Optional cover page config |
createdBy |
string | Username of creator |
createdAt |
LocalDateTime | Creation timestamp |
pages[] |
array | Ordered list of PageResponse objects |
D. PageResponse
| Field | Type | Description |
|---|---|---|
pageId |
UUID | Page identifier |
title |
string | Page heading shown to attendee |
description |
string | Subheading or instruction text |
displayOrder |
integer | 1-based position within the form |
actionButtonText |
string | Label for the next/submit button |
fields[] |
array | Ordered list of FieldResponse objects (soft-deleted excluded) |
E. FieldResponse
| Field | Type | Description |
|---|---|---|
fieldId |
UUID | Field identifier |
type |
string | TEXT, TEXTAREA, EMAIL, PHONE, NUMBER, URL, DATE, TIME, DATETIME, DROPDOWN, RADIO, CHECKBOX, FILE, RATING, HEADER |
label |
string | Field label. For HEADER type this is the section title |
description |
string | Helper text shown below the field |
placeholder |
string | Input placeholder text |
displayOrder |
integer | 1-based order within the page |
required |
boolean | Whether the field must be answered. Always false for HEADER |
validation |
object | See FieldValidation below |
options[] |
array | OptionResponse entries — only for DROPDOWN, RADIO, CHECKBOX |
FieldValidation Object
| Field | Applicable To | Description |
|---|---|---|
minLength / maxLength |
TEXT, TEXTAREA |
Character count constraints |
pattern / patternMessage |
TEXT |
Regex pattern and custom error message |
min / max |
NUMBER |
Numeric range |
minDate / maxDate |
DATE |
Date range (YYYY-MM-DD) |
minSelections / maxSelections |
CHECKBOX |
Selection count constraints |
maxSizeMb / accept |
FILE |
File size limit and accepted MIME types |
F. OptionResponse
| Field | Type | Description |
|---|---|---|
optionId |
UUID | Option identifier |
label |
string | Display label shown to attendee |
displayOrder |
integer | 1-based order within the field |
G. FormResponseObject (Attendee Submission)
| Field | Type | Description |
|---|---|---|
responseId |
UUID | Unique response identifier |
formId |
UUID | Internal form ID |
submittedBy |
string | Username of the attendee |
status |
string | DRAFT, SUBMITTED, UNDER_REVIEW, APPROVED, REJECTED, WITHDRAWN |
completedPageIds |
array | UUIDs of pages marked complete |
currentPageIndex |
integer | 0-based index of current page |
startedAt |
LocalDateTime | When the attendee started the form |
submittedAt |
LocalDateTime | When the response was submitted |
completionTimeSeconds |
integer | Total seconds from start to submit |
answers[] |
array | See AnswerResponse below |
AnswerResponse
| Field | Type | Description |
|---|---|---|
answerId |
UUID | Answer identifier |
fieldId |
UUID | Field ID (null if field was hard-deleted) |
fieldLabel |
string | Label snapshot — preserved even if field is later deleted |
fieldType |
string | Type snapshot — preserved even if field is later deleted |
fieldDeleted |
boolean | true when the source field has been soft-deleted |
value |
any | Submitted value. String for text/single-choice, array of strings for checkbox, number for NUMBER/RATING |
answeredAt |
LocalDateTime | When this answer was saved |
fileUrl / fileName / fileSize / fileType |
various | File upload metadata (FILE fields only) |
H. BulkOperationResult
| Field | Type | Description |
|---|---|---|
successCount |
integer | Number of items successfully processed |
failureCount |
integer | Number of items that failed |
errors[] |
array | Error message per failed item |
createdFields[] |
array | FieldResponse objects (bulk field operations) |
createdPages[] |
array | PageResponse objects (bulk page operations) |
I. BulkDeleteResult
| Field | Type | Description |
|---|---|---|
successCount |
integer | Number of items successfully deleted |
failureCount |
integer | Number of items that failed |
errors[] |
array | Error message per failed item |
deletedIds[] |
array | UUIDs of successfully deleted items |
J. FormAnalytics
| Field | Type | Description |
|---|---|---|
formId |
UUID | Form identifier |
formTitle |
string | Form title |
stats.totalStarted |
integer | Total responses started (including drafts) |
stats.totalDrafts |
integer | Responses still in DRAFT |
stats.totalSubmitted |
integer | Responses with SUBMITTED status |
stats.totalWithdrawn |
integer | Withdrawn responses |
stats.completionRate |
double | Submitted / totalStarted × 100 |
stats.dropOffRate |
double | Inverse of completion rate |
stats.avgCompletionTimeSeconds |
double | Mean time from start to submit |
stats.fastestTimeSeconds |
integer | Fastest submission time |
stats.slowestTimeSeconds |
integer | Slowest submission time |
fieldAnalytics[] |
array | Per-field breakdown |
fieldAnalytics[].fieldId |
UUID | Field ID |
fieldAnalytics[].fieldLabel |
string | Field label snapshot |
fieldAnalytics[].fieldType |
string | Field type snapshot |
fieldAnalytics[].fieldDeleted |
boolean | Whether field is soft-deleted |
fieldAnalytics[].totalResponses |
integer | Total answers for this field |
fieldAnalytics[].choiceDistribution[] |
array | { option, count, percentage } — choice fields |
fieldAnalytics[].numericStats |
object | { min, max, avg, median } — NUMBER/RATING fields |
fieldAnalytics[].textResponses[] |
array | Raw text answers — TEXT/TEXTAREA fields |
dailySubmissions[] |
array | { date, count } — submissions per day |
Standard Error Types
Application-Level Exceptions (400–499)
400 BAD_REQUEST— Form already enabled for this event, field type does not support options, already submitted401 UNAUTHORIZED— Missing, expired, or malformed Bearer token403 FORBIDDEN— Authenticated but not the event organizer; or registration window not open/already closed404 NOT_FOUND— Event not found, form not enabled, page/field/option not found, no response found422 UNPROCESSABLE_ENTITY— Bean validation failures, required field missing on save
Server-Level Exceptions (500+)
500 INTERNAL_SERVER_ERROR— Unexpected runtime error
Shared Error Response Examples
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Token has expired",
"action_time": "2025-02-17T10:30:45",
"data": "Token has expired"
}
403 — Forbidden (not organizer):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "Only organizer can manage form",
"action_time": "2025-02-17T10:30:45",
"data": "Only organizer can manage form"
}
403 — Forbidden (registration closed):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "Registration closed",
"action_time": "2025-02-17T10:30:45",
"data": "Registration closed"
}
404 — Form not enabled:
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Form not enabled",
"action_time": "2025-02-17T10:30:45",
"data": "Form not enabled"
}
Endpoints
Form Setup
1. Enable Applicant Form
Purpose: Enables the applicant form for an event. Internally creates a Form Builder form titled "Attendee Questions - {event title}" and an initial default page titled "Attendee Information". Only the event organizer can call this. Returns the full form structure alongside the config.
Endpoint: POST /api/v1/e-events/applicant-form/events/{eventId}/enable
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 enable the form for | Must be a valid UUID |
Request JSON Sample:
{
"displayTime": "BEFORE_CHECKOUT",
"isRequiredOnline": true,
"applyToAtDoor": false
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
displayTime |
string | No | When attendees see the form in the UI flow | Enum: BEFORE_CHECKOUT, AFTER_CHECKOUT. Defaults to BEFORE_CHECKOUT |
isRequiredOnline |
boolean | No | Whether online ticket buyers must complete the form before proceeding | Defaults to false |
applyToAtDoor |
boolean | No | Whether at-door / walk-in attendees also see the form | Defaults to false |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "CREATED",
"message": "Applicant form enabled with default page",
"action_time": "2025-02-17T10:30:45",
"data": {
"config": {
"id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"formId": "9f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"displayTime": "BEFORE_CHECKOUT",
"isRequiredOnline": true,
"applyToAtDoor": false,
"createdAt": "2025-02-17T10:30:45+03:00",
"updatedAt": null
},
"form": {
"formId": "9f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"title": "Attendee Questions - Dar es Salaam Jazz Festival 2025",
"description": null,
"settings": null,
"coverPage": null,
"createdBy": "amina.hassan",
"createdAt": "2025-02-17T10:30:45",
"pages": [
{
"pageId": "1a2b3c4d-...",
"title": "Attendee Information",
"description": "Please provide your details",
"displayOrder": 1,
"actionButtonText": "Next",
"fields": []
}
]
}
}
}
Success Response Fields: data is an ApplicantFormSetupResponse.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer, or form is already enabled for this event |
404 |
Event not found |
2. Update Form Settings
Purpose: Updates the form configuration for the event — displayTime, isRequiredOnline, and applyToAtDoor. Does not affect the form structure (pages/fields). All fields are optional.
Endpoint: PUT /api/v1/e-events/applicant-form/events/{eventId}/settings
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 |
Request JSON Sample:
{
"displayTime": "AFTER_CHECKOUT",
"isRequiredOnline": false,
"applyToAtDoor": true
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
displayTime |
string | No | Updated display time | Enum: BEFORE_CHECKOUT, AFTER_CHECKOUT |
isRequiredOnline |
boolean | No | Updated required flag for online buyers | — |
applyToAtDoor |
boolean | No | Updated at-door flag | — |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Form settings updated",
"action_time": "2025-02-17T10:30:45",
"data": {
"id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"formId": "9f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"displayTime": "AFTER_CHECKOUT",
"isRequiredOnline": false,
"applyToAtDoor": true,
"createdAt": "2025-02-17T10:30:45+03:00",
"updatedAt": "2025-02-17T11:00:00+03:00"
}
}
Success Response Fields: data is an EventApplicantFormEntity config object.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or form not enabled |
3. Disable Form
Purpose: Disables and permanently removes the applicant form from the event. The underlying Form Builder form is soft-deleted. Existing submitted responses are preserved. This action cannot be undone — to re-enable a form, call Endpoint 1 again which will create a fresh form.
Endpoint: DELETE /api/v1/e-events/applicant-form/events/{eventId}/disable
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 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Applicant form disabled",
"action_time": "2025-02-17T10:30:45",
"data": null
}
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or form not enabled |
Page Management
4. Add Page
Purpose: Appends a new blank page to the form. displayOrder is auto-assigned as the next position. Fields are added to the page separately.
Endpoint: POST /api/v1/e-events/applicant-form/events/{eventId}/pages
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 |
Request JSON Sample:
{
"title": "Emergency Contact",
"description": "In case we need to reach someone on your behalf.",
"actionButtonText": "Next"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
title |
string | Yes | Page heading shown to attendee | Max: 255 characters |
description |
string | No | Subheading or instruction text | Max: 500 characters |
actionButtonText |
string | No | Label for the page's action button | Max: 50 characters |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "CREATED",
"message": "Page added",
"action_time": "2025-02-17T10:30:45",
"data": {
"pageId": "2b3c4d5e-f6a7-8b9c-0d1e-2f3a4b5c6d7e",
"title": "Emergency Contact",
"description": "In case we need to reach someone on your behalf.",
"displayOrder": 2,
"actionButtonText": "Next",
"fields": []
}
}
Success Response Fields: data is a PageResponse.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or form not enabled |
422 |
title is blank |
5. Update Page
Purpose: Updates the title, description, or action button label of an existing page.
Endpoint: PUT /api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}
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 |
pageId |
UUID | Yes | The page to update | Must be a valid UUID |
Request JSON Sample:
{
"title": "Your Background",
"actionButtonText": "Continue"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
title |
string | No | Updated page title | Max: 255 characters |
description |
string | No | Updated description | Max: 500 characters |
actionButtonText |
string | No | Updated button label | Max: 50 characters |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Page updated",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "PageResponse object" }
}
Success Response Fields: data is a PageResponse.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or page not found |
6. Delete Page
Purpose: Deletes a page. Pass ?hard=false (default) for soft delete — the page is hidden but historical answer data is preserved. Pass ?hard=true to permanently remove the page and all its fields.
Endpoint: DELETE /api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}
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 |
pageId |
UUID | Yes | The page to delete | Must be a valid UUID |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
hard |
boolean | No | true = permanent delete, false = soft delete |
— | false |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Page deleted",
"action_time": "2025-02-17T10:30:45",
"data": null
}
When
hard=true,messageis"Page permanently deleted".
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or page not found |
7. Bulk Add Pages (with Fields)
Purpose: Creates multiple pages in one call. Each page can optionally include an inline list of fields.
Endpoint: POST /api/v1/e-events/applicant-form/events/{eventId}/pages/bulk
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 |
Request JSON Sample:
{
"pages": [
{
"title": "Personal Details",
"actionButtonText": "Next",
"fields": [
{ "type": "TEXT", "label": "Full Name", "required": true },
{ "type": "EMAIL", "label": "Email", "required": true }
]
},
{
"title": "Dietary Requirements",
"actionButtonText": "Submit",
"fields": [
{ "type": "DROPDOWN", "label": "Dietary preference", "required": false }
]
}
]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
pages |
array | Yes | Pages to create | Min 1 item |
pages[].title |
string | Yes | Page title | Max: 255 characters |
pages[].description |
string | No | Page description | Max: 500 characters |
pages[].actionButtonText |
string | No | Button label | Max: 50 characters |
pages[].fields |
array | No | Fields to create inline | See CreateField below |
pages[].fields[].type |
string | Yes | Field type | See FieldType enum |
pages[].fields[].label |
string | Yes | Field label | Max: 255 characters |
pages[].fields[].description |
string | No | Helper text | Max: 500 characters |
pages[].fields[].placeholder |
string | No | Placeholder text | Max: 255 characters |
pages[].fields[].required |
boolean | No | Required flag | Defaults to false |
pages[].fields[].validation |
object | No | Validation rules | See FieldValidation |
Note: For
DROPDOWN,RADIO, andCHECKBOXfields created via bulk, options must still be added separately using the option endpoints after this call.
Success Response JSON Sample:
{
"success": true,
"httpStatus": "CREATED",
"message": "2 pages added, 0 failed",
"action_time": "2025-02-17T10:30:45",
"data": {
"successCount": 2,
"failureCount": 0,
"errors": [],
"createdPages": [
{ "...": "PageResponse for page 1" },
{ "...": "PageResponse for page 2" }
]
}
}
Success Response Fields: data is a BulkOperationResult.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or form not enabled |
8. Bulk Delete Pages
Purpose: Deletes multiple pages in one call. Supports both soft delete (default) and hard delete via ?hard query param. Items are processed individually — a failure on one does not stop the rest.
Endpoint: DELETE /api/v1/e-events/applicant-form/events/{eventId}/pages/bulk
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 |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
hard |
boolean | No | true = permanent delete |
— | false |
Request JSON Sample:
{
"ids": ["pageId-1", "pageId-2"]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
ids |
array | Yes | UUIDs of pages to delete | Min 1 item |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "2 pages deleted, 0 failed",
"action_time": "2025-02-17T10:30:45",
"data": {
"successCount": 2,
"failureCount": 0,
"errors": [],
"deletedIds": ["pageId-1", "pageId-2"]
}
}
Success Response Fields: data is a BulkDeleteResult.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found. Individual page errors reported in data.errors[] |
9. Clone Page
Purpose: Creates an exact copy of an existing page including all its active fields and options. The clone is appended at the end of the form with "(Copy)" added to the title.
Endpoint: POST /api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}/clone
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 |
pageId |
UUID | Yes | The page to clone | Must be a valid UUID |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "CREATED",
"message": "Page cloned successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "PageResponse for the cloned page" }
}
Success Response Fields: data is a PageResponse for the new clone.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or page not found |
Field Management
10. Add Field
Purpose: Adds a single field to a page. displayOrder is auto-assigned. For DROPDOWN, RADIO, and CHECKBOX types, options can be passed inline in the same request or added separately via the Add Option endpoint.
Endpoint: POST /api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}/fields
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 |
pageId |
UUID | Yes | The page to add the field to | Must be a valid UUID |
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
type |
string | Yes | Field type | Enum: TEXT, TEXTAREA, EMAIL, PHONE, NUMBER, URL, DATE, TIME, DATETIME, DROPDOWN, RADIO, CHECKBOX, FILE, RATING, HEADER |
label |
string | Yes | Field label (or section title for HEADER) |
Max: 255 characters |
description |
string | No | Helper text | Max: 500 characters |
placeholder |
string | No | Input placeholder | Max: 255 characters |
required |
boolean | No | Required flag. Forced false for HEADER |
Defaults to false |
validation |
object | No | Validation rules | See FieldValidation |
options |
array | No | Inline options for DROPDOWN, RADIO, CHECKBOX. Ignored for other types. Blank labels are skipped |
— |
options[].label |
string | Yes (if options provided) | Option label | Max: 255 characters |
Example 1 — TEXT field with validation
Request:
{
"type": "TEXT",
"label": "Full Name",
"description": "As it appears on your ID.",
"placeholder": "e.g. Amina Hassan",
"required": true,
"validation": {
"minLength": 2,
"maxLength": 100
}
}
Response:
{
"success": true,
"httpStatus": "CREATED",
"message": "Field added",
"action_time": "2025-02-17T10:30:45",
"data": {
"fieldId": "c3d4e5f6-a7b8-9c0d-1e2f-3a4b5c6d7e8f",
"type": "TEXT",
"label": "Full Name",
"description": "As it appears on your ID.",
"placeholder": "e.g. Amina Hassan",
"displayOrder": 1,
"required": true,
"validation": { "minLength": 2, "maxLength": 100 },
"options": []
}
}
Example 2 — DROPDOWN field with inline options
Request:
{
"type": "DROPDOWN",
"label": "T-Shirt Size",
"description": "Select your preferred size.",
"placeholder": "Choose a size",
"required": true,
"options": [
{ "label": "Small (S)" },
{ "label": "Medium (M)" },
{ "label": "Large (L)" },
{ "label": "Extra Large (XL)" }
]
}
Response:
{
"success": true,
"httpStatus": "CREATED",
"message": "Field added",
"action_time": "2025-02-17T10:30:45",
"data": {
"fieldId": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a",
"type": "DROPDOWN",
"label": "T-Shirt Size",
"description": "Select your preferred size.",
"placeholder": "Choose a size",
"displayOrder": 2,
"required": true,
"validation": null,
"options": [
{ "optionId": "a1b2c3d4-...", "label": "Small (S)", "displayOrder": 1 },
{ "optionId": "b2c3d4e5-...", "label": "Medium (M)", "displayOrder": 2 },
{ "optionId": "c3d4e5f6-...", "label": "Large (L)", "displayOrder": 3 },
{ "optionId": "d4e5f6a7-...", "label": "Extra Large (XL)", "displayOrder": 4 }
]
}
}
Example 3 — RADIO field with inline options
Request:
{
"type": "RADIO",
"label": "What is your attendance mode?",
"description": "Select how you will attend the event.",
"required": true,
"options": [
{ "label": "In Person" },
{ "label": "Online" }
]
}
Response:
{
"success": true,
"httpStatus": "CREATED",
"message": "Field added",
"action_time": "2025-02-17T10:30:45",
"data": {
"fieldId": "e5f6a7b8-c9d0-1e2f-3a4b-5c6d7e8f9a0b",
"type": "RADIO",
"label": "What is your attendance mode?",
"description": "Select how you will attend the event.",
"placeholder": null,
"displayOrder": 3,
"required": true,
"validation": null,
"options": [
{ "optionId": "e5f6a7b8-...", "label": "In Person", "displayOrder": 1 },
{ "optionId": "f6a7b8c9-...", "label": "Online", "displayOrder": 2 }
]
}
}
Example 4 — CHECKBOX field with inline options and validation
Request:
{
"type": "CHECKBOX",
"label": "Which sessions will you attend?",
"description": "Select all that apply. You may choose up to 3.",
"required": false,
"validation": {
"minSelections": 1,
"maxSelections": 3
},
"options": [
{ "label": "Morning Session (9am - 12pm)" },
{ "label": "Afternoon Session (1pm - 4pm)" },
{ "label": "Evening Session (6pm - 9pm)" }
]
}
Response:
{
"success": true,
"httpStatus": "CREATED",
"message": "Field added",
"action_time": "2025-02-17T10:30:45",
"data": {
"fieldId": "f6a7b8c9-d0e1-2f3a-4b5c-6d7e8f9a0b1c",
"type": "CHECKBOX",
"label": "Which sessions will you attend?",
"description": "Select all that apply. You may choose up to 3.",
"placeholder": null,
"displayOrder": 4,
"required": false,
"validation": { "minSelections": 1, "maxSelections": 3 },
"options": [
{ "optionId": "g7a8b9c0-...", "label": "Morning Session (9am - 12pm)", "displayOrder": 1 },
{ "optionId": "h8b9c0d1-...", "label": "Afternoon Session (1pm - 4pm)", "displayOrder": 2 },
{ "optionId": "i9c0d1e2-...", "label": "Evening Session (6pm - 9pm)", "displayOrder": 3 }
]
}
}
Example 5 — HEADER field (section divider)
Request:
{
"type": "HEADER",
"label": "Emergency Contact Information"
}
Response:
{
"success": true,
"httpStatus": "CREATED",
"message": "Field added",
"action_time": "2025-02-17T10:30:45",
"data": {
"fieldId": "a0b1c2d3-e4f5-6a7b-8c9d-0e1f2a3b4c5d",
"type": "HEADER",
"label": "Emergency Contact Information",
"description": null,
"placeholder": null,
"displayOrder": 5,
"required": false,
"validation": null,
"options": []
}
}
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or page not found |
422 |
type is null, or label is blank |
11. Update Field
Purpose: Updates the metadata of an existing field. All fields are optional. Field type cannot be changed — if a different type is needed, delete the field and create a new one. Options are managed via dedicated option endpoints.
Endpoint: PUT /api/v1/e-events/applicant-form/events/{eventId}/fields/{fieldId}
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 |
fieldId |
UUID | Yes | The field to update | Must be a valid UUID |
Request JSON Sample:
{
"label": "Legal Full Name",
"required": true,
"validation": {
"minLength": 3,
"maxLength": 150
}
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
label |
string | No | Updated label | Max: 255 characters |
description |
string | No | Updated helper text | Max: 500 characters |
placeholder |
string | No | Updated placeholder | Max: 255 characters |
required |
boolean | No | Updated required flag. Forced false if type is HEADER |
— |
validation |
object | No | Updated validation rules (full replacement) | See FieldValidation |
⚠️ Note:
typecannot be changed via this endpoint. Attempting to pass a differenttypewill return a400error with the message:"Field type cannot be changed. Delete this field and create a new one with the correct type."To change the field type, delete this field and create a new one.
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Field updated",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "FieldResponse object" }
}
Success Response Fields: data is a FieldResponse.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or field not found |
400 |
Attempting to change field type |
12. Delete Field
Purpose: Deletes a field. Soft delete (default) preserves historical answer snapshots. Hard delete permanently removes the field record.
Endpoint: DELETE /api/v1/e-events/applicant-form/events/{eventId}/fields/{fieldId}
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 |
fieldId |
UUID | Yes | The field to delete | Must be a valid UUID |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
hard |
boolean | No | true = permanent delete |
— | false |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Field deleted",
"action_time": "2025-02-17T10:30:45",
"data": null
}
When
hard=true,messageis"Field permanently deleted".
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or field not found |
13. Bulk Add Fields
Purpose: Adds multiple fields to a page in one call. Fields are appended after existing fields in array order. For DROPDOWN, RADIO, and CHECKBOX types, options can be passed inline per field.
Endpoint: POST /api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}/fields/bulk
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 |
pageId |
UUID | Yes | The page to add fields to | Must be a valid UUID |
Request JSON Sample:
{
"fields": [
{ "type": "EMAIL", "label": "Email Address", "required": true },
{ "type": "PHONE", "label": "Phone Number", "required": false },
{ "type": "HEADER", "label": "Travel Details" },
{
"type": "DROPDOWN",
"label": "Arrival Method",
"required": true,
"options": [
{ "label": "Flight" },
{ "label": "Bus" },
{ "label": "Car" }
]
}
]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
fields |
array | Yes | Fields to create | Min 1 item |
fields[].type |
string | Yes | Field type | See FieldType enum |
fields[].label |
string | Yes | Field label | Max: 255 characters |
fields[].description |
string | No | Helper text | Max: 500 characters |
fields[].placeholder |
string | No | Placeholder text | Max: 255 characters |
fields[].required |
boolean | No | Required flag | Defaults to false |
fields[].validation |
object | No | Validation rules | See FieldValidation |
fields[].options |
array | No | Inline options for DROPDOWN, RADIO, CHECKBOX. Ignored for other types. Blank labels are skipped |
— |
fields[].options[].label |
string | Yes (if options provided) | Option label | Max: 255 characters |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "CREATED",
"message": "4 fields added, 0 failed",
"action_time": "2025-02-17T10:30:45",
"data": {
"successCount": 4,
"failureCount": 0,
"errors": [],
"createdFields": [ "[ FieldResponse objects ]" ]
}
}
Success Response Fields: data is a BulkOperationResult.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or page not found |
14. Bulk Delete Fields
Purpose: Deletes multiple fields from a page in one call. Supports soft and hard delete via ?hard.
Endpoint: DELETE /api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}/fields/bulk
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 |
pageId |
UUID | Yes | The page containing the fields | Must be a valid UUID |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
hard |
boolean | No | true = permanent delete |
— | false |
Request JSON Sample:
{
"ids": ["fieldId-1", "fieldId-2"]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
ids |
array | Yes | UUIDs of fields to delete | Min 1 item |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "2 fields deleted, 0 failed",
"action_time": "2025-02-17T10:30:45",
"data": {
"successCount": 2,
"failureCount": 0,
"errors": [],
"deletedIds": ["fieldId-1", "fieldId-2"]
}
}
Success Response Fields: data is a BulkDeleteResult.
Possible Error Responses: Same as Endpoint 8 (401, 403, 404).
15. Bulk Update Fields
Purpose: Updates required flag and/or validation rules across multiple fields at once.
Endpoint: PATCH /api/v1/e-events/applicant-form/events/{eventId}/fields/bulk
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 |
Request JSON Sample:
{
"fieldIds": ["fieldId-1", "fieldId-2"],
"required": true,
"validation": { "maxLength": 200 }
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
fieldIds |
array | Yes | UUIDs of fields to update | Min 1 item |
required |
boolean | No | New required value for all listed fields | — |
validation |
object | No | New validation rules for all listed fields (full replacement) | See FieldValidation |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "2 fields updated, 0 failed",
"action_time": "2025-02-17T10:30:45",
"data": {
"successCount": 2,
"failureCount": 0,
"errors": [],
"createdFields": [ "[ Updated FieldResponse objects ]" ]
}
}
Success Response Fields: data is a BulkOperationResult.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
One or more field IDs not found — reported in data.errors[] |
16. Clone Field
Purpose: Creates a copy of a field (with its options) and appends it to a target page. Label gets "(Copy)" appended.
Endpoint: POST /api/v1/e-events/applicant-form/events/{eventId}/fields/{fieldId}/clone
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 |
fieldId |
UUID | Yes | The field to clone | Must be a valid UUID |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
targetPageId |
UUID | Yes | The page to clone the field into | Must be a valid UUID | — |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "CREATED",
"message": "Field cloned successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "FieldResponse for the cloned field" }
}
Success Response Fields: data is a FieldResponse for the new clone.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found, field not found, or target page not found |
Option Management
17. Add Option
Purpose: Adds a choice option to a DROPDOWN, RADIO, or CHECKBOX field. Calling this on any other field type returns 400.
Endpoint: POST /api/v1/e-events/applicant-form/events/{eventId}/fields/{fieldId}/options
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 |
fieldId |
UUID | Yes | The field to add an option to | Must be DROPDOWN, RADIO, or CHECKBOX type |
Request JSON Sample:
{
"label": "By Car"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
label |
string | Yes | Display label for this option | Max: 255 characters |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "CREATED",
"message": "Option added",
"action_time": "2025-02-17T10:30:45",
"data": {
"optionId": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a",
"label": "By Car",
"displayOrder": 1
}
}
Success Response Fields: data is an OptionResponse.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
400 |
Field type does not support options |
403 |
Not the event organizer |
404 |
Event not found or field not found |
422 |
label is blank |
18. Update Option
Purpose: Updates the label of an existing option.
Endpoint: PUT /api/v1/e-events/applicant-form/events/{eventId}/options/{optionId}
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 |
optionId |
UUID | Yes | The option to update | Must be a valid UUID |
Request JSON Sample:
{
"label": "Public Transport"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
label |
string | No | Updated label | Max: 255 characters |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Option updated",
"action_time": "2025-02-17T10:30:45",
"data": {
"optionId": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a",
"label": "Public Transport",
"displayOrder": 1
}
}
Success Response Fields: data is an OptionResponse.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or option not found |
19. Delete Option
Purpose: Deletes an option. Soft delete (default) preserves previously selected answer values. Hard delete permanently removes the option record.
Endpoint: DELETE /api/v1/e-events/applicant-form/events/{eventId}/options/{optionId}
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 |
optionId |
UUID | Yes | The option to delete | Must be a valid UUID |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
hard |
boolean | No | true = permanent delete |
— | false |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Option deleted",
"action_time": "2025-02-17T10:30:45",
"data": null
}
When
hard=true,messageis"Option permanently deleted".
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or option not found |
20. Bulk Delete Options
Purpose: Deletes multiple options from a field in one call. Supports soft and hard delete via ?hard.
Endpoint: DELETE /api/v1/e-events/applicant-form/events/{eventId}/fields/{fieldId}/options/bulk
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 |
fieldId |
UUID | Yes | The field containing the options | Must be a valid UUID |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
hard |
boolean | No | true = permanent delete |
— | false |
Request JSON Sample:
{
"ids": ["optionId-1", "optionId-2"]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
ids |
array | Yes | UUIDs of options to delete | Min 1 item |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "2 options deleted, 0 failed",
"action_time": "2025-02-17T10:30:45",
"data": {
"successCount": 2,
"failureCount": 0,
"errors": [],
"deletedIds": ["optionId-1", "optionId-2"]
}
}
Success Response Fields: data is a BulkDeleteResult.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or field not found. Individual errors in data.errors[] |
21. Bulk Update Options
Purpose: Updates the labels of multiple options in one call.
Endpoint: PATCH /api/v1/e-events/applicant-form/events/{eventId}/options/bulk
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 |
Request JSON Sample:
{
"options": [
{ "optionId": "optionId-1", "label": "Train" },
{ "optionId": "optionId-2", "label": "Bus" }
]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
options |
array | Yes | List of option updates | Min 1 item |
options[].optionId |
UUID | Yes | The option to update | Must be a valid UUID |
options[].label |
string | No | New label | Max: 255 characters |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "2 options updated, 0 failed",
"action_time": "2025-02-17T10:30:45",
"data": {
"successCount": 2,
"failureCount": 0,
"errors": []
}
}
Success Response Fields: data is a BulkOperationResult.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
An option ID not found — reported in data.errors[] |
Preview
22. Get Preview Metadata
Purpose: Returns a lightweight summary of the form structure — total pages, page numbers, and titles — without loading full field data. Used to render a progress indicator before the attendee starts filling.
Endpoint: GET /api/v1/e-events/applicant-form/events/{eventId}/preview/metadata
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 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Metadata retrieved",
"data": {
"formId": "uuid",
"formTitle": "Attendee Questions - Tech Summit 2025",
"totalPages": 3,
"pageNumbers": [
{ "pageNumber": 1, "pageTitle": "Personal Info" },
{ "pageNumber": 2, "pageTitle": "Experience" },
{ "pageNumber": 3, "pageTitle": "Preferences" }
],
"responseId": "uuid",
"responseStatus": "DRAFT",
"currentPageIndex": 1,
"completedPageIds": ["page-uuid-1"]
}
}
Success Response Fields:
| Field | Description |
|---|---|
formId |
Internal form ID |
formTitle |
Form title |
totalPages |
Total active pages |
pageNumbers[].pageNumber |
1-based page number |
pageNumbers[].pageTitle |
Page heading |
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or form not enabled |
23. Preview Page by Number
Purpose: Returns a specific page by its 1-based position number, including all its active fields and options. Allows the organizer to preview what attendees will see before publishing.
Endpoint: GET /api/v1/e-events/applicant-form/events/{eventId}/preview/pages/{pageNumber}
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 |
pageNumber |
integer | Yes | 1-based page position | Must be ≥ 1 and ≤ total active pages |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Preview page retrieved",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "PageResponse object" }
}
Success Response Fields: data is a PageResponse.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found, form not enabled, or pageNumber out of range |
24. Validate Preview Page Answers
Purpose: Runs validation on a set of answers for a specific page number without creating or saving any data. Lets the organizer test field validation rules before the form goes live.
Endpoint: POST /api/v1/e-events/applicant-form/events/{eventId}/preview/pages/{pageNumber}/validate
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 |
pageNumber |
integer | Yes | 1-based page number to validate against | Must be ≥ 1 and ≤ total active pages |
Request JSON Sample:
{
"fieldId-fullname": { "value": "" },
"fieldId-email": { "value": "not-an-email" }
}
Request Body: A flat map of fieldId (UUID) → AnswerValue. AnswerValue has value (any), plus optional fileUrl, fileName, fileSize, fileType for FILE fields.
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Validation complete",
"action_time": "2025-02-17T10:30:45",
"data": {
"valid": false,
"errors": [
{
"pageId": "1a2b3c4d-...",
"pageTitle": "Attendee Information",
"fieldId": "fieldId-fullname",
"fieldLabel": "Full Name",
"errorMessage": "Full Name is required",
"errorType": "REQUIRED"
},
{
"pageId": "1a2b3c4d-...",
"pageTitle": "Attendee Information",
"fieldId": "fieldId-email",
"fieldLabel": "Email",
"errorMessage": "Email must be valid email",
"errorType": "INVALID_FORMAT"
}
]
}
}
Success Response Fields:
| Field | Description |
|---|---|
data.valid |
true if all fields passed validation |
data.errors[].pageId |
Page UUID |
data.errors[].pageTitle |
Page title |
data.errors[].fieldId |
Field UUID |
data.errors[].fieldLabel |
Field label |
data.errors[].errorMessage |
Human-readable error |
data.errors[].errorType |
REQUIRED, INVALID_FORMAT, INVALID_TYPE, VALIDATION_FAILED |
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found, form not enabled, or pageNumber out of range |
Attendee Submission
25. Start Submission
Purpose: Initiates the attendee's form response session. Creates a new DRAFT response tied to this event's form. If the attendee already has a draft or submitted response for this event's form, the existing response is returned — this endpoint is idempotent. Requires the event to be PUBLISHED and within its registration window.
Endpoint: POST /api/v1/e-events/applicant-form/events/{eventId}/start
Access Level: 🔒 Protected (Any authenticated attendee)
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 published event | Must be a valid UUID |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Form submission started",
"action_time": "2025-02-17T10:30:45",
"data": {
"responseId": "e5f6a7b8-c9d0-1e2f-3a4b-5c6d7e8f9a0b",
"formId": "9f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"submittedBy": "john.doe",
"status": "DRAFT",
"completedPageIds": [],
"currentPageIndex": 0,
"startedAt": "2025-02-17T10:30:45",
"submittedAt": null,
"completionTimeSeconds": null,
"answers": []
}
}
Success Response Fields: data is a FormResponseObject.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Event not published, registration not yet open, or registration already closed |
404 |
Event not found or form not enabled |
26. Save Page Answers
Purpose: Saves the attendee's answers for a specific page. Can be called multiple times — each call replaces the previous answers for that page. This is a save-in-place operation; it does not advance the page index. Call submit (Endpoint 28) when all pages are complete.
Endpoint: PUT /api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}/save
Access Level: 🔒 Protected (Attendee — response owner)
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 |
pageId |
UUID | Yes | The page being answered | Must be a valid UUID belonging to this event's form |
Request JSON Sample:
{
"c3d4e5f6-a7b8-9c0d-1e2f-3a4b5c6d7e8f": {
"value": "Amina Hassan"
},
"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a": {
"value": "amina@example.com"
},
"e5f6a7b8-c9d0-1e2f-3a4b-5c6d7e8f9a0b": {
"value": "optionId-bycar"
},
"f6a7b8c9-d0e1-2f3a-4b5c-6d7e8f9a0b1c": {
"value": null,
"fileUrl": "https://cdn.example.com/uploads/id.pdf",
"fileName": "national-id.pdf",
"fileSize": 204800,
"fileType": "application/pdf"
}
}
Request Body: A flat Map<UUID, AnswerValue> where each key is a fieldId.
| Key | Type | Description |
|---|---|---|
{fieldId} |
UUID (map key) | The field being answered |
.value |
any | Answer value. See answer value types below |
.fileUrl |
string | FILE fields — uploaded file URL |
.fileName |
string | FILE fields — original file name |
.fileSize |
long | FILE fields — file size in bytes |
.fileType |
string | FILE fields — MIME type |
Answer Value Types
| Field Type | Expected Value |
|---|---|
TEXT, TEXTAREA, EMAIL, PHONE, URL |
String |
NUMBER |
Number (integer or decimal) |
DATE |
String — YYYY-MM-DD |
TIME |
String — HH:mm |
DATETIME |
String — ISO 8601 datetime |
DROPDOWN, RADIO |
String — UUID of selected option |
CHECKBOX |
Array of strings — UUIDs of selected options |
RATING |
Integer — 1 to 5 |
FILE |
null — metadata goes in fileUrl, fileName, etc. |
HEADER |
Omit entirely — no answer needed |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Page saved",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full FormResponseObject with updated answers" }
}
Success Response Fields: data is a FormResponseObject reflecting the updated answer state.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Registration window closed or event not published |
404 |
Event not found, form not enabled, or no active draft found — call /start first |
422 |
A required field has no value |
27. Get My Response
Purpose: Returns the authenticated attendee's current response for the event's form — whether still in draft or already submitted.
Endpoint: GET /api/v1/e-events/applicant-form/events/{eventId}/my-response
Access Level: 🔒 Protected (Any authenticated attendee)
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 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Response retrieved",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full FormResponseObject" }
}
Success Response Fields: data is a FormResponseObject.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
404 |
No response found for this attendee on this event's form |
28. Submit
Purpose: Finalises and submits the attendee's response. The system validates all pages before accepting. Sets status to SUBMITTED and records completionTimeSeconds.
Endpoint: POST /api/v1/e-events/applicant-form/events/{eventId}/submit
Access Level: 🔒 Protected (Attendee — response owner)
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 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Form submitted successfully",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full FormResponseObject with status SUBMITTED" }
}
Success Response Fields: data is a FormResponseObject. data.status will be "SUBMITTED".
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Already submitted, or registration window closed |
404 |
Event not found, form not enabled, or no draft response found |
422 |
Required fields on one or more pages are unanswered |
Organizer Response Management
29. Get Response by ID
Purpose: Retrieves a specific attendee response by ID. Available to the event organizer only.
Endpoint: GET /api/v1/e-events/applicant-form/events/{eventId}/responses/{responseId}
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 |
responseId |
UUID | Yes | The response to retrieve | Must be a valid UUID |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Response retrieved",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full FormResponseObject" }
}
Success Response Fields: data is a FormResponseObject.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or response not found |
30. Get All Responses
Purpose: Returns a paginated list of all attendee responses for the event's form. For organizer review and export.
Endpoint: GET /api/v1/e-events/applicant-form/events/{eventId}/responses
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 |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
page |
integer | No | Page number (0-based) | Min: 0 | 0 |
size |
integer | No | Items per page | Min: 1 | 20 |
Note: This endpoint uses 0-based pagination (
page=0is the first page), matching the SpringPageabledefault passed from the controller.
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Responses retrieved",
"action_time": "2025-02-17T10:30:45",
"data": {
"content": [ "[ FormResponseObject entries ]" ],
"totalElements": 48,
"totalPages": 3,
"first": true,
"last": false,
"empty": false
}
}
Success Response Fields: data.content[] contains FormResponseObject entries.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or form not enabled |
31. Get Analytics
Purpose: Returns full submission analytics for the event's form — stats overview, per-field distributions, and daily submission trends.
Endpoint: GET /api/v1/e-events/applicant-form/events/{eventId}/analytics
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 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Analytics retrieved",
"action_time": "2025-02-17T10:30:45",
"data": {
"formId": "9f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"formTitle": "Attendee Questions - Dar es Salaam Jazz Festival 2025",
"stats": {
"totalStarted": 120,
"totalDrafts": 18,
"totalSubmitted": 95,
"totalWithdrawn": 7,
"completionRate": 79.2,
"dropOffRate": 20.8,
"avgCompletionTimeSeconds": 312.0,
"fastestTimeSeconds": 45,
"slowestTimeSeconds": 1890
},
"fieldAnalytics": [
{
"fieldId": "c3d4e5f6-...",
"fieldLabel": "Full Name",
"fieldType": "TEXT",
"fieldDeleted": false,
"totalResponses": 95,
"uniqueResponses": 95,
"textResponses": ["Amina Hassan", "John Doe", "..."]
},
{
"fieldId": "e5f6a7b8-...",
"fieldLabel": "Arrival method",
"fieldType": "DROPDOWN",
"fieldDeleted": false,
"totalResponses": 95,
"choiceDistribution": [
{ "option": "By Car", "count": 42, "percentage": 44.2 },
{ "option": "Public Transport", "count": 35, "percentage": 36.8 },
{ "option": "On Foot", "count": 18, "percentage": 18.9 }
]
}
],
"dailySubmissions": [
{ "date": "2025-07-15", "count": 12 },
{ "date": "2025-07-16", "count": 28 }
]
}
}
Success Response Fields: data is a FormAnalytics.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found or form not enabled |
Quick Reference — Endpoint Summary
| # | Method | Path | Auth | Who | Description |
|---|---|---|---|---|---|
| Form Setup | |||||
| 1 | POST | /events/{eventId}/enable |
🔒 | Organizer | Enable form + create default page |
| 2 | PUT | /events/{eventId}/settings |
🔒 | Organizer | Update form display settings |
| 3 | DELETE | /events/{eventId}/disable |
🔒 | Organizer | Disable and remove form |
| Page Management | |||||
| 4 | POST | /events/{eventId}/pages |
🔒 | Organizer | Add a page |
| 5 | PUT | /events/{eventId}/pages/{pageId} |
🔒 | Organizer | Update a page |
| 6 | DELETE | /events/{eventId}/pages/{pageId}?hard=false |
🔒 | Organizer | Delete a page (soft or hard) |
| 7 | POST | /events/{eventId}/pages/bulk |
🔒 | Organizer | Bulk add pages with fields |
| 8 | DELETE | /events/{eventId}/pages/bulk?hard=false |
🔒 | Organizer | Bulk delete pages |
| 9 | POST | /events/{eventId}/pages/{pageId}/clone |
🔒 | Organizer | Clone a page |
| Field Management | |||||
| 10 | POST | /events/{eventId}/pages/{pageId}/fields |
🔒 | Organizer | Add a field to a page |
| 11 | PUT | /events/{eventId}/fields/{fieldId} |
🔒 | Organizer | Update a field |
| 12 | DELETE | /events/{eventId}/fields/{fieldId}?hard=false |
🔒 | Organizer | Delete a field (soft or hard) |
| 13 | POST | /events/{eventId}/pages/{pageId}/fields/bulk |
🔒 | Organizer | Bulk add fields |
| 14 | DELETE | /events/{eventId}/pages/{pageId}/fields/bulk?hard=false |
🔒 | Organizer | Bulk delete fields |
| 15 | PATCH | /events/{eventId}/fields/bulk |
🔒 | Organizer | Bulk update fields |
| 16 | POST | /events/{eventId}/fields/{fieldId}/clone |
🔒 | Organizer | Clone a field |
| Option Management | |||||
| 17 | POST | /events/{eventId}/fields/{fieldId}/options |
🔒 | Organizer | Add option to a field |
| 18 | PUT | /events/{eventId}/options/{optionId} |
🔒 | Organizer | Update an option |
| 19 | DELETE | /events/{eventId}/options/{optionId}?hard=false |
🔒 | Organizer | Delete an option (soft or hard) |
| 20 | DELETE | /events/{eventId}/fields/{fieldId}/options/bulk?hard=false |
🔒 | Organizer | Bulk delete options |
| 21 | PATCH | /events/{eventId}/options/bulk |
🔒 | Organizer | Bulk update option labels |
| Preview | |||||
| 22 | GET | /events/{eventId}/preview/metadata |
🔒 | Organizer | Get form page metadata |
| 23 | GET | /events/{eventId}/preview/pages/{pageNumber} |
🔒 | Organizer | Preview a page by number |
| 24 | POST | /events/{eventId}/preview/pages/{pageNumber}/validate |
🔒 | Organizer | Validate answers without saving |
| Attendee Submission | |||||
| 25 | POST | /events/{eventId}/start |
🔒 | Attendee | Start form / get existing draft |
| 26 | PUT | /events/{eventId}/pages/{pageId}/save |
🔒 | Attendee | Save page answers |
| 27 | GET | /events/{eventId}/my-response |
🔒 | Attendee | Get my response |
| 28 | POST | /events/{eventId}/submit |
🔒 | Attendee | Submit form |
| Organizer — Responses & Analytics | |||||
| 29 | GET | /events/{eventId}/responses/{responseId} |
🔒 | Organizer | Get response by ID |
| 30 | GET | /events/{eventId}/responses |
🔒 | Organizer | Get all responses (paginated) |
| 31 | GET | /events/{eventId}/analytics |
🔒 | Organizer | Get form analytics |
All paths are prefixed with
/api/v1/e-events/applicant-form.
Data Format Standards
| Concern | Standard |
|---|---|
| Timestamps | ZonedDateTime — ISO 8601 with offset: 2025-02-17T10:30:45+03:00 |
| Local timestamps | LocalDateTime — ISO 8601 no offset: 2025-02-17T10:30:45 |
| Dates | YYYY-MM-DD — 2025-07-18 |
| Times | HH:mm — 18:00 |
| IDs | UUID v4 — 3fa85f64-5717-4562-b3fc-2c963f66afa6 |
| Pagination (responses) | 0-based page param, Spring Page wrapper |
| Enums | Uppercase: BEFORE_CHECKOUT, SUBMITTED, DROPDOWN, REQUIRED |
Attendance Analytics API
Base URL: https://api.nexgate.com/api/v1
Short Description: The Attendance Analytics API provides comprehensive attendee tracking and analytics for event organizers. This API enables organizers to view attendance statistics with per-day breakdowns, list all checked-in attendees with filters, track absentees by category (full no-shows vs specific-day absences), and view detailed check-in history for individual attendees. The system supports multi-day events with separate tracking per day and provides real-time attendance metrics.
Hints:
- Organizer Only: All endpoints restricted to event organizers
- Multi-Day Support: Per-day statistics and filtering
- Real-Time: Updates instantly after check-ins
- Absentee Categories: FULL_NO_SHOW, SPECIFIC_DAY_ONLY, ALL
- Ticket Type Filter: Filter by specific ticket types
- Search: Search attendees by name/email
- Pagination: Lists paginated (default 50 per page)
- Check-In History: Complete per-day check-in records
- Attendance Patterns: Tracks which days attended/absent
Response Structures
AttendanceStatsResponse
{
"eventId": "uuid",
"eventTitle": "East African Tech Summit 2025",
"totalDays": 3,
"eventSchedule": [
{"dayNumber": 1, "dayName": "Day 1 - Opening", "date": "2025-12-15"},
{"dayNumber": 2, "dayName": "Day 2 - Conference", "date": "2025-12-16"},
{"dayNumber": 3, "dayName": "Day 3 - Closing", "date": "2025-12-17"}
],
"overallStats": {
"totalTickets": 500,
"totalCheckedIn": 462,
"totalAbsent": 38,
"attendanceRate": 92.4,
"byDay": [
{
"dayNumber": 1,
"dayName": "Day 1 - Opening",
"date": "2025-12-15",
"totalTickets": 500,
"checkedIn": 475,
"absent": 25,
"attendanceRate": 95.0,
"status": "COMPLETED"
}
]
},
"byTicketType": [
{
"ticketTypeId": "uuid",
"ticketTypeName": "VIP Pass",
"totalSold": 100,
"totalCheckedIn": 98,
"totalAbsent": 2,
"attendanceRate": 98.0,
"byDay": [
{
"dayNumber": 1,
"dayName": "Day 1 - Opening",
"checkedIn": 99,
"absent": 1,
"attendanceRate": 99.0
}
]
}
]
}
AttendeeListResponse
{
"eventId": "uuid",
"eventTitle": "East African Tech Summit 2025",
"dayNumber": 1,
"dayName": "Day 1 - Opening",
"dayDate": "2025-12-15",
"ticketTypeId": "uuid",
"ticketTypeName": "VIP Pass",
"summary": {
"totalTicketsForType": 100,
"checkedInThisDay": 99
},
"attendees": [
{
"ticketInstanceId": "uuid",
"attendeeName": "John Doe",
"attendeeEmail": "john@example.com",
"attendeePhone": "+255712345678",
"ticketType": "VIP Pass",
"ticketSeries": "VIP-0001",
"bookingReference": "EVT-A3F4B21C",
"pricePaid": 150.00,
"checkInTime": "2025-12-15T09:15:00+03:00",
"checkInLocation": "Main Gate",
"checkedInBy": "Scanner Operator 1",
"scannerId": "uuid-string"
}
],
"pagination": {
"currentPage": 0,
"pageSize": 50,
"totalPages": 2,
"totalElements": 99,
"hasNext": true,
"hasPrevious": false
}
}
AbsenteeListResponse
{
"eventId": "uuid",
"eventTitle": "East African Tech Summit 2025",
"dayNumber": 1,
"dayName": "Day 1 - Opening",
"dayDate": "2025-12-15",
"ticketTypeId": null,
"ticketTypeName": null,
"summary": {
"totalTicketsForType": 500,
"absentThisDay": 25,
"absenteeRate": 5.0,
"breakdown": {
"fullNoShow": 10,
"specificDayOnly": 15
}
},
"absentees": [
{
"ticketInstanceId": "uuid",
"attendeeName": "Jane Smith",
"attendeeEmail": "jane@example.com",
"attendeePhone": "+255723456789",
"ticketType": "General Admission",
"ticketSeries": "GENER-0042",
"bookingReference": "EVT-B5D2E12F",
"pricePaid": 50.00,
"statusForThisDay": "NOT_CHECKED_IN",
"attendancePattern": {
"totalEventDays": 3,
"daysAttended": 2,
"daysAbsent": 1,
"attendedDayNumbers": [2, 3],
"absentDayNumbers": [1],
"category": "SPECIFIC_DAY_ONLY"
}
}
],
"pagination": {
"currentPage": 0,
"pageSize": 50,
"totalPages": 1,
"totalElements": 25,
"hasNext": false,
"hasPrevious": false
}
}
AttendeeDetailResponse
{
"ticketInstanceId": "uuid",
"attendeeName": "John Doe",
"attendeeEmail": "john@example.com",
"attendeePhone": "+255712345678",
"ticketType": "VIP Pass",
"ticketSeries": "VIP-0001",
"bookingReference": "EVT-A3F4B21C",
"pricePaid": 150.00,
"overallStatus": "FULLY_ATTENDED",
"daysAttended": 3,
"daysTotal": 3,
"checkInsByDay": [
{
"dayNumber": 1,
"dayName": "Day 1 - Opening",
"dayDate": "2025-12-15",
"status": "CHECKED_IN",
"checkInTime": "2025-12-15T09:15:00+03:00",
"checkInLocation": "Main Gate",
"checkedInBy": "Scanner Operator 1",
"scannerId": "uuid-string"
},
{
"dayNumber": 2,
"dayName": "Day 2 - Conference",
"dayDate": "2025-12-16",
"status": "CHECKED_IN",
"checkInTime": "2025-12-16T08:45:00+03:00",
"checkInLocation": "VIP Entrance",
"checkedInBy": "Scanner Operator 2",
"scannerId": "uuid-string"
},
{
"dayNumber": 3,
"dayName": "Day 3 - Closing",
"dayDate": "2025-12-17",
"status": "CHECKED_IN",
"checkInTime": "2025-12-17T09:00:00+03:00",
"checkInLocation": "Main Gate",
"checkedInBy": "Scanner Operator 1",
"scannerId": "uuid-string"
}
]
}
Endpoints
1. Get Attendance Stats
Endpoint: GET /attendance/{eventId}/stats
Access: 🔒 Event Organizer Only
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| eventId | string (UUID) | Yes | Event identifier |
Success Response: Returns AttendanceStatsResponse
Success Response Message: "Attendance stats retrieved"
Behavior:
- Validates organizer owns event
- Calculates overall attendance metrics
- Breaks down by day (multi-day events)
- Breaks down by ticket type
- Real-time from check-in data
Errors:
403 FORBIDDEN: Not event organizer404 NOT_FOUND: Event not found
2. Get Attendees
Endpoint: GET /attendance/{eventId}/attendees?dayNumber=1&ticketTypeId=uuid&search=john&page=0&size=50
Access: 🔒 Event Organizer Only
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| eventId | string (UUID) | Yes | Event identifier |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| dayNumber | integer | Multi-day: Yes | Day number (1, 2, 3...) |
| ticketTypeId | string (UUID) | No | Filter by ticket type |
| search | string | No | Search by name/email |
| page | integer | No | Page number (0-indexed, default: 0) |
| size | integer | No | Items per page (default: 50) |
Success Response: Returns AttendeeListResponse with pagination
Success Response Message: "Attendees retrieved"
Behavior:
- Lists checked-in attendees for specified day
- Filters by ticket type if provided
- Searches by name/email if provided
- Paginated results
- Shows check-in details
Validation:
- Multi-day events require dayNumber
- Ticket type must belong to event
- Day number must be valid (1 to totalDays)
3. Get Absentees
Endpoint: GET /attendance/{eventId}/absentees?dayNumber=1&ticketTypeId=uuid&category=FULL_NO_SHOW&search=jane&page=0&size=50
Access: 🔒 Event Organizer Only
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| eventId | string (UUID) | Yes | Event identifier |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| dayNumber | integer | Multi-day: Yes | Day number (1, 2, 3...) |
| ticketTypeId | string (UUID) | No | Filter by ticket type |
| category | enum | No | ALL, FULL_NO_SHOW, SPECIFIC_DAY_ONLY (default: ALL) |
| search | string | No | Search by name/email |
| page | integer | No | Page number (0-indexed, default: 0) |
| size | integer | No | Items per page (default: 50) |
Success Response: Returns AbsenteeListResponse with pagination
Success Response Message: "Absentees retrieved"
Behavior:
- Lists NOT checked-in attendees for specified day
- Categories:
- ALL: Everyone not checked-in for this day
- FULL_NO_SHOW: Never checked-in any day
- SPECIFIC_DAY_ONLY: Attended other days but not this one
- Shows attendance patterns
- Paginated results
Absentee Breakdown:
- Full no-shows: Tickets never used
- Specific day only: Partial attendance
4. Get Attendee Detail
Endpoint: GET /attendance/{eventId}/attendees/{ticketInstanceId}
Access: 🔒 Event Organizer Only
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| eventId | string (UUID) | Yes | Event identifier |
| ticketInstanceId | string (UUID) | Yes | Ticket instance identifier |
Success Response: Returns AttendeeDetailResponse
Success Response Message: "Attendee detail retrieved"
Behavior:
- Shows complete check-in history
- Per-day status (CHECKED_IN, NOT_CHECKED_IN, UPCOMING)
- Overall attendance status
- Check-in details (time, location, scanner)
Overall Status:
- FULLY_ATTENDED: Checked-in all days
- PARTIALLY_ATTENDED: Checked-in some days
- NOT_ATTENDED: Never checked-in
Absentee Categories Explained
ALL (Default)
Who: Everyone who didn't check in for the specified day
Includes:
- Full no-shows (never attended any day)
- Specific-day absentees (attended other days)
Use Case: Complete list of who's missing today
FULL_NO_SHOW
Who: Tickets never checked-in for ANY event day
Characteristics:
- Purchased ticket but never attended
- 0% attendance rate
- Wasted tickets
Use Case: Identify completely unused tickets for follow-up
SPECIFIC_DAY_ONLY
Who: Attended some days but NOT this specific day
Characteristics:
- Checked-in on other event days
- Partial attendance
- Shows attendance pattern
Use Case: Identify who skipped specific days (e.g., missed keynote)
Example:
3-Day Festival:
- Ticket A: Attended Day 1, 2, 3 → FULLY_ATTENDED
- Ticket B: Attended Day 1, 3 (missed Day 2) → SPECIFIC_DAY_ONLY for Day 2
- Ticket C: Never attended → FULL_NO_SHOW for all days
Multi-Day Event Logic
Day Number Requirements
Single-Day Events:
dayNumberoptional (defaults to 1)- Only one day to track
Multi-Day Events:
dayNumberREQUIRED- Must specify which day (1, 2, 3...)
- System returns error if missing
Validation:
Event has 3 days → dayNumber must be 1, 2, or 3
Request dayNumber=5 → Error: "Invalid day 5. Valid: 1-3"
Per-Day Tracking
Statistics:
- Total tickets (same for all days)
- Checked-in (varies per day)
- Absent (varies per day)
- Attendance rate (varies per day)
Day Status:
- COMPLETED: Day has passed
- ONGOING: Day is today
- UPCOMING: Day hasn't started
Example:
3-Day Event (Dec 15-17), Today: Dec 16
Day 1: status=COMPLETED, checkedIn=475
Day 2: status=ONGOING, checkedIn=450
Day 3: status=UPCOMING, checkedIn=0
Attendance Rate Calculations
Overall Attendance Rate
Rate = (Unique Tickets Checked-In / Total Tickets) × 100
Example:
- Total tickets: 500
- Tickets with at least one check-in: 462
- Rate: 92.4%
Per-Day Attendance Rate
Rate = (Checked-In This Day / Total Tickets) × 100
Example:
- Total tickets: 500
- Checked-in Day 1: 475
- Rate: 95.0%
Per-Ticket-Type Rate
Rate = (Checked-In for Type / Total Sold for Type) × 100
Example:
- VIP tickets sold: 100
- VIP checked-in: 98
- Rate: 98.0%
Search Functionality
Searches:
- Attendee name (case-insensitive)
- Attendee email (case-insensitive)
Match: Partial match (contains)
Examples:
- Search "john" → Matches "John Doe", "Johnny Smith", "john@example.com"
- Search "@gmail" → Matches all Gmail addresses
- Search "255712" → Matches phone numbers containing this
Use Cases
Dashboard Overview
GET /attendance/{eventId}/stats
Shows:
- Overall attendance rate
- Per-day breakdown
- Per-ticket-type breakdown
- Real-time metrics
Check Who Attended Today
GET /attendance/{eventId}/attendees?dayNumber=1
Shows:
- All checked-in attendees for Day 1
- Check-in times and locations
- Searchable and filterable
Find No-Shows
GET /attendance/{eventId}/absentees?dayNumber=1&category=FULL_NO_SHOW
Shows:
- Tickets that were never used
- For follow-up or refund processing
Track Partial Attendance
GET /attendance/{eventId}/absentees?dayNumber=2&category=SPECIFIC_DAY_ONLY
Shows:
- Who attended Day 1 but missed Day 2
- Useful for understanding engagement patterns
Individual Ticket History
GET /attendance/{eventId}/attendees/{ticketInstanceId}
Shows:
- Complete check-in history
- Which days attended/missed
- Check-in details per day
Best Practices
For Organizers
✅ Monitor attendance in real-time during event
✅ Follow up with full no-shows post-event
✅ Track partial attendance patterns
✅ Export attendee lists for post-event communications
✅ Use absentee data to improve future events
For Developers
✅ Require dayNumber for multi-day events
✅ Show attendance rate with 1 decimal (92.4%)
✅ Implement export to CSV functionality
✅ Cache stats (refresh every 5 minutes)
✅ Display check-in times in event timezone
Quick Reference
HTTP Status Codes
200 OK: Successful request401 UNAUTHORIZED: Authentication required403 FORBIDDEN: Not event organizer404 NOT_FOUND: Event/ticket not found
Attendance Status
- FULLY_ATTENDED: All days checked-in
- PARTIALLY_ATTENDED: Some days checked-in
- NOT_ATTENDED: Never checked-in
Day Status
- COMPLETED: Day has passed
- ONGOING: Day is today
- UPCOMING: Day hasn't started
Absentee Categories
- ALL: All absentees for this day
- FULL_NO_SHOW: Never attended any day
- SPECIFIC_DAY_ONLY: Missed this day only
Conclusion
The Attendance Analytics API provides comprehensive tracking with:
✅ Real-Time Stats: Instant attendance metrics
✅ Multi-Day Support: Per-day breakdowns
✅ Absentee Tracking: Full no-shows vs partial attendance
✅ Search & Filter: By ticket type, day, name/email
✅ Individual History: Complete check-in records
✅ Attendance Patterns: Understand engagement
Event Feedback API
Base URL: https://api.nexgate.com/api/v1
Short Description: The Event Feedback API enables attendees to rate and review events after attendance. This API provides a simple 5-star rating system with optional comments, prevents duplicate feedback, ensures only attendees (non-organizers) can submit reviews, and allows anyone to view event feedback with pagination. The system supports escrow release decisions based on feedback quality and helps organizers improve future events.
Hints:
- One Feedback Per User: Each user can only submit one feedback per event
- Attendee Only: Event organizers cannot review their own events
- 5-Star Rating: Required rating from 1-5 stars
- Optional Comments: Text reviews up to 1000 characters
- Public Viewing: Anyone can view event feedback (paginated)
- Chronological Order: Newest feedback first
- Escrow Integration: Feedback may influence escrow release (future)
- Simple Model: Focus on rating quality over complex metrics
Response Structure
EventFeedbackResponse
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"eventId": "770e8400-e29b-41d4-a716-446655440002",
"eventTitle": "East African Tech Summit 2025",
"userId": "660e8400-e29b-41d4-a716-446655440001",
"userName": "johndoe",
"rating": 5,
"comment": "Amazing event! The speakers were excellent and the venue was perfect. Definitely attending next year!",
"createdAt": "2025-12-18T10:30:45Z"
}
Paginated Response
{
"success": true,
"httpStatus": "OK",
"message": "Feedbacks retrieved successfully",
"action_time": "2025-12-11T10:30:45",
"data": {
"content": [
{
"id": "uuid",
"eventId": "uuid",
"eventTitle": "East African Tech Summit 2025",
"userId": "uuid",
"userName": "johndoe",
"rating": 5,
"comment": "Great event!",
"createdAt": "2025-12-18T10:30:45Z"
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 20,
"offset": 0
},
"totalElements": 145,
"totalPages": 8,
"last": false,
"first": true,
"numberOfElements": 20,
"size": 20,
"number": 0,
"empty": false
}
}
Endpoints
1. Create Feedback
Endpoint: POST /feedbacks/event/{eventId}
Access: 🔒 Authenticated Users (Non-Organizers)
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| eventId | string (UUID) | Yes | Event identifier |
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token |
| Content-Type | string | Yes | application/json |
Request Body:
{
"rating": 5,
"comment": "Amazing event! The speakers were excellent and the venue was perfect. Definitely attending next year!"
}
Request Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| rating | integer | Yes | Star rating | Min: 1, Max: 5 |
| comment | string | No | Text review | Max: 1000 characters |
Success Response: Returns EventFeedbackResponse
Success Response Message: "Feedback submitted successfully"
HTTP Status Code: 201 CREATED
Behavior:
- Validates user is authenticated
- Validates event exists
- Checks user is NOT the event organizer
- Checks user hasn't already submitted feedback
- Creates feedback record
- Returns created feedback
Validation Rules:
- ✅ Rating must be 1-5 (integer)
- ✅ Comment optional, max 1000 chars
- ✅ One feedback per user per event
- ✅ Organizers cannot review own events
- ✅ Event must exist
Standard Error Types:
400 BAD_REQUEST: Invalid rating (not 1-5)401 UNAUTHORIZED: Not authenticated403 FORBIDDEN: Organizer trying to review own event404 NOT_FOUND: Event not found409 CONFLICT: Already submitted feedback
Error Response Examples:
Invalid Rating (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Rating must be at least 1",
"action_time": "2025-12-11T10:30:45",
"data": "Rating must be at least 1"
}
Organizer Cannot Review (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "Event organizers cannot submit feedback for their own events.",
"action_time": "2025-12-11T10:30:45",
"data": "Event organizers cannot submit feedback for their own events."
}
Already Submitted (409):
{
"success": false,
"httpStatus": "CONFLICT",
"message": "You have already provided feedback for this event",
"action_time": "2025-12-11T10:30:45",
"data": "You have already provided feedback for this event"
}
2. Get Event Feedbacks
Endpoint: GET /feedbacks/event/{eventId}?page=0&size=20
Access: 🔓 Public (No Authentication Required)
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| eventId | string (UUID) | Yes | Event identifier |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | integer | No | Page number (0-indexed, default: 0) |
| size | integer | No | Items per page (default: 20) |
Success Response: Returns Spring Page object with EventFeedbackResponse items
Success Response Message: "Feedbacks retrieved successfully"
HTTP Status Code: 200 OK
Behavior:
- Returns paginated list of feedback
- Sorted by creation date (newest first)
- No authentication required (public access)
- Validates event exists
Pagination Details:
- Zero-indexed pages (0, 1, 2...)
- Default page size: 20
- Max page size: 100 (recommended)
- Includes total elements and pages
Standard Error Types:
404 NOT_FOUND: Event not found
Rating System
Star Ratings (1-5)
| Rating | Meaning | Emoji | Description |
|---|---|---|---|
| 5 ⭐⭐⭐⭐⭐ | Excellent | 😍 | Outstanding event, exceeded expectations |
| 4 ⭐⭐⭐⭐ | Very Good | 😊 | Great event, minor improvements possible |
| 3 ⭐⭐⭐ | Good | 🙂 | Satisfactory event, met expectations |
| 2 ⭐⭐ | Fair | 😐 | Below expectations, needs improvement |
| 1 ⭐ | Poor | 😞 | Disappointing event, significant issues |
Average Rating Calculation
Average = Sum of all ratings / Number of feedbacks
Example:
Ratings: 5, 5, 4, 5, 3, 4, 5, 5, 4, 5
Total: 45
Count: 10
Average: 4.5 stars
Display Format: 4.5 ⭐ (10 reviews)
Comment Guidelines
Recommended Comment Content
Good Comments Include:
- ✅ What you enjoyed
- ✅ Specific highlights (speakers, venue, activities)
- ✅ Suggestions for improvement
- ✅ Overall experience summary
Example Good Comments:
"Amazing event! The keynote speakers were excellent and provided valuable insights.
The venue was perfect and well-organized. Only suggestion would be to have longer
lunch breaks. Will definitely attend next year!"
"Great networking opportunities and diverse range of topics. Sound system could
be improved in the main hall."
"Well organized but sessions felt rushed. Would prefer fewer sessions with more
time for Q&A."
Avoid:
- ❌ Offensive language
- ❌ Personal attacks
- ❌ Spam or promotional content
- ❌ Off-topic comments
Character Limit
- Maximum: 1000 characters
- Recommended: 100-500 characters
- Minimum: None (optional field)
Access Control
Who Can Submit Feedback?
Allowed:
- ✅ Any authenticated user
- ✅ Attendees (purchased tickets)
- ✅ Non-attendees (also allowed currently)
Not Allowed:
- ❌ Event organizers (for their own events)
- ❌ Users who already submitted feedback
- ❌ Unauthenticated users
Future Restriction (Recommended):
- Only users who purchased tickets
- Requires checking booking history
- Prevents fake reviews
Who Can View Feedback?
Anyone (Public access):
- ✅ No authentication required
- ✅ Potential attendees researching events
- ✅ Event organizers viewing their reviews
- ✅ Platform admins
Use Cases
Submit Feedback After Event
POST /feedbacks/event/{eventId}
{
"rating": 5,
"comment": "Great event, highly recommend!"
}
User provides honest review after attending
View Event Reviews Before Booking
GET /feedbacks/event/{eventId}?page=0&size=20
Potential attendee checks reviews before purchasing tickets
Sees average rating and recent comments
Organizer Checks Feedback
GET /feedbacks/event/{eventId}?page=0&size=20
Organizer views all feedback for their event
Identifies areas for improvement
Plans better future events
Platform Quality Monitoring
GET /feedbacks/event/{eventId}?page=0&size=20
Platform admin reviews feedback
Identifies low-rated events
May influence escrow release decisions
Future Enhancements
Planned Features
1. Verified Attendee Badge
- Only show "Verified Attendee" if user has booking
- Increases trust in reviews
- Current: Anyone can review (not implemented)
2. Average Rating Endpoint
GET /feedbacks/event/{eventId}/summary
{
"averageRating": 4.5,
"totalReviews": 145,
"ratingDistribution": {
"5": 90,
"4": 35,
"3": 15,
"2": 3,
"1": 2
}
}
3. Helpful Votes
- Users can mark reviews as helpful
- Sort by most helpful
- Bubble up quality reviews
4. Organizer Response
- Organizers can reply to feedback
- Shows engagement and care
- Builds trust with future attendees
5. Escrow Integration
- Average rating influences escrow release
- Events <3 stars may require manual review
- Automatic release for 4+ stars
- Current: Not implemented
Escrow Release Logic (Future)
How Feedback May Affect Escrow
High Ratings (4-5 stars average):
- ✅ Automatic escrow release
- Indicates successful event
- No manual review needed
Medium Ratings (3-3.9 stars average):
- ⚠️ Manual review triggered
- Platform checks for issues
- May contact organizer
- Usually released after review
Low Ratings (<3 stars average):
- ❌ Escrow release delayed
- Investigation required
- May require organizer explanation
- Possible refunds if serious issues
Example:
Event with 4.5 average rating (90% 5-star):
→ Escrow automatically released 24 hours after event
Event with 2.8 average rating (50% 1-2 star):
→ Escrow held, manual review, contact organizer
→ Possible partial refunds if issues confirmed
Current Status: Not implemented (all events release automatically)
Best Practices
For Attendees
✅ Submit honest, constructive feedback
✅ Mention specific positives and negatives
✅ Wait until after event to review
✅ Be respectful in comments
✅ Update review if organizer addresses issues (future)
For Organizers
✅ Read all feedback carefully
✅ Identify patterns in complaints
✅ Thank reviewers for positive feedback (future)
✅ Address concerns in organizer response (future)
✅ Use feedback to improve future events
For Platform
✅ Monitor feedback quality
✅ Flag suspicious reviews
✅ Use ratings in escrow decisions (future)
✅ Display average ratings prominently
✅ Encourage verified attendee reviews (future)
Quick Reference
HTTP Status Codes
200 OK: Feedbacks retrieved successfully201 CREATED: Feedback submitted successfully400 BAD_REQUEST: Invalid rating value401 UNAUTHORIZED: Not authenticated403 FORBIDDEN: Organizer reviewing own event404 NOT_FOUND: Event not found409 CONFLICT: Already submitted feedback
Rating Range
- Minimum: 1 star (Poor)
- Maximum: 5 stars (Excellent)
- Type: Integer only (no half stars)
Comment Length
- Minimum: 0 (optional)
- Maximum: 1000 characters
- Recommended: 100-500 characters
Pagination
- Default Page: 0 (first page)
- Default Size: 20 items
- Max Size: 100 (recommended)
- Sort Order: Newest first (createdAt DESC)
Data Formats
- Event ID: UUID format
- User ID: UUID format
- Created At: ISO 8601 timestamp (Instant)
- Rating: Integer (1-5)
Integration Examples
Submit Feedback Form
const submitFeedback = async (eventId, rating, comment) => {
const response = await fetch(`/api/v1/e-events/feedbacks/event/${eventId}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ rating, comment })
});
if (response.status === 201) {
alert('Thank you for your feedback!');
} else if (response.status === 409) {
alert('You have already reviewed this event');
} else if (response.status === 403) {
alert('Organizers cannot review their own events');
}
};
Display Feedback List
const loadFeedback = async (eventId, page = 0) => {
const response = await fetch(
`/api/v1/e-events/feedbacks/event/${eventId}?page=${page}&size=20`
);
const data = await response.json();
const feedbacks = data.data.content;
const averageRating = calculateAverage(feedbacks);
displayFeedbacks(feedbacks, averageRating);
};
const calculateAverage = (feedbacks) => {
if (feedbacks.length === 0) return 0;
const sum = feedbacks.reduce((acc, f) => acc + f.rating, 0);
return (sum / feedbacks.length).toFixed(1);
};
Star Rating Component
const StarRating = ({ rating, onChange }) => {
const stars = [1, 2, 3, 4, 5];
return (
<div className="star-rating">
{stars.map(star => (
<span
key={star}
className={star <= rating ? 'star filled' : 'star'}
onClick={() => onChange(star)}
>
⭐
</span>
))}
</div>
);
};
Conclusion
The Event Feedback API provides simple yet effective review system with:
✅ 5-Star Ratings: Simple and universally understood
✅ Optional Comments: Detailed written feedback
✅ One Per User: Prevents spam and duplicate reviews
✅ Organizer Protection: Can't review own events
✅ Public Access: Anyone can view to inform decisions
✅ Pagination: Handles large numbers of reviews
✅ Future Integration: Ready for escrow release logic
Event Fund Claims
Overview
When an event organizer sells tickets, revenue is held in escrow — one escrow entry per buyer checkout session. A Fund Claim is a formal request to release all currently HELD escrows for an event into the organizer's wallet. Every claim goes through a PENDING → APPROVED / REJECTED lifecycle, and funds only move on explicit admin approval.
Claim approval is an all-or-nothing escrow operation — it releases every HELD escrow for that event at the moment of approval. Partial escrow release is not supported by design (see Financial Safety). What the organizer does with the wallet balance after release is handled by the separate DisbursementService.
Authentication: All endpoints require Authorization: Bearer <jwt_token>
Business Rules
| Rule | Detail |
|---|---|
| One pending per event | At most ONE PENDING claim per event at any time |
| Claimable amount formula | totalRevenue − totalRefunded − totalClaimed − totalPendingClaims |
| Admin-only for active events | If event has not ended, only ADMIN can initiate a claim |
| Refund deadline | 3 days before event start. Past this → no refunds, organizer may claim early |
| Escrow release | Approval releases ALL HELD escrows for the event atomically → organizer wallet. No partial release |
| No partial amounts | Escrows are atomic per buyer. "80% release" has no business meaning at the escrow layer — staging happens via DisbursementService after funds land in wallet |
| Admin claims require note | adminNote is mandatory on admin-initiated claims |
| Refund safety | Refunds and approvals use pessimistic write lock on EscrowAccount to prevent race conditions |
Claim Status Lifecycle
| Status | Meaning |
|---|---|
PENDING |
Submitted, awaiting admin review. Escrow NOT yet released |
APPROVED |
Admin approved. Escrow atomically released to organizer wallet |
REJECTED |
Admin rejected. No funds moved. Organizer may submit a new claim |
CANCELLED |
Organizer cancelled before admin action. No funds moved |
Standard Response Format
Success
{
"success": true,
"httpStatus": "OK",
"message": "Fund claim submitted successfully",
"action_time": "2026-04-27T10:30:45",
"data": { }
}
Error
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "A pending claim already exists for this event",
"action_time": "2026-04-27T10:30:45",
"data": "A pending claim already exists for this event"
}
Endpoints
1. List All Claims (Admin)
Endpoint: GET /api/v1/e-events/claims
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Returns all fund claims across all events. Optionally filter by status.
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
status |
ClaimStatus enum |
No | Filter by: PENDING, APPROVED, REJECTED, CANCELLED |
— (all returned) |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Fund claims retrieved",
"data": [
{
"claimId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"claimNumber": "EFC-2026-000001",
"eventId": "uuid",
"eventTitle": "Dar Jazz Night",
"eventStatus": "PUBLISHED",
"organizerId": "uuid",
"organizerName": "Amina Hassan",
"status": "PENDING",
"claimedAmount": 50000.00,
"totalRevenueSnapshot": 80000.00,
"totalRefundedSnapshot": 5000.00,
"totalPreviouslyClaimedSnapshot": 0.00,
"totalPendingAtSubmission": 0.00,
"currency": "TZS",
"adminInitiated": false,
"adminId": null,
"adminNote": null,
"organizerNote": "Requesting first partial claim",
"reviewedById": null,
"reviewerName": null,
"reviewNote": null,
"reviewedAt": null,
"escrowsReleasedCount": 0,
"escrowsSkippedCount": 0,
"actualReleasedAmount": null,
"initiatedAt": "2026-04-20T08:00:00",
"updatedAt": "2026-04-20T08:00:00"
}
]
}
Response Fields:
| Field | Type | Description |
|---|---|---|
claimId |
UUID |
Unique claim identifier |
claimNumber |
string |
Human-readable reference, e.g. EFC-2026-000001 |
eventId |
UUID |
The event this claim belongs to |
eventTitle |
string |
Display name of the event |
eventStatus |
EventStatus |
Current event status |
organizerId |
UUID |
Organizer's account ID |
organizerName |
string |
Organizer's display name |
status |
ClaimStatus |
Current claim status |
claimedAmount |
BigDecimal |
Amount the organizer requested |
totalRevenueSnapshot |
BigDecimal |
Organizer's net revenue at time of submission |
totalRefundedSnapshot |
BigDecimal |
Total refunds issued at time of submission |
totalPreviouslyClaimedSnapshot |
BigDecimal |
Already released to wallet before this claim |
totalPendingAtSubmission |
BigDecimal |
Other pending claim amounts at submission time |
currency |
string |
Currency code, e.g. TZS |
adminInitiated |
boolean |
true if an admin submitted on behalf of organizer |
adminId |
UUID |
Admin's account ID (null if organizer-initiated) |
adminNote |
string |
Admin's reason for initiating (null if organizer-initiated) |
organizerNote |
string |
Optional note from organizer |
reviewedById |
UUID |
ID of admin who reviewed (null if pending) |
reviewerName |
string |
Name of reviewing admin (null if pending) |
reviewNote |
string |
Admin's review note (null if pending) |
reviewedAt |
LocalDateTime |
Timestamp of review (null if pending) |
escrowsReleasedCount |
Integer |
Number of escrow entries released on approval |
escrowsSkippedCount |
Integer |
Escrow entries skipped (e.g. already refunded) |
actualReleasedAmount |
BigDecimal |
Actual amount credited to wallet on approval |
initiatedAt |
LocalDateTime |
When the claim was submitted |
updatedAt |
LocalDateTime |
Last updated timestamp |
Possible Errors:
| HTTP Status | Description |
|---|---|
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Caller does not have admin role |
2. Get My Claims (Organizer)
Endpoint: GET /api/v1/e-events/claims/my-claims
Access: 🔒 Authenticated organizer (any role)
Purpose: Returns all fund claims submitted by the currently authenticated organizer, across all their events.
No query parameters.
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "My fund claims retrieved",
"data": [ /* array of EventFundClaimResponse — same structure as endpoint 1 */ ]
}
Possible Errors:
| HTTP Status | Description |
|---|---|
401 UNAUTHORIZED |
Invalid or expired JWT |
3. Get Claim by ID
Endpoint: GET /api/v1/e-events/claims/{claimId}
Access: 🔒 Authenticated — admin sees any claim; organizer sees only their own
Purpose: Returns full details of a single fund claim.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
claimId |
UUID |
Yes | The claim's unique identifier |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Claim retrieved",
"data": { /* EventFundClaimResponse */ }
}
Possible Errors:
| HTTP Status | Description |
|---|---|
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Organizer attempting to view another organizer's claim |
404 NOT_FOUND |
Claim with given ID does not exist |
4. Get Event Claims
Endpoint: GET /api/v1/e-events/claims/event/{eventId}
Access: 🔒 Authenticated — organizer (own events) or admin (any event)
Purpose: Returns all claims ever submitted for a specific event.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
eventId |
UUID |
Yes | The event's unique identifier |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Event claims retrieved",
"data": [ /* array of EventFundClaimResponse */ ]
}
Possible Errors:
| HTTP Status | Description |
|---|---|
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Organizer does not own this event |
404 NOT_FOUND |
Event not found |
5. Get Claimable Amount
Endpoint: GET /api/v1/e-events/claims/event/{eventId}/claimable-amount
Access: 🔒 Authenticated — organizer (own events) or admin
Purpose: Returns the current claimable amount breakdown for an event, including eligibility status and the active pending claim if one exists.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
eventId |
UUID |
Yes | The event's unique identifier |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Claimable amount retrieved",
"data": {
"eventId": "uuid",
"eventTitle": "Dar Jazz Night",
"totalRevenue": 80000.00,
"totalRefunded": 5000.00,
"totalClaimed": 0.00,
"totalPendingClaims": 0.00,
"claimableAmount": 60000.00,
"currency": "TZS",
"eligible": true,
"ineligibilityReason": null,
"activePendingClaimId": null,
"refundDeadline": "2026-05-10T00:00:00+03:00",
"pastRefundDeadline": false
}
}
Note on claimable amount:
claimableAmount = totalRevenue − totalRefunded − totalClaimed − totalPendingClaims. This is a snapshot estimate used to indicate eligibility — theactualReleasedAmounton approval is the real number, as it reflects the liveHELDescrow balance at the moment of approval.
Response Fields:
| Field | Type | Description |
|---|---|---|
totalRevenue |
BigDecimal |
Net organizer share of all non-refunded tickets |
totalRefunded |
BigDecimal |
Gross amount returned to buyers |
totalClaimed |
BigDecimal |
Already released to organizer wallet |
totalPendingClaims |
BigDecimal |
Locked by current PENDING claims |
claimableAmount |
BigDecimal |
What the organizer can claim right now |
eligible |
boolean |
Whether a new claim can be submitted |
ineligibilityReason |
string |
Human-readable reason if eligible = false |
activePendingClaimId |
UUID |
ID of the existing pending claim, or null |
refundDeadline |
ZonedDateTime |
eventStartDate − 3 days |
pastRefundDeadline |
boolean |
true = refund window closed, full amount claimable |
Possible Errors:
| HTTP Status | Description |
|---|---|
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Organizer does not own this event |
404 NOT_FOUND |
Event not found |
6. Get Revenue Summary (Admin)
Endpoint: GET /api/v1/e-events/claims/event/{eventId}/revenue-summary
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Returns a full financial summary of an event including gross sales, platform fees, refunds, claimed amounts, and escrow balance. Used for admin audit and oversight.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
eventId |
UUID |
Yes | The event's unique identifier |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Event revenue summary retrieved",
"data": {
"eventId": "uuid",
"eventTitle": "Dar Jazz Night",
"grossRevenue": 100000.00,
"totalRefunded": 5000.00,
"platformFees": 10000.00,
"netOrganizerRevenue": 85000.00,
"totalClaimed": 0.00,
"totalPendingClaims": 0.00,
"escrowBalance": 85000.00,
"currency": "TZS"
}
}
Possible Errors:
| HTTP Status | Description |
|---|---|
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Caller does not have admin role |
404 NOT_FOUND |
Event not found |
7. Submit Claim (Organizer)
Endpoint: POST /api/v1/e-events/claims/event/{eventId}
Access: 🔒 Authenticated organizer — must own the event
Purpose: Organizer submits a fund claim for the full current claimable amount. The claim amount is computed server-side — organizer cannot override it.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
eventId |
UUID |
Yes | The event to submit a claim for |
Request Body (optional):
{
"organizerNote": "Requesting first partial claim before event ends"
}
Request Body Fields:
| Field | Type | Required | Description | Validation |
|---|---|---|---|---|
organizerNote |
string |
No | Optional note from the organizer | Max 1000 characters |
Guard Rules Applied Server-Side:
- Event must belong to the authenticated organizer
- Event must have ended OR
pastRefundDeadline = true(otherwise only admin can claim) claimableAmountmust be > 0- No existing
PENDINGclaim for this event
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Fund claim submitted successfully",
"data": { /* EventFundClaimResponse with status: PENDING */ }
}
Possible Errors:
| HTTP Status | Description |
|---|---|
400 BAD_REQUEST |
A pending claim already exists for this event |
400 BAD_REQUEST |
Claimable amount is zero — nothing to claim |
400 BAD_REQUEST |
Event has not ended and refund deadline has not passed — only admin can claim |
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Authenticated user does not own this event |
404 NOT_FOUND |
Event not found |
8. Admin-Initiate Claim
Endpoint: POST /api/v1/e-events/claims/event/{eventId}/admin-initiate
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Admin submits a fund claim on behalf of the organizer. Used when the event is still active (ticket sales ongoing). The claim is flagged adminInitiated = true with a mandatory audit note.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
eventId |
UUID |
Yes | The event to claim funds for |
Request Body:
{
"adminNote": "Organizer requested early release. Event ending tomorrow, no pending refunds."
}
Request Body Fields:
| Field | Type | Required | Description | Validation |
|---|---|---|---|---|
adminNote |
string |
Yes | Mandatory reason for admin-initiated claim | Not blank |
Guard Rules Applied Server-Side:
claimableAmountmust be > 0- No existing
PENDINGclaim for this event - Claim is capped at
claimableAmount(never fulltotalRevenue)
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Admin-initiated claim created",
"data": {
"claimId": "uuid",
"claimNumber": "EFC-2026-000003",
"status": "PENDING",
"adminInitiated": true,
"adminId": "uuid-of-admin",
"adminNote": "Organizer requested early release. Event ending tomorrow, no pending refunds.",
"claimedAmount": 64000.00,
"currency": "TZS"
}
}
Possible Errors:
| HTTP Status | Description |
|---|---|
400 BAD_REQUEST |
A pending claim already exists for this event |
400 BAD_REQUEST |
Claimable amount is zero — nothing to claim |
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Caller does not have admin role |
404 NOT_FOUND |
Event not found |
422 UNPROCESSABLE_ENTITY |
adminNote is blank |
9. Approve Claim (Admin)
Endpoint: POST /api/v1/e-events/claims/{claimId}/approve
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Admin approves a pending claim. This atomically acquires a pessimistic write lock on the EscrowAccount, fetches all escrows currently in HELD status for the event, releases every one of them, and credits the full released amount to the organizer's wallet.
Design note: Approval releases ALL
HELDescrows — not just theclaimedAmountsubset. Each escrow is atomic (one buyer's payment for one checkout session) so partial release has no valid business meaning. TheclaimedAmounton the claim is a snapshot estimate taken at submission time. TheactualReleasedAmountstored on approval is the authoritative number — it reflects the liveHELDbalance at the moment the admin approves, and may differ if refunds were processed in between.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
claimId |
UUID |
Yes | The claim to approve |
Request Body (optional):
{
"reviewNote": "Verified escrow balance. Approved for release."
}
Request Body Fields:
| Field | Type | Required | Description |
|---|---|---|---|
reviewNote |
string |
No | Optional note from reviewing admin |
Critical: Approval acquires a
PESSIMISTIC_WRITElock on theEscrowAccountrow. Concurrent refund operations for the same event will block until the approval transaction completes, preventing escrow from going negative.
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Fund claim approved and funds released",
"data": {
"claimId": "uuid",
"claimNumber": "EFC-2026-000001",
"status": "APPROVED",
"claimedAmount": 50000.00,
"actualReleasedAmount": 48000.00,
"escrowsReleasedCount": 12,
"escrowsSkippedCount": 1,
"reviewedById": "admin-uuid",
"reviewerName": "Admin John",
"reviewNote": "Verified escrow balance. Approved for release.",
"reviewedAt": "2026-04-27T14:30:00"
}
}
Note on
actualReleasedAmount: May be less thanclaimedAmountif one or more escrow entries were refunded between claim submission and admin approval. TheescrowsSkippedCountindicates how many entries were skipped for this reason.
Possible Errors:
| HTTP Status | Description |
|---|---|
400 BAD_REQUEST |
Claim is not in PENDING status |
400 BAD_REQUEST |
Escrow balance insufficient to cover claim amount |
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Caller does not have admin role |
404 NOT_FOUND |
Claim not found |
10. Reject Claim (Admin)
Endpoint: POST /api/v1/e-events/claims/{claimId}/reject
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Admin rejects a pending claim. No funds are moved. The organizer may submit a new claim after rejection.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
claimId |
UUID |
Yes | The claim to reject |
Request Body (optional):
{
"reviewNote": "Pending dispute investigation. Please resubmit after resolution."
}
Request Body Fields:
| Field | Type | Required | Description |
|---|---|---|---|
reviewNote |
string |
No | Reason for rejection (shown to organizer) |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Fund claim rejected",
"data": {
"claimId": "uuid",
"claimNumber": "EFC-2026-000001",
"status": "REJECTED",
"reviewedById": "admin-uuid",
"reviewerName": "Admin John",
"reviewNote": "Pending dispute investigation. Please resubmit after resolution.",
"reviewedAt": "2026-04-27T14:35:00"
}
}
Possible Errors:
| HTTP Status | Description |
|---|---|
400 BAD_REQUEST |
Claim is not in PENDING status |
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Caller does not have admin role |
404 NOT_FOUND |
Claim not found |
11. Cancel Claim (Organizer)
Endpoint: DELETE /api/v1/e-events/claims/{claimId}
Access: 🔒 Authenticated organizer — must be the claim owner
Purpose: Organizer cancels their own pending claim before any admin action. This frees up the pending amount, allowing a new claim to be submitted.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
claimId |
UUID |
Yes | The claim to cancel |
No request body.
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Fund claim cancelled",
"data": null
}
Possible Errors:
| HTTP Status | Description |
|---|---|
400 BAD_REQUEST |
Claim is not in PENDING status (already reviewed) |
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Organizer does not own this claim |
404 NOT_FOUND |
Claim not found |
Financial Safety: Refund & Claim Concurrency
This section explains how the system prevents money from being over-released when a refund and a claim approval happen simultaneously, and why partial escrow release is not supported.
Why Partial Release Is Not Supported
Each escrow is atomic — it represents one buyer's payment for one checkout session. To release "80% of claimable funds" the service would have to arbitrarily pick specific escrow entries to release and leave others HELD. There is no business logic that justifies which individual buyer's payment stays locked — the organizer delivered the event to all of them equally.
10 escrows × 950 TZS = 9,500 TZS claimable
"Release 80%" = 7,600 TZS = release 8 escrows, hold 2
→ WHY are those 2 buyers' payments still locked?
→ No valid answer exists at the escrow layer.
The correct model is:
Claim approval → ALL HELD escrows released → Organizer wallet (full amount lands)
↓
Organizer controls staged
payouts via DisbursementService
The organizer's wallet is the right layer for staged payouts. The claim domain's job is simply: verify eligibility, get admin approval, atomically convert all HELD escrows into wallet balance.
The Race Condition Risk
Revenue = 10,000 TZS in HELD escrows
PENDING claim submitted
→ Refund of 2,000 processed concurrently
→ Claim approved → releases all HELD (now 8,000)
→ Refund deducts 2,000 from already-decremented escrow
→ Escrow goes negative 💀
Protections in Place
| Layer | Mechanism |
|---|---|
| Refund deadline | No refunds allowed within 3 days of event. Eliminates the race entirely post-deadline. |
| Pessimistic DB lock | Both approveClaim() and processRefund() acquire PESSIMISTIC_WRITE lock on the EscrowAccount row — serializing the two operations. |
| Balance check | Before any deduction, service verifies escrow.balance >= amount. Throws InsufficientEscrowException if not. |
| Snapshot vs actual | claimedAmount is a snapshot estimate at submission time. actualReleasedAmount is the real released amount at approval time — reflects live HELD balance after any intervening refunds. |
escrowsSkippedCount |
Tracks how many escrow entries were already REFUNDED or DISPUTED at approval time and therefore skipped. |
Claimable Amount Formula
claimableAmount = totalRevenue − totalRefunded − totalClaimed − totalPendingClaims
This formula gives an estimate of what's claimable. The actual amount released on approval equals the live sum of all HELD escrow sellerAmount values at that moment.
Endpoint Quick Reference
| # | Method | Path | Access | Description |
|---|---|---|---|---|
| 1 | GET |
/claims |
Admin | List all claims (filter by status) |
| 2 | GET |
/claims/my-claims |
Organizer | Get my own claims |
| 3 | GET |
/claims/{claimId} |
Organizer / Admin | Get single claim by ID |
| 4 | GET |
/claims/event/{eventId} |
Organizer / Admin | Get all claims for an event |
| 5 | GET |
/claims/event/{eventId}/claimable-amount |
Organizer / Admin | Get claimable amount breakdown |
| 6 | GET |
/claims/event/{eventId}/revenue-summary |
Admin | Full event revenue summary |
| 7 | POST |
/claims/event/{eventId} |
Organizer | Submit a new claim |
| 8 | POST |
/claims/event/{eventId}/admin-initiate |
Admin | Admin-initiate claim for active event |
| 9 | POST |
/claims/{claimId}/approve |
Admin | Approve claim + release escrow |
| 10 | POST |
/claims/{claimId}/reject |
Admin | Reject claim |
| 11 | DELETE |
/claims/{claimId} |
Organizer | Cancel pending claim |
Standard Error Reference
| HTTP Status | Scenario |
|---|---|
400 BAD_REQUEST |
Business rule violation (duplicate pending, zero claimable, wrong status) |
401 UNAUTHORIZED |
Missing, expired, or malformed JWT |
403 FORBIDDEN |
Insufficient role or ownership violation |
404 NOT_FOUND |
Event or claim does not exist |
422 UNPROCESSABLE_ENTITY |
Validation failure (e.g. blank adminNote) |
500 INTERNAL_SERVER_ERROR |
Unexpected server error |