Skip to content

Customer API

The customer API gives your server read-only access to one employer’s establishments, public holidays, employees, agency workers, teams, tasks, published shifts and time entries. It uses OAuth 2.0 client credentials over HTTPS. The production origin is https://workforce-api.vesta.services.

Vesta activates API access per employer. An employer administrator opens Settings > Integrations > API access in the employer portal. While access is not active, that tab shows API access is not active and a Request API access button that emails Vesta.

  1. Sign in to the employer portal as an employer administrator.
  2. Open Settings > Integrations > API access and select Create integration.
  3. Enter a name and select only the permissions the software needs.
  4. Copy the client ID and client secret into your server’s secret store, then select I have stored the secret. The secret is shown only once.

Also copy the Employer ID from the same page: every data URL contains it.

  • An integration belongs to the employer and reads only that employer’s data.
  • Permissions are fixed when the integration is created. To change them, create a new integration and revoke the old one.
  • An employer can have up to 25 active integrations.
  • Keep the client secret and access tokens on your server. Never put them in browser code, mobile apps, URLs or logs.

Send POST /oauth/customer/token with a form-encoded body. Request one or more scopes, separated by spaces. Authenticate with HTTP Basic:

curl --request POST "https://workforce-api.vesta.services/oauth/customer/token" \
  --user "$VESTA_CLIENT_ID:$VESTA_CLIENT_SECRET" \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode 'scope=establishments:read planning:read'

Or send the credentials in the body (client_secret_post):

curl --request POST "https://workforce-api.vesta.services/oauth/customer/token" \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode "client_id=$VESTA_CLIENT_ID" \
  --data-urlencode "client_secret=$VESTA_CLIENT_SECRET" \
  --data-urlencode 'scope=establishments:read planning:read'
{ "access_token": "eyJhbGciOi...", "token_type": "Bearer", "expires_in": 300 }

A token is valid for 300 seconds. There is no refresh token: reuse the token for every call until shortly before it expires, then request a new one with the same credentials.

A refused token request returns an OAuth error body such as { "error": "invalid_scope" }:

Error Status When
invalid_request 400 A required parameter is missing or malformed, or credentials are sent both in the header and in the body.
invalid_client 401 Unknown client ID, wrong secret, or an integration that was revoked or whose secret was rotated.
unauthorized_client 400 The credentials are valid, but API access is no longer active for the employer.
invalid_scope 400 No scope, an unknown scope, a scope the integration was not given, or a scope whose module the employer no longer has.
unsupported_grant_type 400 grant_type is not client_credentials.

Do not retry invalid_client or unauthorized_client automatically: they need a change in the portal.

Every scope requires active API access. Some also require a product module.

Scope Portal permission Endpoints Also requires
establishments:read Read establishments /establishments, /public-holidays
employees:read Read employees /employees
agency-workers:read Read agency workers /agency-workers
planning:read Read planning /teams, /tasks, /shifts Planning module
time-entries:read Read time entries /time-entries Time registration module

All endpoints are GET requests under https://workforce-api.vesta.services/api/customer/v1/employers/{employerId} and need the header Authorization: Bearer <access_token>.

Endpoint Returns Query
/establishments Establishments with their IANA time zone limit, cursor
/public-holidays Public holidays of one year and the date the employer observes each, after any replacement year (2000 to 2100, required)
/employees Employees: name and active flag only limit, cursor
/agency-workers Workers of the agencies the employer works with limit, cursor
/teams Teams limit, cursor
/tasks Planning tasks limit, cursor
/shifts Published shifts from, to, limit, cursor
/time-entries Time entries from, to, limit, cursor
curl "https://workforce-api.vesta.services/api/customer/v1/employers/$VESTA_EMPLOYER_ID/shifts?from=2026-10-01&to=2026-10-31" \
  --header "Authorization: Bearer $VESTA_ACCESS_TOKEN"

A token only works on its own employer’s URLs. The API reference lists every field.

  • from and to are required YYYY-MM-DD dates between 2000 and 2100. Both are included. A range covers at most 93 days.
  • Shifts are selected by their planned date. Time entries are selected by the local day they started at their own establishment, the day workDate reports.
  • limit is 1 to 200, default 100. A list returns { "items": [...], "nextCursor": "..." }. Send nextCursor as cursor, with the same other parameters, until nextCursor is null. A cursor is opaque and only works on the list, employer and date range that returned it.
  • Paging is not a snapshot. A record that changes while you page can be missed or returned twice; the next full read corrects it.

There is no change feed and no modifiedSince filter. To stay in sync:

  1. Re-read the date window you keep, for example the past two weeks and the coming four weeks of shifts, and page until nextCursor is null.
  2. Upsert every record by id.
  3. Only after the last page, treat any stored id in that window that was not returned as deleted. A shift that disappears was deleted or unpublished.

Reference lists are small: re-read them in full the same way. Archived employees drop out of /employees; use isActive for everything else.

  • Every id is a UUID that stays the same across reads, including time entry ids.
  • startsAt, endsAt and break times are local times with the establishment’s UTC offset, for example 2026-10-03T22:00:00+02:00. Fields ending in Utc are UTC.
  • Shifts are published shifts only. date is the planned day. A shift that runs past midnight ends on the next day. plannedMinutes is the time that actually passes between startsAt and endsAt, minus breakMinutes: a night shift across a daylight saving change counts one hour more or less than its clock times suggest. fillType is Internal for an employee or External for an agency or external worker.
  • Time entry status is Working, OnBreak, Completed or Cancelled. endsAt is empty while an entry is open, and workedMinutes is present only when it is Completed. shiftId links the published shift the entry was worked on, when there is one. reviewedAtUtc is set once the employer has reviewed the entry. Cancelled entries are kept for history and do not count as worked time.
  • Shifts and time entries at a deleted establishment are left out. A reference to a deleted team, task, agency or agency worker is null, just as that record is gone from its own list. An archived employee is the exception: shifts and time entries keep their employeeId.
  • Employees contain firstName, lastName and isActive only: no national number, bank account, contact details or birth date.
Requests Limit Counted per
Token requests 30 per 60 seconds IP address and client ID. With client_secret_post, all token requests from one IP address share one budget.
Data requests 600 per 60 seconds Employer, shared by all its integrations

Vesta’s general limit of 500 requests per minute per IP address also applies. Over a limit, the API answers 429 with a Retry-After header in seconds: wait at least that long. Reusing tokens keeps you well within the token budget.

Data endpoints answer errors as problem details (application/problem+json).

Status Meaning What to do
400 A parameter is missing or malformed, such as from, to, year or limit. Fix the request.
401 The token is missing, invalid or expired, its integration was revoked or rotated, the employer lost API access, or none of the token’s scopes is still available. Request a new token once. If that is refused, stop.
403 The token is for another employer, lacks the scope, or the employer lost the module that scope reads. Check the employer ID and scopes.
422 to is before from, a date is outside 2000 to 2100, the range exceeds 93 days, or the cursor is invalid or belongs to another list, employer or date range. Fix the request.
429 A rate limit was reached. Wait for Retry-After.
5xx Vesta could not answer. Retry with increasing, jittered delays.

On API access, each integration’s menu offers Rotate secret and Revoke access. Last used shows when the integration last received a token.

  • Rotate secret shows a new secret once. The old secret and every token issued with it stop working immediately, so update your server straight away.
  • Revoke access permanently disables the integration and all its tokens. You can revoke even after API access has ended.
  • Support sessions, in which Vesta staff act in your portal, cannot create integrations or rotate secrets. They can revoke.