Werzeick API

API reference

Every endpoint of https://werzeick-dev.isc.hm/api/, generated from the server's own code: what each takes, what it answers with, and why it answers as it does. The words are the code's and are in English.

Every request but the public ones carries the key in a header, as the guide shows: Authorization: Bearer wzk_…

Download as OpenAPI 3.1 (JSON) · the same reference, for client generators and other tools.

It stays in this field and goes only to https://werzeick-dev.isc.hm/api/, in the header, never with your sign-in: a request sent from here is the key's and nobody else's. A request that writes asks twice.

Signing in and your session

Signing in and the session: a code by email, the session it buys, and the entry that session is in.

A 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.

POST /api/auth/request-code

Emails a sign-in code.

Public: no key and no session needed.

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.

Another 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.

Request body

FieldTypeWhat it is
email string

Response

Request code response

Example
{
  "status": "sent if an account exists",
  "resend_after_seconds": 30
}

Status codes

CodeMessage
202
400email is required · invalid JSON body
429a code was sent moments ago; wait {seconds} seconds before asking for another · too many codes requested; wait a few minutes
500internal server error
Try it

POST /api/auth/verify

Exchanges a code for a session.

Public: no key and no session needed.

Request body

FieldTypeWhat it is
email string
code string

Response

Session

Example
{
  "membership": {
    "membership_id": 12,
    "org_name": "Alpha Stavby",
    "role": "worker",
    "first_name": "Petra",
    "last_name": "Nováková",
    "emergency_responses_enabled": true,
    "cancellation_emails_enabled": true
  },
  "memberships": [
    {
      "membership_id": 12,
      "org_name": "Alpha Stavby",
      "role": "worker",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": true,
      "cancellation_emails_enabled": true
    },
    {
      "membership_id": 27,
      "org_name": "Beta Montáže",
      "role": "manager",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": false,
      "cancellation_emails_enabled": false
    },
    {
      "membership_id": 27,
      "org_name": "Beta Montáže",
      "role": "worker",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": false,
      "cancellation_emails_enabled": false
    }
  ],
  "language": "cs"
}

Status codes

CodeMessage
200
400email and code are required · invalid JSON body
401that code is not valid
403this account is not a member of any organization
500internal server error
Try it

POST /api/auth/sign-out

Revokes the session.

A session only: an API key is refused with 403.

It succeeds even for a token that was already invalid: signing out must never fail.

Response

No body.

Status codes

CodeMessage
204
500internal server error

GET /api/auth/me

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.

An API key or a session.

Response

Session

Example
{
  "membership": {
    "membership_id": 12,
    "org_name": "Alpha Stavby",
    "role": "worker",
    "first_name": "Petra",
    "last_name": "Nováková",
    "emergency_responses_enabled": true,
    "cancellation_emails_enabled": true
  },
  "memberships": [
    {
      "membership_id": 12,
      "org_name": "Alpha Stavby",
      "role": "worker",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": true,
      "cancellation_emails_enabled": true
    },
    {
      "membership_id": 27,
      "org_name": "Beta Montáže",
      "role": "manager",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": false,
      "cancellation_emails_enabled": false
    },
    {
      "membership_id": 27,
      "org_name": "Beta Montáže",
      "role": "worker",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": false,
      "cancellation_emails_enabled": false
    }
  ],
  "language": "cs"
}

Status codes

CodeMessage
200
401authentication required
500internal server error
Try it

POST /api/auth/switch

Moves the caller to another of their entries — another company, or the other role at the same one — rotating the session.

A session only: an API key is refused with 403.

The old token stops working immediately, so nothing cached against the previous organization or role can outlive the switch.

A 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.

Request body

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.

FieldTypeWhat it is
membership_id integer
role string

Response

Session

Example
{
  "membership": {
    "membership_id": 12,
    "org_name": "Alpha Stavby",
    "role": "worker",
    "first_name": "Petra",
    "last_name": "Nováková",
    "emergency_responses_enabled": true,
    "cancellation_emails_enabled": true
  },
  "memberships": [
    {
      "membership_id": 12,
      "org_name": "Alpha Stavby",
      "role": "worker",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": true,
      "cancellation_emails_enabled": true
    },
    {
      "membership_id": 27,
      "org_name": "Beta Montáže",
      "role": "manager",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": false,
      "cancellation_emails_enabled": false
    },
    {
      "membership_id": 27,
      "org_name": "Beta Montáže",
      "role": "worker",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": false,
      "cancellation_emails_enabled": false
    }
  ],
  "language": "cs"
}

Status codes

CodeMessage
200
400invalid JSON body · role must be worker or manager
401authentication required
403not permitted
500internal server error

PUT /api/auth/me/emergency-responses

Records whether this worker wants to hear about emergency cover at the company they are acting as.

An API key or a session.

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.

Request body

FieldTypeWhat it is
enabled boolean, or null

Response

Session

Example
{
  "membership": {
    "membership_id": 12,
    "org_name": "Alpha Stavby",
    "role": "worker",
    "first_name": "Petra",
    "last_name": "Nováková",
    "emergency_responses_enabled": true,
    "cancellation_emails_enabled": true
  },
  "memberships": [
    {
      "membership_id": 12,
      "org_name": "Alpha Stavby",
      "role": "worker",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": true,
      "cancellation_emails_enabled": true
    },
    {
      "membership_id": 27,
      "org_name": "Beta Montáže",
      "role": "manager",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": false,
      "cancellation_emails_enabled": false
    },
    {
      "membership_id": 27,
      "org_name": "Beta Montáže",
      "role": "worker",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": false,
      "cancellation_emails_enabled": false
    }
  ],
  "language": "cs"
}

Status codes

CodeMessage
200
400enabled is required · invalid JSON body
401authentication required
403only a worker can take emergency shifts
500internal server error
Try it

PUT /api/auth/me/cancellation-emails

Records whether this worker wants a cancelled shift emailed as well as pushed, at the company they are acting as.

An API key or a session.

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.

Request body

FieldTypeWhat it is
enabled boolean, or null

Response

Session

Example
{
  "membership": {
    "membership_id": 12,
    "org_name": "Alpha Stavby",
    "role": "worker",
    "first_name": "Petra",
    "last_name": "Nováková",
    "emergency_responses_enabled": true,
    "cancellation_emails_enabled": true
  },
  "memberships": [
    {
      "membership_id": 12,
      "org_name": "Alpha Stavby",
      "role": "worker",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": true,
      "cancellation_emails_enabled": true
    },
    {
      "membership_id": 27,
      "org_name": "Beta Montáže",
      "role": "manager",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": false,
      "cancellation_emails_enabled": false
    },
    {
      "membership_id": 27,
      "org_name": "Beta Montáže",
      "role": "worker",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": false,
      "cancellation_emails_enabled": false
    }
  ],
  "language": "cs"
}

Status codes

CodeMessage
200
400enabled is required · invalid JSON body
401authentication required
403only a worker is sent cancellations
500internal server error
Try it

PUT /api/auth/me/language

Records the account's choice of language: PUT /api/auth/me/language, {"language": "cs"}.

A session only: an API key is refused with 403.

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.

It names no user: the account is the session's. Answered with the whole session, like the other settings here.

Request body

FieldTypeWhat it is
language string

Response

Session

Example
{
  "membership": {
    "membership_id": 12,
    "org_name": "Alpha Stavby",
    "role": "worker",
    "first_name": "Petra",
    "last_name": "Nováková",
    "emergency_responses_enabled": true,
    "cancellation_emails_enabled": true
  },
  "memberships": [
    {
      "membership_id": 12,
      "org_name": "Alpha Stavby",
      "role": "worker",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": true,
      "cancellation_emails_enabled": true
    },
    {
      "membership_id": 27,
      "org_name": "Beta Montáže",
      "role": "manager",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": false,
      "cancellation_emails_enabled": false
    },
    {
      "membership_id": 27,
      "org_name": "Beta Montáže",
      "role": "worker",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": false,
      "cancellation_emails_enabled": false
    }
  ],
  "language": "cs"
}

Status codes

CodeMessage
200
400invalid JSON body · language must be cs or en
401authentication required
500internal server error

Your account

Lets somebody delete their own account from the app.

GET /api/auth/me/deletion

Says what deleting the caller's account would do, and changes nothing.

A session only: an API key is refused with 403.

The app asks this before it asks the person, so the numbers are in the question.

Response

Deletion

Example
{
  "can_delete": false,
  "companies": [
    {
      "org_name": "Alpha Stavby",
      "upcoming_shifts": 2,
      "invitations": 1,
      "clocked_in": true,
      "last_manager": false
    },
    {
      "org_name": "Beta Montáže",
      "upcoming_shifts": 0,
      "invitations": 0,
      "clocked_in": false,
      "last_manager": true
    }
  ]
}

Status codes

CodeMessage
200
401authentication required
500internal server error

DELETE /api/auth/me

Erases the caller's account.

A session only: an API key is refused with 403.

204 when it is gone; every token the person held stops working with it, this one included.

The 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.

Response

No body.

Status codes

CodeMessage
204
401authentication required
409
500internal server error

Invitations to a company

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.

POST /api/invitations/preview

Describes an invitation to whoever holds its link: which company, what they are invited to do, and the rate offered.

Public: no key and no session needed.

Nothing is accepted.

Request body

Carries the invitation's token, for Preview and Accept.

Public 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.

FieldTypeWhat it is
token string

Response

Preview invitation

With an hourly rate
{
  "org_name": "Alpha Stavby",
  "is_manager": false,
  "is_worker": true,
  "first_name": "Petra",
  "email": "petra@example.test",
  "rate_amount": 4200000,
  "rate_currency": "CZK"
}
Without one, to both roles
{
  "org_name": "Beta Montáže",
  "is_manager": true,
  "is_worker": true,
  "first_name": "Petra",
  "email": "petra@example.test",
  "rate_amount": null,
  "rate_currency": null
}

Status codes

CodeMessage
200
400invalid JSON body
404this invitation is invalid, expired, or already used
500internal server error
Try it

POST /api/invitations/accept

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.

Public: no key and no session needed.

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.

Request body

Carries the invitation's token, for Preview and Accept.

Public 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.

FieldTypeWhat it is
token string

Response

Acceptance

Accepting made the account: signed in
{
  "outcome": "signed_in",
  "email": "petra@example.test",
  "code_sent": false,
  "session": {
    "token": "not-a-real-token-only-this-fixture-has-it",
    "expires_at": "2026-11-06T07:30:00Z",
    "membership": {
      "membership_id": 12,
      "org_name": "Alpha Stavby",
      "role": "worker",
      "first_name": "Petra",
      "last_name": "Nováková",
      "emergency_responses_enabled": false,
      "cancellation_emails_enabled": true
    },
    "memberships": [
      {
        "membership_id": 12,
        "org_name": "Alpha Stavby",
        "role": "worker",
        "first_name": "Petra",
        "last_name": "Nováková",
        "emergency_responses_enabled": false,
        "cancellation_emails_enabled": true
      }
    ],
    "language": null
  },
  "membership": null
}
The account existed: a code is on its way
{
  "outcome": "needs_code",
  "email": "petra@example.test",
  "code_sent": true,
  "session": null,
  "membership": null
}
Already signed in as that account: joined
{
  "outcome": "joined",
  "email": "petra@example.test",
  "code_sent": false,
  "session": null,
  "membership": {
    "membership_id": 27,
    "org_name": "Beta Montáže",
    "role": "manager",
    "first_name": "Petra",
    "last_name": "Nováková",
    "emergency_responses_enabled": false,
    "cancellation_emails_enabled": true
  }
}

Status codes

CodeMessage
200
400invalid JSON body
404this invitation is invalid, expired, or already used
500internal server error
Try it

POST /api/invitations

Offers someone a job at the acting manager's company.

An API key or a session.

There is no org field: the company comes from the session, so a manager cannot invite into a firm they do not belong to.

Request body

FieldTypeWhat it is
email string
first_name string
last_name string
is_manager boolean

What they are invited to do: either, or both. At least one must be true.

is_worker boolean
rate_amount integer, or null

Money crosses the wire as a raw int64 with 4 implied decimals plus its currency, never pre-formatted and never a float.

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

rate_currency string, or null

Response

Invitation

Status codes

CodeMessage
201
400a 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
401authentication required
403not permitted
409that person is already in your organization
500internal server error
Try it

GET /api/invitations

Shows who has been invited but has not yet accepted.

An API key or a session.

Response

list of Invitation

Status codes

CodeMessage
200
401authentication required
403not permitted
500internal server error
Try it

Your week

The worker's schedule lives here now, and so does taking an open shift, because that is where the app shows them.

GET /api/dashboard

Returns the caller's dashboard at the active company.

An API key or a session.

A value that is not a number is a 400 rather than quietly the default: the app sent something it did not mean.

Query parameters

  • weeks

Response

Dashboard

A worker's
{
  "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
    }
  }
}
A manager's
{
  "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"
}

Status codes

CodeMessage
200
400weeks must be a whole number
401authentication required
403not permitted
500internal server error
Try it

POST /api/timeslots/{id}/take

Puts the caller on a shift a PM opened to the company.

An API key or a session.

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.

What 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.

In the path

ParameterWhat it is
idThe shift's id.

Request body

FieldTypeWhat it is
partial boolean
role_id integer

Response

Assignment

Example
{
  "id": 2,
  "timeslot_id": 2,
  "membership_id": 2,
  "origin": "sos",
  "status": "accepted",
  "rate_amount": 6300000,
  "rate_currency": "CZK",
  "invited_at": "2026-09-07T09:00:00Z",
  "responded_at": "2026-09-07T09:05:00Z",
  "expires_at": "2026-09-10T05:00:00Z",
  "created_at": "2026-09-07T09:00:00Z",
  "takes_from": "2026-09-10T09:00:00Z",
  "takes_until": "2026-09-10T13:00:00Z",
  "role_id": 2,
  "role_label": "crane op"
}

Status codes

CodeMessage
201
400invalid timeslot id · this shift has several roles; send role_id
401authentication required
403not permitted · this shift raises an hourly rate, and you have none here to raise
404shift not found
409
500internal server error
Try it

Shifts and answers

The worker's side of staffing: what am I being asked to do, and my answer.

A 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.

GET /api/schedule

Returns the caller's own assignments at the active company.

An API key or a session.

Response

list of Schedule entry

Example
[
  {
    "id": 1,
    "timeslot_id": 1,
    "membership_id": 2,
    "origin": "staffed",
    "status": "pending",
    "rate_amount": 4200000,
    "rate_currency": "CZK",
    "invited_at": "2026-09-07T08:00:00Z",
    "responded_at": null,
    "expires_at": "2026-09-09T07:00:00Z",
    "created_at": "2026-09-07T08:00:00Z",
    "takes_from": null,
    "takes_until": null,
    "role_id": 1,
    "role_label": null,
    "timeslot": {
      "id": 1,
      "gig_id": 1,
      "day_id": 1,
      "starts_at": "2026-09-09T07:00:00Z",
      "ends_at": "2026-09-09T15:00:00Z",
      "required_workers": 2,
      "role_label": null,
      "created_at": "2026-09-07T08:00:00Z",
      "accepted": 0,
      "pending": 1
    },
    "gig_id": 1,
    "gig_title": "Warehouse roof",
    "full": false,
    "collides_with_title": "Depot fit-out",
    "collides_with_company": null,
    "collides_from": "2026-09-09T05:00:00Z",
    "collides_until": "2026-09-09T11:00:00Z",
    "can_take_from": "2026-09-09T11:00:00Z",
    "can_take_until": "2026-09-09T15:00:00Z"
  },
  {
    "id": 2,
    "timeslot_id": 2,
    "membership_id": 2,
    "origin": "sos",
    "status": "accepted",
    "rate_amount": 6300000,
    "rate_currency": "CZK",
    "invited_at": "2026-09-07T09:00:00Z",
    "responded_at": "2026-09-07T09:05:00Z",
    "expires_at": "2026-09-10T05:00:00Z",
    "created_at": "2026-09-07T09:00:00Z",
    "takes_from": "2026-09-10T09:00:00Z",
    "takes_until": "2026-09-10T13:00:00Z",
    "role_id": 2,
    "role_label": "crane op",
    "timeslot": {
      "id": 2,
      "gig_id": 2,
      "day_id": 2,
      "starts_at": "2026-09-10T05:00:00Z",
      "ends_at": "2026-09-10T13:00:00Z",
      "required_workers": 1,
      "role_label": "crane op",
      "created_at": "2026-09-07T09:00:00Z",
      "accepted": 1,
      "pending": 0
    },
    "gig_id": 2,
    "gig_title": "Depot fit-out",
    "full": false,
    "collides_with_title": null,
    "collides_with_company": null,
    "collides_from": null,
    "collides_until": null,
    "can_take_from": null,
    "can_take_until": null
  },
  {
    "id": 3,
    "timeslot_id": 3,
    "membership_id": 2,
    "origin": "open",
    "status": "pending",
    "rate_amount": 6300000,
    "rate_currency": "CZK",
    "invited_at": "2026-09-07T10:00:00Z",
    "responded_at": null,
    "expires_at": "2026-09-11T05:00:00Z",
    "created_at": "2026-09-07T10:00:00Z",
    "takes_from": null,
    "takes_until": null,
    "role_id": 3,
    "role_label": null,
    "timeslot": {
      "id": 3,
      "gig_id": 1,
      "day_id": 3,
      "starts_at": "2026-09-11T05:00:00Z",
      "ends_at": "2026-09-11T13:00:00Z",
      "required_workers": 2,
      "role_label": null,
      "created_at": "2026-09-07T10:00:00Z",
      "accepted": 1,
      "pending": 1
    },
    "gig_id": 1,
    "gig_title": "Warehouse roof",
    "full": false,
    "collides_with_title": null,
    "collides_with_company": null,
    "collides_from": null,
    "collides_until": null,
    "can_take_from": null,
    "can_take_until": null
  },
  {
    "id": 4,
    "timeslot_id": 4,
    "membership_id": 2,
    "origin": "staffed",
    "status": "pending",
    "rate_amount": null,
    "rate_currency": null,
    "invited_at": "2026-09-07T11:00:00Z",
    "responded_at": null,
    "expires_at": "2026-09-14T05:00:00Z",
    "created_at": "2026-09-07T11:00:00Z",
    "takes_from": null,
    "takes_until": null,
    "role_id": 4,
    "role_label": null,
    "timeslot": {
      "id": 4,
      "gig_id": 1,
      "day_id": 4,
      "starts_at": "2026-09-14T05:00:00Z",
      "ends_at": "2026-09-14T13:00:00Z",
      "required_workers": 2,
      "role_label": null,
      "created_at": "2026-09-07T11:00:00Z",
      "accepted": 2,
      "pending": 1
    },
    "gig_id": 1,
    "gig_title": "Warehouse roof",
    "full": true,
    "collides_with_title": null,
    "collides_with_company": null,
    "collides_from": null,
    "collides_until": null,
    "can_take_from": null,
    "can_take_until": null
  }
]

Status codes

CodeMessage
200
401authentication required
403not permitted
500internal server error
Try it

POST /api/assignments/{id}/respond

Records a worker's answer to one assignment.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe assignment's id: your place on a shift.

Request body

FieldTypeWhat it is
action string
partial boolean

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.

Response

Assignment

Example
{
  "id": 2,
  "timeslot_id": 2,
  "membership_id": 2,
  "origin": "sos",
  "status": "accepted",
  "rate_amount": 6300000,
  "rate_currency": "CZK",
  "invited_at": "2026-09-07T09:00:00Z",
  "responded_at": "2026-09-07T09:05:00Z",
  "expires_at": "2026-09-10T05:00:00Z",
  "created_at": "2026-09-07T09:00:00Z",
  "takes_from": "2026-09-10T09:00:00Z",
  "takes_until": "2026-09-10T13:00:00Z",
  "role_id": 2,
  "role_label": "crane op"
}

Status codes

CodeMessage
200
400action must be "accept", "decline" or "withdraw" · invalid JSON body · invalid assignment id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found
409that 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
500internal server error
Try it

POST /api/gigs/{id}/invitations/respond

Answers every invitation the caller has not answered on one project: "Accept all" and "Decline all" beside a project's invitations.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe project's id.

Request body

FieldTypeWhat it is
action string

Response

Answered invitations

Example
{
  "answered": 2,
  "full": 1,
  "colliding": 0
}

Status codes

CodeMessage
200
400action must be "accept" or "decline" · invalid JSON body · invalid project id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found
409that 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
500internal server error
Try it

POST /api/devices

Records an FCM token so this phone can be reached.

A session only: an API key is refused with 403.

Request body

FieldTypeWhat it is
token string
platform string
language string

Response

No body.

Status codes

CodeMessage
204
400invalid JSON body
401authentication required

DELETE /api/devices

Drops this phone's token, on sign-out.

A session only: an API key is refused with 403.

Without it, a worker who signs out — or hands the phone on — keeps receiving shift notifications for a session they no longer have.

Request body

FieldTypeWhat it is
token string

Response

No body.

Status codes

CodeMessage
204
400a device token is required · invalid JSON body
401authentication required
403not permitted
500internal server error

Clocking in and out

Clocking in and out, from the app.

This 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:

  • 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.
  • Every write is idempotent on the device's own event id, so a retry after a dropped connection is harmless.

What 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.

POST /api/time/clock-in

Starts a stretch of work.

An API key or a session.

Request body

The entire wire shape of a clock event. Keep it that way.

FieldTypeWhat it is
gig_id integer
at string

at is the device's clock, RFC 3339 with an offset. Omitted means now.

source string

source is "manual" or "geofence"; empty means manual.

client_event_id string

client_event_id is the device's id for this event, and the reason a replayed push creates one row rather than two.

Response

Time entry

Example
{
  "id": 2,
  "membership_id": 2,
  "gig_id": 1,
  "timeslot_id": null,
  "clock_in_at": "2026-09-09T06:20:00Z",
  "clock_out_at": null,
  "source": "geofence",
  "rate_amount": 4200000,
  "rate_currency": "CZK",
  "earned_amount": 0,
  "corrected_at": null,
  "created_at": "2026-09-09T06:20:00Z"
}

Status codes

CodeMessage
200
400at must be an RFC 3339 timestamp · invalid JSON body
401authentication required
403not permitted
404time entry not found
409
500internal server error
Try it

POST /api/time/clock-out

Ends one, naming it by the device's event id when the phone has been offline and by project otherwise.

An API key or a session.

Request body

The entire wire shape of a clock event. Keep it that way.

FieldTypeWhat it is
gig_id integer
at string

at is the device's clock, RFC 3339 with an offset. Omitted means now.

source string

source is "manual" or "geofence"; empty means manual.

client_event_id string

client_event_id is the device's id for this event, and the reason a replayed push creates one row rather than two.

Response

Time entry

Example
{
  "id": 1,
  "membership_id": 2,
  "gig_id": 1,
  "timeslot_id": 1,
  "clock_in_at": "2026-09-08T07:45:00Z",
  "clock_out_at": "2026-09-08T17:10:00Z",
  "source": "manual",
  "rate_amount": 4200000,
  "rate_currency": "CZK",
  "earned_amount": 39550000,
  "client_event_id": "9f8c1a2b3d4e5f60718293a4b5c6d7e8",
  "corrected_at": null,
  "created_at": "2026-09-08T07:45:00Z"
}

Status codes

CodeMessage
200
400at must be an RFC 3339 timestamp · clock-out needs a gig_id or a client_event_id · invalid JSON body
401authentication required
403not permitted
404time entry not found
409
500internal server error
Try it

GET /api/time/entries

The worker's own record of their hours.

An API key or a session.

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.

Query parameters

  • since

Response

list of Time entry

Example
[
  {
    "id": 1,
    "membership_id": 2,
    "gig_id": 1,
    "timeslot_id": 1,
    "clock_in_at": "2026-09-08T07:45:00Z",
    "clock_out_at": "2026-09-08T17:10:00Z",
    "source": "manual",
    "rate_amount": 4200000,
    "rate_currency": "CZK",
    "earned_amount": 39550000,
    "client_event_id": "9f8c1a2b3d4e5f60718293a4b5c6d7e8",
    "corrected_at": null,
    "created_at": "2026-09-08T07:45:00Z"
  },
  {
    "id": 2,
    "membership_id": 2,
    "gig_id": 1,
    "timeslot_id": null,
    "clock_in_at": "2026-09-09T06:20:00Z",
    "clock_out_at": null,
    "source": "geofence",
    "rate_amount": 4200000,
    "rate_currency": "CZK",
    "earned_amount": 0,
    "corrected_at": null,
    "created_at": "2026-09-09T06:20:00Z"
  },
  {
    "id": 3,
    "membership_id": 2,
    "gig_id": 1,
    "timeslot_id": null,
    "clock_in_at": "2026-09-10T06:00:00Z",
    "clock_out_at": "2026-09-10T14:30:00Z",
    "source": "manual",
    "rate_amount": null,
    "rate_currency": null,
    "earned_amount": null,
    "corrected_at": null,
    "created_at": "2026-09-10T06:00:00Z"
  }
]

Status codes

CodeMessage
200
400since must be an RFC 3339 timestamp
401authentication required
403not permitted
404time entry not found
409
500internal server error
Try it

POST /api/time/entries/{id}/correct

Fixes an entry's times.

An API key or a session.

A worker owns their own hours; a manager may resolve anyone's, which is how discrepancies get settled.

In the path

ParameterWhat it is
idThe time entry's id.

Request body

FieldTypeWhat it is
clock_in_at string
clock_out_at string, or null

Response

Time entry

Example
{
  "id": 1,
  "membership_id": 2,
  "gig_id": 1,
  "timeslot_id": 1,
  "clock_in_at": "2026-09-08T07:45:00Z",
  "clock_out_at": "2026-09-08T17:10:00Z",
  "source": "manual",
  "rate_amount": 4200000,
  "rate_currency": "CZK",
  "earned_amount": 39550000,
  "client_event_id": "9f8c1a2b3d4e5f60718293a4b5c6d7e8",
  "corrected_at": null,
  "created_at": "2026-09-08T07:45:00Z"
}

Status codes

CodeMessage
200
400clock_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
401authentication required
403not permitted
404time entry not found
409
500internal server error
Try it

Projects and expenses

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.

GET /api/gigs

Lists the caller's projects at the company they are acting in.

An API key or a session.

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.

all=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.

Query parameters

  • all

Response

list of Project

Example
{
  "id": 3,
  "title": "Foundation pour",
  "owner_id": 4,
  "project_manager": {
    "name": "Martin Dvořák",
    "phone": "+420603456789",
    "email": null
  },
  "client_id": 7,
  "client_name": "Kaufland Česká republika",
  "client_contact_name": "Petra Marková",
  "client_contact_phone": "+420602123456",
  "client_contact_email": "petra.markova@kaufland.cz",
  "job_contact": {
    "company": null,
    "name": "Jana Horáková, has the keys",
    "phone": "+420777111222"
  },
  "latitude": 49.8209,
  "longitude": 18.2625,
  "geofence_radius_m": 150,
  "site_address": "Gate 3, Průmyslová 1472/12, 702 00 Ostrava",
  "expected_minutes": 480,
  "is_assigned": true,
  "is_invited": false,
  "shifts_to_take": 2,
  "archived_at": null,
  "expenses": [],
  "created_at": "2026-09-07T16:06:11.113221+02:00",
  "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
    }
  ]
}

Status codes

CodeMessage
200
400all=1 reads every company: leave out X-Werzeick-Company
401authentication required
403not permitted
500internal server error
Try it

GET /api/gigs/{id}

One project, in the same shape the list gives it.

An API key or a session.

A project the caller cannot list, another company's included, answers 404: it does not exist for them.

In the path

ParameterWhat it is
idThe project's id.

Response

Project

Example
{
  "id": 3,
  "title": "Foundation pour",
  "owner_id": 4,
  "project_manager": {
    "name": "Martin Dvořák",
    "phone": "+420603456789",
    "email": null
  },
  "client_id": 7,
  "client_name": "Kaufland Česká republika",
  "client_contact_name": "Petra Marková",
  "client_contact_phone": "+420602123456",
  "client_contact_email": "petra.markova@kaufland.cz",
  "job_contact": {
    "company": null,
    "name": "Jana Horáková, has the keys",
    "phone": "+420777111222"
  },
  "latitude": 49.8209,
  "longitude": 18.2625,
  "geofence_radius_m": 150,
  "site_address": "Gate 3, Průmyslová 1472/12, 702 00 Ostrava",
  "expected_minutes": 480,
  "is_assigned": true,
  "is_invited": false,
  "shifts_to_take": 2,
  "archived_at": null,
  "expenses": [],
  "created_at": "2026-09-07T16:06:11.113221+02:00",
  "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
    }
  ]
}

Status codes

CodeMessage
200
400invalid gig id
401authentication required
403not permitted
404gig not found
500internal server error
Try it

GET /api/gigs/{id}/earnings

Answers "what has this project paid me, and what will it".

An API key or a session.

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.

The two figures come from two different tables, each carrying its own rate snapshot, and neither is ever rebuilt from the membership rate.

In the path

ParameterWhat it is
idThe project's id.

Response

Earnings

Example
{
  "earned": [
    {
      "amount": 33600000,
      "currency": "CZK"
    }
  ],
  "upcoming": [
    {
      "amount": 50400000,
      "currency": "CZK"
    }
  ],
  "earned_unrated_minutes": 0,
  "upcoming_unrated_minutes": 360,
  "running": null
}

Status codes

CodeMessage
200
400invalid {thing} id
401authentication required
403not permitted
404gig not found
500internal server error
Try it

POST /api/gigs

Makes a project at the caller's company, with the caller as its manager.

An API key or a session.

Managers only.

Request body

The JSON body of POST /api/gigs.

There 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.

FieldTypeWhat it is
title string
client_id integer, or null
latitude number, or null
longitude number, or null
geofence_radius_m integer, or null
site_outline list of Point

Response

Project

Example
{
  "id": 2,
  "title": "Roof replacement",
  "owner_id": 1,
  "project_manager": {
    "name": "Tereza Nováková",
    "phone": null,
    "email": "tereza.novakova@example.com"
  },
  "client_id": null,
  "client_name": null,
  "client_contact_name": null,
  "client_contact_phone": null,
  "client_contact_email": null,
  "job_contact": null,
  "latitude": null,
  "longitude": null,
  "geofence_radius_m": null,
  "site_address": null,
  "expected_minutes": null,
  "is_assigned": false,
  "is_invited": true,
  "shifts_to_take": 0,
  "archived_at": null,
  "expenses": [
    {
      "id": 4,
      "gig_id": 2,
      "created_by_membership_id": 1,
      "created_by_name": "Tereza Nováková",
      "description": "Tiles",
      "cost_amount": 1250000,
      "cost_currency": "CZK",
      "created_at": "2026-09-07T12:52:03.941837+02:00"
    }
  ],
  "created_at": "2026-09-07T16:05:07.353539+02:00",
  "site_outline": null
}

Status codes

CodeMessage
201
400invalid JSON body · title is required
401authentication required
403not permitted
500internal server error
Try it

POST /api/gigs/{id}/expenses

Books a cost against a project.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe project's id.

Request body

The JSON body of a create or an update.

Money 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.

FieldTypeWhat it is
description string
cost_amount integer

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

cost_currency string

Response

Expense

Example
{
  "id": 4,
  "gig_id": 2,
  "created_by_membership_id": 1,
  "created_by_name": "Tereza Nováková",
  "description": "Tiles",
  "cost_amount": 1250000,
  "cost_currency": "CZK",
  "created_at": "2026-09-07T12:52:03.941837+02:00"
}

Status codes

CodeMessage
201
400invalid JSON body · invalid {thing} id
401authentication required
403not permitted
404expense not found · gig not found
500internal server error
Try it

PATCH /api/gigs/{id}/expenses/{expenseID}

Corrects a cost already booked.

An API key or a session.

In the path

ParameterWhat it is
idThe project's id.
expenseIDThe expense's id.

Request body

The JSON body of a create or an update.

Money 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.

FieldTypeWhat it is
description string
cost_amount integer

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

cost_currency string

Response

Expense

Example
{
  "id": 4,
  "gig_id": 2,
  "created_by_membership_id": 1,
  "created_by_name": "Tereza Nováková",
  "description": "Tiles",
  "cost_amount": 1250000,
  "cost_currency": "CZK",
  "created_at": "2026-09-07T12:52:03.941837+02:00"
}

Status codes

CodeMessage
200
400invalid JSON body · invalid {thing} id
401authentication required
403not permitted
404expense not found · gig not found
500internal server error
Try it

DELETE /api/gigs/{id}/expenses/{expenseID}

Deletes a cost.

An API key or a session.

In the path

ParameterWhat it is
idThe project's id.
expenseIDThe expense's id.

Response

No body.

Status codes

CodeMessage
204
400invalid {thing} id
401authentication required
403not permitted
404expense not found · gig not found
500internal server error
Try it

Tools

Serves a worker's own kit list.

Every 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.

GET /api/tool-icons

Returns the fixed set of icons the product ships.

Public: no key and no session needed.

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.

Response

list of Tool icon

Example
[
  {
    "name": "hammer",
    "label": "Hammer"
  },
  {
    "name": "wrench",
    "label": "Spanner"
  },
  {
    "name": "screwdriver",
    "label": "Screwdriver"
  },
  {
    "name": "pliers",
    "label": "Pliers"
  },
  {
    "name": "saw",
    "label": "Saw"
  },
  {
    "name": "trowel",
    "label": "Trowel"
  },
  {
    "name": "shovel",
    "label": "Shovel"
  },
  {
    "name": "drill",
    "label": "Drill"
  },
  {
    "name": "grinder",
    "label": "Angle grinder"
  },
  {
    "name": "breaker",
    "label": "Breaker"
  },
  {
    "name": "mixer",
    "label": "Mixer"
  },
  {
    "name": "measure",
    "label": "Tape measure"
  },
  {
    "name": "level",
    "label": "Spirit level"
  },
  {
    "name": "ladder",
    "label": "Ladder"
  },
  {
    "name": "paint",
    "label": "Paint roller"
  },
  {
    "name": "cable",
    "label": "Power lead"
  },
  {
    "name": "light",
    "label": "Work light"
  },
  {
    "name": "toolbox",
    "label": "Toolbox"
  },
  {
    "name": "safety",
    "label": "Safety gear"
  },
  {
    "name": "other",
    "label": "Other"
  }
]

Status codes

CodeMessage
200
Try it

GET /api/tools

The caller's own kit list at this company, with the ids of the projects each tool is attached to.

An API key or a session.

Response

list of Tool

Example
[
  {
    "id": 12,
    "name": "SDS drill",
    "icon": "drill",
    "quantity": null,
    "created_at": "2026-09-08T14:22:41.117204+02:00",
    "gig_ids": [
      3,
      7
    ]
  },
  {
    "id": 11,
    "name": "Clamps",
    "icon": "other",
    "quantity": 6,
    "created_at": "2026-09-08T14:21:02.884113+02:00",
    "gig_ids": []
  }
]

Status codes

CodeMessage
200
400
401authentication required
403not permitted
404tool not found
500internal server error
Try it

POST /api/tools

Adds a tool to the caller's kit list.

An API key or a session.

The icon is a name from GET /api/tool-icons; the quantity is optional.

Request body

The JSON body of a create or an update.

No membership or org field: both come from the session. A client that could name either could write somebody else's kit list.

FieldTypeWhat it is
name string
icon string
quantity integer, or null

Response

Tool

Example
{
  "id": 12,
  "name": "SDS drill",
  "icon": "drill",
  "quantity": null,
  "created_at": "2026-09-08T14:22:41.117204+02:00",
  "gig_ids": [
    3,
    7
  ]
}

Status codes

CodeMessage
201
400invalid JSON body
401authentication required
403not permitted
404tool not found
500internal server error
Try it

PATCH /api/tools/{id}

Renames a tool, or changes its icon or quantity.

An API key or a session.

In the path

ParameterWhat it is
idThe tool's id.

Request body

The JSON body of a create or an update.

No membership or org field: both come from the session. A client that could name either could write somebody else's kit list.

FieldTypeWhat it is
name string
icon string
quantity integer, or null

Response

Tool

Example
{
  "id": 11,
  "name": "Clamps",
  "icon": "other",
  "quantity": 6,
  "created_at": "2026-09-08T14:21:02.884113+02:00",
  "gig_ids": []
}

Status codes

CodeMessage
200
400invalid JSON body · invalid {thing} id
401authentication required
403not permitted
404tool not found
500internal server error
Try it

DELETE /api/tools/{id}

Deletes a tool from the kit list, and detaches it from every project it was taken to.

An API key or a session.

In the path

ParameterWhat it is
idThe tool's id.

Response

No body.

Status codes

CodeMessage
204
400invalid {thing} id
401authentication required
403not permitted
404tool not found
500internal server error
Try it

GET /api/gigs/{id}/tools

Returns the caller's own tools attached to one project — what they have to load into the van for this job.

An API key or a session.

In the path

ParameterWhat it is
idThe project's id.

Response

list of Tool

Example
[
  {
    "id": 12,
    "name": "SDS drill",
    "icon": "drill",
    "quantity": null,
    "created_at": "2026-09-08T14:22:41.117204+02:00",
    "gig_ids": [
      3,
      7
    ]
  },
  {
    "id": 11,
    "name": "Clamps",
    "icon": "other",
    "quantity": 6,
    "created_at": "2026-09-08T14:21:02.884113+02:00",
    "gig_ids": []
  }
]

Status codes

CodeMessage
200
400invalid {thing} id
401authentication required
403not permitted
404tool not found
500internal server error
Try it

POST /api/gigs/{id}/tools

Says "I am taking this to that job".

An API key or a session.

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.

In the path

ParameterWhat it is
idThe project's id.

Request body

Names the tool being taken to a project.

FieldTypeWhat it is
tool_id integer

Response

No body.

Status codes

CodeMessage
204
400invalid JSON body · invalid {thing} id
401authentication required
403not permitted
404tool not found
500internal server error
Try it

DELETE /api/gigs/{id}/tools/{toolID}

Removes the link and NOT the tool.

An API key or a session.

Coming off one job must not cost the worker the entry.

In the path

ParameterWhat it is
idThe project's id.
toolIDThe tool's id.

Response

No body.

Status codes

CodeMessage
204
400invalid {thing} id
401authentication required
403not permitted
404tool not found
500internal server error
Try it

Planning a project's calendar

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.

GET /api/gigs/{id}/calendar

A project's days in a range, each with its shifts, roles and staffing counts.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe project's id.

Query parameters

  • from
  • to

Response

list of Calendar day

Example
[
  {
    "day": "2026-09-10",
    "day_id": 1,
    "shifts": [
      {
        "id": 1,
        "gig_id": 1,
        "day_id": 1,
        "starts_at": "2026-09-10T05:00:00Z",
        "ends_at": "2026-09-10T13:00:00Z",
        "required_workers": 3,
        "role_label": "1× crane op, 2× labourer",
        "created_at": "2026-09-07T09:00:00Z",
        "accepted": 1,
        "pending": 1,
        "short": 2,
        "is_open": false,
        "open_auto_accept": false,
        "offered_rate_multiplier": null,
        "roles": [
          {
            "id": 1,
            "label": "crane op",
            "headcount": 1,
            "rate_amount": 5200000,
            "rate_currency": "CZK",
            "accepted": 1,
            "pending": 0
          },
          {
            "id": 2,
            "label": "labourer",
            "headcount": 2,
            "rate_amount": null,
            "rate_currency": null,
            "accepted": 0,
            "pending": 1
          }
        ]
      }
    ]
  },
  {
    "day": "2026-09-11",
    "day_id": 2,
    "shifts": []
  }
]

Status codes

CodeMessage
200
400from 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
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/gigs/{id}/days

Puts days on a project's calendar.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe project's id.

Request body

Names calendar days, "2026-10-27".

FieldTypeWhat it is
days list of string
confirm boolean

confirm is read by the removal only: without it nothing is removed and the answer says what would be.

Response

list of Scheduled day

Status codes

CodeMessage
200
400invalid JSON body · invalid {thing} id · name at least one day
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/gigs/{id}/days/remove

Takes days off a project's calendar, with their shifts.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe project's id.

Request body

Names calendar days, "2026-10-27".

FieldTypeWhat it is
days list of string
confirm boolean

confirm is read by the removal only: without it nothing is removed and the answer says what would be.

Response

Removed days

Status codes

CodeMessage
200
400invalid JSON body · invalid {thing} id · name at least one day
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/gigs/{id}/shifts

Lays one shift on each of the days named, with its roles.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe project's id.

Request body

One shift laid on each of several days.

FieldTypeWhat it is
days list of string

days are calendar days, "2026-10-27". Each gets a shift; a day not yet on the calendar is scheduled by it.

starts_at string

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.

ends_at string
roles list of Role to add

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.

headcount integer

headcount is the places on a plain shift, read only when roles is empty.

open boolean

open advertises every shift to the company at once; open_auto_accept confirms a taker outright rather than as an applicant.

open_auto_accept boolean
offered_rate_multiplier integer, or null

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.

A multiplier with four implied decimal places: 15000 is 1.5×.

Response

Added shifts

Example
{
  "id": 1,
  "gig_id": 1,
  "day_id": 1,
  "starts_at": "2026-09-10T05:00:00Z",
  "ends_at": "2026-09-10T13:00:00Z",
  "required_workers": 3,
  "role_label": "1× crane op, 2× labourer",
  "created_at": "2026-09-07T09:00:00Z",
  "accepted": 1,
  "pending": 1,
  "short": 2,
  "is_open": false,
  "open_auto_accept": false,
  "offered_rate_multiplier": null,
  "roles": [
    {
      "id": 1,
      "label": "crane op",
      "headcount": 1,
      "rate_amount": 5200000,
      "rate_currency": "CZK",
      "accepted": 1,
      "pending": 0
    },
    {
      "id": 2,
      "label": "labourer",
      "headcount": 2,
      "rate_amount": null,
      "rate_currency": null,
      "accepted": 0,
      "pending": 1
    }
  ]
}

Status codes

CodeMessage
201
400ends_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
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/gigs/{id}/shifts/move

Moves shifts to new dates, by a number of whole days.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe project's id.

Request body

Moves shifts by whole days.

FieldTypeWhat it is
timeslot_ids list of integer

timeslot_ids are the shifts to move, all of this project.

days integer

days is the offset in whole days, forwards or back; never zero.

confirm boolean

confirm says to go ahead. Without it nothing changes and the answer says what would.

Response

Moved shifts

Status codes

CodeMessage
200
400invalid JSON body · invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/timeslots/{id}/remove

Takes a shift off its day.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe shift's id.

Request body

A removal's consent.

FieldTypeWhat it is
confirm boolean

confirm says to go ahead. Without it nothing changes and the answer says what would.

Response

Removed shift

Status codes

CodeMessage
200
400invalid JSON body · invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

Staffing a project

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.

GET /api/timeslots/{id}

One shift, with its roles and everyone on it.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe shift's id.

Response

Shift with its people

Example
{
  "id": 1,
  "gig_id": 1,
  "day_id": 1,
  "starts_at": "2026-09-10T05:00:00Z",
  "ends_at": "2026-09-10T13:00:00Z",
  "required_workers": 3,
  "role_label": "1× crane op, 2× labourer",
  "created_at": "2026-09-07T09:00:00Z",
  "accepted": 1,
  "pending": 1,
  "short": 2,
  "is_open": true,
  "open_auto_accept": false,
  "offered_rate_multiplier": 15000,
  "roles": [
    {
      "id": 1,
      "label": "crane op",
      "headcount": 1,
      "rate_amount": 5200000,
      "rate_currency": "CZK",
      "accepted": 1,
      "pending": 0
    },
    {
      "id": 2,
      "label": "labourer",
      "headcount": 2,
      "rate_amount": null,
      "rate_currency": null,
      "accepted": 0,
      "pending": 1
    }
  ],
  "people": [
    {
      "id": 1,
      "timeslot_id": 1,
      "membership_id": 2,
      "origin": "staffed",
      "status": "accepted",
      "rate_amount": 5200000,
      "rate_currency": "CZK",
      "role_id": 1,
      "role_label": "crane op",
      "invited_at": "2026-09-07T09:00:00Z",
      "responded_at": "2026-09-07T09:05:00Z",
      "expires_at": "2026-09-10T05:00:00Z",
      "created_at": "2026-09-07T09:00:00Z",
      "takes_from": null,
      "takes_until": null,
      "worker_name": "Jana Horáková"
    },
    {
      "id": 2,
      "timeslot_id": 1,
      "membership_id": 3,
      "origin": "sos",
      "status": "pending",
      "rate_amount": null,
      "rate_currency": null,
      "role_id": 2,
      "role_label": "labourer",
      "invited_at": "2026-09-08T14:00:00Z",
      "responded_at": null,
      "expires_at": "2026-09-10T13:00:00Z",
      "created_at": "2026-09-08T14:00:00Z",
      "takes_from": null,
      "takes_until": null,
      "worker_name": "Petr Dvořák"
    }
  ]
}

Status codes

CodeMessage
200
400invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/timeslots/{id}/invitations

Asks one person to one shift.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe shift's id.

Request body

Names who to invite to a shift, and for which role.

FieldTypeWhat it is
membership_id integer

membership_id is the person, as the roster lists them.

role_id integer

role_id is the role on the shift they are invited for. It may be left out for a shift with one role.

Response

Assignment

Example
{
  "id": 2,
  "timeslot_id": 1,
  "membership_id": 3,
  "origin": "sos",
  "status": "pending",
  "rate_amount": null,
  "rate_currency": null,
  "role_id": 2,
  "role_label": "labourer",
  "invited_at": "2026-09-08T14:00:00Z",
  "responded_at": null,
  "expires_at": "2026-09-10T13:00:00Z",
  "created_at": "2026-09-08T14:00:00Z",
  "takes_from": null,
  "takes_until": null,
  "worker_name": "Petr Dvořák"
}

Status codes

CodeMessage
201
400invalid JSON body · invalid {thing} id · membership_id is required
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/gigs/{id}/invitations/batch

Asks several people to several shifts of one project at once.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe project's id.

Request body

Asks several people to several shifts at once.

FieldTypeWhat it is
membership_ids list of integer

membership_ids are the people, from the roster.

timeslot_ids list of integer

timeslot_ids are the shifts, all of this project.

role string, or null

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.

preview boolean

preview says to work out what the batch would do and send nothing.

Response

Invitation batch

Example
{
  "preview": false,
  "people": 2,
  "shifts": 2,
  "invitations": 3,
  "skipped": [
    {
      "membership_id": 2,
      "worker_name": "Jana Horáková",
      "shift": {
        "timeslot_id": 1,
        "starts_at": "2026-09-10T05:00:00Z",
        "ends_at": "2026-09-10T13:00:00Z"
      }
    }
  ],
  "over_filled": [
    {
      "shift": {
        "timeslot_id": 2,
        "starts_at": "2026-09-11T05:00:00Z",
        "ends_at": "2026-09-11T13:00:00Z"
      },
      "invited": 2,
      "places": 1
    }
  ],
  "clashes": [
    {
      "membership_id": 3,
      "worker_name": "Petr Dvořák",
      "shift": {
        "timeslot_id": 2,
        "starts_at": "2026-09-11T05:00:00Z",
        "ends_at": "2026-09-11T13:00:00Z"
      },
      "with": {
        "timeslot_id": 7,
        "starts_at": "2026-09-11T10:00:00Z",
        "ends_at": "2026-09-11T14:00:00Z"
      },
      "in_batch": false
    }
  ],
  "sent": [
    {
      "id": 3,
      "timeslot_id": 2,
      "membership_id": 2,
      "origin": "staffed",
      "status": "pending",
      "rate_amount": 4200000,
      "rate_currency": "CZK",
      "role_id": 3,
      "role_label": null,
      "invited_at": "2026-09-08T14:00:00Z",
      "responded_at": null,
      "expires_at": "2026-09-11T05:00:00Z",
      "created_at": "2026-09-08T14:00:00Z",
      "takes_from": null,
      "takes_until": null,
      "worker_name": "Jana Horáková"
    }
  ]
}

Status codes

CodeMessage
200
400invalid JSON body · invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/assignments/{id}/approve

Confirms an applicant on a shift the manager reviews takers for: an open shift or an SOS.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe assignment's id: your place on a shift.

Response

Assignment

Example
{
  "id": 1,
  "timeslot_id": 1,
  "membership_id": 2,
  "origin": "staffed",
  "status": "accepted",
  "rate_amount": 5200000,
  "rate_currency": "CZK",
  "role_id": 1,
  "role_label": "crane op",
  "invited_at": "2026-09-07T09:00:00Z",
  "responded_at": "2026-09-07T09:05:00Z",
  "expires_at": "2026-09-10T05:00:00Z",
  "created_at": "2026-09-07T09:00:00Z",
  "takes_from": null,
  "takes_until": null,
  "worker_name": "Jana Horáková"
}

Status codes

CodeMessage
200
400invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/assignments/{id}/cancel

Takes one person off one shift.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe assignment's id: your place on a shift.

Request body

A removal's consent.

FieldTypeWhat it is
confirm boolean

confirm says to go ahead. Without it nothing changes and the answer says what would.

Response

Assignment

Status codes

CodeMessage
200
400invalid JSON body · invalid {thing} id · send confirm: this cancels their place and tells them
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

GET /api/gigs/{id}/people

Lists everyone with a live place on a project's shifts still to come: accepted, or asked and not yet answered, each once.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe project's id.

Response

list of Person on a project

Status codes

CodeMessage
200
400invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/gigs/{id}/people/{membershipID}/remove

Takes one person off every upcoming shift of a project at once.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe project's id.
membershipIDThe person's membership id, as the roster lists it.

Request body

A removal's consent.

FieldTypeWhat it is
confirm boolean

confirm says to go ahead. Without it nothing changes and the answer says what would.

Response

Taken off a project

Status codes

CodeMessage
200
400invalid JSON body · invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/timeslots/{id}/open

Advertises a shift to the company: any worker there may take a place on it without being invited.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe shift's id.

Request body

The terms a shift is opened on.

FieldTypeWhat it is
auto_accept boolean

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.

offered_rate_multiplier integer, or null

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.

A multiplier with four implied decimal places: 15000 is 1.5×.

Response

Shift as a manager sees it

Example
{
  "id": 1,
  "gig_id": 1,
  "day_id": 1,
  "starts_at": "2026-09-10T05:00:00Z",
  "ends_at": "2026-09-10T13:00:00Z",
  "required_workers": 3,
  "role_label": "1× crane op, 2× labourer",
  "created_at": "2026-09-07T09:00:00Z",
  "accepted": 1,
  "pending": 1,
  "short": 2,
  "is_open": true,
  "open_auto_accept": false,
  "offered_rate_multiplier": 15000,
  "roles": [
    {
      "id": 1,
      "label": "crane op",
      "headcount": 1,
      "rate_amount": 5200000,
      "rate_currency": "CZK",
      "accepted": 1,
      "pending": 0
    },
    {
      "id": 2,
      "label": "labourer",
      "headcount": 2,
      "rate_amount": null,
      "rate_currency": null,
      "accepted": 0,
      "pending": 1
    }
  ]
}

Status codes

CodeMessage
200
400a multiplier must be more than nothing · invalid JSON body · invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/timeslots/{id}/close

Stops advertising a shift.

An API key or a session.

The people already on it stay on it. Managers only.

In the path

ParameterWhat it is
idThe shift's id.

Response

Shift as a manager sees it

Example
{
  "id": 1,
  "gig_id": 1,
  "day_id": 1,
  "starts_at": "2026-09-10T05:00:00Z",
  "ends_at": "2026-09-10T13:00:00Z",
  "required_workers": 3,
  "role_label": "1× crane op, 2× labourer",
  "created_at": "2026-09-07T09:00:00Z",
  "accepted": 1,
  "pending": 1,
  "short": 2,
  "is_open": false,
  "open_auto_accept": false,
  "offered_rate_multiplier": null,
  "roles": [
    {
      "id": 1,
      "label": "crane op",
      "headcount": 1,
      "rate_amount": 5200000,
      "rate_currency": "CZK",
      "accepted": 1,
      "pending": 0
    },
    {
      "id": 2,
      "label": "labourer",
      "headcount": 2,
      "rate_amount": null,
      "rate_currency": null,
      "accepted": 0,
      "pending": 1
    }
  ]
}

Status codes

CodeMessage
200
400invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/timeslots/{id}/sos

Raises an emergency on one short shift.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe shift's id.

Request body

The terms an SOS is raised on.

FieldTypeWhat it is
offered_rate_multiplier integer, or null

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.

A multiplier with four implied decimal places: 15000 is 1.5×.

auto_accept boolean

auto_accept confirms a responder outright, up to the role's headcount; without it responders are applicants for the manager to approve.

Response

SOS raised

Example
{
  "id": 1,
  "timeslot_id": 1,
  "offered_rate_multiplier": 15000,
  "opened_at": "2026-09-09T16:00:00Z",
  "told": 0
}

Status codes

CodeMessage
201
400a multiplier must be more than nothing · invalid JSON body · invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/gigs/{id}/sos

Raises one ordinary SOS on each of several short shifts of a project, in one go, and tells each person once.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe project's id.

Request body

Raises an SOS over a project's short shifts.

FieldTypeWhat it is
offered_rate_multiplier integer, or null

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.

A multiplier with four implied decimal places: 15000 is 1.5×.

auto_accept boolean

auto_accept confirms a responder outright, up to the role's headcount; without it responders are applicants for the manager to approve.

timeslot_ids list of integer

timeslot_ids are the shifts to cover, all of this project. Empty means every short shift of the project that is not over.

Response

SOS over a project

Example
{
  "raised": [
    {
      "id": 1,
      "timeslot_id": 1,
      "offered_rate_multiplier": 15000,
      "opened_at": "2026-09-09T16:00:00Z",
      "told": 0
    }
  ],
  "already_raised": 0,
  "filled": 1,
  "over": 0,
  "told": 4
}

Status codes

CodeMessage
201
400a multiplier must be more than nothing · invalid JSON body · invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

GET /api/staffing/short

Lists the company's upcoming shifts that need attention: short of accepted people, or carrying invitations nobody has answered.

An API key or a session.

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.

all=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.

Query parameters

  • all
  • days

Response

list of Shift needing attention

Example
[
  {
    "id": 1,
    "gig_id": 1,
    "day_id": 1,
    "starts_at": "2026-09-10T05:00:00Z",
    "ends_at": "2026-09-10T13:00:00Z",
    "required_workers": 3,
    "role_label": "1× crane op, 2× labourer",
    "created_at": "2026-09-07T09:00:00Z",
    "accepted": 1,
    "pending": 1,
    "gig_title": "Warehouse roof",
    "short": 2,
    "sos_open": true,
    "roles": [
      {
        "id": 1,
        "label": "crane op",
        "headcount": 1,
        "rate_amount": 5200000,
        "rate_currency": "CZK",
        "accepted": 1,
        "pending": 0
      },
      {
        "id": 2,
        "label": "labourer",
        "headcount": 2,
        "rate_amount": null,
        "rate_currency": null,
        "accepted": 0,
        "pending": 1
      }
    ]
  }
]

Status codes

CodeMessage
200
400all=1 reads every company: leave out X-Werzeick-Company · days must be a whole number from 1 to 92
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

People at the company

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.

GET /api/people

Lists everyone at the company, by name.

An API key or a session.

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.

q 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.

Query parameters

  • q

Response

list of Person response

Example
[
  {
    "id": 1,
    "is_manager": true,
    "is_worker": false,
    "rate_amount": null,
    "rate_currency": null,
    "emergency_responses_enabled": false,
    "cancellation_emails_enabled": true,
    "is_disabled": false,
    "created_at": "2026-09-01T08:00:00Z",
    "org_name": "Alpha Stavby",
    "first_name": "Tomáš",
    "last_name": "Novák",
    "email": "tomas@alpha.test",
    "certifications": []
  },
  {
    "id": 2,
    "is_manager": false,
    "is_worker": true,
    "rate_amount": 4200000,
    "rate_currency": "CZK",
    "emergency_responses_enabled": true,
    "cancellation_emails_enabled": true,
    "is_disabled": false,
    "created_at": "2026-09-01T08:05:00Z",
    "org_name": "Alpha Stavby",
    "first_name": "Jana",
    "last_name": "Horáková",
    "email": "jana@alpha.test",
    "certifications": [
      {
        "id": 1,
        "membership_id": 2,
        "kind": "lift_crane_overhead",
        "label": null,
        "scope": "bridge cranes to 10 t",
        "expires_on": "2027-06-30",
        "granted_by_membership_id": 1,
        "granted_by_name": "Tomáš Novák",
        "created_at": "2026-10-28T09:00:00Z",
        "certificate_id": 1,
        "name": "Overhead or gantry crane operator"
      }
    ]
  }
]

Status codes

CodeMessage
200
401authentication required
403not permitted
500internal server error
Try it

POST /api/people/{id}/certifications

Notes a person as certified for something, by the manager's say-so, or by approving one of their certificates.

An API key or a session.

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.

One 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.

In the path

ParameterWhat it is
idThe person's membership id, as the roster lists it.

Request body

Notes somebody as certified for something: by the manager's say-so, or by approving one of the person's certificates.

FieldTypeWhat it is
certificate_id integer

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.

kind string

kind is a kind's id from the certificate kinds endpoint, or other_personal for one the list has no name for.

label string

label says what an other_personal note is for. Required for that kind, refused for any other, at most 120 characters.

scope string

At most 200 characters.

expires_on string, or null

expires_on is the day it stops being valid, "2027-06-30", or null for unknown. Blank never means "for ever".

Response

Certification response

Example
{
  "id": 1,
  "membership_id": 2,
  "kind": "lift_crane_overhead",
  "label": null,
  "scope": "bridge cranes to 10 t",
  "expires_on": "2027-06-30",
  "granted_by_membership_id": 1,
  "granted_by_name": "Tomáš Novák",
  "created_at": "2026-10-28T09:00:00Z",
  "certificate_id": 1,
  "name": "Overhead or gantry crane operator"
}

Status codes

CodeMessage
201
400expires_on must be YYYY-MM-DD or null · invalid JSON body · invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

DELETE /api/people/{id}/certifications/{certificationID}

Takes a note away.

An API key or a session.

Nobody is told. Managers only.

In the path

ParameterWhat it is
idThe person's membership id, as the roster lists it.
certificationIDThe note's id, as the roster lists it under the person.

Response

No body.

Status codes

CodeMessage
204
400invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

GET /api/people/{id}/certificates

Lists a person's certificates: the documents they hold, with their pages' sizes, as the person themselves sees them.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe person's membership id, as the roster lists it.

Response

list of Certificate response

Example
[
  {
    "id": 1,
    "kind": "lift_crane_overhead",
    "label": null,
    "scope": "bridge cranes to 10 t",
    "number": "JB-4471",
    "issued_on": "2024-05-02",
    "expires_on": "2029-05-01",
    "pages": [
      {
        "position": 1,
        "content_type": "image/jpeg",
        "width": 1600,
        "height": 1067
      },
      {
        "position": 2,
        "content_type": "image/jpeg",
        "width": 1600,
        "height": 1067
      }
    ],
    "uploaded_by": {
      "membership_id": 1,
      "name": "Tomáš Novák",
      "company": "Alpha Stavby"
    },
    "created_at": "2026-10-28T09:00:00Z",
    "updated_at": "2026-10-28T09:04:00Z",
    "name": "Overhead or gantry crane operator"
  },
  {
    "id": 2,
    "kind": "other_personal",
    "label": "Drone pilot",
    "scope": null,
    "number": null,
    "issued_on": null,
    "expires_on": null,
    "pages": [],
    "uploaded_by": null,
    "created_at": "2026-10-28T10:00:00Z",
    "updated_at": "2026-10-28T10:00:00Z",
    "name": "Drone pilot"
  }
]

Status codes

CodeMessage
200
400invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/people/{id}/certificates

Adds a certificate on a person's behalf, for when the office holds the scans.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe person's membership id, as the roster lists it.

Request body

What is typed about a certificate: what is printed on the card.

FieldTypeWhat it is
kind string

kind is a kind's id from the certificate kinds endpoint, or other_personal for one the list has no name for.

label string

label says what an other_personal certificate is. Required for that kind, refused for any other, at most 120 characters.

scope string

At most 200 characters.

number string

number is the number printed on the card, if any. At most 60 characters; nothing is checked against it.

issued_on string, or null

issued_on is the day it was issued, "2024-05-02", or null.

expires_on string, or null

expires_on is the day it stops being valid, "2029-05-01", or null for unknown. Blank never means "for ever".

Response

Certificate response

Example
{
  "id": 2,
  "kind": "other_personal",
  "label": "Drone pilot",
  "scope": null,
  "number": null,
  "issued_on": null,
  "expires_on": null,
  "pages": [],
  "uploaded_by": null,
  "created_at": "2026-10-28T10:00:00Z",
  "updated_at": "2026-10-28T10:00:00Z",
  "name": "Drone pilot"
}

Status codes

CodeMessage
201
400invalid JSON body · invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/people/{id}/certificates/{certificateID}/pages

Adds the next page to a person's certificate, as the person's own pages endpoint does.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe person's membership id, as the roster lists it.
certificateIDThe certificate's id, as the list of certificates gives it.

Request body

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.

FieldTypeWhat it is
image string

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.

Response

Certificate response

Example
{
  "id": 1,
  "kind": "lift_crane_overhead",
  "label": null,
  "scope": "bridge cranes to 10 t",
  "number": "JB-4471",
  "issued_on": "2024-05-02",
  "expires_on": "2029-05-01",
  "pages": [
    {
      "position": 1,
      "content_type": "image/jpeg",
      "width": 1600,
      "height": 1067
    },
    {
      "position": 2,
      "content_type": "image/jpeg",
      "width": 1600,
      "height": 1067
    }
  ],
  "uploaded_by": {
    "membership_id": 1,
    "name": "Tomáš Novák",
    "company": "Alpha Stavby"
  },
  "created_at": "2026-10-28T09:00:00Z",
  "updated_at": "2026-10-28T09:04:00Z",
  "name": "Overhead or gantry crane operator"
}

Status codes

CodeMessage
201
400image must be base64 · invalid JSON body · invalid {thing} id · send the image as a part named page · the upload could not be read
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

GET /api/people/{id}/certificates/{certificateID}/pages/{position}

Answers one page of a person's certificate as the image itself, not as JSON.

An API key or a session.

Managers only.

The body is the JPEG, with its content type.

In the path

ParameterWhat it is
idThe person's membership id, as the roster lists it.
certificateIDThe certificate's id, as the list of certificates gives it.
positionThe page's position on the certificate, counted from 1.

Response

No body.

Status codes

CodeMessage
200
400invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · page not found · shift not found
409that 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
500internal server error
Try it

Certificates

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.

GET /api/certificate-kinds

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.

An API key or a session.

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.

Response

Certificate kinds

Example
{
  "groups": [
    {
      "id": "education",
      "name": "Education"
    },
    {
      "id": "qualification",
      "name": "Qualification"
    }
  ],
  "kinds": [
    {
      "id": "other_personal",
      "name": "Other document",
      "group": "other",
      "aliases": [
        "jiný certifikát",
        "jiné oprávnění",
        "neznámý doklad"
      ]
    },
    {
      "id": "lift_crane_overhead",
      "name": "Overhead or gantry crane operator",
      "group": "induction",
      "aliases": [
        "jeřábnický průkaz",
        "mostový jeřáb"
      ]
    },
    {
      "id": "weld_iso9606",
      "name": "Welder qualification test (EN ISO 9606)",
      "group": "qualification",
      "aliases": [
        "svářečský certifikát",
        "úřední zkouška",
        "ISO 9606",
        "EN 287"
      ]
    }
  ],
  "trades": [
    {
      "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"
      ],
      "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"
      ]
    }
  ]
}

Status codes

CodeMessage
200
401authentication required
Try it

GET /api/certificates

Lists the caller's own certificates, oldest first, each with its pages' sizes.

An API key or a session.

The pages themselves are read one at a time. Either role: the documents are the person's, whatever the key acts as.

Response

list of Certificate response

Example
[
  {
    "id": 1,
    "kind": "lift_crane_overhead",
    "label": null,
    "scope": "bridge cranes to 10 t",
    "number": "JB-4471",
    "issued_on": "2024-05-02",
    "expires_on": "2029-05-01",
    "pages": [
      {
        "position": 1,
        "content_type": "image/jpeg",
        "width": 1600,
        "height": 1067
      },
      {
        "position": 2,
        "content_type": "image/jpeg",
        "width": 1600,
        "height": 1067
      }
    ],
    "uploaded_by": {
      "membership_id": 1,
      "name": "Tomáš Novák",
      "company": "Alpha Stavby"
    },
    "created_at": "2026-10-28T09:00:00Z",
    "updated_at": "2026-10-28T09:04:00Z",
    "name": "Overhead or gantry crane operator"
  },
  {
    "id": 2,
    "kind": "other_personal",
    "label": "Drone pilot",
    "scope": null,
    "number": null,
    "issued_on": null,
    "expires_on": null,
    "pages": [],
    "uploaded_by": null,
    "created_at": "2026-10-28T10:00:00Z",
    "updated_at": "2026-10-28T10:00:00Z",
    "name": "Drone pilot"
  }
]

Status codes

CodeMessage
200
401authentication required
403not permitted
500internal server error
Try it

POST /api/certificates

Adds a certificate of the caller's own, with no pages yet: the photos are added one by one afterwards.

An API key or a session.

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.

Request body

What is typed about a certificate: what is printed on the card.

FieldTypeWhat it is
kind string

kind is a kind's id from the certificate kinds endpoint, or other_personal for one the list has no name for.

label string

label says what an other_personal certificate is. Required for that kind, refused for any other, at most 120 characters.

scope string

At most 200 characters.

number string

number is the number printed on the card, if any. At most 60 characters; nothing is checked against it.

issued_on string, or null

issued_on is the day it was issued, "2024-05-02", or null.

expires_on string, or null

expires_on is the day it stops being valid, "2029-05-01", or null for unknown. Blank never means "for ever".

Response

Certificate response

Example
{
  "id": 2,
  "kind": "other_personal",
  "label": "Drone pilot",
  "scope": null,
  "number": null,
  "issued_on": null,
  "expires_on": null,
  "pages": [],
  "uploaded_by": null,
  "created_at": "2026-10-28T10:00:00Z",
  "updated_at": "2026-10-28T10:00:00Z",
  "name": "Drone pilot"
}

Status codes

CodeMessage
201
400invalid JSON body
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/certificates/read

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.

An API key or a session.

Nothing is stored; the client fills its form in from the answer and the person checks it before saving. Either role.

The 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.

Request body

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.

FieldTypeWhat it is
images list of string

They are read and then forgotten; nothing is stored until the certificate is created.

Response

Certificate reading

Example
{
  "kind": "lift_crane_overhead",
  "kind_name": "Overhead or gantry crane operator",
  "issued_on": "2024-05-02",
  "expires_on": "2029-05-01",
  "number": "JB-4471",
  "pages": [
    {
      "position": 1,
      "readability": 94,
      "legible": true
    },
    {
      "position": 2,
      "readability": 41,
      "legible": false
    },
    {
      "position": 3,
      "readability": null,
      "legible": false
    }
  ],
  "legible": false
}

Status codes

CodeMessage
200
400image 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
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
503reading photos is not set up on this server; fill the form in by hand
Try it

PATCH /api/certificates/{id}

Corrects what is typed about one of the caller's certificates.

An API key or a session.

Every field is written as sent, so send the whole record; the pages are untouched.

In the path

ParameterWhat it is
idThe certificate's id, as the list of certificates gives it.

Request body

What is typed about a certificate: what is printed on the card.

FieldTypeWhat it is
kind string

kind is a kind's id from the certificate kinds endpoint, or other_personal for one the list has no name for.

label string

label says what an other_personal certificate is. Required for that kind, refused for any other, at most 120 characters.

scope string

At most 200 characters.

number string

number is the number printed on the card, if any. At most 60 characters; nothing is checked against it.

issued_on string, or null

issued_on is the day it was issued, "2024-05-02", or null.

expires_on string, or null

expires_on is the day it stops being valid, "2029-05-01", or null for unknown. Blank never means "for ever".

Response

Certificate response

Example
{
  "id": 1,
  "kind": "lift_crane_overhead",
  "label": null,
  "scope": "bridge cranes to 10 t",
  "number": "JB-4471",
  "issued_on": "2024-05-02",
  "expires_on": "2029-05-01",
  "pages": [
    {
      "position": 1,
      "content_type": "image/jpeg",
      "width": 1600,
      "height": 1067
    },
    {
      "position": 2,
      "content_type": "image/jpeg",
      "width": 1600,
      "height": 1067
    }
  ],
  "uploaded_by": {
    "membership_id": 1,
    "name": "Tomáš Novák",
    "company": "Alpha Stavby"
  },
  "created_at": "2026-10-28T09:00:00Z",
  "updated_at": "2026-10-28T09:04:00Z",
  "name": "Overhead or gantry crane operator"
}

Status codes

CodeMessage
200
400invalid JSON body · invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

DELETE /api/certificates/{id}

Removes one of the caller's certificates with its pages.

An API key or a session.

A note a company made from it stays, saying it has no proof on file. Nobody is told.

In the path

ParameterWhat it is
idThe certificate's id, as the list of certificates gives it.

Response

No body.

Status codes

CodeMessage
204
400invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/certificates/{id}/pages

Adds the next page to one of the caller's certificates: the front, the back, an attachment, at most 8.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe certificate's id, as the list of certificates gives it.

Request body

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.

FieldTypeWhat it is
image string

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.

Response

Certificate response

Example
{
  "id": 1,
  "kind": "lift_crane_overhead",
  "label": null,
  "scope": "bridge cranes to 10 t",
  "number": "JB-4471",
  "issued_on": "2024-05-02",
  "expires_on": "2029-05-01",
  "pages": [
    {
      "position": 1,
      "content_type": "image/jpeg",
      "width": 1600,
      "height": 1067
    },
    {
      "position": 2,
      "content_type": "image/jpeg",
      "width": 1600,
      "height": 1067
    }
  ],
  "uploaded_by": {
    "membership_id": 1,
    "name": "Tomáš Novák",
    "company": "Alpha Stavby"
  },
  "created_at": "2026-10-28T09:00:00Z",
  "updated_at": "2026-10-28T09:04:00Z",
  "name": "Overhead or gantry crane operator"
}

Status codes

CodeMessage
201
400image must be base64 · invalid JSON body · invalid {thing} id · send the image as a part named page · the upload could not be read
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

GET /api/certificates/{id}/pages/{position}

Answers one page of one of the caller's certificates as the image itself, not as JSON.

An API key or a session.

The body is the JPEG, with its content type.

In the path

ParameterWhat it is
idThe certificate's id, as the list of certificates gives it.
positionThe page's position on the certificate, counted from 1.

Response

No body.

Status codes

CodeMessage
200
400invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · page not found · shift not found
409that 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
500internal server error
Try it

DELETE /api/certificates/{id}/pages/{position}

Takes one page off one of the caller's certificates; the pages after it move up.

An API key or a session.

Answers the certificate as it is now.

In the path

ParameterWhat it is
idThe certificate's id, as the list of certificates gives it.
positionThe page's position on the certificate, counted from 1.

Response

Certificate response

Example
{
  "id": 2,
  "kind": "other_personal",
  "label": "Drone pilot",
  "scope": null,
  "number": null,
  "issued_on": null,
  "expires_on": null,
  "pages": [],
  "uploaded_by": null,
  "created_at": "2026-10-28T10:00:00Z",
  "updated_at": "2026-10-28T10:00:00Z",
  "name": "Drone pilot"
}

Status codes

CodeMessage
200
400invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · page not found · shift not found
409that 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
500internal server error
Try it

Clients

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.

GET /api/clients

Lists the company's clients, by name.

An API key or a session.

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.

Query parameters

  • archived

Response

list of Client

Example
[
  {
    "id": 1,
    "name": "Stavby Novák s.r.o.",
    "ico": "12345678",
    "dic": "CZ12345678",
    "address_line1": "Průmyslová 12",
    "address_line2": null,
    "city": "Ostrava",
    "postal_code": "702 00",
    "country": "CZ",
    "contact_name": "Petr Novák",
    "contact_email": "petr@stavbynovak.test",
    "contact_phone": "+420777123456",
    "is_archived": false,
    "created_at": "2026-09-01T09:00:00Z"
  },
  {
    "id": 2,
    "name": "Jan Dvořák",
    "ico": null,
    "dic": null,
    "address_line1": null,
    "address_line2": null,
    "city": null,
    "postal_code": null,
    "country": "CZ",
    "contact_name": null,
    "contact_email": null,
    "contact_phone": null,
    "is_archived": false,
    "created_at": "2026-09-02T09:00:00Z"
  }
]

Status codes

CodeMessage
200
401authentication required
403not permitted
500internal server error
Try it

GET /api/clients/{id}

One client.

An API key or a session.

Another company's is not found.

In the path

ParameterWhat it is
idThe client's id.

Response

Client

Example
{
  "id": 1,
  "name": "Stavby Novák s.r.o.",
  "ico": "12345678",
  "dic": "CZ12345678",
  "address_line1": "Průmyslová 12",
  "address_line2": null,
  "city": "Ostrava",
  "postal_code": "702 00",
  "country": "CZ",
  "contact_name": "Petr Novák",
  "contact_email": "petr@stavbynovak.test",
  "contact_phone": "+420777123456",
  "is_archived": false,
  "created_at": "2026-09-01T09:00:00Z"
}

Status codes

CodeMessage
200
400invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

POST /api/clients

Adds a client to the company.

An API key or a session.

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.

Request body

A new client.

FieldTypeWhat it is
name string
ico string, or null

ico is the Czech company registration number, eight digits with any leading zeros. Null for a private individual.

dic string, or null

dic is the VAT number, "CZ12345678". Null when not VAT registered.

address_line1 string, or null
address_line2 string, or null
city string, or null
postal_code string, or null
country string

country is a two-letter code; "CZ" unless given.

contact_name string, or null

The client's own contact person, who a worker rings about a job when the project names no site contact of its own.

contact_email string, or null
contact_phone string, or null

Response

Client

Example
{
  "id": 2,
  "name": "Jan Dvořák",
  "ico": null,
  "dic": null,
  "address_line1": null,
  "address_line2": null,
  "city": null,
  "postal_code": null,
  "country": "CZ",
  "contact_name": null,
  "contact_email": null,
  "contact_phone": null,
  "is_archived": false,
  "created_at": "2026-09-02T09:00:00Z"
}

Status codes

CodeMessage
201
400client name is required · invalid JSON body
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

A project's hours

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.

GET /api/gigs/{id}/hours

Lists every time entry on a project, newest first.

An API key or a session.

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.

In the path

ParameterWhat it is
idThe project's id.

Response

list of Hours row

Example
[
  {
    "id": 1,
    "membership_id": 2,
    "gig_id": 1,
    "timeslot_id": 1,
    "clock_in_at": "2026-09-10T04:52:00Z",
    "clock_out_at": "2026-09-10T13:06:00Z",
    "source": "manual",
    "rate_amount": 5200000,
    "rate_currency": "CZK",
    "earned_amount": 42813333,
    "client_event_id": "a1f0c2f1-3d6b-4b9c-9f3e-6e2d9a4c1b11",
    "corrected_at": null,
    "created_at": "2026-09-10T04:52:03Z",
    "worker_name": "Jana Horáková",
    "slot_starts_at": "2026-09-10T05:00:00Z",
    "slot_ends_at": "2026-09-10T13:00:00Z",
    "recorded_by": ""
  }
]

Status codes

CodeMessage
200
400invalid {thing} id
401authentication required
403not permitted · that application is the manager's to approve
404assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found
409that 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
500internal server error
Try it

Health

Answers the liveness check that uptime alerting polls.

Availability is a product risk here, not just an ops one: an SOS that cannot reach the server is emergency staffing that fails.

GET /healthz

Reports 200 when the server can reach its database, 503 otherwise.

Public: no key and no session needed.

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.

Response

object

Example
{
  "status": "ok"
}

Status codes

CodeMessage
200
503
Try it

Types

The shapes the endpoints above answer with, each documented once. A field's type says whether it may be null, and a list says what it is a list of.

Request code response requestCodeResponse

The answer to asking for a sign-in code.

FieldTypeWhat it is
status string
resend_after_seconds integer

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.

Session sessionResponse

Describes who the caller is and where they may act.

token 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.

FieldTypeWhat it is
token string

Left out of the body when it is empty.

expires_at string

Left out of the body when it is empty.

membership Entry
memberships list of Entry
language string, or null

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.

Deletion deletionResponse

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.

FieldTypeWhat it is
can_delete boolean

can_delete is false while something refuses it — today, being the only manager a company has. The app shows why and offers no button.

companies list of Leaving company

Preview invitation services.PreviewInvitation

Describes an invitation to whoever holds the link, before they accept it.

It 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.

FieldTypeWhat it is
org_name string
is_manager boolean
is_worker boolean
first_name string
email string
rate_amount integer, or null

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

rate_currency string, or null

Acceptance acceptResponse

Says what the app does next. outcome is one of three, and each fills its own fields and leaves the others null:

  • "signed_in": accepting created the account. session is the body a sign-in answers with, token included, and the app is in.
  • "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".
  • "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.
FieldTypeWhat it is
outcome string
email string
code_sent boolean
session Session, or null
membership Entry, or null

Invitation services.Invitation

A pending offer of a job at one company.

FieldTypeWhat it is
id integer
email string
first_name string
last_name string
is_manager boolean
is_worker boolean
rate_amount integer, or null

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

rate_currency string, or null
expires_at timestamp, RFC 3339 in UTC
accepted_at timestamp, RFC 3339 in UTC, or null
created_at timestamp, RFC 3339 in UTC

Dashboard services.Dashboard

One person's home screen at one company.

Exactly 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.

FieldTypeWhat it is
role string: worker | manager
days list of string

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.

today string

today is one of days, so a client can mark it without deciding for itself which zone "today" is in.

worker Worker dashboard, or null

Left out of the body when it is empty.

manager Manager dashboard, or null

Left out of the body when it is empty.

Assignment services.Assignment

One worker's invitation into one timeslot.

FieldTypeWhat it is
id integer
timeslot_id integer
membership_id integer
origin string: staffed | sos | open | backfilled
status string: pending | accepted | declined | expired | withdrawn | cancelled
rate_amount integer, or null

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.

Null 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.

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

rate_currency string, or null
role_id integer

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.

role_label string, or null
invited_at timestamp, RFC 3339 in UTC
responded_at timestamp, RFC 3339 in UTC, or null
expires_at timestamp, RFC 3339 in UTC
created_at timestamp, RFC 3339 in UTC
takes_from timestamp, RFC 3339 in UTC, or null

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.

takes_until timestamp, RFC 3339 in UTC, or null
worker_name string

worker_name is filled by reads a PM makes, so a staffing view can show who was invited without a second query per row.

Left out of the body when it is empty.

Schedule entry services.ScheduleEntry

One row of a worker's own schedule.

FieldTypeWhat it is
id integer
timeslot_id integer
membership_id integer
origin string: staffed | sos | open | backfilled
status string: pending | accepted | declined | expired | withdrawn | cancelled
rate_amount integer, or null

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.

Null 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.

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

rate_currency string, or null
role_id integer

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.

role_label string, or null
invited_at timestamp, RFC 3339 in UTC
responded_at timestamp, RFC 3339 in UTC, or null
expires_at timestamp, RFC 3339 in UTC
created_at timestamp, RFC 3339 in UTC
takes_from timestamp, RFC 3339 in UTC, or null

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.

takes_until timestamp, RFC 3339 in UTC, or null
worker_name string

worker_name is filled by reads a PM makes, so a staffing view can show who was invited without a second query per row.

Left out of the body when it is empty.

timeslot Shift
gig_id integer
gig_title string
full boolean

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.

collides_with_title string, or null

Set on PENDING entries only. No client works any of this out for itself.

collides_with_company string, or null

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.

collides_from timestamp, RFC 3339 in UTC, or null
collides_until timestamp, RFC 3339 in UTC, or null
can_take_from timestamp, RFC 3339 in UTC, or null
can_take_until timestamp, RFC 3339 in UTC, or null

Answered invitations answeredMany

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.

FieldTypeWhat it is
answered integer

answered is how many invitations were accepted, or declined.

full integer

full is how many were left waiting because their shift has no place. Always zero for a decline.

colliding integer

colliding is how many were left waiting because they collide with accepted work. Always zero for a decline.

Time entry services.TimeEntry

One stretch of work: when someone started, when they stopped, and what it was worth.

FieldTypeWhat it is
id integer
membership_id integer
gig_id integer
timeslot_id integer, or null

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.

clock_in_at timestamp, RFC 3339 in UTC
clock_out_at timestamp, RFC 3339 in UTC, or null

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.

source string: manual | geofence | backfilled
rate_amount integer, or null

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.

Null 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.

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

rate_currency string, or null
client_event_id string

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.

Left out of the body when it is empty.

corrected_at timestamp, RFC 3339 in UTC, or null
created_at timestamp, RFC 3339 in UTC

Project gigResponse

A project as the app receives it: the project, and how to reach the manager running it.

Every endpoint that answers with a project answers with this, so the app parses one shape whichever screen asked.

FieldTypeWhat it is
id integer
title string
owner_id integer

owner_id is a membership id — the PM *at this company* — not a user id.

client_id integer, or null

The company being invoiced for this work. Nullable: a project may be internal, or booked before anyone has agreed who is paying.

client_name string, or null
client_contact_name string, or null

The client's own contact person, from the client's record.

It 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.

client_contact_phone string, or null
client_contact_email string, or null
job_contact Job contact, or null

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.

latitude number, or null

Where the work happens, in WGS84 degrees. Nullable because a project can be created before anyone has pinned the site.

This 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.

longitude number, or null
geofence_radius_m integer, or null

Radius in metres for optional geofence auto-clocking.

site_address string, or null

Where to go, in words: "Gate 3, Průmyslová 12, Ostrava".

Stored 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.

site_outline list of Point

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.

Kept 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.

expected_minutes integer, or null

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.

expenses list of Expense
created_at timestamp, RFC 3339 in UTC
is_assigned boolean
is_invited boolean

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.

shifts_to_take integer
archived_at timestamp, RFC 3339 in UTC, or null

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.

project_manager Project manager, or null

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.

company Company tag, or null

company is which of the caller's entries the row came from, on a list across them and nowhere else.

Left out of the body when it is empty.

Earnings projectEarnings

What one worker has earned on one project, and what they stand to earn on it.

Two lists, per currency, never mixed and never converted: this product holds no exchange rate and inventing one would be inventing money.

FieldTypeWhat it is
earned list of Money total

earned is hours already worked, valued from TIME ENTRIES.

upcoming list of Money total

upcoming is accepted work not yet done, valued from ASSIGNMENTS.

earned_unrated_minutes integer

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.

upcoming_unrated_minutes integer
running Running shift, or null

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.

Point geo.Point

A WGS84 coordinate in degrees.

FieldTypeWhat it is
lat number
lon number

Expense services.GigExpense

A cost booked against a gig.

cost_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.

FieldTypeWhat it is
id integer
gig_id integer

gig_id is the one project this cost belongs to.

created_by_membership_id integer

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.

It 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.

created_by_name string

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.

description string
cost_amount integer

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

cost_currency string

"CZK" or "EUR"

created_at timestamp, RFC 3339 in UTC

Tool icon services.ToolIcon

One choice in the picker: the stored name and what to call it.

FieldTypeWhat it is
name string

name is what goes in the database and crosses the wire. Lower case, no spaces, stable forever.

label string

label is the English word for it.

Tool services.Tool

One item on a worker's kit list.

A 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.

FieldTypeWhat it is
id integer
name string
icon string
quantity integer, or null

quantity is null for "one, or some". Nothing decrements it: this is a kit list, not a stock level.

created_at timestamp, RFC 3339 in UTC
gig_ids list of integer

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.

Calendar day calendarDay

One day a project runs, with the shifts on it.

FieldTypeWhat it is
day string

day is the calendar day, "2026-10-27". day_id is its id, which a new shift names.

day_id integer
shifts list of Shift as a manager sees it

shifts are the day's shifts, earliest first; a day scheduled with no shift yet has none.

Scheduled day services.GigDay

One day a project runs.

The calendar is a set of days, not a range: a project pauses and resumes, and a gap is simply an absent row.

FieldTypeWhat it is
id integer
gig_id integer
day string

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.

Removed days removedDays

What removing days did, or would do.

FieldTypeWhat it is
confirmed boolean

confirmed is false for a preview, in which nothing changed.

days integer

days is how many days are on the calendar to come off (a preview), or came off it.

people integer

people is how many different people hold a live place on those days: who will be told. In a preview only.

cancelled integer

cancelled is how many places were cancelled, once confirmed.

Role to add roleRequest

One role on a new shift.

FieldTypeWhat it is
label string

label names the kind of worker. A shift with one role may leave it empty; two roles need names to be told apart.

headcount integer

headcount is how many of this role the shift needs, at least 1.

rate_amount integer, or null

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.

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

rate_currency string, or null

Added shifts addedShifts

What laying a shift on days made.

FieldTypeWhat it is
shifts list of Shift as a manager sees it
scheduled integer

scheduled is how many of the days were not on the calendar before.

Role roleResponse

One role on a shift: a kind of worker with its own places and, where the manager set one, its own hourly rate.

FieldTypeWhat it is
id integer
label string, or null

label names the kind of worker ("crane op"). Null for the one unnamed role of a plain shift.

headcount integer

headcount is how many of this role the shift needs.

rate_amount integer, or null

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.

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

rate_currency string, or null
accepted integer

accepted fills the role's places; pending is what nobody has answered yet, and fills nothing.

pending integer

Moved shifts movedShifts

What a move did, or would do.

FieldTypeWhat it is
confirmed boolean

confirmed is false for a preview, in which nothing changed.

shifts integer

shifts is how many move; skipped how many were left where they were, having started or carrying an SOS.

skipped integer
people integer

people is how many hold a live place on the moving shifts and are asked again.

first_day string

first_day is the earliest moved shift's new day, "2026-10-29".

Removed shift removedShift

What removing a shift did, or would do.

FieldTypeWhat it is
confirmed boolean

confirmed is false for a preview, in which nothing changed.

accepted integer

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.

pending integer
cancelled integer

cancelled is how many places were cancelled, once confirmed.

Shift with its people shiftDetail

A shift with the people on it.

FieldTypeWhat it is
id integer
gig_id integer
day_id integer
starts_at timestamp, RFC 3339 in UTC
ends_at timestamp, RFC 3339 in UTC
required_workers integer
role_label string, or null

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.

created_at timestamp, RFC 3339 in UTC
accepted integer

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.

pending integer
short integer

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.

is_open boolean

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.

open_auto_accept boolean
offered_rate_multiplier integer, or null

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.

A multiplier with four implied decimal places: 15000 is 1.5×.

roles list of Role
people list of Assignment

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.

Person on a project personOnProject

One person with live places on a project's upcoming shifts.

FieldTypeWhat it is
membership_id integer
name string
accepted integer

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.

pending integer
first_start timestamp, RFC 3339 in UTC

first_start and last_start are the earliest and latest of those shifts.

last_start timestamp, RFC 3339 in UTC

Taken off a project takenOff

What taking somebody off a project did, or would do.

FieldTypeWhat it is
confirmed boolean

confirmed is false for a preview, in which nothing changed.

accepted integer

accepted and pending are the places cancelled: work they had agreed to, and invitations or applications still open.

pending integer
first_day string

first_day is the earliest cancelled shift's day on site, "2026-10-29".

Shift as a manager sees it shiftResponse

A shift as a manager reads it: the shift, how short it is, whether it is open to the company, and its roles.

FieldTypeWhat it is
id integer
gig_id integer
day_id integer
starts_at timestamp, RFC 3339 in UTC
ends_at timestamp, RFC 3339 in UTC
required_workers integer
role_label string, or null

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.

created_at timestamp, RFC 3339 in UTC
accepted integer

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.

pending integer
short integer

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.

is_open boolean

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.

open_auto_accept boolean
offered_rate_multiplier integer, or null

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.

A multiplier with four implied decimal places: 15000 is 1.5×.

roles list of Role

SOS raised sosRaised

One emergency opened on one shift.

FieldTypeWhat it is
id integer
timeslot_id integer
offered_rate_multiplier integer

offered_rate_multiplier is the raise, with four implied decimals.

A multiplier with four implied decimal places: 15000 is 1.5×.

opened_at timestamp, RFC 3339 in UTC
told integer

told is how many people were told, each once: every worker at the company who opted into emergency cover and could take it.

SOS over a project sosBatch

What an SOS over several shifts did.

FieldTypeWhat it is
raised list of SOS raised

raised is the emergency opened on each shift, earliest first.

already_raised integer

What was skipped and why: an SOS already running, every place taken meanwhile, the shift over.

filled integer
over integer
told integer

told is how many people were told, each once for the whole batch.

Shift needing attention shortShift

An upcoming shift that needs a manager's attention.

FieldTypeWhat it is
id integer
gig_id integer
day_id integer
starts_at timestamp, RFC 3339 in UTC
ends_at timestamp, RFC 3339 in UTC
required_workers integer
role_label string, or null

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.

created_at timestamp, RFC 3339 in UTC
accepted integer

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.

pending integer
gig_title string
short integer

short is the places nobody has accepted, role by role.

sos_open boolean

sos_open says an emergency is already running on it, so no second one is raised.

roles list of Role
company Company tag, or null

company is which of the caller's entries the shift came from, on a list across them and nowhere else.

Left out of the body when it is empty.

Person response personResponse

One person on the roster: the membership, and the company's "certified for" notes about them.

FieldTypeWhat it is
id integer
is_manager boolean
is_worker boolean
rate_amount integer, or null

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.

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

rate_currency string, or null
emergency_responses_enabled boolean
cancellation_emails_enabled boolean

cancellation_emails_enabled: a cancelled accepted shift is emailed as well as pushed. On by default; only the worker turns it off.

is_disabled boolean
created_at timestamp, RFC 3339 in UTC
org_name string
first_name string
last_name string
email string
certifications list of Certification response

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.

Certification response certificationResponse

One "certified for" note, with the kind's name in the request's language.

FieldTypeWhat it is
id integer
membership_id integer
kind string

kind is the kind's id from the list of certificate kinds, or "other_personal" with a label.

label string, or null

label says what an "other" certification is for; null for a listed kind, whose name the list gives.

scope string, or null
expires_on string, or null

expires_on is the day it stops being valid, "2027-06-30", or null for unknown; never "for ever".

granted_by_membership_id integer

granted_by_membership_id is the manager who made the note, by membership id, and granted_by_name their name, or "Deleted user".

granted_by_name string
created_at timestamp, RFC 3339 in UTC
certificate_id integer, or null

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.

name string

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.

Certificate response certificateResponse

One certificate, with the kind's name in the request's language.

FieldTypeWhat it is
id integer
kind string

kind is the kind's id from the list of certificate kinds, or "other_personal" with a label.

label string, or null

label says what an "other" certificate is; null for a listed kind.

scope string, or null
number string, or null

number is what is printed on the card, as typed.

issued_on string, or null

issued_on and expires_on are calendar days, "2027-06-30", or null. A null expiry is unknown, never "for ever".

expires_on string, or null
pages list of Certificate page

pages are the photos, in order, without their bytes: each page is read on its own by position.

uploaded_by Uploaded by, or null

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.

created_at timestamp, RFC 3339 in UTC
updated_at timestamp, RFC 3339 in UTC
name string

name is what the certificate is, in words: the kind's name, or the typed label of one whose kind is other_personal.

Certificate kinds certificateKinds

The list of kinds, in the request's language.

FieldTypeWhat it is
groups list of Certificate group

groups are the headings kinds are listed under, in order.

kinds list of Certificate kind

kinds are every kind, in the list's own order.

trades list of Certificate trade

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.

Certificate reading certificateReading

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.

FieldTypeWhat it is
kind string, or null

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.

kind_name string, or null

kind_name is that kind's name in the request's language, or null.

issued_on string, or null

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.

expires_on string, or null
number string, or null

number is the number printed after "č." or "No.", or null.

pages list of Page reading

pages says how well each page read, in the order sent.

legible boolean

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.

Client services.Client

A company you invoice.

The tax identifiers are here because invoicing is planned and a Czech invoice is not valid without them.

FieldTypeWhat it is
id integer
name string
ico string, or null

IČO, the Czech company registration number: eight digits including any leading zeros, hence text. Null for a private individual.

dic string, or null

DIČ, the VAT number, e.g. "CZ12345678". Null when not VAT registered.

address_line1 string, or null
address_line2 string, or null
city string, or null
postal_code string, or null
country string
contact_name string, or null
contact_email string, or null
contact_phone string, or null
is_archived boolean
created_at timestamp, RFC 3339 in UTC

Hours row hoursRow

One time entry on a project, with who worked it.

FieldTypeWhat it is
id integer
membership_id integer
gig_id integer
timeslot_id integer, or null

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.

clock_in_at timestamp, RFC 3339 in UTC
clock_out_at timestamp, RFC 3339 in UTC, or null

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.

source string: manual | geofence | backfilled
rate_amount integer, or null

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.

Null 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.

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

rate_currency string, or null
client_event_id string

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.

Left out of the body when it is empty.

corrected_at timestamp, RFC 3339 in UTC, or null
created_at timestamp, RFC 3339 in UTC
worker_name string

worker_name is who clocked the hours.

slot_starts_at timestamp, RFC 3339 in UTC, or null

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.

slot_ends_at timestamp, RFC 3339 in UTC, or null
recorded_by string

recorded_by names the manager who entered the hours for somebody added to a shift afterwards, and is empty for hours the worker clocked.

Entry membershipResponse

What a client needs to render "who am I, and where": one entry of the company switcher.

An 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.

It carries no organization id a client could send back: switching names a membership and a role, and the server proves the caller holds both.

FieldTypeWhat it is
membership_id integer
org_name string
role string
first_name string
last_name string
emergency_responses_enabled boolean

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.

cancellation_emails_enabled boolean

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.

Leaving company services.LeavingCompany

What deleting an account does at one of the companies the person belongs to. It only ever describes the caller's own memberships.

FieldTypeWhat it is
org_name string
upcoming_shifts integer

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.

invitations integer
clocked_in boolean

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.

last_manager boolean

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.

Worker dashboard services.WorkerDashboard

"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.

Shifts 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.

FieldTypeWhat it is
awaiting_you list of Schedule entry

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.

One 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.

invitation_summaries list of Invitation summary

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.

this_week list of Schedule entry

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.

later list of Schedule entry

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.

open_shifts list of Open shift

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.

expiring_certificates list of Expiring certificate

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.

worked_this_week Worked

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.

worked_this_month Worked
still_to_earn list of Money total
still_to_earn_unrated_minutes integer

still_to_earn_unrated_minutes is accepted work ahead with no hourly rate agreed, and so in none of still_to_earn.

Manager dashboard services.ManagerDashboard

What needs a PM's attention, then the week in numbers.

FieldTypeWhat it is
attention list of Short shift

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.

attention_summaries list of Attention summary

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.

turned_down list of Decline

turned_down is who said no, recently, to a shift still short.

hours_to_check list of Flagged entry

hours_to_check are time entries somebody should ask about.

week list of Day staffing

week is the company's staffing for each of Days — the whole range, whatever its length, despite the name.

worked_this_week Worked

worked_this_week is every worker's finished hours this calendar week, at the rates agreed for them, whatever range is shown.

Shift services.Timeslot

A block of work on one day needing some number of workers.

FieldTypeWhat it is
id integer
gig_id integer
day_id integer
starts_at timestamp, RFC 3339 in UTC
ends_at timestamp, RFC 3339 in UTC
required_workers integer
role_label string, or null

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.

created_at timestamp, RFC 3339 in UTC
accepted integer

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.

pending integer

Job contact services.JobContact

The one person a worker is told to ring about a job.

A 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.

FieldTypeWhat it is
company string, or null

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.

name string, or null
phone string, or null

Project manager services.ProjectManager

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.

Deliberately 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.

FieldTypeWhat it is
name string
phone string, or null
email string, or null

Company tag companyTag

Says which of the person's entries a row of a read across them came from, and what to send to act on it.

FieldTypeWhat it is
entry string

entry is the value of X-Werzeick-Company (or the MCP's company) that acts as this entry: "12:manager".

org_name string
role string

Money total services.MoneyTotal

A sum in one currency.

Currencies 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.

The amount goes raw as an int64 with four implied decimals, never pre-formatted and never a float.

FieldTypeWhat it is
amount integer

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

currency string

Running shift services.RunningShift

A shift the acting worker is on the clock for right now, and what it has come to so far.

It is the one figure in this product about work that is not finished, and three things keep it honest:

  • No client multiplies anything, and the membership's rate today has no part in it.
  • 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.
  • A forgotten clock-out has none.
FieldTypeWhat it is
since timestamp, RFC 3339 in UTC

since is when they clocked in.

minutes integer
amount integer, or null

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.

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

currency string, or null
as_of timestamp, RFC 3339 in UTC

as_of is the server's clock when this was worked out. The figure is true of that minute and of no later one.

Skipped invitation skippedInvite

A person already live on a shift, so not asked again.

FieldTypeWhat it is
membership_id integer
worker_name string
shift Shift reference

Over-invited shift overInvited

A shift asked of more people than it has places: allowed, since some will say no, and said.

FieldTypeWhat it is
shift Shift reference
invited integer
places integer

Overlapping invitation inviteClash

A person asked to a shift that overlaps other work of theirs at this company: allowed, and said.

FieldTypeWhat it is
membership_id integer
worker_name string
shift Shift reference
with Shift reference

with is the shift it overlaps; in_batch says that one is in this very batch rather than already on their schedule.

in_batch boolean

Certificate page services.CertificatePage

One photo of a certificate, by position, without its bytes.

FieldTypeWhat it is
position integer
content_type string
width integer
height integer

Uploaded by services.UploadedBy

Names the manager who added a certificate, and at which company. The name reads "Deleted user" for one who has left.

FieldTypeWhat it is
membership_id integer
name string
company string

Certificate group certificateGroup

FieldTypeWhat it is
id string
name string

Certificate kind certificateKind

FieldTypeWhat it is
id string

id is what a note or a certificate carries as its kind.

name string
group string

group is the heading it is listed under, by the group's id.

aliases list of string

aliases are the words people use for it, in Czech, for a search.

Certificate trade certificateTrade

FieldTypeWhat it is
id string
name string
preferred list of string

preferred, related and common are kinds by id, in the order to offer them for this trade.

related list of string
common list of string

Page reading pageReading

How well one page read.

FieldTypeWhat it is
position integer

position is the page's place among those sent, counted from 1.

readability integer, or null

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.

legible boolean

legible is whether the page read well enough to trust: enough words, at 60 or more.

Invitation summary services.InvitationSummary

What one project's unanswered invitations add up to: how many, which days, how long, and what they would pay.

It is worked out here and not in a client because one of its figures is money.

Nothing 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.

FieldTypeWhat it is
gig_id integer
gig_title string
count integer

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.

full_count integer
first_day string

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.

last_day string
shift_summary string

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".

Left out of the body when it is empty.

minutes integer
would_earn list of Money total

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.

unrated_minutes integer

Open shift services.OpenSlotRow

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.

FieldTypeWhat it is
id integer
gig_id integer
day_id integer
starts_at timestamp, RFC 3339 in UTC
ends_at timestamp, RFC 3339 in UTC
required_workers integer
role_label string, or null

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.

created_at timestamp, RFC 3339 in UTC
accepted integer

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.

pending integer
gig_title string
role_id integer

role_id is the role this row offers, and what a take names.

rate_amount integer, or null

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.

Null 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.

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

rate_currency string, or null
mine_status string: pending | accepted | declined | expired | withdrawn | cancelled

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.

The 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.

taking_confirms boolean

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.

collides_with_title string, or null

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.

collides_with_company string, or null

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.

collides_from timestamp, RFC 3339 in UTC, or null
collides_until timestamp, RFC 3339 in UTC, or null
can_take_from timestamp, RFC 3339 in UTC, or null
can_take_until timestamp, RFC 3339 in UTC, or null

Expiring certificate services.ExpiringCertificate

One of the person's own certificates that is expiring or has expired, as the worker's dashboard and bell list it.

FieldTypeWhat it is
id integer
kind string
label string, or null

label is the typed name of an "other" certificate, null otherwise.

name string

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.

expires_on string

expires_on is the last valid day, "2026-11-20".

days_left integer

days_left is whole days until then: 0 on the day, negative once past.

expired boolean

expired says the day has passed; otherwise it is within the window.

Worked services.Worked

Hours on the clock in some period, and what they came to.

Open 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.

FieldTypeWhat it is
minutes integer
earned list of Money total
unrated_minutes integer

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.

Short shift services.VacancyRow

One upcoming timeslot that a PM should look at, with enough context to act on it without a second query.

FieldTypeWhat it is
id integer
gig_id integer
day_id integer
starts_at timestamp, RFC 3339 in UTC
ends_at timestamp, RFC 3339 in UTC
required_workers integer
role_label string, or null

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.

created_at timestamp, RFC 3339 in UTC
accepted integer

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.

pending integer
gig_title string

Attention summary services.AttentionSummary

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).

FieldTypeWhat it is
gig_id integer
gig_title string
count integer
first_day string
last_day string
shift_summary string

Left out of the body when it is empty.

required integer
accepted integer
pending integer
short integer
imminent boolean

Decline services.TurnedDownRow

Somebody saying no to work a PM was counting on: an invitation declined, or an accepted shift given up.

FieldTypeWhat it is
id integer
timeslot_id integer
membership_id integer
origin string: staffed | sos | open | backfilled
status string: pending | accepted | declined | expired | withdrawn | cancelled
rate_amount integer, or null

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.

Null 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.

Money: an integer with four implied decimal places, so 4200000 is 420.00, with its currency beside it.

rate_currency string, or null
role_id integer

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.

role_label string, or null
invited_at timestamp, RFC 3339 in UTC
responded_at timestamp, RFC 3339 in UTC, or null
expires_at timestamp, RFC 3339 in UTC
created_at timestamp, RFC 3339 in UTC
takes_from timestamp, RFC 3339 in UTC, or null

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.

takes_until timestamp, RFC 3339 in UTC, or null
worker_name string

worker_name is filled by reads a PM makes, so a staffing view can show who was invited without a second query per row.

Left out of the body when it is empty.

timeslot Shift
gig_title string

Flagged entry services.FlaggedEntry

One time entry a manager should look at, from any project.

The flags are decided here, on the server, so both clients show the same reason.

FieldTypeWhat it is
entry Time entry
worker_name string
gig_title string
slot_starts_at timestamp, RFC 3339 in UTC, or null
slot_ends_at timestamp, RFC 3339 in UTC, or null
missing_clock_out boolean
outside_shift boolean

Day staffing services.DayStaffing

One calendar day's staffing, summed across its timeslots.

This 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.

FieldTypeWhat it is
day string

YYYY-MM-DD

slots integer
required integer
accepted integer
pending integer
short integer

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.

Shift reference shiftRef

One shift, as a batch's answer names it.

FieldTypeWhat it is
timeslot_id integer
starts_at timestamp, RFC 3339 in UTC
ends_at timestamp, RFC 3339 in UTC