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 → TICKETS. 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. - Banner media is upload-then-reference, not a URL string. Upload the banner image through the FileThunder upload endpoint first to obtain a
fileId, then pass thatfileIdasbannerFileIdwhen creating/updating the event. The API resolves it into a full media object (bannerMedia) in responses — do not construct or pass CDN URLs directly. - On a
PUBLISHEDevent, the banner can only be changed via the dedicatedPATCH /{eventId}/published/bannerendpoint (query param, not body) — it is no longer part of the "Update Published Basic Info" request.
Event Creation User Journey
[Organizer]
│
│ POST /draft
▼
┌─────────────┐
│ BASIC INFO │ title, category, format, description, bannerFileId (optional)
└──────┬──────┘
│ 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)
│
│ (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, ctaLabel auto-derived if not set,
│ │ 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). Stripped to null on the public GET /{eventId} endpoint — only returned to the organizer via draft/my-events endpoints |
virtualDetails.meetingId |
string | Platform meeting ID (same stripping rule as above) |
virtualDetails.passcode |
string | Meeting passcode (same stripping rule as above) |
bannerMedia |
object | null | Resolved banner media object. See EventBannerMedia definition below. null if no banner was ever set |
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 |
object | See OrganizerInfo definition below |
ctaLabel |
string | CTA button label (e.g., "Get Tickets", "Register for Free"). Auto-derived from ticket pricing at publish if not explicitly set |
hasApplicantForm |
boolean | Whether an applicant form is configured for this event |
applicantForm.displayTime |
string | When the form is shown: BEFORE_CHECKOUT or AFTER_CHECKOUT |
applicantForm.isRequired |
boolean | Whether form submission is required for online attendees |
applicantForm.applyToAtDoor |
boolean | Whether the form also applies to at-door check-in |
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 |
profileMedia |
object | null | Resolved profile picture — only populated for PLATFORM_USER entries, auto-refreshed from the linked account on every read. Same lean shape as EventBannerPrimaryMedia (fileId, status, thumbUrl, mimeType). CUSTOM entries have no image/media at all — there is no way to attach a photo to a custom lineup entry |
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 |
presenterMedia |
object | null | Resolved profile picture — only populated for PLATFORM_USER presenters, auto-refreshed from the linked account on every read. Same lean shape as EventBannerPrimaryMedia (fileId, status, thumbUrl, mimeType). CUSTOM presenters have no image/media at all — there is no way to attach a photo to a custom presenter |
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) |
salesChannel |
string | Ticket sales channel enum value |
ticketPricingType |
string | FREE, PAID, or DONATION |
totalTickets |
integer | Total slots allocated |
ticketsSold |
integer | Number sold |
ticketsAvailable |
integer | Remaining slots |
ticketsReserved |
integer | Slots currently held in an active (unpaid) checkout |
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 within its sales window |
saleStatusMessage |
string | Human-readable explanation of the current sale status |
EventBannerMedia (full media object — on EventResponse)
| Field | Type | Description |
|---|---|---|
fileId |
UUID | FileThunder file identifier |
status |
string | File processing status enum (e.g., READY, PROCESSING, FAILED) |
variants |
map<string,string> | All resolved size/format variants, keyed by variant name (e.g., "thumb", "medium", "original") → signed/public URL |
mimeType |
string | MIME type of the uploaded file |
EventBannerPrimaryMedia (lean media object — on EventSummaryResponse)
Lightweight single-URL counterpart used in list/search results — only the "thumb" variant is resolved (falls back to the first available variant if no thumb exists).
| Field | Type | Description |
|---|---|---|
fileId |
UUID | FileThunder file identifier |
status |
string | File processing status enum |
thumbUrl |
string | Single resolved thumbnail URL |
mimeType |
string | MIME type of the uploaded file |
OrganizerInfo (on EventResponse only)
| Field | Type | Description |
|---|---|---|
id |
UUID | Organizer account ID |
name |
string | Organizer full name (firstName + " " + lastName) |
username |
string | Organizer system username |
profileMedia |
object | null | Organizer's profile picture, lean shape — same fields as EventBannerPrimaryMedia (fileId, status, thumbUrl, mimeType) |
Note:
EventSummaryResponse(list/search results) does not use this nested object — it still exposes flatorganizerId/organizerName/organizerUsernamefields with no profile picture at all. This is a known inconsistency versus the fullEventResponseshape (which was updated to includeprofileMedia); the summary shape has not been migrated yet.
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") |
bannerMedia |
object | null | Lean resolved banner media — see EventBannerPrimaryMedia above. null if no banner was ever set |
hasApplicantForm |
boolean | Whether an applicant form is configured |
ctaLabel |
string | CTA button label |
pricing.pricingType |
string | FREE, PAID, DONATION, or MIXED — frontend uses this directly for the display label |
pricing.minPrice |
BigDecimal | null | Lowest available ticket price (PAID/FREE tickets only, excludes DONATION). null if pricing is FREE-only or DONATION-only |
pricing.maxPrice |
BigDecimal | null | Highest available ticket price (same exclusion rule as minPrice) |
pricing.hasFreeTickets |
boolean | True when at least one active FREE ticket type exists |
pricing.hasPaidTickets |
boolean | True when at least one active PAID ticket type exists |
pricing.hasDonationTickets |
boolean | True when at least one active DONATION ticket type exists |
organizerId |
UUID | Organizer ID (flat field — no nested media/profile picture on this lightweight object, see note above) |
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.
D. EventStatusResponse (Lightweight Status Object)
Returned by the publish/unpublish/cancel transition endpoints. These no longer return the full EventResponse — only the minimal fields needed to confirm the transition.
| Field | Type | Description |
|---|---|---|
id |
UUID | Event ID |
slug |
string | Event URL slug |
status |
string | New status after the transition (PUBLISHED, DRAFT, or CANCELLED) |
If you need the full updated event (media, tickets, organizer, etc.) after a publish/unpublish/cancel call, follow up with
GET /{eventId}(Endpoint 25) orGET /drafts/{draftId}(Endpoint 3).
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.",
"bannerFileId": "6b1a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"
}
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 |
bannerFileId |
UUID | No | ID of a file already uploaded via FileThunder to use as the banner | Must reference an existing uploaded file |
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": 25,
"canPublish": false,
"schedule": null,
"venue": null,
"virtualDetails": null,
"bannerMedia": {
"fileId": "6b1a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"status": "READY",
"variants": {
"thumb": "https://cdn.example.com/ft/6b1a2c3d/thumb.jpg",
"medium": "https://cdn.example.com/ft/6b1a2c3d/medium.jpg",
"original": "https://cdn.example.com/ft/6b1a2c3d/original.jpg"
},
"mimeType": "image/jpeg"
},
"highlights": null,
"faqs": null,
"lineup": null,
"agenda": null,
"linkedProducts": [],
"linkedShops": [],
"tickets": [],
"organizer": {
"id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"name": "Amina Hassan",
"username": "amina.hassan",
"profileMedia": {
"fileId": "9f8e7d6c-5b4a-3c2d-1e0f-a1b2c3d4e5f6",
"status": "READY",
"thumbUrl": "https://cdn.example.com/ft/9f8e7d6c/thumb.jpg",
"mimeType": "image/jpeg"
}
},
"ctaLabel": null,
"hasApplicantForm": false,
"applicantForm": null,
"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,
"bannerMedia": {
"fileId": "6b1a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"status": "READY",
"thumbUrl": "https://cdn.example.com/ft/6b1a2c3d/thumb.jpg",
"mimeType": "image/jpeg"
},
"hasApplicantForm": false,
"ctaLabel": null,
"pricing": {
"pricingType": "FREE",
"minPrice": null,
"maxPrice": null,
"hasFreeTickets": false,
"hasPaidTickets": false,
"hasDonationTickets": false
},
"organizerId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"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",
"ctaLabel": "Get Tickets",
"bannerFileId": "6b1a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"
}
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 |
ctaLabel |
string | No | CTA button label override | Max: 50 characters. If omitted, existing value is kept; auto-derived at publish if never set |
bannerFileId |
UUID | No | ID of a file already uploaded via FileThunder to replace the banner | Must reference an existing uploaded file |
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. Advances currentStage to TICKETS.
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 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 |
Missing required venue/virtual fields for the event format |
8. 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 |
9. 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 8 (401, 403, 404).
10. 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.",
"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[].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, andprofileMedia(resolved profile picture object) from the user's profile. These are also refreshed on everyGETof the event. There is noimagefield on this request —CUSTOMentries cannot have a photo at all; onlyPLATFORM_USERentries get one, sourced from the linked account.
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 |
11. 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 |
There is no
presenterImagefield on this request —CUSTOMpresenters cannot have a photo at all; onlyPLATFORM_USERpresenters get one (presenterMedia, resolved profile picture object), sourced from the linked account.
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 |
12. 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/{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 |
13. 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/{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 |
14. 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/{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 |
15. 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/{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 13 (401, 403, 404).
16. 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, generates an RSA key pair used for secure ticket QR code signing, and auto-derives the ctaLabel from ticket pricing if the organizer has not set one.
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 ✓- At least one active ticket exists for the event ✓
- 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": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"slug": "dar-es-salaam-jazz-festival-2025-a1b2c3d4",
"status": "PUBLISHED"
}
}
Success Response Fields: data is an EventStatusResponse — not the full EventResponse.
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 no active tickets exist |
500 |
RSA key generation failed |
17. 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": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"slug": "dar-es-salaam-jazz-festival-2025-a1b2c3d4",
"status": "DRAFT"
}
}
Success Response Fields: data is an EventStatusResponse — not the full EventResponse.
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 |
18. 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": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"slug": "dar-es-salaam-jazz-festival-2025-a1b2c3d4",
"status": "CANCELLED"
}
}
Success Response Fields: data is an EventStatusResponse — not the full EventResponse.
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 |
19. Update Published Event — Basic Info
Purpose: Updates the description and/or CTA label of a published event. Title, category, and format changes are blocked on published events as the slug has already been shared publicly. Banner updates are no longer part of this endpoint — use Endpoint 19a — Update Published Event — Banner instead.
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.",
"ctaLabel": "Get Tickets Now"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
description |
string | No | Updated event description | Min: 15, Max: 5000 characters |
ctaLabel |
string | No | CTA button label override | Max: 50 characters. If provided and non-blank → saved as-is. If omitted or null → re-derived from current ticket pricing |
Excluded fields:
title,categoryId, andeventFormatare not present on this request at all (too breaking for a published event whose slug is already shared).eventVisibilityand banner media are also not accepted here — banner updates now go through Endpoint 19a as a separate call.
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Event info updated",
"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 |
19a. Update Published Event — Banner
Purpose: Replaces the banner image on a published event. Split out from the basic-info endpoint since the banner is a FileThunder-managed upload, not a plain string field.
Endpoint: PATCH /api/v1/e-events/{eventId}/published/banner
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 published event to update | Must be a valid UUID |
Query Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
bannerFileId |
UUID | Yes | ID of a file already uploaded via FileThunder to use as the new banner | Must reference an existing uploaded file |
Note: Unlike every other update endpoint in this API, this one takes its input as a query parameter, not a JSON request body — there is no request body for this call.
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Event banner updated",
"action_time": "2025-02-17T10:30:45",
"data": { "...": "Full EventResponse object" }
}
Success Response Fields: data is a full EventResponse. data.bannerMedia reflects the newly resolved banner.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
403 |
Not the event organizer |
404 |
Event not found, or bannerFileId does not reference an existing file |
400 |
Event is not in PUBLISHED status |
20. 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 8 — 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 |
21. 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 9 — 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 20 (401, 403, 404, 400).
22. 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 10 — 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 |
22a. Update Check-in Settings
Purpose: Configures the check-in window for attendee entry — how early before start, and how late after start, attendees are allowed to check in.
Endpoint: PATCH /api/v1/e-events/{eventId}/checkin-settings
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 event to update | Must be a valid UUID |
Request JSON Sample:
{
"checkInBeforeValue": 2,
"checkInBeforeUnit": "HOURS",
"checkInAfterValue": 30,
"checkInAfterUnit": "MINUTES"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
checkInBeforeValue |
integer | Yes | How long before start check-in opens | Min: 1 |
checkInBeforeUnit |
string | Yes | Unit for checkInBeforeValue |
Enum: MINUTES, HOURS, DAYS |
checkInAfterValue |
integer | Yes | How long after start check-in remains open | Min: 0 |
checkInAfterUnit |
string | Yes | Unit for checkInAfterValue |
Enum: MINUTES, HOURS, DAYS |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Check-in settings updated",
"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 |
422 |
Missing/invalid check-in value or unit |
23. 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 11 — 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 |
24. 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 |
25. 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. data.virtualDetails is always null on this endpoint — it uses the public-response mapping, which strips meeting link/ID/passcode even when the requester is the event's own organizer. While the event is still DRAFT, virtual meeting details can be read via GET /drafts/{draftId} (Endpoint 3); there is currently no endpoint that returns virtualDetails for an already-PUBLISHED event, even to its organizer.
Possible Error Responses:
| Status | Scenario |
|---|---|
403 |
Attempting to access a DRAFT event without being its organizer |
404 |
Event not found or is soft-deleted |
26. 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.
27. 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 26.
Possible Error Responses:
| Status | Scenario |
|---|---|
401 |
No or expired token |
400 |
Invalid status enum value |
28. 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).
28a. Get Past Events
Purpose: Returns a paginated list of PUBLISHED events whose end date/time has already passed (e.g., for an "Events you missed" / past-events archive view). Note that this is distinct from COMPLETED status — an event moves to COMPLETED via a separate scheduled job, so there can be a lag where a past-dated event still shows here as PUBLISHED.
Endpoint: GET /api/v1/e-events/events-feed/past
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": "Past events retrieved successfully",
"action_time": "2025-02-17T10:30:45",
"data": {
"content": [ "[ EventSummaryResponse objects — PUBLISHED, end date in the past ]" ],
"totalElements": 23,
"totalPages": 3
}
}
Success Response Fields: data.content[] contains EventSummaryResponse objects.
Possible Error Responses: None expected (no authentication required).
29. 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).
30. 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 |
31. 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) |
32. 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 |
33. Get Event OG Meta by Slug
Purpose: Returns lightweight Open Graph metadata for a published event, for use by link-preview crawlers (social media unfurling, messaging apps) when someone shares an event URL.
Endpoint: GET /api/v1/e-events/slug/{slug}/og-meta
Access Level: 🌐 Public
Authentication: None required
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
slug |
string | Yes | The event's URL slug | Must match an existing event |
⚠️ Non-standard response shape: Unlike every other endpoint in this API, this one returns the
EventOGMetaResponseobject directly as the HTTP body — it is not wrapped in the standard{ success, httpStatus, message, action_time, data }envelope described at the top of this document. Build OG-meta consumers accordingly.
Success Response JSON Sample:
{
"title": "Dar es Salaam Jazz Festival 2025",
"eventId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"description": "An annual celebration of jazz and live music at the heart of the city.",
"banner": "https://cdn.example.com/ft/6b1a2c3d/medium.jpg",
"startDateTime": "2025-07-18T18:00:00+03:00",
"locationSummary": "Dar es Salaam, TZ"
}
Success Response Fields:
| Field | Type | Description |
|---|---|---|
title |
string | Event title |
eventId |
UUID | Event ID |
description |
string | Full event description |
banner |
string | null | Resolved banner image URL (single string, not the full bannerMedia object) |
startDateTime |
ZonedDateTime | Event start date/time with offset |
locationSummary |
string | Human-readable location summary |
Possible Error Responses:
| Status | Scenario |
|---|---|
404 |
No event found with the given slug |
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
- Wallet balance is validated at session creation time for PAID tickets — if insufficient, the session is never created and a structured
422response is returned withshortfallandrecommendedTopUpso the frontend can direct the user to top up immediately - 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"
}
Insufficient Balance Error Structure (422)
Returned when wallet balance is insufficient at session creation time for a PAID ticket. No session is created. The data field carries rich balance details so the frontend can guide the user directly to top up.
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Insufficient wallet balance to complete checkout",
"action_time": "2025-09-23T10:30:45",
"data": {
"walletBalance": 50000.00,
"sessionTotal": 150000.00,
"shortfall": 100000.00,
"hasSufficientBalance": false,
"recommendedTopUp": 100000.00,
"pspMinimum": 500.00,
"currency": "TZS"
}
}
| Field | Description |
|---|---|
walletBalance |
Current wallet balance of the user in TZS |
sessionTotal |
Total amount required for this checkout |
shortfall |
Amount missing (sessionTotal - walletBalance) |
hasSufficientBalance |
Always false when this error is returned |
recommendedTopUp |
Suggested top-up amount — at minimum equals shortfall, rounded up to pspMinimum if needed |
pspMinimum |
Minimum top-up amount accepted by the payment provider |
currency |
Always TZS |
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
| [ Wallet balance check ]
| ....................
| . INSUFFICIENT .
| ....................
| |
| [ 422 response — no session created ]
| [ data: { shortfall, recommendedTopUp, ... } ]
|
| ....................
| . SUFFICIENT .
| ....................
| |
v v
[ Auto-processed immediately ] [ Ticket hold applied ]
| [ Session = PENDING_PAYMENT ]
| |
| 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, 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 |
Insufficient wallet balance (returns rich data with shortfall and recommendedTopUp); or validation failed on request fields |
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, or session is expired |
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 payment processing or booking creation error |
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 ]
|
v
[ assertSufficientBalanceForCheckout(total) ]
............................................
. INSUFFICIENT BALANCE .
............................................
|
[ 422 returned immediately ]
[ No session created ]
[ data: { ]
[ walletBalance, ]
[ sessionTotal, ]
[ shortfall, ]
[ recommendedTopUp ]
[ } ]
............................................
. SUFFICIENT BALANCE .
............................................
|
v
[ 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 at session creation — if insufficient, a
422is returned with rich balance data (shortfall,recommendedTopUp) and no session is created. A second safety-net check runs at actual payment time in case the balance changed between the two calls - 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 manages confirmed ticket purchases for events on the Nexgate platform. It allows customers to view their bookings with JWT-signed QR codes, track multi-day check-in history, download ticket PDFs, and enables event organizers to monitor all orders for their events with filtering and revenue stats.
Hints:
- Bookings are created automatically after successful checkout payment — never created directly via this API
- QR codes are RSA-signed JWTs containing full ticket and event data; they cannot be forged
- Event details (title, venue, organizer) are snapshotted at booking time and never change on a booking record
- Ticket series are auto-generated (e.g.,
VIP-0001,GENER-0042) using a per-ticket-type counter - Multi-day events store a full
checkInsarray per ticket — one record per day attended - Customers see their own bookings only; organizers see bookings for their events; admins see all
- The PDF endpoint returns a binary file, not a JSON response
Standard Response Format
All JSON endpoints follow a consistent structure using the Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2026-05-23T10:30:45",
"data": {}
}
Error Response Structure
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Booking not found",
"action_time": "2026-05-23T10:30:45",
"data": "Booking not found"
}
Standard Response Fields
| Field | Type | Description |
|---|---|---|
success |
boolean | true for successful operations, false for errors |
httpStatus |
string | HTTP status name (OK, NOT_FOUND, FORBIDDEN, 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 message string on failure |
HTTP Method Badge Standards
- GET - GET - Green (Safe, read-only operations)
- POST - POST - Blue (Create new resources)
- PUT - PUT - Yellow (Update/replace entire resource)
- PATCH - PATCH - Orange (Partial updates)
- DELETE - DELETE - Red (Remove resources)
Endpoints
1. Get Booking by ID
Purpose: Retrieve complete booking details including all tickets, check-in history, and event snapshot for a specific booking.
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 | Validation |
|---|---|---|---|---|
bookingId |
string (UUID) | Yes | Unique booking order ID | Must be valid UUID |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Booking retrieved successfully",
"action_time": "2026-05-23T10:30:45",
"data": {
"bookingId": "550e8400-e29b-41d4-a716-446655440000",
"bookingReference": "EVT-A3F4B21C",
"status": "CONFIRMED",
"formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"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",
"hasApplicantForm": true,
"virtualDetails": {
"meetingLink": "https://zoom.us/j/123456789",
"meetingId": "123 456 789",
"passcode": "summit2025"
}
},
"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",
"formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"ticketTypeName": "VIP Pass",
"ticketSeries": "VIP-0001",
"ticketNumber": "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",
"buyerType": "SYSTEM_USER"
},
"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",
"formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"ticketTypeName": "General Admission",
"ticketSeries": "GENER-0042",
"ticketNumber": "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",
"buyerType": "SYSTEM_USER"
},
"checkIns": [],
"hasBeenCheckedIn": false,
"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
}
}
Success Response Fields:
| Field | Description |
|---|---|
bookingId |
Unique booking UUID |
bookingReference |
Short readable code (e.g., EVT-A3F4B21C) |
status |
Booking status: CONFIRMED, CANCELLED |
formResponseId |
UUID of the buyer's applicant form response — null if the event had no form |
event.eventId |
Event UUID |
event.title |
Event title at time of booking (immutable snapshot) |
event.startDateTime |
Event start — LocalDateTime (no timezone) |
event.endDateTime |
Event end — LocalDateTime (no timezone) |
event.timezone |
IANA timezone string (e.g., Africa/Nairobi) |
event.location |
Full venue address snapshotted at booking |
event.format |
IN_PERSON, ONLINE, or HYBRID |
event.hasApplicantForm |
true if the event had an applicant form at time of booking |
event.virtualDetails |
Virtual meeting info — only present for ONLINE/HYBRID events |
event.virtualDetails.meetingLink |
Full meeting URL |
event.virtualDetails.meetingId |
Meeting ID (optional) |
event.virtualDetails.passcode |
Meeting passcode (optional) |
organizer.name |
Organizer name snapshotted at booking |
organizer.email |
Organizer email |
organizer.phone |
Organizer phone |
customer.customerId |
Customer account UUID |
customer.name |
Customer username |
customer.email |
Customer email |
tickets |
Array of all booked ticket instances |
tickets[].ticketInstanceId |
Unique ticket instance UUID |
tickets[].formResponseId |
UUID of the applicant form response linked to this ticket — same as order-level formResponseId |
tickets[].ticketTypeName |
Ticket type name (e.g., VIP Pass) |
tickets[].ticketSeries |
Auto-generated series (e.g., VIP-0001) |
tickets[].ticketNumber |
Same as ticketSeries — used for PDF labeling |
tickets[].price |
Price paid for this ticket |
tickets[].qrCode |
RSA-signed JWT — used for gate check-in scanning |
tickets[].attendanceMode |
IN_PERSON or ONLINE (relevant for hybrid events) |
tickets[].attendee |
Person this ticket is for |
tickets[].attendee.name |
Attendee full name |
tickets[].attendee.email |
Attendee email |
tickets[].attendee.phone |
Attendee phone |
tickets[].buyer |
Person who purchased this ticket |
tickets[].buyer.name |
Buyer name |
tickets[].buyer.email |
Buyer email (null for AT_DOOR purchases) |
tickets[].buyer.buyerType |
SYSTEM_USER (online) or AT_DOOR (sold at gate) |
tickets[].checkIns |
Full check-in history — one record per day attended |
tickets[].checkIns[].checkInTime |
ZonedDateTime of check-in |
tickets[].checkIns[].checkInLocation |
Where checked in (e.g., Main Gate, VIP Entrance) |
tickets[].checkIns[].checkedInBy |
Staff/scanner operator name |
tickets[].checkIns[].dayName |
Event day label (e.g., Day 1 - Opening Day) |
tickets[].checkIns[].scannerId |
Scanner device ID |
tickets[].checkIns[].checkInMethod |
QR_SCAN (default), MANUAL, or NFC |
tickets[].hasBeenCheckedIn |
true if ticket has at least one check-in |
tickets[].lastCheckedInAt |
Most recent check-in time (ZonedDateTime, null if none) |
tickets[].lastCheckedInBy |
Who performed the most recent check-in (null if none) |
tickets[].lastCheckInLocation |
Location of most recent check-in (null if none) |
tickets[].lastCheckInDayName |
Day label of most recent check-in (null if none) |
tickets[].status |
ACTIVE, USED, or CANCELLED |
tickets[].validFrom |
Ticket validity start — ZonedDateTime |
tickets[].validUntil |
Ticket validity end — ZonedDateTime |
totalTickets |
Total ticket count in this booking |
checkedInTicketsCount |
Count of tickets with at least one check-in |
subtotal |
Subtotal before any deductions |
total |
Total amount paid |
bookedAt |
When the booking was created — LocalDateTime |
cancelledAt |
When cancelled — LocalDateTime (null if still active) |
Error Response JSON Sample:
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Booking not found: 550e8400-e29b-41d4-a716-446655440000",
"action_time": "2026-05-23T10:30:45",
"data": "Booking not found: 550e8400-e29b-41d4-a716-446655440000"
}
Standard Error Types:
2. Get My Bookings
Purpose: Retrieve a summary list of all bookings for the authenticated user, sorted newest first.
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 JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Bookings retrieved successfully",
"action_time": "2026-05-23T10: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",
"formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
},
{
"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",
"formResponseId": null
}
]
}
Success Response Fields:
| Field | Description |
|---|---|
bookingId |
Unique booking UUID |
bookingReference |
Short readable code (e.g., EVT-A3F4B21C) |
status |
CONFIRMED or CANCELLED |
eventTitle |
Event title snapshotted at booking time |
eventStartDateTime |
Event start date/time — LocalDateTime (no timezone) |
eventLocation |
Venue address snapshotted at booking time |
totalTickets |
Total number of tickets in this booking |
checkedInTickets |
Number of tickets with at least one check-in |
total |
Total amount paid |
bookedAt |
When booking was created — LocalDateTime |
formResponseId |
UUID of the applicant form response — null if the event had no form |
Error Response JSON Sample:
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "User not found",
"action_time": "2026-05-23T10:30:45",
"data": "User not found"
}
Standard Error Types:
3. Download Ticket PDF
Purpose: Download or preview the PDF ticket for a specific ticket instance belonging to the authenticated user.
Endpoint: GET {base_url}/e-events/booking-orders/tickets/{ticketInstanceId}/pdf
Access Level: 🔒 Protected (Ticket Owner)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer token (format: Bearer <token>) |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
ticketInstanceId |
string (UUID) | Yes | Unique ID of the ticket to download | Must be valid UUID |
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
mode |
string | No | download prompts a save dialog; inline opens the PDF in the browser tab |
download |
Success Response: Binary PDF file (not JSON)
Success Response Headers:
| Header | Value |
|---|---|
Content-Type |
application/pdf |
Content-Disposition |
attachment; filename="ticket-{series}.pdf" or inline; ... by mode |
PDF Content Includes:
- Event name, date, and venue
- Attendee name
- Ticket series and type
- QR code (RSA-signed JWT) for gate scanning
- Ticket validity period
Error Response JSON Sample:
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "You don't have permission to access this ticket",
"action_time": "2026-05-23T10:30:45",
"data": "You don't have permission to access this ticket"
}
Standard Error Types:
4. Get Event Orders (Organizer)
Purpose: Retrieve a paginated list of all booking orders for a specific event with filtering by status, buyer type, and ticket type, plus aggregated revenue and order stats.
Endpoint: GET {base_url}/e-events/booking-orders/event/{eventId}
Access Level: 🔒 Protected (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 | Validation |
|---|---|---|---|---|
eventId |
string (UUID) | Yes | Target event UUID | Must be valid UUID |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
status |
string (enum) | No | Filter by booking status | CONFIRMED, CANCELLED |
— |
buyerType |
string (enum) | No | Filter by how the ticket was purchased | SYSTEM_USER, AT_DOOR |
— |
ticketTypeId |
string (UUID) | No | Filter orders that include a specific ticket type | Must be valid UUID | — |
search |
string | No | Search by buyer name, email, or booking reference | — | — |
page |
integer | No | Zero-based page index | Min: 0 | 0 |
size |
integer | No | Number of orders per page | Min: 1 | 20 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Event orders retrieved",
"action_time": "2026-05-23T10:30:45",
"data": {
"eventId": "770e8400-e29b-41d4-a716-446655440002",
"eventTitle": "East African Tech Summit 2025",
"appliedFilters": {
"status": "CONFIRMED",
"buyerType": null,
"ticketTypeId": null,
"ticketTypeName": null,
"search": null
},
"stats": {
"totalOrders": 142,
"totalTicketsSold": 318,
"totalRevenue": 47500.00,
"confirmedOrders": 139,
"cancelledOrders": 3,
"refundedOrders": 0,
"onlineOrders": 128,
"atDoorOrders": 14
},
"orders": [
{
"bookingId": "550e8400-e29b-41d4-a716-446655440000",
"bookingReference": "EVT-A3F4B21C",
"status": "CONFIRMED",
"bookedAt": "2025-12-11T10:30:45",
"formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"buyer": {
"buyerName": "John Doe",
"buyerEmail": "john@example.com",
"buyerType": "SYSTEM_USER"
},
"totalTickets": 2,
"checkedInCount": 1,
"total": 200.00,
"ticketTypes": [
{ "ticketTypeName": "VIP Pass", "quantity": 1 },
{ "ticketTypeName": "General Admission", "quantity": 1 }
]
},
{
"bookingId": "550e8400-e29b-41d4-a716-446655440099",
"bookingReference": "EVT-C7E1F34A",
"status": "CONFIRMED",
"bookedAt": "2025-12-10T18:15:00",
"formResponseId": null,
"buyer": {
"buyerName": "Grace Njeri",
"buyerEmail": null,
"buyerType": "AT_DOOR"
},
"totalTickets": 1,
"checkedInCount": 1,
"total": 50.00,
"ticketTypes": [
{ "ticketTypeName": "General Admission", "quantity": 1 }
]
}
],
"pagination": {
"currentPage": 0,
"pageSize": 20,
"totalPages": 8,
"totalElements": 142,
"hasNext": true,
"hasPrevious": false
}
}
}
Success Response Fields:
| Field | Description |
|---|---|
eventId |
UUID of the queried event |
eventTitle |
Event title |
appliedFilters |
Echo of the active filter values (null = not filtered) |
appliedFilters.status |
Active booking status filter |
appliedFilters.buyerType |
Active buyer type filter |
appliedFilters.ticketTypeId |
Active ticket type UUID filter |
appliedFilters.ticketTypeName |
Resolved name of the filtered ticket type |
appliedFilters.search |
Active search string |
stats |
Aggregated stats across ALL orders for the event (not just current page) |
stats.totalOrders |
Total booking count |
stats.totalTicketsSold |
Total individual tickets across all bookings |
stats.totalRevenue |
Sum of all order totals |
stats.confirmedOrders |
Orders with status CONFIRMED |
stats.cancelledOrders |
Orders with status CANCELLED |
stats.refundedOrders |
Orders with status REFUNDED |
stats.onlineOrders |
Orders placed online (SYSTEM_USER buyer type) |
stats.atDoorOrders |
Orders sold at the gate (AT_DOOR buyer type) |
orders |
Paginated list of order rows |
orders[].bookingId |
Booking UUID |
orders[].bookingReference |
Short readable code (e.g., EVT-A3F4B21C) |
orders[].status |
Booking status string |
orders[].bookedAt |
When booking was created — LocalDateTime |
orders[].formResponseId |
UUID of the buyer's applicant form response — null if no form or not yet submitted |
orders[].buyer.buyerName |
Buyer's name |
orders[].buyer.buyerEmail |
Buyer's email (null for AT_DOOR) |
orders[].buyer.buyerType |
SYSTEM_USER or AT_DOOR |
orders[].totalTickets |
Number of tickets in this order |
orders[].checkedInCount |
Number of tickets checked in at least once |
orders[].total |
Order total amount |
orders[].ticketTypes |
Breakdown of ticket types in this order |
orders[].ticketTypes[].ticketTypeName |
Name of the ticket type |
orders[].ticketTypes[].quantity |
How many of this type are in the order |
pagination.currentPage |
Zero-based current page index |
pagination.pageSize |
Number of items per page |
pagination.totalPages |
Total number of pages |
pagination.totalElements |
Total matching orders |
pagination.hasNext |
true if a next page exists |
pagination.hasPrevious |
true if a previous page exists |
Error Response JSON Sample:
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "Access denied: you are not the organizer of this event",
"action_time": "2026-05-23T10:30:45",
"data": "Access denied: you are not the organizer of this event"
}
Standard Error Types:
Ticket Series Generation
Format: {TICKET_CODE}-{COUNTER}
The ticket code is the first 5 characters of the ticket type name (uppercased, spaces removed). The counter is a zero-padded incrementing integer per ticket type, managed with pessimistic locking to prevent duplicates.
Examples:
VIP Pass→VIP-0001,VIP-0002,VIP-0003General Admission→GENER-0001,GENER-0042Early Bird→EARLY-0001
The counter never resets — ticket series are globally unique per type forever.
JWT-Signed QR Codes
Each ticket's qrCode field is a full RSA-signed JWT (2048-bit key pair generated when the event is published). The JWT payload contains:
ticketInstanceId,ticketTypeId,ticketTypeName,ticketSerieseventId,eventName,eventStartDateTimeattendeeName,attendeeEmail,attendeePhone,attendanceModebookingReferenceeventSchedules— array of day objects for multi-day events:
[
{
"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"
}
]
validFrom,validUntil
Why JWT? Scanners can verify the ticket offline using the public key without a database lookup. Any tampering breaks the signature. The embedded eventSchedules lets scanners enforce per-day check-in rules without an internet connection.
Multi-Day Check-Ins
For multi-day events, each ticket can be checked in once per day. The checkIns array stores the full history. The hasBeenCheckedIn, lastCheckedInAt, lastCheckedInBy, lastCheckInLocation, and lastCheckInDayName convenience fields reflect the most recent entry.
dayNamein a check-in record must match one of theeventSchedulesday names embedded in the JWT- A ticket cannot be checked in twice on the same day
- Ticket status stays
ACTIVEacross days and transitions toUSEDafter all expected check-ins
Event Snapshots
Event details (title, venue, times, organizer contacts) are captured into the booking record at the moment of purchase and never change. This means:
- PDF tickets always show what the customer actually bought
- Bookings remain accurate even if the organizer later renames or reschedules the event
- The
eventandorganizerblocks inBookingOrderResponsealways reflect booking-time state
Booking Creation Flow
Bookings are created automatically by EventPaymentCompletedListener after a successful payment — never through a direct API call. The flow:
- Fetch checkout session, event, and ticket type from the database
- Generate a unique booking reference (
EVT-{8-char-UUID}) - Create ticket instances (one per quantity unit, tagged with attendee and buyer info)
- Sign a JWT QR code for each ticket using the event's RSA private key
- Snapshot current event and organizer details into the booking record
- Save the booking and mark the checkout session as
COMPLETED - Publish a
BookingCreatedEventto trigger email notifications to buyer, attendees, and organizer
The entire flow runs in a single transaction — no partial bookings are created.
Access Control Rules
| Actor | Endpoint | Allowed? |
|---|---|---|
| Booking owner (customer) | GET /booking-orders/{id} |
Yes |
| Event organizer | GET /booking-orders/{id} |
Yes |
| Admin (SUPER_ADMIN/STAFF_ADMIN) | GET /booking-orders/{id} |
Yes |
| Other authenticated user | GET /booking-orders/{id} |
No — 403 |
| Any authenticated user | GET /my-bookings |
Yes (own bookings only) |
| Ticket owner | GET /tickets/{id}/pdf |
Yes |
| Other authenticated user | GET /tickets/{id}/pdf |
No — 403 |
| Event organizer | GET /booking-orders/event/{id} |
Yes |
| Admin | GET /booking-orders/event/{id} |
Yes |
| Non-organizer user | GET /booking-orders/event/{id} |
No — 403 |
Booking and Ticket Status
Booking Status:
| Status | Description |
|---|---|
CONFIRMED |
Active booking, tickets are valid |
CANCELLED |
Booking cancelled (placeholder — cancellation logic not yet implemented) |
Ticket Instance Status:
| Status | Description |
|---|---|
ACTIVE |
Ticket ready for use; can be scanned |
USED |
Ticket has been checked in; still scannable for additional days |
CANCELLED |
Ticket cancelled; cannot be used for entry |
Date/Time Formats
| Type | Format | Example | Used For |
|---|---|---|---|
LocalDateTime |
YYYY-MM-DDTHH:mm:ss |
2025-12-15T09:00:00 |
bookedAt, cancelledAt, event snapshots |
ZonedDateTime |
YYYY-MM-DDTHH:mm:ss±HH:mm |
2025-12-15T09:00:00+03:00 |
validFrom, validUntil, checkInTime |
ZonedDateTime fields carry the event's local timezone offset and display correctly regardless of the viewer's locale.
Endpoint Summary
| # | Endpoint | Access |
|---|---|---|
| 1 | GET /e-events/booking-orders/{bookingId} |
Owner / Organizer / Admin |
| 2 | GET /e-events/booking-orders/my-bookings |
Any authenticated user |
| 3 | GET /e-events/booking-orders/tickets/{ticketInstanceId}/pdf |
Ticket owner |
| 4 | GET /e-events/booking-orders/event/{eventId} |
Event organizer / Admin |
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). enableFormcreates the underlying form with no pages. Pages must be added by the organizer via the page endpoints. The form must have at least one page before the event can be published.- Use
GET /events/{eventId}/full-formto load the complete form state (all pages, fields, and options) in a single call — intended for the form builder UI. startSubmissionis idempotent — if the attendee already has a draft response it returns the existing one instead of creating a new one.savePageaccepts a?moveToNextPage=false/truequery parameter that controls validation behaviour. Whenfalse(default), answers are written to the draft with no validation — use this for background auto-save. Whentrue, the page is validated first; if any field fails, errors are returned and nothing is saved; if all fields pass, answers are saved, the page is marked complete, and the index advances to the next page. This is the "Next" button action.submitrequires every page to have been completed via?moveToNextPage=trueat least once. Pages that were only auto-saved (without passing "Next" validation) will block submission with aPAGE_INCOMPLETEerror. This ensures every page was validated before the form is accepted.- 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 (empty) │
│ - Form created internally │
│ - No pages yet (pages: []) │
│ - Config saved (displayTime etc.) │
└──────────────┬──────────────────────┘
│
│ Load builder state (on re-open):
│ GET /events/{eventId}/full-form
│
│ 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 │
│ ⚠ Publish blocked if 0 pages │
└─────────────────────────────────────┘
Attendee Submission Journey
[Attendee — event must be PUBLISHED and within registration window]
│
│ POST /events/{eventId}/start
▼
┌──────────────────────────────────────────────────┐
│ Response created (DRAFT) │
│ or existing draft returned (idempotent) │
│ status: DRAFT, completedPageIds: [], index: 0 │
└──────────────┬───────────────────────────────────┘
│
│ ┌──────────────────────────────────────────────────────────┐
│ │ For each page (repeat until all pages completed): │
│ │ │
│ │ [Background auto-save while typing] │
│ │ PUT /pages/{pageId}/save?moveToNextPage=false │
│ │ → No validation. Saves whatever is there. Always 200. │
│ │ → completedPageIds unchanged. │
│ │ │
│ │ [User clicks "Next"] │
│ │ PUT /pages/{pageId}/save?moveToNextPage=true │
│ │ ├─ Validation FAILS → returns errors, nothing │
│ │ │ saved, page stays, success: false │
│ │ └─ Validation PASSES → answers saved, page │
│ │ added to completedPageIds, index advances │
│ └──────────────────────────────────────────────────────────┘
│
│ POST /events/{eventId}/submit
│ ├─ Any page NOT in completedPageIds → 422 PAGE_INCOMPLETE
│ └─ All pages complete + required fields present → SUBMITTED
▼
┌──────────────────────────────────────────────────┐
│ Response SUBMITTED │
│ status: SUBMITTED, completionTimeSeconds set │
└──────────────────────────────────────────────────┘
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. SavePageResponse
Returned by the Save Page Answers endpoint (Endpoint 27). The shape of the response is the same whether the save succeeded or validation failed — the client always reads saved and isValid to decide what to do next.
| Field | Type | Description |
|---|---|---|
eventId |
UUID | The event ID |
saved |
boolean | true if answers were actually written to the database. false if validation failed and nothing was saved |
savedAt |
LocalDateTime | Timestamp of the save. null when saved is false |
isValid |
boolean | true if all fields on the page passed validation. Mirrors saved |
errors |
array / null | null when isValid is true. Array of FieldValidationError when validation failed |
errors[].fieldId |
UUID | The field that failed validation |
errors[].fieldLabel |
string | Label of the failing field (e.g. "Email") |
errors[].errorCode |
string | Machine-readable error type: REQUIRED, INVALID_FORMAT, INVALID_TYPE, VALIDATION_FAILED |
errors[].message |
string | Human-readable error description (e.g. "must be valid email") |
Note:
totalFieldsOnPage,answeredFieldsOnPage, andoverallProgressare reserved fields in the response object but are not currently populated — they will benulland omitted from the JSON (@JsonInclude(NON_NULL)).
K. 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; orsubmitcalled when one or more pages were not completed via?moveToNextPage=true(PAGE_INCOMPLETEerror type); or required field still unanswered at submit time (REQUIREDerror type). For save validation failures (?moveToNextPage=true), the response returns HTTP200withsuccess: false— not a 422 — because the client needs to display inline errors without treating it as a hard failure
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}" with no pages. Pages are added separately by the organizer. Only the event organizer can call this. Returns the empty 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",
"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": []
}
}
}
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 |
4. Get Full Form
Purpose: Returns the complete form structure — all pages, fields, and options — in a single call. Intended for the form builder UI to load the current state when the organizer opens the builder. Only the event organizer can call this.
Endpoint: GET /api/v1/e-events/applicant-form/events/{eventId}/full-form
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": "Form retrieved",
"action_time": "2026-05-21T10:30:45",
"data": {
"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": "2026-05-21T10:30:45",
"pages": [
{
"pageId": "1a2b3c4d-...",
"title": "Personal Details",
"description": null,
"displayOrder": 1,
"actionButtonText": "Next",
"fields": [
{
"fieldId": "c3d4e5f6-...",
"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": []
},
{
"fieldId": "d4e5f6a7-...",
"type": "DROPDOWN",
"label": "T-Shirt Size",
"displayOrder": 2,
"required": true,
"validation": null,
"options": [
{ "optionId": "a1b2c3d4-...", "label": "Small (S)", "displayOrder": 1 },
{ "optionId": "b2c3d4e5-...", "label": "Medium (M)", "displayOrder": 2 }
]
}
]
},
{
"pageId": "2b3c4d5e-...",
"title": "Dietary Requirements",
"displayOrder": 2,
"actionButtonText": "Submit",
"fields": []
}
]
}
}
Success Response Fields: data is a FormResponse with all pages, fields, and options fully nested. Soft-deleted items are excluded. Pages and fields are ordered by displayOrder.
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
23. Get Preview Metadata
Purpose: Returns a lightweight summary of the form structure — total pages, page numbers, and titles — without loading full field data. Used by the attendee to render a progress indicator before filling. Also returns the attendee's current response state (responseId, currentPageIndex, completedPageIds) if they have an existing draft.
Endpoint: GET /api/v1/e-events/applicant-form/events/{eventId}/preview/metadata
Access Level: 🔒 Protected (Organizer always; attendee when event is PUBLISHED)
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 |
24. Preview Page by Number
Purpose: Returns a specific page by its 1-based position number, including all its active fields and options. Used by attendees to load each page as they fill the form. Also accessible to the organizer for previewing before publishing.
Endpoint: GET /api/v1/e-events/applicant-form/events/{eventId}/preview/pages/{pageNumber}
Access Level: 🔒 Protected (Organizer always; attendee when event is PUBLISHED)
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 |
25. 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 (Organizer always; attendee when event is PUBLISHED)
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
26. 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 |
27. Save Page Answers
Purpose: Saves the attendee's answers for a specific page. Behaviour is controlled by the ?moveToNextPage query parameter which maps directly to two distinct client actions — background auto-save and the "Next" button.
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 |
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
moveToNextPage |
boolean | No | Controls validation and page advancement. See behaviour table below | false |
moveToNextPage Behaviour
| Value | Intended Use | Validation | What Happens on Success | What Happens on Failure |
|---|---|---|---|---|
false |
Background auto-save while the user is typing | None — answers are stored as-is regardless of format or required state | Answers written. completedPageIds unchanged. Always success: true |
N/A — cannot fail |
true |
User clicks "Next" to advance to the next page | Full — all fields on the page are validated (required + format + type) | Answers written, page added to completedPageIds, currentPageIndex advances. success: true |
Nothing saved, nothing advanced. success: false with errors[] in data |
Key rule: A page only enters
completedPageIdsthrough a successful?moveToNextPage=truecall.submitwill reject the form if any page is missing fromcompletedPageIds.
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 |
Example 1 — Auto-save (moveToNextPage=false)
Request:
PUT /events/{eventId}/pages/{pageId}/save?moveToNextPage=false
{
"091467dd-50f9-413f-aa5f-0feb92682945": { "value": "Eddie Murphy" },
"6246ef54-a1b1-4b06-bd7c-cc1bc7950b8e": { "value": "notanemail@@bad" }
}
Response — always 200 OK, always success: true, no validation applied:
{
"success": true,
"httpStatus": "OK",
"message": "Page saved",
"action_time": "2026-05-22T14:30:00",
"data": {
"eventId": "d7c52ae3-627f-42b4-a069-dd7c2d7cd78c",
"saved": true,
"savedAt": "2026-05-22T14:30:00",
"isValid": true,
"errors": null
}
}
Example 2 — User clicks "Next", validation fails (moveToNextPage=true)
Request:
PUT /events/{eventId}/pages/{pageId}/save?moveToNextPage=true
{
"091467dd-50f9-413f-aa5f-0feb92682945": { "value": "Eddie Murphy" },
"6246ef54-a1b1-4b06-bd7c-cc1bc7950b8e": { "value": "notanemail@@bad" },
"956b1e2e-3e47-4ba9-8248-a6668305ca42": { "value": "" },
"7bfb4439-1d4d-43e5-a6df-a7dba86f45ea": { "value": "Male" }
}
Response — 200 OK but success: false. Nothing saved. Client shows inline errors and keeps user on the same page:
{
"success": false,
"httpStatus": "OK",
"message": "Validation failed",
"action_time": "2026-05-22T14:31:00",
"data": {
"eventId": "d7c52ae3-627f-42b4-a069-dd7c2d7cd78c",
"saved": false,
"savedAt": null,
"isValid": false,
"errors": [
{
"fieldId": "6246ef54-a1b1-4b06-bd7c-cc1bc7950b8e",
"fieldLabel": "Email",
"errorCode": "INVALID_FORMAT",
"message": "must be valid email"
},
{
"fieldId": "956b1e2e-3e47-4ba9-8248-a6668305ca42",
"fieldLabel": "Phone Number",
"errorCode": "REQUIRED",
"message": "Phone Number is required"
}
]
}
}
Example 3 — User clicks "Next", validation passes (moveToNextPage=true)
Request:
PUT /events/{eventId}/pages/{pageId}/save?moveToNextPage=true
{
"091467dd-50f9-413f-aa5f-0feb92682945": { "value": "Eddie Murphy" },
"6246ef54-a1b1-4b06-bd7c-cc1bc7950b8e": { "value": "graphereddy@gmail.com" },
"956b1e2e-3e47-4ba9-8248-a6668305ca42": { "value": "0627489964" },
"7bfb4439-1d4d-43e5-a6df-a7dba86f45ea": { "value": "Male" }
}
Response — 200 OK, success: true. Page written, added to completedPageIds, index advances:
{
"success": true,
"httpStatus": "OK",
"message": "Page saved",
"action_time": "2026-05-22T14:32:00",
"data": {
"eventId": "d7c52ae3-627f-42b4-a069-dd7c2d7cd78c",
"saved": true,
"savedAt": "2026-05-22T14:32:00",
"isValid": true,
"errors": null
}
}
Error Codes in errors[].errorCode
| errorCode | Meaning |
|---|---|
REQUIRED |
Field is marked required and no value was provided |
INVALID_FORMAT |
Value format is wrong (e.g. bad email, invalid phone pattern) |
INVALID_TYPE |
Value is the wrong data type for the field (e.g. passing a string to a DROPDOWN expecting a UUID) |
VALIDATION_FAILED |
Value fails a custom rule (minLength, maxLength, min, max, minSelections, etc.) |
Success Response Fields: data is a SavePageResponse.
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 |
28. 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 |
29. Submit
Purpose: Finalises and submits the attendee's response. The server performs a two-stage validation before accepting: first it checks that every page is in completedPageIds (meaning the attendee clicked "Next" and passed validation for each page), then it checks that all required fields still have answers. If either check fails the response is rejected with 422 and a list of field-level errors in data. On success, status transitions to SUBMITTED and completionTimeSeconds is recorded.
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": "2026-05-22T14:35:00",
"data": {
"responseId": "8a067ebe-5eff-496c-a7eb-e3f8ad853b9f",
"formId": "9f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"submittedBy": "john.doe",
"status": "SUBMITTED",
"completedPageIds": ["a64a2f29-428e-41fc-8d14-043841d857a2"],
"currentPageIndex": 1,
"startedAt": "2026-05-22T14:30:00",
"submittedAt": "2026-05-22T14:35:00",
"completionTimeSeconds": 312,
"answers": [ "..." ]
}
}
Success Response Fields: data is a FormResponseObject. data.status will be "SUBMITTED".
Error Response — page not completed via "Next" (422):
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Please complete all required pages",
"action_time": "2026-05-22T14:34:00",
"data": [
{
"pageId": "a64a2f29-428e-41fc-8d14-043841d857a2",
"pageTitle": "Personal Info",
"fieldId": null,
"fieldLabel": null,
"errorType": "PAGE_INCOMPLETE",
"errorMessage": "Page not completed — please review all fields and click Next"
}
]
}
Error Response — required field still unanswered (422):
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Please complete all required pages",
"action_time": "2026-05-22T14:34:00",
"data": [
{
"pageId": "a64a2f29-428e-41fc-8d14-043841d857a2",
"pageTitle": "Personal Info",
"fieldId": "956b1e2e-3e47-4ba9-8248-a6668305ca42",
"fieldLabel": "Phone Number",
"errorType": "REQUIRED",
"errorMessage": "This field is required"
}
]
}
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 |
One or more pages not in completedPageIds (PAGE_INCOMPLETE); or a required field has no answer (REQUIRED). Error list is in data[] |
Organizer Response Management
30. 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 |
31. 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 |
32. 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 (empty — no pages) |
| 2 | PUT | /events/{eventId}/settings |
🔒 | Organizer | Update form display settings |
| 3 | DELETE | /events/{eventId}/disable |
🔒 | Organizer | Disable and remove form |
| 4 | GET | /events/{eventId}/full-form |
🔒 | Organizer | Load full form (all pages + fields) for builder |
| Page Management | |||||
| 5 | POST | /events/{eventId}/pages |
🔒 | Organizer | Add a page |
| 6 | PUT | /events/{eventId}/pages/{pageId} |
🔒 | Organizer | Update a page |
| 7 | DELETE | /events/{eventId}/pages/{pageId}?hard=false |
🔒 | Organizer | Delete a page (soft or hard) |
| 8 | POST | /events/{eventId}/pages/bulk |
🔒 | Organizer | Bulk add pages with fields |
| 9 | DELETE | /events/{eventId}/pages/bulk?hard=false |
🔒 | Organizer | Bulk delete pages |
| 10 | POST | /events/{eventId}/pages/{pageId}/clone |
🔒 | Organizer | Clone a page |
| Field Management | |||||
| 11 | POST | /events/{eventId}/pages/{pageId}/fields |
🔒 | Organizer | Add a field to a page |
| 12 | PUT | /events/{eventId}/fields/{fieldId} |
🔒 | Organizer | Update a field |
| 13 | DELETE | /events/{eventId}/fields/{fieldId}?hard=false |
🔒 | Organizer | Delete a field (soft or hard) |
| 14 | POST | /events/{eventId}/pages/{pageId}/fields/bulk |
🔒 | Organizer | Bulk add fields |
| 15 | DELETE | /events/{eventId}/pages/{pageId}/fields/bulk?hard=false |
🔒 | Organizer | Bulk delete fields |
| 16 | PATCH | /events/{eventId}/fields/bulk |
🔒 | Organizer | Bulk update fields |
| 17 | POST | /events/{eventId}/fields/{fieldId}/clone |
🔒 | Organizer | Clone a field |
| Option Management | |||||
| 18 | POST | /events/{eventId}/fields/{fieldId}/options |
🔒 | Organizer | Add option to a field |
| 19 | PUT | /events/{eventId}/options/{optionId} |
🔒 | Organizer | Update an option |
| 20 | DELETE | /events/{eventId}/options/{optionId}?hard=false |
🔒 | Organizer | Delete an option (soft or hard) |
| 21 | DELETE | /events/{eventId}/fields/{fieldId}/options/bulk?hard=false |
🔒 | Organizer | Bulk delete options |
| 22 | PATCH | /events/{eventId}/options/bulk |
🔒 | Organizer | Bulk update option labels |
| Preview | |||||
| 23 | GET | /events/{eventId}/preview/metadata |
🔒 | Organizer + Attendee* | Form page metadata + attendee progress |
| 24 | GET | /events/{eventId}/preview/pages/{pageNumber} |
🔒 | Organizer + Attendee* | Load a page by number (fields + options) |
| 25 | POST | /events/{eventId}/preview/pages/{pageNumber}/validate |
🔒 | Organizer + Attendee* | Validate answers without saving |
| Attendee Submission | |||||
| 26 | POST | /events/{eventId}/start |
🔒 | Attendee | Start form / get existing draft |
| 27 | PUT | /events/{eventId}/pages/{pageId}/save?moveToNextPage=false |
🔒 | Attendee | Auto-save answers (no validation) |
| 27 | PUT | /events/{eventId}/pages/{pageId}/save?moveToNextPage=true |
🔒 | Attendee | "Next" — validate page and advance if clean |
| 28 | GET | /events/{eventId}/my-response |
🔒 | Attendee | Get my response |
| 29 | POST | /events/{eventId}/submit |
🔒 | Attendee | Submit form (requires all pages completed via "Next") |
| Organizer — Responses & Analytics | |||||
| 30 | GET | /events/{eventId}/responses/{responseId} |
🔒 | Organizer | Get response by ID |
| 31 | GET | /events/{eventId}/responses |
🔒 | Organizer | Get all responses (paginated) |
| 32 | GET | /events/{eventId}/analytics |
🔒 | Organizer | Get form analytics |
* Attendee access to preview endpoints requires the event to be PUBLISHED.
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 Management API provides comprehensive attendee tracking and analytics for event organizers. Organizers can view overall attendance statistics with per-day and per-ticket-type breakdowns, browse a unified attendance list (present, absent, and partially attended in one view) with rich filtering, and drill into the full check-in history for any individual ticket. The system supports both single-day and multi-day events.
Hints:
- Organizer Only: All endpoints restricted to the event organizer
- Unified List: Present and absent tickets are in one list — use
?status=PRESENT|ABSENT|PARTIALLY_ATTENDEDto filter - Ticket-Based Rows: Each row in the list is one ticket, not one person — one buyer can appear multiple times
- Attendee Number: Unique human-readable ticket ID =
bookingReference + "-" + ticketSeries(e.g.EVT-A3F4B21C-VIP-0042) - Buyer vs Attendee:
buyer= who paid,attendee= who holds the ticket — can be different people - Buyer Types:
SYSTEM_USER= online purchase with account,AT_DOOR= walk-in name only (no account) - Day Filtering:
?dayNumberis optional — omit for overall view, provide for per-day status - Partial Attendance:
PARTIALLY_ATTENDEDonly applies when nodayNumberfilter is set on multi-day events - Pagination: Default 20 per page
- Form Response:
formResponseIdon each attendee/ticket row links to the buyer's applicant form submission —nullif the event had no form or the form was not yet submitted
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": "2026-05-23T10:30:45",
"data": {
}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2026-05-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, NOT_FOUND, FORBIDDEN, etc.) |
message |
string | Human-readable message describing the result |
action_time |
string | ISO 8601 timestamp of when the response was generated |
data |
object | Response payload for success, error string for failures |
HTTP Method Badge Standards
- GET - GET - Green (Safe, read-only operations)
- POST - POST - Blue (Create new resources)
- PUT - PUT - Yellow (Update/replace entire resource)
- PATCH - PATCH - Orange (Partial updates)
- DELETE - DELETE - Red (Remove resources)
Endpoints
1. Get Attendance Summary
Purpose: Returns overall attendance statistics for an event including per-day and per-ticket-type breakdowns. Always covers the full event — no filtering applied.
Endpoint: GET /e-events/attendance/{eventId}/summary
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 | Event identifier | Must be a valid UUID of an existing event owned by the authenticated organizer |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Attendance summary retrieved",
"action_time": "2026-05-23T10:30:45",
"data": {
"eventId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"eventTitle": "East African Tech Summit 2026",
"totalDays": 3,
"eventSchedule": [
{ "dayNumber": 1, "dayName": "Day 1 - Opening", "date": "2026-12-15" },
{ "dayNumber": 2, "dayName": "Day 2 - Conference", "date": "2026-12-16" },
{ "dayNumber": 3, "dayName": "Day 3 - Closing", "date": "2026-12-17" }
],
"overallStats": {
"totalTickets": 500,
"present": 310,
"partiallyAttended": 45,
"absent": 145,
"attendanceRate": 71.0,
"byDay": [
{
"dayNumber": 1,
"dayName": "Day 1 - Opening",
"date": "2026-12-15",
"totalTickets": 500,
"checkedIn": 400,
"absent": 100,
"attendanceRate": 80.0,
"status": "COMPLETED"
},
{
"dayNumber": 2,
"dayName": "Day 2 - Conference",
"date": "2026-12-16",
"totalTickets": 500,
"checkedIn": 355,
"absent": 145,
"attendanceRate": 71.0,
"status": "ONGOING"
},
{
"dayNumber": 3,
"dayName": "Day 3 - Closing",
"date": "2026-12-17",
"totalTickets": 500,
"checkedIn": 0,
"absent": 500,
"attendanceRate": 0.0,
"status": "UPCOMING"
}
]
},
"byTicketType": [
{
"ticketTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa7",
"ticketTypeName": "VIP Pass",
"totalSold": 100,
"present": 75,
"partiallyAttended": 10,
"absent": 15,
"attendanceRate": 85.0,
"byDay": [
{
"dayNumber": 1,
"dayName": "Day 1 - Opening",
"checkedIn": 90,
"absent": 10,
"attendanceRate": 90.0
}
]
}
]
}
}
Success Response Fields:
| Field | Description |
|---|---|
data.eventId |
Event UUID |
data.eventTitle |
Event name |
data.totalDays |
Number of event days |
data.eventSchedule |
List of days with dayNumber, dayName, date |
data.overallStats.totalTickets |
Total tickets sold |
data.overallStats.present |
Tickets checked in for all days |
data.overallStats.partiallyAttended |
Tickets checked in for some days (multi-day only) |
data.overallStats.absent |
Tickets never checked in |
data.overallStats.attendanceRate |
((present + partiallyAttended) / total) × 100, 2 dp |
data.overallStats.byDay[].status |
COMPLETED, ONGOING, or UPCOMING |
data.byTicketType |
Same stats broken down per ticket type |
Error Response JSON Sample:
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Event not found",
"action_time": "2026-05-23T10:30:45",
"data": "Event not found"
}
Standard Error Types:
2. Get Attendance List
Purpose: Returns a paginated, filterable list of all tickets for an event. Each row is one ticket. Present and absent tickets are unified in one list — use the status filter to narrow down.
Endpoint: GET /e-events/attendance/{eventId}/list
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 | Event identifier | Must be a valid UUID of an existing event owned by the authenticated organizer |
Query Parameters:
| Parameter | Type | Required | Description | Validation | Default |
|---|---|---|---|---|---|
status |
enum | No | Filter by attendance status | PRESENT, ABSENT, PARTIALLY_ATTENDED |
All |
dayNumber |
integer | No | Filter for a specific event day | Must be between 1 and totalDays |
None (overall) |
ticketTypeId |
UUID | No | Filter by ticket type | Must belong to this event | All types |
buyerType |
enum | No | Filter by how the ticket was purchased | SYSTEM_USER, AT_DOOR |
All |
search |
string | No | Search by attendee name, email, or phone | Partial match, case-insensitive | None |
page |
integer | No | Page number (0-indexed) | Min: 0 |
0 |
size |
integer | No | Items per page | Min: 1 |
20 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Attendance list retrieved",
"action_time": "2026-05-23T10:30:45",
"data": {
"eventId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"eventTitle": "East African Tech Summit 2026",
"appliedFilters": {
"ticketTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa7",
"ticketTypeName": "VIP Pass",
"status": "ALL",
"dayNumber": 1,
"dayName": "Day 1 - Opening",
"buyerType": "ALL",
"search": null
},
"summary": {
"totalTickets": 100,
"present": 90,
"absent": 10,
"partiallyAttended": null,
"attendanceRate": 90.0
},
"attendees": [
{
"ticketInstanceId": "3fa85f64-5717-4562-b3fc-2c963f66afa8",
"formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"attendeeNumber": "EVT-A3F4B21C-VIP-0042",
"ticketTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa7",
"ticketType": "VIP Pass",
"ticketSeries": "VIP-0042",
"attendeeName": "John Doe",
"attendeeEmail": "john@example.com",
"attendeePhone": "+255712345678",
"buyer": {
"buyerName": "Jane Doe",
"buyerEmail": "jane@example.com",
"buyerId": "3fa85f64-5717-4562-b3fc-2c963f66afa9",
"buyerType": "SYSTEM_USER"
},
"bookingReference": "EVT-A3F4B21C",
"pricePaid": 150.00,
"attendanceStatus": "PRESENT",
"checkInTime": "2026-12-15T09:15:00+03:00",
"checkInLocation": "Main Gate",
"checkedInBy": "Scanner Operator 1",
"scannerId": "3fa85f64-5717-4562-b3fc-2c963f66afb0"
},
{
"ticketInstanceId": "3fa85f64-5717-4562-b3fc-2c963f66afb1",
"formResponseId": null,
"attendeeNumber": "EVT-B5D2E12F-GENE-0101",
"ticketType": "General Admission",
"ticketSeries": "GENE-0101",
"attendeeName": "Ali Hassan",
"attendeeEmail": null,
"attendeePhone": null,
"buyer": {
"buyerName": "Ali Hassan",
"buyerEmail": null,
"buyerId": null,
"buyerType": "AT_DOOR"
},
"bookingReference": "EVT-B5D2E12F",
"pricePaid": 50.00,
"attendanceStatus": "ABSENT",
"checkInTime": null,
"checkInLocation": null,
"checkedInBy": null,
"scannerId": null
}
],
"pagination": {
"currentPage": 0,
"pageSize": 20,
"totalPages": 5,
"totalElements": 100,
"hasNext": true,
"hasPrevious": false
}
}
}
Success Response Fields:
| Field | Description |
|---|---|
data.appliedFilters |
Echo of all active filters so the client knows what was applied |
data.summary.totalTickets |
Total tickets matching the ticket type filter (unaffected by status/search) |
data.summary.present |
Tickets with PRESENT status under current filters |
data.summary.absent |
Tickets with ABSENT status under current filters |
data.summary.partiallyAttended |
null when dayNumber is active; count when viewing overall multi-day |
data.summary.attendanceRate |
((present + partiallyAttended) / total) × 100, 2 dp |
data.attendees[].ticketInstanceId |
Unique ticket instance UUID |
data.attendees[].formResponseId |
UUID of the buyer's applicant form response — null if the event had no form |
data.attendees[].attendeeNumber |
Unique readable ID = bookingReference + "-" + ticketSeries |
data.attendees[].attendanceStatus |
PRESENT, ABSENT, or PARTIALLY_ATTENDED |
data.attendees[].buyer.buyerType |
SYSTEM_USER (online) or AT_DOOR (walk-in) |
data.attendees[].buyer.buyerId |
Populated for SYSTEM_USER, null for AT_DOOR |
data.attendees[].checkInTime |
null when attendanceStatus is ABSENT |
data.pagination |
Standard pagination envelope |
Notes:
- When
dayNumberis provided:attendanceStatusisPRESENTorABSENTfor that specific day only - When
dayNumberis omitted:attendanceStatusreflects the overall across all days (PARTIALLY_ATTENDEDpossible) summaryreflects the current filter scope, not the whole eventpartiallyAttendedinsummaryisnullwhen adayNumberfilter is active
Error Response JSON Sample:
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Invalid day 5. Valid: 1-3",
"action_time": "2026-05-23T10:30:45",
"data": "Invalid day 5. Valid: 1-3"
}
Standard Error Types:
3. Get Attendee Detail
Purpose: Returns the full check-in history for a single ticket across all event days. Shows per-day status including upcoming days.
Endpoint: GET /e-events/attendance/{eventId}/attendees/{ticketInstanceId}
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 | Event identifier | Must be owned by the authenticated organizer |
ticketInstanceId |
UUID | Yes | Individual ticket instance identifier | Must belong to a booking under this event |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Attendee detail retrieved",
"action_time": "2026-05-23T10:30:45",
"data": {
"ticketInstanceId": "3fa85f64-5717-4562-b3fc-2c963f66afa8",
"formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"attendeeNumber": "EVT-A3F4B21C-VIP-0042",
"attendeeName": "John Doe",
"attendeeEmail": "john@example.com",
"attendeePhone": "+255712345678",
"ticketType": "VIP Pass",
"ticketSeries": "VIP-0042",
"bookingReference": "EVT-A3F4B21C",
"pricePaid": 150.00,
"overallStatus": "PARTIALLY_ATTENDED",
"daysAttended": 2,
"daysTotal": 3,
"checkInsByDay": [
{
"dayNumber": 1,
"dayName": "Day 1 - Opening",
"dayDate": "2026-12-15",
"status": "CHECKED_IN",
"checkInTime": "2026-12-15T09:15:00+03:00",
"checkInLocation": "Main Gate",
"checkedInBy": "Scanner Operator 1",
"scannerId": "3fa85f64-5717-4562-b3fc-2c963f66afb0"
},
{
"dayNumber": 2,
"dayName": "Day 2 - Conference",
"dayDate": "2026-12-16",
"status": "NOT_CHECKED_IN",
"checkInTime": null,
"checkInLocation": null,
"checkedInBy": null,
"scannerId": null
},
{
"dayNumber": 3,
"dayName": "Day 3 - Closing",
"dayDate": "2026-12-17",
"status": "UPCOMING",
"checkInTime": null,
"checkInLocation": null,
"checkedInBy": null,
"scannerId": null
}
]
}
}
Success Response Fields:
| Field | Description |
|---|---|
data.ticketInstanceId |
Unique ticket instance UUID |
data.formResponseId |
UUID of the buyer's applicant form response — null if the event had no form |
data.attendeeNumber |
Unique readable ID = bookingReference + "-" + ticketSeries |
data.overallStatus |
PRESENT (all days), PARTIALLY_ATTENDED (some), ABSENT (none) |
data.daysAttended |
Count of days this ticket was checked in |
data.daysTotal |
Total number of event days |
data.checkInsByDay[].status |
CHECKED_IN, NOT_CHECKED_IN, or UPCOMING |
data.checkInsByDay[].checkInTime |
null when not checked in or day is upcoming |
data.checkInsByDay[].checkedInBy |
Name of the scanner operator who processed the check-in |
Error Response JSON Sample:
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Ticket not found",
"action_time": "2026-05-23T10:30:45",
"data": "Ticket not found"
}
Standard Error Types:
Quick Reference Guide
Attendance Status Values
| Value | Meaning | Available when |
|---|---|---|
PRESENT |
Checked in (all days if no day filter) | Always |
ABSENT |
Never checked in | Always |
PARTIALLY_ATTENDED |
Checked in some days but not all | No dayNumber filter, multi-day events only |
Day Status Values
| Value | Meaning |
|---|---|
CHECKED_IN |
Ticket was scanned on that day |
NOT_CHECKED_IN |
Day passed, ticket not scanned |
UPCOMING |
Day has not started yet |
Buyer Type Values
| Value | buyerEmail |
buyerId |
Meaning |
|---|---|---|---|
SYSTEM_USER |
Populated | Populated | Bought online, has platform account |
AT_DOOR |
null |
null |
Walk-in sale, name only captured at door |
Common HTTP Status Codes
200 OK: Successful request401 UNAUTHORIZED: Authentication required or token invalid403 FORBIDDEN: Not the event organizer404 NOT_FOUND: Event, ticket, or day number not found500 INTERNAL_SERVER_ERROR: Unexpected server error
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 |