Werzeick API
Your firm's shifts and hours, from your own software.
The API the Werzeick app uses is yours to use too. A key from your account page, one header on every request, JSON both ways. This guide takes you from the key to a working integration; the reference has every endpoint.
Before you start
The API is the same one the Werzeick app talks to. Everything a person can see and do in the app, a program holding their key can see and do too: their projects, their week, their hours, their tools. Nothing more, and nothing of anybody else's.
Requests go to https://werzeick-dev.isc.hm/api/. Bodies are JSON, answers are JSON, and every key is snake_case. The one word to know before reading any of it: on the wire a project is a gig. The paths say /api/gigs, the fields say gig_id, and the screens say project. They are the same thing.
Available to every Werzeick account for now. Which plan it will belong to later is not settled, and nothing here will change when it is.
Your API key
A key is made on the web, on your account page, and it is shown exactly once.
- Sign in to the web client and open your account page, by your name at the foot of the menu.
- Under “API keys”, press “New API key” and give it a name that says what it is for: “Payroll export”, “Site display”.
- Copy the key. It starts
wzk_and the page never shows it again; the list shows only its last four characters.
Send it with every request, as a bearer token:
Authorization: Bearer wzk_YOUR_KEY
A key is a session that does not expire, and it is yours, not one company's. Unless a request says otherwise it acts as the entry you were in when you made it: the company, and the role at that company. A request can name any other of your entries instead, in one header; see “Several companies” below.
You can hold up to 20 live keys. Each one records when it was last used, which is how you tell a forgotten one from one in use. Revoking a key stops it at once.
Your first request
Ask the API who you are:
curl https://werzeick-dev.isc.hm/api/auth/me \
-H "Authorization: Bearer wzk_YOUR_KEY"
{
"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": [ … ],
"language": "cs"
}
membership is the entry the key acts as: the company, the role there, and the person's own two settings. memberships is every entry the person holds, the same shape each, and any of them is one a request may name.
language is the language the account chose on the web, or null if it never did. It is about the person, not the key.
Several companies
Somebody who works for two firms, or manages at one and works shifts at another, has an entry for each in memberships. One key reaches all of them. A request acts as one entry at a time: the key's own, or the one named in the X-Werzeick-Company header, as membership_id:role (12:manager), or as membership_id alone for that membership's first role.
Two lists read across every entry at once with all=1: the projects (GET /api/gigs?all=1) and the shifts that need a manager (GET /api/staffing/short?all=1, at every company you manage). Each row carries company, and its entry is what to send to act on that row:
curl "https://werzeick-dev.isc.hm/api/gigs?all=1" \
-H "Authorization: Bearer wzk_YOUR_KEY"
[{"id": 7, "title": "Karlín, bytový dům", …,
"company": {"entry": "12:manager", "org_name": "Alpha Stavby", "role": "manager"}},
{"id": 31, "title": "Hala Kladno", …,
"company": {"entry": "40:manager", "org_name": "Beta Construction", "role": "manager"}}]
curl https://werzeick-dev.isc.hm/api/gigs/31/calendar \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "X-Werzeick-Company: 40:manager"
An entry that is not yours answers 404, as another company's project does; one you have left stops working on the next request. A session in the app or the web is one entry and changes by switching, so a session that names one is refused with 400. Nothing is ever done in two companies by one request.
How the API speaks
- Ids are integers, and they are never reused.
- Timestamps are RFC 3339 in UTC:
2026-10-19T04:58:00Z. You may send one with an offset; you will always be answered in UTC. - Calendar days are dates,
2026-10-19, with no time and no zone. Which days a project runs is a date question, and the API never answers it with a timestamp. - Money is an integer with four implied decimal places, beside its currency. 420 CZK an hour is
4200000with"CZK". Never a float, and never pre-formatted: you format it at the edge, and nothing in between divides by ten thousand. The currency isCZKorEUR, and the two are never mixed in one total: a company working in both sees two. - No hourly rate is a real state, and it is null, never zero. A shift agreed without a rate records hours and no money; an
earned_amountof null is “hours only”, not unpaid work. - A multiplier (an SOS raise, 1.5× by default) uses the same four decimal places:
15000is 1.5×.
"rate_amount": 4200000,
"rate_currency": "CZK"
Errors are a status code and a line of plain text, not a JSON document. 400 is a body the server could not read or a field it refuses; 401 is a missing or revoked credential; 403 is a credential that is good and a thing it may not do; 404 is a thing that does not exist for you, which is also how another company's data reads; 409 is a request that was well formed and permitted and could not be done because the world has moved on: the shift filled, the invitation lapsed, the clock was already out. The reference lists each endpoint's codes and the messages they carry.
HTTP/1.1 409 Conflict
Content-Type: text/plain; charset=utf-8
that shift is already full
The language of a message follows the Accept-Language header: cs or en, nothing else is read. Without it, messages are English. It also chooses the language of the shift summaries the dashboard words for you.
No request ever carries a company's id. There is no org_id in any body, path or query, and one you add is ignored. The company is the key's own, or one of your own entries that the request names, which the server proves is yours on every request. That is what keeps one firm's data from another's, and it is not optional.
A key cannot do what is about the session or the account: switch entry, sign out, register a phone for push, choose a language, or delete the account. Those answer 403 to a key. Everything about work takes a key like a session.
A worker's week
A worker's home screen is GET /api/dashboard: the invitations waiting for an answer, the week's shifts, what lies beyond, the open shifts they could take, and what the week and the month have earned so far. weeks looks one to four weeks ahead, from this Monday.
curl "https://werzeick-dev.isc.hm/api/dashboard?weeks=2" \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Accept-Language: cs"
Invitations arrive grouped by project in awaiting_you, and invitation_summaries says what each project's group adds up to: how many shifts, which days, how many hours, what accepting them all would pay. Neither you nor the app has to add anything up; the figures are the server's, at the rate each invitation snapshotted when it was made.
An invitation is answered on its own assignment:
curl -X POST https://werzeick-dev.isc.hm/api/assignments/42/respond \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"action": "accept"}'
action is accept, decline, or withdraw for work already accepted. The answer is the assignment with its new status. A shift whose places are all taken refuses an accept with 409 and leaves the invitation waiting, so it can be accepted if a place reopens; the dashboard marks such a shift full before you try.
A project's invitations can be answered together, naming the project and not a list of ids: which invitations are yours to answer is decided on the server at that moment.
curl -X POST https://werzeick-dev.isc.hm/api/gigs/7/invitations/respond \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"action": "accept"}'
Open shifts are in open_shifts, each with the rate rate_amount this worker would be paid and taking_confirms, which says whether taking it books them outright or makes them an applicant the manager picks from. Both are part of the offer. Taking one sends the shift's id and nothing else; the rate is settled on the server, never sent by a client.
A shift that collides with work already accepted can be taken in part. The dashboard says what collides and the free remainder (can_take_from, can_take_until); sending partial: true is consent to take that remainder, and the server computes the span. Without it the take is refused.
curl -X POST https://werzeick-dev.isc.hm/api/timeslots/318/take \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"partial": true}'
Hours: clocking in and out
A clock event is the one request designed for a phone with no signal, and your software gets the same guarantees. Give each event an id of your own in client_event_id and send it as many times as you like: the first creates the entry, every retry gets the same entry back. Give it the device's own time in at, with its offset; the server takes it, rather than stamping the entry with whenever it arrived.
curl -X POST https://werzeick-dev.isc.hm/api/time/clock-in \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"gig_id": 7,
"at": "2026-10-19T06:58:00+02:00",
"source": "manual",
"client_event_id": "9f8c1a2b-3d4e-5f60-7182-93a4b5c6d7e8"
}'
source is manual or geofence. Nothing else goes in a clock event, and in particular no coordinate: the API has no field for a position, and never will. A worker's phone measures its distance to the one site it was given and sends only the event.
A worker can clock in only to a project they hold an accepted shift on. The entry links to that shift when their times overlap, and stands alone otherwise, which is normal and not an error. Hours are never trimmed to the shift.
curl -X POST https://werzeick-dev.isc.hm/api/time/clock-out \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"gig_id": 7, "at": "2026-10-19T15:32:00+02:00"}'
Clock out names the project, or the client_event_id of the clock-in when the phone was offline. The entries are then the worker's record:
curl "https://werzeick-dev.isc.hm/api/time/entries?since=2026-10-01T00:00:00Z" \
-H "Authorization: Bearer wzk_YOUR_KEY"
Each entry carries the rate it snapshotted when it was created and earned_amount, the finished entry's worth to the minute. Paying to the minute has a rounding rule, and the server applies it so that your figure and the payslip never differ by a koruna: format the number, never recompute it. An open entry has clock_out_at null and earns nothing yet.
curl https://werzeick-dev.isc.hm/api/gigs/7/earnings \
-H "Authorization: Bearer wzk_YOUR_KEY"
earned is from time entries, upcoming from accepted shifts ahead, each at its own snapshot; running is the shift on the clock right now and what it has come to so far, or null. Minutes with no hourly rate are counted beside the money, never dropped.
A worker corrects their own entry, and a manager anybody's, with POST /api/time/entries/{id}/correct. A null clock_out_at reopens an entry that was closed by mistake.
Running a project as a manager
A key made in a manager entry sees every live project at the company and can make one:
curl -X POST https://werzeick-dev.isc.hm/api/gigs \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Hala Ostrava, střecha",
"latitude": 49.8356,
"longitude": 18.2851,
"geofence_radius_m": 150
}'
The site is optional, and it is the site's position, never a person's. client_id names one of the company's clients, from the web client. A manager also invites people into the company, by email; an address that already has a Werzeick account gains a membership, one that does not becomes an account.
curl -X POST https://werzeick-dev.isc.hm/api/invitations \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "jana@example.cz",
"first_name": "Jana",
"last_name": "Horáková",
"is_worker": true,
"is_manager": false,
"rate_amount": 4200000,
"rate_currency": "CZK"
}'
The rate is optional, and it is the person's default at this company. A project's own pay rate wins over it when one is set, and every shift snapshots the figure it was offered at: changing either rate later moves nothing already promised.
A project's calendar is planned by laying shifts on days. One request lays the same shift on each day named, with its roles: each role has its own places and, where you set one, its own hourly rate. The clock times are on site, and a day not yet on the calendar is scheduled by the shift.
curl -X POST https://werzeick-dev.isc.hm/api/gigs/7/shifts \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"days": ["2026-11-02", "2026-11-03", "2026-11-04"],
"starts_at": "07:00",
"ends_at": "15:30",
"roles": [
{"label": "crane op", "headcount": 1, "rate_amount": 5200000, "rate_currency": "CZK"},
{"label": "labourer", "headcount": 2}
]
}'
Staffing is per role. An invitation names the person, from GET /api/people, and the role, from the shift's roles; it is pending until they accept, and it fills nothing before. GET /api/gigs/{id}/calendar shows the days with their shifts and counts; GET /api/timeslots/{id} one shift with everyone on it; GET /api/gigs/{id}/people everyone on the project's upcoming shifts. Several people are invited to several shifts in one request with POST /api/gigs/{id}/invitations/batch, which previews what it would do when asked.
curl -X POST https://werzeick-dev.isc.hm/api/timeslots/318/invitations \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"membership_id": 12, "role_id": 41}'
What needs attention is GET /api/staffing/short: every upcoming shift short of accepted people or waiting on an answer. An SOS on a short shift opens it to the company at a raised rate, 1.5× by default, and tells everyone who opted into emergency cover; POST /api/gigs/{id}/sos does it for every short shift of a project at once.
curl "https://werzeick-dev.isc.hm/api/staffing/short?days=3" \
-H "Authorization: Bearer wzk_YOUR_KEY"
curl -X POST https://werzeick-dev.isc.hm/api/timeslots/318/sos \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"offered_rate_multiplier": 15000}'
Removing days or a shift, taking somebody off a shift or off the whole project, and moving shifts to other dates each tell the people affected, so each asks first: without confirm nothing changes and the answer says what would. Send it again with confirm to go ahead.
curl -X POST https://werzeick-dev.isc.hm/api/gigs/7/days/remove \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"days": ["2026-11-04"]}'
{"confirmed": false, "days": 1, "people": 3, "cancelled": 0}
curl -X POST https://werzeick-dev.isc.hm/api/gigs/7/days/remove \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"days": ["2026-11-04"], "confirm": true}'
{"confirmed": true, "days": 1, "people": 0, "cancelled": 3}
The project's hours are GET /api/gigs/{id}/hours, every entry at the rate it was agreed at. Clients are GET and POST /api/clients. A project's pay rate, maximum rate and billing rate, payroll, invoicing and the reports stay in the web client and have no API.
An assistant: the MCP server
The same API is an MCP server at https://werzeick-dev.isc.hm/api/mcp, for an AI assistant that speaks the Model Context Protocol: Claude Code, Claude Desktop, Cursor and others. Every tool it offers is one endpoint above, called as your key; nothing the assistant can do is anything your key could not do with curl, and a refusal is the API's own.
claude mcp add --transport http werzeick https://werzeick-dev.isc.hm/api/mcp \
--header "Authorization: Bearer wzk_YOUR_KEY"
{
"mcpServers": {
"werzeick": {
"type": "http",
"url": "https://werzeick-dev.isc.hm/api/mcp",
"headers": {"Authorization": "Bearer wzk_YOUR_KEY"}
}
}
}
The tools are the manager's: the projects, a project's calendar and people, adding and moving shifts, inviting, approving, taking off, opening a shift, raising an SOS, the roster, the clients and a project's hours. Each tool that removes or moves work people have agreed to asks first, as the endpoint does: the assistant is told what would happen and calls again with confirm. The credential is the key, sent as a header, or a connector's token granted on a sign-in (the next section); the server speaks streamable HTTP, statelessly, and answers plain JSON.
Every tool takes company, the entry to act as, with the same values as the header; without it the tool acts as the key's own. list_projects and short_shifts take all too. So an assistant asked about every site you run can read across your companies, and acts in one of them per call.
A key made in a worker entry can connect, but the manager's tools refuse it unless a call names one of your manager entries. The worker's own endpoints — answering invitations, taking a shift, clocking in and out — are not offered as tools.
Connect from claude.ai
The claude.ai app, Claude Desktop, Claude's mobile apps and other assistants that take a custom connector cannot send a key: they send you through a sign-in instead, and act as you afterwards. Werzeick is an OAuth 2.1 authorization server for exactly that, so nothing is configured by hand on either side.
- In claude.ai, open Settings → Connectors → Add custom connector and paste this address:
https://werzeick-dev.isc.hm/api/mcp
- Press Connect. The assistant registers itself with Werzeick and opens Werzeick's sign-in in your browser.
- Sign in with your email and the code it sends, as you do on the web.
- On the consent screen, check the assistant's name and the address it will send you back to, choose the company and role it will act as, and press Allow.
- You are sent back to the assistant, which can now call the same tools a key opens, as that company and role.
A connector acts as the company and role you chose, unless a call names another of yours with company, like a key. It is listed on your account page under Connected assistants, with the program's name and when it was last used, and is revoked there like a key: revoking ends it at once, and the assistant asks you to connect again. Deleting your account ends every connector.
Under the hood it is the standard flow the Model Context Protocol specifies, and any client that implements it can use it: a 401 from the MCP endpoint names the resource metadata; the authorization server's metadata lists the endpoints; the client registers with dynamic client registration (RFC 7591); the authorization code is bound with PKCE (S256 only) and a resource of the MCP endpoint; the token endpoint issues an access token good for an hour and a refresh token, rotated on every use, good until the connector is revoked. Only https and loopback redirect addresses are accepted, and a consent is asked for every registration.
GET https://werzeick-dev.isc.hm/api/mcp
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://werzeick-dev.isc.hm/.well-known/oauth-protected-resource/api/mcp"
GET https://werzeick-dev.isc.hm/.well-known/oauth-protected-resource/api/mcp
GET https://werzeick-dev.isc.hm/.well-known/oauth-authorization-server
POST https://werzeick-dev.isc.hm/oauth/register
GET https://werzeick-dev.isc.hm/oauth/authorize?response_type=code&client_id=…&redirect_uri=…&code_challenge=…&code_challenge_method=S256&resource=https://werzeick-dev.isc.hm/api/mcp
POST https://werzeick-dev.isc.hm/oauth/token
Claude Code and clients that take a URL and a header can still use a key, as the previous section shows; the two credentials open exactly the same tools. Client ID metadata documents are not supported yet: a client that prefers them falls back to registering, as claude.ai does.
Tools and expenses
A worker keeps a kit list and takes tools to projects, so what goes in the van is on the project and not in their memory. The icon is a name from GET /api/tool-icons, which needs no key.
curl -X POST https://werzeick-dev.isc.hm/api/tools \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "SDS drill", "icon": "drill", "quantity": 1}'
curl -X POST https://werzeick-dev.isc.hm/api/gigs/7/tools \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"tool_id": 12}'
Attaching is idempotent: attaching twice answers 204 twice. Detaching leaves the tool on the list.
An expense is booked against one project by whoever bought the thing, worker or manager. A worker sees only the expenses they booked; a manager every one on the company's projects.
curl -X POST https://werzeick-dev.isc.hm/api/gigs/7/expenses \
-H "Authorization: Bearer wzk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"description": "Drill bits", "cost_amount": 1250000, "cost_currency": "CZK"}'
Keeping a key safe
- A key is a credential. Send it in the header only, never in a URL, and never put it in a page a browser loads: on a web page a key is no credential at all, so it cannot be spent there, but it can be read there.
- Make one key per integration, named for it, so that revoking one stops one thing.
- Look at “Last used” now and then. A key nobody has used for months is one to revoke.
- A key is the person's, like their session. When they leave the company, or lose the role, their keys stop with them.
- Only over HTTPS. The server does not answer plain HTTP.
Every endpoint, every field and every status code, generated from the server's own code so that it is always what this server does: