{
  "components": {
    "schemas": {
      "Acceptance": {
        "description": "Says what the app does next. outcome is one of three, and each fills its own fields and leaves the others null:\n\n- \"signed_in\": accepting created the account. session is the body a sign-in answers with, token included, and the app is in.\n\n- \"needs_code\": the account was there already. code_sent says a code is on its way to email, and the app opens at \"enter the code\".\n\n- \"joined\": the caller is already signed in as that account. membership is the entry it gained, so the app can offer to switch to it; the session it holds is unchanged and is simply read again.",
        "properties": {
          "code_sent": {
            "type": "boolean"
          },
          "email": {
            "type": "string"
          },
          "membership": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Entry"
              },
              {
                "type": "null"
              }
            ]
          },
          "outcome": {
            "type": "string"
          },
          "session": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Session"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "type": "object"
      },
      "AddedShifts": {
        "description": "What laying a shift on days made.",
        "properties": {
          "scheduled": {
            "description": "scheduled is how many of the days were not on the calendar before.",
            "format": "int64",
            "type": "integer"
          },
          "shifts": {
            "items": {
              "$ref": "#/components/schemas/ShiftAsAManagerSeesIt"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "AnsweredInvitations": {
        "description": "What answering all of a project's invitations did, as counts. The app re-reads the dashboard for the shifts themselves, as it does after answering one; these two numbers are what it has to say something about.",
        "properties": {
          "answered": {
            "description": "answered is how many invitations were accepted, or declined.",
            "format": "int64",
            "type": "integer"
          },
          "colliding": {
            "description": "colliding is how many were left waiting because they collide with accepted work. Always zero for a decline.",
            "format": "int64",
            "type": "integer"
          },
          "full": {
            "description": "full is how many were left waiting because their shift has no place. Always zero for a decline.",
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "Assignment": {
        "description": "One worker's invitation into one timeslot.",
        "properties": {
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "expires_at": {
            "format": "date-time",
            "type": "string"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "invited_at": {
            "format": "date-time",
            "type": "string"
          },
          "membership_id": {
            "format": "int64",
            "type": "integer"
          },
          "origin": {
            "enum": [
              "staffed",
              "sos",
              "open",
              "backfilled"
            ],
            "type": "string"
          },
          "rate_amount": {
            "description": "rate_amount is the rate agreed for THIS assignment, snapshotted when the invitation was created. Never recompute it from the project or the membership: editing either rate must not change what already-promised work is worth.\n\nNull when no hourly rate was agreed — the project sets none and neither does the worker. That is a real state, \"hours only\", and never a zero, which would read as unpaid work. Both halves are null together; the database refuses one without the other.\n\nMoney: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "rate_currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "responded_at": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "role_id": {
            "description": "role_id is the role on the shift this assignment fills: where its rate came from, and whose places it takes one of. role_label is that role's name, filled by the reads a person sees it through (the schedule, the staffing screen); nil for the unnamed role of a plain shift, and on a write's answer, which does not join it.",
            "format": "int64",
            "type": "integer"
          },
          "role_label": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "enum": [
              "pending",
              "accepted",
              "declined",
              "expired",
              "withdrawn",
              "cancelled"
            ],
            "type": "string"
          },
          "takes_from": {
            "description": "takes_from/takes_until are the part of the shift the worker agreed to work, where they took it PARTIALLY around a colliding accepted shift. Nil is the whole shift, the ordinary case. Computed server-side at the moment of taking — never sent by a client — and settled then: a colliding shift later given up does not widen it.",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "takes_until": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "timeslot_id": {
            "format": "int64",
            "type": "integer"
          },
          "worker_name": {
            "description": "worker_name is filled by reads a PM makes, so a staffing view can show who was invited without a second query per row.\n\nLeft out of the body when it is empty.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "AttentionSummary": {
        "description": "What one project's upcoming shifts needing attention add up to: how many, which days, dynamic shift schedule, and staffing counts (accepted, required, pending, vacancies).",
        "properties": {
          "accepted": {
            "format": "int64",
            "type": "integer"
          },
          "count": {
            "format": "int64",
            "type": "integer"
          },
          "first_day": {
            "type": "string"
          },
          "gig_id": {
            "format": "int64",
            "type": "integer"
          },
          "gig_title": {
            "type": "string"
          },
          "imminent": {
            "type": "boolean"
          },
          "last_day": {
            "type": "string"
          },
          "pending": {
            "format": "int64",
            "type": "integer"
          },
          "required": {
            "format": "int64",
            "type": "integer"
          },
          "shift_summary": {
            "description": "Left out of the body when it is empty.",
            "type": "string"
          },
          "short": {
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "CalendarDay": {
        "description": "One day a project runs, with the shifts on it.",
        "properties": {
          "day": {
            "description": "day is the calendar day, \"2026-10-27\". day_id is its id, which a new shift names.",
            "type": "string"
          },
          "day_id": {
            "format": "int64",
            "type": "integer"
          },
          "shifts": {
            "description": "shifts are the day's shifts, earliest first; a day scheduled with no shift yet has none.",
            "items": {
              "$ref": "#/components/schemas/ShiftAsAManagerSeesIt"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "CertificateGroup": {
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "CertificateKind": {
        "properties": {
          "aliases": {
            "description": "aliases are the words people use for it, in Czech, for a search.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "group": {
            "description": "group is the heading it is listed under, by the group's id.",
            "type": "string"
          },
          "id": {
            "description": "id is what a note or a certificate carries as its kind.",
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "CertificateKinds": {
        "description": "The list of kinds, in the request's language.",
        "properties": {
          "groups": {
            "description": "groups are the headings kinds are listed under, in order.",
            "items": {
              "$ref": "#/components/schemas/CertificateGroup"
            },
            "type": "array"
          },
          "kinds": {
            "description": "kinds are every kind, in the list's own order.",
            "items": {
              "$ref": "#/components/schemas/CertificateKind"
            },
            "type": "array"
          },
          "trades": {
            "description": "trades are lines of work, each with the kinds it offers first, the ones its activities may need, and the ones anybody may hold. A trade is a way of ordering the list and is stored nowhere.",
            "items": {
              "$ref": "#/components/schemas/CertificateTrade"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "CertificatePage": {
        "description": "One photo of a certificate, by position, without its bytes.",
        "properties": {
          "content_type": {
            "type": "string"
          },
          "height": {
            "format": "int64",
            "type": "integer"
          },
          "position": {
            "format": "int64",
            "type": "integer"
          },
          "width": {
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "CertificateReading": {
        "description": "What the photos of one document say, as far as they can be read: a proposal for each field, null where the pages gave nothing, and how well each page read. Every field is a proposal for the person to check and change; none of it is stored until they save.",
        "properties": {
          "expires_on": {
            "type": [
              "string",
              "null"
            ]
          },
          "issued_on": {
            "description": "issued_on and expires_on are the dates found on the pages, \"2024-05-02\", or null: the date after \"platnost do\" or \"valid until\" is the expiry, the one after \"vydáno\" or \"issued\" the issue date; without such a word the later of two dates is the expiry and the earlier the issue date, and a lone date still ahead is the expiry.",
            "type": [
              "string",
              "null"
            ]
          },
          "kind": {
            "description": "kind is the kind of document the words point to, as a kind's id from the certificate kinds endpoint, or null when no kind stood out. Never other_personal.",
            "type": [
              "string",
              "null"
            ]
          },
          "kind_name": {
            "description": "kind_name is that kind's name in the request's language, or null.",
            "type": [
              "string",
              "null"
            ]
          },
          "legible": {
            "description": "legible is true when every page read well. A page that did not can still be kept; the client says it is hard to read and suggests taking the photo again.",
            "type": "boolean"
          },
          "number": {
            "description": "number is the number printed after \"č.\" or \"No.\", or null.",
            "type": [
              "string",
              "null"
            ]
          },
          "pages": {
            "description": "pages says how well each page read, in the order sent.",
            "items": {
              "$ref": "#/components/schemas/PageReading"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "CertificateResponse": {
        "description": "One certificate, with the kind's name in the request's language.",
        "properties": {
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "expires_on": {
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "issued_on": {
            "description": "issued_on and expires_on are calendar days, \"2027-06-30\", or null. A null expiry is unknown, never \"for ever\".",
            "type": [
              "string",
              "null"
            ]
          },
          "kind": {
            "description": "kind is the kind's id from the list of certificate kinds, or \"other_personal\" with a label.",
            "type": "string"
          },
          "label": {
            "description": "label says what an \"other\" certificate is; null for a listed kind.",
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "description": "name is what the certificate is, in words: the kind's name, or the typed label of one whose kind is other_personal.",
            "type": "string"
          },
          "number": {
            "description": "number is what is printed on the card, as typed.",
            "type": [
              "string",
              "null"
            ]
          },
          "pages": {
            "description": "pages are the photos, in order, without their bytes: each page is read on its own by position.",
            "items": {
              "$ref": "#/components/schemas/CertificatePage"
            },
            "type": "array"
          },
          "scope": {
            "type": [
              "string",
              "null"
            ]
          },
          "updated_at": {
            "format": "date-time",
            "type": "string"
          },
          "uploaded_by": {
            "description": "uploaded_by is the manager who added it on the person's behalf, or null for one the person added. A manager at another company is shown null here too: which other firms somebody works for is not theirs to learn from a photo's byline.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/UploadedBy"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "type": "object"
      },
      "CertificateTrade": {
        "properties": {
          "common": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "preferred": {
            "description": "preferred, related and common are kinds by id, in the order to offer them for this trade.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "related": {
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "CertificationResponse": {
        "description": "One \"certified for\" note, with the kind's name in the request's language.",
        "properties": {
          "certificate_id": {
            "description": "certificate_id is the person's certificate the note was approved from (step 97b), or null for a note made with no proof, or one whose certificate the person has since deleted.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "expires_on": {
            "description": "expires_on is the day it stops being valid, \"2027-06-30\", or null for unknown; never \"for ever\".",
            "type": [
              "string",
              "null"
            ]
          },
          "granted_by_membership_id": {
            "description": "granted_by_membership_id is the manager who made the note, by membership id, and granted_by_name their name, or \"Deleted user\".",
            "format": "int64",
            "type": "integer"
          },
          "granted_by_name": {
            "type": "string"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "kind": {
            "description": "kind is the kind's id from the list of certificate kinds, or \"other_personal\" with a label.",
            "type": "string"
          },
          "label": {
            "description": "label says what an \"other\" certification is for; null for a listed kind, whose name the list gives.",
            "type": [
              "string",
              "null"
            ]
          },
          "membership_id": {
            "format": "int64",
            "type": "integer"
          },
          "name": {
            "description": "name is what the note is for, in words: the kind's name, or the typed label of a note whose kind is other_personal.",
            "type": "string"
          },
          "scope": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "Client": {
        "description": "A company you invoice.\n\nThe tax identifiers are here because invoicing is planned and a Czech invoice is not valid without them.",
        "properties": {
          "address_line1": {
            "type": [
              "string",
              "null"
            ]
          },
          "address_line2": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "contact_email": {
            "type": [
              "string",
              "null"
            ]
          },
          "contact_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "contact_phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": "string"
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "dic": {
            "description": "DIČ, the VAT number, e.g. \"CZ12345678\". Null when not VAT registered.",
            "type": [
              "string",
              "null"
            ]
          },
          "ico": {
            "description": "IČO, the Czech company registration number: eight digits including any leading zeros, hence text. Null for a private individual.",
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "is_archived": {
            "type": "boolean"
          },
          "name": {
            "type": "string"
          },
          "postal_code": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "CompanyTag": {
        "description": "Says which of the person's entries a row of a read across them came from, and what to send to act on it.",
        "properties": {
          "entry": {
            "description": "entry is the value of X-Werzeick-Company (or the MCP's company) that acts as this entry: \"12:manager\".",
            "type": "string"
          },
          "org_name": {
            "type": "string"
          },
          "role": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "Dashboard": {
        "description": "One person's home screen at one company.\n\nExactly one of worker and manager is set, matching role. role comes from the active membership, so the same person can be a manager on one company's dashboard and a worker on the next.",
        "properties": {
          "days": {
            "description": "days are the dates of the range, Monday of this week first, as YYYY-MM-DD: seven per week asked for, so seven by default.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "manager": {
            "description": "Left out of the body when it is empty.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/ManagerDashboard"
              },
              {
                "type": "null"
              }
            ]
          },
          "role": {
            "enum": [
              "worker",
              "manager"
            ],
            "type": "string"
          },
          "today": {
            "description": "today is one of days, so a client can mark it without deciding for itself which zone \"today\" is in.",
            "type": "string"
          },
          "worker": {
            "description": "Left out of the body when it is empty.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/WorkerDashboard"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "type": "object"
      },
      "DayStaffing": {
        "description": "One calendar day's staffing, summed across its timeslots.\n\nThis is what turns the calendar from \"which days does this run\" into \"which days need me\". A day is the unit a PM actually thinks in — they staff a week, not a timeslot — so the counts are aggregated to the day and the calendar renders them directly.",
        "properties": {
          "accepted": {
            "format": "int64",
            "type": "integer"
          },
          "day": {
            "description": "YYYY-MM-DD",
            "type": "string"
          },
          "pending": {
            "format": "int64",
            "type": "integer"
          },
          "required": {
            "format": "int64",
            "type": "integer"
          },
          "short": {
            "description": "short is the day's places nobody has accepted, summed over its shifts role by role: never required less accepted, which a labourer too many on one shift would flatter.",
            "format": "int64",
            "type": "integer"
          },
          "slots": {
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "Decline": {
        "description": "Somebody saying no to work a PM was counting on: an invitation declined, or an accepted shift given up.",
        "properties": {
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "expires_at": {
            "format": "date-time",
            "type": "string"
          },
          "gig_title": {
            "type": "string"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "invited_at": {
            "format": "date-time",
            "type": "string"
          },
          "membership_id": {
            "format": "int64",
            "type": "integer"
          },
          "origin": {
            "enum": [
              "staffed",
              "sos",
              "open",
              "backfilled"
            ],
            "type": "string"
          },
          "rate_amount": {
            "description": "rate_amount is the rate agreed for THIS assignment, snapshotted when the invitation was created. Never recompute it from the project or the membership: editing either rate must not change what already-promised work is worth.\n\nNull when no hourly rate was agreed — the project sets none and neither does the worker. That is a real state, \"hours only\", and never a zero, which would read as unpaid work. Both halves are null together; the database refuses one without the other.\n\nMoney: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "rate_currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "responded_at": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "role_id": {
            "description": "role_id is the role on the shift this assignment fills: where its rate came from, and whose places it takes one of. role_label is that role's name, filled by the reads a person sees it through (the schedule, the staffing screen); nil for the unnamed role of a plain shift, and on a write's answer, which does not join it.",
            "format": "int64",
            "type": "integer"
          },
          "role_label": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "enum": [
              "pending",
              "accepted",
              "declined",
              "expired",
              "withdrawn",
              "cancelled"
            ],
            "type": "string"
          },
          "takes_from": {
            "description": "takes_from/takes_until are the part of the shift the worker agreed to work, where they took it PARTIALLY around a colliding accepted shift. Nil is the whole shift, the ordinary case. Computed server-side at the moment of taking — never sent by a client — and settled then: a colliding shift later given up does not widen it.",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "takes_until": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "timeslot": {
            "$ref": "#/components/schemas/Shift"
          },
          "timeslot_id": {
            "format": "int64",
            "type": "integer"
          },
          "worker_name": {
            "description": "worker_name is filled by reads a PM makes, so a staffing view can show who was invited without a second query per row.\n\nLeft out of the body when it is empty.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "Deletion": {
        "description": "What deleting the caller's account would do, company by company. It only ever describes the caller's own memberships, and carries no id a client could send back: the deletion names nothing, because the account it deletes is the session's.",
        "properties": {
          "can_delete": {
            "description": "can_delete is false while something refuses it — today, being the only manager a company has. The app shows why and offers no button.",
            "type": "boolean"
          },
          "companies": {
            "items": {
              "$ref": "#/components/schemas/LeavingCompany"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "Earnings": {
        "description": "What one worker has earned on one project, and what they stand to earn on it.\n\nTwo lists, per currency, never mixed and never converted: this product holds no exchange rate and inventing one would be inventing money.",
        "properties": {
          "earned": {
            "description": "earned is hours already worked, valued from TIME ENTRIES.",
            "items": {
              "$ref": "#/components/schemas/MoneyTotal"
            },
            "type": "array"
          },
          "earned_unrated_minutes": {
            "description": "The minutes of each that had no hourly rate agreed, and so are in neither list. Always present, zero when everything had a rate, so the app can say \"hours only\" rather than show nothing.",
            "format": "int64",
            "type": "integer"
          },
          "running": {
            "description": "running is the shift the caller is on the clock for here right now, and what it has come to so far. Null when they are not on the clock, and null for a clock-out forgotten long ago. It is in neither list above: earned stays finished work only.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/RunningShift"
              },
              {
                "type": "null"
              }
            ]
          },
          "upcoming": {
            "description": "upcoming is accepted work not yet done, valued from ASSIGNMENTS.",
            "items": {
              "$ref": "#/components/schemas/MoneyTotal"
            },
            "type": "array"
          },
          "upcoming_unrated_minutes": {
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "Entry": {
        "description": "What a client needs to render \"who am I, and where\": one entry of the company switcher.\n\nAn entry is a membership in ONE role, and its identity is the pair (membership_id, role). Somebody who manages projects and works shifts at one company appears twice in memberships, with the same membership_id and a different role, and membership is the one of the two the session is in. role stays a single string, so a client that asks \"is this a manager\" has one answer per entry.\n\nIt carries no organization id a client could send back: switching names a membership and a role, and the server proves the caller holds both.",
        "properties": {
          "cancellation_emails_enabled": {
            "description": "Whether a cancelled accepted shift is emailed too, on top of the push. The worker's own setting, like the one above, and only ever describing the caller's own memberships.",
            "type": "boolean"
          },
          "emergency_responses_enabled": {
            "description": "The worker's own opt-in to emergency cover, per membership: they may want emergency shifts from one employer and not another. This only ever describes the caller's own memberships — there is no endpoint that tells a manager who has opted in.",
            "type": "boolean"
          },
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "membership_id": {
            "format": "int64",
            "type": "integer"
          },
          "org_name": {
            "type": "string"
          },
          "role": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "Expense": {
        "description": "A cost booked against a gig.\n\ncost_amount is an int64 with 4 implied decimal places — 42 CZK is 420000 — and is never converted to a float anywhere in this codebase. It crosses the wire raw, alongside its currency, and is formatted only at the edge.",
        "properties": {
          "cost_amount": {
            "description": "Money: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": "integer"
          },
          "cost_currency": {
            "description": "\"CZK\" or \"EUR\"",
            "type": "string"
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "created_by_membership_id": {
            "description": "created_by_membership_id is who booked it, as a membership rather than a user — the same person at another company is a different author.\n\nIt is the whole access rule: a worker sees, edits and deletes only the expenses they booked, a manager every one on the organization's projects. Sent to clients so a screen can tell \"mine\" from \"theirs\" without asking again; the server decides regardless.",
            "format": "int64",
            "type": "integer"
          },
          "created_by_name": {
            "description": "created_by_name is the author's name, resolved for display. Managers see whose cost each one is; a worker only ever receives their own rows, so it is their own name.",
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "gig_id": {
            "description": "gig_id is the one project this cost belongs to.",
            "format": "int64",
            "type": "integer"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "ExpiringCertificate": {
        "description": "One of the person's own certificates that is expiring or has expired, as the worker's dashboard and bell list it.",
        "properties": {
          "days_left": {
            "description": "days_left is whole days until then: 0 on the day, negative once past.",
            "format": "int64",
            "type": "integer"
          },
          "expired": {
            "description": "expired says the day has passed; otherwise it is within the window.",
            "type": "boolean"
          },
          "expires_on": {
            "description": "expires_on is the last valid day, \"2026-11-20\".",
            "type": "string"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "kind": {
            "type": "string"
          },
          "label": {
            "description": "label is the typed name of an \"other\" certificate, null otherwise.",
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "description": "name is what it is, in words, in the request's language: the kind's name or the label. The server words it, as the certificates list does; a client never looks a kind up for itself.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "FlaggedEntry": {
        "description": "One time entry a manager should look at, from any project.\n\nThe flags are decided here, on the server, so both clients show the same reason.",
        "properties": {
          "entry": {
            "$ref": "#/components/schemas/TimeEntry"
          },
          "gig_title": {
            "type": "string"
          },
          "missing_clock_out": {
            "type": "boolean"
          },
          "outside_shift": {
            "type": "boolean"
          },
          "slot_ends_at": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "slot_starts_at": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "worker_name": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "HoursRow": {
        "description": "One time entry on a project, with who worked it.",
        "properties": {
          "client_event_id": {
            "description": "client_event_id is the device's own id for this event, and the reason a retried push from a phone that was offline creates one row rather than two. Empty for entries created server-side.\n\nLeft out of the body when it is empty.",
            "type": "string"
          },
          "clock_in_at": {
            "format": "date-time",
            "type": "string"
          },
          "clock_out_at": {
            "description": "clock_out_at is null while the worker is still on site — and also when they forgot. The two are indistinguishable, and nothing here guesses.",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "corrected_at": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "gig_id": {
            "format": "int64",
            "type": "integer"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "membership_id": {
            "format": "int64",
            "type": "integer"
          },
          "rate_amount": {
            "description": "rate_amount is snapshotted at creation: from the assignment when the entry linked to one, otherwise the usual rate at that moment — the project's, else the membership's. Never recomputed.\n\nNull when no hourly rate applied: the hours are recorded and the entry is worth no money figure — not zero, which would read as unpaid work.\n\nMoney: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "rate_currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "recorded_by": {
            "description": "recorded_by names the manager who entered the hours for somebody added to a shift afterwards, and is empty for hours the worker clocked.",
            "type": "string"
          },
          "slot_ends_at": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "slot_starts_at": {
            "description": "slot_starts_at and slot_ends_at are the shift the entry linked to, or null for hours worked outside any shift, which is normal and not an error.",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "enum": [
              "manual",
              "geofence",
              "backfilled"
            ],
            "type": "string"
          },
          "timeslot_id": {
            "description": "timeslot_id is null for a stand-alone entry, which is normal rather than an error: someone who worked on a day nobody planned still worked.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "worker_name": {
            "description": "worker_name is who clocked the hours.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "Invitation": {
        "description": "A pending offer of a job at one company.",
        "properties": {
          "accepted_at": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "expires_at": {
            "format": "date-time",
            "type": "string"
          },
          "first_name": {
            "type": "string"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "is_manager": {
            "type": "boolean"
          },
          "is_worker": {
            "type": "boolean"
          },
          "last_name": {
            "type": "string"
          },
          "rate_amount": {
            "description": "Money: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "rate_currency": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "InvitationBatch": {
        "description": "What a batch of invitations does, or would do.",
        "properties": {
          "clashes": {
            "items": {
              "$ref": "#/components/schemas/OverlappingInvitation"
            },
            "type": "array"
          },
          "invitations": {
            "description": "invitations is how many are made: every pair, less the skipped.",
            "format": "int64",
            "type": "integer"
          },
          "over_filled": {
            "items": {
              "$ref": "#/components/schemas/Over-invitedShift"
            },
            "type": "array"
          },
          "people": {
            "format": "int64",
            "type": "integer"
          },
          "preview": {
            "description": "preview is true when nothing was sent.",
            "type": "boolean"
          },
          "sent": {
            "description": "sent are the invitations made, each pending; empty in a preview.",
            "items": {
              "$ref": "#/components/schemas/Assignment"
            },
            "type": "array"
          },
          "shifts": {
            "format": "int64",
            "type": "integer"
          },
          "skipped": {
            "items": {
              "$ref": "#/components/schemas/SkippedInvitation"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "InvitationSummary": {
        "description": "What one project's unanswered invitations add up to: how many, which days, how long, and what they would pay.\n\nIt is worked out here and not in a client because one of its figures is money.\n\nNothing stores that invitations were sent together, so this is every invitation the worker has not answered on the project, read now. It is the same set \"Accept all\" and \"Decline all\" act on.",
        "properties": {
          "count": {
            "description": "count is how many invitations are waiting on the project. full_count is how many of those are to a shift with no place left, which cannot be accepted until one opens. They are in every figure below all the same: the summary says what was asked, and full_count is what lets a client say that part of it cannot be taken yet.",
            "format": "int64",
            "type": "integer"
          },
          "first_day": {
            "description": "first_day and last_day are the dates, on site, the earliest and the latest shift start on, as YYYY-MM-DD. Equal when every shift is on one day. Dates and not instants, so a client prints them without deciding which zone a day is in; a night shift belongs to the day it starts.",
            "type": "string"
          },
          "full_count": {
            "format": "int64",
            "type": "integer"
          },
          "gig_id": {
            "format": "int64",
            "type": "integer"
          },
          "gig_title": {
            "type": "string"
          },
          "last_day": {
            "type": "string"
          },
          "minutes": {
            "format": "int64",
            "type": "integer"
          },
          "shift_summary": {
            "description": "shift_summary is a brief dynamic summary of the shifts: which days carry which hours and which days are off, e.g. \"Shifts Mon - Fri, 07:00 - 15:00, Sat 07:00 - 12:00; Sundays off\" or \"Shifts Mon - Fri, 07:00 - 15:00; weekends off\".\n\nLeft out of the body when it is empty.",
            "type": "string"
          },
          "unrated_minutes": {
            "format": "int64",
            "type": "integer"
          },
          "would_earn": {
            "description": "would_earn is what accepting every one of them would come to, a total per currency. unrated_minutes is the part of minutes on invitations with no hourly rate agreed, and so in none of would_earn: empty totals beside unrated minutes is \"no hourly rate\", never zero.",
            "items": {
              "$ref": "#/components/schemas/MoneyTotal"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "JobContact": {
        "description": "The one person a worker is told to ring about a job.\n\nA worker standing at a locked gate needs a number, not a screen telling them to find one — and it has to be the right number.",
        "properties": {
          "company": {
            "description": "company is the client's name when this is the client's own contact, and null when a manager named the person for the site: a foreman or a caretaker is not said to answer for the customer. It can be all there is — a client with no contact person — because somebody holding only the company's name can still find a number.",
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "LeavingCompany": {
        "description": "What deleting an account does at one of the companies the person belongs to. It only ever describes the caller's own memberships.",
        "properties": {
          "clocked_in": {
            "description": "clocked_in: an entry here has no clock-out. It is left open, for a manager to settle, and the person is told so they can clock out first.",
            "type": "boolean"
          },
          "invitations": {
            "format": "int64",
            "type": "integer"
          },
          "last_manager": {
            "description": "last_manager: they are the only manager this company has. That refuses the deletion, exactly as it refuses demoting or disabling them: there is no platform admin to let anybody back in.",
            "type": "boolean"
          },
          "org_name": {
            "type": "string"
          },
          "upcoming_shifts": {
            "description": "upcoming_shifts they have accepted and will be withdrawn from. invitations (and applications) not answered yet, which will be declined. Both count only shifts that have not started.",
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "ManagerDashboard": {
        "description": "What needs a PM's attention, then the week in numbers.",
        "properties": {
          "attention": {
            "description": "attention is upcoming shifts short of accepted workers or waiting on somebody's answer. Unanswered invitations count: on an imminent shift they are as urgent as an empty place.",
            "items": {
              "$ref": "#/components/schemas/ShortShift"
            },
            "type": "array"
          },
          "attention_summaries": {
            "description": "attention_summaries says what each project's run of attention adds up to: one per project, in the order the projects appear there. Both clients show a project with several shifts needing attention as its summary, with the shifts folded away under it, matching worker invitations.",
            "items": {
              "$ref": "#/components/schemas/AttentionSummary"
            },
            "type": "array"
          },
          "hours_to_check": {
            "description": "hours_to_check are time entries somebody should ask about.",
            "items": {
              "$ref": "#/components/schemas/FlaggedEntry"
            },
            "type": "array"
          },
          "turned_down": {
            "description": "turned_down is who said no, recently, to a shift still short.",
            "items": {
              "$ref": "#/components/schemas/Decline"
            },
            "type": "array"
          },
          "week": {
            "description": "week is the company's staffing for each of Days — the whole range, whatever its length, despite the name.",
            "items": {
              "$ref": "#/components/schemas/DayStaffing"
            },
            "type": "array"
          },
          "worked_this_week": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Worked"
              }
            ],
            "description": "worked_this_week is every worker's finished hours this calendar week, at the rates agreed for them, whatever range is shown."
          }
        },
        "type": "object"
      },
      "MoneyTotal": {
        "description": "A sum in one currency.\n\nCurrencies are never mixed, and never converted: this product holds no exchange rate and inventing one would be inventing money. A company working in two currencies sees two totals.\n\nThe amount goes raw as an int64 with four implied decimals, never pre-formatted and never a float.",
        "properties": {
          "amount": {
            "description": "Money: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": "integer"
          },
          "currency": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "MovedShifts": {
        "description": "What a move did, or would do.",
        "properties": {
          "confirmed": {
            "description": "confirmed is false for a preview, in which nothing changed.",
            "type": "boolean"
          },
          "first_day": {
            "description": "first_day is the earliest moved shift's new day, \"2026-10-29\".",
            "type": "string"
          },
          "people": {
            "description": "people is how many hold a live place on the moving shifts and are asked again.",
            "format": "int64",
            "type": "integer"
          },
          "shifts": {
            "description": "shifts is how many move; skipped how many were left where they were, having started or carrying an SOS.",
            "format": "int64",
            "type": "integer"
          },
          "skipped": {
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "OpenShift": {
        "description": "One ROLE on a shift a worker could take: a shift with two roles is two rows, each with its own places, label and figure, because taking is per role.",
        "properties": {
          "accepted": {
            "description": "Staffing is filled by the reads that compute it. accepted is what fills the slot; pending is what a PM still has no answer on. Short is the places nobody has accepted, counted ROLE BY ROLE: a labourer too many does not stand in for the crane op who is missing. Off the wire: the clients are told what follows from it (full, which open shifts are takeable), never asked to count.",
            "format": "int64",
            "type": "integer"
          },
          "can_take_from": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "can_take_until": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "collides_from": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "collides_until": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "collides_with_company": {
            "description": "collides_with_company names the company the colliding shift is at when that is another of the worker's companies, and is null for this company's own: the clients say \"at Beta\" only then.",
            "type": [
              "string",
              "null"
            ]
          },
          "collides_with_title": {
            "description": "Shift conflicts, the same fields a worker's own schedule entry carries and for the same reason: said here so neither client works it out, and nobody taps a take that can only be refused.",
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "day_id": {
            "format": "int64",
            "type": "integer"
          },
          "ends_at": {
            "format": "date-time",
            "type": "string"
          },
          "gig_id": {
            "format": "int64",
            "type": "integer"
          },
          "gig_title": {
            "type": "string"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "mine_status": {
            "description": "mine_status is the worker's own live place on this shift, when they have one: 'accepted' if they are on it, 'pending' if they applied and the PM has not picked yet. Empty when they hold neither.\n\nThe two are not the same thing to the person reading it — one is work they have, the other is work they might get — and a screen collapsing both into \"taken\" would be promising somebody a shift nobody has agreed they are doing.",
            "enum": [
              "pending",
              "accepted",
              "declined",
              "expired",
              "withdrawn",
              "cancelled"
            ],
            "type": "string"
          },
          "pending": {
            "format": "int64",
            "type": "integer"
          },
          "rate_amount": {
            "description": "rate_amount is what THIS worker would be paid for it: the role's rate, else the usual rate — the project's, else their own — raised by the shift's multiplier where it has one, capped by the project. Resolved server-side for the same reason it is resolved at signup: nobody takes a shift without seeing the figure, and the figure is not a client's to decide.\n\nNull when the shift offers nothing and there is no usual rate either: taking it records hours with no hourly rate, which is a real state.\n\nMoney: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "rate_currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "required_workers": {
            "format": "int64",
            "type": "integer"
          },
          "role_id": {
            "description": "role_id is the role this row offers, and what a take names.",
            "format": "int64",
            "type": "integer"
          },
          "role_label": {
            "description": "role_label is the one-string summary of Roles (\"2× crane op, 3× labourer\"; a single named role is its label alone; the unnamed role of a plain shift is nil), written by this service in the same transaction as the lines. It is what the app and most screens show of the roles.",
            "type": [
              "string",
              "null"
            ]
          },
          "starts_at": {
            "format": "date-time",
            "type": "string"
          },
          "taking_confirms": {
            "description": "taking_confirms is the slot's auto-accept flag, under a name that says what it means to the person about to tap: taking it books them outright, or lands them as an applicant the PM picks from. The terms are part of the offer, like the figure, and nobody takes a shift without both.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "Over-invitedShift": {
        "description": "A shift asked of more people than it has places: allowed, since some will say no, and said.",
        "properties": {
          "invited": {
            "format": "int64",
            "type": "integer"
          },
          "places": {
            "format": "int64",
            "type": "integer"
          },
          "shift": {
            "$ref": "#/components/schemas/ShiftReference"
          }
        },
        "type": "object"
      },
      "OverlappingInvitation": {
        "description": "A person asked to a shift that overlaps other work of theirs at this company: allowed, and said.",
        "properties": {
          "in_batch": {
            "type": "boolean"
          },
          "membership_id": {
            "format": "int64",
            "type": "integer"
          },
          "shift": {
            "$ref": "#/components/schemas/ShiftReference"
          },
          "with": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ShiftReference"
              }
            ],
            "description": "with is the shift it overlaps; in_batch says that one is in this very batch rather than already on their schedule."
          },
          "worker_name": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "PageReading": {
        "description": "How well one page read.",
        "properties": {
          "legible": {
            "description": "legible is whether the page read well enough to trust: enough words, at 60 or more.",
            "type": "boolean"
          },
          "position": {
            "description": "position is the page's place among those sent, counted from 1.",
            "format": "int64",
            "type": "integer"
          },
          "readability": {
            "description": "readability is how sure the reader is of the words it found, 0 to 100, or null for a page it could not read at all.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "PersonOnAProject": {
        "description": "One person with live places on a project's upcoming shifts.",
        "properties": {
          "accepted": {
            "description": "accepted and pending are their upcoming places: what they agreed to, and what is still waiting on an answer. Never merged: an invitation staffs nothing.",
            "format": "int64",
            "type": "integer"
          },
          "first_start": {
            "description": "first_start and last_start are the earliest and latest of those shifts.",
            "format": "date-time",
            "type": "string"
          },
          "last_start": {
            "format": "date-time",
            "type": "string"
          },
          "membership_id": {
            "format": "int64",
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "pending": {
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "PersonResponse": {
        "description": "One person on the roster: the membership, and the company's \"certified for\" notes about them.",
        "properties": {
          "cancellation_emails_enabled": {
            "description": "cancellation_emails_enabled: a cancelled accepted shift is emailed as well as pushed. On by default; only the worker turns it off.",
            "type": "boolean"
          },
          "certifications": {
            "description": "certifications is what this company has noted the person as certified for, oldest first. Empty for nobody noted. A note is the company's own and requires nothing: nothing is filtered by it.",
            "items": {
              "$ref": "#/components/schemas/CertificationResponse"
            },
            "type": "array"
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "emergency_responses_enabled": {
            "type": "boolean"
          },
          "first_name": {
            "type": "string"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "is_disabled": {
            "type": "boolean"
          },
          "is_manager": {
            "type": "boolean"
          },
          "is_worker": {
            "type": "boolean"
          },
          "last_name": {
            "type": "string"
          },
          "org_name": {
            "type": "string"
          },
          "rate_amount": {
            "description": "The default rate for new agreements at this company. Assignments and time entries snapshot their own copy; changing this never alters past work. Null for somebody who does not work shifts.\n\nMoney: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "rate_currency": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "Point": {
        "description": "A WGS84 coordinate in degrees.",
        "properties": {
          "lat": {
            "format": "double",
            "type": "number"
          },
          "lon": {
            "format": "double",
            "type": "number"
          }
        },
        "type": "object"
      },
      "PreviewInvitation": {
        "description": "Describes an invitation to whoever holds the link, before they accept it.\n\nIt deliberately reveals only the company name and what they are invited to do — enough to decide whether to accept, and nothing about the organization's people or projects, since anyone holding the link is by definition not yet a member.",
        "properties": {
          "email": {
            "type": "string"
          },
          "first_name": {
            "type": "string"
          },
          "is_manager": {
            "type": "boolean"
          },
          "is_worker": {
            "type": "boolean"
          },
          "org_name": {
            "type": "string"
          },
          "rate_amount": {
            "description": "Money: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "rate_currency": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "Project": {
        "description": "A project as the app receives it: the project, and how to reach the manager running it.\n\nEvery endpoint that answers with a project answers with this, so the app parses one shape whichever screen asked.",
        "properties": {
          "archived_at": {
            "description": "archived_at is when a manager finished with this project, and null while it is live. An archived project takes no new work — no days, shifts, invitations or clock-ins — and leaves every list of what is coming up, but nothing on it is removed: its hours, expenses and invoices are all still there. On the wire so a client can say \"archived\" instead of offering a clock-in that would be refused. Who archived it stays here, like every other manager's detail.",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "client_contact_email": {
            "type": [
              "string",
              "null"
            ]
          },
          "client_contact_name": {
            "description": "The client's own contact person, from the client's record.\n\nIt is NOT what a screen shows as \"who to ring\": that is job_contact, below, which is this person only when the project names nobody for the site.",
            "type": [
              "string",
              "null"
            ]
          },
          "client_contact_phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "client_id": {
            "description": "The company being invoiced for this work. Nullable: a project may be internal, or booked before anyone has agreed who is paying.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "client_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "company": {
            "description": "company is which of the caller's entries the row came from, on a list across them and nowhere else.\n\nLeft out of the body when it is empty.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/CompanyTag"
              },
              {
                "type": "null"
              }
            ]
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "expected_minutes": {
            "description": "expected_minutes is how much work this project is thought to be, and it is a FORECAST ONLY: it never reaches a rate, an assignment or a payslip. It exists so a project nobody has scheduled into timeslots yet can still say roughly what it is worth. Where timeslots exist they are the better answer and the screens prefer them. Stored in minutes, shown as hours.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "expenses": {
            "items": {
              "$ref": "#/components/schemas/Expense"
            },
            "type": "array"
          },
          "geofence_radius_m": {
            "description": "Radius in metres for optional geofence auto-clocking.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "is_assigned": {
            "type": "boolean"
          },
          "is_invited": {
            "description": "is_invited and shifts_to_take are what the acting worker's list says about each project beside is_assigned: an invitation of theirs still waiting for an answer, and how many of its open shifts they could take now. Worked out here so both clients mark a row the same way, and always false and 0 for a manager.",
            "type": "boolean"
          },
          "job_contact": {
            "description": "job_contact is who a worker rings about this job, already chosen: the site contact where the project names one, else the client's. Null when there is nobody at all.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/JobContact"
              },
              {
                "type": "null"
              }
            ]
          },
          "latitude": {
            "description": "Where the work happens, in WGS84 degrees. Nullable because a project can be created before anyone has pinned the site.\n\nThis is the SITE's location, never a worker's. The server never receives a position from a device — the app holds these coordinates, computes distance to them locally, and sends only a clock event.",
            "format": "double",
            "type": [
              "number",
              "null"
            ]
          },
          "longitude": {
            "format": "double",
            "type": [
              "number",
              "null"
            ]
          },
          "owner_id": {
            "description": "owner_id is a membership id — the PM *at this company* — not a user id.",
            "format": "int64",
            "type": "integer"
          },
          "project_manager": {
            "description": "Null when there is nobody to show: the owner has left, or is no longer a manager. Never another company's manager, and never a worker.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/ProjectManager"
              },
              {
                "type": "null"
              }
            ]
          },
          "shifts_to_take": {
            "format": "int64",
            "type": "integer"
          },
          "site_address": {
            "description": "Where to go, in words: \"Gate 3, Průmyslová 12, Ostrava\".\n\nStored rather than looked up on the phone, because the map tiles are the first thing to fail on a bad connection and that is precisely when somebody needs to know which gate. Free text — a site often has no postal address at all. Never the client's address, which is where the invoice goes.",
            "type": [
              "string",
              "null"
            ]
          },
          "site_outline": {
            "description": "The drawn site outline, if the manager traced one rather than dropping a pin. latitude, longitude and geofence_radius_m are DERIVED from this when it is present — stored alongside rather than recomputed on read, because the geofence compares against them on a phone that may be offline.\n\nKept so the outline can be drawn again on any map, not only reduced to a circle: a worker looking at an L-shaped site should see its shape.",
            "items": {
              "$ref": "#/components/schemas/Point"
            },
            "type": "array"
          },
          "title": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "ProjectManager": {
        "description": "How somebody working on a project reaches the manager who runs it: a name to ask for and the two ways to get hold of them.\n\nDeliberately nothing else from the membership. A worker reads this, and a manager's rate, role history or other projects are none of their business. The phone and email are the person's own, from users, because that is the only place this product keeps them.",
        "properties": {
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "RemovedDays": {
        "description": "What removing days did, or would do.",
        "properties": {
          "cancelled": {
            "description": "cancelled is how many places were cancelled, once confirmed.",
            "format": "int64",
            "type": "integer"
          },
          "confirmed": {
            "description": "confirmed is false for a preview, in which nothing changed.",
            "type": "boolean"
          },
          "days": {
            "description": "days is how many days are on the calendar to come off (a preview), or came off it.",
            "format": "int64",
            "type": "integer"
          },
          "people": {
            "description": "people is how many different people hold a live place on those days: who will be told. In a preview only.",
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "RemovedShift": {
        "description": "What removing a shift did, or would do.",
        "properties": {
          "accepted": {
            "description": "accepted and pending are the live places on the shift: who had agreed to work it, and who was asked and had not answered. In a preview.",
            "format": "int64",
            "type": "integer"
          },
          "cancelled": {
            "description": "cancelled is how many places were cancelled, once confirmed.",
            "format": "int64",
            "type": "integer"
          },
          "confirmed": {
            "description": "confirmed is false for a preview, in which nothing changed.",
            "type": "boolean"
          },
          "pending": {
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "RequestCodeResponse": {
        "description": "The answer to asking for a sign-in code.",
        "properties": {
          "resend_after_seconds": {
            "description": "resend_after_seconds is how long until another code may be asked for the same address. A request sooner is answered 429 with a Retry-After.",
            "format": "int64",
            "type": "integer"
          },
          "status": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "Role": {
        "description": "One role on a shift: a kind of worker with its own places and, where the manager set one, its own hourly rate.",
        "properties": {
          "accepted": {
            "description": "accepted fills the role's places; pending is what nobody has answered yet, and fills nothing.",
            "format": "int64",
            "type": "integer"
          },
          "headcount": {
            "description": "headcount is how many of this role the shift needs.",
            "format": "int64",
            "type": "integer"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "label": {
            "description": "label names the kind of worker (\"crane op\"). Null for the one unnamed role of a plain shift.",
            "type": [
              "string",
              "null"
            ]
          },
          "pending": {
            "format": "int64",
            "type": "integer"
          },
          "rate_amount": {
            "description": "rate_amount is what an hour in this role pays, or null for the usual rate: the project's pay rate, else each worker's own.\n\nMoney: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "rate_currency": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "RoleToAdd": {
        "description": "One role on a new shift.",
        "properties": {
          "headcount": {
            "description": "headcount is how many of this role the shift needs, at least 1.",
            "format": "int64",
            "type": "integer"
          },
          "label": {
            "description": "label names the kind of worker. A shift with one role may leave it empty; two roles need names to be told apart.",
            "type": "string"
          },
          "rate_amount": {
            "description": "rate_amount is what an hour in this role pays. Leave both null for the usual rate: the project's pay rate, else each worker's own. Refused above the project's maximum hourly rate.\n\nMoney: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "rate_currency": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "RunningShift": {
        "description": "A shift the acting worker is on the clock for right now, and what it has come to so far.\n\nIt is the one figure in this product about work that is not finished, and three things keep it honest:\n\n- No client multiplies anything, and the membership's rate today has no part in it.\n\n- It is \"so far\", and it is in no total. So a figure a worker has seen in a total never goes down because they clocked out and then corrected the entry.\n\n- A forgotten clock-out has none.",
        "properties": {
          "amount": {
            "description": "amount is those minutes at the entry's snapshotted rate. Null, with currency, when no hourly rate was agreed: hours and no money figure, never zero.\n\nMoney: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "as_of": {
            "description": "as_of is the server's clock when this was worked out. The figure is true of that minute and of no later one.",
            "format": "date-time",
            "type": "string"
          },
          "currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "minutes": {
            "format": "int64",
            "type": "integer"
          },
          "since": {
            "description": "since is when they clocked in.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "SOSOverAProject": {
        "description": "What an SOS over several shifts did.",
        "properties": {
          "already_raised": {
            "description": "What was skipped and why: an SOS already running, every place taken meanwhile, the shift over.",
            "format": "int64",
            "type": "integer"
          },
          "filled": {
            "format": "int64",
            "type": "integer"
          },
          "over": {
            "format": "int64",
            "type": "integer"
          },
          "raised": {
            "description": "raised is the emergency opened on each shift, earliest first.",
            "items": {
              "$ref": "#/components/schemas/SOSRaised"
            },
            "type": "array"
          },
          "told": {
            "description": "told is how many people were told, each once for the whole batch.",
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "SOSRaised": {
        "description": "One emergency opened on one shift.",
        "properties": {
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "offered_rate_multiplier": {
            "description": "offered_rate_multiplier is the raise, with four implied decimals.\n\nA multiplier with four implied decimal places: 15000 is 1.5×.",
            "format": "int64",
            "type": "integer"
          },
          "opened_at": {
            "format": "date-time",
            "type": "string"
          },
          "timeslot_id": {
            "format": "int64",
            "type": "integer"
          },
          "told": {
            "description": "told is how many people were told, each once: every worker at the company who opted into emergency cover and could take it.",
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "ScheduleEntry": {
        "description": "One row of a worker's own schedule.",
        "properties": {
          "can_take_from": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "can_take_until": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "collides_from": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "collides_until": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "collides_with_company": {
            "description": "collides_with_company names the company the colliding shift is at when that is another of the worker's companies, and is null for this company's own: the clients say \"at Beta\" only then.",
            "type": [
              "string",
              "null"
            ]
          },
          "collides_with_title": {
            "description": "Set on PENDING entries only. No client works any of this out for itself.",
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "expires_at": {
            "format": "date-time",
            "type": "string"
          },
          "full": {
            "description": "full says every place in THIS entry's role is taken by somebody who accepted. A pending invitation to a full role cannot be accepted, so the clients show it as waiting for a place rather than offering an Accept that would be refused. Worked out here, not in a client.",
            "type": "boolean"
          },
          "gig_id": {
            "format": "int64",
            "type": "integer"
          },
          "gig_title": {
            "type": "string"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "invited_at": {
            "format": "date-time",
            "type": "string"
          },
          "membership_id": {
            "format": "int64",
            "type": "integer"
          },
          "origin": {
            "enum": [
              "staffed",
              "sos",
              "open",
              "backfilled"
            ],
            "type": "string"
          },
          "rate_amount": {
            "description": "rate_amount is the rate agreed for THIS assignment, snapshotted when the invitation was created. Never recompute it from the project or the membership: editing either rate must not change what already-promised work is worth.\n\nNull when no hourly rate was agreed — the project sets none and neither does the worker. That is a real state, \"hours only\", and never a zero, which would read as unpaid work. Both halves are null together; the database refuses one without the other.\n\nMoney: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "rate_currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "responded_at": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "role_id": {
            "description": "role_id is the role on the shift this assignment fills: where its rate came from, and whose places it takes one of. role_label is that role's name, filled by the reads a person sees it through (the schedule, the staffing screen); nil for the unnamed role of a plain shift, and on a write's answer, which does not join it.",
            "format": "int64",
            "type": "integer"
          },
          "role_label": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "enum": [
              "pending",
              "accepted",
              "declined",
              "expired",
              "withdrawn",
              "cancelled"
            ],
            "type": "string"
          },
          "takes_from": {
            "description": "takes_from/takes_until are the part of the shift the worker agreed to work, where they took it PARTIALLY around a colliding accepted shift. Nil is the whole shift, the ordinary case. Computed server-side at the moment of taking — never sent by a client — and settled then: a colliding shift later given up does not widen it.",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "takes_until": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "timeslot": {
            "$ref": "#/components/schemas/Shift"
          },
          "timeslot_id": {
            "format": "int64",
            "type": "integer"
          },
          "worker_name": {
            "description": "worker_name is filled by reads a PM makes, so a staffing view can show who was invited without a second query per row.\n\nLeft out of the body when it is empty.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ScheduledDay": {
        "description": "One day a project runs.\n\nThe calendar is a set of days, not a range: a project pauses and resumes, and a gap is simply an absent row.",
        "properties": {
          "day": {
            "description": "Rendered as YYYY-MM-DD. A calendar day is a DATE, never a timestamp: answering \"which days does this run\" with instants shifts days across timezones.",
            "type": "string"
          },
          "gig_id": {
            "format": "int64",
            "type": "integer"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "Session": {
        "description": "Describes who the caller is and where they may act.\n\ntoken and expires_at are omitted when no new session was issued — /me returns this same shape without minting anything, and an empty token field would invite a client to store one.",
        "properties": {
          "expires_at": {
            "description": "Left out of the body when it is empty.",
            "type": "string"
          },
          "language": {
            "description": "language is the language the account chose, \"cs\" or \"en\", and null for one that never chose. It is about the person, not the entry. The app shows it only on a phone with no per-app language setting (Android 12 and older), where its own picker sets it; everywhere else the phone says which language the app is in.",
            "type": [
              "string",
              "null"
            ]
          },
          "membership": {
            "$ref": "#/components/schemas/Entry"
          },
          "memberships": {
            "items": {
              "$ref": "#/components/schemas/Entry"
            },
            "type": "array"
          },
          "token": {
            "description": "Left out of the body when it is empty.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "Shift": {
        "description": "A block of work on one day needing some number of workers.",
        "properties": {
          "accepted": {
            "description": "Staffing is filled by the reads that compute it. accepted is what fills the slot; pending is what a PM still has no answer on. Short is the places nobody has accepted, counted ROLE BY ROLE: a labourer too many does not stand in for the crane op who is missing. Off the wire: the clients are told what follows from it (full, which open shifts are takeable), never asked to count.",
            "format": "int64",
            "type": "integer"
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "day_id": {
            "format": "int64",
            "type": "integer"
          },
          "ends_at": {
            "format": "date-time",
            "type": "string"
          },
          "gig_id": {
            "format": "int64",
            "type": "integer"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "pending": {
            "format": "int64",
            "type": "integer"
          },
          "required_workers": {
            "format": "int64",
            "type": "integer"
          },
          "role_label": {
            "description": "role_label is the one-string summary of Roles (\"2× crane op, 3× labourer\"; a single named role is its label alone; the unnamed role of a plain shift is nil), written by this service in the same transaction as the lines. It is what the app and most screens show of the roles.",
            "type": [
              "string",
              "null"
            ]
          },
          "starts_at": {
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ShiftAsAManagerSeesIt": {
        "description": "A shift as a manager reads it: the shift, how short it is, whether it is open to the company, and its roles.",
        "properties": {
          "accepted": {
            "description": "Staffing is filled by the reads that compute it. accepted is what fills the slot; pending is what a PM still has no answer on. Short is the places nobody has accepted, counted ROLE BY ROLE: a labourer too many does not stand in for the crane op who is missing. Off the wire: the clients are told what follows from it (full, which open shifts are takeable), never asked to count.",
            "format": "int64",
            "type": "integer"
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "day_id": {
            "format": "int64",
            "type": "integer"
          },
          "ends_at": {
            "format": "date-time",
            "type": "string"
          },
          "gig_id": {
            "format": "int64",
            "type": "integer"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "is_open": {
            "description": "is_open says the shift is advertised to the company, so any worker there may take a place without being invited. open_auto_accept says a taker is confirmed outright; otherwise they land as an applicant for the manager to approve.",
            "type": "boolean"
          },
          "offered_rate_multiplier": {
            "description": "offered_rate_multiplier raises each taker's usual rate, with four implied decimals: 15000 is 1.5×. Null when the shift offers each role's rate as it stands. An SOS is an open shift with this set.\n\nA multiplier with four implied decimal places: 15000 is 1.5×.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "open_auto_accept": {
            "type": "boolean"
          },
          "pending": {
            "format": "int64",
            "type": "integer"
          },
          "required_workers": {
            "format": "int64",
            "type": "integer"
          },
          "role_label": {
            "description": "role_label is the one-string summary of Roles (\"2× crane op, 3× labourer\"; a single named role is its label alone; the unnamed role of a plain shift is nil), written by this service in the same transaction as the lines. It is what the app and most screens show of the roles.",
            "type": [
              "string",
              "null"
            ]
          },
          "roles": {
            "items": {
              "$ref": "#/components/schemas/Role"
            },
            "type": "array"
          },
          "short": {
            "description": "short is the places nobody has accepted, counted role by role: a labourer too many does not stand in for the crane op who is missing.",
            "format": "int64",
            "type": "integer"
          },
          "starts_at": {
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ShiftNeedingAttention": {
        "description": "An upcoming shift that needs a manager's attention.",
        "properties": {
          "accepted": {
            "description": "Staffing is filled by the reads that compute it. accepted is what fills the slot; pending is what a PM still has no answer on. Short is the places nobody has accepted, counted ROLE BY ROLE: a labourer too many does not stand in for the crane op who is missing. Off the wire: the clients are told what follows from it (full, which open shifts are takeable), never asked to count.",
            "format": "int64",
            "type": "integer"
          },
          "company": {
            "description": "company is which of the caller's entries the shift came from, on a list across them and nowhere else.\n\nLeft out of the body when it is empty.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/CompanyTag"
              },
              {
                "type": "null"
              }
            ]
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "day_id": {
            "format": "int64",
            "type": "integer"
          },
          "ends_at": {
            "format": "date-time",
            "type": "string"
          },
          "gig_id": {
            "format": "int64",
            "type": "integer"
          },
          "gig_title": {
            "type": "string"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "pending": {
            "format": "int64",
            "type": "integer"
          },
          "required_workers": {
            "format": "int64",
            "type": "integer"
          },
          "role_label": {
            "description": "role_label is the one-string summary of Roles (\"2× crane op, 3× labourer\"; a single named role is its label alone; the unnamed role of a plain shift is nil), written by this service in the same transaction as the lines. It is what the app and most screens show of the roles.",
            "type": [
              "string",
              "null"
            ]
          },
          "roles": {
            "items": {
              "$ref": "#/components/schemas/Role"
            },
            "type": "array"
          },
          "short": {
            "description": "short is the places nobody has accepted, role by role.",
            "format": "int64",
            "type": "integer"
          },
          "sos_open": {
            "description": "sos_open says an emergency is already running on it, so no second one is raised.",
            "type": "boolean"
          },
          "starts_at": {
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ShiftReference": {
        "description": "One shift, as a batch's answer names it.",
        "properties": {
          "ends_at": {
            "format": "date-time",
            "type": "string"
          },
          "starts_at": {
            "format": "date-time",
            "type": "string"
          },
          "timeslot_id": {
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "ShiftWithItsPeople": {
        "description": "A shift with the people on it.",
        "properties": {
          "accepted": {
            "description": "Staffing is filled by the reads that compute it. accepted is what fills the slot; pending is what a PM still has no answer on. Short is the places nobody has accepted, counted ROLE BY ROLE: a labourer too many does not stand in for the crane op who is missing. Off the wire: the clients are told what follows from it (full, which open shifts are takeable), never asked to count.",
            "format": "int64",
            "type": "integer"
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "day_id": {
            "format": "int64",
            "type": "integer"
          },
          "ends_at": {
            "format": "date-time",
            "type": "string"
          },
          "gig_id": {
            "format": "int64",
            "type": "integer"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "is_open": {
            "description": "is_open says the shift is advertised to the company, so any worker there may take a place without being invited. open_auto_accept says a taker is confirmed outright; otherwise they land as an applicant for the manager to approve.",
            "type": "boolean"
          },
          "offered_rate_multiplier": {
            "description": "offered_rate_multiplier raises each taker's usual rate, with four implied decimals: 15000 is 1.5×. Null when the shift offers each role's rate as it stands. An SOS is an open shift with this set.\n\nA multiplier with four implied decimal places: 15000 is 1.5×.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "open_auto_accept": {
            "type": "boolean"
          },
          "pending": {
            "format": "int64",
            "type": "integer"
          },
          "people": {
            "description": "people is everyone with an assignment on the shift, in the order they were asked: invited, accepted, declined, lapsed, withdrawn, cancelled. Only accepted fills a place.",
            "items": {
              "$ref": "#/components/schemas/Assignment"
            },
            "type": "array"
          },
          "required_workers": {
            "format": "int64",
            "type": "integer"
          },
          "role_label": {
            "description": "role_label is the one-string summary of Roles (\"2× crane op, 3× labourer\"; a single named role is its label alone; the unnamed role of a plain shift is nil), written by this service in the same transaction as the lines. It is what the app and most screens show of the roles.",
            "type": [
              "string",
              "null"
            ]
          },
          "roles": {
            "items": {
              "$ref": "#/components/schemas/Role"
            },
            "type": "array"
          },
          "short": {
            "description": "short is the places nobody has accepted, counted role by role: a labourer too many does not stand in for the crane op who is missing.",
            "format": "int64",
            "type": "integer"
          },
          "starts_at": {
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ShortShift": {
        "description": "One upcoming timeslot that a PM should look at, with enough context to act on it without a second query.",
        "properties": {
          "accepted": {
            "description": "Staffing is filled by the reads that compute it. accepted is what fills the slot; pending is what a PM still has no answer on. Short is the places nobody has accepted, counted ROLE BY ROLE: a labourer too many does not stand in for the crane op who is missing. Off the wire: the clients are told what follows from it (full, which open shifts are takeable), never asked to count.",
            "format": "int64",
            "type": "integer"
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "day_id": {
            "format": "int64",
            "type": "integer"
          },
          "ends_at": {
            "format": "date-time",
            "type": "string"
          },
          "gig_id": {
            "format": "int64",
            "type": "integer"
          },
          "gig_title": {
            "type": "string"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "pending": {
            "format": "int64",
            "type": "integer"
          },
          "required_workers": {
            "format": "int64",
            "type": "integer"
          },
          "role_label": {
            "description": "role_label is the one-string summary of Roles (\"2× crane op, 3× labourer\"; a single named role is its label alone; the unnamed role of a plain shift is nil), written by this service in the same transaction as the lines. It is what the app and most screens show of the roles.",
            "type": [
              "string",
              "null"
            ]
          },
          "starts_at": {
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "SkippedInvitation": {
        "description": "A person already live on a shift, so not asked again.",
        "properties": {
          "membership_id": {
            "format": "int64",
            "type": "integer"
          },
          "shift": {
            "$ref": "#/components/schemas/ShiftReference"
          },
          "worker_name": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "TakenOffAProject": {
        "description": "What taking somebody off a project did, or would do.",
        "properties": {
          "accepted": {
            "description": "accepted and pending are the places cancelled: work they had agreed to, and invitations or applications still open.",
            "format": "int64",
            "type": "integer"
          },
          "confirmed": {
            "description": "confirmed is false for a preview, in which nothing changed.",
            "type": "boolean"
          },
          "first_day": {
            "description": "first_day is the earliest cancelled shift's day on site, \"2026-10-29\".",
            "type": "string"
          },
          "pending": {
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "TimeEntry": {
        "description": "One stretch of work: when someone started, when they stopped, and what it was worth.",
        "properties": {
          "client_event_id": {
            "description": "client_event_id is the device's own id for this event, and the reason a retried push from a phone that was offline creates one row rather than two. Empty for entries created server-side.\n\nLeft out of the body when it is empty.",
            "type": "string"
          },
          "clock_in_at": {
            "format": "date-time",
            "type": "string"
          },
          "clock_out_at": {
            "description": "clock_out_at is null while the worker is still on site — and also when they forgot. The two are indistinguishable, and nothing here guesses.",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "corrected_at": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "gig_id": {
            "format": "int64",
            "type": "integer"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "membership_id": {
            "format": "int64",
            "type": "integer"
          },
          "rate_amount": {
            "description": "rate_amount is snapshotted at creation: from the assignment when the entry linked to one, otherwise the usual rate at that moment — the project's, else the membership's. Never recomputed.\n\nNull when no hourly rate applied: the hours are recorded and the entry is worth no money figure — not zero, which would read as unpaid work.\n\nMoney: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "rate_currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "enum": [
              "manual",
              "geofence",
              "backfilled"
            ],
            "type": "string"
          },
          "timeslot_id": {
            "description": "timeslot_id is null for a stand-alone entry, which is normal rather than an error: someone who worked on a day nobody planned still worked.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "Tool": {
        "description": "One item on a worker's kit list.\n\nA client is only ever shown its own tools, so the field would tell it something it already knows — and echoing an id a client never sends invites it to start sending one.",
        "properties": {
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "gig_ids": {
            "description": "gig_ids are the projects this tool is currently attached to, so a list screen can show where it is expected without a query per row.",
            "items": {
              "format": "int64",
              "type": "integer"
            },
            "type": "array"
          },
          "icon": {
            "type": "string"
          },
          "id": {
            "format": "int64",
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "quantity": {
            "description": "quantity is null for \"one, or some\". Nothing decrements it: this is a kit list, not a stock level.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "ToolIcon": {
        "description": "One choice in the picker: the stored name and what to call it.",
        "properties": {
          "label": {
            "description": "label is the English word for it.",
            "type": "string"
          },
          "name": {
            "description": "name is what goes in the database and crosses the wire. Lower case, no spaces, stable forever.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "UploadedBy": {
        "description": "Names the manager who added a certificate, and at which company. The name reads \"Deleted user\" for one who has left.",
        "properties": {
          "company": {
            "type": "string"
          },
          "membership_id": {
            "format": "int64",
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "Worked": {
        "description": "Hours on the clock in some period, and what they came to.\n\nOpen entries count for neither: a shift in progress has no length yet, and a forgotten clock-out is a discrepancy for a PM, not a number to estimate.",
        "properties": {
          "earned": {
            "items": {
              "$ref": "#/components/schemas/MoneyTotal"
            },
            "type": "array"
          },
          "minutes": {
            "format": "int64",
            "type": "integer"
          },
          "unrated_minutes": {
            "description": "unrated_minutes are the part of minutes worked with no hourly rate, and so in none of earned. Zero at a firm where everybody has one.",
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "WorkerDashboard": {
        "description": "\"this week, this much, what is open\" — and it is where the worker's schedule lives. On Monday morning they do not yet know which project to open, so the cross-project week has to be here.\n\nShifts are grouped on the server so both clients put the same shift under the same heading: which group something is in is a decision, not styling.",
        "properties": {
          "awaiting_you": {
            "description": "awaiting_you are invitations the worker has not answered, whenever they are. They lapse on their own at the shift's start and reopen a vacancy, so they lead.\n\nOne project's invitations sit together: the projects in the order of their earliest shift, and each project's shifts in the order they happen. A manager can invite somebody to a week in one go, and both clients draw a project's run under one heading with \"Accept all\" beside it — by starting a new heading wherever gig_id changes, so the grouping is decided here and neither client sorts anything.",
            "items": {
              "$ref": "#/components/schemas/ScheduleEntry"
            },
            "type": "array"
          },
          "expiring_certificates": {
            "description": "expiring_certificates is the worker's own certificates that expire within 30 days or have expired, soonest first. Derived from each one's expires_on on every read; nothing stores that one was seen. A certificate with no expiry on record is never here. Empty for nothing expiring.",
            "items": {
              "$ref": "#/components/schemas/ExpiringCertificate"
            },
            "type": "array"
          },
          "invitation_summaries": {
            "description": "invitation_summaries says what each project's run of awaiting_you adds up to: one per project, in the order the projects appear there. Both clients show a project with several invitations as its summary, with the shifts folded away under it, so a week of invitations is one thing to answer and not seven rows to scroll past.",
            "items": {
              "$ref": "#/components/schemas/InvitationSummary"
            },
            "type": "array"
          },
          "later": {
            "description": "later is what lies beyond the range that is still live, plus anything a manager cancelled. The worker's own declines are left out: they know.",
            "items": {
              "$ref": "#/components/schemas/ScheduleEntry"
            },
            "type": "array"
          },
          "open_shifts": {
            "description": "open_shifts are shifts at the company a PM opened that this worker could take now, each with the figure they would be paid and whether taking it books them outright. Ones they already hold are in their shifts instead.",
            "items": {
              "$ref": "#/components/schemas/OpenShift"
            },
            "type": "array"
          },
          "still_to_earn": {
            "items": {
              "$ref": "#/components/schemas/MoneyTotal"
            },
            "type": "array"
          },
          "still_to_earn_unrated_minutes": {
            "description": "still_to_earn_unrated_minutes is accepted work ahead with no hourly rate agreed, and so in none of still_to_earn.",
            "format": "int64",
            "type": "integer"
          },
          "this_week": {
            "description": "this_week is every other assignment on a shift within Days, in any state — booked, applied for, and also cancelled or given up, because a cancelled shift must be obvious rather than silently absent. It is named for the default range; asked for two weeks, it holds two.",
            "items": {
              "$ref": "#/components/schemas/ScheduleEntry"
            },
            "type": "array"
          },
          "worked_this_month": {
            "$ref": "#/components/schemas/Worked"
          },
          "worked_this_week": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Worked"
              }
            ],
            "description": "Earned is from time entries; still_to_earn from accepted assignments. Two tables, two snapshots, neither the membership rate. The worked figures are the calendar week and month whatever range is shown: the range is how far ahead to look, and hours already worked are behind."
          }
        },
        "type": "object"
      }
    },
    "securitySchemes": {
      "apiKey": {
        "bearerFormat": "wzk_…",
        "description": "An API key from the account page (wzk_…), or a session token. A key acts as the company and role it was made in, unless X-Werzeick-Company names another of its person's.",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "externalDocs": {
    "description": "The API guide",
    "url": "https://werzeick-dev.isc.hm/en/docs"
  },
  "info": {
    "description": "The API the Werzeick app uses, for a company's own software. Generated from the server's own code, so it describes the version running at the server below. Money is an integer with four implied decimal places beside its currency; a rate that is null is \"no hourly rate\", never zero. No request carries a company's id. A key acts as the company it was made in, or as another of its person's own, named by membership in the X-Werzeick-Company header and proved on every request.",
    "title": "Werzeick API",
    "version": "current"
  },
  "openapi": "3.1.0",
  "paths": {
    "/api/assignments/{id}/approve": {
      "post": {
        "description": "The answer is the assignment, accepted, at the rate it was offered. A full role, an application that has lapsed, an invitation (which only the worker can accept), and an applicant who has since accepted colliding work are each refused. The person is told. Managers only.",
        "operationId": "postApiAssignmentsIdApprove",
        "parameters": [
          {
            "description": "The assignment's id: your place on a shift.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "created_at": "2026-09-07T09:00:00Z",
                      "expires_at": "2026-09-10T05:00:00Z",
                      "id": 1,
                      "invited_at": "2026-09-07T09:00:00Z",
                      "membership_id": 2,
                      "origin": "staffed",
                      "rate_amount": 5200000,
                      "rate_currency": "CZK",
                      "responded_at": "2026-09-07T09:05:00Z",
                      "role_id": 1,
                      "role_label": "crane op",
                      "status": "accepted",
                      "takes_from": null,
                      "takes_until": null,
                      "timeslot_id": 1,
                      "worker_name": "Jana Horáková"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Assignment"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Confirms an applicant on a shift the manager reviews takers for: an open shift or an SOS.",
        "tags": [
          "Staffing a project"
        ]
      }
    },
    "/api/assignments/{id}/cancel": {
      "post": {
        "description": "Their place is cancelled and reopens, and they are told once; if they had accepted they are emailed too, unless they turned that off. confirm is required: without it nothing changes and this answers 400, because the act is what tells a person not to come. A place already settled — declined, lapsed, withdrawn, cancelled — is refused. To take somebody off every upcoming shift of the project at once, use the project's people endpoint. Managers only.",
        "operationId": "postApiAssignmentsIdCancel",
        "parameters": [
          {
            "description": "The assignment's id: your place on a shift.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "A removal's consent.",
                "properties": {
                  "confirm": {
                    "description": "confirm says to go ahead. Without it nothing changes and the answer says what would.",
                    "type": "boolean"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Assignment"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id; send confirm: this cancels their place and tells them."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Takes one person off one shift.",
        "tags": [
          "Staffing a project"
        ]
      }
    },
    "/api/assignments/{id}/respond": {
      "post": {
        "description": "Accept, decline and withdraw share a route because they share a lifecycle; the action is in the body rather than the path so a client cannot invent one.",
        "operationId": "postApiAssignmentsIdRespond",
        "parameters": [
          {
            "description": "The assignment's id: your place on a shift.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "action": {
                    "type": "string"
                  },
                  "partial": {
                    "description": "partial is consent to take only the free remainder of a shift that collides with accepted work. The span itself is the server's to compute under its locks; a client never sends one.",
                    "type": "boolean"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "created_at": "2026-09-07T09:00:00Z",
                      "expires_at": "2026-09-10T05:00:00Z",
                      "id": 2,
                      "invited_at": "2026-09-07T09:00:00Z",
                      "membership_id": 2,
                      "origin": "sos",
                      "rate_amount": 6300000,
                      "rate_currency": "CZK",
                      "responded_at": "2026-09-07T09:05:00Z",
                      "role_id": 2,
                      "role_label": "crane op",
                      "status": "accepted",
                      "takes_from": "2026-09-10T09:00:00Z",
                      "takes_until": "2026-09-10T13:00:00Z",
                      "timeslot_id": 2
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Assignment"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: action must be \"accept\", \"decline\" or \"withdraw\"; invalid JSON body; invalid assignment id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Records a worker's answer to one assignment.",
        "tags": [
          "Shifts and answers"
        ]
      }
    },
    "/api/auth/me": {
      "delete": {
        "description": "204 when it is gone; every token the person held stops working with it, this one included.\n\nThe request has no body and names nobody. A refusal is 409 with the same shape the preview answers with, so the app can say which company would be left without a manager.\n\nA session only: an API key is refused with 403.",
        "operationId": "deleteApiAuthMe",
        "responses": {
          "204": {
            "description": "Done, with no body."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text saying why."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Erases the caller's account.",
        "tags": [
          "Your account"
        ]
      },
      "get": {
        "operationId": "getApiAuthMe",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "language": "cs",
                      "membership": {
                        "cancellation_emails_enabled": true,
                        "emergency_responses_enabled": true,
                        "first_name": "Petra",
                        "last_name": "Nováková",
                        "membership_id": 12,
                        "org_name": "Alpha Stavby",
                        "role": "worker"
                      },
                      "memberships": [
                        {
                          "cancellation_emails_enabled": true,
                          "emergency_responses_enabled": true,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 12,
                          "org_name": "Alpha Stavby",
                          "role": "worker"
                        },
                        {
                          "cancellation_emails_enabled": false,
                          "emergency_responses_enabled": false,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 27,
                          "org_name": "Beta Montáže",
                          "role": "manager"
                        },
                        {
                          "cancellation_emails_enabled": false,
                          "emergency_responses_enabled": false,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 27,
                          "org_name": "Beta Montáže",
                          "role": "worker"
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            },
            "description": "OK."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Describes the caller: their identity, the company they are acting as, and the other companies they could switch to, or that a key could name in X-Werzeick-Company.",
        "tags": [
          "Signing in and your session"
        ]
      }
    },
    "/api/auth/me/cancellation-emails": {
      "put": {
        "description": "Its own endpoint rather than a second field on the emergency one: each takes {\"enabled\": …} and a missing key is refused, which only works when the key names one setting.",
        "operationId": "putApiAuthMeCancellationEmails",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "enabled": {
                    "type": [
                      "boolean",
                      "null"
                    ]
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "language": "cs",
                      "membership": {
                        "cancellation_emails_enabled": true,
                        "emergency_responses_enabled": true,
                        "first_name": "Petra",
                        "last_name": "Nováková",
                        "membership_id": 12,
                        "org_name": "Alpha Stavby",
                        "role": "worker"
                      },
                      "memberships": [
                        {
                          "cancellation_emails_enabled": true,
                          "emergency_responses_enabled": true,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 12,
                          "org_name": "Alpha Stavby",
                          "role": "worker"
                        },
                        {
                          "cancellation_emails_enabled": false,
                          "emergency_responses_enabled": false,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 27,
                          "org_name": "Beta Montáže",
                          "role": "manager"
                        },
                        {
                          "cancellation_emails_enabled": false,
                          "emergency_responses_enabled": false,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 27,
                          "org_name": "Beta Montáže",
                          "role": "worker"
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: enabled is required; invalid JSON body."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: only a worker is sent cancellations."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Records whether this worker wants a cancelled shift emailed as well as pushed, at the company they are acting as.",
        "tags": [
          "Signing in and your session"
        ]
      }
    },
    "/api/auth/me/deletion": {
      "get": {
        "description": "The app asks this before it asks the person, so the numbers are in the question.\n\nA session only: an API key is refused with 403.",
        "operationId": "getApiAuthMeDeletion",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "can_delete": false,
                      "companies": [
                        {
                          "clocked_in": true,
                          "invitations": 1,
                          "last_manager": false,
                          "org_name": "Alpha Stavby",
                          "upcoming_shifts": 2
                        },
                        {
                          "clocked_in": false,
                          "invitations": 0,
                          "last_manager": true,
                          "org_name": "Beta Montáže",
                          "upcoming_shifts": 0
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Deletion"
                }
              }
            },
            "description": "OK."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Says what deleting the caller's account would do, and changes nothing.",
        "tags": [
          "Your account"
        ]
      }
    },
    "/api/auth/me/emergency-responses": {
      "put": {
        "description": "It names no membership: the acting one comes from the session, which is what stops this being a way to opt somebody else in or read whether they have.",
        "operationId": "putApiAuthMeEmergencyResponses",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "enabled": {
                    "type": [
                      "boolean",
                      "null"
                    ]
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "language": "cs",
                      "membership": {
                        "cancellation_emails_enabled": true,
                        "emergency_responses_enabled": true,
                        "first_name": "Petra",
                        "last_name": "Nováková",
                        "membership_id": 12,
                        "org_name": "Alpha Stavby",
                        "role": "worker"
                      },
                      "memberships": [
                        {
                          "cancellation_emails_enabled": true,
                          "emergency_responses_enabled": true,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 12,
                          "org_name": "Alpha Stavby",
                          "role": "worker"
                        },
                        {
                          "cancellation_emails_enabled": false,
                          "emergency_responses_enabled": false,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 27,
                          "org_name": "Beta Montáže",
                          "role": "manager"
                        },
                        {
                          "cancellation_emails_enabled": false,
                          "emergency_responses_enabled": false,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 27,
                          "org_name": "Beta Montáže",
                          "role": "worker"
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: enabled is required; invalid JSON body."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: only a worker can take emergency shifts."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Records whether this worker wants to hear about emergency cover at the company they are acting as.",
        "tags": [
          "Signing in and your session"
        ]
      }
    },
    "/api/auth/me/language": {
      "put": {
        "description": "The app sends it from its own picker, which it has only on a phone with no per-app language setting (Android 12 and older); elsewhere the phone decides. The web client's menu records the same choice.\n\nIt names no user: the account is the session's. Answered with the whole session, like the other settings here.\n\nA session only: an API key is refused with 403.",
        "operationId": "putApiAuthMeLanguage",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "language": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "language": "cs",
                      "membership": {
                        "cancellation_emails_enabled": true,
                        "emergency_responses_enabled": true,
                        "first_name": "Petra",
                        "last_name": "Nováková",
                        "membership_id": 12,
                        "org_name": "Alpha Stavby",
                        "role": "worker"
                      },
                      "memberships": [
                        {
                          "cancellation_emails_enabled": true,
                          "emergency_responses_enabled": true,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 12,
                          "org_name": "Alpha Stavby",
                          "role": "worker"
                        },
                        {
                          "cancellation_emails_enabled": false,
                          "emergency_responses_enabled": false,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 27,
                          "org_name": "Beta Montáže",
                          "role": "manager"
                        },
                        {
                          "cancellation_emails_enabled": false,
                          "emergency_responses_enabled": false,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 27,
                          "org_name": "Beta Montáže",
                          "role": "worker"
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; language must be cs or en."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Records the account's choice of language: PUT /api/auth/me/language, {\"language\": \"cs\"}.",
        "tags": [
          "Signing in and your session"
        ]
      }
    },
    "/api/auth/request-code": {
      "post": {
        "description": "It answers 202 whether or not the address has an account. Anything else would make this endpoint a way to discover who is on the platform, and since onboarding is invite-only that would also reveal which companies have invited whom.\n\nAnother code for the same address may be asked for 30 seconds after the last, and not sooner: a request within that wait is answered 429, with a Retry-After header giving the seconds left. Any address is answered so, whether or not it has an account. Too many codes in a quarter of an hour is a 429 with no Retry-After.\n\nPublic: no key and no session needed.",
        "operationId": "postApiAuthRequestCode",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "email": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "resend_after_seconds": 30,
                      "status": "sent if an account exists"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/RequestCodeResponse"
                }
              }
            },
            "description": "Accepted."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: email is required; invalid JSON body."
          },
          "429": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: a code was sent moments ago; wait {seconds} seconds before asking for another; too many codes requested; wait a few minutes."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "security": [],
        "summary": "Emails a sign-in code.",
        "tags": [
          "Signing in and your session"
        ]
      }
    },
    "/api/auth/sign-out": {
      "post": {
        "description": "It succeeds even for a token that was already invalid: signing out must never fail.\n\nA session only: an API key is refused with 403.",
        "operationId": "postApiAuthSignOut",
        "responses": {
          "204": {
            "description": "Done, with no body."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Revokes the session.",
        "tags": [
          "Signing in and your session"
        ]
      }
    },
    "/api/auth/switch": {
      "post": {
        "description": "The old token stops working immediately, so nothing cached against the previous organization or role can outlive the switch.\n\nA role the membership does not hold answers 403, exactly like a membership that is not the caller's: neither says which of the two it was.\n\nA session only: an API key is refused with 403.",
        "operationId": "postApiAuthSwitch",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "Names the entry to move to: a membership and the role to act in there. Both are required, and both are proved by the service.",
                "properties": {
                  "membership_id": {
                    "format": "int64",
                    "type": "integer"
                  },
                  "role": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "language": "cs",
                      "membership": {
                        "cancellation_emails_enabled": true,
                        "emergency_responses_enabled": true,
                        "first_name": "Petra",
                        "last_name": "Nováková",
                        "membership_id": 12,
                        "org_name": "Alpha Stavby",
                        "role": "worker"
                      },
                      "memberships": [
                        {
                          "cancellation_emails_enabled": true,
                          "emergency_responses_enabled": true,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 12,
                          "org_name": "Alpha Stavby",
                          "role": "worker"
                        },
                        {
                          "cancellation_emails_enabled": false,
                          "emergency_responses_enabled": false,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 27,
                          "org_name": "Beta Montáže",
                          "role": "manager"
                        },
                        {
                          "cancellation_emails_enabled": false,
                          "emergency_responses_enabled": false,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 27,
                          "org_name": "Beta Montáže",
                          "role": "worker"
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; role must be worker or manager."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Moves the caller to another of their entries — another company, or the other role at the same one — rotating the session.",
        "tags": [
          "Signing in and your session"
        ]
      }
    },
    "/api/auth/verify": {
      "post": {
        "description": "Public: no key and no session needed.",
        "operationId": "postApiAuthVerify",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "code": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "language": "cs",
                      "membership": {
                        "cancellation_emails_enabled": true,
                        "emergency_responses_enabled": true,
                        "first_name": "Petra",
                        "last_name": "Nováková",
                        "membership_id": 12,
                        "org_name": "Alpha Stavby",
                        "role": "worker"
                      },
                      "memberships": [
                        {
                          "cancellation_emails_enabled": true,
                          "emergency_responses_enabled": true,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 12,
                          "org_name": "Alpha Stavby",
                          "role": "worker"
                        },
                        {
                          "cancellation_emails_enabled": false,
                          "emergency_responses_enabled": false,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 27,
                          "org_name": "Beta Montáže",
                          "role": "manager"
                        },
                        {
                          "cancellation_emails_enabled": false,
                          "emergency_responses_enabled": false,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 27,
                          "org_name": "Beta Montáže",
                          "role": "worker"
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: email and code are required; invalid JSON body."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that code is not valid."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: this account is not a member of any organization."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "security": [],
        "summary": "Exchanges a code for a session.",
        "tags": [
          "Signing in and your session"
        ]
      }
    },
    "/api/certificate-kinds": {
      "get": {
        "description": "One kind, other_personal, is for a certificate the list has no name for; a note or a certificate of that kind carries a typed label. Either role.",
        "operationId": "getApiCertificateKinds",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "groups": [
                        {
                          "id": "education",
                          "name": "Education"
                        },
                        {
                          "id": "qualification",
                          "name": "Qualification"
                        }
                      ],
                      "kinds": [
                        {
                          "aliases": [
                            "jiný certifikát",
                            "jiné oprávnění",
                            "neznámý doklad"
                          ],
                          "group": "other",
                          "id": "other_personal",
                          "name": "Other document"
                        },
                        {
                          "aliases": [
                            "jeřábnický průkaz",
                            "mostový jeřáb"
                          ],
                          "group": "induction",
                          "id": "lift_crane_overhead",
                          "name": "Overhead or gantry crane operator"
                        },
                        {
                          "aliases": [
                            "svářečský certifikát",
                            "úřední zkouška",
                            "ISO 9606",
                            "EN 287"
                          ],
                          "group": "qualification",
                          "id": "weld_iso9606",
                          "name": "Welder qualification test (EN ISO 9606)"
                        }
                      ],
                      "trades": [
                        {
                          "common": [
                            "edu_apprenticeship",
                            "edu_diploma",
                            "qual_nsk",
                            "edu_retraining",
                            "training_manufacturer",
                            "training_bozp",
                            "training_fire",
                            "training_first_aid",
                            "training_heights",
                            "training_fall_protection",
                            "scheme_scc",
                            "scheme_vca",
                            "other_personal"
                          ],
                          "id": "crane_operator",
                          "name": "Crane / loader crane operator",
                          "preferred": [
                            "lift_crane_overhead",
                            "lift_crane_mobile",
                            "lift_crane_tower",
                            "lift_loader_crane"
                          ],
                          "related": [
                            "lift_slinger",
                            "lift_signaller",
                            "driver_licence"
                          ]
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/CertificateKinds"
                }
              }
            },
            "description": "OK."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          }
        },
        "summary": "Lists every kind of certificate there is, with its name in the request's language, the group it is listed under, the words people use for it, and the trades with the order each offers kinds in.",
        "tags": [
          "Certificates"
        ]
      }
    },
    "/api/certificates": {
      "get": {
        "description": "The pages themselves are read one at a time. Either role: the documents are the person's, whatever the key acts as.",
        "operationId": "getApiCertificates",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": [
                      {
                        "created_at": "2026-10-28T09:00:00Z",
                        "expires_on": "2029-05-01",
                        "id": 1,
                        "issued_on": "2024-05-02",
                        "kind": "lift_crane_overhead",
                        "label": null,
                        "name": "Overhead or gantry crane operator",
                        "number": "JB-4471",
                        "pages": [
                          {
                            "content_type": "image/jpeg",
                            "height": 1067,
                            "position": 1,
                            "width": 1600
                          },
                          {
                            "content_type": "image/jpeg",
                            "height": 1067,
                            "position": 2,
                            "width": 1600
                          }
                        ],
                        "scope": "bridge cranes to 10 t",
                        "updated_at": "2026-10-28T09:04:00Z",
                        "uploaded_by": {
                          "company": "Alpha Stavby",
                          "membership_id": 1,
                          "name": "Tomáš Novák"
                        }
                      },
                      {
                        "created_at": "2026-10-28T10:00:00Z",
                        "expires_on": null,
                        "id": 2,
                        "issued_on": null,
                        "kind": "other_personal",
                        "label": "Drone pilot",
                        "name": "Drone pilot",
                        "number": null,
                        "pages": [],
                        "scope": null,
                        "updated_at": "2026-10-28T10:00:00Z",
                        "uploaded_by": null
                      }
                    ]
                  }
                },
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/CertificateResponse"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Lists the caller's own certificates, oldest first, each with its pages' sizes.",
        "tags": [
          "Certificates"
        ]
      },
      "post": {
        "description": "The managers of every company the caller works for are pushed once when the first page lands, naming nothing. A person holds at most 50.",
        "operationId": "postApiCertificates",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "What is typed about a certificate: what is printed on the card.",
                "properties": {
                  "expires_on": {
                    "description": "expires_on is the day it stops being valid, \"2029-05-01\", or null for unknown. Blank never means \"for ever\".",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "issued_on": {
                    "description": "issued_on is the day it was issued, \"2024-05-02\", or null.",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "kind": {
                    "description": "kind is a kind's id from the certificate kinds endpoint, or other_personal for one the list has no name for.",
                    "type": "string"
                  },
                  "label": {
                    "description": "label says what an other_personal certificate is. Required for that kind, refused for any other, at most 120 characters.",
                    "type": "string"
                  },
                  "number": {
                    "description": "number is the number printed on the card, if any. At most 60 characters; nothing is checked against it.",
                    "type": "string"
                  },
                  "scope": {
                    "description": "At most 200 characters.",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "created_at": "2026-10-28T10:00:00Z",
                      "expires_on": null,
                      "id": 2,
                      "issued_on": null,
                      "kind": "other_personal",
                      "label": "Drone pilot",
                      "name": "Drone pilot",
                      "number": null,
                      "pages": [],
                      "scope": null,
                      "updated_at": "2026-10-28T10:00:00Z",
                      "uploaded_by": null
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/CertificateResponse"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Adds a certificate of the caller's own, with no pages yet: the photos are added one by one afterwards.",
        "tags": [
          "Certificates"
        ]
      }
    },
    "/api/certificates/read": {
      "post": {
        "description": "Nothing is stored; the client fills its form in from the answer and the person checks it before saving. Either role.\n\nThe photos come as multipart/form-data, each in a part named page, or as JSON with the images in base64; at most 8, each at most 4 MB. A photo is shrunk here before it is read, as it is before it is stored. Reading takes about a second a page. Answers 503 when reading is not set up on this server, in which case the form works by hand.",
        "operationId": "postApiCertificatesRead",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "The photos to read, as JSON. The same endpoint also takes multipart/form-data with each photo in a part named page, which is what the app and the web client send.",
                "properties": {
                  "images": {
                    "description": "They are read and then forgotten; nothing is stored until the certificate is created.",
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "expires_on": "2029-05-01",
                      "issued_on": "2024-05-02",
                      "kind": "lift_crane_overhead",
                      "kind_name": "Overhead or gantry crane operator",
                      "legible": false,
                      "number": "JB-4471",
                      "pages": [
                        {
                          "legible": true,
                          "position": 1,
                          "readability": 94
                        },
                        {
                          "legible": false,
                          "position": 2,
                          "readability": 41
                        },
                        {
                          "legible": false,
                          "position": 3,
                          "readability": null
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/CertificateReading"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: image must be base64; invalid JSON body; send at least one page to read; send the image as a part named page; the upload could not be read."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          },
          "503": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: reading photos is not set up on this server; fill the form in by hand."
          }
        },
        "summary": "Reads the words off the photos of one document and proposes what the certificate's fields should say: its kind, the dates on it and its number, with how well each photo read.",
        "tags": [
          "Certificates"
        ]
      }
    },
    "/api/certificates/{id}": {
      "delete": {
        "description": "A note a company made from it stays, saying it has no proof on file. Nobody is told.",
        "operationId": "deleteApiCertificatesId",
        "parameters": [
          {
            "description": "The certificate's id, as the list of certificates gives it.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done, with no body."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Removes one of the caller's certificates with its pages.",
        "tags": [
          "Certificates"
        ]
      },
      "patch": {
        "description": "Every field is written as sent, so send the whole record; the pages are untouched.",
        "operationId": "patchApiCertificatesId",
        "parameters": [
          {
            "description": "The certificate's id, as the list of certificates gives it.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "What is typed about a certificate: what is printed on the card.",
                "properties": {
                  "expires_on": {
                    "description": "expires_on is the day it stops being valid, \"2029-05-01\", or null for unknown. Blank never means \"for ever\".",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "issued_on": {
                    "description": "issued_on is the day it was issued, \"2024-05-02\", or null.",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "kind": {
                    "description": "kind is a kind's id from the certificate kinds endpoint, or other_personal for one the list has no name for.",
                    "type": "string"
                  },
                  "label": {
                    "description": "label says what an other_personal certificate is. Required for that kind, refused for any other, at most 120 characters.",
                    "type": "string"
                  },
                  "number": {
                    "description": "number is the number printed on the card, if any. At most 60 characters; nothing is checked against it.",
                    "type": "string"
                  },
                  "scope": {
                    "description": "At most 200 characters.",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "created_at": "2026-10-28T09:00:00Z",
                      "expires_on": "2029-05-01",
                      "id": 1,
                      "issued_on": "2024-05-02",
                      "kind": "lift_crane_overhead",
                      "label": null,
                      "name": "Overhead or gantry crane operator",
                      "number": "JB-4471",
                      "pages": [
                        {
                          "content_type": "image/jpeg",
                          "height": 1067,
                          "position": 1,
                          "width": 1600
                        },
                        {
                          "content_type": "image/jpeg",
                          "height": 1067,
                          "position": 2,
                          "width": 1600
                        }
                      ],
                      "scope": "bridge cranes to 10 t",
                      "updated_at": "2026-10-28T09:04:00Z",
                      "uploaded_by": {
                        "company": "Alpha Stavby",
                        "membership_id": 1,
                        "name": "Tomáš Novák"
                      }
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/CertificateResponse"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Corrects what is typed about one of the caller's certificates.",
        "tags": [
          "Certificates"
        ]
      }
    },
    "/api/certificates/{id}/pages": {
      "post": {
        "description": "The image comes as multipart/form-data in a part named page, or as JSON with the bytes in base64. Whatever is sent, what is kept is a JPEG of the pixels and nothing else: no camera metadata, no position. Answers the certificate with its pages.",
        "operationId": "postApiCertificatesIdPages",
        "parameters": [
          {
            "description": "The certificate's id, as the list of certificates gives it.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "One page of a certificate as JSON: the image's bytes in base64. The same endpoint also takes multipart/form-data with the file in a part named page, which is what the app and the web client send.",
                "properties": {
                  "image": {
                    "description": "A PDF is refused; a client with one renders its pages to images first. The server shrinks the image to about 1600 px on its long edge, re-encodes it as JPEG and keeps nothing else of it: no camera metadata, no position.",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "created_at": "2026-10-28T09:00:00Z",
                      "expires_on": "2029-05-01",
                      "id": 1,
                      "issued_on": "2024-05-02",
                      "kind": "lift_crane_overhead",
                      "label": null,
                      "name": "Overhead or gantry crane operator",
                      "number": "JB-4471",
                      "pages": [
                        {
                          "content_type": "image/jpeg",
                          "height": 1067,
                          "position": 1,
                          "width": 1600
                        },
                        {
                          "content_type": "image/jpeg",
                          "height": 1067,
                          "position": 2,
                          "width": 1600
                        }
                      ],
                      "scope": "bridge cranes to 10 t",
                      "updated_at": "2026-10-28T09:04:00Z",
                      "uploaded_by": {
                        "company": "Alpha Stavby",
                        "membership_id": 1,
                        "name": "Tomáš Novák"
                      }
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/CertificateResponse"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: image must be base64; invalid JSON body; invalid {thing} id; send the image as a part named page; the upload could not be read."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Adds the next page to one of the caller's certificates: the front, the back, an attachment, at most 8.",
        "tags": [
          "Certificates"
        ]
      }
    },
    "/api/certificates/{id}/pages/{position}": {
      "delete": {
        "description": "Answers the certificate as it is now.",
        "operationId": "deleteApiCertificatesIdPagesPosition",
        "parameters": [
          {
            "description": "The certificate's id, as the list of certificates gives it.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "The page's position on the certificate, counted from 1.",
            "in": "path",
            "name": "position",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "created_at": "2026-10-28T10:00:00Z",
                      "expires_on": null,
                      "id": 2,
                      "issued_on": null,
                      "kind": "other_personal",
                      "label": "Drone pilot",
                      "name": "Drone pilot",
                      "number": null,
                      "pages": [],
                      "scope": null,
                      "updated_at": "2026-10-28T10:00:00Z",
                      "uploaded_by": null
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/CertificateResponse"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; page not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Takes one page off one of the caller's certificates; the pages after it move up.",
        "tags": [
          "Certificates"
        ]
      },
      "get": {
        "description": "The body is the JPEG, with its content type.",
        "operationId": "getApiCertificatesIdPagesPosition",
        "parameters": [
          {
            "description": "The certificate's id, as the list of certificates gives it.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "The page's position on the certificate, counted from 1.",
            "in": "path",
            "name": "position",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; page not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Answers one page of one of the caller's certificates as the image itself, not as JSON.",
        "tags": [
          "Certificates"
        ]
      }
    },
    "/api/clients": {
      "get": {
        "description": "Archived clients are left out unless ?archived=1. Any member of the company may read the list, as the app shows a project's client.",
        "operationId": "getApiClients",
        "parameters": [
          {
            "in": "query",
            "name": "archived",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": [
                      {
                        "address_line1": "Průmyslová 12",
                        "address_line2": null,
                        "city": "Ostrava",
                        "contact_email": "petr@stavbynovak.test",
                        "contact_name": "Petr Novák",
                        "contact_phone": "+420777123456",
                        "country": "CZ",
                        "created_at": "2026-09-01T09:00:00Z",
                        "dic": "CZ12345678",
                        "ico": "12345678",
                        "id": 1,
                        "is_archived": false,
                        "name": "Stavby Novák s.r.o.",
                        "postal_code": "702 00"
                      },
                      {
                        "address_line1": null,
                        "address_line2": null,
                        "city": null,
                        "contact_email": null,
                        "contact_name": null,
                        "contact_phone": null,
                        "country": "CZ",
                        "created_at": "2026-09-02T09:00:00Z",
                        "dic": null,
                        "ico": null,
                        "id": 2,
                        "is_archived": false,
                        "name": "Jan Dvořák",
                        "postal_code": null
                      }
                    ]
                  }
                },
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/Client"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Lists the company's clients, by name.",
        "tags": [
          "Clients"
        ]
      },
      "post": {
        "description": "A name is required and must be new at the company; everything else may be filled in later on the web, where the form can look a Czech company up by its registration number. Managers only.",
        "operationId": "postApiClients",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "A new client.",
                "properties": {
                  "address_line1": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "address_line2": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "city": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "contact_email": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "contact_name": {
                    "description": "The client's own contact person, who a worker rings about a job when the project names no site contact of its own.",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "contact_phone": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "country": {
                    "description": "country is a two-letter code; \"CZ\" unless given.",
                    "type": "string"
                  },
                  "dic": {
                    "description": "dic is the VAT number, \"CZ12345678\". Null when not VAT registered.",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "ico": {
                    "description": "ico is the Czech company registration number, eight digits with any leading zeros. Null for a private individual.",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "name": {
                    "type": "string"
                  },
                  "postal_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "address_line1": null,
                      "address_line2": null,
                      "city": null,
                      "contact_email": null,
                      "contact_name": null,
                      "contact_phone": null,
                      "country": "CZ",
                      "created_at": "2026-09-02T09:00:00Z",
                      "dic": null,
                      "ico": null,
                      "id": 2,
                      "is_archived": false,
                      "name": "Jan Dvořák",
                      "postal_code": null
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: client name is required; invalid JSON body."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Adds a client to the company.",
        "tags": [
          "Clients"
        ]
      }
    },
    "/api/clients/{id}": {
      "get": {
        "description": "Another company's is not found.",
        "operationId": "getApiClientsId",
        "parameters": [
          {
            "description": "The client's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "address_line1": "Průmyslová 12",
                      "address_line2": null,
                      "city": "Ostrava",
                      "contact_email": "petr@stavbynovak.test",
                      "contact_name": "Petr Novák",
                      "contact_phone": "+420777123456",
                      "country": "CZ",
                      "created_at": "2026-09-01T09:00:00Z",
                      "dic": "CZ12345678",
                      "ico": "12345678",
                      "id": 1,
                      "is_archived": false,
                      "name": "Stavby Novák s.r.o.",
                      "postal_code": "702 00"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "One client.",
        "tags": [
          "Clients"
        ]
      }
    },
    "/api/dashboard": {
      "get": {
        "description": "A value that is not a number is a 400 rather than quietly the default: the app sent something it did not mean.",
        "operationId": "getApiDashboard",
        "parameters": [
          {
            "in": "query",
            "name": "weeks",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example1": {
                    "summary": "A worker's",
                    "value": {
                      "days": [
                        "2026-09-07",
                        "2026-09-08",
                        "2026-09-09",
                        "2026-09-10",
                        "2026-09-11",
                        "2026-09-12",
                        "2026-09-13"
                      ],
                      "role": "worker",
                      "today": "2026-09-09",
                      "worker": {
                        "awaiting_you": [
                          {
                            "can_take_from": "2026-09-10T11:00:00Z",
                            "can_take_until": "2026-09-10T15:00:00Z",
                            "collides_from": "2026-09-10T05:00:00Z",
                            "collides_until": "2026-09-10T11:00:00Z",
                            "collides_with_company": null,
                            "collides_with_title": "Depot fit-out",
                            "created_at": "2026-10-28T13:33:30.733171Z",
                            "expires_at": "2026-09-10T07:00:00Z",
                            "full": false,
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 1,
                            "invited_at": "2026-09-09T08:00:00Z",
                            "membership_id": 2,
                            "origin": "staffed",
                            "rate_amount": 4200000,
                            "rate_currency": "CZK",
                            "responded_at": null,
                            "role_id": 1,
                            "role_label": "crane op",
                            "status": "pending",
                            "takes_from": null,
                            "takes_until": null,
                            "timeslot": {
                              "accepted": 0,
                              "created_at": "2026-10-28T13:33:30.731584Z",
                              "day_id": 1,
                              "ends_at": "2026-09-10T15:00:00Z",
                              "gig_id": 1,
                              "id": 1,
                              "pending": 0,
                              "required_workers": 1,
                              "role_label": "crane op",
                              "starts_at": "2026-09-10T07:00:00Z"
                            },
                            "timeslot_id": 1
                          },
                          {
                            "can_take_from": null,
                            "can_take_until": null,
                            "collides_from": null,
                            "collides_until": null,
                            "collides_with_company": null,
                            "collides_with_title": null,
                            "created_at": "2026-10-28T13:33:30.781699Z",
                            "expires_at": "2026-09-14T07:00:00Z",
                            "full": false,
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 9,
                            "invited_at": "2026-09-09T08:00:00Z",
                            "membership_id": 2,
                            "origin": "staffed",
                            "rate_amount": null,
                            "rate_currency": null,
                            "responded_at": null,
                            "role_id": 11,
                            "role_label": null,
                            "status": "pending",
                            "takes_from": null,
                            "takes_until": null,
                            "timeslot": {
                              "accepted": 0,
                              "created_at": "2026-10-28T13:33:30.78101Z",
                              "day_id": 11,
                              "ends_at": "2026-09-14T15:00:00Z",
                              "gig_id": 1,
                              "id": 11,
                              "pending": 0,
                              "required_workers": 1,
                              "role_label": null,
                              "starts_at": "2026-09-14T07:00:00Z"
                            },
                            "timeslot_id": 11
                          },
                          {
                            "can_take_from": null,
                            "can_take_until": null,
                            "collides_from": null,
                            "collides_until": null,
                            "collides_with_company": null,
                            "collides_with_title": null,
                            "created_at": "2026-10-28T13:33:30.783787Z",
                            "expires_at": "2026-09-15T07:00:00Z",
                            "full": true,
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 10,
                            "invited_at": "2026-09-09T08:00:00Z",
                            "membership_id": 2,
                            "origin": "staffed",
                            "rate_amount": null,
                            "rate_currency": null,
                            "responded_at": null,
                            "role_id": 12,
                            "role_label": null,
                            "status": "pending",
                            "takes_from": null,
                            "takes_until": null,
                            "timeslot": {
                              "accepted": 1,
                              "created_at": "2026-10-28T13:33:30.783093Z",
                              "day_id": 12,
                              "ends_at": "2026-09-15T15:00:00Z",
                              "gig_id": 1,
                              "id": 12,
                              "pending": 0,
                              "required_workers": 1,
                              "role_label": null,
                              "starts_at": "2026-09-15T07:00:00Z"
                            },
                            "timeslot_id": 12
                          }
                        ],
                        "expiring_certificates": [
                          {
                            "days_left": 12,
                            "expired": false,
                            "expires_on": "2026-09-21",
                            "id": 1,
                            "kind": "weld_iso9606",
                            "label": null,
                            "name": "Welder qualification test (EN ISO 9606)"
                          }
                        ],
                        "invitation_summaries": [
                          {
                            "count": 3,
                            "first_day": "2026-09-10",
                            "full_count": 1,
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "last_day": "2026-09-15",
                            "minutes": 1440,
                            "shift_summary": "Shifts Mon - Tue, Thu, 07:00 - 15:00",
                            "unrated_minutes": 960,
                            "would_earn": [
                              {
                                "amount": 33600000,
                                "currency": "CZK"
                              }
                            ]
                          }
                        ],
                        "later": [
                          {
                            "can_take_from": null,
                            "can_take_until": null,
                            "collides_from": null,
                            "collides_until": null,
                            "collides_with_company": null,
                            "collides_with_title": null,
                            "created_at": "2026-10-28T13:33:30.751859Z",
                            "expires_at": "2026-09-16T07:00:00Z",
                            "full": true,
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 5,
                            "invited_at": "2026-09-09T08:00:00Z",
                            "membership_id": 2,
                            "origin": "staffed",
                            "rate_amount": 4200000,
                            "rate_currency": "CZK",
                            "responded_at": "2026-09-09T08:00:00Z",
                            "role_id": 5,
                            "role_label": null,
                            "status": "accepted",
                            "takes_from": null,
                            "takes_until": null,
                            "timeslot": {
                              "accepted": 1,
                              "created_at": "2026-10-28T13:33:30.751116Z",
                              "day_id": 5,
                              "ends_at": "2026-09-16T15:00:00Z",
                              "gig_id": 1,
                              "id": 5,
                              "pending": 0,
                              "required_workers": 1,
                              "role_label": null,
                              "starts_at": "2026-09-16T07:00:00Z"
                            },
                            "timeslot_id": 5
                          }
                        ],
                        "open_shifts": [
                          {
                            "accepted": 0,
                            "can_take_from": "2026-09-13T09:00:00Z",
                            "can_take_until": "2026-09-13T15:00:00Z",
                            "collides_from": "2026-09-13T05:00:00Z",
                            "collides_until": "2026-09-13T09:00:00Z",
                            "collides_with_company": "Beta Construction",
                            "collides_with_title": "Beta depot",
                            "created_at": "2026-10-28T13:33:30.755612Z",
                            "day_id": 6,
                            "ends_at": "2026-09-13T15:00:00Z",
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 6,
                            "mine_status": "",
                            "pending": 0,
                            "rate_amount": 5000000,
                            "rate_currency": "CZK",
                            "required_workers": 1,
                            "role_id": 6,
                            "role_label": "crane op",
                            "starts_at": "2026-09-13T07:00:00Z",
                            "taking_confirms": true
                          },
                          {
                            "accepted": 0,
                            "can_take_from": "2026-09-13T09:00:00Z",
                            "can_take_until": "2026-09-13T15:00:00Z",
                            "collides_from": "2026-09-13T05:00:00Z",
                            "collides_until": "2026-09-13T09:00:00Z",
                            "collides_with_company": "Beta Construction",
                            "collides_with_title": "Beta depot",
                            "created_at": "2026-10-28T13:33:30.757429Z",
                            "day_id": 6,
                            "ends_at": "2026-09-13T15:00:00Z",
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 7,
                            "mine_status": "",
                            "pending": 0,
                            "rate_amount": null,
                            "rate_currency": null,
                            "required_workers": 1,
                            "role_id": 7,
                            "role_label": "crane op",
                            "starts_at": "2026-09-13T07:00:00Z",
                            "taking_confirms": false
                          }
                        ],
                        "still_to_earn": [
                          {
                            "amount": 92400000,
                            "currency": "CZK"
                          }
                        ],
                        "still_to_earn_unrated_minutes": 0,
                        "this_week": [
                          {
                            "can_take_from": null,
                            "can_take_until": null,
                            "collides_from": null,
                            "collides_until": null,
                            "collides_with_company": null,
                            "collides_with_title": null,
                            "created_at": "2026-10-28T13:33:30.736907Z",
                            "expires_at": "2026-09-10T05:00:00Z",
                            "full": true,
                            "gig_id": 2,
                            "gig_title": "Depot fit-out",
                            "id": 2,
                            "invited_at": "2026-09-09T08:00:00Z",
                            "membership_id": 2,
                            "origin": "staffed",
                            "rate_amount": 4200000,
                            "rate_currency": "CZK",
                            "responded_at": "2026-09-09T08:00:00Z",
                            "role_id": 2,
                            "role_label": null,
                            "status": "accepted",
                            "takes_from": null,
                            "takes_until": null,
                            "timeslot": {
                              "accepted": 1,
                              "created_at": "2026-10-28T13:33:30.736124Z",
                              "day_id": 2,
                              "ends_at": "2026-09-10T11:00:00Z",
                              "gig_id": 2,
                              "id": 2,
                              "pending": 0,
                              "required_workers": 1,
                              "role_label": null,
                              "starts_at": "2026-09-10T05:00:00Z"
                            },
                            "timeslot_id": 2
                          },
                          {
                            "can_take_from": null,
                            "can_take_until": null,
                            "collides_from": null,
                            "collides_until": null,
                            "collides_with_company": null,
                            "collides_with_title": null,
                            "created_at": "2026-10-28T13:33:30.741932Z",
                            "expires_at": "2026-09-11T07:00:00Z",
                            "full": true,
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 3,
                            "invited_at": "2026-09-09T08:00:00Z",
                            "membership_id": 2,
                            "origin": "staffed",
                            "rate_amount": 4200000,
                            "rate_currency": "CZK",
                            "responded_at": "2026-09-09T08:00:00Z",
                            "role_id": 3,
                            "role_label": null,
                            "status": "accepted",
                            "takes_from": null,
                            "takes_until": null,
                            "timeslot": {
                              "accepted": 1,
                              "created_at": "2026-10-28T13:33:30.741178Z",
                              "day_id": 3,
                              "ends_at": "2026-09-11T15:00:00Z",
                              "gig_id": 1,
                              "id": 3,
                              "pending": 0,
                              "required_workers": 1,
                              "role_label": null,
                              "starts_at": "2026-09-11T07:00:00Z"
                            },
                            "timeslot_id": 3
                          },
                          {
                            "can_take_from": null,
                            "can_take_until": null,
                            "collides_from": null,
                            "collides_until": null,
                            "collides_with_company": null,
                            "collides_with_title": null,
                            "created_at": "2026-10-28T13:33:30.746586Z",
                            "expires_at": "2026-09-12T07:00:00Z",
                            "full": false,
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 4,
                            "invited_at": "2026-09-09T08:00:00Z",
                            "membership_id": 2,
                            "origin": "staffed",
                            "rate_amount": 4200000,
                            "rate_currency": "CZK",
                            "responded_at": "2026-09-09T08:00:00Z",
                            "role_id": 4,
                            "role_label": null,
                            "status": "cancelled",
                            "takes_from": null,
                            "takes_until": null,
                            "timeslot": {
                              "accepted": 0,
                              "created_at": "2026-10-28T13:33:30.745806Z",
                              "day_id": 4,
                              "ends_at": "2026-09-12T15:00:00Z",
                              "gig_id": 1,
                              "id": 4,
                              "pending": 0,
                              "required_workers": 1,
                              "role_label": null,
                              "starts_at": "2026-09-12T07:00:00Z"
                            },
                            "timeslot_id": 4
                          }
                        ],
                        "worked_this_month": {
                          "earned": [
                            {
                              "amount": 16800000,
                              "currency": "CZK"
                            }
                          ],
                          "minutes": 240,
                          "unrated_minutes": 0
                        },
                        "worked_this_week": {
                          "earned": [
                            {
                              "amount": 16800000,
                              "currency": "CZK"
                            }
                          ],
                          "minutes": 240,
                          "unrated_minutes": 0
                        }
                      }
                    }
                  },
                  "example2": {
                    "summary": "A manager's",
                    "value": {
                      "days": [
                        "2026-09-07",
                        "2026-09-08",
                        "2026-09-09",
                        "2026-09-10",
                        "2026-09-11",
                        "2026-09-12",
                        "2026-09-13"
                      ],
                      "manager": {
                        "attention": [
                          {
                            "accepted": 0,
                            "created_at": "2026-10-28T13:33:30.731584Z",
                            "day_id": 1,
                            "ends_at": "2026-09-10T15:00:00Z",
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 1,
                            "pending": 1,
                            "required_workers": 1,
                            "role_label": "crane op",
                            "starts_at": "2026-09-10T07:00:00Z"
                          },
                          {
                            "accepted": 0,
                            "created_at": "2026-10-28T13:33:30.76944Z",
                            "day_id": 1,
                            "ends_at": "2026-09-10T22:00:00Z",
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 9,
                            "pending": 0,
                            "required_workers": 2,
                            "role_label": null,
                            "starts_at": "2026-09-10T14:00:00Z"
                          },
                          {
                            "accepted": 0,
                            "created_at": "2026-10-28T13:33:30.745806Z",
                            "day_id": 4,
                            "ends_at": "2026-09-12T15:00:00Z",
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 4,
                            "pending": 0,
                            "required_workers": 1,
                            "role_label": null,
                            "starts_at": "2026-09-12T07:00:00Z"
                          },
                          {
                            "accepted": 0,
                            "created_at": "2026-10-28T13:33:30.755612Z",
                            "day_id": 6,
                            "ends_at": "2026-09-13T15:00:00Z",
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 6,
                            "pending": 0,
                            "required_workers": 1,
                            "role_label": "crane op",
                            "starts_at": "2026-09-13T07:00:00Z"
                          },
                          {
                            "accepted": 0,
                            "created_at": "2026-10-28T13:33:30.757429Z",
                            "day_id": 6,
                            "ends_at": "2026-09-13T15:00:00Z",
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 7,
                            "pending": 0,
                            "required_workers": 1,
                            "role_label": "crane op",
                            "starts_at": "2026-09-13T07:00:00Z"
                          },
                          {
                            "accepted": 0,
                            "created_at": "2026-10-28T13:33:30.78101Z",
                            "day_id": 11,
                            "ends_at": "2026-09-14T15:00:00Z",
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 11,
                            "pending": 1,
                            "required_workers": 1,
                            "role_label": null,
                            "starts_at": "2026-09-14T07:00:00Z"
                          },
                          {
                            "accepted": 1,
                            "created_at": "2026-10-28T13:33:30.783093Z",
                            "day_id": 12,
                            "ends_at": "2026-09-15T15:00:00Z",
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "id": 12,
                            "pending": 1,
                            "required_workers": 1,
                            "role_label": null,
                            "starts_at": "2026-09-15T07:00:00Z"
                          }
                        ],
                        "attention_summaries": [
                          {
                            "accepted": 1,
                            "count": 7,
                            "first_day": "2026-09-10",
                            "gig_id": 1,
                            "gig_title": "Warehouse roof",
                            "imminent": true,
                            "last_day": "2026-09-15",
                            "pending": 3,
                            "required": 8,
                            "shift_summary": "Shifts Mon - Tue, Sat, 07:00 - 15:00, Thu 07:00 - 15:00, 14:00 - 22:00, Sun 07:00 - 15:00, 07:00 - 15:00",
                            "short": 7
                          }
                        ],
                        "hours_to_check": [
                          {
                            "entry": {
                              "clock_in_at": "2026-09-02T07:00:00Z",
                              "clock_out_at": "2026-09-02T22:00:00Z",
                              "corrected_at": null,
                              "created_at": "2026-10-28T13:33:30.768583Z",
                              "earned_amount": 57000000,
                              "gig_id": 1,
                              "id": 3,
                              "membership_id": 5,
                              "rate_amount": 3800000,
                              "rate_currency": "CZK",
                              "source": "manual",
                              "timeslot_id": 8
                            },
                            "gig_title": "Warehouse roof",
                            "missing_clock_out": false,
                            "outside_shift": true,
                            "slot_ends_at": "2026-09-02T15:00:00Z",
                            "slot_starts_at": "2026-09-02T07:00:00Z",
                            "worker_name": "Second Alpha Stavby"
                          },
                          {
                            "entry": {
                              "clock_in_at": "2026-09-03T07:00:00Z",
                              "clock_out_at": "2026-09-03T22:00:00Z",
                              "corrected_at": null,
                              "created_at": "2026-10-28T13:33:30.779978Z",
                              "earned_amount": null,
                              "gig_id": 1,
                              "id": 4,
                              "membership_id": 6,
                              "rate_amount": null,
                              "rate_currency": null,
                              "source": "manual",
                              "timeslot_id": 10
                            },
                            "gig_title": "Warehouse roof",
                            "missing_clock_out": false,
                            "outside_shift": true,
                            "slot_ends_at": "2026-09-03T15:00:00Z",
                            "slot_starts_at": "2026-09-03T07:00:00Z",
                            "worker_name": "Unrated Alpha Stavby"
                          },
                          {
                            "entry": {
                              "clock_in_at": "2026-09-08T06:00:00Z",
                              "clock_out_at": null,
                              "corrected_at": null,
                              "created_at": "2026-10-28T13:33:30.763173Z",
                              "earned_amount": 0,
                              "gig_id": 1,
                              "id": 2,
                              "membership_id": 2,
                              "rate_amount": 4200000,
                              "rate_currency": "CZK",
                              "source": "manual",
                              "timeslot_id": null
                            },
                            "gig_title": "Warehouse roof",
                            "missing_clock_out": true,
                            "outside_shift": false,
                            "slot_ends_at": null,
                            "slot_starts_at": null,
                            "worker_name": "Worker Alpha Stavby"
                          }
                        ],
                        "turned_down": [
                          {
                            "created_at": "2026-10-28T13:33:30.770123Z",
                            "expires_at": "2026-09-10T14:00:00Z",
                            "gig_title": "Warehouse roof",
                            "id": 7,
                            "invited_at": "2026-09-09T08:00:00Z",
                            "membership_id": 5,
                            "origin": "staffed",
                            "rate_amount": 3800000,
                            "rate_currency": "CZK",
                            "responded_at": "2026-09-09T08:00:00Z",
                            "role_id": 9,
                            "role_label": null,
                            "status": "withdrawn",
                            "takes_from": null,
                            "takes_until": null,
                            "timeslot": {
                              "accepted": 0,
                              "created_at": "2026-10-28T13:33:30.76944Z",
                              "day_id": 1,
                              "ends_at": "2026-09-10T22:00:00Z",
                              "gig_id": 1,
                              "id": 9,
                              "pending": 0,
                              "required_workers": 2,
                              "role_label": null,
                              "starts_at": "2026-09-10T14:00:00Z"
                            },
                            "timeslot_id": 9,
                            "worker_name": "Second Alpha Stavby"
                          }
                        ],
                        "week": [
                          {
                            "accepted": 0,
                            "day": "2026-09-07",
                            "pending": 0,
                            "required": 0,
                            "short": 0,
                            "slots": 0
                          },
                          {
                            "accepted": 0,
                            "day": "2026-09-08",
                            "pending": 0,
                            "required": 0,
                            "short": 0,
                            "slots": 0
                          },
                          {
                            "accepted": 0,
                            "day": "2026-09-09",
                            "pending": 0,
                            "required": 0,
                            "short": 0,
                            "slots": 0
                          },
                          {
                            "accepted": 1,
                            "day": "2026-09-10",
                            "pending": 1,
                            "required": 4,
                            "short": 3,
                            "slots": 3
                          },
                          {
                            "accepted": 1,
                            "day": "2026-09-11",
                            "pending": 0,
                            "required": 1,
                            "short": 0,
                            "slots": 1
                          },
                          {
                            "accepted": 0,
                            "day": "2026-09-12",
                            "pending": 0,
                            "required": 1,
                            "short": 1,
                            "slots": 1
                          },
                          {
                            "accepted": 0,
                            "day": "2026-09-13",
                            "pending": 0,
                            "required": 2,
                            "short": 2,
                            "slots": 2
                          }
                        ],
                        "worked_this_week": {
                          "earned": [
                            {
                              "amount": 16800000,
                              "currency": "CZK"
                            }
                          ],
                          "minutes": 240,
                          "unrated_minutes": 0
                        }
                      },
                      "role": "manager",
                      "today": "2026-09-09"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Dashboard"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: weeks must be a whole number."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Returns the caller's dashboard at the active company.",
        "tags": [
          "Your week"
        ]
      }
    },
    "/api/devices": {
      "delete": {
        "description": "Without it, a worker who signs out — or hands the phone on — keeps receiving shift notifications for a session they no longer have.\n\nA session only: an API key is refused with 403.",
        "operationId": "deleteApiDevices",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "token": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Done, with no body."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: a device token is required; invalid JSON body."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Drops this phone's token, on sign-out.",
        "tags": [
          "Shifts and answers"
        ]
      },
      "post": {
        "description": "A session only: an API key is refused with 403.",
        "operationId": "postApiDevices",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "language": {
                    "type": "string"
                  },
                  "platform": {
                    "type": "string"
                  },
                  "token": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Done, with no body."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          }
        },
        "summary": "Records an FCM token so this phone can be reached.",
        "tags": [
          "Shifts and answers"
        ]
      }
    },
    "/api/gigs": {
      "get": {
        "description": "A manager gets every live project. A worker gets the ones they have a part in, newest first, and after those the ones with an open shift they could take; each row says which with is_assigned, is_invited and shifts_to_take. Archived projects are not listed, though each still answers at its own address.\n\nall=1 lists them at every company and role the caller holds, each entry's projects as that entry would list them, with company on each row naming the entry: send its entry as X-Werzeick-Company to act on the project. Somebody who both manages and works at one company sees such a project once for each. It cannot be combined with X-Werzeick-Company.",
        "operationId": "getApiGigs",
        "parameters": [
          {
            "in": "query",
            "name": "all",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "archived_at": null,
                      "client_contact_email": "petra.markova@kaufland.cz",
                      "client_contact_name": "Petra Marková",
                      "client_contact_phone": "+420602123456",
                      "client_id": 7,
                      "client_name": "Kaufland Česká republika",
                      "created_at": "2026-09-07T16:06:11.113221+02:00",
                      "expected_minutes": 480,
                      "expenses": [],
                      "geofence_radius_m": 150,
                      "id": 3,
                      "is_assigned": true,
                      "is_invited": false,
                      "job_contact": {
                        "company": null,
                        "name": "Jana Horáková, has the keys",
                        "phone": "+420777111222"
                      },
                      "latitude": 49.8209,
                      "longitude": 18.2625,
                      "owner_id": 4,
                      "project_manager": {
                        "email": null,
                        "name": "Martin Dvořák",
                        "phone": "+420603456789"
                      },
                      "shifts_to_take": 2,
                      "site_address": "Gate 3, Průmyslová 1472/12, 702 00 Ostrava",
                      "site_outline": [
                        {
                          "lat": 49.8205,
                          "lon": 18.2615
                        },
                        {
                          "lat": 49.8205,
                          "lon": 18.2636
                        },
                        {
                          "lat": 49.82131,
                          "lon": 18.2636
                        },
                        {
                          "lat": 49.82131,
                          "lon": 18.2615
                        }
                      ],
                      "title": "Foundation pour"
                    }
                  }
                },
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/Project"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: all=1 reads every company: leave out X-Werzeick-Company."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Lists the caller's projects at the company they are acting in.",
        "tags": [
          "Projects and expenses"
        ]
      },
      "post": {
        "description": "Managers only.",
        "operationId": "postApiGigs",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "The JSON body of POST /api/gigs.\n\nThere is deliberately no owner_id or org_id here: the owner is the acting manager's own membership and the organization comes from the session. A client that could name either could create work in someone else's company.",
                "properties": {
                  "client_id": {
                    "format": "int64",
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "geofence_radius_m": {
                    "format": "int64",
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "latitude": {
                    "format": "double",
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "longitude": {
                    "format": "double",
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "site_outline": {
                    "items": {
                      "$ref": "#/components/schemas/Point"
                    },
                    "type": "array"
                  },
                  "title": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "archived_at": null,
                      "client_contact_email": null,
                      "client_contact_name": null,
                      "client_contact_phone": null,
                      "client_id": null,
                      "client_name": null,
                      "created_at": "2026-09-07T16:05:07.353539+02:00",
                      "expected_minutes": null,
                      "expenses": [
                        {
                          "cost_amount": 1250000,
                          "cost_currency": "CZK",
                          "created_at": "2026-09-07T12:52:03.941837+02:00",
                          "created_by_membership_id": 1,
                          "created_by_name": "Tereza Nováková",
                          "description": "Tiles",
                          "gig_id": 2,
                          "id": 4
                        }
                      ],
                      "geofence_radius_m": null,
                      "id": 2,
                      "is_assigned": false,
                      "is_invited": true,
                      "job_contact": null,
                      "latitude": null,
                      "longitude": null,
                      "owner_id": 1,
                      "project_manager": {
                        "email": "tereza.novakova@example.com",
                        "name": "Tereza Nováková",
                        "phone": null
                      },
                      "shifts_to_take": 0,
                      "site_address": null,
                      "site_outline": null,
                      "title": "Roof replacement"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; title is required."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Makes a project at the caller's company, with the caller as its manager.",
        "tags": [
          "Projects and expenses"
        ]
      }
    },
    "/api/gigs/{id}": {
      "get": {
        "description": "A project the caller cannot list, another company's included, answers 404: it does not exist for them.",
        "operationId": "getApiGigsId",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "archived_at": null,
                      "client_contact_email": "petra.markova@kaufland.cz",
                      "client_contact_name": "Petra Marková",
                      "client_contact_phone": "+420602123456",
                      "client_id": 7,
                      "client_name": "Kaufland Česká republika",
                      "created_at": "2026-09-07T16:06:11.113221+02:00",
                      "expected_minutes": 480,
                      "expenses": [],
                      "geofence_radius_m": 150,
                      "id": 3,
                      "is_assigned": true,
                      "is_invited": false,
                      "job_contact": {
                        "company": null,
                        "name": "Jana Horáková, has the keys",
                        "phone": "+420777111222"
                      },
                      "latitude": 49.8209,
                      "longitude": 18.2625,
                      "owner_id": 4,
                      "project_manager": {
                        "email": null,
                        "name": "Martin Dvořák",
                        "phone": "+420603456789"
                      },
                      "shifts_to_take": 2,
                      "site_address": "Gate 3, Průmyslová 1472/12, 702 00 Ostrava",
                      "site_outline": [
                        {
                          "lat": 49.8205,
                          "lon": 18.2615
                        },
                        {
                          "lat": 49.8205,
                          "lon": 18.2636
                        },
                        {
                          "lat": 49.82131,
                          "lon": 18.2636
                        },
                        {
                          "lat": 49.82131,
                          "lon": 18.2615
                        }
                      ],
                      "title": "Foundation pour"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid gig id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: gig not found."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "One project, in the same shape the list gives it.",
        "tags": [
          "Projects and expenses"
        ]
      }
    },
    "/api/gigs/{id}/calendar": {
      "get": {
        "description": "from and to are calendar days, \"2026-10-27\", both inclusive; without them the four weeks from today on site. A range may be up to a year. A day is listed only if the project runs on it, so the gaps in a project that pauses are simply absent days. Who is on each shift is the shift's own endpoint. Managers only; a worker's week is the dashboard.",
        "operationId": "getApiGigsIdCalendar",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "from",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "to",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": [
                      {
                        "day": "2026-09-10",
                        "day_id": 1,
                        "shifts": [
                          {
                            "accepted": 1,
                            "created_at": "2026-09-07T09:00:00Z",
                            "day_id": 1,
                            "ends_at": "2026-09-10T13:00:00Z",
                            "gig_id": 1,
                            "id": 1,
                            "is_open": false,
                            "offered_rate_multiplier": null,
                            "open_auto_accept": false,
                            "pending": 1,
                            "required_workers": 3,
                            "role_label": "1× crane op, 2× labourer",
                            "roles": [
                              {
                                "accepted": 1,
                                "headcount": 1,
                                "id": 1,
                                "label": "crane op",
                                "pending": 0,
                                "rate_amount": 5200000,
                                "rate_currency": "CZK"
                              },
                              {
                                "accepted": 0,
                                "headcount": 2,
                                "id": 2,
                                "label": "labourer",
                                "pending": 1,
                                "rate_amount": null,
                                "rate_currency": null
                              }
                            ],
                            "short": 2,
                            "starts_at": "2026-09-10T05:00:00Z"
                          }
                        ]
                      },
                      {
                        "day": "2026-09-11",
                        "day_id": 2,
                        "shifts": []
                      }
                    ]
                  }
                },
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/CalendarDay"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: from must be a calendar day, YYYY-MM-DD; invalid {thing} id; the range must run forwards and be at most a year; to must be a calendar day, YYYY-MM-DD."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "A project's days in a range, each with its shifts, roles and staffing counts.",
        "tags": [
          "Planning a project's calendar"
        ]
      }
    },
    "/api/gigs/{id}/days": {
      "post": {
        "description": "Days already on it are left as they are, so sending a range twice is harmless. Laying a shift on a day schedules the day too, so this is only needed for a day that runs with no shift planned yet. Up to a year of days in one request. The answer is the project's whole calendar. Managers only.",
        "operationId": "postApiGigsIdDays",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "Names calendar days, \"2026-10-27\".",
                "properties": {
                  "confirm": {
                    "description": "confirm is read by the removal only: without it nothing is removed and the answer says what would be.",
                    "type": "boolean"
                  },
                  "days": {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/ScheduledDay"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id; name at least one day."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Puts days on a project's calendar.",
        "tags": [
          "Planning a project's calendar"
        ]
      }
    },
    "/api/gigs/{id}/days/remove": {
      "post": {
        "description": "Anybody with a live place on them — accepted, or invited and not yet answered — is taken off and told once, however many of their shifts went; anybody who had accepted is emailed as well. So it asks first: without confirm nothing changes and the answer says how many people that is. A shift that has started is a record and its day cannot be removed. Managers only.",
        "operationId": "postApiGigsIdDaysRemove",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "Names calendar days, \"2026-10-27\".",
                "properties": {
                  "confirm": {
                    "description": "confirm is read by the removal only: without it nothing is removed and the answer says what would be.",
                    "type": "boolean"
                  },
                  "days": {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RemovedDays"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id; name at least one day."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Takes days off a project's calendar, with their shifts.",
        "tags": [
          "Planning a project's calendar"
        ]
      }
    },
    "/api/gigs/{id}/earnings": {
      "get": {
        "description": "Its own endpoint rather than a field on the gig, for two reasons. The list screen has no use for it and should not pay for it on every row; and a tool attached or an hour corrected changes this without changing the project, so the app can re-read one without the other.\n\nThe two figures come from two different tables, each carrying its own rate snapshot, and neither is ever rebuilt from the membership rate.",
        "operationId": "getApiGigsIdEarnings",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "earned": [
                        {
                          "amount": 33600000,
                          "currency": "CZK"
                        }
                      ],
                      "earned_unrated_minutes": 0,
                      "running": null,
                      "upcoming": [
                        {
                          "amount": 50400000,
                          "currency": "CZK"
                        }
                      ],
                      "upcoming_unrated_minutes": 360
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Earnings"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: gig not found."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Answers \"what has this project paid me, and what will it\".",
        "tags": [
          "Projects and expenses"
        ]
      }
    },
    "/api/gigs/{id}/expenses": {
      "post": {
        "description": "Workers may call this, not only managers: the app is where somebody standing in a builders' merchant records what they just bought. The service decides who may do what.",
        "operationId": "postApiGigsIdExpenses",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "The JSON body of a create or an update.\n\nMoney crosses the wire as the raw int64 with its currency, never pre-formatted and never a float. There is no author field: the acting membership books it, and a client that could name the author could book a cost against somebody else.",
                "properties": {
                  "cost_amount": {
                    "description": "Money: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
                    "format": "int64",
                    "type": "integer"
                  },
                  "cost_currency": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "cost_amount": 1250000,
                      "cost_currency": "CZK",
                      "created_at": "2026-09-07T12:52:03.941837+02:00",
                      "created_by_membership_id": 1,
                      "created_by_name": "Tereza Nováková",
                      "description": "Tiles",
                      "gig_id": 2,
                      "id": 4
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Expense"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: expense not found; gig not found."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Books a cost against a project.",
        "tags": [
          "Projects and expenses"
        ]
      }
    },
    "/api/gigs/{id}/expenses/{expenseID}": {
      "delete": {
        "operationId": "deleteApiGigsIdExpensesExpenseid",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "The expense's id.",
            "in": "path",
            "name": "expenseID",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done, with no body."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: expense not found; gig not found."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Deletes a cost.",
        "tags": [
          "Projects and expenses"
        ]
      },
      "patch": {
        "operationId": "patchApiGigsIdExpensesExpenseid",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "The expense's id.",
            "in": "path",
            "name": "expenseID",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "The JSON body of a create or an update.\n\nMoney crosses the wire as the raw int64 with its currency, never pre-formatted and never a float. There is no author field: the acting membership books it, and a client that could name the author could book a cost against somebody else.",
                "properties": {
                  "cost_amount": {
                    "description": "Money: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
                    "format": "int64",
                    "type": "integer"
                  },
                  "cost_currency": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "cost_amount": 1250000,
                      "cost_currency": "CZK",
                      "created_at": "2026-09-07T12:52:03.941837+02:00",
                      "created_by_membership_id": 1,
                      "created_by_name": "Tereza Nováková",
                      "description": "Tiles",
                      "gig_id": 2,
                      "id": 4
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Expense"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: expense not found; gig not found."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Corrects a cost already booked.",
        "tags": [
          "Projects and expenses"
        ]
      }
    },
    "/api/gigs/{id}/hours": {
      "get": {
        "description": "earned_amount is each entry's worth at its own rate, worked out here to the minute; an entry still on the clock has no clock-out and counts in no total. An archived project's hours are still listed. Managers only.",
        "operationId": "getApiGigsIdHours",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": [
                      {
                        "client_event_id": "a1f0c2f1-3d6b-4b9c-9f3e-6e2d9a4c1b11",
                        "clock_in_at": "2026-09-10T04:52:00Z",
                        "clock_out_at": "2026-09-10T13:06:00Z",
                        "corrected_at": null,
                        "created_at": "2026-09-10T04:52:03Z",
                        "earned_amount": 42813333,
                        "gig_id": 1,
                        "id": 1,
                        "membership_id": 2,
                        "rate_amount": 5200000,
                        "rate_currency": "CZK",
                        "recorded_by": "",
                        "slot_ends_at": "2026-09-10T13:00:00Z",
                        "slot_starts_at": "2026-09-10T05:00:00Z",
                        "source": "manual",
                        "timeslot_id": 1,
                        "worker_name": "Jana Horáková"
                      }
                    ]
                  }
                },
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/HoursRow"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Lists every time entry on a project, newest first.",
        "tags": [
          "A project's hours"
        ]
      }
    },
    "/api/gigs/{id}/invitations/batch": {
      "post": {
        "description": "Each pair becomes an ordinary invitation with its own rate and its own deadline; nothing records that they were made together. Somebody already live on a shift is skipped, not refused. More people than a shift has places, and a shift overlapping other work of theirs at this company, are allowed and said. Another project's shift, somebody who does not work shifts here, or a shift that has started refuses the whole batch and says which. Each person is told once. Send preview to see all of that without sending anything. Managers only.",
        "operationId": "postApiGigsIdInvitationsBatch",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "Asks several people to several shifts at once.",
                "properties": {
                  "membership_ids": {
                    "description": "membership_ids are the people, from the roster.",
                    "items": {
                      "format": "int64",
                      "type": "integer"
                    },
                    "type": "array"
                  },
                  "preview": {
                    "description": "preview says to work out what the batch would do and send nothing.",
                    "type": "boolean"
                  },
                  "role": {
                    "description": "role names the role, by label, on a shift that has several; a shift with one role takes the batch as that role whatever the name. Null where every shift has one role.",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "timeslot_ids": {
                    "description": "timeslot_ids are the shifts, all of this project.",
                    "items": {
                      "format": "int64",
                      "type": "integer"
                    },
                    "type": "array"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "clashes": [
                        {
                          "in_batch": false,
                          "membership_id": 3,
                          "shift": {
                            "ends_at": "2026-09-11T13:00:00Z",
                            "starts_at": "2026-09-11T05:00:00Z",
                            "timeslot_id": 2
                          },
                          "with": {
                            "ends_at": "2026-09-11T14:00:00Z",
                            "starts_at": "2026-09-11T10:00:00Z",
                            "timeslot_id": 7
                          },
                          "worker_name": "Petr Dvořák"
                        }
                      ],
                      "invitations": 3,
                      "over_filled": [
                        {
                          "invited": 2,
                          "places": 1,
                          "shift": {
                            "ends_at": "2026-09-11T13:00:00Z",
                            "starts_at": "2026-09-11T05:00:00Z",
                            "timeslot_id": 2
                          }
                        }
                      ],
                      "people": 2,
                      "preview": false,
                      "sent": [
                        {
                          "created_at": "2026-09-08T14:00:00Z",
                          "expires_at": "2026-09-11T05:00:00Z",
                          "id": 3,
                          "invited_at": "2026-09-08T14:00:00Z",
                          "membership_id": 2,
                          "origin": "staffed",
                          "rate_amount": 4200000,
                          "rate_currency": "CZK",
                          "responded_at": null,
                          "role_id": 3,
                          "role_label": null,
                          "status": "pending",
                          "takes_from": null,
                          "takes_until": null,
                          "timeslot_id": 2,
                          "worker_name": "Jana Horáková"
                        }
                      ],
                      "shifts": 2,
                      "skipped": [
                        {
                          "membership_id": 2,
                          "shift": {
                            "ends_at": "2026-09-10T13:00:00Z",
                            "starts_at": "2026-09-10T05:00:00Z",
                            "timeslot_id": 1
                          },
                          "worker_name": "Jana Horáková"
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/InvitationBatch"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Asks several people to several shifts of one project at once.",
        "tags": [
          "Staffing a project"
        ]
      }
    },
    "/api/gigs/{id}/invitations/respond": {
      "post": {
        "description": "It names a project and an action and nothing else. Which invitations those are is decided here, at the moment of the tap, from the caller's own membership — a client never sends a list of ids it believes are its own. Another company's project, or one with nothing waiting, answers 200 with zeros: there was nothing to do, and that is all anybody is told.",
        "operationId": "postApiGigsIdInvitationsRespond",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "action": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "answered": 2,
                      "colliding": 0,
                      "full": 1
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/AnsweredInvitations"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: action must be \"accept\" or \"decline\"; invalid JSON body; invalid project id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Answers every invitation the caller has not answered on one project: \"Accept all\" and \"Decline all\" beside a project's invitations.",
        "tags": [
          "Shifts and answers"
        ]
      }
    },
    "/api/gigs/{id}/people": {
      "get": {
        "description": "A shift that has started is a record and is not counted; somebody whose only shifts are over is on the project's hours, not here. Managers only.",
        "operationId": "getApiGigsIdPeople",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/PersonOnAProject"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Lists everyone with a live place on a project's shifts still to come: accepted, or asked and not yet answered, each once.",
        "tags": [
          "Staffing a project"
        ]
      }
    },
    "/api/gigs/{id}/people/{membershipID}/remove": {
      "post": {
        "description": "Every live place of theirs on a shift of the project still to come is cancelled in one go, and they are told once: one push, and one email listing the accepted shifts. Their places on other projects, and on a shift of this project that has started, are not touched. It asks first: without confirm nothing changes and the answer counts the places. Managers only.",
        "operationId": "postApiGigsIdPeopleMembershipidRemove",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "The person's membership id, as the roster lists it.",
            "in": "path",
            "name": "membershipID",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "A removal's consent.",
                "properties": {
                  "confirm": {
                    "description": "confirm says to go ahead. Without it nothing changes and the answer says what would.",
                    "type": "boolean"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TakenOffAProject"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Takes one person off every upcoming shift of a project at once.",
        "tags": [
          "Staffing a project"
        ]
      }
    },
    "/api/gigs/{id}/shifts": {
      "post": {
        "description": "The same clock times on each day, worked out day by day on site, so a week that crosses a change of the clocks is right on every day. The answer is each shift made, with its roles' ids, which an invitation names. Managers only; an archived project takes no new shifts.",
        "operationId": "postApiGigsIdShifts",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "One shift laid on each of several days.",
                "properties": {
                  "days": {
                    "description": "days are calendar days, \"2026-10-27\". Each gets a shift; a day not yet on the calendar is scheduled by it.",
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "ends_at": {
                    "type": "string"
                  },
                  "headcount": {
                    "description": "headcount is the places on a plain shift, read only when roles is empty.",
                    "format": "int64",
                    "type": "integer"
                  },
                  "offered_rate_multiplier": {
                    "description": "offered_rate_multiplier raises each taker's usual rate on an open shift, with four implied decimals: 15000 is 1.5×. Null offers the rates as they stand.\n\nA multiplier with four implied decimal places: 15000 is 1.5×.",
                    "format": "int64",
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "open": {
                    "description": "open advertises every shift to the company at once; open_auto_accept confirms a taker outright rather than as an applicant.",
                    "type": "boolean"
                  },
                  "open_auto_accept": {
                    "type": "boolean"
                  },
                  "roles": {
                    "description": "roles are the shift's roles; the headcount is their sum. One role with no label is a plain shift. Leave empty and send headcount for a plain shift instead.",
                    "items": {
                      "$ref": "#/components/schemas/RoleToAdd"
                    },
                    "type": "array"
                  },
                  "starts_at": {
                    "description": "starts_at and ends_at are clock times on site, \"07:00\" and \"15:30\". A shift that ends at or before it starts runs into the next day. The clock is kept on site whichever side of a change of the clocks a day falls.",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "accepted": 1,
                      "created_at": "2026-09-07T09:00:00Z",
                      "day_id": 1,
                      "ends_at": "2026-09-10T13:00:00Z",
                      "gig_id": 1,
                      "id": 1,
                      "is_open": false,
                      "offered_rate_multiplier": null,
                      "open_auto_accept": false,
                      "pending": 1,
                      "required_workers": 3,
                      "role_label": "1× crane op, 2× labourer",
                      "roles": [
                        {
                          "accepted": 1,
                          "headcount": 1,
                          "id": 1,
                          "label": "crane op",
                          "pending": 0,
                          "rate_amount": 5200000,
                          "rate_currency": "CZK"
                        },
                        {
                          "accepted": 0,
                          "headcount": 2,
                          "id": 2,
                          "label": "labourer",
                          "pending": 1,
                          "rate_amount": null,
                          "rate_currency": null
                        }
                      ],
                      "short": 2,
                      "starts_at": "2026-09-10T05:00:00Z"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/AddedShifts"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: ends_at must be a time of day, as 15:00; invalid JSON body; invalid {thing} id; name at least one day; starts_at must be a time of day, as 07:00, on a day written YYYY-MM-DD."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Lays one shift on each of the days named, with its roles.",
        "tags": [
          "Planning a project's calendar"
        ]
      }
    },
    "/api/gigs/{id}/shifts/move": {
      "post": {
        "description": "Clock times are kept on site. Everyone with an accepted place is asked again: their place goes back to pending with the new date as its deadline, they are told once, and anyone who had accepted is emailed. A shift that has started, or has an SOS running, stays where it is and is counted as skipped; a move that would land a shift in the past is refused whole. It asks first: without confirm nothing changes and the answer has the counts. Managers only.",
        "operationId": "postApiGigsIdShiftsMove",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "Moves shifts by whole days.",
                "properties": {
                  "confirm": {
                    "description": "confirm says to go ahead. Without it nothing changes and the answer says what would.",
                    "type": "boolean"
                  },
                  "days": {
                    "description": "days is the offset in whole days, forwards or back; never zero.",
                    "format": "int64",
                    "type": "integer"
                  },
                  "timeslot_ids": {
                    "description": "timeslot_ids are the shifts to move, all of this project.",
                    "items": {
                      "format": "int64",
                      "type": "integer"
                    },
                    "type": "array"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MovedShifts"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Moves shifts to new dates, by a number of whole days.",
        "tags": [
          "Planning a project's calendar"
        ]
      }
    },
    "/api/gigs/{id}/sos": {
      "post": {
        "description": "Name the shifts, or none for every short shift of the project that is not over, read now. A shift that filled, ended or has an SOS already is skipped and counted; a shift of another project refuses the lot. Managers only.",
        "operationId": "postApiGigsIdSos",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "Raises an SOS over a project's short shifts.",
                "properties": {
                  "auto_accept": {
                    "description": "auto_accept confirms a responder outright, up to the role's headcount; without it responders are applicants for the manager to approve.",
                    "type": "boolean"
                  },
                  "offered_rate_multiplier": {
                    "description": "offered_rate_multiplier raises each taker's usual rate, with four implied decimals. Null is the default, 15000: half as much again. The project's maximum hourly rate caps what comes out.\n\nA multiplier with four implied decimal places: 15000 is 1.5×.",
                    "format": "int64",
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "timeslot_ids": {
                    "description": "timeslot_ids are the shifts to cover, all of this project. Empty means every short shift of the project that is not over.",
                    "items": {
                      "format": "int64",
                      "type": "integer"
                    },
                    "type": "array"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "already_raised": 0,
                      "filled": 1,
                      "over": 0,
                      "raised": [
                        {
                          "id": 1,
                          "offered_rate_multiplier": 15000,
                          "opened_at": "2026-09-09T16:00:00Z",
                          "timeslot_id": 1,
                          "told": 0
                        }
                      ],
                      "told": 4
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/SOSOverAProject"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: a multiplier must be more than nothing; invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Raises one ordinary SOS on each of several short shifts of a project, in one go, and tells each person once.",
        "tags": [
          "Staffing a project"
        ]
      }
    },
    "/api/gigs/{id}/tools": {
      "get": {
        "operationId": "getApiGigsIdTools",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": [
                      {
                        "created_at": "2026-09-08T14:22:41.117204+02:00",
                        "gig_ids": [
                          3,
                          7
                        ],
                        "icon": "drill",
                        "id": 12,
                        "name": "SDS drill",
                        "quantity": null
                      },
                      {
                        "created_at": "2026-09-08T14:21:02.884113+02:00",
                        "gig_ids": [],
                        "icon": "other",
                        "id": 11,
                        "name": "Clamps",
                        "quantity": 6
                      }
                    ]
                  }
                },
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/Tool"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: tool not found."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Returns the caller's own tools attached to one project — what they have to load into the van for this job.",
        "tags": [
          "Tools"
        ]
      },
      "post": {
        "description": "Idempotent: attaching a tool that is already attached answers 204 rather than a conflict. Two taps on a building site is a bad radio, not a disagreement, and the second one meant the same as the first.",
        "operationId": "postApiGigsIdTools",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "Names the tool being taken to a project.",
                "properties": {
                  "tool_id": {
                    "format": "int64",
                    "type": "integer"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Done, with no body."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: tool not found."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Says \"I am taking this to that job\".",
        "tags": [
          "Tools"
        ]
      }
    },
    "/api/gigs/{id}/tools/{toolID}": {
      "delete": {
        "description": "Coming off one job must not cost the worker the entry.",
        "operationId": "deleteApiGigsIdToolsToolid",
        "parameters": [
          {
            "description": "The project's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "The tool's id.",
            "in": "path",
            "name": "toolID",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done, with no body."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: tool not found."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Removes the link and NOT the tool.",
        "tags": [
          "Tools"
        ]
      }
    },
    "/api/invitations": {
      "get": {
        "operationId": "getApiInvitations",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/Invitation"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Shows who has been invited but has not yet accepted.",
        "tags": [
          "Invitations to a company"
        ]
      },
      "post": {
        "description": "There is no org field: the company comes from the session, so a manager cannot invite into a firm they do not belong to.",
        "operationId": "postApiInvitations",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "first_name": {
                    "type": "string"
                  },
                  "is_manager": {
                    "description": "What they are invited to do: either, or both. At least one must be true.",
                    "type": "boolean"
                  },
                  "is_worker": {
                    "type": "boolean"
                  },
                  "last_name": {
                    "type": "string"
                  },
                  "rate_amount": {
                    "description": "Money crosses the wire as a raw int64 with 4 implied decimals plus its currency, never pre-formatted and never a float.\n\nMoney: an integer with four implied decimal places (4200000 is 420.00), with its currency beside it.",
                    "format": "int64",
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "rate_currency": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invitation"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: a rate needs both an amount and a currency; email, first_name and last_name are required; invalid JSON body; is_manager or is_worker must be true."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that person is already in your organization."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Offers someone a job at the acting manager's company.",
        "tags": [
          "Invitations to a company"
        ]
      }
    },
    "/api/invitations/accept": {
      "post": {
        "description": "Public, like Preview: somebody invited has no account yet. It still looks at the request's own session when there is one, because somebody accepting from an app they are already signed in to has nothing left to prove. That identity comes from the bearer token and never from the body.\n\nPublic: no key and no session needed.",
        "operationId": "postApiInvitationsAccept",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "Carries the invitation's token, for Preview and Accept.\n\nPublic by necessity: the invitee has no account yet. The token is read from the body rather than a query string, because a query string reaches proxy logs, browser history and Referer headers — and this token is a credential.",
                "properties": {
                  "token": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example1": {
                    "summary": "Accepting made the account: signed in",
                    "value": {
                      "code_sent": false,
                      "email": "petra@example.test",
                      "membership": null,
                      "outcome": "signed_in",
                      "session": {
                        "expires_at": "2026-11-06T07:30:00Z",
                        "language": null,
                        "membership": {
                          "cancellation_emails_enabled": true,
                          "emergency_responses_enabled": false,
                          "first_name": "Petra",
                          "last_name": "Nováková",
                          "membership_id": 12,
                          "org_name": "Alpha Stavby",
                          "role": "worker"
                        },
                        "memberships": [
                          {
                            "cancellation_emails_enabled": true,
                            "emergency_responses_enabled": false,
                            "first_name": "Petra",
                            "last_name": "Nováková",
                            "membership_id": 12,
                            "org_name": "Alpha Stavby",
                            "role": "worker"
                          }
                        ],
                        "token": "not-a-real-token-only-this-fixture-has-it"
                      }
                    }
                  },
                  "example2": {
                    "summary": "The account existed: a code is on its way",
                    "value": {
                      "code_sent": true,
                      "email": "petra@example.test",
                      "membership": null,
                      "outcome": "needs_code",
                      "session": null
                    }
                  },
                  "example3": {
                    "summary": "Already signed in as that account: joined",
                    "value": {
                      "code_sent": false,
                      "email": "petra@example.test",
                      "membership": {
                        "cancellation_emails_enabled": true,
                        "emergency_responses_enabled": false,
                        "first_name": "Petra",
                        "last_name": "Nováková",
                        "membership_id": 27,
                        "org_name": "Beta Montáže",
                        "role": "manager"
                      },
                      "outcome": "joined",
                      "session": null
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Acceptance"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: this invitation is invalid, expired, or already used."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "security": [],
        "summary": "Turns an invitation into a membership, and says how the person gets in: the same three answers the web page gives, from the same place.",
        "tags": [
          "Invitations to a company"
        ]
      }
    },
    "/api/invitations/preview": {
      "post": {
        "description": "Nothing is accepted.\n\nPublic: no key and no session needed.",
        "operationId": "postApiInvitationsPreview",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "Carries the invitation's token, for Preview and Accept.\n\nPublic by necessity: the invitee has no account yet. The token is read from the body rather than a query string, because a query string reaches proxy logs, browser history and Referer headers — and this token is a credential.",
                "properties": {
                  "token": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example1": {
                    "summary": "With an hourly rate",
                    "value": {
                      "email": "petra@example.test",
                      "first_name": "Petra",
                      "is_manager": false,
                      "is_worker": true,
                      "org_name": "Alpha Stavby",
                      "rate_amount": 4200000,
                      "rate_currency": "CZK"
                    }
                  },
                  "example2": {
                    "summary": "Without one, to both roles",
                    "value": {
                      "email": "petra@example.test",
                      "first_name": "Petra",
                      "is_manager": true,
                      "is_worker": true,
                      "org_name": "Beta Montáže",
                      "rate_amount": null,
                      "rate_currency": null
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/PreviewInvitation"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: this invitation is invalid, expired, or already used."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "security": [],
        "summary": "Describes an invitation to whoever holds its link: which company, what they are invited to do, and the rate offered.",
        "tags": [
          "Invitations to a company"
        ]
      }
    },
    "/api/people": {
      "get": {
        "description": "rate_amount is each person's own default rate here, the fallback for a project with no pay rate of its own and a role with none; null is \"no hourly rate\". is_disabled marks somebody a manager has switched off, who cannot be invited. certifications is what the company has noted the person as certified for; such people are listed first in the web client's staffing picker, and nothing requires one. Somebody whose account was deleted is not listed. Managers only.\n\nq narrows the list to the people a search finds, as the web client's person pickers do: every word of it has to be the start of a word of the person's name, or of the name, another common name or the scope of something they are certified for, with no case or accents needed. ?q=dvo finds Dvořák, ?q=vzv the forklift drivers, ?q=novák jeřáb a Novák noted for a crane, ?q=jeřáb 10 t a crane operator whose note's scope says 10 t. Without it, everybody.",
        "operationId": "getApiPeople",
        "parameters": [
          {
            "in": "query",
            "name": "q",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": [
                      {
                        "cancellation_emails_enabled": true,
                        "certifications": [],
                        "created_at": "2026-09-01T08:00:00Z",
                        "email": "tomas@alpha.test",
                        "emergency_responses_enabled": false,
                        "first_name": "Tomáš",
                        "id": 1,
                        "is_disabled": false,
                        "is_manager": true,
                        "is_worker": false,
                        "last_name": "Novák",
                        "org_name": "Alpha Stavby",
                        "rate_amount": null,
                        "rate_currency": null
                      },
                      {
                        "cancellation_emails_enabled": true,
                        "certifications": [
                          {
                            "certificate_id": 1,
                            "created_at": "2026-10-28T09:00:00Z",
                            "expires_on": "2027-06-30",
                            "granted_by_membership_id": 1,
                            "granted_by_name": "Tomáš Novák",
                            "id": 1,
                            "kind": "lift_crane_overhead",
                            "label": null,
                            "membership_id": 2,
                            "name": "Overhead or gantry crane operator",
                            "scope": "bridge cranes to 10 t"
                          }
                        ],
                        "created_at": "2026-09-01T08:05:00Z",
                        "email": "jana@alpha.test",
                        "emergency_responses_enabled": true,
                        "first_name": "Jana",
                        "id": 2,
                        "is_disabled": false,
                        "is_manager": false,
                        "is_worker": true,
                        "last_name": "Horáková",
                        "org_name": "Alpha Stavby",
                        "rate_amount": 4200000,
                        "rate_currency": "CZK"
                      }
                    ]
                  }
                },
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/PersonResponse"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Lists everyone at the company, by name.",
        "tags": [
          "People at the company"
        ]
      }
    },
    "/api/people/{id}/certificates": {
      "get": {
        "description": "A certificate is the person's, not the company's, so this is the same list the person's other employers see; who uploaded one is named only when it was a manager at your own company. Managers only.",
        "operationId": "getApiPeopleIdCertificates",
        "parameters": [
          {
            "description": "The person's membership id, as the roster lists it.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": [
                      {
                        "created_at": "2026-10-28T09:00:00Z",
                        "expires_on": "2029-05-01",
                        "id": 1,
                        "issued_on": "2024-05-02",
                        "kind": "lift_crane_overhead",
                        "label": null,
                        "name": "Overhead or gantry crane operator",
                        "number": "JB-4471",
                        "pages": [
                          {
                            "content_type": "image/jpeg",
                            "height": 1067,
                            "position": 1,
                            "width": 1600
                          },
                          {
                            "content_type": "image/jpeg",
                            "height": 1067,
                            "position": 2,
                            "width": 1600
                          }
                        ],
                        "scope": "bridge cranes to 10 t",
                        "updated_at": "2026-10-28T09:04:00Z",
                        "uploaded_by": {
                          "company": "Alpha Stavby",
                          "membership_id": 1,
                          "name": "Tomáš Novák"
                        }
                      },
                      {
                        "created_at": "2026-10-28T10:00:00Z",
                        "expires_on": null,
                        "id": 2,
                        "issued_on": null,
                        "kind": "other_personal",
                        "label": "Drone pilot",
                        "name": "Drone pilot",
                        "number": null,
                        "pages": [],
                        "scope": null,
                        "updated_at": "2026-10-28T10:00:00Z",
                        "uploaded_by": null
                      }
                    ]
                  }
                },
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/CertificateResponse"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Lists a person's certificates: the documents they hold, with their pages' sizes, as the person themselves sees them.",
        "tags": [
          "People at the company"
        ]
      },
      "post": {
        "description": "It is recorded as uploaded by you, shown to the person as such, and is theirs afterwards like one they uploaded: they may correct or delete it, and you may not. The pages are added afterwards, one by one. Managers only.",
        "operationId": "postApiPeopleIdCertificates",
        "parameters": [
          {
            "description": "The person's membership id, as the roster lists it.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "What is typed about a certificate: what is printed on the card.",
                "properties": {
                  "expires_on": {
                    "description": "expires_on is the day it stops being valid, \"2029-05-01\", or null for unknown. Blank never means \"for ever\".",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "issued_on": {
                    "description": "issued_on is the day it was issued, \"2024-05-02\", or null.",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "kind": {
                    "description": "kind is a kind's id from the certificate kinds endpoint, or other_personal for one the list has no name for.",
                    "type": "string"
                  },
                  "label": {
                    "description": "label says what an other_personal certificate is. Required for that kind, refused for any other, at most 120 characters.",
                    "type": "string"
                  },
                  "number": {
                    "description": "number is the number printed on the card, if any. At most 60 characters; nothing is checked against it.",
                    "type": "string"
                  },
                  "scope": {
                    "description": "At most 200 characters.",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "created_at": "2026-10-28T10:00:00Z",
                      "expires_on": null,
                      "id": 2,
                      "issued_on": null,
                      "kind": "other_personal",
                      "label": "Drone pilot",
                      "name": "Drone pilot",
                      "number": null,
                      "pages": [],
                      "scope": null,
                      "updated_at": "2026-10-28T10:00:00Z",
                      "uploaded_by": null
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/CertificateResponse"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Adds a certificate on a person's behalf, for when the office holds the scans.",
        "tags": [
          "People at the company"
        ]
      }
    },
    "/api/people/{id}/certificates/{certificateID}/pages": {
      "post": {
        "description": "The image comes as multipart/form-data with a part named page, or as JSON with the image in base64. At most 8 pages; what is kept is the pixels and nothing else. Managers only.",
        "operationId": "postApiPeopleIdCertificatesCertificateidPages",
        "parameters": [
          {
            "description": "The person's membership id, as the roster lists it.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "The certificate's id, as the list of certificates gives it.",
            "in": "path",
            "name": "certificateID",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "One page of a certificate as JSON: the image's bytes in base64. The same endpoint also takes multipart/form-data with the file in a part named page, which is what the app and the web client send.",
                "properties": {
                  "image": {
                    "description": "A PDF is refused; a client with one renders its pages to images first. The server shrinks the image to about 1600 px on its long edge, re-encodes it as JPEG and keeps nothing else of it: no camera metadata, no position.",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "created_at": "2026-10-28T09:00:00Z",
                      "expires_on": "2029-05-01",
                      "id": 1,
                      "issued_on": "2024-05-02",
                      "kind": "lift_crane_overhead",
                      "label": null,
                      "name": "Overhead or gantry crane operator",
                      "number": "JB-4471",
                      "pages": [
                        {
                          "content_type": "image/jpeg",
                          "height": 1067,
                          "position": 1,
                          "width": 1600
                        },
                        {
                          "content_type": "image/jpeg",
                          "height": 1067,
                          "position": 2,
                          "width": 1600
                        }
                      ],
                      "scope": "bridge cranes to 10 t",
                      "updated_at": "2026-10-28T09:04:00Z",
                      "uploaded_by": {
                        "company": "Alpha Stavby",
                        "membership_id": 1,
                        "name": "Tomáš Novák"
                      }
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/CertificateResponse"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: image must be base64; invalid JSON body; invalid {thing} id; send the image as a part named page; the upload could not be read."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Adds the next page to a person's certificate, as the person's own pages endpoint does.",
        "tags": [
          "People at the company"
        ]
      }
    },
    "/api/people/{id}/certificates/{certificateID}/pages/{position}": {
      "get": {
        "description": "Managers only.\n\nThe body is the JPEG, with its content type.",
        "operationId": "getApiPeopleIdCertificatesCertificateidPagesPosition",
        "parameters": [
          {
            "description": "The person's membership id, as the roster lists it.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "The certificate's id, as the list of certificates gives it.",
            "in": "path",
            "name": "certificateID",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "The page's position on the certificate, counted from 1.",
            "in": "path",
            "name": "position",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; page not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Answers one page of a person's certificate as the image itself, not as JSON.",
        "tags": [
          "People at the company"
        ]
      }
    },
    "/api/people/{id}/certifications": {
      "post": {
        "description": "Approving is sending certificate_id: the note copies what the document says and is linked to it. The note outlives the document: a certificate deleted later leaves it standing with certificate_id null.\n\nOne note per person and kind (per label for other_personal); a second of the same is refused. The person is pushed once. Nothing requires a note: it is the company's own record, shown to its managers when they staff a shift. Managers only.",
        "operationId": "postApiPeopleIdCertifications",
        "parameters": [
          {
            "description": "The person's membership id, as the roster lists it.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "Notes somebody as certified for something: by the manager's say-so, or by approving one of the person's certificates.",
                "properties": {
                  "certificate_id": {
                    "description": "certificate_id approves one of the person's certificates, as the people certificates endpoint lists them: the note takes the document's kind, label, scope and expiry and points at it, and the other fields here are ignored. Leave it out for a note with no proof.",
                    "format": "int64",
                    "type": "integer"
                  },
                  "expires_on": {
                    "description": "expires_on is the day it stops being valid, \"2027-06-30\", or null for unknown. Blank never means \"for ever\".",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "kind": {
                    "description": "kind is a kind's id from the certificate kinds endpoint, or other_personal for one the list has no name for.",
                    "type": "string"
                  },
                  "label": {
                    "description": "label says what an other_personal note is for. Required for that kind, refused for any other, at most 120 characters.",
                    "type": "string"
                  },
                  "scope": {
                    "description": "At most 200 characters.",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "certificate_id": 1,
                      "created_at": "2026-10-28T09:00:00Z",
                      "expires_on": "2027-06-30",
                      "granted_by_membership_id": 1,
                      "granted_by_name": "Tomáš Novák",
                      "id": 1,
                      "kind": "lift_crane_overhead",
                      "label": null,
                      "membership_id": 2,
                      "name": "Overhead or gantry crane operator",
                      "scope": "bridge cranes to 10 t"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/CertificationResponse"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: expires_on must be YYYY-MM-DD or null; invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Notes a person as certified for something, by the manager's say-so, or by approving one of their certificates.",
        "tags": [
          "People at the company"
        ]
      }
    },
    "/api/people/{id}/certifications/{certificationID}": {
      "delete": {
        "description": "Nobody is told. Managers only.",
        "operationId": "deleteApiPeopleIdCertificationsCertificationid",
        "parameters": [
          {
            "description": "The person's membership id, as the roster lists it.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "The note's id, as the roster lists it under the person.",
            "in": "path",
            "name": "certificationID",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done, with no body."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Takes a note away.",
        "tags": [
          "People at the company"
        ]
      }
    },
    "/api/schedule": {
      "get": {
        "operationId": "getApiSchedule",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": [
                      {
                        "can_take_from": "2026-09-09T11:00:00Z",
                        "can_take_until": "2026-09-09T15:00:00Z",
                        "collides_from": "2026-09-09T05:00:00Z",
                        "collides_until": "2026-09-09T11:00:00Z",
                        "collides_with_company": null,
                        "collides_with_title": "Depot fit-out",
                        "created_at": "2026-09-07T08:00:00Z",
                        "expires_at": "2026-09-09T07:00:00Z",
                        "full": false,
                        "gig_id": 1,
                        "gig_title": "Warehouse roof",
                        "id": 1,
                        "invited_at": "2026-09-07T08:00:00Z",
                        "membership_id": 2,
                        "origin": "staffed",
                        "rate_amount": 4200000,
                        "rate_currency": "CZK",
                        "responded_at": null,
                        "role_id": 1,
                        "role_label": null,
                        "status": "pending",
                        "takes_from": null,
                        "takes_until": null,
                        "timeslot": {
                          "accepted": 0,
                          "created_at": "2026-09-07T08:00:00Z",
                          "day_id": 1,
                          "ends_at": "2026-09-09T15:00:00Z",
                          "gig_id": 1,
                          "id": 1,
                          "pending": 1,
                          "required_workers": 2,
                          "role_label": null,
                          "starts_at": "2026-09-09T07:00:00Z"
                        },
                        "timeslot_id": 1
                      },
                      {
                        "can_take_from": null,
                        "can_take_until": null,
                        "collides_from": null,
                        "collides_until": null,
                        "collides_with_company": null,
                        "collides_with_title": null,
                        "created_at": "2026-09-07T09:00:00Z",
                        "expires_at": "2026-09-10T05:00:00Z",
                        "full": false,
                        "gig_id": 2,
                        "gig_title": "Depot fit-out",
                        "id": 2,
                        "invited_at": "2026-09-07T09:00:00Z",
                        "membership_id": 2,
                        "origin": "sos",
                        "rate_amount": 6300000,
                        "rate_currency": "CZK",
                        "responded_at": "2026-09-07T09:05:00Z",
                        "role_id": 2,
                        "role_label": "crane op",
                        "status": "accepted",
                        "takes_from": "2026-09-10T09:00:00Z",
                        "takes_until": "2026-09-10T13:00:00Z",
                        "timeslot": {
                          "accepted": 1,
                          "created_at": "2026-09-07T09:00:00Z",
                          "day_id": 2,
                          "ends_at": "2026-09-10T13:00:00Z",
                          "gig_id": 2,
                          "id": 2,
                          "pending": 0,
                          "required_workers": 1,
                          "role_label": "crane op",
                          "starts_at": "2026-09-10T05:00:00Z"
                        },
                        "timeslot_id": 2
                      },
                      {
                        "can_take_from": null,
                        "can_take_until": null,
                        "collides_from": null,
                        "collides_until": null,
                        "collides_with_company": null,
                        "collides_with_title": null,
                        "created_at": "2026-09-07T10:00:00Z",
                        "expires_at": "2026-09-11T05:00:00Z",
                        "full": false,
                        "gig_id": 1,
                        "gig_title": "Warehouse roof",
                        "id": 3,
                        "invited_at": "2026-09-07T10:00:00Z",
                        "membership_id": 2,
                        "origin": "open",
                        "rate_amount": 6300000,
                        "rate_currency": "CZK",
                        "responded_at": null,
                        "role_id": 3,
                        "role_label": null,
                        "status": "pending",
                        "takes_from": null,
                        "takes_until": null,
                        "timeslot": {
                          "accepted": 1,
                          "created_at": "2026-09-07T10:00:00Z",
                          "day_id": 3,
                          "ends_at": "2026-09-11T13:00:00Z",
                          "gig_id": 1,
                          "id": 3,
                          "pending": 1,
                          "required_workers": 2,
                          "role_label": null,
                          "starts_at": "2026-09-11T05:00:00Z"
                        },
                        "timeslot_id": 3
                      },
                      {
                        "can_take_from": null,
                        "can_take_until": null,
                        "collides_from": null,
                        "collides_until": null,
                        "collides_with_company": null,
                        "collides_with_title": null,
                        "created_at": "2026-09-07T11:00:00Z",
                        "expires_at": "2026-09-14T05:00:00Z",
                        "full": true,
                        "gig_id": 1,
                        "gig_title": "Warehouse roof",
                        "id": 4,
                        "invited_at": "2026-09-07T11:00:00Z",
                        "membership_id": 2,
                        "origin": "staffed",
                        "rate_amount": null,
                        "rate_currency": null,
                        "responded_at": null,
                        "role_id": 4,
                        "role_label": null,
                        "status": "pending",
                        "takes_from": null,
                        "takes_until": null,
                        "timeslot": {
                          "accepted": 2,
                          "created_at": "2026-09-07T11:00:00Z",
                          "day_id": 4,
                          "ends_at": "2026-09-14T13:00:00Z",
                          "gig_id": 1,
                          "id": 4,
                          "pending": 1,
                          "required_workers": 2,
                          "role_label": null,
                          "starts_at": "2026-09-14T05:00:00Z"
                        },
                        "timeslot_id": 4
                      }
                    ]
                  }
                },
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/ScheduleEntry"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Returns the caller's own assignments at the active company.",
        "tags": [
          "Shifts and answers"
        ]
      }
    },
    "/api/staffing/short": {
      "get": {
        "description": "days is how far ahead to look, 7 unless given, up to 92. Earliest first, across every live project. A shift whose places are all covered by unanswered invitations is listed: an invitation staffs nothing. Managers only.\n\nall=1 lists them at every company the caller manages, earliest first within each, with company on each row naming the entry: send its entry as X-Werzeick-Company to act on the shift. A caller acting as a worker may ask too, and gets the companies they manage. It cannot be combined with X-Werzeick-Company.",
        "operationId": "getApiStaffingShort",
        "parameters": [
          {
            "in": "query",
            "name": "all",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "days",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": [
                      {
                        "accepted": 1,
                        "created_at": "2026-09-07T09:00:00Z",
                        "day_id": 1,
                        "ends_at": "2026-09-10T13:00:00Z",
                        "gig_id": 1,
                        "gig_title": "Warehouse roof",
                        "id": 1,
                        "pending": 1,
                        "required_workers": 3,
                        "role_label": "1× crane op, 2× labourer",
                        "roles": [
                          {
                            "accepted": 1,
                            "headcount": 1,
                            "id": 1,
                            "label": "crane op",
                            "pending": 0,
                            "rate_amount": 5200000,
                            "rate_currency": "CZK"
                          },
                          {
                            "accepted": 0,
                            "headcount": 2,
                            "id": 2,
                            "label": "labourer",
                            "pending": 1,
                            "rate_amount": null,
                            "rate_currency": null
                          }
                        ],
                        "short": 2,
                        "sos_open": true,
                        "starts_at": "2026-09-10T05:00:00Z"
                      }
                    ]
                  }
                },
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/ShiftNeedingAttention"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: all=1 reads every company: leave out X-Werzeick-Company; days must be a whole number from 1 to 92."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Lists the company's upcoming shifts that need attention: short of accepted people, or carrying invitations nobody has answered.",
        "tags": [
          "Staffing a project"
        ]
      }
    },
    "/api/time/clock-in": {
      "post": {
        "operationId": "postApiTimeClockIn",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "The entire wire shape of a clock event. Keep it that way.",
                "properties": {
                  "at": {
                    "description": "at is the device's clock, RFC 3339 with an offset. Omitted means now.",
                    "type": "string"
                  },
                  "client_event_id": {
                    "description": "client_event_id is the device's id for this event, and the reason a replayed push creates one row rather than two.",
                    "type": "string"
                  },
                  "gig_id": {
                    "format": "int64",
                    "type": "integer"
                  },
                  "source": {
                    "description": "source is \"manual\" or \"geofence\"; empty means manual.",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "clock_in_at": "2026-09-09T06:20:00Z",
                      "clock_out_at": null,
                      "corrected_at": null,
                      "created_at": "2026-09-09T06:20:00Z",
                      "earned_amount": 0,
                      "gig_id": 1,
                      "id": 2,
                      "membership_id": 2,
                      "rate_amount": 4200000,
                      "rate_currency": "CZK",
                      "source": "geofence",
                      "timeslot_id": null
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/TimeEntry"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: at must be an RFC 3339 timestamp; invalid JSON body."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: time entry not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text saying why."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Starts a stretch of work.",
        "tags": [
          "Clocking in and out"
        ]
      }
    },
    "/api/time/clock-out": {
      "post": {
        "operationId": "postApiTimeClockOut",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "The entire wire shape of a clock event. Keep it that way.",
                "properties": {
                  "at": {
                    "description": "at is the device's clock, RFC 3339 with an offset. Omitted means now.",
                    "type": "string"
                  },
                  "client_event_id": {
                    "description": "client_event_id is the device's id for this event, and the reason a replayed push creates one row rather than two.",
                    "type": "string"
                  },
                  "gig_id": {
                    "format": "int64",
                    "type": "integer"
                  },
                  "source": {
                    "description": "source is \"manual\" or \"geofence\"; empty means manual.",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "client_event_id": "9f8c1a2b3d4e5f60718293a4b5c6d7e8",
                      "clock_in_at": "2026-09-08T07:45:00Z",
                      "clock_out_at": "2026-09-08T17:10:00Z",
                      "corrected_at": null,
                      "created_at": "2026-09-08T07:45:00Z",
                      "earned_amount": 39550000,
                      "gig_id": 1,
                      "id": 1,
                      "membership_id": 2,
                      "rate_amount": 4200000,
                      "rate_currency": "CZK",
                      "source": "manual",
                      "timeslot_id": 1
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/TimeEntry"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: at must be an RFC 3339 timestamp; clock-out needs a gig_id or a client_event_id; invalid JSON body."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: time entry not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text saying why."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Ends one, naming it by the device's event id when the phone has been offline and by project otherwise.",
        "tags": [
          "Clocking in and out"
        ]
      }
    },
    "/api/time/entries": {
      "get": {
        "description": "since defaults to four weeks back: enough to check a pay period, little enough that a phone on a slow connection is not downloading a year.",
        "operationId": "getApiTimeEntries",
        "parameters": [
          {
            "in": "query",
            "name": "since",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": [
                      {
                        "client_event_id": "9f8c1a2b3d4e5f60718293a4b5c6d7e8",
                        "clock_in_at": "2026-09-08T07:45:00Z",
                        "clock_out_at": "2026-09-08T17:10:00Z",
                        "corrected_at": null,
                        "created_at": "2026-09-08T07:45:00Z",
                        "earned_amount": 39550000,
                        "gig_id": 1,
                        "id": 1,
                        "membership_id": 2,
                        "rate_amount": 4200000,
                        "rate_currency": "CZK",
                        "source": "manual",
                        "timeslot_id": 1
                      },
                      {
                        "clock_in_at": "2026-09-09T06:20:00Z",
                        "clock_out_at": null,
                        "corrected_at": null,
                        "created_at": "2026-09-09T06:20:00Z",
                        "earned_amount": 0,
                        "gig_id": 1,
                        "id": 2,
                        "membership_id": 2,
                        "rate_amount": 4200000,
                        "rate_currency": "CZK",
                        "source": "geofence",
                        "timeslot_id": null
                      },
                      {
                        "clock_in_at": "2026-09-10T06:00:00Z",
                        "clock_out_at": "2026-09-10T14:30:00Z",
                        "corrected_at": null,
                        "created_at": "2026-09-10T06:00:00Z",
                        "earned_amount": null,
                        "gig_id": 1,
                        "id": 3,
                        "membership_id": 2,
                        "rate_amount": null,
                        "rate_currency": null,
                        "source": "manual",
                        "timeslot_id": null
                      }
                    ]
                  }
                },
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/TimeEntry"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: since must be an RFC 3339 timestamp."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: time entry not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text saying why."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "The worker's own record of their hours.",
        "tags": [
          "Clocking in and out"
        ]
      }
    },
    "/api/time/entries/{id}/correct": {
      "post": {
        "description": "A worker owns their own hours; a manager may resolve anyone's, which is how discrepancies get settled.",
        "operationId": "postApiTimeEntriesIdCorrect",
        "parameters": [
          {
            "description": "The time entry's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "clock_in_at": {
                    "type": "string"
                  },
                  "clock_out_at": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "client_event_id": "9f8c1a2b3d4e5f60718293a4b5c6d7e8",
                      "clock_in_at": "2026-09-08T07:45:00Z",
                      "clock_out_at": "2026-09-08T17:10:00Z",
                      "corrected_at": null,
                      "created_at": "2026-09-08T07:45:00Z",
                      "earned_amount": 39550000,
                      "gig_id": 1,
                      "id": 1,
                      "membership_id": 2,
                      "rate_amount": 4200000,
                      "rate_currency": "CZK",
                      "source": "manual",
                      "timeslot_id": 1
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/TimeEntry"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: clock_in_at must be an RFC 3339 timestamp; clock_out_at must be an RFC 3339 timestamp or null; invalid JSON body; invalid time entry id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: time entry not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text saying why."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Fixes an entry's times.",
        "tags": [
          "Clocking in and out"
        ]
      }
    },
    "/api/timeslots/{id}": {
      "get": {
        "description": "Each person's status is as it stands now: an unanswered invitation past the shift's start reads expired. Another company's shift is not found. Managers only.",
        "operationId": "getApiTimeslotsId",
        "parameters": [
          {
            "description": "The shift's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "accepted": 1,
                      "created_at": "2026-09-07T09:00:00Z",
                      "day_id": 1,
                      "ends_at": "2026-09-10T13:00:00Z",
                      "gig_id": 1,
                      "id": 1,
                      "is_open": true,
                      "offered_rate_multiplier": 15000,
                      "open_auto_accept": false,
                      "pending": 1,
                      "people": [
                        {
                          "created_at": "2026-09-07T09:00:00Z",
                          "expires_at": "2026-09-10T05:00:00Z",
                          "id": 1,
                          "invited_at": "2026-09-07T09:00:00Z",
                          "membership_id": 2,
                          "origin": "staffed",
                          "rate_amount": 5200000,
                          "rate_currency": "CZK",
                          "responded_at": "2026-09-07T09:05:00Z",
                          "role_id": 1,
                          "role_label": "crane op",
                          "status": "accepted",
                          "takes_from": null,
                          "takes_until": null,
                          "timeslot_id": 1,
                          "worker_name": "Jana Horáková"
                        },
                        {
                          "created_at": "2026-09-08T14:00:00Z",
                          "expires_at": "2026-09-10T13:00:00Z",
                          "id": 2,
                          "invited_at": "2026-09-08T14:00:00Z",
                          "membership_id": 3,
                          "origin": "sos",
                          "rate_amount": null,
                          "rate_currency": null,
                          "responded_at": null,
                          "role_id": 2,
                          "role_label": "labourer",
                          "status": "pending",
                          "takes_from": null,
                          "takes_until": null,
                          "timeslot_id": 1,
                          "worker_name": "Petr Dvořák"
                        }
                      ],
                      "required_workers": 3,
                      "role_label": "1× crane op, 2× labourer",
                      "roles": [
                        {
                          "accepted": 1,
                          "headcount": 1,
                          "id": 1,
                          "label": "crane op",
                          "pending": 0,
                          "rate_amount": 5200000,
                          "rate_currency": "CZK"
                        },
                        {
                          "accepted": 0,
                          "headcount": 2,
                          "id": 2,
                          "label": "labourer",
                          "pending": 1,
                          "rate_amount": null,
                          "rate_currency": null
                        }
                      ],
                      "short": 2,
                      "starts_at": "2026-09-10T05:00:00Z"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ShiftWithItsPeople"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "One shift, with its roles and everyone on it.",
        "tags": [
          "Staffing a project"
        ]
      }
    },
    "/api/timeslots/{id}/close": {
      "post": {
        "description": "The people already on it stay on it. Managers only.",
        "operationId": "postApiTimeslotsIdClose",
        "parameters": [
          {
            "description": "The shift's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "accepted": 1,
                      "created_at": "2026-09-07T09:00:00Z",
                      "day_id": 1,
                      "ends_at": "2026-09-10T13:00:00Z",
                      "gig_id": 1,
                      "id": 1,
                      "is_open": false,
                      "offered_rate_multiplier": null,
                      "open_auto_accept": false,
                      "pending": 1,
                      "required_workers": 3,
                      "role_label": "1× crane op, 2× labourer",
                      "roles": [
                        {
                          "accepted": 1,
                          "headcount": 1,
                          "id": 1,
                          "label": "crane op",
                          "pending": 0,
                          "rate_amount": 5200000,
                          "rate_currency": "CZK"
                        },
                        {
                          "accepted": 0,
                          "headcount": 2,
                          "id": 2,
                          "label": "labourer",
                          "pending": 1,
                          "rate_amount": null,
                          "rate_currency": null
                        }
                      ],
                      "short": 2,
                      "starts_at": "2026-09-10T05:00:00Z"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ShiftAsAManagerSeesIt"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Stops advertising a shift.",
        "tags": [
          "Staffing a project"
        ]
      }
    },
    "/api/timeslots/{id}/invitations": {
      "post": {
        "description": "The answer is the invitation, pending: it fills nothing until they accept, and it lapses at the shift's start if they never do. The rate on it is settled now — the role's, else the project's pay rate, else their own — and does not move afterwards. A person already live on the shift, a shift that has started, and somebody who does not work shifts here are each refused. The person is told once. Managers only.",
        "operationId": "postApiTimeslotsIdInvitations",
        "parameters": [
          {
            "description": "The shift's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "Names who to invite to a shift, and for which role.",
                "properties": {
                  "membership_id": {
                    "description": "membership_id is the person, as the roster lists them.",
                    "format": "int64",
                    "type": "integer"
                  },
                  "role_id": {
                    "description": "role_id is the role on the shift they are invited for. It may be left out for a shift with one role.",
                    "format": "int64",
                    "type": "integer"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "created_at": "2026-09-08T14:00:00Z",
                      "expires_at": "2026-09-10T13:00:00Z",
                      "id": 2,
                      "invited_at": "2026-09-08T14:00:00Z",
                      "membership_id": 3,
                      "origin": "sos",
                      "rate_amount": null,
                      "rate_currency": null,
                      "responded_at": null,
                      "role_id": 2,
                      "role_label": "labourer",
                      "status": "pending",
                      "takes_from": null,
                      "takes_until": null,
                      "timeslot_id": 1,
                      "worker_name": "Petr Dvořák"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Assignment"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id; membership_id is required."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Asks one person to one shift.",
        "tags": [
          "Staffing a project"
        ]
      }
    },
    "/api/timeslots/{id}/open": {
      "post": {
        "description": "A shift is open only because a manager said so; it is never open merely for being short. The manager chooses whether a taker is confirmed outright or reviewed, and may raise the rate. The answer is the shift as it now stands. Managers only.",
        "operationId": "postApiTimeslotsIdOpen",
        "parameters": [
          {
            "description": "The shift's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "The terms a shift is opened on.",
                "properties": {
                  "auto_accept": {
                    "description": "auto_accept confirms a taker outright, up to each role's headcount. Without it a taker lands as an applicant for the manager to approve.",
                    "type": "boolean"
                  },
                  "offered_rate_multiplier": {
                    "description": "offered_rate_multiplier raises each taker's usual rate, with four implied decimals: 15000 is 1.5×. Null offers each role's rate as it stands.\n\nA multiplier with four implied decimal places: 15000 is 1.5×.",
                    "format": "int64",
                    "type": [
                      "integer",
                      "null"
                    ]
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "accepted": 1,
                      "created_at": "2026-09-07T09:00:00Z",
                      "day_id": 1,
                      "ends_at": "2026-09-10T13:00:00Z",
                      "gig_id": 1,
                      "id": 1,
                      "is_open": true,
                      "offered_rate_multiplier": 15000,
                      "open_auto_accept": false,
                      "pending": 1,
                      "required_workers": 3,
                      "role_label": "1× crane op, 2× labourer",
                      "roles": [
                        {
                          "accepted": 1,
                          "headcount": 1,
                          "id": 1,
                          "label": "crane op",
                          "pending": 0,
                          "rate_amount": 5200000,
                          "rate_currency": "CZK"
                        },
                        {
                          "accepted": 0,
                          "headcount": 2,
                          "id": 2,
                          "label": "labourer",
                          "pending": 1,
                          "rate_amount": null,
                          "rate_currency": null
                        }
                      ],
                      "short": 2,
                      "starts_at": "2026-09-10T05:00:00Z"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ShiftAsAManagerSeesIt"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: a multiplier must be more than nothing; invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Advertises a shift to the company: any worker there may take a place on it without being invited.",
        "tags": [
          "Staffing a project"
        ]
      }
    },
    "/api/timeslots/{id}/remove": {
      "post": {
        "description": "Everybody with a live place on it is taken off and told, as removing a day tells them. It asks first: without confirm nothing changes and the answer counts the accepted and pending places. Managers only.",
        "operationId": "postApiTimeslotsIdRemove",
        "parameters": [
          {
            "description": "The shift's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "A removal's consent.",
                "properties": {
                  "confirm": {
                    "description": "confirm says to go ahead. Without it nothing changes and the answer says what would.",
                    "type": "boolean"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RemovedShift"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Takes a shift off its day.",
        "tags": [
          "Planning a project's calendar"
        ]
      }
    },
    "/api/timeslots/{id}/sos": {
      "post": {
        "description": "An SOS is an open shift with the rate raised and a notification sent: every worker at the company who opted into emergency cover is told, and each may take a place at the raised rate until the shift ends. It is refused on a shift that is not short, that is over, or that has an SOS running; somebody who called in sick is taken off first, which is what makes the shift short. Managers only.",
        "operationId": "postApiTimeslotsIdSos",
        "parameters": [
          {
            "description": "The shift's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "The terms an SOS is raised on.",
                "properties": {
                  "auto_accept": {
                    "description": "auto_accept confirms a responder outright, up to the role's headcount; without it responders are applicants for the manager to approve.",
                    "type": "boolean"
                  },
                  "offered_rate_multiplier": {
                    "description": "offered_rate_multiplier raises each taker's usual rate, with four implied decimals. Null is the default, 15000: half as much again. The project's maximum hourly rate caps what comes out.\n\nA multiplier with four implied decimal places: 15000 is 1.5×.",
                    "format": "int64",
                    "type": [
                      "integer",
                      "null"
                    ]
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "id": 1,
                      "offered_rate_multiplier": 15000,
                      "opened_at": "2026-09-09T16:00:00Z",
                      "timeslot_id": 1,
                      "told": 0
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/SOSRaised"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: a multiplier must be more than nothing; invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; that application is the manager's to approve."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: assignment not found; certificate not found; certification not found; client not found; gig not found; shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: that invitation is no longer open; that is an invitation, and only the worker can accept it; that shift collides with work you have accepted; that shift is already full."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Raises an emergency on one short shift.",
        "tags": [
          "Staffing a project"
        ]
      }
    },
    "/api/timeslots/{id}/take": {
      "post": {
        "description": "The body names the role being taken, role_id, as the dashboard's open shift row carried it: an open shift is listed once per role, each with its own places and figure. It may be left out for a shift with one role. The request carries no rate: the figure is resolved on the server at signup and snapshotted, exactly as the dashboard quoted it; a client never sends the number it believes it was offered. partial is consent to take only the free remainder of a shift that collides with accepted work.\n\nWhat comes back is the assignment: accepted on a shift that confirms takers, pending on one where the manager picks. The app re-reads the dashboard either way.",
        "operationId": "postApiTimeslotsIdTake",
        "parameters": [
          {
            "description": "The shift's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "partial": {
                    "type": "boolean"
                  },
                  "role_id": {
                    "format": "int64",
                    "type": "integer"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "created_at": "2026-09-07T09:00:00Z",
                      "expires_at": "2026-09-10T05:00:00Z",
                      "id": 2,
                      "invited_at": "2026-09-07T09:00:00Z",
                      "membership_id": 2,
                      "origin": "sos",
                      "rate_amount": 6300000,
                      "rate_currency": "CZK",
                      "responded_at": "2026-09-07T09:05:00Z",
                      "role_id": 2,
                      "role_label": "crane op",
                      "status": "accepted",
                      "takes_from": "2026-09-10T09:00:00Z",
                      "takes_until": "2026-09-10T13:00:00Z",
                      "timeslot_id": 2
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Assignment"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid timeslot id; this shift has several roles; send role_id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted; this shift raises an hourly rate, and you have none here to raise."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: shift not found."
          },
          "409": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text saying why."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Puts the caller on a shift a PM opened to the company.",
        "tags": [
          "Your week"
        ]
      }
    },
    "/api/tool-icons": {
      "get": {
        "description": "It is the one definition both clients read, so a picker cannot offer a name the server would refuse. Deliberately not behind the session: it is a constant, discloses nothing, and an app that could not draw its own picker until sign-in would be worse for no gain.\n\nPublic: no key and no session needed.",
        "operationId": "getApiToolIcons",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": [
                      {
                        "label": "Hammer",
                        "name": "hammer"
                      },
                      {
                        "label": "Spanner",
                        "name": "wrench"
                      },
                      {
                        "label": "Screwdriver",
                        "name": "screwdriver"
                      },
                      {
                        "label": "Pliers",
                        "name": "pliers"
                      },
                      {
                        "label": "Saw",
                        "name": "saw"
                      },
                      {
                        "label": "Trowel",
                        "name": "trowel"
                      },
                      {
                        "label": "Shovel",
                        "name": "shovel"
                      },
                      {
                        "label": "Drill",
                        "name": "drill"
                      },
                      {
                        "label": "Angle grinder",
                        "name": "grinder"
                      },
                      {
                        "label": "Breaker",
                        "name": "breaker"
                      },
                      {
                        "label": "Mixer",
                        "name": "mixer"
                      },
                      {
                        "label": "Tape measure",
                        "name": "measure"
                      },
                      {
                        "label": "Spirit level",
                        "name": "level"
                      },
                      {
                        "label": "Ladder",
                        "name": "ladder"
                      },
                      {
                        "label": "Paint roller",
                        "name": "paint"
                      },
                      {
                        "label": "Power lead",
                        "name": "cable"
                      },
                      {
                        "label": "Work light",
                        "name": "light"
                      },
                      {
                        "label": "Toolbox",
                        "name": "toolbox"
                      },
                      {
                        "label": "Safety gear",
                        "name": "safety"
                      },
                      {
                        "label": "Other",
                        "name": "other"
                      }
                    ]
                  }
                },
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/ToolIcon"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          }
        },
        "security": [],
        "summary": "Returns the fixed set of icons the product ships.",
        "tags": [
          "Tools"
        ]
      }
    },
    "/api/tools": {
      "get": {
        "operationId": "getApiTools",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": [
                      {
                        "created_at": "2026-09-08T14:22:41.117204+02:00",
                        "gig_ids": [
                          3,
                          7
                        ],
                        "icon": "drill",
                        "id": 12,
                        "name": "SDS drill",
                        "quantity": null
                      },
                      {
                        "created_at": "2026-09-08T14:21:02.884113+02:00",
                        "gig_ids": [],
                        "icon": "other",
                        "id": 11,
                        "name": "Clamps",
                        "quantity": 6
                      }
                    ]
                  }
                },
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/Tool"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text saying why."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: tool not found."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "The caller's own kit list at this company, with the ids of the projects each tool is attached to.",
        "tags": [
          "Tools"
        ]
      },
      "post": {
        "description": "The icon is a name from GET /api/tool-icons; the quantity is optional.",
        "operationId": "postApiTools",
        "parameters": [
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "The JSON body of a create or an update.\n\nNo membership or org field: both come from the session. A client that could name either could write somebody else's kit list.",
                "properties": {
                  "icon": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "quantity": {
                    "format": "int64",
                    "type": [
                      "integer",
                      "null"
                    ]
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "created_at": "2026-09-08T14:22:41.117204+02:00",
                      "gig_ids": [
                        3,
                        7
                      ],
                      "icon": "drill",
                      "id": 12,
                      "name": "SDS drill",
                      "quantity": null
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Tool"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: tool not found."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Adds a tool to the caller's kit list.",
        "tags": [
          "Tools"
        ]
      }
    },
    "/api/tools/{id}": {
      "delete": {
        "operationId": "deleteApiToolsId",
        "parameters": [
          {
            "description": "The tool's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done, with no body."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: tool not found."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Deletes a tool from the kit list, and detaches it from every project it was taken to.",
        "tags": [
          "Tools"
        ]
      },
      "patch": {
        "operationId": "patchApiToolsId",
        "parameters": [
          {
            "description": "The tool's id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "With an API key: which of your companies and roles to act as, as membership_id or membership_id:role (\"12:manager\"), from GET /api/auth/me. Without it the key acts as the company it was made in. A company you are not in answers 404; with a session it is refused with 400.",
            "in": "header",
            "name": "X-Werzeick-Company",
            "required": false,
            "schema": {
              "pattern": "^[0-9]+(:(manager|worker))?$",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "The JSON body of a create or an update.\n\nNo membership or org field: both come from the session. A client that could name either could write somebody else's kit list.",
                "properties": {
                  "icon": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "quantity": {
                    "format": "int64",
                    "type": [
                      "integer",
                      "null"
                    ]
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "created_at": "2026-09-08T14:21:02.884113+02:00",
                      "gig_ids": [],
                      "icon": "other",
                      "id": 11,
                      "name": "Clamps",
                      "quantity": 6
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Tool"
                }
              }
            },
            "description": "OK."
          },
          "400": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: invalid JSON body; invalid {thing} id."
          },
          "401": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: authentication required."
          },
          "403": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: not permitted."
          },
          "404": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: tool not found."
          },
          "500": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text: internal server error."
          }
        },
        "summary": "Renames a tool, or changes its icon or quantity.",
        "tags": [
          "Tools"
        ]
      }
    },
    "/healthz": {
      "get": {
        "description": "The response body is deliberately free of detail: this endpoint is unauthenticated and reachable from the internet, so it says whether the service is up and nothing about why it is not.\n\nPublic: no key and no session needed.",
        "operationId": "getHealthz",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "example": {
                    "value": {
                      "status": "ok"
                    }
                  }
                },
                "schema": {
                  "additionalProperties": {
                    "type": "string"
                  },
                  "type": "object"
                }
              }
            },
            "description": "OK."
          },
          "503": {
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "A line of plain text saying why."
          }
        },
        "security": [],
        "summary": "Reports 200 when the server can reach its database, 503 otherwise.",
        "tags": [
          "Health"
        ]
      }
    }
  },
  "security": [
    {
      "apiKey": []
    }
  ],
  "servers": [
    {
      "url": "https://werzeick-dev.isc.hm"
    }
  ],
  "tags": [
    {
      "description": "Signing in and the session: a code by email, the session it buys, and the entry that session is in.\n\nA session is one membership in one role. Switching company, or role at the same company, is a new session; the old token is revoked. An API key is made in one entry too, and acts as it unless a request names another of the same person's in the X-Werzeick-Company header, as a membership_id or membership_id:role from memberships below; the server proves it is theirs on every request. No request carries a company's id: the server reads the company from the session or the named membership, which is what keeps one firm's data from another's.",
      "name": "Signing in and your session"
    },
    {
      "description": "Lets somebody delete their own account from the app.",
      "name": "Your account"
    },
    {
      "description": "How people join a company. There is no sign-up: a manager invites somebody by email, and the link in that email is accepted here. An address that already has an account gains a membership; one that does not becomes an account.",
      "name": "Invitations to a company"
    },
    {
      "description": "The worker's schedule lives here now, and so does taking an open shift, because that is where the app shows them.",
      "name": "Your week"
    },
    {
      "description": "The worker's side of staffing: what am I being asked to do, and my answer.\n\nA PM's staffing screens live in the web client for now; the same services back both, so a manager overview in the app is rendering work, not new logic.",
      "name": "Shifts and answers"
    },
    {
      "description": "Clocking in and out, from the app.\n\nThis is the endpoint a worker uses standing on a building site, often with one bar of signal, sometimes with none. Two consequences run through it:\n\n- The device supplies the timestamp, and the server takes it. Stamping an entry with the moment it happened to sync would be wrong by hours for exactly the worker who most needs the record to be right.\n\n- Every write is idempotent on the device's own event id, so a retry after a dropped connection is harmless.\n\nWhat is NOT here, and must never be: any coordinate. Geofence auto-clocking computes distance on the phone against the one site it was given and posts the same clock event a thumb would. source is the whole of the difference.",
      "name": "Clocking in and out"
    },
    {
      "description": "Projects, which the API calls gigs: the unit of work a company plans, from one shift to months, with a site, a client and the expenses booked against it. A manager sees every project at the company; a worker sees the ones they have a part in and the ones with a shift they could take.",
      "name": "Projects and expenses"
    },
    {
      "description": "Serves a worker's own kit list.\n\nEvery route here answers for the ACTING MEMBERSHIP and nobody else. There is no endpoint that lists another person's tools, or everybody's tools on a project, and there is no role that would change that — a PM view of who owns what is precisely what this feature does not have.",
      "name": "Tools"
    },
    {
      "description": "A project's calendar, for its managers: which days it runs, the shifts on each, and laying shifts on days or taking them off again. Removing work people have accepted asks first: without confirm nothing changes and the answer says how many people would be told. A worker sees none of this; the project's days reach a worker as their shifts.",
      "name": "Planning a project's calendar"
    },
    {
      "description": "Staffing as a manager does it: who is on a shift, inviting people to one shift or to many, approving an applicant, taking somebody off a shift or off the whole project, opening a shift to the company, raising an SOS, and the shifts short of people. An invitation is a question — nobody counts as staffed until they accept — and every removal tells the person once. The worker's side, answering and taking shifts, is elsewhere.",
      "name": "Staffing a project"
    },
    {
      "description": "The company's roster, for its managers: everyone with a membership at the company, what each may do here (manage projects, work shifts, or both), the default rate for anyone who works shifts, and what the company has noted each as certified for. An invitation to a shift names a person by the id listed here. Inviting somebody new to the company is the invitations endpoint.",
      "name": "People at the company"
    },
    {
      "description": "About certificates: the documents a person holds, a crane licence or a welding test, with their photos, and the kinds there are. The list of kinds is the server's, so that no client carries a copy that can drift. The certificates here are the caller's own, whichever company or role the key acts in: a certificate is the person's, and every company they work for sees the same one. A manager reads and adds a member's certificates on the people endpoints, and notes somebody as certified for one there too.",
      "name": "Certificates"
    },
    {
      "description": "The company's clients: the companies it invoices, each with the Czech tax identifiers an invoice needs. A project names one by its id. Clients are the company's own; one firm never sees another's.",
      "name": "Clients"
    },
    {
      "description": "A project's hours as its managers read them: every time entry on the project, whoever worked it, at the rate each entry snapshotted. Records and totals only: there is no approval step, no payslip and no tax here.",
      "name": "A project's hours"
    },
    {
      "description": "Answers the liveness check that uptime alerting polls.\n\nAvailability is a product risk here, not just an ops one: an SOS that cannot reach the server is emergency staffing that fails.",
      "name": "Health"
    }
  ]
}
