# Attendance Analytics API

**Author**: Josh S. Sakweli, Backend Lead Team
**Last Updated**: 2026-05-23
**Version**: v2.0

**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_ATTENDED` to 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**: `?dayNumber` is optional — omit for overall view, provide for per-day status
- **Partial Attendance**: `PARTIALLY_ATTENDED` only applies when no `dayNumber` filter is set on multi-day events
- **Pagination**: Default 20 per page
- **Form Response**: `formResponseId` on each attendee/ticket row links to the buyer's applicant form submission — `null` if 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
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2026-05-23T10:30:45",
  "data": {
  }
}
```

### Error Response Structure
```json
{
  "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** - <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> - Green (Safe, read-only operations)
- **POST** - <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> - Blue (Create new resources)
- **PUT** - <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> - Yellow (Update/replace entire resource)
- **PATCH** - <span style="background-color: #fd7e14; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> - Orange (Partial updates)
- **DELETE** - <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> - 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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `/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**:
```json
{
  "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**:
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Event not found",
  "action_time": "2026-05-23T10:30:45",
  "data": "Event not found"
}
```

**Standard Error Types**:
- `401 UNAUTHORIZED`: Missing or invalid token
- `403 FORBIDDEN`: Authenticated user is not the event organizer
- `404 NOT_FOUND`: Event does not exist

---

## 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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `/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**:
```json
{
  "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 `dayNumber` is provided: `attendanceStatus` is `PRESENT` or `ABSENT` for that specific day only
- When `dayNumber` is omitted: `attendanceStatus` reflects the overall across all days (`PARTIALLY_ATTENDED` possible)
- `summary` reflects the current filter scope, not the whole event
- `partiallyAttended` in `summary` is `null` when a `dayNumber` filter is active

**Error Response JSON Sample**:
```json
{
  "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**:
- `401 UNAUTHORIZED`: Missing or invalid token
- `403 FORBIDDEN`: Authenticated user is not the event organizer
- `404 NOT_FOUND`: Event not found / day number out of range / ticket type not found

---

## 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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `/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**:
```json
{
  "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**:
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Ticket not found",
  "action_time": "2026-05-23T10:30:45",
  "data": "Ticket not found"
}
```

**Standard Error Types**:
- `401 UNAUTHORIZED`: Missing or invalid token
- `403 FORBIDDEN`: Authenticated user is not the event organizer
- `404 NOT_FOUND`: Event not found or ticket instance does not belong to this event

---

## 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 request
- `401 UNAUTHORIZED`: Authentication required or token invalid
- `403 FORBIDDEN`: Not the event organizer
- `404 NOT_FOUND`: Event, ticket, or day number not found
- `500 INTERNAL_SERVER_ERROR`: Unexpected server error