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

# Reservar una sesión

> Reserva una sesión para un cliente, con las mismas reglas que tiene el cliente al reservar desde la web
del estudio: la anticipación máxima, el tope de reservas, el límite de ausencias, las clases que solo
reserva el estudio y la sede propia. Cuesta 1 crédito. Si la sesión está completa, anota al cliente en
la lista de espera, salvo con `allow_waitlist: false`. Avisa al cliente como cualquier reserva, salvo con
`notify: false`.

Responde `201` con la reserva. Si el cliente había cancelado esa sesión, la reserva vuelve con el mismo
`bkg_`. Con `dry_run=true` responde `200` con la vista previa.

Rechazos (`409`): `already_booked`, `session_full`, `session_started`, `session_cancelled`,
`session_on_vacation`, `class_archived`, `client_inactive`, `membership_suspended`,
`membership_frozen`, `class_restricted_to_studio`, `outside_home_center`, `booking_window_closed`,
`no_show_limit_reached`, `max_active_bookings_reached`, `insufficient_credits`,
`credits_not_valid_for_class`, `credits_not_valid_for_date`. Un `client_id` o un `session_id` que no
es de tu negocio responde `404 not_found`.

Una escritura: lleva `Idempotency-Key` (salvo con `dry_run=true`), solo para estudios (en una academia
responde `wrong_vertical`), y solo si tu negocio tiene habilitada la escritura por la API
(`write_access_disabled` si no). Un campo que el cuerpo no declara responde `validation_failed` con
`unknown_field`. Cuenta para el límite de escrituras (30 por minuto).




## OpenAPI

````yaml /openapi.yaml post /bookings
openapi: 3.1.0
info:
  title: API de Craqui
  version: '2026-12-01'
  summary: Conecta tu web o tu sistema a tu estudio o academia en Craqui.
  description: >
    API REST de Craqui. Cada clave pertenece a un solo negocio (un estudio o una
    academia) y todo lo

    que devuelve es de ese negocio.


    - **Autenticación:** `Authorization: Bearer <clave>`. Las claves se crean en
    Configuración →
      Integraciones. Una clave `crq_live_` usa los datos reales del negocio; una `crq_test_` usa su
      estudio de prueba: la misma configuración (sedes, clases, packs, planes) con clientes y
      reservas de ejemplo, que no le escribe a nadie. La URL es la misma: el entorno lo decide la
      clave. Todo objeto dice en `livemode` de cuál de los dos viene.
    - **Versión:** cada clave queda fijada a la versión vigente cuando se creó.
    El header
      `Craqui-Version` la sobrescribe por pedido, y la respuesta siempre dice cuál se usó.
    - **Errores:** `application/problem+json` (RFC 9457), con un `code` estable
    para programar y un
      `request_id` para buscar el pedido en tu panel.
    - **Montos:** enteros en `amount_cents` (siempre ×100) junto con la moneda
    ISO 4217.

    - **Fechas y horas:** instantes en RFC 3339 con el offset del negocio;
    fechas sin hora como
      `YYYY-MM-DD` en la fecha local del negocio.
  contact:
    name: Craqui
    url: https://craqui.com
servers:
  - url: https://api.craqui.com/v1
    description: Producción
security:
  - bearerAuth: []
tags:
  - name: business
    description: El negocio al que pertenece la clave (un estudio o una academia).
  - name: booking_policy
    description: Las reglas de reserva del estudio. Solo estudios.
  - name: centers
    description: Las sedes del estudio. Solo estudios.
  - name: rooms
    description: Los salones de cada sede. Solo estudios.
  - name: teachers
    description: Quienes dan clases en el estudio. Solo estudios.
  - name: courses
    description: Las disciplinas del estudio (Yoga, Pilates…). Solo estudios.
  - name: classes
    description: Las clases con su horario semanal. Solo estudios.
  - name: sessions
    description: El horario del estudio, sesión por sesión, con sus lugares. Solo estudios.
  - name: clients
    description: >-
      Los clientes del estudio, con el contacto separado en su propio permiso.
      Solo estudios.
  - name: bookings
    description: Las reservas de las sesiones. Solo estudios.
  - name: credit_packs
    description: Los packs de créditos que vende el estudio. Solo estudios.
  - name: membership_plans
    description: Los planes de membresía que vende el estudio. Solo estudios.
  - name: intro_offers
    description: Las ofertas de bienvenida que vende el estudio. Solo estudios.
  - name: credits
    description: Los créditos de los clientes, sus lotes y sus movimientos. Solo estudios.
  - name: memberships
    description: Las membresías de los clientes y sus congelamientos. Solo estudios.
  - name: fees
    description: Las cuotas que el estudio le cobra a cada cliente. Solo estudios.
  - name: payments
    description: Los pagos de las cuotas. Solo estudios.
  - name: refunds
    description: Los reembolsos de los pagos. Solo estudios.
  - name: events
    description: >
      Lo que cambió en el estudio en los últimos 30 días, para enterarte sin
      consultar cada lista. Solo

      estudios.
  - name: webhook_endpoints
    description: >
      Las URLs a las que Craqui le avisa cada vez que algo cambia en el estudio,
      firmado y con reintentos. Solo

      estudios y solo con una clave `crq_live_`.
paths:
  /bookings:
    post:
      tags:
        - bookings
      summary: Reservar una sesión
      description: >
        Reserva una sesión para un cliente, con las mismas reglas que tiene el
        cliente al reservar desde la web

        del estudio: la anticipación máxima, el tope de reservas, el límite de
        ausencias, las clases que solo

        reserva el estudio y la sede propia. Cuesta 1 crédito. Si la sesión está
        completa, anota al cliente en

        la lista de espera, salvo con `allow_waitlist: false`. Avisa al cliente
        como cualquier reserva, salvo con

        `notify: false`.


        Responde `201` con la reserva. Si el cliente había cancelado esa sesión,
        la reserva vuelve con el mismo

        `bkg_`. Con `dry_run=true` responde `200` con la vista previa.


        Rechazos (`409`): `already_booked`, `session_full`, `session_started`,
        `session_cancelled`,

        `session_on_vacation`, `class_archived`, `client_inactive`,
        `membership_suspended`,

        `membership_frozen`, `class_restricted_to_studio`,
        `outside_home_center`, `booking_window_closed`,

        `no_show_limit_reached`, `max_active_bookings_reached`,
        `insufficient_credits`,

        `credits_not_valid_for_class`, `credits_not_valid_for_date`. Un
        `client_id` o un `session_id` que no

        es de tu negocio responde `404 not_found`.


        Una escritura: lleva `Idempotency-Key` (salvo con `dry_run=true`), solo
        para estudios (en una academia

        responde `wrong_vertical`), y solo si tu negocio tiene habilitada la
        escritura por la API

        (`write_access_disabled` si no). Un campo que el cuerpo no declara
        responde `validation_failed` con

        `unknown_field`. Cuenta para el límite de escrituras (30 por minuto).
      operationId: createBooking
      parameters:
        - $ref: '#/components/parameters/CraquiVersion'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/DryRun'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BookingCreateRequest'
            example:
              client_id: cli_01jqy553dmxw6vmq7g5vzfa9yz
              session_id: ses_01j2d1vb84pqs90137c12qvz8q
      responses:
        '200':
          description: Con `dry_run=true`, la vista previa. No se escribió nada.
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
            Craqui-Version:
              $ref: '#/components/headers/CraquiVersion'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Preview'
              example:
                object: preview
                livemode: true
                action: booking.create
                result:
                  object: booking
                  id: null
                  client_id: cli_01jqy553dmxw6vmq7g5vzfa9yz
                  session_id: ses_01j2d1vb84pqs90137c12qvz8q
                  status: confirmed
                  credits_charged: 1
                  waitlist_position: null
                effects:
                  credits_charged: 1
                  credits_refunded: 0
                  credit_balance_after: 5
                  waitlist_position: null
                  notifies_client: true
                warnings: []
                summary: Se reserva la clase para el cliente.
        '201':
          description: La reserva, confirmada o en lista de espera.
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
            Craqui-Version:
              $ref: '#/components/headers/CraquiVersion'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            Idempotent-Replayed:
              $ref: '#/components/headers/IdempotentReplayed'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Booking'
              example:
                id: bkg_3w3mn9wjsz9t59t8azd9xrs78e
                object: booking
                livemode: true
                client_id: cli_01jqy553dmxw6vmq7g5vzfa9yz
                session_id: ses_01j2d1vb84pqs90137c12qvz8q
                status: confirmed
                credits_charged: 1
                waitlist_position: null
                booked_at: '2026-12-01T10:00:04-03:00'
                cancelled_at: null
                free_cancel_until: '2026-12-02T20:00:00-03:00'
                recurring: false
                credit_refunded: false
                source: api
                created_at: '2026-12-01T10:00:04-03:00'
                updated_at: '2026-12-01T10:00:04-03:00'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
components:
  parameters:
    CraquiVersion:
      name: Craqui-Version
      in: header
      required: false
      description: >
        Versión de la API para este pedido (`YYYY-MM-DD`). Si no la mandas, se
        usa la versión fijada en

        la clave cuando se creó.
      schema:
        type: string
        pattern: ^\d{4}-\d{2}-\d{2}$
        examples:
          - '2026-12-01'
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >
        Obligatorio en todo pedido que crea o cambia algo, salvo con
        `dry_run=true`. Un valor único por

        operación (por ejemplo un UUID). Si repites el pedido con la misma clave
        y el mismo cuerpo dentro de 24

        horas, recibes la misma respuesta (con el header `Idempotent-Replayed:
        true`) sin que se repita el efecto,

        también la misma respuesta de error: para intentar de nuevo algo que se
        rechazó, usa una clave nueva.

        Con un `dry_run=true` la clave se ignora y no se guarda: el pedido real
        puede usar la misma. Pasadas las 24

        horas, repetir una reserva con la misma clave y el mismo cuerpo sigue
        devolviendo la reserva que se hizo

        (como está ahora), y la misma clave con otro cuerpo responde
        `idempotency_key_reused`.
      schema:
        type: string
        minLength: 1
        maxLength: 255
    DryRun:
      name: dry_run
      in: query
      required: false
      description: >
        `true` para ver qué pasaría sin hacerlo: Craqui evalúa el pedido con las
        mismas reglas que el real y

        responde `200` con un objeto `preview`, sin escribir nada ni avisarle a
        nadie. Cuenta para el límite de

        escrituras. `false` (o sin el parámetro) hace el pedido real. Es el
        único parámetro de consulta de una

        escritura.
      schema:
        type: boolean
  schemas:
    BookingCreateRequest:
      type: object
      additionalProperties: false
      required:
        - client_id
        - session_id
      properties:
        client_id:
          $ref: '#/components/schemas/ClientId'
        session_id:
          $ref: '#/components/schemas/SessionId'
        allow_waitlist:
          type: boolean
          default: true
          description: >
            Con la sesión completa, `true` anota al cliente en la lista de
            espera (sin descontarle créditos hasta

            que entre) y `false` responde `409 session_full` sin escribir nada.
        notify:
          $ref: '#/components/schemas/Notify'
    Preview:
      type: object
      description: >
        Lo que haría la escritura, sin hacerla. Craqui la evalúa con las mismas
        reglas que el pedido real: si el

        pedido real se rechazaría, la vista previa responde el mismo error.
        Entre la vista previa y el pedido real

        algo puede cambiar (otra persona ocupa el último lugar), y entonces el
        real responde lo que corresponda

        en ese momento.
      required:
        - object
        - livemode
        - action
        - result
        - effects
        - warnings
        - summary
      properties:
        object:
          type: string
          const: preview
        livemode:
          type: boolean
          description: >-
            `false` si la vista previa es de tu estudio de prueba (claves
            `crq_test_`); `true` con claves `crq_live_`.
        action:
          type: string
          description: >-
            Qué escritura se evaluó: `booking.create`, `booking.cancel` o
            `booking.set_attendance`. La lista puede crecer.
          enum:
            - booking.create
            - booking.cancel
            - booking.set_attendance
          x-extensible-enum: true
        result:
          $ref: '#/components/schemas/PreviewBooking'
        effects:
          $ref: '#/components/schemas/PreviewEffects'
        warnings:
          type: array
          description: >
            Lo que conviene saber, como códigos: `waitlisted` (la reserva queda
            en lista de espera),

            `waitlist_promoted` (cancelar hace entrar a otra persona de la lista
            de espera) y `no_change` (la

            reserva ya está así; el pedido real no cambia nada). La lista puede
            crecer.
          items:
            type: string
            enum:
              - waitlisted
              - waitlist_promoted
              - no_change
            x-extensible-enum: true
        summary:
          type: string
          description: >-
            Lo mismo en una frase en español, para mostrarle a una persona. No
            nombra al cliente.
    Booking:
      type: object
      description: >
        La reserva de un cliente en una sesión.


        Volver a reservar una sesión que el cliente había cancelado **reusa** el
        mismo `bkg_`: el id es

        uno por cliente y sesión, no uno por intento. `booked_at` es el del
        último intento.
      required:
        - id
        - object
        - livemode
        - client_id
        - session_id
        - status
        - credits_charged
        - waitlist_position
        - booked_at
        - cancelled_at
        - free_cancel_until
        - recurring
        - credit_refunded
        - source
        - created_at
        - updated_at
      properties:
        id:
          $ref: '#/components/schemas/BookingId'
        object:
          type: string
          const: booking
        livemode:
          type: boolean
          description: >-
            `false` si el objeto es de tu estudio de prueba (claves
            `crq_test_`); `true` con claves `crq_live_`.
        client_id:
          description: >
            El cliente. `null` si quien reservó no es cliente del estudio (por
            ejemplo, alguien del equipo).
          oneOf:
            - $ref: '#/components/schemas/ClientId'
            - type: 'null'
        session_id:
          description: La sesión. `null` si la sesión se borró.
          oneOf:
            - $ref: '#/components/schemas/SessionId'
            - type: 'null'
        status:
          type: string
          description: >
            `confirmed`, `waitlisted`, `cancelled`, `attended` o `no_show`. La
            lista puede crecer: trata un

            valor que no conoces como una reserva que no ocupa lugar.
          enum:
            - confirmed
            - waitlisted
            - cancelled
            - attended
            - no_show
          x-extensible-enum: true
        credits_charged:
          type: integer
          description: Los créditos que se descontaron al reservar (0 en lista de espera).
        waitlist_position:
          type:
            - integer
            - 'null'
          description: El lugar en la lista de espera; `null` fuera de ella.
        booked_at:
          $ref: '#/components/schemas/Instant'
        cancelled_at:
          oneOf:
            - $ref: '#/components/schemas/Instant'
            - type: 'null'
        free_cancel_until:
          description: >
            Hasta cuándo cancelar devuelve el crédito, con el plazo de la clase,
            de su disciplina o del

            estudio, y la regla de la lista de espera. Es una **predicción**: lo
            decide Craqui al cancelar.

            `null` si la reserva no está `confirmed` ni `waitlisted`, o si la
            sesión ya empezó.
          oneOf:
            - $ref: '#/components/schemas/Instant'
            - type: 'null'
        recurring:
          type: boolean
          description: Si la reserva salió de una reserva fija.
        credit_refunded:
          type: boolean
          description: >-
            Si se le devolvió el crédito de esta reserva (desde su último
            `booked_at`).
        source:
          type: string
          description: >
            De dónde salió la reserva: `api`, `recurring` (una reserva fija),
            `aggregator` (una plataforma

            de terceros) o `craqui` (el estudio o el cliente en Craqui). La
            lista puede crecer.
          enum:
            - api
            - recurring
            - aggregator
            - craqui
          x-extensible-enum: true
        created_at:
          $ref: '#/components/schemas/Instant'
        updated_at:
          $ref: '#/components/schemas/Instant'
        client:
          description: >-
            Solo con `expand[]=client` (pide `clients:read`). `null` si
            `client_id` es `null`.
          oneOf:
            - $ref: '#/components/schemas/Client'
            - type: 'null'
        session:
          description: >-
            Solo con `expand[]=session` (pide `sessions:read`). `null` si
            `session_id` es `null`.
          oneOf:
            - $ref: '#/components/schemas/Session'
            - type: 'null'
    ClientId:
      type: string
      description: Id de un cliente del estudio.
      pattern: ^cli_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - cli_01jatfha5h4nkg77c3wfrvd3cw
    SessionId:
      type: string
      description: Id de una sesión (una fecha de una clase).
      pattern: ^ses_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - ses_01jnb07m4yxxe0s687jf750tkv
    Notify:
      type: boolean
      description: >
        `false` para no enviarle al cliente los avisos de esta escritura (email,
        WhatsApp). Necesita el permiso

        `notifications:suppress`; sin él, la respuesta es `403 scope_required`.
        Nunca silencia el aviso a otra

        persona: si una cancelación hace entrar a alguien de la lista de espera,
        esa persona se entera igual. Los

        recordatorios posteriores salen igual. Por defecto `true`. Tu estudio de
        prueba no le avisa a nadie,

        diga lo que diga este campo.
      default: true
    PreviewBooking:
      type: object
      description: La reserva como quedaría después de la escritura.
      required:
        - object
        - id
        - client_id
        - session_id
        - status
        - credits_charged
        - waitlist_position
      properties:
        object:
          type: string
          const: booking
        id:
          description: El id de la reserva; `null` si la escritura crearía una nueva.
          oneOf:
            - $ref: '#/components/schemas/BookingId'
            - type: 'null'
        client_id:
          oneOf:
            - $ref: '#/components/schemas/ClientId'
            - type: 'null'
        session_id:
          oneOf:
            - $ref: '#/components/schemas/SessionId'
            - type: 'null'
        status:
          type: string
          enum:
            - confirmed
            - waitlisted
            - cancelled
            - attended
            - no_show
          x-extensible-enum: true
        credits_charged:
          type: integer
          description: Los créditos descontados de la reserva.
        waitlist_position:
          type:
            - integer
            - 'null'
          description: El lugar en la lista de espera; `null` fuera de ella.
    PreviewEffects:
      type: object
      description: Lo que la escritura le haría al saldo del cliente y a quién avisaría.
      required:
        - credits_charged
        - credits_refunded
        - credit_balance_after
        - waitlist_position
        - notifies_client
      properties:
        credits_charged:
          type: integer
          description: Créditos que se descontarían (0 en lista de espera).
        credits_refunded:
          type: integer
          description: >-
            Créditos que se devolverían (al cancelar a tiempo, o al marcar
            ausente si el estudio no los cobra).
        credit_balance_after:
          type: integer
          description: El saldo de créditos vigentes del cliente después de la escritura.
        waitlist_position:
          type:
            - integer
            - 'null'
          description: El lugar en la lista de espera, si la reserva quedaría ahí.
        notifies_client:
          type: boolean
          description: >
            Si la escritura le dispara un aviso al cliente; le llega por los
            canales que el estudio tenga

            encendidos. `false` con `notify: false`, en tu estudio de prueba y
            al marcar presente.
    BookingId:
      type: string
      description: Id de una reserva.
      pattern: ^bkg_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - bkg_01jx9eak0437zdf0yw0atg7g0r
    Instant:
      type: string
      format: date-time
      description: Instante en RFC 3339 con el offset del negocio en ese momento.
      examples:
        - '2026-12-02T19:00:00-03:00'
    Client:
      type: object
      description: >
        Un cliente del estudio. Nunca trae fecha de nacimiento, dirección,
        documento, datos fiscales, de

        salud, notas ni contacto de emergencia.


        Un cliente `deleted` (el estudio lo dio de baja) responde solo por id o
        dentro de un `expand`,

        porque una reserva o una cuota vieja sigue apuntando a su id: trae `id`,
        `status` y fechas, con

        `first_name` y `last_name` en `null`, y nunca `email` ni `phone`, con
        ningún permiso.
      required:
        - id
        - object
        - livemode
        - first_name
        - last_name
        - status
        - home_center_id
        - created_at
        - updated_at
      properties:
        id:
          $ref: '#/components/schemas/ClientId'
        object:
          type: string
          const: client
        livemode:
          type: boolean
          description: >-
            `false` si el objeto es de tu estudio de prueba (claves
            `crq_test_`); `true` con claves `crq_live_`.
        first_name:
          type:
            - string
            - 'null'
          description: '`null` en un cliente `deleted`, o si el estudio no lo cargó.'
          examples:
            - Sofía
        last_name:
          type:
            - string
            - 'null'
          description: '`null` en un cliente `deleted`, o si el estudio no lo cargó.'
          examples:
            - Pérez
        status:
          type: string
          description: >
            `active`, `inactive` o `deleted`. Las listas traen solo `active` e
            `inactive`. La lista puede

            crecer: trata un valor que no conoces como inactivo.
          enum:
            - active
            - inactive
            - deleted
          x-extensible-enum: true
        home_center_id:
          description: La sede habitual del cliente, si el estudio le asignó una.
          oneOf:
            - $ref: '#/components/schemas/CenterId'
            - type: 'null'
        created_at:
          $ref: '#/components/schemas/Instant'
          description: Cuándo pasó a ser cliente de este estudio.
        updated_at:
          $ref: '#/components/schemas/Instant'
        email:
          type:
            - string
            - 'null'
          description: >
            Solo con el permiso `clients.pii:read`: sin él, el campo no viene
            (no es `null`). `null` si el

            cliente no tiene email.
        phone:
          type:
            - string
            - 'null'
          description: >
            Solo con el permiso `clients.pii:read`: sin él, el campo no viene.
            En formato E.164

            (`+59899123456`); `null` si no tiene teléfono o si no se pudo leer
            como uno.
          pattern: ^\+[1-9]\d{6,14}$
          examples:
            - '+59899123456'
        active_membership:
          description: >
            Solo en `GET /v1/clients/{id}` con `expand[]=active_membership`
            (pide `memberships:read`): la

            membresía que tiene ahora (la `active`; si no hay, la más nueva
            `paused`, `suspended` o

            `pending_payment`). `null` si no tiene ninguna.
          oneOf:
            - $ref: '#/components/schemas/Membership'
            - type: 'null'
        credit_balance:
          $ref: '#/components/schemas/CreditBalance'
          description: >-
            Solo en `GET /v1/clients/{id}` con `expand[]=credit_balance` (pide
            `credits:read`).
    Session:
      type: object
      description: >
        Una sesión: una fecha de una clase, con su horario, quién la da y sus
        lugares. Solo sesiones de

        clases: los workshops no están en esta versión.
      required:
        - id
        - object
        - livemode
        - class_id
        - course_id
        - center_id
        - room_id
        - teacher_id
        - starts_at
        - ends_at
        - local_date
        - local_start_time
        - local_end_time
        - time_zone
        - status
        - capacity
        - booked_count
        - waitlist_count
        - available_spots
        - created_at
        - updated_at
      properties:
        id:
          $ref: '#/components/schemas/SessionId'
        object:
          type: string
          const: session
        livemode:
          type: boolean
          description: >-
            `false` si el objeto es de tu estudio de prueba (claves
            `crq_test_`); `true` con claves `crq_live_`.
        class_id:
          $ref: '#/components/schemas/ClassId'
        course_id:
          $ref: '#/components/schemas/CourseId'
        center_id:
          oneOf:
            - $ref: '#/components/schemas/CenterId'
            - type: 'null'
        room_id:
          description: El salón de esta sesión, si tiene uno asignado.
          oneOf:
            - $ref: '#/components/schemas/RoomId'
            - type: 'null'
        teacher_id:
          description: >
            Quien da esta sesión, con la suplencia aplicada (igual que el
            calendario del estudio). `null`

            si no tiene instructor asignado.
          oneOf:
            - $ref: '#/components/schemas/TeacherId'
            - type: 'null'
        starts_at:
          $ref: '#/components/schemas/Instant'
          description: >
            El inicio, con el offset del estudio en ese momento. Una hora local
            que no existe por el

            cambio de horario (por ejemplo 00:30 la noche en que se adelanta el
            reloj) se corre una hora,

            igual que al reservar: en ese caso `starts_at` y `local_start_time`
            no coinciden.
        ends_at:
          $ref: '#/components/schemas/Instant'
          description: El fin. Una clase que cruza la medianoche termina al día siguiente.
        local_date:
          $ref: '#/components/schemas/LocalDate'
        local_start_time:
          $ref: '#/components/schemas/LocalTime'
        local_end_time:
          $ref: '#/components/schemas/LocalTime'
        time_zone:
          type:
            - string
            - 'null'
          description: >-
            La zona horaria del estudio (IANA). `null` si el estudio no eligió
            una; los instantes salen en `+00:00`.
          examples:
            - America/Montevideo
        status:
          type: string
          description: >
            `scheduled`, `cancelled` (el estudio la canceló) o `studio_closed`
            (el estudio cerró ese día).

            La lista puede crecer: trata un valor que no conoces como una sesión
            que no se da.
          enum:
            - scheduled
            - cancelled
            - studio_closed
          x-extensible-enum: true
        capacity:
          type:
            - integer
            - 'null'
          description: El cupo de la sesión. `null` si la clase no tiene cupo.
        booked_count:
          type: integer
          description: >
            Los lugares tomados: reservas confirmadas, asistidas y ausentes
            hasta que la sesión empieza. Es

            la misma cuenta que usa Craqui al reservar.
          minimum: 0
        waitlist_count:
          type: integer
          description: Cuántas personas esperan en la lista de espera.
          minimum: 0
        available_spots:
          type:
            - integer
            - 'null'
          description: >-
            `capacity - booked_count`, nunca menos de 0. `null` si la clase no
            tiene cupo.
          minimum: 0
        created_at:
          $ref: '#/components/schemas/Instant'
        updated_at:
          $ref: '#/components/schemas/Instant'
    Problem:
      type: object
      description: >
        Un error. Programa contra `code`, que no cambia; `title` y `detail` son
        para mostrarle a una

        persona y pueden cambiar de redacción.
      required:
        - type
        - title
        - status
        - code
        - request_id
      properties:
        type:
          type: string
          format: uri
          description: URL de la página de la documentación de este error.
          examples:
            - https://docs.craqui.com/errors/invalid_api_key
        title:
          type: string
          description: Resumen corto, en español.
          examples:
            - La clave no es válida
        status:
          type: integer
          description: El mismo código HTTP de la respuesta.
          examples:
            - 401
        code:
          type: string
          description: >
            Código estable del error. La lista puede crecer: trata un `code` que
            no conoces como un

            error genérico de su `status`.
          examples:
            - invalid_api_key
        detail:
          type: string
          description: Qué pasó y qué hacer, en español.
          examples:
            - >-
              Revisa que copiaste la clave completa, incluido el prefijo
              (crq_live_ o crq_test_).
        request_id:
          type: string
          pattern: ^req_[0-9a-hjkmnp-tv-z]{26}$
          description: >-
            Identificador del pedido. Búscalo en Configuración → Integraciones →
            Actividad de la API.
          examples:
            - req_01hzx8m4k2a7d9q3f5w6y8z0bc
        doc_url:
          type: string
          format: uri
          description: >-
            Igual a `type`, por compatibilidad con clientes que buscan este
            campo.
        required_scope:
          type: string
          description: >-
            Solo en `scope_required` y `event_type_not_permitted`. El permiso
            que le falta a la clave.
          examples:
            - business:read
        errors:
          type: array
          description: Solo en errores de validación. Un elemento por campo con problemas.
          items:
            $ref: '#/components/schemas/FieldError'
    CenterId:
      type: string
      description: Id de una sede.
      pattern: ^ctr_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - ctr_01j65pdnq74qvnh0953pzvtczg
    Membership:
      type: object
      description: >
        La membresía de un cliente a un plan. Una membresía anulada o una compra
        que no se terminó no existen

        para la API: no se listan y su id responde `not_found`.
      required:
        - id
        - object
        - livemode
        - client_id
        - membership_plan_id
        - status
        - current_period_start
        - current_period_end
        - renews_at
        - next_credit_date
        - started_at
        - cancelled_at
        - suspension_reason
        - discount
        - active_freeze_id
        - created_at
        - updated_at
      properties:
        id:
          $ref: '#/components/schemas/MembershipId'
        object:
          type: string
          const: membership
        livemode:
          type: boolean
          description: >-
            `false` si el objeto es de tu estudio de prueba (claves
            `crq_test_`); `true` con claves `crq_live_`.
        client_id:
          description: >-
            El cliente. `null` si la membresía es de alguien que no es cliente
            del estudio.
          oneOf:
            - $ref: '#/components/schemas/ClientId'
            - type: 'null'
        membership_plan_id:
          $ref: '#/components/schemas/MembershipPlanId'
        status:
          type: string
          description: >
            `active`, `paused` (congelada), `suspended`, `pending_payment`,
            `cancelled` o `expired`. La lista

            puede crecer: trata un valor que no conoces como una membresía que
            no da créditos.
          enum:
            - active
            - paused
            - suspended
            - pending_payment
            - cancelled
            - expired
          x-extensible-enum: true
        current_period_start:
          $ref: '#/components/schemas/Instant'
        current_period_end:
          $ref: '#/components/schemas/Instant'
        renews_at:
          description: >
            Cuándo se renueva sola: el fin del período actual si la membresía
            está `active` y su plan se

            renueva solo; si no, `null`.
          oneOf:
            - $ref: '#/components/schemas/Instant'
            - type: 'null'
        next_credit_date:
          description: >-
            La fecha local en que recibe los próximos créditos; `null` si no hay
            una prevista.
          oneOf:
            - $ref: '#/components/schemas/LocalDate'
            - type: 'null'
        started_at:
          $ref: '#/components/schemas/Instant'
        cancelled_at:
          oneOf:
            - $ref: '#/components/schemas/Instant'
            - type: 'null'
        suspension_reason:
          type:
            - string
            - 'null'
          description: Por qué se suspendió, como texto; `null` si no está suspendida.
        discount:
          description: El descuento que tiene la membresía; `null` si no tiene.
          oneOf:
            - $ref: '#/components/schemas/MembershipDiscount'
            - type: 'null'
        active_freeze_id:
          description: >-
            El congelamiento en curso o programado; `null` si no hay. El detalle
            está en `GET /v1/memberships/{id}/freezes`.
          oneOf:
            - $ref: '#/components/schemas/MembershipFreezeId'
            - type: 'null'
        created_at:
          $ref: '#/components/schemas/Instant'
        updated_at:
          $ref: '#/components/schemas/Instant'
    CreditBalance:
      type: object
      description: >
        El saldo de créditos de un cliente. `total_credits` es el saldo que
        vale: es el que ve el estudio y el

        que usa Craqui al reservar. La suma de `grants` puede no coincidir con
        él (por ejemplo, por días de

        gracia de una membresía o un lote que vence hoy).
      required:
        - object
        - livemode
        - client_id
        - total_credits
        - grants
        - grants_has_more
      properties:
        object:
          type: string
          const: credit_balance
        livemode:
          type: boolean
          description: >-
            `false` si el objeto es de tu estudio de prueba (claves
            `crq_test_`); `true` con claves `crq_live_`.
        client_id:
          $ref: '#/components/schemas/ClientId'
        total_credits:
          type: integer
          description: El saldo del cliente; `0` si nunca tuvo créditos.
        grants:
          type: array
          description: >-
            Los lotes que se pueden usar (sin anular, con créditos y sin
            vencer), del que vence primero al último. Como mucho 100.
          items:
            $ref: '#/components/schemas/CreditBalanceGrant'
        grants_has_more:
          type: boolean
          description: >-
            `true` si el cliente tiene más de 100 lotes vivos; la lista completa
            está en `GET /v1/credit_grants?client_id=…&status=active`.
    ClassId:
      type: string
      description: Id de una clase (el grupo con su horario semanal).
      pattern: ^cls_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - cls_01jryg07jeazdbx61dnx96ckmv
    CourseId:
      type: string
      description: Id de una disciplina.
      pattern: ^crs_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - crs_01jff81dk7rxtacmye5nanfp0h
    RoomId:
      type: string
      description: Id de un salón.
      pattern: ^room_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - room_01jhweg897s9npa0tasfgt6a65
    TeacherId:
      type: string
      description: Id de un instructor.
      pattern: ^tch_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - tch_01jmwqh91chk154n8xwnaw5ynd
    LocalDate:
      type: string
      format: date
      description: Fecha local del negocio, `YYYY-MM-DD`.
      pattern: ^\d{4}-\d{2}-\d{2}$
      examples:
        - '2026-12-02'
    LocalTime:
      type: string
      description: Hora local del negocio, `HH:MM` (24 horas).
      pattern: ^\d{2}:\d{2}$
      examples:
        - '19:00'
    FieldError:
      type: object
      required:
        - field
        - code
      properties:
        field:
          type: string
          description: El campo, como lo mandaste (por ejemplo `session_id`).
          examples:
            - session_id
        code:
          type: string
          description: Qué tiene mal el campo.
          examples:
            - invalid_prefix
        detail:
          type: string
          description: Explicación en español.
    MembershipId:
      type: string
      description: Id de una membresía de un cliente.
      pattern: ^mem_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - mem_01j1q27t44qvr6v2s50cv4rbvq
    MembershipPlanId:
      type: string
      description: Id de un plan de membresía.
      pattern: ^mpl_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - mpl_01jt05nvk11xa1zm1pwve2qj7r
    MembershipDiscount:
      type: object
      required:
        - percent
        - fixed_amount
        - periods_remaining
      properties:
        percent:
          type:
            - number
            - 'null'
          description: >-
            El porcentaje de descuento (por ejemplo `15` es 15 %); `null` si es
            un monto fijo.
        fixed_amount:
          description: >-
            El monto fijo que se descuenta de cada cuota; `null` si es un
            porcentaje.
          oneOf:
            - $ref: '#/components/schemas/Money'
            - type: 'null'
        periods_remaining:
          type:
            - integer
            - 'null'
          description: Cuántos períodos más se aplica; `null` si se aplica siempre.
    MembershipFreezeId:
      type: string
      description: Id de un congelamiento de una membresía.
      pattern: ^frz_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - frz_01jtk6e3z2vm9ych2cc7kadyja
    CreditBalanceGrant:
      type: object
      required:
        - credit_grant_id
        - kind
        - credits_remaining
        - expires_at
        - course_ids
      properties:
        credit_grant_id:
          $ref: '#/components/schemas/CreditGrantId'
        kind:
          type:
            - string
            - 'null'
          description: '`pack`, `intro_offer` o `membership_period`. La lista puede crecer.'
          enum:
            - pack
            - intro_offer
            - membership_period
            - null
          x-extensible-enum: true
        credits_remaining:
          type: integer
        expires_at:
          description: Cuándo vencen; `null` si no vencen.
          oneOf:
            - $ref: '#/components/schemas/Instant'
            - type: 'null'
        course_ids:
          description: Las disciplinas en las que sirven; `null` si sirven en todas.
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/CourseId'
    Money:
      type: object
      required:
        - amount_cents
        - currency
      properties:
        amount_cents:
          type: integer
          description: El monto multiplicado por 100. `250000` con `UYU` es $ 2.500,00.
          examples:
            - 250000
        currency:
          $ref: '#/components/schemas/CurrencyCode'
    CreditGrantId:
      type: string
      description: >-
        Id de un lote de créditos (un pack, una oferta o el período de una
        membresía).
      pattern: ^cgr_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - cgr_01jdwsxvjgc7y12y0h12f43149
    CurrencyCode:
      type: string
      description: Moneda ISO 4217, en mayúsculas.
      pattern: ^[A-Z]{3}$
      examples:
        - UYU
  headers:
    RequestId:
      description: Identificador del pedido. Aparece también en el cuerpo de los errores.
      schema:
        type: string
        pattern: ^req_[0-9a-hjkmnp-tv-z]{26}$
    CraquiVersion:
      description: La versión de la API con la que se respondió.
      schema:
        type: string
        pattern: ^\d{4}-\d{2}-\d{2}$
    RateLimit:
      description: >-
        Pedidos que quedan en la ventana actual y segundos hasta que se renueva
        (draft IETF httpapi-ratelimit-headers).
      schema:
        type: string
        examples:
          - '"read";r=97;t=42'
    RateLimitPolicy:
      description: Límite de la clave por ventana (draft IETF httpapi-ratelimit-headers).
      schema:
        type: string
        examples:
          - '"read";q=100;w=60'
    IdempotentReplayed:
      description: >-
        `true` cuando la respuesta es la guardada de un pedido anterior con la
        misma `Idempotency-Key`.
      schema:
        type: string
        enum:
          - 'true'
    RetryAfter:
      description: Segundos que hay que esperar antes de reintentar.
      schema:
        type: integer
        minimum: 0
  responses:
    BadRequest:
      description: >
        El pedido está mal formado. Códigos posibles: `invalid_api_version`,
        `idempotency_key_required`,

        `validation_failed` (con un elemento de `errors` por parámetro:
        `unknown_parameter`,

        `invalid_prefix`, `invalid_id`, `invalid_date`, `invalid_boolean`,
        `invalid_enum`,

        `invalid_limit`, `duplicate_parameter`, `expand_not_supported`…) e
        `invalid_cursor` (un

        `starting_after` de otra búsqueda: repite la primera página con los
        mismos filtros). En las URLs de

        avisos, `webhook_url_invalid` (en `errors`, la causa: `not_https`,
        `port_not_allowed`,

        `credentials_in_url`, `ip_literal`, `internal_hostname`,
        `private_address`, `craqui_host` o `too_long`).

        El detalle de un error nunca repite el valor que mandaste.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Unauthorized:
      description: >
        Falta la clave o no sirve. Códigos posibles: `missing_api_key`,
        `invalid_api_key`,

        `revoked_api_key`, `expired_api_key`, `legacy_api_key`.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            revoked:
              value:
                type: https://docs.craqui.com/errors/revoked_api_key
                title: La clave fue revocada
                status: 401
                code: revoked_api_key
                detail: Crea una clave nueva en Configuración → Integraciones.
                request_id: req_01hzx8m4k2a7d9q3f5w6y8z0bc
    Forbidden:
      description: >
        La clave es válida pero no puede hacer esto. Códigos posibles:
        `scope_required`,

        `plan_required`, `api_key_suspended`, `browser_not_allowed`,
        `wrong_vertical`,

        `event_type_not_permitted` (eventos de un recurso que la clave no puede
        leer; `required_scope` dice cuál) y

        `write_access_disabled` (en una ruta que crea o cambia datos, si tu
        negocio no tiene habilitada la escritura por

        la API).
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    NotFound:
      description: >-
        No existe, o no es de tu negocio. Código `not_found` o
        `route_not_found`.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Conflict:
      description: >
        Solo con una clave `crq_test_`: tu estudio de prueba no está listo.
        Código

        `sandbox_unavailable`. Restablece tus datos de prueba en Configuración →
        Integraciones y vuelve

        a intentar. Una clave de prueba nunca responde con los datos reales del
        negocio. En los pedidos que

        crean o modifican algo, también `idempotency_in_progress` (el mismo
        pedido todavía se está procesando) y,

        al crear una URL de avisos, `webhook_endpoint_limit_reached` (ya hay
        10). En una ruta que crea o cambia datos

        del estudio, además, sus reglas no dejan hacerlo: la descripción de cada
        ruta lista sus códigos.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            sandbox_unavailable:
              value:
                type: https://docs.craqui.com/errors/sandbox_unavailable
                title: Tu estudio de prueba no está listo
                status: 409
                code: sandbox_unavailable
                detail: >-
                  Restablece tus datos de prueba en Configuración →
                  Integraciones y vuelve a intentar.
                request_id: req_01hzx8m4k2a7d9q3f5w6y8z0bc
    UnprocessableEntity:
      description: >
        El pedido se entendió pero no se puede aplicar así. Códigos posibles:
        `idempotency_key_reused` (esa

        `Idempotency-Key` ya se usó con otro pedido: genera una nueva),
        `sandbox_contact_not_allowed` y

        `sandbox_account_reserved` (en tu estudio de prueba, un contacto que no
        es ficticio).
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    TooManyRequests:
      description: >-
        Se pasó el límite de pedidos por minuto de la clave. Código
        `rate_limited`.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    InternalError:
      description: >-
        Falló algo de nuestro lado. Código `internal_error`. Reintenta; si
        sigue, escríbenos con el `request_id`.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: crq_live_sk_… / crq_live_rk_…
      description: >
        Clave del negocio. `sk` es una clave completa (todo lo que el rol de
        quien la creó puede dar)

        y `rk` una restringida a los permisos que se eligieron al crearla. Solo
        se usa desde un

        servidor: un pedido desde un navegador se rechaza con
        `browser_not_allowed`.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.