> ## 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.

# Import bookings

> Asynchronously imports a batch of bookings. The whole batch is validated synchronously against each ticket's booking form — use the booking form fields endpoints to discover the import contract — and either every row is accepted (202 with a job to poll) or nothing is imported (422 with every problem, aggregated by error class).

`externalBookingId` is the row's idempotency key: imports are create-once, so re-importing one returns the original booking (reported as `alreadyImported`) rather than creating a second — supply a fresh row `idempotencyKey` to import corrected data for the same id. Send an `Idempotency-Key` header to make the submission replayable: retrying the same key and body returns the original job; reusing a key with a different body is rejected (409).

A row may pin the booking form it validates against via `bookingFormId`; the pin is required (`AMBIGUOUS_BOOKING_FORM`) when the ticket has more than one live form, and a supplied pin must be a live form of the row's ticket (`UNKNOWN_BOOKING_FORM`).

Rows sharing an `externalOrderId` are given one `bookingReference`, so a purchase that produced several bookings stays one order; the reference is derived from the id, so rows split across batches or retried later still join it. `externalOrderId` is deliberately not an idempotency key — reusing one is how an order gains bookings, and rows carrying it still dedup individually on `externalBookingId`. It groups bookings, not payments: the financial records stay one per booking.

A row may carry a `status` for backfilling history: withdrawn, deferred and canceled bookings, and bookings flagged as due to be transferred, are created in that state rather than created and then changed, so importing them sends no withdrawal or deferral emails to participants. Omit it for a confirmed booking. A completed transfer is not importable, because its meaning lives in the link between two bookings, which this API gives you no way to express; nor is a refund, or a refund taken as credit, because both imply payment or credit records an import does not create.

<Warning>This endpoint is 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.</Warning>


## OpenAPI

````yaml /openapi.json post /v0/bookings/import
openapi: 3.1.0
info:
  title: Let's Do This API
  version: 0.1.0
  description: >
    The full reference — getting started, pagination, error responses, rate
    limits and the changelog — lives at

    [partner-help.letsdothis.com/api-reference](https://partner-help.letsdothis.com/api-reference/overview).

    This page is the interactive OpenAPI view of the same specification.


    # Authentication


    Create an API key from **Settings** → **Credentials** in your Let's Do This
    account and send it as a bearer token:


    ```bash

    curl --request GET \
     --url "https://api.letsdothis.com/v0/participants" \
     --header "Authorization: Bearer YOUR-API-KEY"
    ```


    Keep the key secret: it identifies your organization, so do not ship it in a
    website or native app. If a key is exposed,

    revoke it from the same section and create a replacement.


    # OpenAPI schema


    Download the [OpenAPI schema](https://api.letsdothis.com/documentation/json)
    to use with Postman or an OpenAPI client

    generator.
servers:
  - url: https://api.letsdothis.com
    description: Production
  - url: https://api.staging.letsdothis.com
    description: Staging
security:
  - bearerAuth: []
tags:
  - name: Events
    x-group: Events
    description: Events that participants can register for.
  - name: EventOccurrences
    x-group: Event occurrences
    description: >-
      Event occurrences scoped to the organizer associated with your Public API
      credentials. Unlike the public Events catalog, results are limited to your
      own events.
  - name: Races
    x-group: Races
    description: >-
      Races within an event occurrence. An event occurrence may have one or more
      races, for example a 5K and a 10K within the same race day.
  - name: Tickets
    x-group: Tickets
    description: Tickets that are available for events.
  - name: BookingForms
    x-group: Booking forms
    description: >-
      Booking forms define the questions a booker answers when registering for a
      ticket. A ticket usually references a single booking form, so expect zero
      or one, while a booking form may be shared across multiple tickets.
  - name: BookingFormFields
    x-group: Booking form fields
    description: >-
      Booking form fields are the individual questions on a booking form — their
      type, label, options, and whether an answer is required. A field can be
      reused across multiple forms.
  - name: Bookings
    x-group: Bookings
    description: >-
      Bookings are the transaction record for a registration. A booking may
      contain one or more participants (entries).
  - name: Participant
    x-group: Participants
    description: Participants are users who successfully booked an event.
  - name: LineItem
    x-group: Line items
    description: Financial details related to individual items within transactions.
  - name: AddOns
    x-group: Add-ons
    description: >-
      Organizer-owned products that can be purchased and allocated to
      participants. Imported products retain stable external identities.
  - name: DiscountCodes
    x-group: Discount codes
    description: >-
      Discount codes participants enter at checkout to reduce the price of a
      booking.
  - name: Credits
    x-group: Credits
    description: >-
      Credits are balances participants can spend at checkout, identified by
      their email address.
  - name: Referrals
    x-group: Referrals
    description: >-
      Referral programs, the referrers registered on them, and asynchronous
      imports of historical referrals.
  - name: Application
    x-group: Applications
    description: >-
      Applications are created when a user applies for tickets that are set up
      to require an application process, such as balloted or "Good For Age"
      entries.
  - name: Partner
    x-group: Partners
    description: >-
      Partners that were created by the organization who can have Reserved
      Entries assigned to them.
  - name: ReservedEntries
    x-group: Reserved entries
    description: >-
      Reserved Entries are entries that are reserved for a partner. After being
      assigned, the partner can then allocate these entries to participants.
  - name: Timing
    x-group: Timing
    description: >-
      Timing providers push bib numbers and bib-pack shipments back to Let's Do
      This for the startlist entries they received. Available to select timing
      partners only.
paths:
  /v0/bookings/import:
    post:
      tags:
        - Bookings
      summary: Import bookings
      description: >-
        Asynchronously imports a batch of bookings. The whole batch is validated
        synchronously against each ticket's booking form — use the booking form
        fields endpoints to discover the import contract — and either every row
        is accepted (202 with a job to poll) or nothing is imported (422 with
        every problem, aggregated by error class).


        `externalBookingId` is the row's idempotency key: imports are
        create-once, so re-importing one returns the original booking (reported
        as `alreadyImported`) rather than creating a second — supply a fresh row
        `idempotencyKey` to import corrected data for the same id. Send an
        `Idempotency-Key` header to make the submission replayable: retrying the
        same key and body returns the original job; reusing a key with a
        different body is rejected (409).


        A row may pin the booking form it validates against via `bookingFormId`;
        the pin is required (`AMBIGUOUS_BOOKING_FORM`) when the ticket has more
        than one live form, and a supplied pin must be a live form of the row's
        ticket (`UNKNOWN_BOOKING_FORM`).


        Rows sharing an `externalOrderId` are given one `bookingReference`, so a
        purchase that produced several bookings stays one order; the reference
        is derived from the id, so rows split across batches or retried later
        still join it. `externalOrderId` is deliberately not an idempotency key
        — reusing one is how an order gains bookings, and rows carrying it still
        dedup individually on `externalBookingId`. It groups bookings, not
        payments: the financial records stay one per booking.


        A row may carry a `status` for backfilling history: withdrawn, deferred
        and canceled bookings, and bookings flagged as due to be transferred,
        are created in that state rather than created and then changed, so
        importing them sends no withdrawal or deferral emails to participants.
        Omit it for a confirmed booking. A completed transfer is not importable,
        because its meaning lives in the link between two bookings, which this
        API gives you no way to express; nor is a refund, or a refund taken as
        credit, because both imply payment or credit records an import does not
        create.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BookingImportRequest'
      responses:
        '202':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BookingImportAccepted'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                description: Bad request
                type: object
                properties:
                  statusCode:
                    type: number
                    enum:
                      - 400
                  error:
                    type: string
                  message:
                    type: string
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                description: Conflict
                type: object
                properties:
                  statusCode:
                    type: number
                    enum:
                      - 409
                  error:
                    type: string
                  message:
                    type: string
        '413':
          description: Payload too large
          content:
            application/json:
              schema:
                description: Payload too large
                type: object
                properties:
                  statusCode:
                    type: number
                    enum:
                      - 413
                  error:
                    type: string
                  message:
                    type: string
        '422':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BookingImportValidationFailure'
        '429':
          description: Rate limit exceeded
          headers:
            retry-after:
              schema:
                type: integer
              description: Seconds to wait before retrying the request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Requests permitted in the current window.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests still permitted in the current window.
            x-ratelimit-reset:
              schema:
                type: integer
              description: Seconds until the current window resets.
          content:
            application/json:
              schema:
                description: Rate limit exceeded
                type: object
                properties:
                  statusCode:
                    type: number
                    enum:
                      - 429
                  error:
                    type: string
                  message:
                    type: string
components:
  schemas:
    BookingImportRequest:
      title: BookingImportRequest
      type: object
      properties:
        bookings:
          type: array
          items:
            $ref: '#/components/schemas/BookingImportRow'
          description: The bookings to import.
          minItems: 1
          maxItems: 1000
      required:
        - bookings
      additionalProperties: false
      description: >-
        An asynchronous booking import request: the whole batch imports or none
        of it.
    BookingImportAccepted:
      title: BookingImportAccepted
      type: object
      properties:
        jobId:
          type: string
          description: The id of the queued import job.
        status:
          type: string
          enum:
            - queued
      required:
        - jobId
        - status
      additionalProperties: false
      description: The accepted import job. Poll the job endpoint for progress and results.
      example:
        jobId: 65d4e5f60718293a4b5c6d7e
        status: queued
    BookingImportValidationFailure:
      title: BookingImportValidationFailure
      type: object
      properties:
        error:
          type: string
          enum:
            - validation_failed
        message:
          type: string
          description: A human summary of the failure.
        totalRows:
          type: number
          description: How many rows the batch contained.
        rowsWithErrors:
          type: number
          description: How many rows had at least one error.
        errorCount:
          type: number
          description: How many errors were found in total.
        summary:
          type: array
          items:
            $ref: '#/components/schemas/BookingImportErrorSummary'
        errors:
          type: array
          items:
            $ref: '#/components/schemas/BookingImportRowErrors'
        truncated:
          type: boolean
          description: Whether `errors` was capped. `summary` is never capped.
      required:
        - error
        - message
        - totalRows
        - rowsWithErrors
        - errorCount
        - summary
        - errors
        - truncated
      additionalProperties: false
      description: >-
        The whole-batch validation failure: nothing was imported. `summary` is
        the load-bearing part — every error class, uncapped, ordered by
        frequency. `errors` carries capped per-row detail; `truncated` refers to
        `errors` only, never `summary`.
    BookingImportRow:
      title: BookingImportRow
      type: object
      properties:
        externalBookingId:
          type: string
          description: >-
            The caller's stable id for this booking, and **the import's
            idempotency key**: it is what makes a row safe to send twice.
            Imports are create-once per idempotency scope, so re-sending the
            same `externalBookingId` (without an `idempotencyKey`) returns the
            originally imported booking untouched, reported as `alreadyImported`
            — it does not update it, and it does not create a second booking.


            One `externalBookingId` therefore means one booking per idempotency
            scope. To import corrected data for an id you have already used,
            supply a fresh `idempotencyKey` — that is a new scope, so it
            deliberately creates a second booking carrying the same
            `externalBookingId`.
        idempotencyKey:
          type: string
          description: >-
            Optional caller-chosen idempotency scope, overriding the default
            `externalBookingId` scope. Supplying a fresh key allows importing
            corrected data for an `externalBookingId` that was already imported
            (e.g. after cancelling the original): the corrected row creates a
            new booking. The key is namespaced to your organizer and hashed
            server-side.
          minLength: 1
          maxLength: 255
        externalOrderId:
          type: string
          description: >-
            Your id for the purchase this booking belongs to, when one purchase
            produced several bookings — someone buying a place for themselves
            and one for a friend, in a single order.


            Every row sharing an `externalOrderId` is given the same
            `bookingReference`, so the order stays recognisable as one order.
            The reference is derived from the id, so rows split across batches,
            retried after a failure, or sent in a later import all resolve to
            the same order.


            **This is not an idempotency key.** Unlike `externalBookingId`,
            reusing an `externalOrderId` is expected and is how an order gets
            more than one booking: every row you send with it joins that order,
            and none of them dedups against the others. Sending the same
            `externalOrderId` twice with two different `externalBookingId`s
            creates two bookings in one order, which is the point.


            A row's order is fixed when that row is first imported. Because
            `externalBookingId` is the idempotency key, re-sending a row you
            have already imported does not add it to an order — the original
            booking is returned untouched, as `alreadyImported`. To correct a
            row, supply a fresh `idempotencyKey`.


            This groups bookings, not payments: the financial records stay one
            per booking, so a shared reference means "same order", not "same
            transaction". Omit it for a single-booking purchase and each booking
            gets its own reference.
          minLength: 1
          maxLength: 255
        ticketId:
          type: string
          description: The ticket the booking is for.
        bookingFormId:
          type: string
          description: >-
            The booking form to validate and import this row against — the same
            `:id` used to read the import contract (`GET
            /v0/booking-forms/:id/import-fields`). Optional when the ticket has
            exactly one live booking form (it is inferred); required when the
            ticket has several (`AMBIGUOUS_BOOKING_FORM` otherwise, listing the
            candidates). When supplied it must be a live form of the row's
            ticket (`UNKNOWN_BOOKING_FORM` otherwise) — a stale id is never
            silently ignored. Discover a ticket's forms via `GET
            /v0/tickets/:id/booking-forms`.
        bookedAt:
          $ref: '#/components/schemas/ISODate'
        participants:
          type: array
          items:
            $ref: '#/components/schemas/BookingImportParticipant'
          description: The booking's participants.
          minItems: 1
        bookerFields:
          type: array
          items:
            $ref: '#/components/schemas/BookingImportFieldAnswer'
          description: >-
            Booking-level booker answers (`booker.*` ids from the import
            contract). One booker per booking, alongside — not per —
            participant.
        status:
          $ref: '#/components/schemas/BookingImportStatus'
      required:
        - externalBookingId
        - ticketId
        - participants
      additionalProperties: false
      description: One booking to import.
    BookingImportErrorSummary:
      title: BookingImportErrorSummary
      type: object
      properties:
        code:
          type: string
        fieldId:
          type: string
          description: The canonical import id the class relates to, when field-scoped.
        count:
          type: number
          description: How many times this class occurred across the batch.
        message:
          type: string
          description: A representative message for the class.
        examples:
          type: array
          items:
            $ref: '#/components/schemas/BookingImportErrorLocator'
          description: The first few occurrences, located.
      required:
        - code
        - count
        - message
        - examples
      additionalProperties: false
      description: One validation problem class, aggregated across the batch.
    BookingImportRowErrors:
      title: BookingImportRowErrors
      type: object
      properties:
        externalBookingId:
          type: string
          description: The `externalBookingId` of the row.
        rowIndex:
          type: number
          description: The zero-based position of the row in `bookings`.
        errors:
          type: array
          items:
            $ref: '#/components/schemas/BookingImportErrorDetail'
      required:
        - rowIndex
        - errors
      additionalProperties: false
      description: The per-row detail of a failed validation, grouped by row.
    ISODate:
      title: ISODate
      type: string
      description: >-
        Represents a date and time in ISO 8601 format as a string.


        The string should follow the format `YYYY-MM-DDTHH:mm:ss.sssZ`, where:

        - `YYYY`: Four-digit year

        - `MM`: Two-digit month (01-12)

        - `DD`: Two-digit day of the month (01-31)

        - `T`: Delimiter indicating the start of the time component

        - `HH`: Two-digit hour in 24-hour format (00-23)

        - `mm`: Two-digit minutes (00-59)

        - `ss.sss`: Seconds with milliseconds (00.000-59.999)

        - `Z`: UTC timezone designator


        **Example:** To represent 11:30 AM on May 8th, 2026, the value would be:
        `"2026-05-08T11:30:00.000Z"`.
      example: '2026-05-08T11:30:00.000Z'
    BookingImportParticipant:
      title: BookingImportParticipant
      type: object
      properties:
        fields:
          type: array
          items:
            $ref: '#/components/schemas/BookingImportFieldAnswer'
          description: The participant's form answers.
        status:
          $ref: '#/components/schemas/BookingImportStatus'
      required:
        - fields
      additionalProperties: false
      description: One participant on an imported booking.
    BookingImportFieldAnswer:
      title: BookingImportFieldAnswer
      type: object
      properties:
        fieldId:
          type: string
          description: The canonical import id of the field being answered.
        value:
          type: string
          description: The answer for `value`-shaped fields.
        values:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                description: The sub-key this entry answers, for keyed composites.
              value:
                type: string
            required:
              - value
            additionalProperties: false
          description: The answer entries for `values`/`keyedValues`-shaped fields.
      required:
        - fieldId
      additionalProperties: false
      description: >-
        One field answer in an import payload. Scalar fields carry `value`; list
        answers (checkbox) and keyed composites (address, group-select) carry
        `values`, where each entry's `id` is the sub-key for keyed composites
        and absent for checkbox items. The field's import contract (see the
        booking form fields endpoints) says which shape applies.


        `WAIVER` fields take a signing state and timestamp as their `value`:
        `signed_date_<ISO 8601 timestamp>` (e.g.
        `signed_date_2026-07-20T09:30:00Z`), or
        `signed_guardian_date_<timestamp>` when a guardian accepted for a minor;
        `unsigned_date_<timestamp>` records an explicit non-acceptance. The
        timestamp is when the participant actually accepted, and is shown as the
        signature date. Do not invent an answer for a waiver you have no record
        of — omit the field and the waiver imports as unsigned, which is also
        where any value that is not a well-formed signing state lands. A
        malformed `signed…` value, however, rejects the row rather than silently
        importing unsigned.
    BookingImportStatus:
      title: BookingImportStatus
      anyOf:
        - $ref: '#/components/schemas/ConfirmedImportStatus'
        - $ref: '#/components/schemas/WithdrawnImportStatus'
        - $ref: '#/components/schemas/DeferredImportStatus'
        - $ref: '#/components/schemas/CancelledImportStatus'
        - $ref: '#/components/schemas/ToBeTransferredImportStatus'
      description: >-
        A state a booking may be imported in.


        Only states that can be described in full are accepted, so that an
        imported booking never claims something nothing backs up. A completed
        transfer is excluded because its meaning lives in the link between two
        bookings, which an import has no way to express; a refund, and a refund
        taken as credit, because they imply payment or credit records an import
        does not create. Credits have their own import if you need them to
        exist.
    BookingImportErrorLocator:
      title: BookingImportErrorLocator
      type: object
      properties:
        externalBookingId:
          type: string
          description: The `externalBookingId` of the offending row.
        rowIndex:
          type: number
          description: The zero-based position of the offending row in `bookings`.
        participantIndex:
          type: number
          description: The zero-based participant position, for participant-level errors.
        value:
          type: string
          description: The offending submitted value, when one exists.
      required:
        - rowIndex
      additionalProperties: false
      description: Where in the batch a validation error occurred.
    BookingImportErrorDetail:
      title: BookingImportErrorDetail
      type: object
      properties:
        code:
          type: string
        fieldId:
          type: string
        message:
          type: string
        participantIndex:
          type: number
          description: The zero-based participant position, for participant-level errors.
      required:
        - code
        - message
      additionalProperties: false
      description: One occurrence of a validation error, located.
    ConfirmedImportStatus:
      title: ConfirmedImportStatus
      type: object
      properties:
        type:
          type: string
          enum:
            - CONFIRMED
      required:
        - type
      additionalProperties: false
      description: >-
        The default. Equivalent to omitting `status`, and accepted so a caller
        mapping mixed history can always supply a value rather than
        conditionally omitting the field.
      example:
        type: CONFIRMED
    WithdrawnImportStatus:
      title: WithdrawnImportStatus
      type: object
      properties:
        type:
          type: string
          enum:
            - WITHDRAWN
        reason:
          type: string
          description: >-
            Why the participant withdrew, as your own records have it. Not
            constrained to the reasons our dashboard offers.
          maxLength: 255
        occurredAt:
          $ref: '#/components/schemas/ISODate'
      required:
        - type
      additionalProperties: false
      example:
        type: WITHDRAWN
        reason: Injured
        occurredAt: '2023-06-14T11:00:00.000Z'
    DeferredImportStatus:
      title: DeferredImportStatus
      type: object
      properties:
        type:
          type: string
          enum:
            - DEFERRED
        reason:
          type: string
          description: >-
            Why the participant deferred, as your own records have it. Not
            constrained to the reasons our dashboard offers.
          maxLength: 255
        deferredToYear:
          type: number
          description: The year the entry was deferred to.
          minimum: 2000
          maximum: 2100
        occurredAt:
          $ref: '#/components/schemas/ISODate'
      required:
        - type
      additionalProperties: false
      example:
        type: DEFERRED
        reason: Postpartum
        deferredToYear: 2026
    CancelledImportStatus:
      title: CancelledImportStatus
      type: object
      properties:
        type:
          type: string
          enum:
            - CANCELLED
        reason:
          type: string
          description: Why the booking was cancelled, as your own records have it.
          maxLength: 255
        occurredAt:
          $ref: '#/components/schemas/ISODate'
      required:
        - type
      additionalProperties: false
      example:
        type: CANCELLED
        reason: Event cancelled
    ToBeTransferredImportStatus:
      title: ToBeTransferredImportStatus
      type: object
      properties:
        type:
          type: string
          enum:
            - TO_BE_TRANSFERRED
        reason:
          type: string
          description: >-
            Why the booking is due to be transferred, as your own records have
            it.
          maxLength: 255
        occurredAt:
          $ref: '#/components/schemas/ISODate'
      required:
        - type
      additionalProperties: false
      description: >-
        The booking is flagged as due to be moved to another race, date or
        person, without that move having happened yet.


        A staging state rather than an ending: the booking still exists and the
        participant still holds a place. Nothing about the intended destination
        is recorded, because the platform records none for this flag either.
      example:
        type: TO_BE_TRANSFERRED
        reason: Requested a later date
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

````