> ## Documentation Index
> Fetch the complete documentation index at: https://partner-help.letsdothis.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API changelog

> Consumer-visible changes to the Let's Do This Public API.

Consumer-visible changes to the public API.

## August 2026

### Read and update discount codes (restricted)

*2026-08-26*

`GET /v0/discount-codes/{id}` returns one of your discount codes, described with the same fields an import row accepts, plus its id and when it was created. `PATCH /v0/discount-codes/{id}` updates the code's internal name (`group`) and leaves every other field on it unchanged. The `id` is the `discountCodeId` an import reports for the code.

A code that belongs to another organization, one that has been deleted, and an id that identifies something other than a discount code — a credit, gift card or referral code — all answer `404`, so an id alone reveals nothing about codes you cannot access.

Both endpoints are restricted and not generally available: organizations that have not been enabled receive a `403`. Contact your Let's Do This representative if you'd like access.

***

### Imported discount codes can discount each entry

*2026-08-25*

Rows sent to `POST /v0/discount-codes/import` accept `appliesPer`, either `TRANSACTION` or `ENTRY`. `TRANSACTION` is the default and is what imported codes have always been: the code discounts a basket once, however many entries it holds. `ENTRY` discounts each participant's entry separately, so a basket of three entries is discounted three times.

The choice also changes how usage limits are counted. A per-entry code counts entries where a transaction code counts bookings, so `limitUsage` of `1` on a per-entry code is spent by the first participant rather than covering a whole booking.

`limitUsagePerTransaction` now requires `appliesPer` to be `ENTRY`. Only a per-entry code is capped that way at checkout, so the field was previously accepted and then silently ignored; a row that sets it without `appliesPer` set to `ENTRY` is now rejected along with the rest of the batch.

***

### Timing ingest accepts shipment updates

*2026-08-24*

`POST /timing-inbound/v1` now accepts a `data.shipping` object, so a timing provider can report that a participant's bib pack has been dispatched — `shippedAt` plus optional `status` (`SHIPPED`, `DELIVERED` or `RETURNED`; defaults to `SHIPPED`), `carrier`, `trackingNumber` and `trackingUrl`.

`schemaVersion` stays `"1.0"`: this is an additive change and nothing previously accepted is now rejected. Two things follow from that:

* `data.bibNumber` is no longer required, because a shipment update need not carry a bib. A message must still carry at least one recognised field — `data` with neither `bibNumber` nor `shipping` answers `400` rather than being accepted and dropped.
* A shipping field you omit leaves the stored value unchanged, so a status update need only carry what changed. It cannot clear a carrier or tracking number.

An unrecognised `status` answers `400`. Bib and shipping updates are ordered independently, so a shipment sent after a bib with an earlier `changedAt` still applies.

***

### Timing ingest endpoint published

*2026-08-20*

`POST /timing-inbound/v1` is now documented under the new **Timing** section. Timing providers enabled for the integration push data (currently bib numbers) back for startlist entries they received from Let's Do This, as a single message or a batch of up to 500.

The endpoint itself is not new — it was previously undocumented while the contract was agreed directly with the first partner. It is available to select timing partners only; other callers receive the standard `401`.

***

### Import endpoints you are not enabled for now answer `403`

*2026-08-10*

Every import surface — bookings, line items, add-on products, discount codes, credits, referrals, and reserved entry participants — previously answered `404` when your organization was not enabled for it. They now answer `403` with a message naming the surface:

```
Booking imports are not enabled for this organizer. Ask your Lets Do This contact to enable them.
```

The `404` said the endpoint did not exist, which was never true — it is documented here — and left no way to tell "not switched on yet" from a wrong URL. If you branch on the status code to detect that your organization lacks access, match `403` rather than `404`.

Job IDs are unaffected: a status endpoint still answers `404` for a job that does not exist or belongs to another organization, so it cannot be used to discover other organizations' jobs.

***

### A permanent import failure is no longer reported as `INTERNAL`

*2026-08-07*

A reserved entry participant assignment the import cannot use — one naming a blank identifier, for instance — now fails with the code `INVALID_ASSIGNMENT` in the job status `errors`. It previously reported `INTERNAL`.

The distinction matters for retries. `INTERNAL` means the cause could not be named, so trying again is reasonable. `INVALID_ASSIGNMENT` means the assignment itself was rejected before anything was written, so resubmitting the same row cannot succeed — correct the row and submit it again.

Other codes describe the state that blocked the assignment rather than the assignment itself, and that state can change: `NO_CAPACITY_REMAINING` clears when a place frees or the group's capacity is raised.

`code` is a free-form string, so no schema change is involved and existing handling continues to work.

***

### Read a booking's add-on allocations

*2026-08-05*

> **Restricted:** This endpoint is not generally available. Organizations that have not been enabled receive a `403`; contact your Let's Do This representative if you'd like access.

`GET /v0/bookings/{id}/add-on-allocations` returns a cursor-paginated list of the participant add-on allocations stored for a booking. Each allocation includes its participant and purchase line-item references, add-on and variant details, operational and financial status, and creation and update timestamps. Imported records also include external add-on, variant, and line-item identifiers when available.

***

## July 2026

### Delete an imported booking

*2026-07-27*

`DELETE /v0/bookings/{id}` retires a booking you imported through the Public API, so you can correct a bad row and import it again under the same `externalBookingId`.

The booking is not removed: it moves to `CANCELLED` and stays readable. The external ID it was imported under is freed at the same time, so re-importing that ID creates a new booking rather than reporting `alreadyImported`. Deleting is idempotent — deleting an already-deleted booking succeeds and changes nothing.

Only bookings whose `source` is `API` can be deleted. Anything else returns `403`, and a booking that does not exist or belongs to another organizer returns `404`.

***

### Breaking: the external-booking-ID lookup returns a list

*2026-07-27*

`GET /v0/bookings/external/{externalBookingId}` now returns a `PagedBookingSummaries` envelope instead of a single `Booking`, and an ID that matches nothing returns an empty list with `200` rather than `404`.

One external ID can legitimately match more than one booking — correcting an import cancels the original and re-imports it, so both survive under the ID you supplied. The previous shape had to choose one on your behalf and could not show you the other. Results are oldest first, matching the other list endpoints, so the most recent match is last. The endpoint accepts the standard `page[size]`, `page[after]`, and `page[before]` parameters.

The summaries are `BookingSummary` rather than the full `Booking` the endpoint used to return, matching the other list endpoints; use `GET /v0/bookings/{id}` to fetch one in full.

***

### Standardize event-occurrences list pagination

*2026-07-02*

`GET /v0/event-occurrences` now accepts the standard `page[size]`, `page[after]`, and `page[before]` query parameters, matching the other list endpoints. The legacy `pageSize`, `cursor[after]`, and `cursor[before]` parameters continue to work but are deprecated. The response schema is renamed to `PagedOrganizerEventOccurrenceSummaries`; the previous `EventOccurrencesResponse` name remains as a deprecated alias.

***

### Paginate the races, tickets, and reserved-entry-groups lists

*2026-07-01*

The races list (`GET /v0/event-occurrences/{id}/races`), the event tickets list (`GET /v0/events/{id}/tickets`), and the reserved entry groups list (`GET /v0/events/{id}/reserved-entries`) now return a `page` cursor alongside `data` and accept `page[size]`, `page[after]`, and `page[before]`, matching the other list endpoints. All continue to return every item by default, so existing integrations are unaffected. The response schemas are renamed to `PagedRaceSummaries`, `PagedTickets`, and `PagedReservedEntryGroups`; the previous `Races`, `Tickets`, and `ReservedEntryGroups` schema names remain as deprecated aliases.

***

### Read a single event occurrence

*2026-07-01*

You can retrieve an individual event occurrence with `GET /v0/event-occurrences/{id}`. Access is scoped to the organizer associated with your Public API credentials.

***

## June 2026

### Navigate from a race to its event occurrence

*2026-06-30*

`GET /v0/races/{id}` now includes `eventOccurrenceId` and `eventId`, so you can navigate from a race back to its event occurrence and event. These fields are returned on the single-race response; the race lists stay lean.

***

### Read a single race

*2026-06-30*

You can retrieve an individual race with `GET /v0/races/{id}`. Access is scoped to the organizer that owns the race's event occurrence.

***

### Find the booking forms that use a field

*2026-06-30*

You can look up which booking forms reference a given field with `GET /v0/booking-form-fields/{id}/booking-forms`. A field can be shared across many forms, so this returns a paginated list.

***

### Read booking form fields

*2026-06-30*

You can read the individual questions on a booking form. Use `GET /v0/booking-form-fields` to list fields, `GET /v0/booking-form-fields/{id}` to fetch one, and `GET /v0/booking-forms/{id}/fields` to list the fields on a specific form. Each field exposes its type, label, options, and whether an answer is required.

***

### Read booking forms

*2026-06-30*

You can retrieve a single booking form with `GET /v0/booking-forms/{id}`, and traverse between tickets and their booking forms: `GET /v0/tickets/{id}/booking-forms` lists the booking forms a ticket uses (usually zero or one), and `GET /v0/booking-forms/{id}/tickets` lists the tickets that share a booking form.

***

### Read individual tickets and a race's tickets

*2026-06-30*

You can retrieve a single ticket with `GET /v0/tickets/{id}`, and list the tickets belonging to a race with `GET /v0/races/{id}/tickets`. Ticket titles now include a `titleByLocale` map, always seeded with the default (`en-GB`) title alongside any translations.

***

### Races by event occurrence

*2026-06-01*

You can retrieve races for an event occurrence. Use `GET /v0/event-occurrences/{id}/races` if your integration needs race-level data.

***

## May 2026

### Application notes

*2026-05-26*

Application responses include stand-alone notes added from the dashboard. Use the `notes` field on application responses to read notes that are not approval or rejection reasons.

### Participant booking source

*2026-05-26*

Participant responses include `bookingSource`. Use this field to distinguish bookings made through Let's Do This from participants imported from an external platform.

### GivenGain partner lookup

*2026-05-22*

Partner lookups can use `givenGain` as an external ID type. Use `externalId=givenGain` when you need to query partners by their GivenGain identifier.

### Team roles on participants

*2026-05-20*

When `?features=teams` is enabled, participant `team` objects include `roles` with the participant's team role descriptions.

### Clearer errors for invalid IDs

*2026-05-08*

Requests with a malformed resource ID in the path (for example, `GET /v0/participants/not-a-valid-id`) now return `404 Not Found` instead of `500 Internal Server Error`.

***

## April 2026

### Implied donation field on applications

*2026-04-14*

Applications for tickets where donation is implied by the ticket include a synthetic `hasDonated` field, so integrations receive the same shape as tickets that ask the donation question directly.

***

## March 2026

### Charity opt-in partner names

*2026-03-11*

Participant `charityOptIn` values return partner names instead of opaque partner IDs where the partner can be resolved.
