openapi: 3.1.0

# The customer-facing part of the Vizito REST API: what an integration can reach
# with an API key issued in the Backoffice (VIZ-TASK-1764). Front office, kiosk,
# billing, user management and internal endpoints are deliberately absent.
#
# Served by the app itself at GET /api/openapi.yaml, and consumed by the
# documentation site (developers.vizito.eu) to generate its API reference.
#
# Keep this file in step with server.js: a route documented here that no longer
# exists is worse than one that was never documented.

info:
  title: Vizito API
  version: "1.0.0"
  description: |
    Read and write your Vizito visitor data over HTTPS.

    Every endpoint takes and returns JSON, and is authenticated with an API key
    created by a global admin on the Integrations page of the Backoffice.

    Almost every endpoint names a **location** — `company_id` in the API. Your
    key is pinned to a set of locations; naming one outside that set is refused
    with `403`. Start at `GET /companiesList`, the one endpoint that needs no
    location id.
  contact:
    name: Vizito support
    email: support@vizito.eu
    url: https://vizito.eu
  termsOfService: https://vizito.eu/terms

servers:
  - url: https://api.vizito.eu/api
    description: Production

security:
  - apiKey: []

tags:
  - name: Locations
    description: The locations your credential covers, and their configuration.
  - name: Visitors
    description: The visits themselves — one visitor is one visit, not a person.
  - name: Registered visitors
    description: Expected visitors — pre-register, invite, and sign in on arrival.
  - name: Hosts
    description: The people who can be visited.
  - name: Visit types
    description: The sign-in flows, and the questions they ask.
  - name: Entry points
    description: The doors and desks within a location.
  - name: Agreements
    description: The documents visitors sign, and the signed PDFs that come out.
  - name: Devices
    description: The kiosks at a location.
  - name: Reporting
    description: Counters, graphs and exports.
  - name: Webhooks
    description: The endpoint Vizito calls when something happens.

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: |
        An API key issued in the Backoffice, sent as `Authorization: Bearer vzk_<key id>_<secret>`.
        A Microsoft Entra ID access token obtained with the client credentials
        grant is accepted on the same header. The same value is also accepted in
        an `X-API-Key` header.

  parameters:
    companyId:
      name: company_id
      in: path
      required: true
      description: The location.
      schema:
        $ref: "#/components/schemas/ObjectId"
    count:
      name: count
      in: query
      required: true
      description: Rows to return.
      schema:
        type: integer
        minimum: 1
        example: 50
    page:
      name: page
      in: query
      required: true
      description: 1-based page number.
      schema:
        type: integer
        minimum: 1
        example: 1
    sorting:
      name: sorting
      in: query
      required: false
      style: deepObject
      explode: true
      description: |
        Sort per field, as `sorting[signed_in]=desc`. `asc` or `desc`.
      schema:
        type: object
        additionalProperties:
          type: string
          enum: [asc, desc]
    filter:
      name: filter
      in: query
      required: false
      style: deepObject
      explode: true
      description: |
        Filter per field, as `filter[company]=acme`. Values match case-insensitively
        on any part of the stored value. A date field takes a range instead:
        `filter[signed_in][startDate]` and `filter[signed_in][endDate]`, both ISO 8601.
      schema:
        type: object
        additionalProperties: true
    languageId:
      name: language_id
      in: query
      required: false
      description: Language for configured text, e.g. `nl`. Falls back to English.
      schema:
        type: string
        example: en

  responses:
    Unauthorized:
      description: |
        The credential was refused, or the endpoint is off limits to API keys.
        Every refusal answers identically — see the Authentication page.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: Invalid API key
    NotFound:
      description: No such object, or a location your credential does not cover.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Message"
    BadRequest:
      description: The request was understood but could not be carried out.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Message"
    RateLimited:
      description: Too many refused credentials from this IP address.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: Too many invalid API key attempts from this IP, please try again later

  schemas:
    ObjectId:
      type: string
      pattern: "^[0-9a-fA-F]{24}$"
      description: A 24-character hexadecimal id.
      example: 5f2a1b9c4d3e2f0011223344

    Message:
      type: object
      properties:
        message:
          type: string

    BulkResult:
      type: array
      description: One entry per id sent, in the same order.
      items:
        type: object
        properties:
          id:
            $ref: "#/components/schemas/ObjectId"
          status:
            type: string
            enum: [success, skipped, error]
          reason:
            type: string
            description: Why it was skipped, or what went wrong.
          message:
            type: string

    Location:
      type: object
      properties:
        _id:
          $ref: "#/components/schemas/ObjectId"
        name:
          type: string
          example: Head office
        location_code:
          type: string
          description: Your own short code for the location.
          example: HQ
        tier:
          type: string
          enum: [Starter, Standard, Pro, Enterprise]
        mod_date:
          type: string
          format: date-time

    LocationDetail:
      allOf:
        - $ref: "#/components/schemas/Location"
        - type: object
          properties:
            address:
              type: string
            country:
              type: string
            timezone:
              type: string
              description: IANA timezone. Dates in reporting paths are read in it.
              example: Europe/Brussels
            languages:
              type: array
              items:
                type: string
            app_settings:
              type: object
              description: Kiosk and flow settings.
              additionalProperties: true

    Visitor:
      type: object
      description: |
        One visit. Every custom field the visit type defines is also present,
        under its own `field_name`.
      additionalProperties: true
      properties:
        _id:
          $ref: "#/components/schemas/ObjectId"
        company_id:
          $ref: "#/components/schemas/ObjectId"
        first_name:
          type: string
          example: Ada
        last_name:
          type: string
          example: Lovelace
        company:
          type: string
          description: The organisation the visitor comes from.
        email:
          type: string
          format: email
        phone:
          type: string
          description: International format.
        recipient:
          type: string
          description: Name of the host being visited.
        recipient_mail:
          type: string
          format: email
        visit_type:
          $ref: "#/components/schemas/ObjectId"
        entrypoint_id:
          $ref: "#/components/schemas/ObjectId"
        known_visitor_id:
          $ref: "#/components/schemas/ObjectId"
        signed_in:
          type: string
          format: date-time
        signed_out:
          type: string
          format: date-time
          description: Absent while the visitor is still inside.
        signed_in_source:
          type: integer
          enum: [0, 1, 2]
          description: "`0` Backoffice or API, `1` kiosk, `2` contactless."
        signed_out_source:
          type: integer
          enum: [0, 1, 2]
        approval:
          type: string
          enum: [pending, approved, rejected]
          description: Present on visit types that screen visitors.
        safe:
          type: boolean
          description: Marked safe during an evacuation.
        agreements:
          type: array
          description: Ids of the agreements signed during this visit.
          items:
            $ref: "#/components/schemas/ObjectId"
        photo:
          type: string
          description: Base64-encoded image. Only on a single-visitor fetch.
        signature:
          type: string
          description: Base64-encoded image. Only on a single-visitor fetch.
        mod_date:
          type: string
          format: date-time

    VisitingOn:
      type: object
      description: When a visit is expected. Absent means "any day".
      properties:
        startDate:
          type: string
          format: date-time
        endDate:
          type: string
          format: date-time

    RegisteredVisitor:
      type: object
      description: |
        An expected visitor. Custom fields defined by the visit type are stored
        here too, under their own `field_name`, and carried onto the visit.
      additionalProperties: true
      properties:
        _id:
          $ref: "#/components/schemas/ObjectId"
        company_id:
          type: array
          items:
            $ref: "#/components/schemas/ObjectId"
        first_name:
          type: string
        last_name:
          type: string
        company:
          type: string
        email:
          type: string
          format: email
        phone:
          type: string
        recipient:
          type: string
        visit_type:
          $ref: "#/components/schemas/ObjectId"
        visiting_on:
          $ref: "#/components/schemas/VisitingOn"
        persistent:
          type: boolean
          description: Kept after the visit; never removed by data retention.
        language:
          type: string
          description: Language of the invitation email.
        pre_registration_by_visitor:
          type: boolean
          description: The visitor filled in their own details through the invitation.
        create_date:
          type: string
          format: date-time
        mod_date:
          type: string
          format: date-time

    Host:
      type: object
      properties:
        _id:
          $ref: "#/components/schemas/ObjectId"
        company_id:
          $ref: "#/components/schemas/ObjectId"
        cn:
          type: string
          description: Full name. A visit's `recipient` matches a host on it.
          example: Jane Doe
        mail:
          type: string
          format: email
          description: Stored lower-cased.
        mobile:
          type: string
          description: Used for SMS notifications.
        telephoneNumber:
          type: string
          description: Desk phone, shown to reception.
        sticky:
          type: boolean
          description: Pinned to the top of the kiosk list.
        external:
          type: boolean
          description: Pushed through the bulk host upload.
        create_date:
          type: string
          format: date-time
        mod_date:
          type: string
          format: date-time

    VisitType:
      type: object
      additionalProperties: true
      properties:
        _id:
          $ref: "#/components/schemas/ObjectId"
        name:
          type: object
          properties:
            description:
              type: string
              example: Visitor
        index:
          type: integer
          description: Position on the kiosk.
        show_on_tablet:
          type: boolean
        deleted:
          type: boolean
          description: Retired visit types stay listed so old visits keep their name.
        printing:
          type: object
          additionalProperties: true

    Field:
      type: object
      properties:
        field_name:
          type: string
          description: The key you send and read.
          example: badgenumber
        field_description:
          type: string
          description: The label as the visitor sees it.
        column_title:
          type: string
          description: The label as the Backoffice column is headed.
        field_content_type:
          type: string
          enum: [text, number, list, check, yes_no, datetime]
        field_status:
          type: string
          enum: [required, optional, hidden, disabled]
          description: Enforced in the front office, not by this API.
        visit_type_id:
          $ref: "#/components/schemas/ObjectId"
        list_values:
          type: array
          items:
            type: object
            properties:
              value_name:
                type: string
                description: What you send.
              value_description:
                type: string
                description: What the visitor reads.
              is_default:
                type: boolean
        field_validation:
          type: object
          properties:
            regex:
              type: string
            message:
              type: string

    EntryPoint:
      type: object
      properties:
        _id:
          $ref: "#/components/schemas/ObjectId"
        name:
          type: string
          example: Main entrance
        company_id:
          $ref: "#/components/schemas/ObjectId"
        deleted:
          type: boolean
        create_date:
          type: string
          format: date-time
        mod_date:
          type: string
          format: date-time

    Agreement:
      type: object
      additionalProperties: true
      properties:
        _id:
          $ref: "#/components/schemas/ObjectId"
        name:
          type: string
        active:
          type: boolean
        type:
          type: string
          enum: [text, pdf, video]
        agreement_text:
          type: object
          description: The text, keyed by language code.
          additionalProperties:
            type: string
        visit_types:
          type: array
          items:
            $ref: "#/components/schemas/ObjectId"
        version:
          type: integer
          description: Bumped when the content changes.
        index:
          type: integer
        expiry_days:
          type: integer
          description: How long a signature stays valid.
        group_only:
          type: boolean

    Device:
      type: object
      additionalProperties: true
      properties:
        _id:
          $ref: "#/components/schemas/ObjectId"
        name:
          type: string
        device_uuid:
          type: string
          description: Stable identifier; what webhook device events name.
        connected:
          type: boolean
        last_online:
          type: string
          format: date-time
        entrypoint_id:
          $ref: "#/components/schemas/ObjectId"
        model:
          type: string
        platform:
          type: string
        platform_version:
          type: string
        app_version:
          type: string
        ip_address:
          type: string
        printer:
          type: string

    Webhook:
      type: object
      properties:
        _id:
          $ref: "#/components/schemas/ObjectId"
        company_id:
          $ref: "#/components/schemas/ObjectId"
        url:
          type: string
          format: uri
        http_method:
          type: string
          enum: [POST, PUT, PATCH, GET]
        enabled:
          type: boolean
        auth_username:
          type: string
        auth_password:
          type: string

paths:
  /companiesList:
    get:
      operationId: listLocations
      summary: Listing locations
      tags: [Locations]
      description: |
        Every location your credential covers. The one endpoint that needs no
        location id — start here, and keep the ids it hands back.
      responses:
        "200":
          description: The locations.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Location"
        "403":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /companies/{company_id}:
    get:
      operationId: getLocation
      summary: Fetching a location
      tags: [Locations]
      description: |
        The full configuration of one location: address, timezone, languages and
        the `app_settings` block that drives the kiosk.

        For backwards compatibility `app_settings` also carries a copy of the
        location's first visit type. Read the settings you need rather than
        diffing the whole object.
      parameters:
        - $ref: "#/components/parameters/companyId"
      responses:
        "200":
          description: The location.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LocationDetail"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /visitors/bycompany/{company_id}:
    get:
      operationId: listVisitors
      summary: Listing visitors
      tags: [Visitors]
      description: |
        The visitor log — the same data the Backoffice grid shows. Photos and
        signatures are left out; fetch a single visitor for those.

        `count` and `page` are required: without them the endpoint answers
        `400 Please include count, page and sorting query parameters.`
      parameters:
        - $ref: "#/components/parameters/companyId"
        - $ref: "#/components/parameters/count"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/sorting"
        - $ref: "#/components/parameters/filter"
      responses:
        "200":
          description: One page of visits.
          content:
            application/json:
              schema:
                type: object
                properties:
                  total:
                    type: integer
                    description: Visits matching the filter, ignoring paging.
                  visitors:
                    type: array
                    items:
                      $ref: "#/components/schemas/Visitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /visitors:
    post:
      operationId: signInVisitor
      summary: Signing in a visitor
      tags: [Visitors]
      description: |
        Creates a visit, running the complete sign-in flow: the host is notified,
        the badge is printed if the visit type prints one, the confirmation email
        goes out, and every integration and webhook fires.

        Required fields and validation rules are enforced in the front office,
        not here — a visit created through the API with a required field missing
        is accepted as-is. There is no idempotency key yet: a retried request
        creates a second visit.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              required: [company_id]
              properties:
                company_id:
                  $ref: "#/components/schemas/ObjectId"
                visit_type:
                  allOf:
                    - $ref: "#/components/schemas/ObjectId"
                  description: Falls back to the location's default visit type.
                first_name:
                  type: string
                last_name:
                  type: string
                company:
                  type: string
                email:
                  type: string
                  format: email
                phone:
                  type: string
                  description: |
                    Prefixed with the visit type's default country code when it
                    does not start with `+`.
                recipient:
                  type: string
                  description: |
                    Name of the host. Match an existing host's `cn` and their
                    contact details are filled in automatically.
                recipient_mail:
                  type: string
                  format: email
                known_visitor_id:
                  allOf:
                    - $ref: "#/components/schemas/ObjectId"
                  description: |
                    Links the visit to a registered visitor. Hidden fields stored
                    on that record are carried over.
                entrypoint_id:
                  $ref: "#/components/schemas/ObjectId"
                signed_in_source:
                  type: string
                  enum: ["0", "1", "2"]
                  default: "1"
                  description: "`0` Backoffice or API, `1` kiosk, `2` contactless."
                signed_in_frombo:
                  type: string
                  format: date-time
                  description: Backdates the sign-in. Defaults to now.
                fo_language:
                  type: string
                  description: Language for the visitor's emails.
            examples:
              minimal:
                summary: A visitor at the default visit type
                value:
                  company_id: 5f2a1b9c4d3e2f0011223344
                  first_name: Ada
                  last_name: Lovelace
                  company: Analytical Engines Ltd
                  email: ada@example.com
                  recipient: Jane Doe
                  recipient_mail: jane.doe@example.com
                  signed_in_source: "0"
      responses:
        "200":
          description: The visit that was created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Visitor added!
                  data:
                    $ref: "#/components/schemas/Visitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /visitors/{visitor_id}:
    parameters:
      - name: visitor_id
        in: path
        required: true
        description: The visit id.
        schema:
          $ref: "#/components/schemas/ObjectId"
    get:
      operationId: getVisitor
      summary: Fetching a visitor
      tags: [Visitors]
      description: |
        One visit in full, including the `photo` and `signature` that the list
        endpoints leave out. Both are base64-encoded images.
      responses:
        "200":
          description: The visit.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Visitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"
    put:
      operationId: signOutVisitor
      summary: Signing out a visitor
      tags: [Visitors]
      description: |
        Closes a visit. The sign-out email and any sign-out notification go out
        exactly as they would from a kiosk.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [company_id]
              properties:
                company_id:
                  $ref: "#/components/schemas/ObjectId"
                signed_out:
                  type: string
                  format: date-time
                  description: Defaults to now.
                signed_out_source:
                  type: string
                  enum: ["0", "1", "2"]
                  default: "1"
      responses:
        "200":
          description: The visit was closed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /visitors/{visitor_id}/details:
    put:
      operationId: updateVisit
      summary: Updating a visit
      tags: [Visitors]
      description: |
        Changes an existing visit — a corrected name, a filled-in badge number, a
        screening decision. Despite the `PUT` this is a partial update: fields
        you leave out keep their value.

        Sending `signed_out` also closes the visit, unless it was already signed
        out, in which case the original time stands.
      parameters:
        - name: visitor_id
          in: path
          required: true
          description: The visit id.
          schema:
            $ref: "#/components/schemas/ObjectId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              required: [company_id]
              properties:
                company_id:
                  $ref: "#/components/schemas/ObjectId"
                visit_type:
                  $ref: "#/components/schemas/ObjectId"
                approval:
                  type: string
                  enum: [approved, rejected]
                signed_out:
                  type: string
                  format: date-time
      responses:
        "200":
          description: The visit was updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /visitors/{visitor_id}/anonymize:
    put:
      operationId: anonymiseVisit
      summary: Anonymising a visit
      tags: [Visitors]
      description: |
        Strips every personal detail from a visit, irreversibly. What remains is
        the shape of the visit — id, visit type, entry point, host, sign-in and
        sign-out times, and `erased: true`. The photo and signature are deleted
        outright and the first name becomes `Anonimized`.

        There is no undo. Routine cleanup does not need this endpoint: data
        retention deletes old visits on its own schedule.
      parameters:
        - name: visitor_id
          in: path
          required: true
          description: The visit id.
          schema:
            $ref: "#/components/schemas/ObjectId"
      responses:
        "200":
          description: The visit was anonymised.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /visitors/signed_current/{company_id}:
    get:
      operationId: listVisitorsInside
      summary: Listing visitors inside
      tags: [Visitors]
      description: |
        Everyone signed in and not yet signed out, honouring the location's
        multi-day sign-out interval. This is the list an evacuation report should
        be built on.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: datetimestamp
          in: query
          required: false
          description: The day to evaluate. Defaults to today.
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: The visitors currently inside.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Visitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /visitors/signed_current/paged/{company_id}:
    get:
      operationId: listVisitorsInsidePaged
      summary: Listing visitors inside, paged
      tags: [Visitors]
      description: The same list as `/visitors/signed_current/{company_id}`, paged and filterable.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - $ref: "#/components/parameters/count"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/filter"
      responses:
        "200":
          description: One page of visitors currently inside.
          content:
            application/json:
              schema:
                type: object
                properties:
                  total:
                    type: integer
                  visitors:
                    type: array
                    items:
                      $ref: "#/components/schemas/Visitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /visitors/recent/{company_id}:
    get:
      operationId: listRecentVisits
      summary: Listing recent visits
      tags: [Visitors]
      description: |
        The 30 most recently modified visits, newest first. A webhook tells you
        the same thing as it happens, and without the 30-row ceiling.
      parameters:
        - $ref: "#/components/parameters/companyId"
      responses:
        "200":
          description: The most recently changed visits.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Visitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /companies/{company_id}/visitors/signout:
    post:
      operationId: signOutSeveralVisitors
      summary: Signing out several visitors
      tags: [Visitors]
      description: |
        Closes several visits at once. Answers per id, so a visit that was
        already closed is reported as `skipped` rather than failing the batch —
        which makes the call safe to repeat.
      parameters:
        - $ref: "#/components/parameters/companyId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids:
                  type: array
                  items:
                    $ref: "#/components/schemas/ObjectId"
                signed_out:
                  type: string
                  format: date-time
                  description: Applied to all of them. Defaults to now.
      responses:
        "200":
          description: One result per id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BulkResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/visitors/{visitor_id}/reprint:
    post:
      operationId: reprintVisitorBadge
      summary: Reprinting a badge
      tags: [Visitors]
      description: |
        Sends the visit's badge to a kiosk printer. Vizito picks a kiosk serving
        the visit's entry point, preferring one that is online and has a printer
        configured; when none matches, any connected kiosk at the location is
        used. A `200` means the job reached the kiosk, not that paper came out.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: visitor_id
          in: path
          required: true
          description: The visit whose badge to print.
          schema:
            $ref: "#/components/schemas/ObjectId"
      responses:
        "200":
          description: Sent to a kiosk.
        "400":
          description: |
            `Visitor not found`, `Company not found or access denied`, or
            `No suitable device found` when the location has no kiosk that can print.
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/visitors/{visitor_id}/safe:
    put:
      operationId: markVisitorSafe
      summary: Marking a visitor safe
      tags: [Visitors]
      description: Sets the `safe` flag the emergency list reads.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: visitor_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/ObjectId"
      responses:
        "200":
          description: How many visits were updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/visitors/{visitor_id}/notsafe:
    put:
      operationId: markVisitorNotSafe
      summary: Marking a visitor not safe
      tags: [Visitors]
      description: Clears the `safe` flag the emergency list reads.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: visitor_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/ObjectId"
      responses:
        "200":
          description: How many visits were updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /knownvisitors:
    post:
      operationId: createRegisteredVisitor
      summary: Creating a registered visitor
      tags: [Registered visitors]
      description: |
        Creates a registered visitor: somebody you expect. When they arrive and
        sign in, a visit is created from this record and the record itself stays
        as it is.

        Creating the record does not mail anybody — send the invitation as a
        separate step.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              required: [company_id]
              properties:
                company_id:
                  $ref: "#/components/schemas/ObjectId"
                visit_type:
                  $ref: "#/components/schemas/ObjectId"
                first_name:
                  type: string
                last_name:
                  type: string
                company:
                  type: string
                email:
                  type: string
                  format: email
                  description: Required if you want to invite them.
                phone:
                  type: string
                recipient:
                  type: string
                visiting_on:
                  $ref: "#/components/schemas/VisitingOn"
                persistent:
                  type: boolean
                  default: false
                language:
                  type: string
            examples:
              oneDay:
                summary: Expected for one day
                value:
                  company_id: 5f2a1b9c4d3e2f0011223344
                  visit_type: 5f2a1b9c4d3e2f0011223399
                  first_name: Ada
                  last_name: Lovelace
                  email: ada@example.com
                  recipient: Jane Doe
                  visiting_on:
                    startDate: "2026-09-03T08:00:00.000Z"
                    endDate: "2026-09-03T17:00:00.000Z"
      responses:
        "200":
          description: The record that was created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Known Visitor added!
                  data:
                    $ref: "#/components/schemas/RegisteredVisitor"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /knownvisitors/bycompany/{company_id}:
    get:
      operationId: listRegisteredVisitors
      summary: Listing registered visitors
      tags: [Registered visitors]
      description: |
        Every registered visitor at a location. Unlike the visitor log this
        endpoint also requires `sorting`; without it you get
        `400 Please include count, page and sorting query parameters.`
      parameters:
        - $ref: "#/components/parameters/companyId"
        - $ref: "#/components/parameters/count"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/sorting"
        - $ref: "#/components/parameters/filter"
      responses:
        "200":
          description: One page of registered visitors.
          content:
            application/json:
              schema:
                type: object
                properties:
                  total:
                    type: integer
                  knownVisitors:
                    type: array
                    items:
                      $ref: "#/components/schemas/RegisteredVisitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /knownvisitors/visitingon/{company_id}/{visiting_on}:
    get:
      operationId: listExpectedVisitors
      summary: Listing expected visitors
      tags: [Registered visitors]
      description: |
        Everyone expected at a location on one day — the list a kiosk offers when
        a visitor says they are expected. Records without a `visiting_on`, the
        persistent ones, are always included.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: visiting_on
          in: path
          required: true
          description: The day, as `YYYY-MM-DD` or a full ISO 8601 timestamp.
          schema:
            type: string
            example: "2026-09-03"
        - name: visit_type
          in: query
          required: false
          description: |
            Only those expected for one visit type. Also applies that visit
            type's sign-in buffer and its one-sign-in-per-visitor rule.
          schema:
            $ref: "#/components/schemas/ObjectId"
      responses:
        "200":
          description: The expected visitors.
          content:
            application/json:
              schema:
                type: array
                items:
                  allOf:
                    - $ref: "#/components/schemas/RegisteredVisitor"
                    - type: object
                      properties:
                        alreadySignedIn:
                          type: boolean
                          description: Present when `visit_type` was given and they are inside.
        "403":
          $ref: "#/components/responses/Unauthorized"

  /knownvisitors/{knownvisitor_id}:
    parameters:
      - name: knownvisitor_id
        in: path
        required: true
        description: The registered visitor id.
        schema:
          $ref: "#/components/schemas/ObjectId"
    get:
      operationId: getRegisteredVisitor
      summary: Fetching a registered visitor
      tags: [Registered visitors]
      description: |
        One registered visitor in full, including any photo and signature stored
        for prefilling their sign-in.

        A record that does not exist answers `200` with
        `{ "message": "No Known Visitor found!" }`, not a `404`.
      responses:
        "200":
          description: The registered visitor, or a message when there is none.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/RegisteredVisitor"
                  - $ref: "#/components/schemas/Message"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"
    put:
      operationId: updateRegisteredVisitor
      summary: Updating a registered visitor
      tags: [Registered visitors]
      description: |
        Only the fields you send are touched. Sending a field as an empty string
        removes it. `_id`, `create_date`, `mod_date` and
        `pre_registration_by_visitor` are ignored.

        Moving the date does not re-send the invitation.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              required: [company_id]
              properties:
                company_id:
                  $ref: "#/components/schemas/ObjectId"
                visiting_on:
                  $ref: "#/components/schemas/VisitingOn"
      responses:
        "200":
          description: The record was updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
    delete:
      operationId: deleteRegisteredVisitor
      summary: Deleting a registered visitor
      tags: [Registered visitors]
      description: |
        Removes a registered visitor — a cancelled meeting, a contractor who is
        no longer coming. Visits already created from the record are untouched.

        Routine cleanup is automatic: records that are not `persistent` are
        deleted once their visit date has passed by the retention period.
      responses:
        "200":
          description: The record was removed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /knownvisitors/sendinvite/{language_id}:
    post:
      operationId: sendInvitation
      summary: Sending an invitation
      tags: [Registered visitors]
      description: |
        Sends the invitation email: the QR code that signs them in on arrival,
        the calendar item, the wallet pass and the pre-registration link —
        whichever the visit type is configured to include.

        Send the object you got back when you created the record. `_id` is
        required: the QR code and the pre-registration link are derived from it,
        so an invitation for an id that does not exist produces a mail whose QR
        code signs nobody in.

        Locations on a trial are capped at 10 invitations.
      parameters:
        - name: language_id
          in: path
          required: true
          description: "Language of the email: `en`, `nl`, `fr`, `de`, … Falls back to `en`."
          schema:
            type: string
            example: en
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              required: [_id, company_id, email]
              properties:
                _id:
                  $ref: "#/components/schemas/ObjectId"
                company_id:
                  $ref: "#/components/schemas/ObjectId"
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: The invitation was queued. Empty body.
        "400":
          description: Too many invitations sent on a trial location.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/knownvisitors/sendinvites:
    post:
      operationId: sendInvitationsInBulk
      summary: Sending invitations in bulk
      tags: [Registered visitors]
      description: |
        Sends the invitation to a list of registered visitors at once. Records
        without a usable email address are skipped rather than failing the call,
        and bulk invitations are rate-limited per location — check `queued`
        against `requested` rather than assuming a `200` means everybody was
        mailed.
      parameters:
        - $ref: "#/components/parameters/companyId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: "#/components/schemas/ObjectId"
      responses:
        "200":
          description: What was sent and what was skipped.
          content:
            application/json:
              schema:
                type: object
                properties:
                  requested:
                    type: integer
                  queued:
                    type: integer
                  skippedNoEmail:
                    type: integer
                  skippedRateLimited:
                    type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/knownvisitors/signin:
    post:
      operationId: signInRegisteredVisitors
      summary: Signing in registered visitors
      tags: [Registered visitors]
      description: |
        Creates a visit from each registered visitor, exactly as scanning their
        QR code at the kiosk would. Somebody already inside is skipped, not
        signed in twice, so the call is safe to repeat.
      parameters:
        - $ref: "#/components/parameters/companyId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: "#/components/schemas/ObjectId"
      responses:
        "200":
          description: One result per id, carrying the new visit id on success.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      $ref: "#/components/schemas/ObjectId"
                    status:
                      type: string
                      enum: [success, skipped, error]
                    visitorId:
                      $ref: "#/components/schemas/ObjectId"
                    reason:
                      type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/knownvisitors/delete:
    post:
      operationId: deleteRegisteredVisitorsInBulk
      summary: Deleting registered visitors in bulk
      tags: [Registered visitors]
      description: |
        Removes several registered visitors at once. A `POST` because the ids
        travel in the body. A record whose visitor is currently signed in is
        skipped.
      parameters:
        - $ref: "#/components/parameters/companyId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: "#/components/schemas/ObjectId"
      responses:
        "200":
          description: One result per id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BulkResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/knownvisitors/{known_visitor_id}/visits/current:
    get:
      operationId: getCurrentVisitOfRegisteredVisitor
      summary: Fetching the current visit
      tags: [Registered visitors]
      description: |
        The open visit created from a registered visitor, or `null` when they are
        not inside. Useful for an access control system deciding whether to open
        a door.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: known_visitor_id
          in: path
          required: true
          description: |
            The registered visitor id — or, when the location encodes another
            field in its QR codes, that field's value.
          schema:
            type: string
        - name: datetimestamp
          in: query
          required: false
          description: The day to evaluate. Defaults to today.
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: The open visit, or null.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/Visitor"
                  - type: "null"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/knownvisitors/{known_visitor_id}/reprint:
    post:
      operationId: reprintRegisteredVisitorBadge
      summary: Reprinting a badge for a registered visitor
      tags: [Registered visitors]
      description: |
        Prints the badge for a registered visitor without waiting for them to
        sign in — for reception preparing badges for a group in the morning.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: known_visitor_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/ObjectId"
      responses:
        "200":
          description: Sent to a kiosk.
        "400":
          description: No suitable device found, or no such record.
        "403":
          $ref: "#/components/responses/Unauthorized"

  /visitees/bycompany/{company_id}:
    get:
      operationId: listHosts
      summary: Listing hosts
      tags: [Hosts]
      description: |
        Every managed host at a location, sorted by name.

        Hosts pushed through the bulk upload carry `external: true` and are
        deliberately not in this list. There is no endpoint that reads the
        external list back — keep your own copy of what you pushed.
      parameters:
        - $ref: "#/components/parameters/companyId"
      responses:
        "200":
          description: The hosts.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Host"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /visitees/bycompany/paged/{company_id}:
    get:
      operationId: listHostsPaged
      summary: Listing hosts, paged
      tags: [Hosts]
      description: The same list, paged and filterable.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - $ref: "#/components/parameters/count"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/sorting"
        - $ref: "#/components/parameters/filter"
      responses:
        "200":
          description: One page of hosts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  total:
                    type: integer
                  visitees:
                    type: array
                    items:
                      $ref: "#/components/schemas/Host"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /visitees:
    post:
      operationId: createHost
      summary: Creating a host
      tags: [Hosts]
      description: |
        Adds a managed host. There is no duplicate check on `cn` — if your
        directory is the source of truth, replace the whole list instead.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [company_id, cn]
              properties:
                company_id:
                  $ref: "#/components/schemas/ObjectId"
                cn:
                  type: string
                  description: |
                    Full name. Keep it identical to the `recipient` your other
                    integrations send — that string links a visit to this host.
                mail:
                  type: string
                  format: email
                mobile:
                  type: string
                telephoneNumber:
                  type: string
                sticky:
                  type: boolean
      responses:
        "200":
          description: The host that was created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Visitee added!
                  data:
                    $ref: "#/components/schemas/Host"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /visitees/{visitee_id}:
    parameters:
      - name: visitee_id
        in: path
        required: true
        description: The host id.
        schema:
          $ref: "#/components/schemas/ObjectId"
    get:
      operationId: getHost
      summary: Fetching a host
      tags: [Hosts]
      description: |
        One managed host. A host that does not exist answers `200` with
        `{ "message": "no visitee found" }`, not a `404`.
      responses:
        "200":
          description: The host, or a message when there is none.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/Host"
                  - $ref: "#/components/schemas/Message"
        "403":
          $ref: "#/components/responses/Unauthorized"
    put:
      operationId: updateHost
      summary: Updating a host
      tags: [Hosts]
      description: |
        Only the fields you send are touched; sending one as an empty string
        clears it. Renaming `cn` does not rewrite past visits: their `recipient`
        keeps the old spelling.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [company_id]
              properties:
                company_id:
                  $ref: "#/components/schemas/ObjectId"
                cn:
                  type: string
                mail:
                  type: string
                  format: email
                mobile:
                  type: string
                telephoneNumber:
                  type: string
                sticky:
                  type: boolean
      responses:
        "200":
          description: The host was updated. Empty body.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Unauthorized"
    delete:
      operationId: deleteHost
      summary: Deleting a host
      tags: [Hosts]
      description: |
        Removes a managed host. Visits that named them keep their `recipient`, so
        history is unaffected.
      responses:
        "200":
          description: The host was removed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /visitees/external/upload/{company_id}:
    post:
      operationId: replaceHostList
      summary: Replacing the host list
      tags: [Hosts]
      description: |
        Replaces the location's **external** host list with what you send. The
        previous external set is deleted first, so this is a full sync rather
        than a merge. An empty array clears the list.

        **One upload per location per 24 hours.** A second call inside that
        window answers `200` with `hosts_imported: 0` and changes nothing — it
        does not report an error. Schedule it as a nightly job.
      parameters:
        - $ref: "#/components/parameters/companyId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [data]
              properties:
                data:
                  type: array
                  items:
                    type: object
                    required: [cn]
                    properties:
                      cn:
                        type: string
                      mail:
                        type: string
                        format: email
                      mobile:
                        type: string
                      telephoneNumber:
                        type: string
      responses:
        "200":
          description: How many hosts were written.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: visitees.SUCCESS
                  hosts_imported:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/visittypes:
    get:
      operationId: listVisitTypes
      summary: Listing visit types
      tags: [Visit types]
      description: |
        The sign-in flows a location offers — ids and names, in the order the
        kiosk shows them. Visit type ids never change: resolve them once at setup
        and store them rather than matching on the name.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: active
          in: query
          required: false
          description: "`true` returns only visit types shown on the kiosk."
          schema:
            type: boolean
        - $ref: "#/components/parameters/languageId"
      responses:
        "200":
          description: The visit types.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/VisitType"
        "403":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /companies/{company_id}/visittypes/{visit_type_id}:
    get:
      operationId: getVisitType
      summary: Fetching a visit type
      tags: [Visit types]
      description: |
        The complete configuration of one visit type, including its `fields`
        array, its notification settings and its email templates.

        Visit types can be changed over the API, but their configuration is
        broad, deeply nested and translated into every language the location
        offers. Read them here; edit them in the Backoffice.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: visit_type_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/ObjectId"
        - $ref: "#/components/parameters/languageId"
      responses:
        "200":
          description: The visit type.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VisitType"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/fields:
    get:
      operationId: listFields
      summary: Listing fields
      tags: [Visit types]
      description: |
        Every question asked at a location, merged across its visit types. This
        is what tells you which keys a visitor or registered visitor accepts.

        A custom field's value is stored under its `field_name`, so two visit
        types defining the same name share one key on the visit.
        `split_by_visit_type=true` gives each its own column instead.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: visit_types
          in: query
          required: false
          description: Restrict to one or more visit type ids.
          schema:
            type: array
            items:
              $ref: "#/components/schemas/ObjectId"
        - $ref: "#/components/parameters/languageId"
        - name: split_by_visit_type
          in: query
          required: false
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: The fields.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Field"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/entrypoints:
    get:
      operationId: listEntryPoints
      summary: Listing entry points
      tags: [Entry points]
      description: |
        The entrances within a location. Deleted entry points are included —
        check `deleted` if you only want the live ones.
      parameters:
        - $ref: "#/components/parameters/companyId"
      responses:
        "200":
          description: The entry points.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/EntryPoint"
        "403":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createEntryPoint
      summary: Creating an entry point
      tags: [Entry points]
      description: Assigning a kiosk to an entry point is done on the device, in the Backoffice.
      parameters:
        - $ref: "#/components/parameters/companyId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  example: Gate 3
      responses:
        "200":
          description: The entry point that was created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: entry point added!
                  data:
                    $ref: "#/components/schemas/EntryPoint"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/entrypoints/{entrypoint_id}:
    parameters:
      - $ref: "#/components/parameters/companyId"
      - name: entrypoint_id
        in: path
        required: true
        schema:
          $ref: "#/components/schemas/ObjectId"
    get:
      operationId: getEntryPoint
      summary: Fetching an entry point
      tags: [Entry points]
      description: One entry point. Deleted ones are not returned — the body is `null`.
      responses:
        "200":
          description: The entry point, or null.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/EntryPoint"
                  - type: "null"
        "403":
          $ref: "#/components/responses/Unauthorized"
    put:
      operationId: updateEntryPoint
      summary: Updating an entry point
      tags: [Entry points]
      description: |
        Visits point at the entry point by id, so a rename applies to history as
        well as to new visits.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        "200":
          description: The entry point was renamed. Empty body.
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"
    delete:
      operationId: deleteEntryPoint
      summary: Deleting an entry point
      tags: [Entry points]
      description: |
        Marks the entry point deleted. Visits that came through it keep their
        `entrypoint_id`, so reporting per entrance stays intact.
      responses:
        "200":
          description: The entry point was retired. Empty body.
        "403":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /companies/{company_id}/agreements:
    get:
      operationId: listAgreements
      summary: Listing agreements
      tags: [Agreements]
      description: |
        The documents visitors sign, in presentation order. Deleted and archived
        agreements are left out.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: active
          in: query
          required: false
          description: "`true` returns only the agreements currently presented."
          schema:
            type: boolean
        - $ref: "#/components/parameters/languageId"
      responses:
        "200":
          description: The agreements.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Agreement"
        "403":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /companies/{company_id}/visittypes/{visit_type_id}/agreements:
    get:
      operationId: listAgreementsOfVisitType
      summary: Listing agreements of a visit type
      tags: [Agreements]
      description: The active agreements a visitor of that visit type has to sign.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: visit_type_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/ObjectId"
        - name: group_only
          in: query
          required: false
          description: "`true` also returns agreements shown only to the person signing in a group."
          schema:
            type: boolean
        - $ref: "#/components/parameters/languageId"
      responses:
        "200":
          description: The agreements.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Agreement"
        "403":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /companies/{company_id}/agreements/{agreement_id}:
    get:
      operationId: getAgreement
      summary: Fetching an agreement
      tags: [Agreements]
      description: One agreement in full, including its text in every configured language.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: agreement_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/ObjectId"
        - $ref: "#/components/parameters/languageId"
      responses:
        "200":
          description: The agreement.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agreement"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/agreements/{agreement_id}/pdf:
    get:
      operationId: downloadBlankAgreementPdf
      summary: Downloading a blank PDF
      tags: [Agreements]
      description: The agreement as a PDF with no visitor data in it — the version to send in advance.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: agreement_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/ObjectId"
        - $ref: "#/components/parameters/languageId"
      responses:
        "200":
          description: The document.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "403":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /companies/{company_id}/agreements/{agreement_id}/signedpdf:
    get:
      operationId: downloadSignedAgreementPdf
      summary: Downloading a signed PDF
      tags: [Agreements]
      description: |
        The same document rendered with one visit's details and the visitor's
        signature in it — the copy you archive.

        Which agreements a visit actually signed is on the visit itself, in its
        `agreements` array. Fetch the visitor first, then request a PDF per
        agreement it lists.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: agreement_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/ObjectId"
        - name: visitor_id
          in: query
          required: false
          description: The visit to render. Without it you get the blank document.
          schema:
            $ref: "#/components/schemas/ObjectId"
        - $ref: "#/components/parameters/languageId"
      responses:
        "200":
          description: The document.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "403":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /companies/{company_id}/devices:
    get:
      operationId: listDevices
      summary: Listing devices
      tags: [Devices]
      description: |
        Every kiosk registered at the location. Rather than polling this on a
        timer, subscribe to the `device_online` and `device_offline` webhook
        events — they arrive the moment the state changes.
      parameters:
        - $ref: "#/components/parameters/companyId"
      responses:
        "200":
          description: The kiosks.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Device"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/devices/{device_id}:
    get:
      operationId: getDevice
      summary: Fetching a device
      tags: [Devices]
      description: |
        One kiosk. Adding, renaming, moving and removing kiosks is Backoffice
        work — a mistake takes a tablet out of service at an entrance.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: device_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/ObjectId"
      responses:
        "200":
          description: The kiosk.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Device"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/devices/{device_uuid}/visittypes:
    get:
      operationId: listVisitTypesOfDevice
      summary: Listing visit types of a device
      tags: [Devices]
      description: |
        The visit types this kiosk shows, which can be a subset of the
        location's. A kiosk with no explicit selection returns the full list.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: device_uuid
          in: path
          required: true
          description: The device's `device_uuid`, not its `_id`.
          schema:
            type: string
      responses:
        "200":
          description: The visit types.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/VisitType"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /companies/{company_id}/statistics/{datetimestamp}:
    get:
      operationId: getStatistics
      summary: Fetching statistics
      tags: [Reporting]
      description: |
        The five numbers behind the Backoffice dashboard, counted server-side.
        One call instead of four — prefer it over the individual counters.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: datetimestamp
          in: path
          required: true
          description: The day to report on. Read in the location's timezone.
          schema:
            type: string
            example: "2026-08-29"
      responses:
        "200":
          description: The counters.
          content:
            application/json:
              schema:
                type: object
                properties:
                  totalVisitorsIn:
                    type: integer
                    description: Visits that started that day.
                  totalVisitorsCurrent:
                    type: integer
                    description: Visitors still inside.
                  totalVisitorsOut:
                    type: integer
                    description: Visits that ended that day.
                  totalMonthlyVisitors:
                    type: integer
                    description: Visits in the calendar month around that day.
                  totalMonthlySMS:
                    type: integer
                    description: SMS notifications sent that month.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /visitors/signed_in/total/{company_id}/{datetimestamp}:
    get:
      operationId: getTotalVisitorsIn
      summary: Counting visitors signed in
      tags: [Reporting]
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: datetimestamp
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Visits that started that day.
          content:
            application/json:
              schema:
                type: object
                properties:
                  totalVisitorsIn:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /visitors/signed_current/total/{company_id}/{datetimestamp}:
    get:
      operationId: getTotalVisitorsCurrent
      summary: Counting visitors inside
      tags: [Reporting]
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: datetimestamp
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Visitors still inside.
          content:
            application/json:
              schema:
                type: object
                properties:
                  totalVisitorsCurrent:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /visitors/signed_out/total/{company_id}/{datetimestamp}:
    get:
      operationId: getTotalVisitorsOut
      summary: Counting visitors signed out
      tags: [Reporting]
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: datetimestamp
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Visits that ended that day.
          content:
            application/json:
              schema:
                type: object
                properties:
                  totalVisitorsOut:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /visitors/monthly/total/{company_id}/{datetimestamp}:
    get:
      operationId: getTotalVisitorsMonthly
      summary: Counting visitors that month
      tags: [Reporting]
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: datetimestamp
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Visits in the calendar month.
          content:
            application/json:
              schema:
                type: object
                properties:
                  totalMonthlyVisitors:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /visitors/graph/{company_id}:
    get:
      operationId: getVisitsPerDay
      summary: Fetching visits per day
      tags: [Reporting]
      description: |
        Visit counts for the last ten days, one entry per day, oldest first.
        Days with no visits are included with `count: 0`. The window is fixed:
        for any other range, page through the visitor log with a date filter.
      parameters:
        - $ref: "#/components/parameters/companyId"
      responses:
        "200":
          description: The series.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    _id:
                      type: object
                      properties:
                        year:
                          type: integer
                        month:
                          type: integer
                        day:
                          type: integer
                    count:
                      type: integer
                    index:
                      type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/dashboard:
    get:
      operationId: getDashboard
      summary: Fetching the dashboard
      tags: [Reporting]
      description: The ten-day graph and the 30 most recently changed visits together.
      parameters:
        - $ref: "#/components/parameters/companyId"
      responses:
        "200":
          description: The dashboard.
          content:
            application/json:
              schema:
                type: object
                properties:
                  graph:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                  recentVisitors:
                    type: array
                    items:
                      $ref: "#/components/schemas/Visitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /visitors/export/bycompany/{company_id}:
    get:
      operationId: exportVisitorLog
      summary: Exporting the visitor log
      tags: [Reporting]
      description: |
        The visitor log as an XLSX file, with the same columns and translated
        headers as the Backoffice export.

        Capped at **200 rows per call**. For a bulk pull, page through the
        visitor log instead: it returns JSON and has no such ceiling.

        Exporting requires an administrator. An API key is a global admin on its
        locations, so it qualifies.
      parameters:
        - $ref: "#/components/parameters/companyId"
        - name: count
          in: query
          required: false
          description: Rows per page, at most 200. Pass both `count` and `page`, or neither.
          schema:
            type: integer
            maximum: 200
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
        - $ref: "#/components/parameters/languageId"
        - $ref: "#/components/parameters/sorting"
        - $ref: "#/components/parameters/filter"
      responses:
        "200":
          description: The export.
          content:
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema:
                type: string
                format: binary
        "400":
          description: |
            `Dataset too large.` above 200 rows, `No data.` when nothing matched,
            or only one of `count` and `page` was sent.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "403":
          $ref: "#/components/responses/Unauthorized"

  /companies/{company_id}/webhooks:
    get:
      operationId: getWebhook
      summary: Fetching the webhook
      tags: [Webhooks]
      description: |
        The webhook configured for the location — one per location.

        An empty object means no webhook is configured, **or** that yours was
        auto-disabled after five consecutive delivery failures. That makes this
        the quickest health check for your integration.

        Webhooks are part of the Enterprise plan; on Standard and Pro this
        answers `404`.
      parameters:
        - $ref: "#/components/parameters/companyId"
      responses:
        "200":
          description: The webhook, or an empty object.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Webhook"
        "403":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: configureWebhook
      summary: Configuring the webhook
      tags: [Webhooks]
      description: |
        Creates the webhook or replaces the existing one. Saving also clears the
        consecutive-failure counter, so this is how you re-enable a webhook that
        was switched off.

        Events carry `eventName` and `companyId` plus the ids of what changed:
        `visitor_signed_in`, `visitor_signed_out`, `visitor_updated`,
        `visitor_screening_approved`, `visitor_screening_rejected`,
        `registered_visitor_created`, `registered_visitor_updated`,
        `registered_visitor_deleted`, `registered_visitor_preregistered`,
        `device_added`, `device_online`, `device_offline`, `device_deleted`,
        `company_updated`.
      parameters:
        - $ref: "#/components/parameters/companyId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, http_method]
              properties:
                url:
                  type: string
                  format: uri
                  description: A valid, publicly reachable URL.
                http_method:
                  type: string
                  enum: [POST, PUT, PATCH, GET]
                  description: With `GET` the event is sent as query parameters instead of a body.
                enabled:
                  type: boolean
                  default: false
                auth_username:
                  type: string
                  description: Sent as HTTP Basic credentials with `auth_password`.
                auth_password:
                  type: string
      responses:
        "200":
          description: The webhook was configured.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Webhook configured successfully
                  webhook_id:
                    $ref: "#/components/schemas/ObjectId"
        "400":
          description: Invalid HTTP method, or a missing or unparseable URL.
        "403":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
