# List all member participation records

`GET /api/participation`

- **Success:** `200 OK`
- **Products:** Operator (`FOH`), Integrations (`INTEGRATIONS`) (any one of these products, not all)
- **Features:** TRP Integration (`TRP`), DataHub (4Global) Integration (`DATAHUB`), Front of House Generic Access (`FOHGENERIC`) (any one of these features, not all)

HTML version: [List all member participation records](https://support.gladstonesoftware.cloud/reference/members/list-all-member-participation-records)

## Overview

Returns a paged, cross-site record of member attendance at your sites in a date range: the classes and other booked activities members were marked as attending, their entry swipes, and the sales that count as participation.

A **participation record** is one occurrence of a member using the operator's facilities. The endpoint merges three separate sources into a single stream, and the `type` field tells you which one produced each record:

- `Booking` — a member's place on a booked activity (a class, a court, a session). By default only places marked as attended are returned.
- `Swipe` — a member presenting their card or credential at an entry point, recorded by access control. There is no booking behind it.
- `ProductSale` — a sold line that counts as participation, such as a casual entry or an item bought against a booking, taken from the member's invoice.

Each record carries the member, the site, a timestamp and exactly one populated block inside `details` matching its `type`. This is the endpoint to poll when you are building attendance reporting, usage analytics or a data warehouse feed — it is a bulk export shaped for reading, not a per-member lookup, and there is no member filter.

Scope and paging are the two things to get right. You must name between one and five sites; each of the three sources has its own cursor, which is why the continuation token carries three ids at once. Do not construct that token yourself — take `links.next` from the previous response and call it verbatim. Note that `links.next` is returned on every response, including the last one, so it is not a signal that more data exists: stop when `data` comes back empty.

Watch the interaction between `limit` and the three sources. The limit you send is divided by three, so each source returns at most a third of it. A `limit` of 100 yields at most 33 bookings, 33 swipes and 33 product sales, and records are grouped by source within the page rather than sorted by `time` — sort client-side if order matters to you.

## Endpoint

`GET {base_url}/api/participation`

`{base_url}` is the environment URL issued to your integration. See [Authenticating with Gladstone APIs](../../guides/authenticating-with-gladstone-apis.md).

## Query parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `siteIds` | string | Yes | Comma-separated list of site ids to report on, for example `siteIds=SITE1,SITE2`. Despite being a query parameter it is not optional: omitting it, or naming more than five sites, fails with `At least one but no more than 5 siteIds can be specified`. Every id must exist or the whole call fails with `404`. |
| `fromDate` | string (date-time) | No | Inclusive lower bound on the record time — booking start time, swipe time or invoice date depending on the record. Defaults to midnight at the start of the current UTC day when omitted, so leaving it out gives you today only, not everything. |
| `toDate` | string (date-time) | No | Inclusive upper bound on the record time. Defaults to `23:59:59` on the current UTC day. If you pass a value with a zero time component, it is moved to `23:59:59` on that day, so a date-only `toDate` always covers the whole day. Must not be earlier than `fromDate`. |
| `continuation-token` | string | No | Cursor for the next page. Omit it on the first call; afterwards take the whole `links.next` URL from the previous response rather than assembling this yourself. It carries three cursors at once in the form `attendance-id:{n}_invoice-id:{n}_swipe-id:{n}` — underscore-separated `key:value` pairs whose values must be integers. Any element that is not a `key:value` pair, or whose value will not convert to an integer, fails with `400`. Unrecognised keys are ignored silently, which will quietly restart that source from the beginning. |
| `limit` | integer (int32) | No | Maximum records per page, defaulting to `100`. The value is divided by three, so each of the three sources returns at most a third of it and a page rarely holds exactly `limit` records. Your original value, not the divided one, is echoed into `links.next`. |
| `attendedBookingsOnly` | boolean | No | Whether to restrict the booking source to places actually marked as attended. Defaults to `true`, which is why bookings a member did not turn up to are missing unless you send `false`. Has no effect on swipes or product sales. |

## Sample request

```bash title="cURL"
curl -X GET '{base_url}/api/participation' \
  -H 'Authorization: Bearer {accessToken}' \
  -H 'X-Use-SSO: true'
```

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `data` | array of object | The page of participation records, mixed across all three sources. Records are grouped by source — product sales, then swipes, then bookings — not sorted by `time`. An empty array means you have reached the end of the range. |
| `data[].memberId` | integer (int32) | The member this record belongs to — the member whose attendance, swipe or sale line this is. There is no way to filter the feed by member; filter client-side. |
| `data[].siteId` | string | The site the participation happened at — one of the ids you passed in `siteIds`. For bookings this is the site of the booked activity; for swipes, the site of the entry point; for product sales, the site the sale was recorded against. |
| `data[].time` | string (date-time) | When the participation happened. The meaning follows `type`: the booking's start time for `Booking`, the swipe timestamp for `Swipe`, and the date the invoice was raised for `ProductSale`. Records are not returned in time order. |
| `data[].type` | string | Which source produced this record, and therefore which block of `details` is populated. One of `Booking`, `Swipe` or `ProductSale`. |
| `data[].details` | object | Holds the source-specific payload. Exactly one of `swipe`, `booking` and `productSale` is populated, matching `type`; the other two are absent. Switch on `type` rather than probing for whichever key is present. |
| `data[].details.swipe` | object | Populated when `type` is `Swipe`. A swipe carries no activity or price information — it records only that the member passed through an entry point at `time`. |
| `data[].details.swipe.entryPointId` | string | The id of the entry point — the turnstile, door or reader — the member was admitted through, as configured in access control. |
| `data[].details.booking` | object | Populated when `type` is `Booking`. Describes the member's place on a booked activity, plus the activity's own configuration at the time of reading. |
| `data[].details.booking.activityId` | string | The id of the booked activity. Records sharing an `activityId` describe the same activity, so this is the key to group or join on. |
| `data[].details.booking.activityName` | string | The activity's description in the system's base language. Use `translations` for other languages. |
| `data[].details.booking.attended` | boolean | Whether the member was marked as having attended. This is always `true` unless you called with `attendedBookingsOnly=false`, which is the only way unattended places enter the feed. |
| `data[].details.booking.locationGroupId` | string | The id of the activity group the activity belongs to — the operator's grouping of related activities, not a physical place. For the physical resources see `locations`. |
| `data[].details.booking.startTime` | string (date-time) | The booked activity's start time. Identical to the record's `time` for booking records. |
| `data[].details.booking.endTime` | string (date-time) | The booked activity's end time. |
| `data[].details.booking.locations` | array of object | The resources the activity is made up of — courts, rooms, lanes and similar. These come from the activity's configuration, so every booking of the same activity reports the same list; it is not a per-booking allocation. Empty when the activity has no resources configured. |
| `data[].details.booking.locations[].id` | string | The id of the resource. |
| `data[].details.booking.locations[].name` | string | The resource's description in the system's base language. |
| `data[].details.booking.locations[].translations` | array of object | Translated forms of the resource's `name`, one entry per configured culture. Empty when none are configured. |
| `data[].details.booking.locations[].translations[].type` | string | |
| `data[].details.booking.locations[].translations[].culture` | string | The culture code this translation is for, matching the codes configured for the operator — for example `en-GB` or `cy-GB`. |
| `data[].details.booking.locations[].translations[].text` | string | The translated resource name for that culture. |
| `data[].details.booking.capacity` | object | The activity's maximum places, split by delivery mode. This is capacity, not how many were taken. |
| `data[].details.booking.capacity.inCentre` | integer (int32) | Maximum in-centre places: the booking's own limit where one is set, otherwise the activity's. `0` when the activity is virtual-only, in which case there are no in-centre places at all. |
| `data[].details.booking.capacity.virtual` | integer (int32) | Maximum virtual places. `0` when the activity is in-centre only. |
| `data[].details.booking.inCentre` | boolean | Whether the activity can be attended in person. `true` for in-centre and mixed activities, `false` only for virtual-only ones. Mixed activities report `true` for both `inCentre` and `virtual`. |
| `data[].details.booking.virtual` | boolean | Whether the activity can be attended remotely. `true` for virtual and mixed activities, `false` only for in-centre-only ones. |
| `data[].details.booking.bookingId` | integer (int32) | The id of the booking this place belongs to. Several participation records can share one `bookingId` when a booking covers more than one member. |
| `data[].details.booking.usage` | integer (int32) | How many people this single record should count as when you total participation. Normally `1`, but for activities configured as party bookings it is the party size, so one record can represent a whole group. Sum this rather than counting records. |
| `data[].details.booking.translations` | array of object | Translated forms of `activityName`, one entry per configured culture. Empty when none are configured. |
| `data[].details.booking.translations[].type` | string | |
| `data[].details.booking.translations[].culture` | string | The culture code this translation is for, matching the codes configured for the operator — for example `en-GB` or `cy-GB`. |
| `data[].details.booking.translations[].text` | string | The translated activity name for that culture — display this in place of `activityName`. |
| `data[].details.productSale` | object | Populated when `type` is `ProductSale`. Describes a sold line that counts as participation — typically a casual entry or an item sold against a booking — taken from the invoice raised for the member. |
| `data[].details.productSale.productId` | string | The id of the product sold. |
| `data[].details.productSale.productName` | string | The product's description in the system's base language. |
| `data[].details.productSale.quantity` | number (double) | How many units were sold on this line. Decimal, so fractional quantities are possible. This is a sales quantity, not a participation count — for participation use `usage`. |
| `data[].details.productSale.usage` | integer (int32) | How many people this sale should count as when totalling participation. Falls back to `1` when the product has no usage configured against it. |
| `data[].details.productSale.translations` | array of object | Translated forms of `productName`, one entry per configured culture. Empty when none are configured. |
| `data[].details.productSale.translations[].type` | string | |
| `data[].details.productSale.translations[].culture` | string | The culture code this translation is for, matching the codes configured for the operator — for example `en-GB` or `cy-GB`. |
| `data[].details.productSale.translations[].text` | string | The translated product name for that culture. |
| `links` | object | Paging links for this response. |
| `links.next` | string | A ready-made absolute URL for the next page, with your `siteIds`, dates, `attendedBookingsOnly`, original `limit` and the advanced continuation token already applied. Call it as-is. Two traps: it is present on every response, including the last, so it does not mean more data exists — stop when `data` is empty; and the dates in it are rewritten as `yyyy-MM-dd`, so any time component you originally sent is dropped from page two onwards. |

## Sample response

```json title="200 OK"
{
  "data": [
    {
      "memberId": 12345,
      "siteId": "string",
      "time": "2026-08-14T19:00:00Z",
      "type": "string",
      "details": {
        "swipe": {
          "entryPointId": "string"
        },
        "booking": {
          "activityId": "string",
          "activityName": "string",
          "attended": true,
          "locationGroupId": "string",
          "startTime": "2026-08-14T19:00:00Z",
          "endTime": "2026-08-14T19:00:00Z",
          "locations": [
            {
              "id": "string",
              "name": "string",
              "translations": [
                {
                  "type": "string",
                  "culture": "string",
                  "text": "string"
                }
              ]
            }
          ],
          "capacity": {
            "inCentre": 1,
            "virtual": 1
          },
          "inCentre": true,
          "virtual": true,
          "bookingId": 12345,
          "usage": 1,
          "translations": [
            {
              "type": "string",
              "culture": "string",
              "text": "string"
            }
          ]
        },
        "productSale": {
          "productId": "string",
          "productName": "string",
          "quantity": 10.5,
          "usage": 1,
          "translations": [
            {
              "type": "string",
              "culture": "string",
              "text": "string"
            }
          ]
        }
      }
    }
  ],
  "links": {
    "next": "string"
  }
}
```

## Errors

| Status | Meaning | When |
|--------|---------|------|
| `400` | Bad request | One of: `The from date can not be after the to date`; `At least one but no more than 5 siteIds can be specified` (also raised when `siteIds` is missing entirely); `Invalid element "{element}" in token "{token}"` when a continuation token segment is not a `key:value` pair; or `Invalid value "{value}" for token value "{key}"` when a token value is not an integer. |
| `404` | Not found | `One or more site Ids do not exist: {siteIds}` — every id in `siteIds` is checked before any records are returned, and one unknown id fails the whole call rather than being skipped. The message lists the ids you sent, not the offending one. |
| `500` | Internal server error | An unexpected server-side failure. Also returned, with an empty body, when the request does not arrive through your issued base URL — the endpoint needs that route to build `links.next`, so calling it by any other address produces this. |
