openapi: 3.0.0
# Hand-authored from the public API controllers in geodynamics-be/subcontractormanagement,
# branch master-equans (PRF-254): PR #924 added the endpoints and the service key scheme,
# PR #926 added internalCode. Verified against origin/master-equans on 2026-08-13, where
# the whole service-key surface is byte-identical to develop, so either branch documents
# the same API. Provisional: replace this file wholesale once Proof publishes a generated
# spec. Nothing else in the portal needs to change when that happens, the generator reads
# whatever is here.
#
# Bodies are camelCase. The web server sets a CustomCamelCaseNamingPolicy on AddJsonOptions,
# so every property in every response comes back camelCased, including the ProblemDetails
# validation keys.
#
# Verified live against api.geodynamics.dev/proof on 2026-08-13 with a service key: every
# property name and the items/filter envelope below match a real response, the atomic
# rejection on the workplaces setter returns exactly the documented error body and leaves
# the group untouched, and the status codes are what the endpoints actually answer. Two
# things stayed source-derived: the 403 (the test key carried all four scopes, so the gate
# never fired) and the create call (verifying it means writing a real group).
#
# PR #955 (PRF-360, merged to develop on 2026-09-10) added the two PUT calls on workplaces
# and the public:scope:workplace:manage scope. Written from origin/develop on 2026-09-14.
# Not live that day: the WorkplaceController on origin/master carries only the GET, and on
# api.geodynamics.dev a PUT with a well-formed GUID, and one on the by-identification path,
# both answered 404 where a deployed route answers 401 without a key. Bodies and status
# codes are source-derived until someone re-checks them with a key that carries the
# manage scope. The PR's integration tests pin four of the documented outcomes at the
# service level, not over HTTP: omitted fields untouched, an empty string clearing the
# code, synchronisation unchanged, and the duplicate-code rejection.
#
# Left out on purpose: /public/ping and /public/v1/environment. Both sit under the same
# /public prefix but run on the JWT scheme (scheme.public.jwt), not on service keys, so a
# partner key gets 401 on them. The environment calls create and delete whole environments,
# which is GeoDynamics provisioning, not a customer surface.
#
# Paths are written as the origin serves them, minus the /public prefix the gateway
# adds. generate-reference.mjs prefixes /proof for display; gd-redirects performs the
# inverse at the edge.
info:
  title: GeoDynamics Proof API
  version: "1"
servers:
  - url: https://api.geodynamics.dev/proof
tags:
  - name: Workplace groups
    description: >
      Workplace groups bundle workplaces so access can be granted per group rather than
      per site. A group has a name, an optional description, an optional internal code of
      your own, a set of workplaces, and a set of users who may reach them.


      Every call needs a service key with the matching scope, sent as
      `Authorization: Bearer sk_...`. A key belongs to one environment, so the
      environment is never a parameter. A key that authenticates but lacks the scope for
      the call gets `403`, not `401`.


      Errors come back in two shapes. A plain problem document carries `title`, `status`
      and `traceId`. A validation problem adds an `errors` object keyed by field. Which
      one you get for an unknown group id depends on the call: reading gives `404`,
      updating and deleting give `400` with `general.notFound` in `errors`, and setting
      the workplaces or users gives `404`. Handle both rather than switching on the
      status code alone.
  - name: Workplaces
    description: >
      The workplaces in your environment, as Proof knows them. Workplaces are created
      inside Proof itself. Over the public API you list them, and you can update the
      name, description and internal code of one. Its identification, address and
      synchronisation settings are managed in Proof and do not change over the API.


      The `identification` is the handle to use elsewhere. Setting the workplaces on a
      group takes identifications, not ids, and the update call has a
      `by-identification` variant for the same reason.
  - name: Users
    description: >
      The people in your environment who can be attached to a workplace group. Read-only
      over the public API.


      Only active members are returned. Invited and deactivated memberships are left out,
      and so is the GeoDynamics support identity.
paths:
  /v1/workplacegroup:
    post:
      summary: Create workplace group
      operationId: Create workplace group
      description: |-
        Creates a workplace group. Returns the new group's `id`.

        The group starts empty. Add workplaces and users with their own calls.

        *Privilege required: `public:scope:workplacegroup:manage`*
      tags:
        - Workplace groups
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  description: Display name of the group.
                description:
                  type: string
                  nullable: true
                  description: Optional free-text description.
                internalCode:
                  type: string
                  nullable: true
                  description: >
                    Optional code of your own, to match the group against your system.
                    Proof stores it and hands it back, nothing else.
            example:
              name: Site North
              description: All workplaces on the northern sites
              internalCode: GRP-NORTH-01
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: Id of the created group.
              example:
                id: 00000000-0000-0000-0000-000000000000
        "400":
          description: Validation failed. The body names the offending fields.
        "401":
          description: Unauthorized. Missing or invalid service key.
        "403":
          description: Forbidden. The key lacks the required scope.
  /v1/workplacegroup/list:
    get:
      summary: Get workplace groups
      operationId: Get workplace groups
      description: |-
        Returns a paged list of workplace groups in the key's environment, 100 per page.

        `workplaceCount` and `userCount` are what the group holds today, so you can spot
        an empty group without fetching it.

        *Privilege required: `public:scope:workplacegroup:read`*
      tags:
        - Workplace groups
      parameters:
        - name: page
          in: query
          description: Zero-based page index. Defaults to 0.
          required: false
          example: 0
          schema:
            type: integer
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    description: Groups on this page.
                  filter:
                    type: object
                    description: >
                      Paging state for this query: page, pageSize, totalCount, and the
                      sort and search fields the public API does not use.
              example:
                items:
                  - id: 00000000-0000-0000-0000-000000000000
                    name: Site North
                    description: All workplaces on the northern sites
                    internalCode: GRP-NORTH-01
                    workplaceCount: 12
                    userCount: 4
                filter:
                  page: 0
                  pageSize: 100
                  totalCount: 1
                  sortAscending: true
                  sortString: ""
                  searchString: ""
        "401":
          description: Unauthorized. Missing or invalid service key.
        "403":
          description: Forbidden. The key lacks the required scope.
  /v1/workplacegroup/{id}:
    get:
      summary: Get workplace group by Id
      operationId: Get workplace group by Id
      description: |-
        Returns one group with its workplace identifications and the users attached to it.

        Workplaces come back as identifications, the same handles you send when you set
        them, so a read and a write round-trip on the same values.

        *Privilege required: `public:scope:workplacegroup:read`*
      tags:
        - Workplace groups
      parameters:
        - name: id
          in: path
          description: Id of the group.
          required: true
          example: 00000000-0000-0000-0000-000000000000
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: Id of the group.
                  name:
                    type: string
                    description: Display name.
                  description:
                    type: string
                    nullable: true
                    description: Free-text description, may be null.
                  internalCode:
                    type: string
                    nullable: true
                    description: Your own code for this group, may be null.
                  workplaceIdentifications:
                    type: array
                    description: Identifications of the workplaces in this group.
                  users:
                    type: array
                    description: Users attached to the group, with name, email and role.
              example:
                id: 00000000-0000-0000-0000-000000000000
                name: Site North
                description: All workplaces on the northern sites
                internalCode: GRP-NORTH-01
                workplaceIdentifications:
                  - WP-0042
                users:
                  - userId: 00000000-0000-0000-0000-000000000000
                    name: John Doe
                    email: john.doe@example.com
                    role: Manager
        "401":
          description: Unauthorized. Missing or invalid service key.
        "403":
          description: Forbidden. The key lacks the required scope.
        "404":
          description: No group with this id in the key's environment.
    put:
      summary: Update workplace group
      operationId: Update workplace group
      description: |-
        Replaces the name, description and internal code of a group, and returns its `id`.
        Workplaces and users are not touched, set those with their own calls.

        This is a full-state replace of the three fields. Leaving out `description` or
        `internalCode` clears the stored value, it does not keep it. Send the fields you
        want the group to end up with.

        *Privilege required: `public:scope:workplacegroup:manage`*
      tags:
        - Workplace groups
      parameters:
        - name: id
          in: path
          description: Id of the group.
          required: true
          example: 00000000-0000-0000-0000-000000000000
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  description: Display name of the group.
                description:
                  type: string
                  nullable: true
                  description: Free-text description. Omitting it clears the stored one.
                internalCode:
                  type: string
                  nullable: true
                  description: Your own code. Omitting it clears the stored one.
            example:
              name: Site North and East
              description: Merged northern and eastern sites
              internalCode: GRP-NORTH-01
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: Id of the updated group.
              example:
                id: 00000000-0000-0000-0000-000000000000
        "400":
          description: >
            Validation failed. An unknown group id also lands here, as
            `general.notFound`, rather than as a 404. That differs from the read call
            on the same path, which does answer 404.
          content:
            application/json:
              schema:
                type: object
                properties:
                  title:
                    type: string
                  status:
                    type: integer
                  errors:
                    type: object
                    description: Field name to list of error messages.
                  traceId:
                    type: string
              example:
                type: https://tools.ietf.org/html/rfc9110#section-15.5.1
                title: One or more validation errors occurred.
                status: 400
                instance: /public/v1/workplacegroup/00000000-0000-0000-0000-000000000000
                errors:
                  general.notFound:
                    - A 'Not Found' error has occurred.
                traceId: 00-0000000000000000-0000000000-00
        "401":
          description: Unauthorized. Missing or invalid service key.
        "403":
          description: Forbidden. The key lacks the required scope.
    delete:
      summary: Delete workplace group
      operationId: Delete workplace group
      description: |-
        Deletes a group and returns `true`. The workplaces and users themselves are left
        alone, only the grouping disappears.

        *Privilege required: `public:scope:workplacegroup:manage`*
      tags:
        - Workplace groups
      parameters:
        - name: id
          in: path
          description: Id of the group.
          required: true
          example: 00000000-0000-0000-0000-000000000000
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: boolean
              example: true
        "400":
          description: >
            An unknown group id lands here, as `general.notFound`, rather than as a 404.
            Same shape as the update call above.
        "401":
          description: Unauthorized. Missing or invalid service key.
        "403":
          description: Forbidden. The key lacks the required scope.
  /v1/workplacegroup/{id}/workplaces:
    put:
      summary: Set workplaces on a group
      operationId: Set workplaces on a group
      description: |-
        Replaces the full set of workplaces in the group. Send every workplace the group
        should contain, not just the additions. An empty list clears the group.

        Workplaces are addressed by `identification`, the same value
        `/v1/workplace/list` returns, not by id.

        The call is atomic. If any identification does not resolve in the key's
        environment, nothing is written and the response is a `400` carrying
        `WorkplacesNotInEnvironment` under `workplaceIdentifications`, with the
        identifications that failed listed under `unresolvedIdentifications`.

        *Privilege required: `public:scope:workplacegroup:manage`*
      tags:
        - Workplace groups
      parameters:
        - name: id
          in: path
          description: Id of the group.
          required: true
          example: 00000000-0000-0000-0000-000000000000
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                workplaceIdentifications:
                  type: array
                  description: >
                    Complete set of workplace identifications for this group. An empty
                    list or a missing field clears the group.
            example:
              workplaceIdentifications:
                - WP-0042
                - WP-0043
      responses:
        "200":
          description: OK
        "400":
          description: >
            One or more identifications do not exist in this environment. Nothing was
            written.
          content:
            application/json:
              schema:
                type: object
                properties:
                  title:
                    type: string
                  status:
                    type: integer
                  errors:
                    type: object
                    description: Field name to list of error messages.
                  traceId:
                    type: string
              example:
                type: https://tools.ietf.org/html/rfc9110#section-15.5.1
                title: One or more validation errors occurred.
                status: 400
                errors:
                  workplaceIdentifications:
                    - WorkplacesNotInEnvironment
                  unresolvedIdentifications:
                    - WP-9999
                traceId: 00-0000000000000000-0000000000-00
        "401":
          description: Unauthorized. Missing or invalid service key.
        "403":
          description: Forbidden. The key lacks the required scope.
        "404":
          description: No group with this id in the key's environment.
  /v1/workplacegroup/{id}/users:
    put:
      summary: Set users on a group
      operationId: Set users on a group
      description: |-
        Replaces the full set of users attached to the group. Send every user who should
        have access, not just the additions. An empty list removes everyone.

        Users are addressed by id, the `id` from `/v1/user/list`.

        *Privilege required: `public:scope:workplacegroup:manage`*
      tags:
        - Workplace groups
      parameters:
        - name: id
          in: path
          description: Id of the group.
          required: true
          example: 00000000-0000-0000-0000-000000000000
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                userIds:
                  type: array
                  description: >
                    Complete set of user ids for this group. An empty list or a missing
                    field removes everyone.
            example:
              userIds:
                - 00000000-0000-0000-0000-000000000000
      responses:
        "200":
          description: OK
        "400":
          description: Validation failed. The body names the offending fields.
        "401":
          description: Unauthorized. Missing or invalid service key.
        "403":
          description: Forbidden. The key lacks the required scope.
        "404":
          description: No group with this id in the key's environment.
  /v1/workplace/list:
    get:
      summary: Get workplaces
      operationId: Get workplaces
      description: |-
        Returns a paged list of the workplaces in the key's environment, 100 per page.

        Use `identification` when you set the workplaces on a group. `internalCode` is
        your own code for the workplace, if one was set in Proof.

        *Privilege required: `public:scope:workplace:read`*
      tags:
        - Workplaces
      parameters:
        - name: page
          in: query
          description: Zero-based page index. Defaults to 0.
          required: false
          example: 0
          schema:
            type: integer
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    description: Workplaces on this page.
                  filter:
                    type: object
                    description: Paging state for this query.
              example:
                items:
                  - id: 00000000-0000-0000-0000-000000000000
                    identification: WP-0042
                    name: Antwerp North site
                    address: Noorderlaan 1, 2030 Antwerpen
                    description: Main northern construction site
                    internalCode: EQ-ANR-N
                filter:
                  page: 0
                  pageSize: 100
                  totalCount: 1
                  sortAscending: true
                  sortString: ""
                  searchString: ""
        "401":
          description: Unauthorized. Missing or invalid service key.
        "403":
          description: Forbidden. The key lacks the required scope.
  /v1/workplace/{id}:
    put:
      summary: Update workplace
      operationId: Update workplace
      description: |-
        Changes the name, description or internal code of a workplace, and returns the
        workplace as it is stored afterwards.

        This is a partial update. Send only the fields you want to change: a field you
        leave out keeps its stored value, and so does a field you send as `null`. An
        empty string is how you clear `description` or `internalCode`. A body of `{}`,
        or one that repeats the stored values, is accepted and answers `200` with the
        workplace unchanged. The rest of the workplace, its `identification`, its
        address and its synchronisation settings, is managed inside Proof and does not
        change.

        `internalCode` is trimmed and has to be unique among the workplaces in your
        environment, compared case-sensitively. A code that another workplace already
        carries is rejected with `400` and nothing is changed. `name` cannot be blank,
        and spaces only count as blank.

        Know the workplace by its identification rather than its id? Use
        `Update workplace by identification` below.

        *Privilege required: `public:scope:workplace:manage`*
      tags:
        - Workplaces
      parameters:
        - name: id
          in: path
          description: >
            Id of the workplace, as `Get workplaces` returns it. A malformed id answers
            an empty 404, even without a key.
          required: true
          example: 00000000-0000-0000-0000-000000000000
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  nullable: true
                  description: >
                    New display name, 500 characters at most and not blank. Left out or
                    `null`, the stored name stays.
                description:
                  type: string
                  nullable: true
                  description: >
                    New description, 500 characters at most. Left out or `null`, it
                    stays. An empty string clears it.
                internalCode:
                  type: string
                  nullable: true
                  description: >
                    Your own code, 50 characters at most, unique in your environment.
                    Left out or `null`, it stays. An empty string clears it.
            example:
              name: Antwerp North site, phase 2
              internalCode: EQ-ANR-N2
      responses:
        "200":
          description: OK. The workplace as stored after the update.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: Id of the workplace.
                  identification:
                    type: string
                    description: Identification of the workplace. Not changed by this call.
                  name:
                    type: string
                    description: Display name.
                  description:
                    type: string
                    description: Free-text description, empty when none is set.
                  internalCode:
                    type: string
                    nullable: true
                    description: Your own code for this workplace, null when none is set.
              example:
                id: 00000000-0000-0000-0000-000000000000
                identification: WP-0042
                name: Antwerp North site, phase 2
                description: Main northern construction site
                internalCode: EQ-ANR-N2
        "400":
          description: >
            Validation failed. `errors` is keyed by field: `name` when it is blank or
            over 500 characters, `description` over 500 characters, `internalCode` over
            50 characters or already in use by another workplace in your environment,
            `environmentId` with `OnlyOwnerCanModify` when the workplace belongs to
            another environment that shares it with you. The messages are Proof's own
            translation codes, so match on the field and on the part before the first
            `|`. A missing, empty or unparseable body gives `400` as well, with the
            reason in `detail` and no field keys in `errors`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  title:
                    type: string
                  status:
                    type: integer
                  errors:
                    type: object
                    description: Field name to list of error codes.
                  traceId:
                    type: string
              example:
                type: https://tools.ietf.org/html/rfc9110#section-15.5.1
                title: One or more validation errors occurred.
                status: 400
                instance: /public/v1/workplace/00000000-0000-0000-0000-000000000000
                errors:
                  internalCode:
                    - AlreadyExists|_p1=InternalCode|_p2=EQ-ANR-N2
                traceId: 00-0000000000000000-0000000000-00
        "401":
          description: Unauthorized. Missing or invalid service key.
        "403":
          description: Forbidden. The key lacks the required scope.
        "404":
          description: >
            No workplace with this id in the key's environment. A plain problem
            document: `title`, `status` and `traceId`, no `errors`.
  /v1/workplace/by-identification/{identification}:
    put:
      summary: Update workplace by identification
      operationId: Update workplace by identification
      description: |-
        The same update as `Update workplace`, addressed by the workplace's
        `identification` instead of its id. Handy when your system knows the workplace
        by the handle you also use to set the workplaces on a group.

        Same body, same rules, same response. Send only the fields you want to change,
        an empty `internalCode` clears the code, and the identification itself cannot be
        changed through this call. URL-encode it when it carries characters that are not
        safe in a path segment.

        *Privilege required: `public:scope:workplace:manage`*
      tags:
        - Workplaces
      parameters:
        - name: identification
          in: path
          description: >
            Identification of the workplace, as `Get workplaces` returns it. Matched
            without regard to case.
          required: true
          example: WP-0042
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  nullable: true
                  description: >
                    New display name, 500 characters at most and not blank. Left out or
                    `null`, the stored name stays.
                description:
                  type: string
                  nullable: true
                  description: >
                    New description, 500 characters at most. Left out or `null`, it
                    stays. An empty string clears it.
                internalCode:
                  type: string
                  nullable: true
                  description: >
                    Your own code, 50 characters at most, unique in your environment.
                    Left out or `null`, it stays. An empty string clears it.
            example:
              description: Main northern construction site, access via gate B
      responses:
        "200":
          description: OK. The workplace as stored after the update.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: Id of the workplace.
                  identification:
                    type: string
                    description: Identification of the workplace. Not changed by this call.
                  name:
                    type: string
                    description: Display name.
                  description:
                    type: string
                    description: Free-text description, empty when none is set.
                  internalCode:
                    type: string
                    nullable: true
                    description: Your own code for this workplace, null when none is set.
              example:
                id: 00000000-0000-0000-0000-000000000000
                identification: WP-0042
                name: Antwerp North site
                description: Main northern construction site, access via gate B
                internalCode: EQ-ANR-N
        "400":
          description: >
            Validation failed. Same shape and same field rules as `Update workplace`:
            `errors` is keyed by field, the messages are Proof's translation codes.
          content:
            application/json:
              schema:
                type: object
                properties:
                  title:
                    type: string
                  status:
                    type: integer
                  errors:
                    type: object
                    description: Field name to list of error codes.
                  traceId:
                    type: string
              example:
                type: https://tools.ietf.org/html/rfc9110#section-15.5.1
                title: One or more validation errors occurred.
                status: 400
                instance: /public/v1/workplace/by-identification/WP-0042
                errors:
                  internalCode:
                    - AlreadyExists|_p1=InternalCode|_p2=EQ-ANR-N
                traceId: 00-0000000000000000-0000000000-00
        "401":
          description: Unauthorized. Missing or invalid service key.
        "403":
          description: Forbidden. The key lacks the required scope.
        "404":
          description: >
            No active workplace with this identification in the key's environment.
            Archived workplaces are not matched here; address those by id. Same plain
            problem document as on `Update workplace`.
  /v1/user/list:
    get:
      summary: Get users
      operationId: Get users
      description: |-
        Returns a paged list of the active users in the key's environment, 100 per page.

        Invited and deactivated memberships are left out, and so is the GeoDynamics
        support identity. What you get back is the set of people you can actually attach
        to a workplace group.

        *Privilege required: `public:scope:user:read`*
      tags:
        - Users
      parameters:
        - name: page
          in: query
          description: Zero-based page index. Defaults to 0.
          required: false
          example: 0
          schema:
            type: integer
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    description: Users on this page.
                  filter:
                    type: object
                    description: Paging state for this query.
              example:
                items:
                  - id: 00000000-0000-0000-0000-000000000000
                    name: John Doe
                    email: john.doe@example.com
                filter:
                  page: 0
                  pageSize: 100
                  totalCount: 1
                  sortAscending: true
                  sortString: ""
                  searchString: ""
        "401":
          description: Unauthorized. Missing or invalid service key.
        "403":
          description: Forbidden. The key lacks the required scope.
components:
  securitySchemes:
    serviceKey:
      type: http
      scheme: bearer
      description: >
        A service key created inside Proof, sent as `Authorization: Bearer sk_...`.
        Keys are scoped and bound to one environment.
security:
  - serviceKey: []
