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
| Field | Type | What it is |
|---|---|---|
email |
string |
Response
{
"status": "sent if an account exists",
"resend_after_seconds": 30
}
Status codes
| Code | Message |
|---|---|
| 202 | |
| 400 | email is required · invalid JSON body |
| 429 | a code was sent moments ago; wait {seconds} seconds before asking for another · too many codes requested; wait a few minutes |
| 500 | internal server error |
Try it
POST /api/auth/verify
Exchanges a code for a session.
Public: no key and no session needed.
Request body
| Field | Type | What it is |
|---|---|---|
email |
string | |
code |
string |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | email and code are required · invalid JSON body |
| 401 | that code is not valid |
| 403 | this account is not a member of any organization |
| 500 | internal 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
| Code | Message |
|---|---|
| 204 | |
| 500 | internal 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
{
"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
| Code | Message |
|---|---|
| 200 | |
| 401 | authentication required |
| 500 | internal 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.
| Field | Type | What it is |
|---|---|---|
membership_id |
integer | |
role |
string |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body · role must be worker or manager |
| 401 | authentication required |
| 403 | not permitted |
| 500 | internal 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
| Field | Type | What it is |
|---|---|---|
enabled |
boolean, or null |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | enabled is required · invalid JSON body |
| 401 | authentication required |
| 403 | only a worker can take emergency shifts |
| 500 | internal 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
| Field | Type | What it is |
|---|---|---|
enabled |
boolean, or null |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | enabled is required · invalid JSON body |
| 401 | authentication required |
| 403 | only a worker is sent cancellations |
| 500 | internal 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
| Field | Type | What it is |
|---|---|---|
language |
string |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body · language must be cs or en |
| 401 | authentication required |
| 500 | internal 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
{
"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
| Code | Message |
|---|---|
| 200 | |
| 401 | authentication required |
| 500 | internal 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
| Code | Message |
|---|---|
| 204 | |
| 401 | authentication required |
| 409 | |
| 500 | internal 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.
| Field | Type | What it is |
|---|---|---|
token |
string |
Response
{
"org_name": "Alpha Stavby",
"is_manager": false,
"is_worker": true,
"first_name": "Petra",
"email": "petra@example.test",
"rate_amount": 4200000,
"rate_currency": "CZK"
}
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body |
| 404 | this invitation is invalid, expired, or already used |
| 500 | internal 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.
| Field | Type | What it is |
|---|---|---|
token |
string |
Response
{
"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
}
{
"outcome": "needs_code",
"email": "petra@example.test",
"code_sent": true,
"session": null,
"membership": null
}
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body |
| 404 | this invitation is invalid, expired, or already used |
| 500 | internal 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
| Field | Type | What 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
Status codes
| Code | Message |
|---|---|
| 201 | |
| 400 | a rate needs both an amount and a currency · email, first_name and last_name are required · invalid JSON body · is_manager or is_worker must be true |
| 401 | authentication required |
| 403 | not permitted |
| 409 | that person is already in your organization |
| 500 | internal server error |
Try it
GET /api/invitations
Shows who has been invited but has not yet accepted.
An API key or a session.
Response
Status codes
| Code | Message |
|---|---|
| 200 | |
| 401 | authentication required |
| 403 | not permitted |
| 500 | internal 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
{
"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
}
}
}
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | weeks must be a whole number |
| 401 | authentication required |
| 403 | not permitted |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The shift's id. |
Request body
| Field | Type | What it is |
|---|---|---|
partial |
boolean | |
role_id |
integer |
Response
{
"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
| Code | Message |
|---|---|
| 201 | |
| 400 | invalid timeslot id · this shift has several roles; send role_id |
| 401 | authentication required |
| 403 | not permitted · this shift raises an hourly rate, and you have none here to raise |
| 404 | shift not found |
| 409 | |
| 500 | internal 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
[
{
"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
| Code | Message |
|---|---|
| 200 | |
| 401 | authentication required |
| 403 | not permitted |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The assignment's id: your place on a shift. |
Request body
| Field | Type | What it is |
|---|---|---|
action |
string | |
partial |
boolean |
|
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | action must be "accept", "decline" or "withdraw" · invalid JSON body · invalid assignment id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Request body
| Field | Type | What it is |
|---|---|---|
action |
string |
Response
{
"answered": 2,
"full": 1,
"colliding": 0
}
Status codes
| Code | Message |
|---|---|
| 200 | |
| 400 | action must be "accept" or "decline" · invalid JSON body · invalid project id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Field | Type | What it is |
|---|---|---|
token |
string | |
platform |
string | |
language |
string |
Response
No body.
Status codes
| Code | Message |
|---|---|
| 204 | |
| 400 | invalid JSON body |
| 401 | authentication 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
| Field | Type | What it is |
|---|---|---|
token |
string |
Response
No body.
Status codes
| Code | Message |
|---|---|
| 204 | |
| 400 | a device token is required · invalid JSON body |
| 401 | authentication required |
| 403 | not permitted |
| 500 | internal 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.
| Field | Type | What it is |
|---|---|---|
gig_id |
integer | |
at |
string |
|
source |
string |
|
client_event_id |
string |
|
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | at must be an RFC 3339 timestamp · invalid JSON body |
| 401 | authentication required |
| 403 | not permitted |
| 404 | time entry not found |
| 409 | |
| 500 | internal 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.
| Field | Type | What it is |
|---|---|---|
gig_id |
integer | |
at |
string |
|
source |
string |
|
client_event_id |
string |
|
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | at must be an RFC 3339 timestamp · clock-out needs a gig_id or a client_event_id · invalid JSON body |
| 401 | authentication required |
| 403 | not permitted |
| 404 | time entry not found |
| 409 | |
| 500 | internal 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
[
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | since must be an RFC 3339 timestamp |
| 401 | authentication required |
| 403 | not permitted |
| 404 | time entry not found |
| 409 | |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The time entry's id. |
Request body
| Field | Type | What it is |
|---|---|---|
clock_in_at |
string | |
clock_out_at |
string, or null |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | clock_in_at must be an RFC 3339 timestamp · clock_out_at must be an RFC 3339 timestamp or null · invalid JSON body · invalid time entry id |
| 401 | authentication required |
| 403 | not permitted |
| 404 | time entry not found |
| 409 | |
| 500 | internal 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
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | all=1 reads every company: leave out X-Werzeick-Company |
| 401 | authentication required |
| 403 | not permitted |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid gig id |
| 401 | authentication required |
| 403 | not permitted |
| 404 | gig not found |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Response
{
"earned": [
{
"amount": 33600000,
"currency": "CZK"
}
],
"upcoming": [
{
"amount": 50400000,
"currency": "CZK"
}
],
"earned_unrated_minutes": 0,
"upcoming_unrated_minutes": 360,
"running": null
}
Status codes
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted |
| 404 | gig not found |
| 500 | internal 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.
| Field | Type | What 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
{
"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
| Code | Message |
|---|---|
| 201 | |
| 400 | invalid JSON body · title is required |
| 401 | authentication required |
| 403 | not permitted |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The 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.
| Field | Type | What 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
{
"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
| Code | Message |
|---|---|
| 201 | |
| 400 | invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted |
| 404 | expense not found · gig not found |
| 500 | internal server error |
Try it
PATCH /api/gigs/{id}/expenses/{expenseID}
Corrects a cost already booked.
An API key or a session.
In the path
| Parameter | What it is |
|---|---|
id | The project's id. |
expenseID | The 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.
| Field | Type | What 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
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted |
| 404 | expense not found · gig not found |
| 500 | internal server error |
Try it
DELETE /api/gigs/{id}/expenses/{expenseID}
Deletes a cost.
An API key or a session.
In the path
| Parameter | What it is |
|---|---|
id | The project's id. |
expenseID | The expense's id. |
Response
No body.
Status codes
| Code | Message |
|---|---|
| 204 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted |
| 404 | expense not found · gig not found |
| 500 | internal 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
[
{
"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
| Code | Message |
|---|---|
| 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
[
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | |
| 401 | authentication required |
| 403 | not permitted |
| 404 | tool not found |
| 500 | internal 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.
| Field | Type | What it is |
|---|---|---|
name |
string | |
icon |
string | |
quantity |
integer, or null |
Response
{
"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
| Code | Message |
|---|---|
| 201 | |
| 400 | invalid JSON body |
| 401 | authentication required |
| 403 | not permitted |
| 404 | tool not found |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The 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.
| Field | Type | What it is |
|---|---|---|
name |
string | |
icon |
string | |
quantity |
integer, or null |
Response
{
"id": 11,
"name": "Clamps",
"icon": "other",
"quantity": 6,
"created_at": "2026-09-08T14:21:02.884113+02:00",
"gig_ids": []
}
Status codes
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted |
| 404 | tool not found |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The tool's id. |
Response
No body.
Status codes
| Code | Message |
|---|---|
| 204 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted |
| 404 | tool not found |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Response
[
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted |
| 404 | tool not found |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Request body
Names the tool being taken to a project.
| Field | Type | What it is |
|---|---|---|
tool_id |
integer |
Response
No body.
Status codes
| Code | Message |
|---|---|
| 204 | |
| 400 | invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted |
| 404 | tool not found |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
toolID | The tool's id. |
Response
No body.
Status codes
| Code | Message |
|---|---|
| 204 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted |
| 404 | tool not found |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Query parameters
fromto
Response
[
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | from must be a calendar day, YYYY-MM-DD · invalid {thing} id · the range must run forwards and be at most a year · to must be a calendar day, YYYY-MM-DD |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Request body
Names calendar days, "2026-10-27".
| Field | Type | What it is |
|---|---|---|
days |
list of string | |
confirm |
boolean |
|
Response
Status codes
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body · invalid {thing} id · name at least one day |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Request body
Names calendar days, "2026-10-27".
| Field | Type | What it is |
|---|---|---|
days |
list of string | |
confirm |
boolean |
|
Response
Status codes
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body · invalid {thing} id · name at least one day |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Request body
One shift laid on each of several days.
| Field | Type | What it is |
|---|---|---|
days |
list of string |
|
starts_at |
string |
|
ends_at |
string | |
roles |
list of Role to add |
|
headcount |
integer |
|
open |
boolean |
|
open_auto_accept |
boolean | |
offered_rate_multiplier |
integer, or null |
A multiplier with four implied decimal places: 15000 is 1.5×. |
Response
{
"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
| Code | Message |
|---|---|
| 201 | |
| 400 | ends_at must be a time of day, as 15:00 · invalid JSON body · invalid {thing} id · name at least one day · starts_at must be a time of day, as 07:00, on a day written YYYY-MM-DD |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Request body
Moves shifts by whole days.
| Field | Type | What it is |
|---|---|---|
timeslot_ids |
list of integer |
|
days |
integer |
|
confirm |
boolean |
|
Response
Status codes
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The shift's id. |
Request body
A removal's consent.
| Field | Type | What it is |
|---|---|---|
confirm |
boolean |
|
Response
Status codes
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The shift's id. |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The shift's id. |
Request body
Names who to invite to a shift, and for which role.
| Field | Type | What it is |
|---|---|---|
membership_id |
integer |
|
role_id |
integer |
|
Response
{
"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
| Code | Message |
|---|---|
| 201 | |
| 400 | invalid JSON body · invalid {thing} id · membership_id is required |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Request body
Asks several people to several shifts at once.
| Field | Type | What it is |
|---|---|---|
membership_ids |
list of integer |
|
timeslot_ids |
list of integer |
|
role |
string, or null |
|
preview |
boolean |
|
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The assignment's id: your place on a shift. |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The assignment's id: your place on a shift. |
Request body
A removal's consent.
| Field | Type | What it is |
|---|---|---|
confirm |
boolean |
|
Response
Status codes
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body · invalid {thing} id · send confirm: this cancels their place and tells them |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Response
Status codes
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
membershipID | The person's membership id, as the roster lists it. |
Request body
A removal's consent.
| Field | Type | What it is |
|---|---|---|
confirm |
boolean |
|
Response
Status codes
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The shift's id. |
Request body
The terms a shift is opened on.
| Field | Type | What it is |
|---|---|---|
auto_accept |
boolean |
|
offered_rate_multiplier |
integer, or null |
A multiplier with four implied decimal places: 15000 is 1.5×. |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | a multiplier must be more than nothing · invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The shift's id. |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The shift's id. |
Request body
The terms an SOS is raised on.
| Field | Type | What it is |
|---|---|---|
offered_rate_multiplier |
integer, or null |
A multiplier with four implied decimal places: 15000 is 1.5×. |
auto_accept |
boolean |
|
Response
{
"id": 1,
"timeslot_id": 1,
"offered_rate_multiplier": 15000,
"opened_at": "2026-09-09T16:00:00Z",
"told": 0
}
Status codes
| Code | Message |
|---|---|
| 201 | |
| 400 | a multiplier must be more than nothing · invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Request body
Raises an SOS over a project's short shifts.
| Field | Type | What it is |
|---|---|---|
offered_rate_multiplier |
integer, or null |
A multiplier with four implied decimal places: 15000 is 1.5×. |
auto_accept |
boolean |
|
timeslot_ids |
list of integer |
|
Response
{
"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
| Code | Message |
|---|---|
| 201 | |
| 400 | a multiplier must be more than nothing · invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
alldays
Response
list of Shift needing attention
[
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | all=1 reads every company: leave out X-Werzeick-Company · days must be a whole number from 1 to 92 |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
[
{
"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
| Code | Message |
|---|---|
| 200 | |
| 401 | authentication required |
| 403 | not permitted |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The 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.
| Field | Type | What it is |
|---|---|---|
certificate_id |
integer |
|
kind |
string |
|
label |
string |
|
scope |
string | At most 200 characters. |
expires_on |
string, or null |
|
Response
{
"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
| Code | Message |
|---|---|
| 201 | |
| 400 | expires_on must be YYYY-MM-DD or null · invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The person's membership id, as the roster lists it. |
certificationID | The note's id, as the roster lists it under the person. |
Response
No body.
Status codes
| Code | Message |
|---|---|
| 204 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The person's membership id, as the roster lists it. |
Response
[
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The person's membership id, as the roster lists it. |
Request body
What is typed about a certificate: what is printed on the card.
| Field | Type | What it is |
|---|---|---|
kind |
string |
|
label |
string |
|
scope |
string | At most 200 characters. |
number |
string |
|
issued_on |
string, or null |
|
expires_on |
string, or null |
|
Response
{
"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
| Code | Message |
|---|---|
| 201 | |
| 400 | invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The person's membership id, as the roster lists it. |
certificateID | The 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.
| Field | Type | What 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
{
"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
| Code | Message |
|---|---|
| 201 | |
| 400 | image must be base64 · invalid JSON body · invalid {thing} id · send the image as a part named page · the upload could not be read |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The person's membership id, as the roster lists it. |
certificateID | The certificate's id, as the list of certificates gives it. |
position | The page's position on the certificate, counted from 1. |
Response
No body.
Status codes
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · page not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
{
"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
| Code | Message |
|---|---|
| 200 | |
| 401 | authentication 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
[
{
"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
| Code | Message |
|---|---|
| 200 | |
| 401 | authentication required |
| 403 | not permitted |
| 500 | internal 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.
| Field | Type | What it is |
|---|---|---|
kind |
string |
|
label |
string |
|
scope |
string | At most 200 characters. |
number |
string |
|
issued_on |
string, or null |
|
expires_on |
string, or null |
|
Response
{
"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
| Code | Message |
|---|---|
| 201 | |
| 400 | invalid JSON body |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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.
| Field | Type | What it is |
|---|---|---|
images |
list of string | They are read and then forgotten; nothing is stored until the certificate is created. |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | image must be base64 · invalid JSON body · send at least one page to read · send the image as a part named page · the upload could not be read |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal server error |
| 503 | reading 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
| Parameter | What it is |
|---|---|
id | The certificate's id, as the list of certificates gives it. |
Request body
What is typed about a certificate: what is printed on the card.
| Field | Type | What it is |
|---|---|---|
kind |
string |
|
label |
string |
|
scope |
string | At most 200 characters. |
number |
string |
|
issued_on |
string, or null |
|
expires_on |
string, or null |
|
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid JSON body · invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The certificate's id, as the list of certificates gives it. |
Response
No body.
Status codes
| Code | Message |
|---|---|
| 204 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The 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.
| Field | Type | What 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
{
"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
| Code | Message |
|---|---|
| 201 | |
| 400 | image must be base64 · invalid JSON body · invalid {thing} id · send the image as a part named page · the upload could not be read |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The certificate's id, as the list of certificates gives it. |
position | The page's position on the certificate, counted from 1. |
Response
No body.
Status codes
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · page not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The certificate's id, as the list of certificates gives it. |
position | The page's position on the certificate, counted from 1. |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · page not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
[
{
"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
| Code | Message |
|---|---|
| 200 | |
| 401 | authentication required |
| 403 | not permitted |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The client's id. |
Response
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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.
| Field | Type | What it is |
|---|---|---|
name |
string | |
ico |
string, or null |
|
dic |
string, or null |
|
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 | 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
{
"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
| Code | Message |
|---|---|
| 201 | |
| 400 | client name is required · invalid JSON body |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
| Parameter | What it is |
|---|---|
id | The project's id. |
Response
[
{
"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
| Code | Message |
|---|---|
| 200 | |
| 400 | invalid {thing} id |
| 401 | authentication required |
| 403 | not permitted · that application is the manager's to approve |
| 404 | assignment not found · certificate not found · certification not found · client not found · gig not found · shift not found |
| 409 | that invitation is no longer open · that is an invitation, and only the worker can accept it · that shift collides with work you have accepted · that shift is already full |
| 500 | internal 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
{
"status": "ok"
}
Status codes
| Code | Message |
|---|---|
| 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.
| Field | Type | What it is |
|---|---|---|
status |
string | |
resend_after_seconds |
integer |
|
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.
| Field | Type | What 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 |
|
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.
| Field | Type | What it is |
|---|---|---|
can_delete |
boolean |
|
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.
| Field | Type | What 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.
sessionis the body a sign-in answers with, token included, and the app is in. - "needs_code": the account was there already.
code_sentsays a code is on its way toemail, and the app opens at "enter the code". - "joined": the caller is already signed in as that account.
membershipis the entry it gained, so the app can offer to switch to it; the session it holds is unchanged and is simply read again.
| Field | Type | What 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.
| Field | Type | What 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.
| Field | Type | What it is |
|---|---|---|
role |
string: worker | manager | |
days |
list of string |
|
today |
string |
|
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.
| Field | Type | What 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 |
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_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_until |
timestamp, RFC 3339 in UTC, or null | |
worker_name |
string |
Left out of the body when it is empty. |
Schedule entry services.ScheduleEntry
One row of a worker's own schedule.
| Field | Type | What 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 |
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_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_until |
timestamp, RFC 3339 in UTC, or null | |
worker_name |
string |
Left out of the body when it is empty. |
timeslot |
Shift | |
gig_id |
integer | |
gig_title |
string | |
full |
boolean |
|
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_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.
| Field | Type | What it is |
|---|---|---|
answered |
integer |
|
full |
integer |
|
colliding |
integer |
|
Time entry services.TimeEntry
One stretch of work: when someone started, when they stopped, and what it was worth.
| Field | Type | What it is |
|---|---|---|
id |
integer | |
membership_id |
integer | |
gig_id |
integer | |
timeslot_id |
integer, or null |
|
clock_in_at |
timestamp, RFC 3339 in UTC | |
clock_out_at |
timestamp, RFC 3339 in UTC, or null |
|
source |
string: manual | geofence | backfilled | |
rate_amount |
integer, or null |
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 |
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.
| Field | Type | What it is |
|---|---|---|
id |
integer | |
title |
string | |
owner_id |
integer |
|
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 |
client_contact_phone |
string, or null | |
client_contact_email |
string, or null | |
job_contact |
Job contact, or null |
|
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. 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 |
|
expenses |
list of Expense | |
created_at |
timestamp, RFC 3339 in UTC | |
is_assigned |
boolean | |
is_invited |
boolean |
|
shifts_to_take |
integer | |
archived_at |
timestamp, RFC 3339 in UTC, or null |
|
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 |
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.
| Field | Type | What it is |
|---|---|---|
earned |
list of Money total |
|
upcoming |
list of Money total |
|
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 |
|
Point geo.Point
A WGS84 coordinate in degrees.
| Field | Type | What 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.
| Field | Type | What it is |
|---|---|---|
id |
integer | |
gig_id |
integer |
|
created_by_membership_id |
integer |
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 |
|
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.
| Field | Type | What it is |
|---|---|---|
name |
string |
|
label |
string |
|
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.
| Field | Type | What it is |
|---|---|---|
id |
integer | |
name |
string | |
icon |
string | |
quantity |
integer, or null |
|
created_at |
timestamp, RFC 3339 in UTC | |
gig_ids |
list of integer |
|
Calendar day calendarDay
One day a project runs, with the shifts on it.
| Field | Type | What it is |
|---|---|---|
day |
string |
|
day_id |
integer | |
shifts |
list of Shift as a manager sees it |
|
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.
| Field | Type | What 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.
| Field | Type | What it is |
|---|---|---|
confirmed |
boolean |
|
days |
integer |
|
people |
integer |
|
cancelled |
integer |
|
Role to add roleRequest
One role on a new shift.
| Field | Type | What it is |
|---|---|---|
label |
string |
|
headcount |
integer |
|
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 |
Added shifts addedShifts
What laying a shift on days made.
| Field | Type | What it is |
|---|---|---|
shifts |
list of Shift as a manager sees it | |
scheduled |
integer |
|
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.
| Field | Type | What it is |
|---|---|---|
id |
integer | |
label |
string, or null |
|
headcount |
integer |
|
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 | |
accepted |
integer |
|
pending |
integer |
Moved shifts movedShifts
What a move did, or would do.
| Field | Type | What it is |
|---|---|---|
confirmed |
boolean |
|
shifts |
integer |
|
skipped |
integer | |
people |
integer |
|
first_day |
string |
|
Removed shift removedShift
What removing a shift did, or would do.
| Field | Type | What it is |
|---|---|---|
confirmed |
boolean |
|
accepted |
integer |
|
pending |
integer | |
cancelled |
integer |
|
Shift with its people shiftDetail
A shift with the people on it.
| Field | Type | What 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 |
|
created_at |
timestamp, RFC 3339 in UTC | |
accepted |
integer | Staffing is filled by the reads that compute it. |
pending |
integer | |
short |
integer |
|
is_open |
boolean |
|
open_auto_accept |
boolean | |
offered_rate_multiplier |
integer, or null |
A multiplier with four implied decimal places: 15000 is 1.5×. |
roles |
list of Role | |
people |
list of Assignment |
|
Invitation batch invitePlan
What a batch of invitations does, or would do.
| Field | Type | What it is |
|---|---|---|
preview |
boolean |
|
people |
integer | |
shifts |
integer | |
invitations |
integer |
|
skipped |
list of Skipped invitation | |
over_filled |
list of Over-invited shift | |
clashes |
list of Overlapping invitation | |
sent |
list of Assignment |
|
Person on a project personOnProject
One person with live places on a project's upcoming shifts.
| Field | Type | What it is |
|---|---|---|
membership_id |
integer | |
name |
string | |
accepted |
integer |
|
pending |
integer | |
first_start |
timestamp, RFC 3339 in UTC |
|
last_start |
timestamp, RFC 3339 in UTC |
Taken off a project takenOff
What taking somebody off a project did, or would do.
| Field | Type | What it is |
|---|---|---|
confirmed |
boolean |
|
accepted |
integer |
|
pending |
integer | |
first_day |
string |
|
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.
| Field | Type | What 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 |
|
created_at |
timestamp, RFC 3339 in UTC | |
accepted |
integer | Staffing is filled by the reads that compute it. |
pending |
integer | |
short |
integer |
|
is_open |
boolean |
|
open_auto_accept |
boolean | |
offered_rate_multiplier |
integer, or null |
A multiplier with four implied decimal places: 15000 is 1.5×. |
roles |
list of Role |
SOS raised sosRaised
One emergency opened on one shift.
| Field | Type | What it is |
|---|---|---|
id |
integer | |
timeslot_id |
integer | |
offered_rate_multiplier |
integer |
A multiplier with four implied decimal places: 15000 is 1.5×. |
opened_at |
timestamp, RFC 3339 in UTC | |
told |
integer |
|
SOS over a project sosBatch
What an SOS over several shifts did.
| Field | Type | What it is |
|---|---|---|
raised |
list of SOS raised |
|
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 |
|
Shift needing attention shortShift
An upcoming shift that needs a manager's attention.
| Field | Type | What 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 |
|
created_at |
timestamp, RFC 3339 in UTC | |
accepted |
integer | Staffing is filled by the reads that compute it. |
pending |
integer | |
gig_title |
string | |
short |
integer |
|
sos_open |
boolean |
|
roles |
list of Role | |
company |
Company tag, or null |
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.
| Field | Type | What 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 |
|
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 |
|
Certification response certificationResponse
One "certified for" note, with the kind's name in the request's language.
| Field | Type | What it is |
|---|---|---|
id |
integer | |
membership_id |
integer | |
kind |
string |
|
label |
string, or null |
|
scope |
string, or null | |
expires_on |
string, or null |
|
granted_by_membership_id |
integer |
|
granted_by_name |
string | |
created_at |
timestamp, RFC 3339 in UTC | |
certificate_id |
integer, or null |
|
name |
string |
|
Certificate response certificateResponse
One certificate, with the kind's name in the request's language.
| Field | Type | What it is |
|---|---|---|
id |
integer | |
kind |
string |
|
label |
string, or null |
|
scope |
string, or null | |
number |
string, or null |
|
issued_on |
string, or null |
|
expires_on |
string, or null | |
pages |
list of Certificate page |
|
uploaded_by |
Uploaded by, or null |
|
created_at |
timestamp, RFC 3339 in UTC | |
updated_at |
timestamp, RFC 3339 in UTC | |
name |
string |
|
Certificate kinds certificateKinds
The list of kinds, in the request's language.
| Field | Type | What it is |
|---|---|---|
groups |
list of Certificate group |
|
kinds |
list of Certificate kind |
|
trades |
list of Certificate trade |
|
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.
| Field | Type | What it is |
|---|---|---|
kind |
string, or null |
|
kind_name |
string, or null |
|
issued_on |
string, or null |
|
expires_on |
string, or null | |
number |
string, or null |
|
pages |
list of Page reading |
|
legible |
boolean |
|
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.
| Field | Type | What 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.
| Field | Type | What it is |
|---|---|---|
id |
integer | |
membership_id |
integer | |
gig_id |
integer | |
timeslot_id |
integer, or null |
|
clock_in_at |
timestamp, RFC 3339 in UTC | |
clock_out_at |
timestamp, RFC 3339 in UTC, or null |
|
source |
string: manual | geofence | backfilled | |
rate_amount |
integer, or null |
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 |
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 |
|
slot_starts_at |
timestamp, RFC 3339 in UTC, or null |
|
slot_ends_at |
timestamp, RFC 3339 in UTC, or null | |
recorded_by |
string |
|
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.
| Field | Type | What 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.
| Field | Type | What it is |
|---|---|---|
org_name |
string | |
upcoming_shifts |
integer |
|
invitations |
integer | |
clocked_in |
boolean |
|
last_manager |
boolean |
|
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.
| Field | Type | What it is |
|---|---|---|
awaiting_you |
list of Schedule entry |
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 |
|
this_week |
list of Schedule entry |
|
later |
list of Schedule entry |
|
open_shifts |
list of Open shift |
|
expiring_certificates |
list of Expiring certificate |
|
worked_this_week |
Worked | Earned is from time entries; |
worked_this_month |
Worked | |
still_to_earn |
list of Money total | |
still_to_earn_unrated_minutes |
integer |
|
Manager dashboard services.ManagerDashboard
What needs a PM's attention, then the week in numbers.
| Field | Type | What it is |
|---|---|---|
attention |
list of Short shift |
|
attention_summaries |
list of Attention summary |
|
turned_down |
list of Decline |
|
hours_to_check |
list of Flagged entry |
|
week |
list of Day staffing |
|
worked_this_week |
Worked |
|
Shift services.Timeslot
A block of work on one day needing some number of workers.
| Field | Type | What 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 |
|
created_at |
timestamp, RFC 3339 in UTC | |
accepted |
integer | Staffing is filled by the reads that compute it. |
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.
| Field | Type | What it is |
|---|---|---|
company |
string, or null |
|
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.
| Field | Type | What 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.
| Field | Type | What it is |
|---|---|---|
entry |
string |
|
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.
| Field | Type | What 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.
| Field | Type | What it is |
|---|---|---|
since |
timestamp, RFC 3339 in UTC |
|
minutes |
integer | |
amount |
integer, or null |
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 |
|
Skipped invitation skippedInvite
A person already live on a shift, so not asked again.
| Field | Type | What 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.
| Field | Type | What 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.
| Field | Type | What it is |
|---|---|---|
membership_id |
integer | |
worker_name |
string | |
shift |
Shift reference | |
with |
Shift reference |
|
in_batch |
boolean |
Certificate page services.CertificatePage
One photo of a certificate, by position, without its bytes.
| Field | Type | What 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.
| Field | Type | What it is |
|---|---|---|
membership_id |
integer | |
name |
string | |
company |
string |
Certificate group certificateGroup
| Field | Type | What it is |
|---|---|---|
id |
string | |
name |
string |
Certificate kind certificateKind
| Field | Type | What it is |
|---|---|---|
id |
string |
|
name |
string | |
group |
string |
|
aliases |
list of string |
|
Certificate trade certificateTrade
| Field | Type | What it is |
|---|---|---|
id |
string | |
name |
string | |
preferred |
list of string |
|
related |
list of string | |
common |
list of string |
Page reading pageReading
How well one page read.
| Field | Type | What it is |
|---|---|---|
position |
integer |
|
readability |
integer, or null |
|
legible |
boolean |
|
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.
| Field | Type | What it is |
|---|---|---|
gig_id |
integer | |
gig_title |
string | |
count |
integer |
|
full_count |
integer | |
first_day |
string |
|
last_day |
string | |
shift_summary |
string |
Left out of the body when it is empty. |
minutes |
integer | |
would_earn |
list of Money total |
|
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.
| Field | Type | What 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 |
|
created_at |
timestamp, RFC 3339 in UTC | |
accepted |
integer | Staffing is filled by the reads that compute it. |
pending |
integer | |
gig_title |
string | |
role_id |
integer |
|
rate_amount |
integer, or null |
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 |
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 |
|
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_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.
| Field | Type | What it is |
|---|---|---|
id |
integer | |
kind |
string | |
label |
string, or null |
|
name |
string |
|
expires_on |
string |
|
days_left |
integer |
|
expired |
boolean |
|
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.
| Field | Type | What it is |
|---|---|---|
minutes |
integer | |
earned |
list of Money total | |
unrated_minutes |
integer |
|
Short shift services.VacancyRow
One upcoming timeslot that a PM should look at, with enough context to act on it without a second query.
| Field | Type | What 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 |
|
created_at |
timestamp, RFC 3339 in UTC | |
accepted |
integer | Staffing is filled by the reads that compute it. |
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).
| Field | Type | What 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.
| Field | Type | What 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 |
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_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_until |
timestamp, RFC 3339 in UTC, or null | |
worker_name |
string |
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.
| Field | Type | What 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.
| Field | Type | What it is |
|---|---|---|
day |
string | YYYY-MM-DD |
slots |
integer | |
required |
integer | |
accepted |
integer | |
pending |
integer | |
short |
integer |
|
Shift reference shiftRef
One shift, as a batch's answer names it.
| Field | Type | What it is |
|---|---|---|
timeslot_id |
integer | |
starts_at |
timestamp, RFC 3339 in UTC | |
ends_at |
timestamp, RFC 3339 in UTC |