openapi: 3.1.0
info:
  title: Belcome public API
  version: 1.0.0
  description: >-
    The same API the Belcome Pro app uses, reached with a per-business API key created by the owner
    in Settings > Integrations > API keys. See docs/contratos/api-publica.md for scopes and rules.
  license:
    name: Proprietary
    url: https://www.belcome.app/terms/
servers:
  - url: https://belcome.app/api/v1
security:
  - apiKey: []
tags:
  - name: account
  - name: keys
  - name: appointments
  - name: clients
  - name: catalog
  - name: staff
  - name: payments
  - name: webhooks
paths:
  /me:
    get:
      tags: [account]
      operationId: me
      summary: The user behind the key; memberships holds only the key's business.
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Error' }
  /businesses/{business}/api-keys:
    parameters: [{ $ref: '#/components/parameters/Business' }]
    get:
      tags: [keys]
      operationId: listApiKeys
      summary: List keys (owner, app session only).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
    post:
      tags: [keys]
      operationId: createApiKey
      summary: Create a key; the plain key is returned once.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [label, scopes]
              properties:
                label: { type: string, maxLength: 60 }
                scopes:
                  type: array
                  minItems: 1
                  items: { $ref: '#/components/schemas/Scope' }
      responses:
        '201': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '422': { $ref: '#/components/responses/Error' }
  /businesses/{business}/api-keys/{key}:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: key, in: path, required: true, schema: { type: string, format: uuid } }
    delete:
      tags: [keys]
      operationId: revokeApiKey
      summary: Revoke a key.
      responses:
        '200': { $ref: '#/components/responses/Ok' }
  /businesses/{business}/appointments:
    parameters: [{ $ref: '#/components/parameters/Business' }]
    get:
      tags: [appointments]
      operationId: listAppointments
      summary: Appointments of a location in a time window (appointments:read).
      parameters:
        - { name: location_id, in: query, required: true, schema: { type: string, format: uuid } }
        - { name: from, in: query, required: true, schema: { type: string, format: date-time } }
        - { name: to, in: query, required: true, schema: { type: string, format: date-time } }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
    post:
      tags: [appointments]
      operationId: createAppointment
      summary: Book a visit (appointments:write).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [location_id, client_id, starts_at, items]
              properties:
                location_id: { type: string, format: uuid }
                client_id: { type: string, format: uuid }
                starts_at: { type: string, format: date-time }
                items:
                  type: array
                  items:
                    type: object
                    required: [service_id, staff_id]
                    properties:
                      service_id: { type: string, format: uuid }
                      staff_id: { type: string, format: uuid }
      responses:
        '201': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
        '422': { $ref: '#/components/responses/Error' }
  /businesses/{business}/availability:
    parameters: [{ $ref: '#/components/parameters/Business' }]
    get:
      tags: [appointments]
      operationId: availability
      summary: Free slots (appointments:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
  /businesses/{business}/clients:
    parameters: [{ $ref: '#/components/parameters/Business' }]
    get:
      tags: [clients]
      operationId: searchClients
      summary: Search clients (clients:read).
      parameters:
        - { name: q, in: query, schema: { type: string } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
    post:
      tags: [clients]
      operationId: createClient
      summary: Create a client (clients:write).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [first_name]
              properties:
                first_name: { type: string, maxLength: 120 }
                last_name: { type: string, maxLength: 120 }
                email: { type: string, format: email }
                phone_e164: { type: string }
                alert: { type: string, maxLength: 200, description: one line the team sees on every visit (allergy, sensitivity) }
                custom_fields: { type: object, additionalProperties: true, description: "answers to the business's own client fields, keyed by field id (GET business, client_fields); text up to 500, choice one of the options, yes_no boolean, date YYYY-MM-DD; null clears" }
                online_booking_blocked: { type: boolean, description: "true stops this client on the booking page and the waitlist (code online_booking_unavailable, the reason is never shown); the team still books them. Read back as online_booking_blocked_at" }
                patch_test_at: { type: string, format: date, nullable: true, description: "Day of the client's last allergy test (Fresha patch tests). A service with requires_patch_test books online only from 48 hours to 6 months after it (code patch_test_required)" }
      responses:
        '201': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '422': { $ref: '#/components/responses/Error' }
  /businesses/{business}/services:
    parameters: [{ $ref: '#/components/parameters/Business' }]
    get:
      tags: [catalog]
      operationId: listServices
      summary: The menu (services:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
  /businesses/{business}/locations:
    parameters: [{ $ref: '#/components/parameters/Business' }]
    get:
      tags: [catalog]
      operationId: listLocations
      summary: Locations (services:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
  /businesses/{business}/staff:
    parameters: [{ $ref: '#/components/parameters/Business' }]
    get:
      tags: [staff]
      operationId: listStaff
      summary: The team (staff:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
  /businesses/{business}/payments:
    parameters: [{ $ref: '#/components/parameters/Business' }]
    get:
      tags: [payments]
      operationId: listPayments
      summary: Payments (payments:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
  /businesses/{business}/webhooks:
    parameters: [{ $ref: '#/components/parameters/Business' }]
    get:
      tags: [webhooks]
      operationId: listWebhooks
      summary: Webhook endpoints of the business (any read scope).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
    post:
      tags: [webhooks]
      operationId: subscribeWebhook
      summary: REST hook subscription; each event needs the read scope of its resource.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                events: { type: array, items: { type: string } }
                source: { type: string, enum: [manual, zapier, make] }
      responses:
        '201': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
  /businesses/{business}:
    parameters:
      - { $ref: '#/components/parameters/Business' }
    get:
      tags: [account]
      operationId: showBusiness
      summary: The business of the key (any scope).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/appointments/{appointment}:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: appointment, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [appointments]
      operationId: showAppointment
      summary: One appointment with its items (appointments:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/appointments/recurring:
    parameters:
      - { $ref: '#/components/parameters/Business' }
    post:
      tags: [appointments]
      operationId: createRecurringAppointments
      summary: Book the same visit every N weeks (appointments:write); body of createAppointment plus every_weeks and occurrences (1 to 52).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [every_weeks, occurrences]
              properties:
                every_weeks: { type: integer, minimum: 1, maximum: 52 }
                occurrences: { type: integer, minimum: 1, maximum: 52 }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/appointments/{appointment}/schedule:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: appointment, in: path, required: true, schema: { type: string, format: uuid } }
    patch:
      tags: [appointments]
      operationId: moveAppointment
      summary: Move to a new start, optionally reassigning staff per item (appointments:write). 409 on a conflict unless force.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [starts_at]
              properties:
                starts_at: { type: string, format: date-time }
                force: { type: boolean }
                items: { type: array, items: { type: object, required: [id, staff_id], properties: { id: { type: string, format: uuid }, staff_id: { type: string, format: uuid } } } }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/appointments/{appointment}/duration:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: appointment, in: path, required: true, schema: { type: string, format: uuid } }
    patch:
      tags: [appointments]
      operationId: resizeAppointment
      summary: Change the minutes of one item (appointments:write).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [item_id, minutes]
              properties:
                item_id: { type: string, format: uuid }
                minutes: { type: integer }
                force: { type: boolean }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/appointments/{appointment}/note:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: appointment, in: path, required: true, schema: { type: string, format: uuid } }
    patch:
      tags: [appointments]
      operationId: updateAppointmentNote
      summary: Write or clear the team note of the visit; the client never sees it (appointments:write).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [internal_note]
              properties:
                internal_note: { type: [string, 'null'], maxLength: 10000, description: empty or null clears it }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
        '422': { $ref: '#/components/responses/Error' }
  /businesses/{business}/appointments/{appointment}/transitions:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: appointment, in: path, required: true, schema: { type: string, format: uuid } }
    post:
      tags: [appointments]
      operationId: transitionAppointment
      summary: Confirm, check in, complete, cancel, decline or mark a no-show (appointments:write). No fee is charged through a key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to]
              properties:
                to: { type: string, enum: [requested, booked, arrived, completed, cancelled, declined, no_show] }
                reason: { type: string, maxLength: 2000, description: only with cancelled or declined }
                notify_client: { type: boolean, default: true, description: only with cancelled }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/appointments/{appointment}/forms/link:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: appointment, in: path, required: true, schema: { type: string, format: uuid } }
    post:
      tags: [appointments]
      operationId: appointmentFormsLink
      summary: A kiosk link for the intake forms of the visit (appointments:write).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/appointments/{appointment}/promotion:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: appointment, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [appointments]
      operationId: appointmentPromotion
      summary: The promotion applied to the visit, if any (appointments:read). Applying one stays in the app.
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/blocks:
    parameters:
      - { $ref: '#/components/parameters/Business' }
    get:
      tags: [appointments]
      operationId: listBlocks
      summary: Manual blocks on the calendar (appointments:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
    post:
      tags: [appointments]
      operationId: createBlock
      summary: Block time for one staff member or resource (appointments:write).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [location_id, starts_at, ends_at]
              properties:
                location_id: { type: string, format: uuid }
                staff_id: { type: string, format: uuid, description: or resource_id }
                resource_id: { type: string, format: uuid }
                starts_at: { type: string, format: date-time }
                ends_at: { type: string, format: date-time }
                label: { type: string, maxLength: 200 }
                every_days: { type: integer, minimum: 1, maximum: 28, description: 'Repeat the block every N days (7 = weekly), at the same local time; needs occurrences' }
                occurrences: { type: integer, minimum: 2, maximum: 90, description: 'How many blocks in the repeat; the answer is {series_id, created, conflicts} and a taken day is skipped' }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/blocks/{block}:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: block, in: path, required: true, schema: { type: string, format: uuid } }
    delete:
      tags: [appointments]
      operationId: deleteBlock
      summary: Remove a manual block (appointments:write); with following=1, the later blocks of its repeat go too.
      parameters:
        - { name: following, in: query, required: false, schema: { type: boolean } }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/clients/{client}:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: client, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [clients]
      operationId: showClient
      summary: One client (clients:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
    patch:
      tags: [clients]
      operationId: updateClient
      summary: Update a client; same fields as createClient (clients:write).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
    delete:
      tags: [clients]
      operationId: deleteClient
      summary: Delete a client (clients:write).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/clients/{client}/appointments:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: client, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [clients]
      operationId: clientHistory
      summary: Visit history of the client (clients:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/clients/{client}/photos:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: client, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [clients]
      operationId: clientPhotos
      summary: Photos on the client's file, newest first, with the before/after stage and the dated image-use consent; each url is signed and opens for 10 minutes (clients:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/clients/{client}/forms:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: client, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [clients]
      operationId: clientForms
      summary: Forms the client filled in (clients:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/clients/{client}/wallet:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: client, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [clients]
      operationId: clientWallet
      summary: Packages, membership credits and gift card balances (clients:read). Selling stays in the app.
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/service-categories:
    parameters:
      - { $ref: '#/components/parameters/Business' }
    get:
      tags: [catalog]
      operationId: listServiceCategories
      summary: Menu categories (services:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/services/{service}:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: service, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [catalog]
      operationId: showService
      summary: One service (services:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/locations/{location}:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: location, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [catalog]
      operationId: showLocation
      summary: One location with hours (services:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/staff/{staff}/hours:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: staff, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [staff]
      operationId: staffHours
      summary: Working hours of a staff member (staff:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/payments/{payment}:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: payment, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [payments]
      operationId: showPayment
      summary: One payment (payments:read). Refunds stay in the app.
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/payments/account:
    parameters:
      - { $ref: '#/components/parameters/Business' }
    get:
      tags: [payments]
      operationId: paymentsAccount
      summary: Status of the business payments account (payments:read).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/webhooks/{endpoint}:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: endpoint, in: path, required: true, schema: { type: string, format: uuid } }
    delete:
      tags: [webhooks]
      operationId: unsubscribeWebhook
      summary: Remove a webhook endpoint (any read scope).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /businesses/{business}/webhooks/{endpoint}/enable:
    parameters:
      - { $ref: '#/components/parameters/Business' }
      - { name: endpoint, in: path, required: true, schema: { type: string, format: uuid } }
    post:
      tags: [webhooks]
      operationId: enableWebhook
      summary: Turn a disabled endpoint back on after repeated failures (any read scope).
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
  parameters:
    Business:
      name: business
      in: path
      required: true
      schema: { type: string, format: uuid }
  schemas:
    Scope:
      type: string
      enum: [appointments:read, appointments:write, clients:read, clients:write, services:read, staff:read, payments:read]
    Envelope:
      type: object
      required: [success]
      properties:
        success: { type: boolean }
        message: { type: string }
        data: {}
    Error:
      type: object
      required: [success, code]
      properties:
        success: { type: boolean, const: false }
        code: { type: string, examples: [forbidden_role, not_found, validation_failed, unauthenticated] }
        message: { type: string }
  responses:
    Ok:
      description: Success envelope.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
    Error:
      description: Error envelope.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
