# Sona Client API — Overview Version 1.14 The Sona Client API provides programmatic access to manage workforce operations including users, shifts, absences, employment periods, and organisational structures. ## Key Concepts ### Authentication All requests require a Bearer token in the `Authorization` header. Tokens are scoped to an organisation and issued by the Sona team during integration setup — there is no self-service token endpoint. Contact your Sona representative if you need a new token or if your token has been revoked. ### API Token Permissions & 403 Errors API tokens can be configured with an allowed list of endpoints. If your token is restricted and you attempt to access an endpoint not in the allowed list, the API will return a `403 Forbidden` response with the message: *"This API credential does not have access to this endpoint"*. Contact Sona to have the required endpoint(s) added to your token's permissions. A token with no explicit endpoint restrictions has access to all endpoints. ### Identifiers: Sona IDs vs External IDs (`idx`) Every resource has a Sona-generated UUID (`id`). Most resources also accept an **external identifier** (`idx` or `external_id`) that you control. This lets you reference resources using your own system's IDs without storing Sona's UUIDs. Where an endpoint accepts `{user_id_or_idx}` in the path, you can pass either. For org unit references in request bodies, pass `{ "id": "" }` or `{ "idx": "" }`. We strongly recommend setting `external_id` / `idx` on every resource you create — it simplifies lookups and avoids the need to maintain an ID mapping table. ### Pagination List endpoints return paginated results. Use `page` (1-indexed, not `page_number`) and `page_size` query parameters. The default page size is 10. Responses include `page_number`, `page_size`, `total_pages`, and `total_entries` alongside the `entries` array. ### Errors The API returns two error shapes. **Non-validation errors** (400, 401, 403, 404, 409, 500) return `{ "error": "Not Found", "message": "..." }`. **Validation errors** (422) on create/update usually return `{ "errors": { "field_name": ["error message", ...] } }` with per-field error arrays, but some 422s (a request that is well-formed but not allowed, or invalid query parameters) use the `{ "error", "message" }` shape instead. Each endpoint's 422 response names the schema it returns; when handling a 422 generically, check for `errors` first and fall back to `message`. The metrics endpoints are an exception: their errors are `{ "error": "..." }` with no `message`. Every endpoint can return `401` (missing or invalid token) and `403` (the token is restricted to other endpoints) before the request is processed. ### Date and Time Formats The API uses **ISO 8601** throughout: - **Datetimes** are UTC with format `YYYY-MM-DDTHH:mm:ssZ` — e.g. `2025-03-15T14:30:00Z`. Always include the trailing `Z` (UTC timezone designator). Datetimes without a timezone are rejected. - **Dates** use format `YYYY-MM-DD` — e.g. `2025-03-15`. Used for fields like `start_date`, `finish_date`, and `final_date_of_employment`. All timestamps returned by the API (`inserted_at`, `updated_at`, shift times, etc.) are in UTC. They end in `Z`, with one exception: `inserted_at` and `updated_at` on org attributes and org unit attributes are UTC but have no designator (e.g. `2025-03-15T14:30:00`). Treat a datetime without a designator as UTC. ### Incremental Sync If you are building an ongoing sync rather than a one-off import, use the `updated_at` query parameter on list endpoints to fetch only records that have changed since your last sync. Currently supported on: - `GET /api/v1/users?updated_at=2025-03-15T10:00:00Z` - `GET /api/v1/employment_periods?updated_at=2025-03-15T10:00:00Z` - `GET /api/v1/services?updated_at=2025-03-15T10:00:00Z` (a service counts as changed when the service itself, its config or one of its demand periods was updated, or when anything was added to its audit log, which covers removed demand periods) - `GET /api/v1/people_we_support?updated_at=2025-03-15T10:00:00Z` (the person's own record; status and care package changes do not count) - `GET /api/v1/care_packages?updated_at=2025-03-15T10:00:00Z` (also on `GET /api/v1/people_we_support/{id}/care_packages`). A package counts as updated when it changes, including being ended by a newer package, or when any of its hour lines gets a new approval status. This is the way to sync approvals: each package comes back with its lines' current statuses, so you don't need a call per hour line. - `GET /api/v1/status_records?updated_at=2025-03-15T10:00:00Z` (also on `GET /api/v1/people_we_support/{id}/status_records`). Every person's active/inactive history; a record counts as updated when it is created or changed, including being ended by a newer record. This returns all records updated on or after the given timestamp. Store the timestamp of each successful sync and pass it on the next run to avoid re-processing unchanged records. Combine with pagination to handle large result sets. ### Reference Data Several resources act as configuration that must exist before you can create users or employment periods. These are typically set up by the Sona team during onboarding, but can be retrieved via the API: | Resource | Endpoint | Used by | |----------|----------|---------| | Org Units | `GET /api/v1/org_units` | Users, Employment Periods | | Levels | `GET /api/v1/levels` | Employment Periods | | Contract Types | `GET /api/v1/contract_types` | Employment Periods (via Employment Specs) | | Jobs | `GET /api/v1/jobs` | Employment Periods | | Permission Roles | `GET /api/v1/permission_roles` | Employment Periods | | Shift Types | `GET /api/v1/shift_types` | Shifts | | Absence Types | `GET /api/v1/absence_types` | Absences | | Termination Reasons | `GET /api/v1/termination_reasons` | Employment Periods (termination) | | Metrics | `GET /api/v1/metrics` | Metric Values | Fetch these first to obtain the IDs you will need when creating users and employment periods. ### User lookup `GET /api/v1/users/lookup` resolves **one** user from **one** identity parameter (`insurance_number`, `employee_number`, `external_id`, `contact_email`, or `email`). The response includes that user, their employment periods (up to 200), and which parameter was used (`lookup.by`). See **Responses** on the endpoint for status codes. --- ## Common Workflows ### Creating a User with an Employment Period A **User** represents a person in the system. An **Employment Period** represents a specific period of employment for that user (start date, contract type, org unit, etc.). Most integrations create both together. **Prerequisites:** Before creating a user you will need an org unit ID (from `GET /api/v1/org_units`). If creating an employment period, you will also need IDs for the level, contract type, and optionally the job — see the Reference Data table above. **Option A — Single request with `create_employment_period`** Set `create_employment_period: true` on `POST /api/v1/users` to create the user and a minimal employment period in one call. You must also supply `administrative_org_unit`. Optionally pass `employment_period_params` to configure the employment period (start date, employee number, level, etc.). **Important:** This option places the user into the Sona **onboarding flow**. Your Sona instance must have onboarding configured for this to work correctly. If your organisation does not use Sona's onboarding feature, use Option B instead. **Option B — Two separate requests** 1. `POST /api/v1/users` — create the user (returns a `user_id`). 2. `POST /api/v1/employment_periods` — create the employment period, referencing the `user_id` from step 1. This requires `user_id`, `start_date`, and an `employment_specs` array containing at least one entry with `contract_types`, `rates_of_pay`, and `contracted_hours`. Use Option B when you need full control over the employment period configuration or when your Sona instance does not have onboarding enabled. **Updating a user** `PUT /api/v1/users/{user_id}` updates user fields. Only the fields you include in the request body are changed — omitted fields are left untouched. **Granting manager access** To make a user a manager, grant access to specific org units via `manager_org_units` (full read/write) or `read_only_org_units` (read only). A user also needs a **base role** on `user.roles` to be able to log in to the manager desktop — if you grant manager access without passing `base_roles`, Sona will automatically assign `manager` (or `read_only_manager` if only read-only access is granted) so the user can log in. Existing roles are never overwritten. Pass `base_roles` explicitly to override the default or to assign a more specific role (e.g. `hr_manager`, `payroll_manager`). --- ### Terminating and Undoing Termination **Prerequisites:** Termination requires the **leaver workflow** to be enabled on your Sona instance. If it is not enabled, the terminate endpoint will return a 422 error. Contact your Sona representative to enable it. If your organisation uses termination reasons, retrieve available reasons from your Sona configuration beforehand. **Terminating a user** `PUT /api/v1/users/{user_id_or_idx}/terminate` ends a user's employment. You can identify the user by their Sona ID or external ID. The only required field is `final_date_of_employment`; you can optionally provide a termination reason and details. See the endpoint documentation for accepted parameters. After termination the user's `state` transitions through: `active` → `leaver` (between the request and the final date) → `left` (after the final date). Shifts beyond the final date are automatically removed. **Undoing a termination** If a termination was made in error, reverse it with `PUT /api/v1/users/{user_id_or_idx}/undo_termination`. No request body is required — the user's state is restored to `active` and the final date of employment is cleared. --- ### Rehiring a former employee **Prerequisites:** A **completed** previous employment period with a **finish date** must already exist in your organisation. You need **non-continuous employment periods** enabled if the **same employee number** has a **gap** between the old period’s end and the new period’s start. Otherwise those periods must be **continuous** (back-to-back). Contact Sona to confirm this setting if you see validation errors about continuity or about starting a new period after a termination. A **rehire** is a **new** employment period for someone whose **previous** period **ended** with a **finish date**, and who is **no longer active** with **`final_date_of_employment`** set and state **`left`**. You use a rehire when the person returns after a real exit. **`undo_termination`** corrects a mistaken termination while the employee is still a **leaver**, but **has not left** yet. **Do not** use **`undo_termination`** for a genuine return after employment has ended. **Do** create a new employment period. **`termination_would_rehire`:** On the **previous** employment period, if this has been set to **`false`**, it means the person is **not suitable for rehire** (do-not-rehire). The Client API does **not** reject a new `POST /api/v1/employment_periods` when this value is **`false`**. You must enforce do-not-rehire in your integration if your policy requires it. **Creating the new period** Call `POST /api/v1/employment_periods` for an **existing** user. Sona **derives** **`is_rehire`** on the new period unless you set it yourself. It is **true** only when **all** of the following hold: at least one **other** employment period is **finished**; **`final_date_of_employment`** is set; the user is **`leaver`** or **`left`** (see **Terminating a user** above); and the **new** period’s **`finish_date`** is **unset** or **strictly after** today. If **`finish_date`** on the new period is **today** or **earlier**, **`is_rehire`** is not derived as true, so rehire cleanup does not run—**`final_date_of_employment`** stays set and the user is not reactivated. **What happens when `is_rehire` is true after create** **`forces_new_holiday_breakdown`** defaults to **`true`** unless you set it (see the employment period parameters in this spec). Sona clears **`final_date_of_employment`**. Sona sets the user’s state to **`active`**. **Payroll note:** A termination and rehire **within the same pay period** can require special handling in Sona (for example pay or average-pay splits). If you use integrated payroll and hit edge cases, work with Sona to align behaviour. --- ### Custom Forms and Entries **Custom Forms** define the structure (fields, validation rules) for collecting additional data about users — for example employee documents, onboarding checklists, or qualifications. **Custom Form Entries** are submitted instances of those forms. **Prerequisites:** Custom forms are configured by the Sona team for your organisation. The form structure — including field names, accepted values, required fields, and validation rules — is defined during this setup and cannot be changed via the API. Contact your Sona representative if you need a new form or changes to an existing one. **Step 1 — Discover available forms** `GET /api/v1/custom_forms` returns all custom forms configured for your organisation. Each form has a unique `id` and describes the fields an entry must include. Use this to determine the `custom_form_id` and the exact field names you need to supply when creating entries. **Step 2 — Create an entry** `POST /api/v1/custom_form_entries` with `custom_form_id`, `user_id`, and the field values defined by the form. The required and optional fields vary per form — refer to the form definition from Step 1. File fields can be uploaded via multipart form-data or as base64-encoded JSON (see the endpoint documentation for upload formats and size limits). **Step 3 — View entries** `GET /api/v1/custom_form_entries` lists all entries (paginated). Use `GET /api/v1/custom_form_entries/{id}` to retrieve a single entry. --- ## Webhooks Sona supports real-time webhook notifications for 25 event types covering users, employment, absences, shifts, amendments, jobs, and agency shifts. **[View the full Webhooks Guide](webhooks/)** for delivery format, HMAC-SHA256 signature verification, all event types, and payload schemas. To manage subscriptions, see the **Webhook Subscriptions** endpoints in the sidebar. ## Error response schemas Endpoint pages name these schemas in their error responses. ### `ErrorResponse` - `error` (string, required): HTTP status phrase (e.g. "Not Found", "Unprocessable Entity") - `message` (string, required): Human-readable error detail ### `ErrorOrValidationErrors` - `error` (string): HTTP status phrase - `errors` (object): Field names mapped to lists of error messages - `message` (object): A string, a list of strings, or an object mapping field names to an error message or a list of messages ### `ValidationErrors` - `errors` (object, required): Object where keys are field names and values are arrays of error messages ### `DetailedErrorResponse` - `error` (string, required): HTTP status phrase (e.g. "Unprocessable Entity") - `message` (object, required): A string, a list of strings, or an object mapping field names to an error message or a list of messages ### `MetricsErrorResponse` - `error` (string, required): What went wrong ### `MetricsForbiddenResponse` - `error` (string, required) - `message` (string) --- # Absences Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of absences `GET /api/v1/absences` List all absences for the current organisation. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `user_id` | query | string | no | Filter by user id. If omitted all absences are returned. | | `user_idx` | query | string | no | Filter by external user id. If omitted all absences are returned. | | `absence_type_id` | query | string | no | Filter by absence type id. If omitted all absences are returned. | | `order_by` | query | string | no | order by starts at DESC if provided starts_at_desc | | `include_payroll_run_rows` | query | boolean | no | When true, each linked/unlinked shift will include a payroll_run_rows array with the raw payroll run row data (start_datetime, end_datetime, duration_in_minutes, entry_type, pay_type, variant). Use this when you need the payroll-authoritative datetimes rather than the scheduled shift times. | ### Responses **200** Success - `entries` (array of `ShowAbsenceResponse`) - `absence_subtype_id` (string): Sona unique identifier of the absence_subtype for this absence - `absence_type_id` (string, required): Sona unique identifier of the absence_type for this absence - `current_employee_number` (string, required): Identifier of the current employee for this absence - `days` (array of object): List of days with sick/not sick status - `date` (string): The date - `status` (string; one of `sick`, `not_sick`): Date status - `documents` (array of object): List of documents uploaded for this absence - `document_type` (string): Legacy document type enum (e.g. doctor_fit, self_certificate, other, return_to_work). Stable for all historically uploaded documents. For types with no legacy equivalent, contains the UUID. Prefer document_type_id for new integrations. - `document_type_id` (string): UUID of the absence_document_type config row, or null for un-migrated legacy values - `document_type_label` (string): Human-readable name of the document type - `document_url` (string): A short-lived URL to download the document - `id` (string): Unique identifier of the document - `ends_at` (string, required): UTC Datetime when this absence period ends in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `finishes_on_local_date` (string): ends_at converted to UK local time (Europe/London), as an ISO 8601 datetime with a UTC offset rather than Z. An end at local midnight is moved back one second so it falls on the last day of the absence, e.g. 2019-07-20T23:59:59+01:00 - `id` (string, required): Sona unique identifier - `idx` (string): An external ID for this absence period - `is_open_ended` (boolean): If the end date of the absence period is confirmed this should be false. - `is_paid` (boolean): Whether this absence is paid. Derived from the absence type's payroll_type. false for unpaid_leave, true for other payroll types, null when payroll_type is not set. - `linked_shifts` (array of `AbsenceShifts`, required): A collection of shift ids, start and end datetime that are contained within an absence period - `end_datetime` (string, required): The end datetime of the shift - `id` (string, required): The Sona unique identifier of the shift - `is_shift_run` (boolean, required): Whether this shift is a shift run parent - `is_travel_gap` (boolean, required): Whether this shift is a travel gap between shifts in a shift run - `payroll_run_rows` (array of `PayrollRunRow`): Raw payroll run rows for this shift, present when include_payroll_run_rows=true - `duration_in_minutes` (integer): Duration in minutes - `end_datetime` (string, required): Payroll row end datetime - `entry_type` (string): Entry type - `id` (string, required): Payroll run row id - `pay_type` (string): Pay type - `start_datetime` (string, required): Payroll row start datetime - `variant` (string): Pay variant - `shift_break` (object) - `duration` (integer): The duration of the break, in minutes - `shift_run_id` (string): The ID of the parent shift run, if this shift is part of a shift run - `start_datetime` (string, required): The start datetime of the shift - `payroll_type` (string): The payroll classification of this absence's type. One of: sickness_ssp, unpaid_leave, holiday, working, sickness_csp, or null. - `sick_note_provided` (boolean): Whether a sick note has been provided for this absence. The SSP sick note policy reads it: when an absence is long enough to need a note, false limits SSP to the days that do not need one. null when it was never recorded. - `starts_at` (string, required): UTC Datetime when this absence period starts in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `starts_on_local_date` (string): starts_at converted to UK local time (Europe/London), as an ISO 8601 datetime with a UTC offset rather than Z, e.g. 2019-07-20T08:47:58+01:00 - `user_id` (string, required): Sona unique identifier of the user for this absence - `user_internal_id` (string, required): Internal identifier of the user for this absence - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** `user_id` or `absence_type_id` is not a valid UUID, or `order_by` is not a supported value — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/absences" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a new absence `POST /api/v1/absences` ### Request body The absence parameters - `absence_subtype_id` (string): Sona unique identifier of the absence_subtype for this absence - `absence_type_id` (string, required): Sona unique identifier of the absence_type for this absence - `create_replacement_shifts` (boolean): If open shifts should be created for the shifts contained within an absence period. - `ends_at` (string, required): The end date time of the absence in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`). If the absence is open-ended then this is the expected end date. - `idx` (string): An external ID for this absence period - `is_open_ended` (boolean): If the end date of the absence period is confirmed this should be false. - `sick_note_provided` (boolean): Whether a sick note has been provided. Optional; defaults to false when left out. Must be true or false; the strings "true", "false", "1" and "0" are also accepted, null is not. true is only allowed when the absence type is an SSP absence type. The SSP sick note policy reads it: when an absence is long enough to need a note, false limits SSP to the days that do not need one. - `starts_at` (string, required): The start date time of the absence in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `user_id` (string, required): Sona unique identifier of the user this absence will be assigned to ### Responses **200** Success, absence created - `absence_subtype_id` (string): Sona unique identifier of the absence_subtype for this absence - `absence_type_id` (string, required): Sona unique identifier of the absence_type for this absence - `days` (array of object): List of days with sick/not sick status - `date` (string): The date - `status` (string; one of `sick`, `not_sick`): Date status - `ends_at` (string, required): UTC Datetime when this absence period ends in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `id` (string, required): Sona unique identifier - `idx` (string): An external ID for this absence period - `is_open_ended` (boolean): If the end date of the absence period is confirmed this should be false. - `linked_shifts` (array of `AbsenceShifts`, required): A collection of shift ids, start and end datetime that are contained within an absence period - Same fields as `AbsenceShifts` above. - `sick_note_provided` (boolean): Whether a sick note has been provided for this absence. The SSP sick note policy reads it: when an absence is long enough to need a note, false limits SSP to the days that do not need one. null when it was never recorded. - `starts_at` (string, required): UTC Datetime when this absence period starts in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `user_id` (string, required): Sona unique identifier of the user for this absence **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unable to create an absence with these parameters, e.g. the absence type is managed in Sona rather than externally, or the end date is before a shift approved for payroll. Usually an `ErrorResponse`; field-level validation failures return `ValidationErrors` instead — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/absences" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "absence_subtype_id": "dbad14a0-e789-4d85-a5e2-1c8a4f1918ed", "absence_type_id": "9f8bda89-002a-45e0-8660-46e2fff714d2", "create_replacement_shifts": false, "ends_at": "2019-04-20T15:47:58+00:00", "idx": "some-external-id", "is_open_ended": false, "sick_note_provided": false, "starts_at": "2019-04-20T07:47:58+00:00", "user_id": "9f8bda89-002a-45e0-8660-46e2fff714d2" }' ``` ## Show an absence `GET /api/v1/absences/{absence_period_id_or_idx}` Show an absence by id or idx ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `absence_period_id_or_idx` | path | string | yes | The Sona unique identifier or external id of the absence | | `include_payroll_run_rows` | query | boolean | no | When true, each linked/unlinked shift will include a payroll_run_rows array with the raw payroll run row data (start_datetime, end_datetime, duration_in_minutes, entry_type, pay_type, variant). Use this when you need the payroll-authoritative datetimes rather than the scheduled shift times. | ### Responses **200** Success Same fields as `ShowAbsenceResponse` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/absences/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update an absence `PUT /api/v1/absences/{absence_period_id_or_idx}` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `absence_period_id_or_idx` | path | string | yes | The Sona unique identifier or external id of the absence | ### Request body The absence parameters - `create_replacement_shifts` (boolean): If open shifts should be created for the shifts that will be linked to the absence period. - `ends_at` (string): The end date time of the absence in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`). If the absence is open-ended then this is the expected end date. - `is_open_ended` (boolean): If the end date of the absence period is confirmed this should be false. - `sick_note_provided` (boolean): Whether a sick note has been provided. Optional; when left out the stored value is kept. Must be true or false; the strings "true", "false", "1" and "0" are also accepted, null is not. true is only allowed when the absence type is an SSP absence type. The SSP sick note policy reads it: when an absence is long enough to need a note, false limits SSP to the days that do not need one. - `starts_at` (string): The start date time of the absence in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`). ### Responses **200** Success, absence updated - `absence_subtype_id` (string): Sona unique identifier of the absence_subtype for this absence - `absence_type_id` (string, required): Sona unique identifier of the absence_type for this absence - `days` (array of object): List of days with sick/not sick status - `date` (string): The date - `status` (string; one of `sick`, `not_sick`): Date status - `ends_at` (string, required): UTC Datetime when this absence period ends in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `id` (string, required): Sona unique identifier - `idx` (string): An external ID for this absence period - `is_open_ended` (boolean): If the end date of the absence period is confirmed this should be false. - `linked_shifts` (array of `AbsenceShifts`, required): A collection of shift ids, start and end datetime that are contained within an absence period - Same fields as `AbsenceShifts` above. - `sick_note_provided` (boolean): Whether a sick note has been provided for this absence. The SSP sick note policy reads it: when an absence is long enough to need a note, false limits SSP to the days that do not need one. null when it was never recorded. - `starts_at` (string, required): UTC Datetime when this absence period starts in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `unlinked_shifts` (array of `AbsenceShifts`, required): A collection of shift ids, start and end datetime that were unlinked from the absence period - Same fields as `AbsenceShifts` above. - `user_id` (string, required): Sona unique identifier of the user for this absence **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** The absence period or its user could not be found — `ErrorResponse` (see the overview) **422** Unable to update the absence. Rejections such as an absence type managed in Sona rather than externally, or a new end date before a shift approved for payroll, return `{error, message}`; field-level validation failures return `{errors}` — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/absences/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "create_replacement_shifts": false, "ends_at": "2019-04-20T15:47:58+00:00", "is_open_ended": false, "sick_note_provided": false, "starts_at": "2019-04-19T15:47:58+00:00" }' ``` ## Delete an absence `DELETE /api/v1/absences/{absence_period_id_or_idx}` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `absence_period_id_or_idx` | path | string | yes | The Sona unique identifier or external id of the absence | ### Request body Optional parameters for deletion behavior - `delete_associated_shifts` (boolean): If true, also deletes shifts associated with this absence period. ### Responses **200** Success, absence deleted - `id` (string, required): Sona unique identifier - `idx` (string): An external ID for this absence period **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Absence not found — `ErrorResponse` (see the overview) **409** SSP is being recalculated for this employee; retry shortly — `ErrorResponse` (see the overview) **422** The absence period has been approved for payroll, or one or more of its shifts are locked for payroll — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/absences/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "delete_associated_shifts": false }' ``` ## Amend an absence period `POST /api/v1/absences/{absence_period_id_or_idx}/amend` Amends an absence period ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `absence_period_id_or_idx` | path | string | yes | The Sona unique identifier or external id of the absence | ### Request body The amendment absence parameters - `create_replacement_shifts` (boolean): If open shifts should be created for the shifts contained within an absence period. - `ends_at` (string): The end date time of the absence in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`). If the absence is open-ended then this is the expected end date. - `is_open_ended` (boolean): If the end date of the absence period is confirmed this should be false. - `payrolled_shifts_options` (object): Actions to take for payrolled shifts that overlap with the amended absence period - `during_absence_period` (object): Action to take for payrolled shifts that will fall inside of the absence period after the amendment - `action` (string; one of `cancel`, `link`, `unlink`, required): The action to apply to payrolled shifts that happen during the absence period - `pay_variant` (string): The pay variant id to use for the shifts. Required when action is link or unlink. - `outside_absence_period` (object): Action to take for payrolled shifts that will fall outside of the absence period after the amendment - `action` (string; one of `cancel`, `link`, `unlink`): The action to apply to payrolled shifts that happen outside the absence period - `pay_variant` (string): The pay variant id to use for the shifts. Required when action is link or unlink. - `starts_at` (string): The start date time of the absence in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`). ### Responses **200** Success: absence period amendment - `absence` (`AmendedAbsence`, required): The amended absence period - `absence_subtype_id` (string): Absence subtype ID - `absence_type_id` (string, required): Absence type ID - `ends_at` (string, required): The end date time of the absence in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`). If the absence is open-ended then this is the expected end date. - `id` (string, required): Absence ID - `is_open_ended` (boolean, required): If the end date of the absence period is confirmed this should be false. - `starts_at` (string, required): The start date time of the absence in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`). - `shifts` (object, required): The amended absence period - `cancelled` (array of `AmendedShift`, required): List of cancelled shifts - `absence_period_id` (string): Linked absence period ID - `cancelled_at` (string): The date time the shifts was cancelled in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`). - `end_datetime` (string, required): The end date time of the shift in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`). - `id` (string, required): Shift ID - `org_unit_id` (string, required): Organisational unit ID - `start_datetime` (string, required): The start date time of the shift in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`). - `user_id` (string, required): User ID - `updated` (array of `AmendedShift`, required): List of updated shifts - Same fields as `AmendedShift` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Absence period not found — `ErrorResponse` (see the overview) **422** The amendment was rejected, e.g. the associated shifts have changed or ends_at is before a payrolled shift. Invalid request fields return `ValidationErrors` instead — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/absences//amend" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "ends_at": "2025-01-13T00:00:00Z", "is_open_ended": false, "payrolled_shifts_options": { "during_absence_period": { "action": "link", "pay_variant": "Sickness" }, "outside_absence_period": { "action": "unlink", "pay_variant": "Sickness" } }, "starts_at": "2025-01-12T00:00:00Z" }' ``` --- # AbsenceSubtypes Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of Absence Subtypes for a given Absence Type `GET /api/v1/absence_types/{absence_type_id}/absence_subtypes` AbsenceSubtype ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `absence_type_id` | path | string | yes | Absence Type ID (UUID) | ### Responses **200** Success - `entries` (array of `AbsenceSubtype`) - `id` (string, required): Sona unique identifier - `name` (string, required): The name of the absence_subtype - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **400** Bad Request — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/absence_types//absence_subtypes" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get an individual Absence Subtype `GET /api/v1/absence_types/{absence_type_id}/absence_subtypes/{id}` Get an individual AbsenceSubtype. See the Absence Subtype listing endpoint for a description of Absence Subtypes ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `absence_type_id` | path | string | yes | Absence Type ID (UUID) | | `id` | path | string | yes | Absence Subtype ID (UUID) | ### Responses **200** Success Same fields as `AbsenceSubtype` above. **400** Bad Request — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/absence_types//absence_subtypes/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # AbsenceTypes Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of AbsenceTypes `GET /api/v1/absence_types` AbsenceType ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `AbsenceType`) - `id` (string, required): Sona unique identifier - `is_holiday` (boolean, required): If this is a holiday absence or not - `is_sickness` (boolean): If this is a sickness absence or not - `name` (string, required): The name of the absence_type - `payroll_type` (string): The payroll type of the absence_type - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/absence_types" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get an individual absence_type `GET /api/v1/absence_types/{absence_type_id}` Get an individual absence_type. See the absence_type listing endpoint for a description of absence_types ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `absence_type_id` | path | string | yes | Sona unique identifier of the absence_type | ### Responses **200** Success Same fields as `AbsenceType` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/absence_types/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # AbsenceYears Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Create a new absence year `POST /api/v1/absence_years` ### Request body The absence year parameters - `contract_id` (string): The Sona unique identifier of the contract the request is about, from `contract_id` on `GET /api/v1/employment_periods`. Only used by organisations with overlapping employment periods enabled. Required when the user holds employment periods on several contracts covering the requested date; without it the only contract covering that date is used. A contract whose employment periods have ended still reads its own absence year covering the date. - `start_date` (string, date, required): The date from when the absence year starts - `user_id` (string, required): The Sona unique identifier of the user ### Responses **201** Created - `accrued` (number, float): Holiday accrued amount - `contract_id` (string): The unique Sona identifier of the contract of the absence year, or null for an absence year without a contract - `denomination` (string; one of `hours`, `days`): Absence year denomination - `end_date` (string, date): Absence year end date - `holiday_calculation_type` (string; one of `none`, `days_per_week`, `days_per_week_flexible`, `irregular_and_part_year`, `twelve_point_zero_seven`, `days_per_week_with_overtime_accrual`): Holiday calculation type - `id` (string): The unique identifier of the absence year entry - `start_date` (string, date): Absence year start date - `user_id` (string): The unique identifier of the user - `yearly_allowance` (number, float): Holiday allowance **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity. A changeset error, such as an overlapping absence year, returns `errors`; a refused `contract_id` or a start date without employment returns `error` and `message`. — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/absence_years" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "contract_id": "4f5c2a36-8f6e-4a0a-9d8e-2b1c7f3e6a51", "start_date": "2024-07-23", "user_id": "550e8400-e29b-41d4-a716-446655440000" }' ``` --- # Agencies Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of Agencies `GET /api/v1/agencies` An agency in Sona is a third party business used to provide staff who can be assigned to shifts. Agencies can be created via the Sona UI or dynamically when using the shifts#assign_to_agency Endpoint. An agency worker is simply a user with a link to an agency, these are returned by the users#index endpoint with their `agency_id` set to the value of an agency. Currently a user can only be associated with one agency. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `Agency`) - `id` (string, required): Sona unique identifier - `name` (string, required): The name of the agency - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/agencies" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get an individual agency `GET /api/v1/agencies/{agency_id}` Returns information about a single agency. See the agency listing endpoint for a description Agencies. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `agency_id` | path | string | yes | Sona unique identifier of the agency | ### Responses **200** Success Same fields as `Agency` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/agencies/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Gets agencies linked to the organisational unit `GET /api/v1/org_units/{org_unit_id}/agencies` Returns a paginated list of agencies linked to the organisational unit. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the organisational unit | ### Responses **200** Success - `entries` (array of `OrgUnitAgency`) - `id` (string, required): Sona agency unique identifier - `name` (string, required): The name of the agency - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/org_units//agencies" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Links an agency to the organisational unit `POST /api/v1/org_units/{org_unit_id}/agencies/{agency_id}/link` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the organisational unit | | `agency_id` | path | string | yes | Sona unique identifier of the agency | ### Responses **204** Success **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/org_units//agencies//link" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Unlinks an agency to the organisational unit `DELETE /api/v1/org_units/{org_unit_id}/agencies/{agency_id}/link` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the organisational unit | | `agency_id` | path | string | yes | Sona unique identifier of the agency | ### Responses **204** Success **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/org_units//agencies//link" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # AnnualLeaveRequests Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of annual leave requests `GET /api/v1/annual_leave_requests` Returns a list of annual leave requests sorted by creation date. Allows filtering by `user_id` and start and end dates. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `user_id` | query | string | no | The user ID to filter by | | `min_start_date` | query | string | no | Returns annual leave requests starting on or after the given date | | `max_end_date` | query | string | no | Returns annual leave requests ending on or before the given date | | `idx` | query | string | no | The external ID to filter by | | `source_type` | query | string | no | The source type of the external ID to filter by | ### Responses **200** Success - `entries` (array of `AnnualLeaveRequest`) - `actual_duration` (number): The actual duration of the annual leave - `contract_id` (string): The unique Sona identifier of the contract of the request's absence year, or null for an absence year without a contract - `denomination` (string; one of `days`, `hours`): The unit in which the duration is given - `duration` (number): The total duration of the requested annual leave - `end_date` (string): The end date of the requested annual leave - `external_ids` (array of object): A list of external IDs of the annual leave request. - `idx` (string): The external ID - `source_type` (string): The source type of the external ID - `id` (string): The identifier of the annual leave request - `request_type` (string; one of `holiday`, `maternity`): The kind of leave requested - `start_date` (string): The start date of the requested annual leave - `state` (string; one of `pending`, `confirmed`, `taken`, `rejected`, `cancelled`, `in_progress`): The state the request is in - `user_id` (string): The ID of the user the request is for - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** A query parameter is invalid: `user_id` is not a UUID, `page`/`page_size` is not an integer, `min_start_date`/`max_end_date` is not a date, or `state`/`source_type` is not an allowed value — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/annual_leave_requests" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get an annual leave request by ID `GET /api/v1/annual_leave_requests/{annual_leave_request_id}` Returns an annual leave request by ID ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `annual_leave_request_id` | path | string | yes | The ID of the annual leave request to get | ### Responses **200** Success Same fields as `AnnualLeaveRequest` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The `id` is not a valid UUID — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/annual_leave_requests/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # AuthStrategies Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of auth strategies for a user `GET /api/v1/users/{users_id}/auth_strategies` List all authentication strategies for the given user ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `users_id` | path | string | yes | Sona unique identifier of the user | ### Responses **200** Success Array of `UserAuthStrategy`: - `id` (string, required): Sona unique identifier of the auth strategy - `key` (string): The authentication key value (e.g. email address, phone number) - `type` (string; one of `email`, `mobile_number`, `okta`, `microsoft`, `adfs`, `kiosk`): The authentication strategy type **401** Unauthorized — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/users//auth_strategies" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get an individual user auth strategy `GET /api/v1/users/{users_id}/auth_strategies/{id}` Get an individual authentication strategy for the given user ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `users_id` | path | string | yes | Sona unique identifier of the user | | `id` | path | string | yes | Sona unique identifier of the auth strategy | ### Responses **200** Success Same fields as `UserAuthStrategy` above. **401** Unauthorized — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/users//auth_strategies/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update an individual user auth strategy `PUT /api/v1/users/{users_id}/auth_strategies/{id}` Update an existing authentication strategy for the given user ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `users_id` | path | string | yes | Sona unique identifier of the user | | `id` | path | string | yes | Sona unique identifier of the auth strategy | ### Request body The auth strategy parameters - `key` (string, required): The new authentication key value ### Responses **200** Success Same fields as `UserAuthStrategy` above. **401** Unauthorized — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/users//auth_strategies/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "key": "newemail@example.com" }' ``` ## Delete an individual user auth strategy `DELETE /api/v1/users/{users_id}/auth_strategies/{id}` Delete an authentication strategy for the given user ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `users_id` | path | string | yes | Sona unique identifier of the user | | `id` | path | string | yes | Sona unique identifier of the auth strategy | ### Responses **204** No Content **401** Unauthorized — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** `users_id` or `id` is not a valid UUID, e.g. `Invalid UUID format for id: 'abc'` — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/users//auth_strategies/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # CarePackages Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## List care packages `GET /api/v1/care_packages` Newest first. Requires demand-based rostering to be enabled for your organisation (403 otherwise). `updated_at` counts a change to the package (including being superseded) or a new approval status on any of its hour lines. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `pws_id` | query | string | no | Only this person's packages (Sona id) | | `pws_idx` | query | string | no | Only this person's packages (their `upload_id`) | | `idx` | query | string | no | Exact match on the package code | | `current` | query | boolean | no | `true` only packages in force today (started, and not ended; UTC date), `false` only the others: ended, or starting later | | `start_date_from` | query | string | no | Packages starting on or after this date (YYYY-MM-DD) | | `start_date_to` | query | string | no | Packages starting on or before this date (YYYY-MM-DD) | | `updated_at` | query | string | no | Only packages changed on or after this UTC datetime (ISO 8601, trailing Z) | ### Responses **200** Success - `entries` (array of `CarePackageData`) - `banking_enabled` (boolean): Banking enabled - `end_date` (string): ISO 8601 date, the last day in force. Null if it has no end yet. - `hours` (array of `HourLineData`): The commissioned hour lines, in the order they were given (a fixed but possibly different order on packages created before 23 October 2026) - `assignment_count` (integer): People supported in the ratio - `demand_periods` (array of `DemandPeriodRef`): The demand periods it applies in - `id` (string): Demand period id - `name` (string): Name - `hours` (number): Commissioned hours - `id` (string): Hour line id - `org_unit_id` (string): Service id - `org_unit_idx` (string): Service `upload_id` - `staff_count` (integer): Staff in the ratio - `status` (string): `pending`, `pre_approved`, `approved` or `rejected`: the latest approval status - `type_of_work_id` (string): Care type id - `type_of_work_idx` (string): Care type `idx` - `id` (string): Care package id - `idx` (string): Package code. Null for packages made in Sona without one. - `name` (string): Name - `notes` (string): Notes - `pws_id` (string): The person - `pws_idx` (string): The person's `upload_id` - `shared_care_during_specified` (boolean): Shared care during specified hours - `start_date` (string): ISO 8601 date - `status` (string): `pending`, `pre_approved`, `approved`, `rejected` or `partially_approved` - `superseded_by_id` (string): The package that replaced it - `updated_at_utc` (string): When the package or an hour line's approval status last changed, UTC ISO 8601 - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/care_packages" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get a care package `GET /api/v1/care_packages/{care_package_id}` One package with its hour lines. Use the Sona id, or the package code (`idx`) with `is_idx=true`. `status` is the package's overall approval state (`pending`, `pre_approved`, `approved`, `rejected` or `partially_approved`), worked out from the latest status of each hour line. Hour lines are returned in the order they were given, except on packages created before 23 October 2026: that order wasn't recorded, so their lines come back in a fixed order that may differ from it. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `care_package_id` | path | string | yes | Sona id, or `idx` when `is_idx=true` | | `is_idx` | query | boolean | no | Set to true to look the package up by `idx` | ### Responses **200** Success Same fields as `CarePackageData` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/care_packages/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## List an hour line's approval statuses `GET /api/v1/care_packages/{care_package_id}/hours/{hour_id}/statuses` The line's approval history, newest first (by `effective_date`, then when it was added). The first entry is the line's current status. Requires demand-based rostering to be enabled for your organisation (403 otherwise). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `care_package_id` | path | string | yes | Sona id, or the package code when `is_idx=true` | | `hour_id` | path | string | yes | The hour line's Sona id, from the care package | | `is_idx` | query | boolean | no | Set to true to look the package up by its code (`idx`) | ### Responses **200** Success - `entries` (array of `HourStatusData`) - `actioned_by_source` (string): `api` for statuses added through this API - `effective_date` (string): ISO 8601 date - `hour_id` (string): The hour line - `id` (string): Status id - `inserted_at_utc` (string): When it was added, UTC ISO 8601 - `notes` (string): Notes - `status` (string): `pending`, `pre_approved`, `approved` or `rejected` - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/care_packages//hours//statuses" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Add an approval status to an hour line `POST /api/v1/care_packages/{care_package_id}/hours/{hour_id}/statuses` Appends an approval status to one hour line. A status is **added, never edited**, and the latest one is the line's current approval. There is no workflow: any status may follow any other. `effective_date` defaults to today, the UTC date as in the admin panel; it can't be in the future, before the package starts, or before the line's current status (a status is added, never back-dated). A new line starts as `pending`. The package's overall status updates from its lines. Once every line is `rejected` the package is withdrawn, as in the admin panel: it no longer counts towards demand and takes no more statuses (409); create a new package instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `care_package_id` | path | string | yes | Sona id, or the package code when `is_idx=true` | | `hour_id` | path | string | yes | The hour line's Sona id, from the care package | | `is_idx` | query | boolean | no | Set to true to look the package up by its code (`idx`) | ### Request body The status - `effective_date` (string): ISO 8601 date the status takes effect. Defaults to today (the UTC date). Not after today in UTC, not before the package starts, and not before the line's current status. - `notes` (string): Free text - `status` (string, required): `pending`, `pre_approved`, `approved` or `rejected` ### Responses **201** Created Same fields as `HourStatusData` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **409** The package is rejected — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorOrValidationErrors` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/care_packages//hours//statuses" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "effective_date": "2026-10-12", "notes": "Signed off by the commissioner", "status": "approved" }' ``` ## Add the same approval status to every hour line of a package `POST /api/v1/care_packages/{care_package_id}/statuses` Applies one status to **every** hour line of the package in one call, all or nothing. The same date rules apply to each line, so if any line can't take the date, none changes. Returns one entry per line. Rejecting every line withdraws the package, and a withdrawn package takes no more statuses (409). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `care_package_id` | path | string | yes | Sona id, or the package code when `is_idx=true` | | `is_idx` | query | boolean | no | Set to true to look the package up by its code (`idx`) | ### Request body The status Same fields as `HourStatusCreateParams` above. ### Responses **201** Created - `entries` (array of `HourStatusData`): One per hour line - Same fields as `HourStatusData` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **409** The package is rejected — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorOrValidationErrors` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/care_packages//statuses" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "effective_date": "2026-10-12", "notes": "Signed off by the commissioner", "status": "approved" }' ``` ## List a person's care packages `GET /api/v1/people_we_support/{person_we_support_id}/care_packages` Newest first. Requires demand-based rostering to be enabled for your organisation (403 otherwise). `updated_at` counts a change to the package (including being superseded) or a new approval status on any of its hour lines. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `person_we_support_id` | path | string | yes | Sona id, or `upload_id` when `is_idx=true` | | `is_idx` | query | boolean | no | Set to true to look the person up by `upload_id` | | `idx` | query | string | no | Exact match on the package code | | `current` | query | boolean | no | `true` only packages in force today (started, and not ended; UTC date), `false` only the others: ended, or starting later | | `start_date_from` | query | string | no | Packages starting on or after this date (YYYY-MM-DD) | | `start_date_to` | query | string | no | Packages starting on or before this date (YYYY-MM-DD) | | `updated_at` | query | string | no | Only packages changed on or after this UTC datetime | ### Responses **200** Success - `entries` (array of `CarePackageData`) - Same fields as `CarePackageData` above. - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/people_we_support//care_packages" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a care package `POST /api/v1/people_we_support/{person_we_support_id}/care_packages` Creates a care package for a person. **Care packages are never edited.** A change is a new package with a later `start_date`, which ends the person's current package the day before and links it. `start_date` must be after the current package's start (no back-dating), and `idx`, your own id for the package, must be unique in your organisation. Everything is referenced by id, never by name: each hour line names its care type (`type_of_work_id` or `type_of_work_idx`), its service (`org_unit_id` or `org_unit_idx`) and at least one `demand_period_ids` entry, and every demand period must belong to that line's service. All or nothing: one invalid line means no package. Every hour line starts as `pending`. Up to 100 hour lines. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `person_we_support_id` | path | string | yes | Sona id, or `upload_id` when `is_idx=true` | | `is_idx` | query | boolean | no | Set to true to look the person up by `upload_id` | ### Request body The package - `banking_enabled` (boolean): Defaults to false - `end_date` (string): ISO 8601 date. Omit or null for ongoing. - `hours` (array of `HourLineInput`, required): At least one, up to 100 - `assignment_count` (integer, required): People supported in the ratio, above 0 - `demand_period_ids` (array of string, required): Ids of this service's demand periods. At least one. - `hours` (number, required): Commissioned hours, above 0 - `org_unit_id` (string): Service (Sona id). Give this or `org_unit_idx`. - `org_unit_idx` (string): Service (your own id, `upload_id`) - `staff_count` (integer, required): Staff in the ratio, above 0 - `type_of_work_id` (string): Care type (Sona id). Give this or `type_of_work_idx`. - `type_of_work_idx` (string): Care type (your own id: the care type's `idx`) - `idx` (string, required): Your own id for the package. Unique in your organisation. - `location` (object): Optional. Updates the person's residency address. - `address_line_1` (string): Address line 1 - `address_line_2` (string): Address line 2 - `address_line_3` (string): Address line 3 - `address_line_4` (string): Address line 4 - `city` (string): City - `country` (string): Country name - `name` (string): Location name - `postcode` (string): Postal code - `name` (string): Package name - `notes` (string): Notes - `shared_care_during_specified` (boolean): Defaults to true - `start_date` (string, required): ISO 8601 date, after the current package's start ### Responses **201** Created Same fields as `CarePackageData` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorOrValidationErrors` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/people_we_support//care_packages" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "banking_enabled": false, "hours": [ { "assignment_count": 1, "demand_period_ids": [ "7a1f0c9e-2b3d-4c5e-8f60-1a2b3c4d5e6f" ], "hours": 84, "org_unit_idx": "SVC001", "staff_count": 1, "type_of_work_idx": "PERSONAL_CARE" } ], "idx": "pkg_a_2026", "name": "Alice 2026", "shared_care_during_specified": true, "start_date": "2026-01-01" }' ``` --- # ContractedHours Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of ContractedHours `GET /api/v1/contracted_hours` Contracted Hours ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `idx` | query | string | no | The external id to filter by | | `start_date` | query | string | no | The start_date to filter by. Expecting YYYY-MM-DD format. | | `finish_date` | query | string | no | The finish_date to filter by. Expecting YYYY-MM-DD format. | | `user_id` | query | string | no | The user_id to filter by | | `employment_period_id` | query | string | no | The employment_period_id to filter results by | ### Responses **200** Success - `entries` (array of `ContractedHour`) - `colour_coding` (boolean): Whether to specially highlight contracted hours on roster - `days` (number, required): Total number of contracted days in a period, or null when not set - `employment_period_id` (string): Sona unique identifier of the employment period these hours belong to. Only returned by the `/api/v1/contracted_hours` endpoints. - `employment_period_idx` (string): External identifier of the employment period these hours belong to. Only returned by the `/api/v1/contracted_hours` endpoints. - `finish_date` (string, date, required): The date from when these contracted hours are no longer applicable, or null when open-ended - `hours` (number, required): Total number of contracted hours in a period - `id` (string, required): The unique Sona identifier of the contracted hours record - `idx` (string): An external identifier of the contracted hours. - `monthly_period_starts_at` (integer, required): The day of the month a monthly period starts at - `org_unit_id` (string): The Sona unique identifier of the org unit these hours apply to - `org_units` (array of object): The org units these hours apply to. Only returned by the `/api/v1/contracted_hours` endpoints. - `id` (string): Sona unique identifier of the org unit - `period` (string; one of `weekly`, `monthly`, `rolling_sequence`, `calendar_month_daily`): The type of period - `rolling_period_sequence_length_days` (integer): The length of rolling sequence in days - `rolling_period_sequence_start` (string, date): When the rolling sequence starts - `start_date` (string, date, required): The date from when these contracted hours are applicable - `type_of_work_id` (string): The Sona unique identifier of the type of work these hours apply to - `types_of_work` (array of object): The types of work these hours apply to. Only returned by the `/api/v1/contracted_hours` endpoints. - `id` (string): Sona unique identifier of the type of work - `weekly_period_starts_at` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`, required): The day of the week a weekly period starts at - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** `user_id` is not a valid UUID, or `type_of_work_id`/`org_unit_id` doesn't belong to the organisation — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/contracted_hours" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create new contracted hours `POST /api/v1/contracted_hours` Create new contracted hours. See the ContractedHours listing endpoint for a description of a contracted hours. One of the employment_period_id or employment_period_idx parameters is required. The hours are bounded by the length of the period: at most 168 for `weekly`, 744 for `monthly` and `calendar_month_daily`, and 24 per day of `rolling_period_sequence_length_days` for `rolling_sequence`. Hours beyond that are rejected with a 422, on create and on any update that names `hours`, `period` or `rolling_period_sequence_length_days`. Contracted hours can't overlap other contracted hours on the same employment spec with the same `period` (for `rolling_sequence`, the same length) and the same or intersecting types of work and org units; an overlap is rejected with a 422 under `duplicate`. Creating open-ended hours (no `finish_date`) that start after open-ended hours of the same kind ends those hours the day before instead. Where the only overlap is the day one record ends and the next of the same kind starts, the earlier record is ended the day before instead. ### Request body The contracted hours parameters - `colour_coding` (boolean): Whether to specially highlight contracted hours on roster - `days` (integer): Amount of days the contracted hours were active - `employment_period_id` (string): The unique Sona identifier of the employment period. At least one of employment_period_id or employment_period_idx is required. - `employment_period_idx` (string): External identifier of the employment_period. At least one of employment_period_id or employment_period_idx is required. - `finish_date` (string, date): The date from when these contracted hours are no longer applicable. Expecting YYYY-MM-DD format e.g. "1990-01-30" - `hours` (number, required): Amount of hours the employee has worked during contracted hours. Bounded by the length of the period — see the endpoint description - `idx` (string): An external identifier of the contracted hours. - `monthly_period_starts_at` (integer): Which day of the month the contracted hours start, e.g. on the 15th or on the 1st - `org_unit_id` (string): (Legacy) The unique Sona identifier of a single org unit. Use org_units instead to assign one or more locations. - `org_units` (array of `OrgUnitRef`): Array of org units (locations) to assign. Supports multiple locations. Each entry must be an object with an id field. Takes precedence over org_unit_id when provided. - `id` (string, required): The unique Sona identifier of the org unit - `period` (string; one of `weekly`, `monthly`, `rolling_sequence`, `calendar_month_daily`, required): Whether the period is monthly or weekly - `rolling_period_sequence_length_days` (integer): The length of rolling sequence in days - `rolling_period_sequence_start` (string, date): When the rolling sequence starts - `start_date` (string, date, required): The date from when these contracted hours are applicable. Expecting YYYY-MM-DD format e.g. "1990-01-30" - `type_of_work_id` (string): (Legacy) The unique Sona identifier of a single type of work. Use types_of_work instead to assign one or more TOWs. - `types_of_work` (array of `TypeOfWorkRef`): Array of types of work to assign. Supports multiple TOWs. Each entry must be an object with an id field. Takes precedence over type_of_work_id when provided. - `id` (string, required): The unique Sona identifier of the type of work - `weekly_period_starts_at` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`): Which day of the week the contracted hours start ### Responses **200** Success Same fields as `ContractedHour` above. **401** Unauthorized — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The request is invalid. `message` is a string for request-level problems (e.g. a missing employment period id or idx, an org unit or type of work outside the organisation, hours beyond the period's bound) or an object mapping each invalid field to its error messages for validation failures — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/contracted_hours" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "colour_coding": true, "days": 5, "employment_period_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "employment_period_idx": "159600fb-3fa9-4687-bbc9-196375de6ee3", "finish_date": "2024-05-05", "hours": 40.0, "idx": "test_idx", "monthly_period_starts_at": 5, "org_units": [ { "id": "159600fb-3fa9-4687-bbc9-196375de6ee3" }, { "id": "2a1b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" } ], "period": "weekly", "rolling_period_sequence_length_days": 20, "rolling_period_sequence_start": "2024-03-01", "start_date": "2024-01-01", "types_of_work": [ { "id": "159600fb-3fa9-4687-bbc9-196375de6ee3" } ], "weekly_period_starts_at": "tuesday" }' ``` ## Get individual contracted_hours `GET /api/v1/contracted_hours/{contracted_hours_id_or_idx}` Get individual contracted_hours. See the contracted_hours listing endpoint for a description of contracted_hours ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `contracted_hours_id_or_idx` | path | string | yes | Sona unique identifier or external id of the contracted hours | | `is_idx` | query | boolean | no | IF contracted_hours_id_or_idx is an external id, this parameter should be set to true | | `start_date` | query | string, date | no | The date from when these contracted hours are applicable | | `finish_date` | query | string, date | no | The date from when these contracted hours are no longer applicable | | `type_of_work_id` | query | string, date | no | Sona unique identifier of the type of work condition to filter contracted hours by | | `org_unit_id` | query | string, date | no | Sona unique identifier of the location condition to filter contracted hours by | ### Responses **200** Success Same fields as `ContractedHour` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/contracted_hours/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update an existing contracted hours `PUT /api/v1/contracted_hours/{contracted_hours_id_or_idx}` Update an existing contracted hours. See the ContractedHours listing endpoint for a description of a contracted hours. One of the id or idx parameters is required. If both employment_period_id and employment_period_idx are given employment_period_id has a higher precedence. The hours are bounded by the length of the period: at most 168 for `weekly`, 744 for `monthly` and `calendar_month_daily`, and 24 per day of `rolling_period_sequence_length_days` for `rolling_sequence`. Hours beyond that are rejected with a 422, on create and on any update that names `hours`, `period` or `rolling_period_sequence_length_days`. An update is rejected with a 422 under `duplicate` if it makes these hours overlap other contracted hours on the same employment spec with the same `period` (for `rolling_sequence`, the same length) and the same or intersecting types of work and org units. Where the only overlap is the day one record ends and the next of the same kind starts, the earlier record is ended the day before instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `contracted_hours_id_or_idx` | path | string | yes | Sona unique identifier or external id of the contracted hours | ### Request body The contracted hours parameters object: Fields to update on a custom form entry. Only provided fields are changed. Warning: updating entries may affect payroll records, approval workflows, shifts, employee documents, form requests, data gathering steps, workflows, and HR reports. See the PUT endpoint description for full details. ### Responses **200** Success Same fields as `ContractedHour` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The request is invalid. `message` is a string for request-level problems (e.g. an org unit or type of work outside the organisation, hours beyond the period's bound) or an object mapping each invalid field to its error messages for validation failures — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/contracted_hours/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "age": 3, "name": "Updated value" }' ``` ## Delete contracted hours `DELETE /api/v1/contracted_hours/{contracted_hours_id_or_idx}` Delete a future dated contracted hours record. Contracted hours that start today or earlier, in your organisation's timezone, cannot be deleted. When addressing the record by external id, start_date and type_of_work_id are required alongside is_idx. A token restricted to specific endpoints needs `DELETE /api/v1/contracted_hours/:id` in its list, or the response is a 403. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `contracted_hours_id_or_idx` | path | string | yes | Sona unique identifier or external id of the contracted hours | | `is_idx` | query | boolean | no | IF contracted_hours_id_or_idx is an external id, this parameter should be set to true | | `start_date` | query | string, date | no | The date from when these contracted hours are applicable | | `type_of_work_id` | query | string | no | Sona unique identifier of the type of work condition to filter contracted hours by | ### Responses **204** No Content **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The contracted hours can't be deleted. `message` is a string, or an object mapping fields to error messages when the deletion fails validation — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/contracted_hours/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # ContractTypes Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of Contract Types `GET /api/v1/contract_types` Contract type ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `ContractType`) - `booking_granularity` (string; one of `full_day`, `half_day`, `full_hour`, `half_hour`, `quarter_hour`, `minute`): Granularity for annual leave booking - `denomination` (string; one of `hours`, `days`): The denomination for holiday allowances - `employment_basis` (string): Free-text employment basis label, or null when not set - `employment_type` (string; one of `full_time`, `part_time`, `flexible`): The employment type classification for this contract. This field is a property of the contract type, not a top-level field on the employment period. - `holiday_allowance_rounding` (string; one of `full_day`, `half_day`, `full_hour`, `half_hour`, `quarter_hour`, `minute`): Rounding method for calculating holiday allowance - `id` (string, required): Sona unique identifier - `include_absence_cost` (boolean): Whether paid absence days contribute to wage cost calculations - `include_absence_hours` (boolean): Whether absence hours count towards the wage cost hours metric. Only has an effect when include_absence_cost is true - `name` (string, required): The name of the contract type - `only_worked_days` (boolean): Whether salary distributions should consider only days worked - `pay_period_start` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`, required): The day of the week the pay period starts - `pay_period_type` (string; one of `weekly`, `monthly`, required): The type of pay period - `payroll_rule` (string; one of `hourly`, `fixed_salary`, required): How the pay is paid - `worked_days_in_year` (number): Number of worked days in the year to distribute salary across when only worked days are enabled (supports decimals) - `worked_weeks_in_year` (number): Number of worked weeks in the year to distribute salary across for weekly salary distribution (supports decimals) - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/contract_types" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get an individual contract type `GET /api/v1/contract_types/{contract_type_id}` Get an individual contract type. See the contract type listing endpoint for a description of contract types ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `contract_type_id` | path | string | yes | Sona unique identifier of the contract_type | ### Responses **200** Success Same fields as `ContractType` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/contract_types/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # CsvImports Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Upload a CSV file to the importer `POST /api/v1/csv_imports/upload/{file_type}` Uploads a CSV file to the importer, and returns a report of running the import ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `file_type` | path | string | yes | CSV file type | | `csv` | formData | file | yes | CSV file | ### Responses **200** Success. The file was processed; row-level failures are reported in `results`, not as an error status. - `file_type` (string): The CSV file type that was imported - `results` (array of object): One entry per imported record: `{"ok": ...}` with the imported record, or `{"error": ...}` with field errors (field name to messages) or a `type`/`message` object **400** Bad request — `ErrorResponse` (see the overview) **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/csv_imports/upload/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -F "csv=@" ``` --- # CustomFormEntries Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of CustomFormEntries `GET /api/v1/custom_form_entries` Custom Form Entries ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `CustomFormEntry`) - `audience_locks` (object): Per-audience signature stamps for forms that use audience-aware locking. Keys are audience identifiers (e.g. `"manager"`, `"employee"`); values record when and by whom that audience signed. Empty object for forms that don't configure audiences. - `custom_form_id` (string, required): The unique Sona identifier of the custom form this entry was created against. - `data` (object): An object containing the submitted field values, keyed by the field names from the custom form's schema. For file upload fields, a successful upload returns an array of objects with `client_filename`, `internal_filename`, `storage_path`, `client_type`, and `client_size`. Fields that were not provided in the request will appear as `null`. - `id` (string, required): Auto-generated unique identifier for this entry. This is NOT the user ID — it is a new UUID created by Sona when the entry is saved. - `locked_at` (string): ISO 8601 timestamp recording when the entry was fully locked. `null` until every audience configured on the form has signed (or, for whole-form locking, until any `lock_on_completion` trigger fires). Locked entries are treated as immutable snapshots by the Sona UI. - `user_id` (string, required): The unique Sona identifier of the user this entry belongs to. This is the same `user_id` you sent in the create request. - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/custom_form_entries" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create new custom form entry `POST /api/v1/custom_form_entries` Create a new custom form entry. **Step-by-step workflow:** 1. **Fetch the custom form** — call `GET /api/v1/custom_forms/{custom_form_id}` to retrieve the form definition. 2. **Check `submission_type`** — this determines which fields are required: - `single` — send `custom_form_id`, `user_id`, and every field from the form's single fieldset. - `multiple` — the form has several fieldsets, one per document type. Pick the document type you need, then send `custom_form_id`, `user_id`, `employee_document_type`, and every field from the matching fieldset. 3. **Find the field keys** — look at the `schema.fieldsets[].fields[].name` values in the custom form response. These are the keys to include in your create request. 4. **For `multiple` forms, find valid document types** — each fieldset (after the first) has a `variation.value` (e.g. `"CV"`, `"Passport"`). Pass the chosen value as `employee_document_type`. **Important — field names must be exact:** any parameter that does not match a field `name` from the custom form's schema is silently ignored. If your file upload or other fields appear as `null` in the response `data` object, check that the parameter names in your request exactly match the field names from `GET /api/v1/custom_forms/{id}`. There is no `file`, `document`, or `type` parameter — the file content must be the value of the field's own name (e.g. `document_1`). **File uploads** can be sent in three ways: 1. `multipart/form-data` with a `Plug.Upload` file field (standard browser-style upload) 2. `application/json` with a data URL string: `"data:application/pdf;base64,JVBERi0xLjQ..."` 3. `application/json` with a JSON object: `{"base64": "JVBERi0xLjQ...", "filename": "doc.pdf", "content_type": "application/pdf"}` - `base64` is required - At least one of `filename` or `content_type` must be provided - If `content_type` is omitted it is inferred from the filename extension **Base64 upload limits:** - Maximum file size: 30 MB (after decoding) - Allowed content types: PDF, PNG, JPEG, GIF, WebP, SVG, Word, Excel, PowerPoint, plain text, CSV, RTF - Maximum JSON request body: 60 MB by default (all fields combined, after base64 encoding) - Note: a 30 MB file is ~40 MB when base64-encoded. For large files, prefer multipart uploads. ### Request body The parameters for the new custom form entry. `custom_form_id` and `user_id` are always required. Additional required or optional fields are defined by the custom form the entry is being created for. - `custom_form_id` (string, required): The unique Sona identifier of the custom form - `employee_document_type` (string): The document type to create an entry for. Required when the custom form has `submission_type: multiple`. Valid values are found in the custom form's `schema.fieldsets[].variation.value`. Not used for `single` submission type forms. - `user_id` (string, required): The unique Sona identifier of a user ### Responses **200** Success Same fields as `CustomFormEntry` above. **401** Not Authorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity. `message` is a string (e.g. missing fields, wrong document type, failed upload), or an object of field errors when validation fails. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/custom_form_entries" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "custom_form_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "employee_document_type": "CV", "user_id": "74a1af00-d181-4c95-bb6a-aab800185595" }' ``` ## Get individual custom_form_entries `GET /api/v1/custom_form_entries/{custom_form_entry_id}` Get an individual custom_form_entry. See the custom_form_entries listing endpoint for a description of custom_form_entries ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `custom_form_entry_id` | path | string | yes | Sona unique identifier of the custom_form_entry | ### Responses **200** Success Same fields as `CustomFormEntry` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/custom_form_entries/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update an existing custom form entry `PUT /api/v1/custom_form_entries/{id}` Update an existing custom form entry. Only the fields provided in the request body will be updated; omitted fields retain their current values. The `custom_form_id` and `user_id` cannot be changed after creation. **⚠ Important — updating entries can affect other parts of the system:** Custom form entries may be referenced by other Sona subsystems. Updating field values can have downstream consequences including: - **Payroll** — If the entry has been used to generate payroll additional payment/deduction elements, updating field values will **not** retroactively update those payroll records. This can cause discrepancies between the entry data and what was submitted to payroll. - **Approval workflows** — Entries linked to an approval (pending or completed) cannot be updated. The request will return a `422` error. - **Shifts** — Entries linked to shifts (e.g. shift-specific custom form data) will reflect the updated values. Ensure the new values are still valid in the context of the shift. - **Employee documents** — If the entry is linked to employee document versions, updating file upload fields may leave stale references to previous document versions. - **Form requests** — Entries created via form requests (e.g. self-service or manager submissions) are linked back to the form request record. Updating the entry directly bypasses any form request validation or approval logic. - **Data gathering steps** — If the entry was created as part of a data gathering workflow, updating it directly bypasses the step completion logic and may cause the step to appear out of sync with its stored data. - **Workflows** — Entries produced by automated workflows are tracked as workflow outputs. Updating these entries does not re-trigger or update the workflow. - **HR reports** — Reports that reference custom form entry data will reflect updated values on next run, which may change historical report outputs. Always verify whether the entry is connected to other processes before updating via the API. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Sona unique identifier of the custom form entry | ### Request body The fields to update on the custom form entry. Only provided fields are changed. object: Fields to update on a custom form entry. Only provided fields are changed. Warning: updating entries may affect payroll records, approval workflows, shifts, employee documents, form requests, data gathering steps, workflows, and HR reports. See the PUT endpoint description for full details. ### Responses **200** Success Same fields as `CustomFormEntry` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/custom_form_entries/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "age": 3, "name": "Updated value" }' ``` ## Delete a custom form entry `DELETE /api/v1/custom_form_entries/{id}` Permanently delete a custom form entry. **This action cannot be undone.** **⚠ Important — deleting entries has cascading effects across the system:** Custom form entries are referenced by multiple Sona subsystems. Deleting an entry will have the following consequences: - **Tasks** — Any incomplete tasks linked to this entry will be **automatically marked as completed**. This is irreversible and may confuse users who see tasks disappear from their task list without explanation. - **Payroll** — Entries that have been submitted to payroll (i.e. have linked payroll additional payment/deduction elements) **cannot be deleted**. The request will return a `422` error. This is enforced by a database constraint to prevent orphaned payroll records. - **Approval workflows** — Entries linked to any approval record (pending, approved, or rejected) **cannot be deleted**. The request will return a `422` error. - **Locked entries** — Entries that have been locked (e.g. by an approval or payroll workflow) **cannot be deleted**. The request will return a `422` error. - **Shifts** — If the entry is linked to a shift via the shifts–custom-form-entries association, deletion will leave the shift with a broken reference. The shift may no longer display its associated custom form data correctly. - **Employee documents** — If the entry is linked to employee document versions, deletion may orphan the document version records, causing missing data in the employee's document history. - **Form requests** — If the entry was created via a form request (self-service or manager submission), the form request record will retain a reference to the now-deleted entry, which may cause errors when viewing the form request history. - **Data gathering steps** — If the entry was created as part of a data gathering workflow, deletion will leave the step referencing a non-existent entry, potentially breaking the data gathering flow. - **Workflows** — If the entry was produced by an automated workflow, the workflow output record will retain a dangling reference. Re-running the workflow will not recreate the deleted entry. - **HR reports** — Reports referencing the deleted entry's data will no longer include it, which may change historical report outputs. Always verify whether the entry is connected to other processes before deleting via the API. Use `GET /api/v1/custom_form_entries/{id}` to inspect the entry first. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Sona unique identifier of the custom form entry | ### Responses **204** No Content **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity - entry is locked, linked to an approval, or has been submitted to payroll — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/custom_form_entries/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # CustomForms Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of CustomForms `GET /api/v1/custom_forms` Custom Forms ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `CustomForm`) - `current_version_id` (string): The published version the form views belong to, or null when the form has none - `form_name` (string): Human-readable name for the custom form - `form_views` (array of object): The current version's form views, ordered by slug. Empty when the form has no current version or its version has no views. - `description` (string): The view's description - `id` (string): The form view's identifier - `schema` (object): Groups of steps of questions, each question with its label, `required`, `required_conditions`, `conditions` (visibility), `editable` and `editable_conditions` - `slot_slug` (string): Where the view is used, e.g. `form_request` for a form sent to an employee to fill in - `slug` (string): The view's slug, unique within its version - `title` (string): The view's title - `id` (string, required): The unique Sona identifier of the custom form - `organisation_id` (string): The unique Sona identifier of the organisation the form belongs to - `schema` (object): The form's field definitions: fieldsets for a `legacy` form, a flat `fields` list for a `view_enabled` form. Inspect this to discover which field keys to include when creating entries. - `fields` (array of object): Only on `view_enabled` forms: the flat list of field definitions, each with at least `name` and `type`. - `fieldsets` (array of object): Array of fieldset objects. Each fieldset contains a `fields` array (field definitions) and, for `multiple` submission type forms, a `variation` object identifying the document type. - `schema_type` (string): `legacy` (fields under `fieldsets`) or `view_enabled` (a flat `fields` list, presented through form views) - `submission_type` (string): Either `single` (one fieldset, no document type selection) or `multiple` (several fieldsets, each representing a different document type). Determines whether `employee_document_type` is required when creating entries. - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/custom_forms" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get individual custom_form `GET /api/v1/custom_forms/{custom_form_id}` Get individual custom_form. See the custom_forms listing endpoint for a description of custom_forms ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `custom_form_id` | path | string | yes | Sona unique identifier | ### Responses **200** Success Same fields as `CustomForm` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/custom_forms/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # EmploymentPeriods Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## List of Employment Periods `GET /api/v1/employment_periods` Returns a paginated list of employment periods. Supports filtering by employee number, external identifier, user ID, or last updated timestamp. **Pagination**: Results are paginated. Use the `page` and `page_size` query parameters to navigate. The default page size is 10. Note: the parameter name is `page` (not `page_number`). The response includes `page_number`, `page_size`, `total_pages`, and `total_entries` metadata to assist with iteration. Example: `GET /api/v1/employment_periods?page=2&page_size=100` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `employee_number` | query | string | no | Unique number of the employee who is working this employment_period | | `idx` | query | string | no | The idx (external identifier) to filter by | | `user_id` | query | string | no | The user_id to filter by | | `updated_at` | query | string | no | UTC datetime to filter employment periods updated on or after this timestamp. Use format YYYY-MM-DDThh:mm:ssZ in UTC (e.g. 2024-01-15T10:00:00Z) | ### Responses **200** Success - `entries` (array of `EmploymentPeriodSimple`) - `active` (boolean, required): Whether or not the user is currently active - `continuous_employment_start_date` (string, date, required) - `contract_id` (string): The unique Sona identifier of the contract this employment period belongs to. Employment periods sharing a contract_id are versions of the same contract. - `employee_number` (string, required): Unique number of the employee who is working this employment_period - `employment_specs` (array of `EmploymentSpec`): related employment specs - `contract_types` (array of `ContractType`): Related contract types. Use `api/v1/contract_types` endpoint to retrieve available Contract Types. Each contract type includes an `employment_type` field with values: `full_time`, `part_time`, or `flexible`. - `booking_granularity` (string; one of `full_day`, `half_day`, `full_hour`, `half_hour`, `quarter_hour`, `minute`): Granularity for annual leave booking - `denomination` (string; one of `hours`, `days`): The denomination for holiday allowances - `employment_basis` (string): Free-text employment basis label, or null when not set - `employment_type` (string; one of `full_time`, `part_time`, `flexible`): The employment type classification for this contract. This field is a property of the contract type, not a top-level field on the employment period. - `holiday_allowance_rounding` (string; one of `full_day`, `half_day`, `full_hour`, `half_hour`, `quarter_hour`, `minute`): Rounding method for calculating holiday allowance - `id` (string, required): Sona unique identifier - `include_absence_cost` (boolean): Whether paid absence days contribute to wage cost calculations - `include_absence_hours` (boolean): Whether absence hours count towards the wage cost hours metric. Only has an effect when include_absence_cost is true - `name` (string, required): The name of the contract type - `only_worked_days` (boolean): Whether salary distributions should consider only days worked - `pay_period_start` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`, required): The day of the week the pay period starts - `pay_period_type` (string; one of `weekly`, `monthly`, required): The type of pay period - `payroll_rule` (string; one of `hourly`, `fixed_salary`, required): How the pay is paid - `worked_days_in_year` (number): Number of worked days in the year to distribute salary across when only worked days are enabled (supports decimals) - `worked_weeks_in_year` (number): Number of worked weeks in the year to distribute salary across for weekly salary distribution (supports decimals) - `contracted_hours` (array of `ContractedHour`): related contracted hours - `colour_coding` (boolean): Whether to specially highlight contracted hours on roster - `days` (number, required): Total number of contracted days in a period, or null when not set - `employment_period_id` (string): Sona unique identifier of the employment period these hours belong to. Only returned by the `/api/v1/contracted_hours` endpoints. - `employment_period_idx` (string): External identifier of the employment period these hours belong to. Only returned by the `/api/v1/contracted_hours` endpoints. - `finish_date` (string, date, required): The date from when these contracted hours are no longer applicable, or null when open-ended - `hours` (number, required): Total number of contracted hours in a period - `id` (string, required): The unique Sona identifier of the contracted hours record - `idx` (string): An external identifier of the contracted hours. - `monthly_period_starts_at` (integer, required): The day of the month a monthly period starts at - `org_unit_id` (string): The Sona unique identifier of the org unit these hours apply to - `org_units` (array of object): The org units these hours apply to. Only returned by the `/api/v1/contracted_hours` endpoints. - `period` (string; one of `weekly`, `monthly`, `rolling_sequence`, `calendar_month_daily`): The type of period - `rolling_period_sequence_length_days` (integer): The length of rolling sequence in days - `rolling_period_sequence_start` (string, date): When the rolling sequence starts - `start_date` (string, date, required): The date from when these contracted hours are applicable - `type_of_work_id` (string): The Sona unique identifier of the type of work these hours apply to - `types_of_work` (array of object): The types of work these hours apply to. Only returned by the `/api/v1/contracted_hours` endpoints. - `weekly_period_starts_at` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`, required): The day of the week a weekly period starts at - `id` (string, required): The unique Sona identifier of the employment_spec - `rates_of_pay` (array of `EmploymentPeriodRateOfPay`): related rates of pay. Use `api/v1/rates_of_pay endpoint to retrieve available Rates of Pay - `finish_date` (string, date, required): The date from when this rate of pay is no longer applicable, or null when open-ended - `id` (string, required): The unique Sona identifier of the rate of pay - `idx` (string): An external identifier of the rate of pay. - `is_default` (boolean): Whether this is a default rate of pay - `label` (string): A label for the rate of pay - `org_unit_id` (string): The unique Sona identifier of the org_unit - `pay_type` (string; one of `hourly`, `salaried`): How it is paid, e.g. hourly / salaried - `rate` (string, required): The payment rate, formatted with its currency symbol (e.g. "£12.50") - `start_date` (string, date, required): The date from when this rate of pay is applicable - `type_of_work_id` (string): The unique Sona identifier of the type_of_work - `finish_date` (string, date, required): The finish date for this period of employment, or null when it is open-ended - `forces_new_holiday_breakdown` (boolean): Whether to create a new holiday breakdown starting with this employment period. Defaults to true if the new employment period is for a rehire. - `id` (string, required): The unique Sona identifier of the employment_period - `idx` (string): External identifier of the employment_period - `job_grade_id` (string, required): The unique Sona identifier of the job grade - `job_id` (string, required): The unique Sona identifier of the job - `level_id` (string, required): The unqiue Soan identifier of the level - `organisation_id` (string): Sona unique identifier of the organisation - `organisation_user_id` (string, required): The unique Sona identifier of the organisation user record - `permission_roles` (array of `PermissionRole`): Related permission roles - `description` (string, required): Description of the role - `id` (string, required): Sona unique identifier - `location_specific` (boolean): Is the role location specific? - `name` (string, required): Name of the role - `permission_roles_locations` (array of object) - `org_unit_ids` (array of string): The unique Sona identifiers of the org units - `permission_role_id` (string): The unique Sona identifier of the permission role - `primary_org_unit_id` (string, required): The Sona unique identifier of the org unit - `raffy_tier` (string, required): The user's current Raffy credit tier: raffy_crew, raffy_gm, or raffy_hq - `reports_to_org_user_id` (string, required): [DEPRECATED: use reports_to_user_id] The unique Sona identifier of the organisation user to whom the employee reports - `reports_to_user_id` (string, required): The unique Sona identifier of the user to whom the employee reports - `start_date` (string, date, required): The start date for this period of employment - `starter_declaration` (string, required): Starter declaration (https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1106535/Starter_checklist.pdf) - `student_loan_plan_five` (boolean, required): Employee has student loan plan five - `student_loan_plan_four` (boolean, required): Employee has student loan plan four - `student_loan_plan_one` (boolean, required): Employee has student loan plan one - `student_loan_plan_two` (boolean, required): Employee has student loan plan two - `student_loan_post_graduate` (boolean, required): Employee has post graduate student loan plan - `termination_reason` (`EmploymentPeriodTerminationReason`): The reason this users employment was terminated - `details` (string): Additional context for the termination - `id` (string, required): The unique Sona identifier of the termination reason record, or null when only `details` were recorded - `name` (string): The reason, or null when only `details` were recorded - `termination_would_rehire` (boolean): Whether or not this person is eligible for rehiring following termination - `tupe` (boolean): Whether the employee has transferred under TUPE regulations - `types_of_work` (array of `TypeOfWork`): related types of work - `id` (string, required): Sona unique identifier - `idx` (string): An external ID for this type of work - `name` (string, required): The name of the type of work - `properties` (object): Additional key-value properties for this type of work - `updated_at_utc` (string, date-time): When the employment period was last updated, in UTC - `user_id` (string, required): The unique Sona identifier of the user record - `page_number` (integer): The current page number (1-indexed) - `page_size` (integer): The number of items per page (default: 10) - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/employment_periods" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a new employment_period `POST /api/v1/employment_periods` Create a new employment period for a user. ## Employment Specs At least one employment spec must be provided, each containing `contract_types`, `rates_of_pay`, and `contracted_hours`. ## Rates of Pay Rates of pay within an employment spec support two modes: 1. **Reference an existing rate**: provide `{"id": ""}` to link a pre-existing rate of pay to the employment spec. 2. **Create a new rate inline**: omit `id` and provide `amount`, `currency`, `start_date`, `pay_type` (and optionally `finish_date`, `idx`, `is_default`, `label`, `type_of_work_id`, `org_unit_id`) to create a new rate of pay as part of the employment period creation. Both modes can be mixed in the same request. When both `id` and inline fields are provided, `id` takes precedence and the inline fields are silently ignored. When creating rates inline, set `is_default` to `true` if the rate should be treated as the default rate for holiday pay derivation (requires the default rates feature to be enabled). For organisations with UK minimum wage validation enabled, an inline GBP rate below the apprentice-rate hard floor (£7.55, £8.00 from April 2026) is rejected with a 422 naming the required amount, and no employment period is created. This floor is not age-banded, so a rate that is legal for an apprentice but below the National Living Wage for the employee's age is still accepted. A salaried rate is judged on its hourly equivalent, derived from the `contracted_hours` in the same request. A currency that is not a recognised three-letter ISO 4217 code is also rejected with a 422. Contracted hours are bounded by the length of the period: at most 168 hours for `weekly`, 744 for `monthly` and `calendar_month_daily`, and 24 per day of `rolling_period_sequence_length_days` for `rolling_sequence`. Hours beyond that are rejected with a 422, and no employment period is created. ## Contract Every employment period belongs to a contract, identified by `contract_id`. **Organisations without the overlapping employment periods feature** manage contracts automatically: the new period joins the person's existing contract (inherited from their latest employment period), or starts a new contract if it is their first period. Neither `contract_id` nor `starts_new_contract` may be passed. **Organisations with the feature enabled** must state the request's intent, one of: - `contract_id` (with `starts_new_contract` false or omitted): the new period continues that contract. The value must be the `contract_id` of one of the same person's existing employment periods. - `starts_new_contract: true`: the new period starts a new contract with an auto-generated `contract_id` — which must therefore not be passed. The contract is fixed at creation and cannot be changed via the update endpoint. Two more rules apply to organisations with the feature enabled: - Employment periods on different contracts whose dates overlap must share one `employee_number`. A different number is rejected with a 422 on `employee_number`. An employment period without an employee number neither constrains nor is constrained. - To continue a contract, first set a `finish_date` on that contract's current employment period with `PUT /api/v1/employment_periods/{employment_period_id}`, then create the new period with that `contract_id`. While the current employment period has no `finish_date`, a later one is rejected with a 422 on `employee_number`: "Please, close the latest existing employment period before creating a new one." Another contract that is still running does not block it. To end one contract while the others continue, set a `finish_date` on its current employment period. `PUT /api/v1/users/{user_id_or_idx}/terminate` ends every contract. ### Request body The employment_period parameters - `active` (boolean): The employment period's own `active` flag (not the user's). On create it is saved, defaulting to false. On update it is not saved: sending `false` only skips reconciling the period's types of work, which the leaver process relies on. - `continuous_employment_start_date` (string, date, required) - `contract_id` (string): The unique Sona identifier of the contract the new employment period continues (only on creation; ignored on update). Requires the overlapping employment periods feature to be enabled for the organisation. Must be the contract_id of one of the same person's existing employment periods, and is required when starts_new_contract is false or omitted. Must not be passed with starts_new_contract: true. Organisations without the feature must omit it: the contract is managed automatically. - `employee_number` (string): Unique number of the employee who is working this employment_period - `employment_specs` (array of `EmploymentSpecParams`): Related employment specs. Required on create (at least one must be provided with contract_types, rates_of_pay, and contracted_hours). Optional on update. - `contract_types` (array of `ContractTypeParams`): Required. Exactly one contract type must be provided per employment spec. - `booking_granularity` (string; one of `full_day`, `half_day`, `full_hour`, `half_hour`, `quarter_hour`, `minute`): Granularity for annual leave booking - `denomination` (string; one of `hours`, `days`): The denomination for holiday allowances - `employment_type` (string; one of `full_time`, `part_time`, `flexible`): The employment type classification for this contract. This field is a property of the contract type, not a top-level field on the employment period. - `holiday_allowance_rounding` (string; one of `full_day`, `half_day`, `full_hour`, `half_hour`, `quarter_hour`, `minute`): Rounding method for calculating holiday allowance - `id` (string, required): Sona unique identifier - `include_absence_cost` (boolean): Whether paid absence days contribute to wage cost calculations - `name` (string): The name of the contract type - `only_worked_days` (boolean): Whether salary distributions should consider only days worked - `pay_period_start` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`): The day of the week the pay period starts - `pay_period_type` (string; one of `weekly`, `monthly`): The type of pay period - `payroll_rule` (string; one of `hourly`, `fixed_salary`): How the pay is paid - `worked_days_in_year` (number): Number of worked days in the year to distribute salary across when only worked days are enabled (supports decimals) - `worked_weeks_in_year` (number): Number of worked weeks in the year to distribute salary across for weekly salary distribution (supports decimals) - `contracted_hours` (array of `ContractedHourParams`): Related contracted hours. Required on create (at least one must be provided). Not updatable via this endpoint. - `days` (number): Total number of contracted days in a period - `finish_date` (string, date): The date from when these contracted hours are no longer applicable - `hours` (number, required): Total number of contracted hours in a period - `id` (string): The unique Sona identifier of the contracted hours record. Not used on create. - `idx` (string): An external identifier of the contracted hours. - `monthly_period_starts_at` (integer): The day of the month a monthly period starts at - `org_unit_id` (string): The Sona unique identifier of the org unit these hours apply to - `period` (string; one of `weekly`, `monthly`, `rolling_sequence`, `calendar_month_daily`, required): The type of period e.g. weekly / monthly - `rolling_period_sequence_length_days` (integer): The length of rolling sequence in days - `rolling_period_sequence_start` (string, date): When the rolling sequence starts - `start_date` (string, date, required): The date from when these contracted hours are applicable - `type_of_work_id` (string): The Sona unique identifier of the type of work these hours apply to - `weekly_period_starts_at` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`): The day of the week a weekly period starts at - `id` (string): The unique Sona identifier of the employment_spec - `rates_of_pay` (array of `RateOfPayParams`): Related rates of pay. Required on create (at least one must be provided). Not updatable via this endpoint. - `amount` (integer): Amount represented in lowest value in given currency, pennies/cents (e.g. amount: 1241 represents 12.41, 500 represents 5.00) - `currency` (string): The currency used - `finish_date` (string, date): The date from when this rate of pay is no longer applicable - `id` (string): The unique Sona identifier of an existing rate of pay. Required when referencing an existing rate; omit when creating a new rate inline. - `idx` (string): An external identifier of the rate of pay. - `is_default` (boolean): Whether this is a default rate of pay - `label` (string): A label for the rate of pay - `org_unit_id` (string): The unique Sona identifier of the org_unit - `pay_type` (string; one of `hourly`, `salaried`): How it is paid, e.g. hourly / salaried - `start_date` (string, date): The date from when this rate of pay is applicable - `type_of_work_id` (string): The unique Sona identifier of the type_of_work - `finish_date` (string, date): The finish date for this period of employment - `forces_new_holiday_breakdown` (boolean): Whether to create a new holiday breakdown starting with this employment period. Defaults to true if the new employment period is for a rehire. - `groups` (array of `Group`): related groups - `id` (string, required): The unique Sona identifier of the Group record - `name` (string): The name of the group - `id` (string): The unique Sona identifier of the employment_period. Auto-generated on create. - `idx` (string): External identifier of the employment_period - `job_grade_id` (string): The unique Sona identifier of the job grade - `job_id` (string): The unique Sona identifier of the job - `level_id` (string): The unique Sona identifier for the employee's level - `pay_group_id` (string): Sona's unique identifier or external ID of the employee's pay group. Only honoured on creation; ignored on update. - `permission_roles` (array of object): Permission roles to assign, by Sona unique identifier - `id` (string, required): Sona unique identifier of the permission role - `primary_org_unit_id` (string): The Sona unique identifier of the primary org unit. Pass either this or `primary_org_unit_idx`. - `primary_org_unit_idx` (string): External ID of the primary org unit, as an alternative to `primary_org_unit_id`. - `probation_end_date` (string, date): The date that probation ends - `reports_to_user_id` (string): The unique Sona identifier of the **user** (not the organisation_user) to whom the employee reports. Use the `user_id` from the `/api/v1/users` endpoint. - `start_date` (string, date, required): The start date for this period of employment - `starter_declaration` (string; one of `A`, `B`, `C`): Starter declaration (https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1106535/Starter_checklist.pdf) - `starts_new_contract` (boolean): Whether the new employment period starts a new contract with an auto-generated contract_id (only on creation). Requires the overlapping employment periods feature to be enabled for the organisation. When false or omitted, contract_id is required. Organisations without the feature must omit it. - `student_loan_plan_five` (boolean): Employee has student loan plan five - `student_loan_plan_four` (boolean): Employee has student loan plan four - `student_loan_plan_one` (boolean): Employee has student loan plan one - `student_loan_plan_two` (boolean): Employee has student loan plan two - `student_loan_post_graduate` (boolean): Employee has post graduate student loan plan - `termination_reason_details` (string): The reason why this user's period of employment has ended - `termination_reason_id` (string): The unique Sona identifier of the termination reason - `termination_would_rehire` (boolean): Whether or not this person is eligible for rehiring following termination - `tupe` (boolean): Whether the employee has transferred under TUPE regulations - `types_of_work` (array of object): Types of work to assign, by Sona unique identifier - `id` (string, required): Sona unique identifier of the type of work - `user_id` (string, required): The unique Sona identifier of the user entity ### Responses **200** Success, employment_period created - `active` (boolean, required): Whether or not the user is currently active - `continuous_employment_start_date` (string, date, required) - `contract_id` (string): The unique Sona identifier of the contract this employment period belongs to. Employment periods sharing a contract_id are versions of the same contract. - `employee_number` (string, required): Unique number of the employee who is working this employment_period - `employment_specs` (array of `EmploymentSpec`): related employment specs - Same fields as `EmploymentSpec` above. - `finish_date` (string, date, required): The finish date for this period of employment, or null when it is open-ended - `forces_new_holiday_breakdown` (boolean): Whether to create a new holiday breakdown starting with this employment period. Defaults to true if the new employment period is for a rehire. - `groups` (array of `Group`): related groups - Same fields as `Group` above. - `id` (string, required): The unique Sona identifier of the employment_period - `idx` (string): External identifier of the employment_period - `job_id` (string, required): The unique Sona identifier of the job - `level_id` (string, required): The unqiue Soan identifier of the level - `organisation_id` (string): Sona unique identifier of the organisation - `organisation_user_id` (string): [DEPRECATED] For backwards compatible reason, this parameter is still available. The unique Sona identifier of the organisation user record - `permission_roles` (array of `PermissionRole`): related permission roles - Same fields as `PermissionRole` above. - `primary_org_unit` (object): User's primary org unit, or null when none is set - `id` (string): The unique Sona identifier for the org unit - `name` (string): The name of the org unit - `reports_to_org_user` (object): Manager information, or null when the employee has no manager - `id` (string): The unique Sona identifier for the manager org user - `name` (string): Manager's first and last name - `work_email` (string): Manager's work email address - `reports_to_user_id` (string, required): The unique Sona identifier of the user to whom the employee reports - `start_date` (string, date, required): The start date for this period of employment - `starter_declaration` (string; one of `A`, `B`, `C`, required): Starter declaration (https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1106535/Starter_checklist.pdf), or null when not recorded - `student_loan_plan_five` (boolean, required): Employee has student loan plan five - `student_loan_plan_four` (boolean, required): Employee has student loan plan four - `student_loan_plan_one` (boolean, required): Employee has student loan plan one - `student_loan_plan_two` (boolean, required): Employee has student loan plan two - `student_loan_post_graduate` (boolean, required): Employee has post graduate student loan plan - `termination_reason` (`EmploymentPeriodTerminationReason`): The reason this user's employment was terminated, or null when not terminated - Same fields as `EmploymentPeriodTerminationReason` above. - `termination_would_rehire` (boolean): Whether or not this person is eligible for rehiring following termination - `tupe` (boolean): Whether the employee has transferred under TUPE regulations - `types_of_work` (array of `TypeOfWork`): related types of work - Same fields as `TypeOfWork` above. - `updated_at_utc` (string, date-time): When the employment period was last updated, in UTC **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** The user or another referenced resource was not found — `ErrorResponse` (see the overview) **422** Unable to create an employment_period with these parameters. `message` is a string for rejections such as an open employment period already existing on the contract, or an object mapping field names to lists of messages for field validation failures (e.g. `{"employment_specs": ["is required"]}`). — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/employment_periods" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "active": true, "continuous_employment_start_date": "2019-07-20", "employee_number": "A568675", "employment_specs": [ { "contract_types": [ { "id": "159600fb-3fa9-4687-bbc9-196375de6ee3" } ], "contracted_hours": [ { "days": 10, "finish_date": "2020-04-20", "hours": 30.0, "idx": "external id", "monthly_period_starts_at": 1, "org_unit_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "period": "weekly", "rolling_period_sequence_length_days": 20, "rolling_period_sequence_start": "2024-03-01", "start_date": "2019-04-20", "type_of_work_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "weekly_period_starts_at": "tuesday" } ], "rates_of_pay": [ { "id": "159600fb-3fa9-4687-bbc9-196375de6ee3" }, { "amount": 1000, "currency": "GBP", "finish_date": "2025-04-20", "idx": "external id", "is_default": true, "pay_type": "salaried", "start_date": "2019-04-20" } ] } ], "finish_date": "2020-04-20", "forces_new_holiday_breakdown": false, "idx": "external idx", "job_grade_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "job_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "level_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "permission_roles": [ { "id": "84f0d476-f2d6-43c4-adbf-b7f952b0af5f" }, { "id": "18c32388-0a1e-442a-9a2f-3e0621cc068a" } ], "primary_org_unit_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "probation_end_date": "2019-07-20", "reports_to_user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "start_date": "2019-04-20", "starter_declaration": "A", "student_loan_plan_five": false, "student_loan_plan_four": false, "student_loan_plan_one": true, "student_loan_plan_two": false, "student_loan_post_graduate": false, "tupe": false, "types_of_work": [ { "id": "3214f01b-7264-4c79-846a-8583a75e1d70" }, { "id": "15bb52f6-7f37-4ac8-84ee-71fbca32106d" } ], "user_id": "7fe0866b-9631-42cb-94b1-d8e426ac0fb0" }' ``` ## Delete an employment period `DELETE /api/v1/employment_periods/{employment_period_id_or_idx}` Delete a future dated employment period. Currently applying employment periods cannot be deleted. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `employment_period_id_or_idx` | path | string | yes | Sona unique identifier of the employment period or external id | ### Responses **204** No Content **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Employment period not found — `ErrorResponse` (see the overview) **422** The employment period can't be deleted, e.g. because it already applies. `message` is an object mapping the failing record to its field errors. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/employment_periods/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update an individual employment_period `PUT /api/v1/employment_periods/{employment_period_id}` Update an individual employment period based on employment_period_id OR the external idx. ## Rates of Pay Rates of pay within an employment spec support two modes: 1. **Reference an existing rate**: provide `{"id": ""}` to link a pre-existing rate of pay to the employment spec. 2. **Create a new rate inline**: omit `id` and provide `amount`, `currency`, `start_date`, `pay_type` (and optionally `finish_date`, `idx`, `is_default`, `label`, `type_of_work_id`, `org_unit_id`) to create a new rate of pay as part of the employment period update. Both modes can be mixed in the same request. When both `id` and inline fields are provided, `id` takes precedence and the inline fields are silently ignored. When creating rates inline, set `is_default` to `true` if the rate should be treated as the default rate for holiday pay derivation (requires the default rates feature to be enabled). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `employment_period_id` | path | string | yes | Sona unique identifier of the employment_period | | `idx` | query | string | no | External identifier of the employment_period | ### Request body The employment_period parameters Same fields as `EmploymentPeriodParameters` above. ### Responses **200** Success, employment_period updated Same fields as `EmploymentPeriod` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** An employment_period with that ID was not found — `ErrorResponse` (see the overview) **422** Unable to update employment_period with these parameters. `message` is an object mapping field names to lists of messages for field validation failures, or a string for other rejections. — `DetailedErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/employment_periods/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "active": true, "continuous_employment_start_date": "2019-07-20", "employee_number": "A568675", "employment_specs": [ { "contract_types": [ { "id": "159600fb-3fa9-4687-bbc9-196375de6ee3" } ], "contracted_hours": [ { "days": 10, "finish_date": "2020-04-20", "hours": 30.0, "idx": "external id", "monthly_period_starts_at": 1, "org_unit_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "period": "weekly", "rolling_period_sequence_length_days": 20, "rolling_period_sequence_start": "2024-03-01", "start_date": "2019-04-20", "type_of_work_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "weekly_period_starts_at": "tuesday" } ], "rates_of_pay": [ { "id": "159600fb-3fa9-4687-bbc9-196375de6ee3" }, { "amount": 1000, "currency": "GBP", "finish_date": "2025-04-20", "idx": "external id", "is_default": true, "pay_type": "salaried", "start_date": "2019-04-20" } ] } ], "finish_date": "2020-04-20", "forces_new_holiday_breakdown": false, "idx": "external idx", "job_grade_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "job_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "level_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "permission_roles": [ { "id": "84f0d476-f2d6-43c4-adbf-b7f952b0af5f" }, { "id": "18c32388-0a1e-442a-9a2f-3e0621cc068a" } ], "primary_org_unit_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "probation_end_date": "2019-07-20", "reports_to_user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "start_date": "2019-04-20", "starter_declaration": "A", "student_loan_plan_five": false, "student_loan_plan_four": false, "student_loan_plan_one": true, "student_loan_plan_two": false, "student_loan_post_graduate": false, "tupe": false, "types_of_work": [ { "id": "3214f01b-7264-4c79-846a-8583a75e1d70" }, { "id": "15bb52f6-7f37-4ac8-84ee-71fbca32106d" } ], "user_id": "7fe0866b-9631-42cb-94b1-d8e426ac0fb0" }' ``` --- # EmploymentSpecs Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Bulk update employment spec contract types `POST /api/v1/employment_specs/contract_type/bulk` Update contract types for multiple employment specs. ### Request body Employment spec contract type updates Array of `EmploymentSpecContractTypeBulkUpdateItem`: - `new_contract_type_id` (string, required): Sona unique identifier of the contract type - `spec_id` (string, required): Sona unique identifier of the employment_spec ### Responses **200** Success - `errors` (array of object) - `employment_spec_id` (string): Employment spec identifier for update-time errors - `errors` (object): Errors for the failed update - `index` (integer): Index of the failed update in the request array - `updated_count` (integer): Number of employment specs updated **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/employment_specs/contract_type/bulk" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '[ { "new_contract_type_id": "e27ad021-af0e-4ccf-95e8-1c7c84656c93", "spec_id": "159600fb-3fa9-4687-bbc9-196375de6ee3" } ]' ``` ## Update an employment spec contract type `PUT /api/v1/employment_specs/{employment_spec_id}/contract_type` Update the contract type for an employment spec. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `employment_spec_id` | path | string | yes | Sona unique identifier of the employment_spec | ### Request body The employment spec contract type parameters - `contract_type_id` (string, required): Sona unique identifier of the contract type ### Responses **200** Success - `contract_types` (array of `ContractType`): Related contract types. Use `api/v1/contract_types` endpoint to retrieve available Contract Types. Each contract type includes an `employment_type` field with values: `full_time`, `part_time`, or `flexible`. - `booking_granularity` (string; one of `full_day`, `half_day`, `full_hour`, `half_hour`, `quarter_hour`, `minute`): Granularity for annual leave booking - `denomination` (string; one of `hours`, `days`): The denomination for holiday allowances - `employment_basis` (string): Free-text employment basis label, or null when not set - `employment_type` (string; one of `full_time`, `part_time`, `flexible`): The employment type classification for this contract. This field is a property of the contract type, not a top-level field on the employment period. - `holiday_allowance_rounding` (string; one of `full_day`, `half_day`, `full_hour`, `half_hour`, `quarter_hour`, `minute`): Rounding method for calculating holiday allowance - `id` (string, required): Sona unique identifier - `include_absence_cost` (boolean): Whether paid absence days contribute to wage cost calculations - `include_absence_hours` (boolean): Whether absence hours count towards the wage cost hours metric. Only has an effect when include_absence_cost is true - `name` (string, required): The name of the contract type - `only_worked_days` (boolean): Whether salary distributions should consider only days worked - `pay_period_start` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`, required): The day of the week the pay period starts - `pay_period_type` (string; one of `weekly`, `monthly`, required): The type of pay period - `payroll_rule` (string; one of `hourly`, `fixed_salary`, required): How the pay is paid - `worked_days_in_year` (number): Number of worked days in the year to distribute salary across when only worked days are enabled (supports decimals) - `worked_weeks_in_year` (number): Number of worked weeks in the year to distribute salary across for weekly salary distribution (supports decimals) - `contracted_hours` (array of `ContractedHour`): related contracted hours - `colour_coding` (boolean): Whether to specially highlight contracted hours on roster - `days` (number, required): Total number of contracted days in a period, or null when not set - `employment_period_id` (string): Sona unique identifier of the employment period these hours belong to. Only returned by the `/api/v1/contracted_hours` endpoints. - `employment_period_idx` (string): External identifier of the employment period these hours belong to. Only returned by the `/api/v1/contracted_hours` endpoints. - `finish_date` (string, date, required): The date from when these contracted hours are no longer applicable, or null when open-ended - `hours` (number, required): Total number of contracted hours in a period - `id` (string, required): The unique Sona identifier of the contracted hours record - `idx` (string): An external identifier of the contracted hours. - `monthly_period_starts_at` (integer, required): The day of the month a monthly period starts at - `org_unit_id` (string): The Sona unique identifier of the org unit these hours apply to - `org_units` (array of object): The org units these hours apply to. Only returned by the `/api/v1/contracted_hours` endpoints. - `id` (string): Sona unique identifier of the org unit - `period` (string; one of `weekly`, `monthly`, `rolling_sequence`, `calendar_month_daily`): The type of period - `rolling_period_sequence_length_days` (integer): The length of rolling sequence in days - `rolling_period_sequence_start` (string, date): When the rolling sequence starts - `start_date` (string, date, required): The date from when these contracted hours are applicable - `type_of_work_id` (string): The Sona unique identifier of the type of work these hours apply to - `types_of_work` (array of object): The types of work these hours apply to. Only returned by the `/api/v1/contracted_hours` endpoints. - `id` (string): Sona unique identifier of the type of work - `weekly_period_starts_at` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`, required): The day of the week a weekly period starts at - `id` (string, required): The unique Sona identifier of the employment_spec - `rates_of_pay` (array of `EmploymentPeriodRateOfPay`): related rates of pay. Use `api/v1/rates_of_pay endpoint to retrieve available Rates of Pay - `finish_date` (string, date, required): The date from when this rate of pay is no longer applicable, or null when open-ended - `id` (string, required): The unique Sona identifier of the rate of pay - `idx` (string): An external identifier of the rate of pay. - `is_default` (boolean): Whether this is a default rate of pay - `label` (string): A label for the rate of pay - `org_unit_id` (string): The unique Sona identifier of the org_unit - `pay_type` (string; one of `hourly`, `salaried`): How it is paid, e.g. hourly / salaried - `rate` (string, required): The payment rate, formatted with its currency symbol (e.g. "£12.50") - `start_date` (string, date, required): The date from when this rate of pay is applicable - `type_of_work_id` (string): The unique Sona identifier of the type_of_work **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The contract type change failed validation (`message` is an object of field names to error lists), or recalculating holiday allowances failed (`message` is a string). — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/employment_specs//contract_type" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "contract_type_id": "e27ad021-af0e-4ccf-95e8-1c7c84656c93" }' ``` --- # ExternalMappings Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of external mappings `GET /api/v1/external_mappings` List external mappings for the current organisation ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `idx` | query | string | no | Filter by external identifier | | `source_type` | query | string | no | Filter by source type | ### Responses **200** Success - `entries` (array of `ExternalMapping`) - `absence_type_id` (string): Sona unique identifier of of an absence type - `contract_type_id` (string): Sona unique identifier of a contract type. Set only on `dayforce_pay_policy` mappings - `id` (string): The unique identifier of the external mapping - `idx` (string): An identifier for the external mapping - `job_id` (string): Sona unique identifier of a job - `level_id` (string): Sona unique identifier of a level - `org_unit_id` (string): Sona unique identifier of an org unit - `pay_group_id` (string): Sona unique identifier of a pay group - `permission_role_id` (string): Sona unique identifier of a permission role - `skill_attribute_id` (string): Sona unique identifier of a skill attribute - `source_type` (string): Type of source that external mapping is relates to - `termination_reason_id` (string): Sona unique identifier of a termination reason. Set only on `dayforce_termination_reason` mappings - `user_id` (string): Sona unique identifier of the user for this external mapping - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/external_mappings" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a new external mapping `POST /api/v1/external_mappings` ### Request body The external mapping parameters - `absence_type_id` (string): Sona unique identifier of of an absence type - `contract_type_id` (string): Sona unique identifier of a contract type. Accepted only for `dayforce_pay_policy` mappings; ignored for other source types - `idx` (string, required): An identifier for the external mapping - `job_id` (string): Sona unique identifier of a job - `level_id` (string): Sona unique identifier of a level - `org_unit_id` (string): Sona unique identifier of an org unit - `pay_group_id` (string): Sona unique identifier of a pay group - `permission_role_id` (string): Sona unique identifier of a permission role - `skill_attribute_id` (string): Sona unique identifier of a skill attribute - `source_type` (string; one of `general`, `role_mapping_peoplexd`, `absence_peoplexd`, `pay_group_peoplexd`, `store_mapping_harri`, `oracle_hcm_user`, `oracle_hcm_org_unit`, `oracle_hcm_job`, `oracle_hcm_type_of_work`, `oracle_hcm_absence_type`, `oracle_hcm_pay_group`, `paycom_user`, `paycom_location`, `paycom_job`, `paycom_labor_job`, `paycom_level`, `paycom_permission_role`, `paycom_rate`, `paycom_punch`, `paycom_position`, `oracle_simphony_employee`, `oracle_simphony_location`, `oracle_simphony_job_code`, `oracle_simphony_timecard`, `assignment_idx`, `budget_allocation_idx`, `oracle_hcm_job_code`, `oracle_hcm_position_name`, `tao_oracle_hcm_location_department`, `sap_zalaris_employee`, `sap_zalaris_job`, `dayforce_employee`, `dayforce_location`, `dayforce_job`, `dayforce_job_type_of_work`, `dayforce_suspension_status`, `dayforce_pay_group`, `dayforce_pay_policy`, `dayforce_termination_reason`): Type of source the external mapping relates to. Defaults to `general` - `termination_reason_id` (string): Sona unique identifier of a termination reason. Accepted only for `dayforce_termination_reason` mappings; ignored for other source types - `user_id` (string): Sona unique identifier of the user for this external mapping ### Responses **201** Created Same fields as `ExternalMapping` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Field validation failed (`{errors}`), or an ID parameter is not a valid UUID (`{error, message}`, with `message` mapping the parameter to its error, e.g. `{"user_id": "invalid format"}`) — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/external_mappings" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "idx": "external_id_of_variable_type", "source_type": "general", "user_id": "bb000313-0497-443c-b05d-7c68a4f9e812" }' ``` ## Get a single external mapping `GET /api/v1/external_mappings/{id}` Get a single external mapping data. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | The Sona unique identifier of the external mapping. | ### Responses **200** Success Same fields as `ExternalMapping` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The `id` is not a valid UUID. `message` maps the parameter to its error, e.g. `{"id": "invalid format"}` — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/external_mappings/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update an external mapping `PUT /api/v1/external_mappings/{id}` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Sona unique identifier of external mapping | ### Request body The external mapping parameters - `idx` (string): An identifier for the external mapping - `source_type` (string; one of `general`, `role_mapping_peoplexd`, `absence_peoplexd`, `pay_group_peoplexd`, `store_mapping_harri`, `oracle_hcm_user`, `oracle_hcm_org_unit`, `oracle_hcm_job`, `oracle_hcm_type_of_work`, `oracle_hcm_absence_type`, `oracle_hcm_pay_group`, `paycom_user`, `paycom_location`, `paycom_job`, `paycom_labor_job`, `paycom_level`, `paycom_permission_role`, `paycom_rate`, `paycom_punch`, `paycom_position`, `oracle_simphony_employee`, `oracle_simphony_location`, `oracle_simphony_job_code`, `oracle_simphony_timecard`, `assignment_idx`, `budget_allocation_idx`, `oracle_hcm_job_code`, `oracle_hcm_position_name`, `tao_oracle_hcm_location_department`, `sap_zalaris_employee`, `sap_zalaris_job`, `dayforce_employee`, `dayforce_location`, `dayforce_job`, `dayforce_job_type_of_work`, `dayforce_suspension_status`, `dayforce_pay_group`, `dayforce_pay_policy`, `dayforce_termination_reason`): Type of source the external mapping relates to ### Responses **200** Success Same fields as `ExternalMapping` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** External mapping not found — `ErrorResponse` (see the overview) **422** Field validation failed (`{errors}`), or an ID parameter is not a valid UUID (`{error, message}`, with `message` mapping the parameter to its error, e.g. `{"user_id": "invalid format"}`) — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/external_mappings/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "idx": "external_id_of_variable_type", "source_type": "general" }' ``` --- # FeatureSet Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Gets the organisational unit features `GET /api/v1/org_units/{org_unit_id}/features` Returns the features of the organisational unit. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the organisational unit | ### Responses **200** Success - `linked_shift_attendance_required` (boolean, required): Whether or not the attendance is linked to the shifts. - `shift_runs_enabled` (boolean, required): Whether or not shift runs are enabled. **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/org_units//features" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Creates new organisational unit features `POST /api/v1/org_units/{org_unit_id}/features` Creates the features for the organisational unit. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the organisational unit | ### Request body The organisational unit features parameters - `linked_shift_attendance_required` (boolean): Whether or not the attendance is linked to the shifts. - `shift_runs_enabled` (boolean): Whether or not shift runs are enabled. ### Responses **200** Success Same fields as `OrgUnitFeatureSet` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **409** Conflict — `ErrorResponse` (see the overview) **422** The feature set failed validation (`{"errors": {...}}`), or the org unit ID is not a valid UUID (`{"error", "message"}` with a string `message`). — `ErrorOrValidationErrors` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/org_units//features" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "linked_shift_attendance_required": false, "shift_runs_enabled": false }' ``` ## Updates the organisational unit features `PUT /api/v1/org_units/{org_unit_id}/features` Updates the features for the organisational unit. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the organisational unit | ### Request body The organisational unit features parameters Same fields as `OrgUnitFeatureSetParams` above. ### Responses **200** Success Same fields as `OrgUnitFeatureSet` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The feature set failed validation (`{"errors": {...}}`), or the org unit ID is not a valid UUID (`{"error", "message"}` with a string `message`). — `ErrorOrValidationErrors` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/org_units//features" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "linked_shift_attendance_required": false, "shift_runs_enabled": false }' ``` ## Deletes the organisational unit features `DELETE /api/v1/org_units/{org_unit_id}/features` Deletes the features for the organisational unit. Succeeds with 204 even if the organisational unit has no features or does not exist, so it is safe to repeat. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the organisational unit | ### Responses **204** Success **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** The org unit ID is not a valid UUID (`ErrorResponse`), or the features could not be deleted (`ValidationErrors`) — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/org_units//features" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # ForecastRuns Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Record the start of a forecast run `POST /api/v1/forecast_runs` Records that a forecast run has started for the authenticated organisation. Returns the forecast run id which the caller must pass back to a terminal endpoint. ### Request body The forecast run start parameters - `started_at` (string, required): ISO8601 timestamp when the forecast started - `target` (string, required): The forecast target ### Responses **200** Success, forecast run started - `data` (`ForecastRun`): The forecast run record - `ended_at` (string): ISO8601 timestamp; null while running - `error_message` (string): Failure detail; null unless failed - `id` (string): The forecast run id - `organisation_id` (string): The organisation id - `started_at` (string): ISO8601 timestamp - `status` (string; one of `running`, `succeeded`, `failed`): Forecast run status - `target` (string): The forecast target - `train_wape` (string): Train WAPE percentage between 0 and 100, as a decimal string; null until known **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unable to start a forecast run with these parameters — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/forecast_runs" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "started_at": "2026-04-27T09:00:00Z", "target": "revenue" }' ``` ## Record the completion of a forecast run `PATCH /api/v1/forecast_runs/{id}/complete` Marks a previously started forecast run as succeeded. `train_wape` is optional at completion; use the train WAPE endpoint to backfill it later. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | The forecast run id returned by the start endpoint | ### Request body The forecast run completion parameters - `ended_at` (string, required): ISO8601 timestamp when the forecast finished - `train_wape` (number): Train WAPE percentage between 0 and 100 ### Responses **200** Success, forecast run updated Same fields as `ForecastRunResponse` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Forecast run not found — `ErrorResponse` (see the overview) **422** Unable to complete the forecast run with these parameters — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PATCH "https://api.sona.is/api/v1/forecast_runs//complete" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "ended_at": "2026-04-27T09:14:32Z", "train_wape": 7.66 }' ``` ## Record the failure of a forecast run `PATCH /api/v1/forecast_runs/{id}/fail` Marks a previously started forecast run as failed and optionally records an error message. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | The forecast run id returned by the start endpoint | ### Request body The forecast run failure parameters - `ended_at` (string, required): ISO8601 timestamp when the forecast failed - `error_message` (string): Optional failure detail ### Responses **200** Success, forecast run updated Same fields as `ForecastRunResponse` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Forecast run not found — `ErrorResponse` (see the overview) **422** Unable to fail the forecast run with these parameters — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PATCH "https://api.sona.is/api/v1/forecast_runs//fail" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "ended_at": "2026-04-27T09:14:32Z", "error_message": "Forecast pipeline timed out" }' ``` ## Backfill forecast train WAPE `PATCH /api/v1/forecast_runs/{id}/train_wape` Updates the train WAPE for a succeeded forecast run without changing the completion timestamp. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | The forecast run id returned by the start endpoint | ### Request body The forecast run train WAPE parameters - `train_wape` (number, required): Train WAPE percentage between 0 and 100 ### Responses **200** Success, forecast run updated Same fields as `ForecastRunResponse` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Forecast run not found — `ErrorResponse` (see the overview) **422** Unable to update the forecast run with these parameters — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PATCH "https://api.sona.is/api/v1/forecast_runs//train_wape" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "train_wape": 7.66 }' ``` ## Get forecast runs `GET /api/v1/forecasts` Returns forecast runs for the authenticated organisation. Results are sorted by `started_at` descending. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `target` | query | string | no | Filter by forecast target | | `started_after` | query | string | no | Filter to runs started on or after this ISO8601 timestamp | | `started_before` | query | string | no | Filter to runs started on or before this ISO8601 timestamp | | `status` | query | string; one of `running`, `succeeded`, `failed` | no | Filter by status | | `train_wape` | query | string; one of `has`, `missing` | no | Filter by train WAPE presence | ### Responses **200** Success - `entries` (array of `ForecastRun`): Forecast run records - Same fields as `ForecastRun` above. - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/forecasts" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get a forecast run `GET /api/v1/forecasts/{id}` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | The forecast run id | ### Responses **200** Success Same fields as `ForecastRunResponse` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Forecast run not found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/forecasts/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # HolidayAdjustments Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of holiday adjustments `GET /api/v1/holiday_adjustments` Returns a list of holiday adjustments sorted by creation date. Allows filtering by `user_id`. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `user_id` | query | string | no | The user ID to filter by | ### Responses **200** Success - `entries` (array of `HolidayAdjustment`) - `active_at` (string): Date identifying when the adjustment is applied - `amount` (number, float): Number of hours/days to adjust - `contract_id` (string): The unique Sona identifier of the contract of the record's absence year, or null for an absence year without a contract - `id` (string): The unique Sona identifier of the rate of pay - `reason` (string): Reason for adjustment - `type` (string; one of `allowance`, `taken_holiday`): Type of adjustment - `units` (string; one of `hours`, `days`): Adjustment unit - `user_id` (string): The unique ID of the user the adjustment is for - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity: `{"errors": ...}` when a filter parameter is invalid; `{"error", "message"}` with `message` mapping parameter names to errors when pagination or sorting parameters are invalid. — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/holiday_adjustments" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a holiday adjustment `POST /api/v1/holiday_adjustments` ### Request body Holiday adjustment parameters - `active_at` (string): Date identifying when the adjustment is applied - `amount` (number, float, required): Number of hours or days to adjust. The unit is determined by the denomination of the absence year - `contract_id` (string): The Sona unique identifier of the contract the request is about, from `contract_id` on `GET /api/v1/employment_periods`. Only used by organisations with overlapping employment periods enabled. Required when the user holds employment periods on several contracts covering the requested date; without it the only contract covering that date is used. A contract whose employment periods have ended still reads its own absence year covering the date. - `reason` (string, required): Reason for adjustment - `type` (string; one of `allowance`, `taken_holiday`, required): Type of adjustment - `user_id` (string, required): The unique ID of the user the adjustment is for - `variation` (string; one of `add`, `deduct`, required): Add or deduct allowance, use `"add"` when creating adjustments of type `"taken_holiday"` ### Responses **200** Success Same fields as `HolidayAdjustment` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Employee not found — `ValidationErrors` (see the overview) **409** Holiday write in progress; retry after the Retry-After delay — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/holiday_adjustments" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "active_at": "2024-10-25", "amount": 1, "contract_id": "4f5c2a36-8f6e-4a0a-9d8e-2b1c7f3e6a51", "reason": "reason", "type": "allowance", "user_id": "0a727402-fcae-44ee-bb6e-b7d88e334e87", "variation": "add" }' ``` --- # HolidayBalance Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a user's holiday balance `GET /api/v1/holiday_balance/{user_id}` Retrieve the holiday balance for a specific user. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `user_id` | path | string | yes | The Sona unique identifier of the user. | | `date` | query | string, date | no | The date for which to show holiday balance | | `contract_id` | query | string | no | The Sona unique identifier of the contract the request is about, from `contract_id` on `GET /api/v1/employment_periods`. Only used by organisations with overlapping employment periods enabled. Required when the user holds employment periods on several contracts covering the requested date; without it the only contract covering that date is used. A contract whose employment periods have ended still reads its own absence year covering the date. | ### Responses **200** Success - `booked` (number): Number of holidays the user has booked - `contract_id` (string): The unique Sona identifier of the contract of the absence year, or null for an absence year without a contract - `denomination` (string): Unit of holiday measurement (e.g., days) - `pending` (number): Number of holidays pending approval for the user - `remaining` (number): Number of holidays remaining for the user - `taken` (number): Number of holidays the user has taken **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** No holiday balance could be found (`{error, message}`), or the user was not found in the organisation (`{"errors": {"user_id": ["not found"]}}`). — `ErrorOrValidationErrors` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/holiday_balance/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # InterpolationPreview Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Preview rule interpolation across a date range `POST /api/v1/org_units/{org_unit_id}/productivity/interpolation_preview` Resolves attribute expressions in productivity rules for each date in a range. Returns concrete rule parameters grouped by date, showing how scheduled attribute changes affect rule interpolation over time. **Rules source:** - If `rules` is provided, those rules are interpolated. - If `rules` is omitted, rules are loaded from the org unit's productivity config. **Date range limit:** `to_date - from_date` must not exceed 90 days. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | UUID of the organisational unit | ### Request body Preview parameters - `from_date` (string, required): Start date (ISO 8601, YYYY-MM-DD) - `rules` (array of object): Optional override rules with rule_type, rule_params, master_shift_type_id, metric_id - `strategy_id` (string): Optional UUID of a labour strategy. When set, rules are resolved per date from the strategy's group assignments (status-agnostic). Mutually exclusive with `rules`. - `to_date` (string, required): End date (ISO 8601, YYYY-MM-DD) ### Responses **200** Success - `dates` (array of object, required): List of date entries with attributes and concrete rules **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Org unit not found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/org_units//productivity/interpolation_preview" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "from_date": "2024-06-15", "rules": [ { "master_shift_type_id": "550e8400-e29b-41d4-a716-446655440002", "metric_id": "550e8400-e29b-41d4-a716-446655440003", "rule_params": { "ratio": "{avg_hourly_footfall} / 20" }, "rule_type": "inflexible_demand_linear" } ], "to_date": "2024-07-15" }' ``` --- # Jobs Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of jobs `GET /api/v1/jobs` A job represents a role or position within the organisation. Jobs can be archived when no longer in use. Use the archived query parameter to filter by active or archived jobs. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `archived` | query | string | no | Filter by whether the job has been archived or not. If omitted all jobs are returned. | ### Responses **200** Success - `entries` (array of `Job`) - `archived_at` (string): The timestamp the job was archived at in ISO8601 format - `id` (string, required): Sona unique identifier - `name` (string, required): The name of the job - `upload_id` (string, required): External identifier used during CSV bulk import (nullable) - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Invalid query parameters: `page` or `page_size` isn't an integer, or `archived` isn't a boolean — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/jobs" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get an individual job `GET /api/v1/jobs/{job_id}` Get an individual job. See the job listing endpoint for a description of jobs. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `job_id` | path | string | yes | Sona unique identifier of the job | ### Responses **200** Success Same fields as `Job` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/jobs/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # Levels Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of levels `GET /api/v1/levels` Levels represent a hierarchical classification within an organisation, such as seniority bands or pay grades. Levels can be nested via a parent reference to form a tree structure. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `Level`) - `id` (string, required): Sona unique identifier - `name` (string, required): The name of the level - `parent` (`Level`): The parent level - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/levels" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get an individual level `GET /api/v1/levels/{level_id}` Get an individual level. See the level listing endpoint for a description of levels ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `level_id` | path | string | yes | Sona unique identifier of the level | ### Responses **200** Success Same fields as `Level` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/levels/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # LockedPeriods Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of locked periods `GET /api/v1/locked_periods` Returns locked periods for the organisation, optionally filtered by date range and/or location. A locked period represents a time range during which the roster for a location is locked and no further changes can be made. Requires the period locking feature to be enabled for the organisation. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `start_datetime` | query | string | no | Start of the date range to filter by (inclusive). in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) | | `end_datetime` | query | string | no | End of the date range to filter by (inclusive). in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) | | `location_ids` | query | array of string | no | Filter to specific location (org unit) IDs. When omitted, all locations are returned. | ### Responses **200** Success - `entries` (array of `LockedPeriod`) - `end_datetime` (string, date-time, required): End of the locked period. in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `id` (string, required): Sona unique identifier - `org_unit_id` (string, required): Unique identifier of the location (org unit) - `organisation_id` (string, required): Unique identifier of the organisation - `start_datetime` (string, date-time, required): Start of the locked period. in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/locked_periods" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # MetricCalculations Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Lists per-customer metric calculations `GET /api/v1/orgs/{organisation_id}/metric-calculations` Returns every metric calculation belonging to the organisation, deduplicated by formula content. Each entry includes an `applied_at` array listing the org_units (one per restaurant / location) that use that exact calculation. When the same name maps to different formulas across configs — or differs in `binding_type` or `filter` of any variable — each variant appears as a separate entry with disjoint `applied_at` lists. Per-config display flags (`show_on_roster`, `key_metric`, etc.) are intentionally omitted. Each entry carries a normalised `ast` so consumers can evaluate the formula without parsing the Sona DSL themselves; calculations whose formulas use syntax outside the supported grammar are omitted from the response (with a server-side log) rather than served partial — see the `ast` schema for the supported grammar. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `organisation_id` | path | string, uuid | yes | Sona unique identifier of the organisation. Must match the bearer token's organisation. | ### Responses **200** Success - `calculations` (array of `MetricCalculationEntry`, required): Distinct metric calculations, deduplicated by content - `aggregation_behaviour` (string; one of `sum`, `formula`, `none`, required): How the calculation aggregates across the rollup - `applied_at` (array of object, required): Sorted list of org_unit UUIDs whose productivity_config uses this calculation - `ast` (`MetricCalculationsAst`, required): Normalised JSON AST of the formula. See the schema for the node grammar. - `root` (object, required): Root AST node (recursive) - `canonical_metric_definition` (`MetricCalculationsCanonicalDefinition`): Canonical metric concept this calculation is mapped to (nullable). Null when the customer's admin has not assigned a canonical concept to this calc; the calc is then customer-specific and won't be referenced by cross-customer report definitions. - `deprecated_at` (string): ISO 8601 UTC timestamp when the canonical concept was soft-deprecated. Null for active concepts. - `description` (string, required): Semantic description of the canonical concept - `display_label` (string, required): Human-readable canonical label - `expected_unit` (string; one of `gbp`, `percentage`, `hours`, `items`, required): Canonical numeric unit - `replacement_slug` (string): Slug of the active replacement concept; populated only when `deprecated_at` is non-null AND a replacement was wired pointing at an active concept. - `slug` (string, required): Stable canonical identifier (lowercase snake_case) - `expanded_formula` (string): Expanded form of the formula (often null) - `formula` (string, required): Raw Sona formula DSL string (e.g. `(wage_actual_cash / cascade_revenue) * 100`) - `name` (string, required): Display name of the calculation - `ratio_denominator_name` (string): Name of the denominator calculation when this is a ratio. See ratio_numerator_name for why we use names not UUIDs. - `ratio_numerator_name` (string): When this calculation is a ratio, the name of the numerator calculation. Sona's data model gives each productivity_config its own UUID for the same logical calc, so deduplication compares by name (semantic identity) not UUID. - `unit` (string; one of `items`, `gbp`, `people`, `percentage`, `usd`, `eur`, `count`, required): Numeric unit - `variables` (array of `MetricCalculationVariable`, required): Variable bindings used by this calculation's formula - `aggregation_behaviour` (string): Per-metric rollup behaviour (`sum`, `max`, `none`). Distinct from the entry-level `aggregation_behaviour`. Present only on metric-bound variables. - `binding_type` (string; one of `actual`, `system_forecasted`, `human_forecasted`, `budget`, `cascade`, `last_year`): Which leg of stored data the binding refers to. Only present on metric-bound variables. - `calc_name` (string): Name of another calculation this variable resolves to. Null when the variable references a raw metric. - `canonical_slug` (string): Canonical slug of the referenced calculation when the variable is calc-bound AND the referenced calc has a canonical mapping. Null when the variable is metric-bound OR the referenced calc has no canonical mapping. Lets cross-customer report definitions resolve recursive-calc references by canonical slug without re-walking the catalogue. - `cascade_order` (array of object): Ordered cascade leg names. Present only when `binding_type` is `"cascade"`. Resolution order: try the first leg; if no value, try the next; etc. - `filter` (`MetricCalculationsFilter`): Polymorphic filter scoping the metric to specific time intervals or types of work. Null when no filter is set; absent for calc-bound variables. - `attribute_id` (string): `Attribute` UUID the filter scopes to. Present when `type` is `attribute_ref` or `time_interval_attribute_ref`. - `interval` (`MetricCalculationsTimeIntervalPayload`): Time-interval payload. Present when `type` is one of `time_interval`, `time_interval_types_of_work`, `time_interval_attribute_ref`, `time_interval_master_shift_type_group_ref`. - `master_shift_type_group_id` (string): `MasterShiftTypeGroup` UUID the filter scopes to. Present when `type` is `master_shift_type_group_ref` or `time_interval_master_shift_type_group_ref`. - `master_shift_type_ids` (array of object): List of `master_shift_type` UUIDs the filter scopes to. Present when `type` is `master_shift_types`. - `name` (string, required): Display name of the filter (`MetricFilter.name`) - `type` (string; one of `time_interval`, `types_of_work`, `master_shift_types`, `time_interval_types_of_work`, `attribute_ref`, `time_interval_attribute_ref`, `master_shift_type_group_ref`, `time_interval_master_shift_type_group_ref`, required): Filter discriminator - `type_of_work_ids` (array of object): List of `type_of_work` UUIDs the filter scopes to. Present when `type` is `types_of_work` or `time_interval_types_of_work`. - `granularity_minutes` (integer): Storage interval of the metric in minutes (one of 15, 30, 60, 120, 180, 1440). Present only on metric-bound variables. - `metric_id` (string, uuid): UUID of the underlying metric this variable resolves to. Null when the variable references another calculation. - `metric_name` (string): Display name of the underlying metric (`Metric.name`). Present only on metric-bound variables. - `metric_type` (string): Sona metric type enum (e.g. `wage_cost`, `hours`, `hourly_pay`). Present only on metric-bound variables. - `unit` (string): Unit of the underlying metric (`items`, `gbp`, `people`, `percentage`, `usd`, `eur`). Present only on metric-bound variables. - `variable` (string, required): Variable name as referenced in the formula string (e.g. `wage_actual_cash`) - `fetched_at` (string, required): ISO 8601 UTC timestamp when this response was generated - `org_calendar` (`MetricCalculationsOrgCalendar`, required): Calendar config — week start, day-boundary time, fiscal-year start. Consumers MUST use this with `calculations` as a single snapshot. - `day_start_time` (string, required): Day-boundary time in 24-hour HH:MM form - `fiscal_year_start` (string, required): Fiscal-year start as MM-DD (no year component) - `week_start_day` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`, required): Lowercase weekday token marking the first day of the week - `organisation_id` (string, uuid, required): UUID of the organisation - `reporting_label_preference` (string; one of `canonical`, `display_name`, required): Organisation-level preference for the label rendered in reports. `display_name` = render the customer's preferred calc display name; `canonical` = render the canonical concept's `display_label`. Existing customers backfill to `display_name`; new customers default to `canonical`. - `schema_version` (integer; one of `5`, required): Grammar version of the response payload **401** Unauthorised — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/orgs//metric-calculations" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # Metrics Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of Metrics `GET /api/v1/metrics` List the metric definitions configured for the organisation. Use this to resolve a metric's `id` for the `metric_id` filter on `GET /api/v1/metric_values` and for the `/api/v1/metrics/{metric_id}/store` endpoints, rather than hardcoding metric ids. Metrics are configured per organisation and `name` is customer-defined, so `name` is not stable across organisations. Where Sona recognises a metric as one of its standard ones, `type` carries that meaning from a fixed vocabulary and is stable; `type` is null for metrics the customer defined themselves, so match on `id` and fall back to `name` for those. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `Metric`) - `calculation_interval_minutes` (integer; one of `15`, `30`, `60`, `120`, `180`, `1440`): The size of the buckets the metric is recorded in, in minutes - `has_actuals` (boolean): Whether the organisation records actual values - `has_human_forecasts` (boolean): Whether the organisation records human-forecasted (manager) values - `has_last_year` (boolean): Whether Sona records `last_year` values for this metric (the actual from the same date one year earlier, stored as its own value type) - `has_system_forecasts` (boolean): Whether the organisation records system-forecasted values - `id` (string, required): Sona unique identifier of the metric - `name` (string, required): Customer-defined label for the metric, e.g. "Food Revenue". Not stable across organisations — match on `type` instead if you need to identify what the metric represents. - `type` (string): The metric's meaning, from a fixed vocabulary, where Sona recognises it as one of its standard metrics. Stable across organisations, but null for metrics the customer defined themselves — which includes most stored revenue metrics. - `unit` (string; one of `items`, `gbp`, `people`, `percentage`, `usd`, `eur`): The unit the metric is measured in - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/metrics" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Store the values of a given metric for certain times `POST /api/v1/metrics/{metric_id}/store` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `metric_id` | path | string | yes | Metric id | | `org_unit_id` | query | string | yes | The org unit id | | `type` | query | string | yes | The type of the metric | ### Request body The metrics to be stored Array of `MetricValueParams`: - `id` (string) - `timestamp` (string) - `value` (number) ### Responses **200** Success - `items` (array of `MetricValue`) - `current` (boolean): Whether this is the current value for its timestamp - `current_finish_timestamp` (string): When this value stopped being current (ISO 8601, UTC); null while current - `current_start_timestamp` (string): When this value became current (ISO 8601, UTC) - `id` (string): Sona unique identifier of the stored value - `metric_id` (string): Sona unique identifier of the metric - `org_unit_id` (string): Sona unique identifier of the org unit - `organisation_id` (string): Sona unique identifier of the organisation - `timestamp` (string): Start of the bucket the value is for (ISO 8601, UTC) - `type` (string; one of `actual`, `system_forecasted`, `human_forecasted`, `budget`, `last_year`): The value type - `value` (number) **400** Bad Request - invalid type or data — `MetricsErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **404** Not Found - metric or org unit not found — `MetricsErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/metrics//store?org_unit_id=&type=" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '[ { "timestamp": "2024-06-15T07:00:00Z", "value": 12.5 } ]' ``` ## Store metric values for multiple org units in a single transaction `POST /api/v1/metrics/{metric_id}/store_bulk` Store metric values for multiple org units. Either all values are stored or none (all-or-nothing). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `metric_id` | path | string | yes | Metric id | ### Request body The metrics to be stored for multiple org units grouped by type - `data` (object): Map of type to org_unit_id to array of values ### Responses **200** Success - `total_inserted` (integer): Total number of metric values inserted **400** Bad Request — `MetricsErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **404** Not Found — `MetricsErrorResponse` (see the overview) **422** Unprocessable Entity — `MetricsErrorResponse` (see the overview) **500** Internal Server Error — `MetricsErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/metrics//store_bulk" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "data": { "actual": { "org_unit_id_1": [ { "timestamp": "2020-01-01T07:00:00Z", "value": 1.0 } ] }, "system_forecasted": { "org_unit_id_2": [ { "timestamp": "2020-01-01T07:00:00Z", "value": 2.0 } ] } } }' ``` ## Store historical metric values with a custom current_timestamp `POST /api/v1/metrics/{metric_id}/store_historical` Store metric values for historical backfill. Unlike /store, this endpoint requires a current_timestamp parameter to specify when the data was recorded. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `metric_id` | path | string | yes | Metric id | | `org_unit_id` | query | string | yes | The org unit id | | `type` | query | string | yes | The type of the metric | | `current_timestamp` | query | string | yes | The timestamp when this data was recorded (ISO8601 format, must be in the past) | ### Request body The metrics to be stored Same fields as `MetricValueParams` above. ### Responses **200** Success - `items` (array of `MetricValue`) - Same fields as `MetricValue` above. **400** Bad Request - invalid current_timestamp — `MetricsErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Forbidden: the token can't use this endpoint (`ErrorResponse`), or the values could not be stored (`MetricsErrorResponse`) — `MetricsForbiddenResponse` (see the overview) **404** Not Found - metric or org unit not found — `MetricsErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/metrics//store_historical?org_unit_id=&type=¤t_timestamp=" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '[ { "timestamp": "2024-06-15T07:00:00Z", "value": 12.5 } ]' ``` ## Store historical metric values for multiple org units with a custom timestamp `POST /api/v1/metrics/{metric_id}/store_historical_bulk` Store metric values for multiple org units with a custom current_timestamp for historical backfill. Similar to /store_bulk but requires a current_timestamp parameter. Either all values are stored or none (all-or-nothing transaction). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `metric_id` | path | string | yes | Metric id | | `current_timestamp` | query | string | yes | The timestamp when this data was recorded (ISO8601 format, must be in the past) | ### Request body The metrics to be stored for multiple org units grouped by type - `data` (object): Map of type to org_unit_id to array of values ### Responses **200** Success - `total_inserted` (integer): Total number of metric values inserted **400** Bad Request - invalid current_timestamp or data — `MetricsErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **404** Not Found - metric or org unit not found — `MetricsErrorResponse` (see the overview) **422** Unprocessable Entity — `MetricsErrorResponse` (see the overview) **500** Internal Server Error — `MetricsErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/metrics//store_historical_bulk?current_timestamp=" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "data": { "actual": { "org_unit_id_1": [ { "timestamp": "2020-01-01T07:00:00Z", "value": 1.0 } ] }, "system_forecasted": { "org_unit_id_2": [ { "timestamp": "2020-01-01T07:00:00Z", "value": 2.0 } ] } } }' ``` ## Get the latest timestamp of a given metric `GET /api/v1/metrics/{metric_id}/{org_unit_id}/latest_timestamp` Returns the latest timestamp recorded for the specified metric and org unit. If nothing has been recorded, or the metric or org unit does not exist, `timestamp` is null. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `metric_id` | path | string | yes | Metric id | | `org_unit_id` | path | string | yes | The org unit id | | `type` | query | string | no | The type of the metric | ### Responses **200** Success - `timestamp` (string, date-time): The latest recorded timestamp for the metric (ISO 8601, UTC); null if none **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `MetricsErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/metrics///latest_timestamp" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # MetricValues Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of Metric Values `GET /api/v1/metric_values` Read recorded metric values — actuals, forecasts and budgets — for the organisation. **Pagination is cursor-based, not page-based.** Unlike other endpoints in this API this one does not accept a `page` parameter. Make the first request without `cursor`, then pass the `next_cursor` from each response as the `cursor` of the next request. When `next_cursor` is `null` you have reached the end of the result set; a page may hold fewer than `page_size` entries while more follow, so do not stop early. Treat the cursor as opaque — its format is not part of this contract. A cursor is a position within one specific result set. It is tied to the `from`, `to`, `metric_id`, `org_unit_id`, `type` and `current` it was issued for, and replaying it against a different combination returns 422 rather than silently skipping rows. To move a window forward, start a new walk without a cursor. Values are returned grouped by org unit and ordered by timestamp within each org unit, not in global chronological order across sites. `from` and `to` are required and are interpreted as UTC midnight, so the window is `timestamp >= from` and `timestamp < to`. Note this is a UTC window, not the organisation's business day. Only unfiltered values are returned. Where a metric is broken down by a filter, Sona stores the per-filter values alongside the unfiltered total; this endpoint returns the total, so values can be summed without double counting. `metric_id` is required — call `GET /api/v1/metrics` first to resolve the ids you need, then pass up to 50 of them per request. A walk is not a consistent snapshot. Values are written while you page, and a value that is re-recorded mid-walk may be returned as a superseded version or not at all on that walk. For incremental sync, re-read the most recent days on each run rather than assuming a completed walk was complete. Reads are rate limited per organisation, and only a couple may run at once — page sequentially rather than in parallel. A 429 carries a `Retry-After` header; wait that long and reissue the same request, cursor included. A 503 means the read could not be served right now and is likewise safe to retry as-is. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `from` | query | string | yes | Start of the window (inclusive), as YYYY-MM-DD, interpreted as UTC midnight | | `to` | query | string | yes | End of the window (exclusive), as YYYY-MM-DD, interpreted as UTC midnight. May be at most 31 days after `from`. | | `metric_id` | query | array of string | yes | The metric ids to return values for, from `GET /api/v1/metrics`. Repeat the parameter (`metric_id[]=a&metric_id[]=b`) or pass a comma-separated list. Required, and at most 50 — an unfiltered read of this table cannot be served efficiently. | | `org_unit_id` | query | array of string | no | Restrict to these org unit (site) ids. Repeat the parameter or pass a comma-separated list. Omit to return all sites. At most 100. | | `type` | query | array of string; one of `actual`, `system_forecasted`, `human_forecasted`, `budget`, `last_year` | no | Restrict to these value types. Repeat the parameter or pass a comma-separated list. Omit to return all types. | | `current` | query | boolean | no | Defaults to true: only the latest recorded version of each value is returned. Sona keeps the full history of every value; pass `false` to include superseded versions as well. | | `page_size` | query | integer | no | Number of values per page. Defaults to 1000, maximum 10000. | | `cursor` | query | string | no | The `next_cursor` from the previous response, with every other parameter unchanged. Omit for the first page. | ### Responses **200** Success - `entries` (array of `StoredMetricValue`, required): The values in this page - `current` (boolean, required): Whether this is the latest recorded version of this value. Sona keeps full history, so a metric/site/timestamp/type can have several versions, of which only one is current. - `inserted_at` (string, date-time, required): When this version was written to Sona. in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `metric` (string, required): The metric's name, denormalised from `GET /api/v1/metrics`. Always present, but null if the metric definition is no longer available. - `metric_id` (string, required): Sona unique identifier of the metric - `org_unit_id` (string, required): Sona unique identifier of the org unit (site) the value was recorded against - `timestamp` (string, date-time, required): The instant the value applies to. in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `type` (string; one of `actual`, `system_forecasted`, `human_forecasted`, `budget`, `last_year`, required): The kind of value this is - `value` (number, required): The recorded value - `next_cursor` (string, required): Pass as `cursor` on the next request, leaving every other parameter unchanged, to fetch the following page. Always present, but null when this is the last page. - `page_size` (integer, required): The number of values requested per page **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Forbidden — this API credential is not allowed this endpoint — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **429** Too Many Requests — retry after the `Retry-After` header — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) **503** Service Unavailable — retry the same request — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/metric_values?from=&to=&metric_id=" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # OrganisationCalendar Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Gets the organisation calendar `GET /api/v1/organisation/calendar` Returns the calendar settings of the API key's organisation: the day its week starts on, the time its business day starts, and the start of its fiscal year. Use them to bucket days, weeks and fiscal years the way the organisation does. ### Responses **200** Success - `day_start_time` (string, required): Time the organisation's business day starts, in 24-hour HH:MM form - `fiscal_year_start` (string, required): Month and day the organisation's fiscal year starts, as MM-DD (no year) - `week_start_day` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`, required): Lowercase name of the day the organisation's week starts on **401** Unauthorised — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/organisation/calendar" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # OrgAttributes Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## List all org attributes `GET /api/v1/org_attributes` Returns all attribute definitions for the authenticated organisation. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `OrgAttribute`, required): Org attributes - `description` (string): Description of the attribute - `id` (string, uuid, required): Unique identifier - `inner_type` (string): Inner type for weekday attributes - `inserted_at` (string): Created timestamp, in UTC without a timezone designator (e.g. 2024-06-15T10:30:00) - `label` (string, required): Display name - `slug` (string, required): Immutable snake_case identifier - `type` (string): Attribute type: time, boolean, integer, float, string, weekday - `updated_at` (string): Updated timestamp, in UTC without a timezone designator (e.g. 2024-06-15T10:30:00) - `value` (string): Default value for the attribute - `page_number` (integer, required): The page number - `page_size` (integer, required): The number of items per page - `total_entries` (integer, required): The total number of entities available - `total_pages` (integer, required): The total number of pages available **401** Unauthorised — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **422** Invalid pagination or filter parameters. `message` is an object of field names to lists of messages. — `DetailedErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/org_attributes" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create an org attribute `POST /api/v1/org_attributes` Creates a new attribute definition for the organisation. ### Request body Attribute parameters - `description` (string): Description - `inner_type` (string): Inner type for weekday attributes - `label` (string, required): Display name - `slug` (string): Snake_case identifier (auto-generated from label if omitted) - `type` (string, required): Attribute type - `value` (string): Default value ### Responses **201** Created Same fields as `OrgAttribute` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/org_attributes" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "description": "The time the location opens for service", "label": "Opening Time", "slug": "opening_time", "type": "time", "value": "07:00" }' ``` ## Get an org attribute `GET /api/v1/org_attributes/{id}` Returns a single attribute definition by ID. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string, uuid | yes | Org attribute ID | ### Responses **200** Success Same fields as `OrgAttribute` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/org_attributes/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update an org attribute `PUT /api/v1/org_attributes/{id}` Updates an existing attribute definition. Slug cannot be changed. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string, uuid | yes | Org attribute ID | ### Request body Attribute parameters - `description` (string): Description - `inner_type` (string): Inner type for weekday attributes - `label` (string): Display name - `type` (string): Attribute type - `value` (string): Default value ### Responses **200** Success Same fields as `OrgAttribute` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/org_attributes/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "description": "The time the location opens for service each day", "label": "Opening Time (Updated)", "type": "time", "value": "08:00" }' ``` ## Delete an org attribute `DELETE /api/v1/org_attributes/{id}` Deletes an attribute definition. Fails if the attribute is used by productivity rulesets. Cascades deletion to all org unit attribute overrides. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string, uuid | yes | Org attribute ID | ### Responses **204** No Content **401** Unauthorised — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity - attribute in use — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/org_attributes/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # OrgAttributesBatch Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Batch query org attribute values `POST /api/v1/org_attributes/batch_query` Returns the resolved value of each requested attribute for each org unit on each date in the given range. Values are resolved using the most specific org-unit-level override, falling back to the organisation-level default. A value of null means the attribute is not configured, while the string "excluded" means the org unit is explicitly excluded from that attribute. If an org unit has no data for any requested attribute, its entry is null. ### Request body Batch query parameters - `attribute_slugs` (array of string, required): The slugs of the org attributes to query - `end_date` (string, date): The end of the date range, inclusive (ISO 8601). Defaults to start_date - `org_unit_ids` (array of string, uuid, required): Sona unique identifiers of the org units to query - `start_date` (string, date): The start of the date range (ISO 8601). Defaults to today ### Responses **200** Success - `data` (object): Org unit attribute values keyed by org unit ID **401** Unauthorised — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **422** Unknown org units or attributes (`message` is a string), or invalid parameters (`message` is an object of field names to lists of messages). — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/org_attributes/batch_query" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "attribute_slugs": [ "opening_time", "closing_time" ], "end_date": "2026-02-03", "org_unit_ids": [ "e27ad021-af0e-4ccf-95e8-1c7c84656c93", "f38be132-bf1f-5dd0-a6f9-2d8d95767da4" ], "start_date": "2026-02-01" }' ``` --- # OrgUnitAttributes Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## List org unit attributes `GET /api/v1/org_unit_attributes` Returns all attribute overrides for a given org unit. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `org_unit_id` | query | string, uuid | yes | Org unit ID (required) | ### Responses **200** Success - `entries` (array of `OrgUnitAttribute`, required): Org unit attributes - `end_date` (string, date): Last day the value is active (inclusive) - `excluded` (boolean): Whether this attribute is excluded for this org unit - `id` (string, uuid, required): Unique identifier - `inserted_at` (string): Created timestamp, in UTC but without a timezone designator (`YYYY-MM-DDTHH:mm:ss`) - `org_attribute` (`OrgAttribute`): The associated org attribute definition. Only `id`, `slug`, `label`, `type` and `inner_type` are included - `description` (string): Description of the attribute - `id` (string, uuid, required): Unique identifier - `inner_type` (string): Inner type for weekday attributes - `inserted_at` (string): Created timestamp, in UTC without a timezone designator (e.g. 2024-06-15T10:30:00) - `label` (string, required): Display name - `slug` (string, required): Immutable snake_case identifier - `type` (string): Attribute type: time, boolean, integer, float, string, weekday - `updated_at` (string): Updated timestamp, in UTC without a timezone designator (e.g. 2024-06-15T10:30:00) - `value` (string): Default value for the attribute - `org_attribute_id` (string, uuid, required): Org attribute ID - `org_unit_id` (string, uuid, required): Org unit ID - `start_date` (string, date): When this value becomes active (inclusive) - `updated_at` (string): Updated timestamp, in UTC but without a timezone designator (`YYYY-MM-DDTHH:mm:ss`) - `value` (string): The attribute value, or null when the attribute is excluded - `page_number` (integer, required): The page number - `page_size` (integer, required): The number of items per page - `total_entries` (integer, required): The total number of entities available - `total_pages` (integer, required): The total number of pages available **401** Unauthorised — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **422** `org_unit_id` is missing or not a valid UUID (string `message`), or the filter or pagination parameters are invalid (`message` is an object of parameter names to error lists). — `DetailedErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/org_unit_attributes?org_unit_id=" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create an org unit attribute `POST /api/v1/org_unit_attributes` Creates a new attribute override for an org unit. Optionally schedule the value with start_date and end_date (both inclusive). ### Request body Org unit attribute parameters - `end_date` (string, date): Last day the value is active (inclusive) - `excluded` (boolean): Set to true to exclude this attribute for this org unit - `org_attribute_id` (string, uuid, required): Org attribute ID - `org_unit_id` (string, uuid, required): Org unit ID - `start_date` (string, date): When this value becomes active (inclusive) - `value` (string): The attribute value (required unless excluded is true) ### Responses **201** Created Same fields as `OrgUnitAttribute` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **422** Field validation failed (`{"errors": {...}}`), or a referenced ID is missing, not a valid UUID or not in this organisation, or the value is used by a ruleset (`{"error", "message"}` with a string `message`). — `ErrorOrValidationErrors` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/org_unit_attributes" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "end_date": "2024-12-31", "excluded": false, "org_attribute_id": "b2c3d4e5-6789-4abc-bcde-222222222222", "org_unit_id": "a1b2c3d4-5678-4def-abcd-111111111111", "start_date": "2024-06-15", "value": "145.5" }' ``` ## Get an org unit attribute `GET /api/v1/org_unit_attributes/{id}` Returns a single org unit attribute override by ID. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string, uuid | yes | Org unit attribute ID | ### Responses **200** Success Same fields as `OrgUnitAttribute` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/org_unit_attributes/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update an org unit attribute `PUT /api/v1/org_unit_attributes/{id}` Updates an existing org unit attribute override. Fails with 422 when the change would leave a required attribute unresolved — excluding the value (which masks the org-level default) or shrinking its active span — for productivity rulesets assigned to the org unit. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string, uuid | yes | Org unit attribute ID | ### Request body Org unit attribute parameters - `end_date` (string, date): Last day the value is active (inclusive) - `excluded` (boolean): Whether this attribute is excluded - `start_date` (string, date): When this value becomes active (inclusive) - `value` (string): The attribute value ### Responses **200** Success Same fields as `OrgUnitAttribute` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Field validation failed (`{"errors": {...}}`), or the value is used by a ruleset (`{"error", "message"}` with a string `message`). — `ErrorOrValidationErrors` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/org_unit_attributes/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "end_date": "2025-03-31", "excluded": false, "start_date": "2024-07-01", "value": "160.0" }' ``` ## Delete an org unit attribute `DELETE /api/v1/org_unit_attributes/{id}` Deletes an org unit attribute override. The org unit will fall back to the organisation-level default value. Fails with 422 when the value has no org-level default and is required by productivity rulesets assigned to the org unit (the delete would leave those rules unable to compute). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string, uuid | yes | Org unit attribute ID | ### Responses **204** No Content **401** Unauthorised — `ErrorResponse` (see the overview) **403** Forbidden — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity - attribute value in use — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/org_unit_attributes/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # OrgUnitLocation Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get an org unit's location `GET /api/v1/org_units/{org_unit_id}/location` Returns the location of an org unit ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the org unit | ### Responses **200** Success - `address_line_1` (string): The first line of address - `address_line_2` (string): The second line of address - `address_line_3` (string): The third line of address - `address_line_4` (string): The fourth line of address - `city` (string): The city the location is in - `country` (string): The full country name, derived from the country code - `country_code` (string; one of `AC`, `AD`, `AE`, `AF`, `AG`, `AI`, `AL`, `AM`, `AO`, `AQ`, `AR`, `AS`, `AT`, `AU`, `AW`, `AX`, `AZ`, `BA`, `BB`, `BD`, `BE`, `BF`, `BG`, `BH`, `BI`, `BJ`, `BL`, `BM`, `BN`, `BO`, `BQ`, `BR`, `BS`, `BT`, `BV`, `BW`, `BY`, `BZ`, `CA`, `CC`, `CD`, `CF`, `CG`, `CH`, `CI`, `CK`, `CL`, `CM`, `CN`, `CO`, `CP`, `CQ`, `CR`, `CU`, `CV`, `CW`, `CX`, `CY`, `CZ`, `DE`, `DG`, `DJ`, `DK`, `DM`, `DO`, `DZ`, `EA`, `EC`, `EE`, `EG`, `EH`, `ER`, `ES`, `ET`, `EU`, `EZ`, `FI`, `FJ`, `FK`, `FM`, `FO`, `FR`, `GA`, `GB`, `GD`, `GE`, `GF`, `GG`, `GH`, `GI`, `GL`, `GM`, `GN`, `GP`, `GQ`, `GR`, `GS`, `GT`, `GU`, `GW`, `GY`, `HK`, `HM`, `HN`, `HR`, `HT`, `HU`, `IC`, `ID`, `IE`, `IL`, `IM`, `IN`, `IO`, `IQ`, `IR`, `IS`, `IT`, `JE`, `JM`, `JO`, `JP`, `KE`, `KG`, `KH`, `KI`, `KM`, `KN`, `KP`, `KR`, `KW`, `KY`, `KZ`, `LA`, `LB`, `LC`, `LI`, `LK`, `LR`, `LS`, `LT`, `LU`, `LV`, `LY`, `MA`, `MC`, `MD`, `ME`, `MF`, `MG`, `MH`, `MK`, `ML`, `MM`, `MN`, `MO`, `MP`, `MQ`, `MR`, `MS`, `MT`, `MU`, `MV`, `MW`, `MX`, `MY`, `MZ`, `NA`, `NC`, `NE`, `NF`, `NG`, `NI`, `NL`, `NO`, `NP`, `NR`, `NU`, `NZ`, `OM`, `PA`, `PE`, `PF`, `PG`, `PH`, `PK`, `PL`, `PM`, `PN`, `PR`, `PS`, `PT`, `PW`, `PY`, `QA`, `QO`, `RE`, `RO`, `RS`, `RU`, `RW`, `SA`, `SB`, `SC`, `SD`, `SE`, `SG`, `SH`, `SI`, `SJ`, `SK`, `SL`, `SM`, `SN`, `SO`, `SR`, `SS`, `ST`, `SV`, `SX`, `SY`, `SZ`, `TA`, `TC`, `TD`, `TF`, `TG`, `TH`, `TJ`, `TK`, `TL`, `TM`, `TN`, `TO`, `TR`, `TT`, `TV`, `TW`, `TZ`, `UA`, `UG`, `UM`, `UN`, `US`, `UY`, `UZ`, `VA`, `VC`, `VE`, `VG`, `VI`, `VN`, `VU`, `WF`, `WS`, `XK`, `YE`, `YT`, `ZA`, `ZM`, `ZW`): The two-letter code repesenting the country the location is in - `id` (string): Sona unique identifier - `latitude` (number): The latitude of the location in decimal degrees, derived from the postcode unless explicitly set - `longitude` (number): The longitude of the location in decimal degrees, derived from the postcode unless explicitly set - `name` (string): The name of the location - `postcode` (string): The postcode of the location **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity: the `org_unit_id` is not a valid UUID (`{"errors": {"org_units_id": ["is invalid"]}}`). — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/org_units//location" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create or update an org unit's location `POST /api/v1/org_units/{org_unit_id}/location` Create or update an org unit's location. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the org unit | ### Request body The org unit parameters - `address_line_1` (string, required): The first line of address - `address_line_2` (string): The second line of address - `address_line_3` (string): The third line of address - `address_line_4` (string): The fourth line of address - `city` (string, required): The city the location is in - `country_code` (string; one of `AC`, `AD`, `AE`, `AF`, `AG`, `AI`, `AL`, `AM`, `AO`, `AQ`, `AR`, `AS`, `AT`, `AU`, `AW`, `AX`, `AZ`, `BA`, `BB`, `BD`, `BE`, `BF`, `BG`, `BH`, `BI`, `BJ`, `BL`, `BM`, `BN`, `BO`, `BQ`, `BR`, `BS`, `BT`, `BV`, `BW`, `BY`, `BZ`, `CA`, `CC`, `CD`, `CF`, `CG`, `CH`, `CI`, `CK`, `CL`, `CM`, `CN`, `CO`, `CP`, `CQ`, `CR`, `CU`, `CV`, `CW`, `CX`, `CY`, `CZ`, `DE`, `DG`, `DJ`, `DK`, `DM`, `DO`, `DZ`, `EA`, `EC`, `EE`, `EG`, `EH`, `ER`, `ES`, `ET`, `EU`, `EZ`, `FI`, `FJ`, `FK`, `FM`, `FO`, `FR`, `GA`, `GB`, `GD`, `GE`, `GF`, `GG`, `GH`, `GI`, `GL`, `GM`, `GN`, `GP`, `GQ`, `GR`, `GS`, `GT`, `GU`, `GW`, `GY`, `HK`, `HM`, `HN`, `HR`, `HT`, `HU`, `IC`, `ID`, `IE`, `IL`, `IM`, `IN`, `IO`, `IQ`, `IR`, `IS`, `IT`, `JE`, `JM`, `JO`, `JP`, `KE`, `KG`, `KH`, `KI`, `KM`, `KN`, `KP`, `KR`, `KW`, `KY`, `KZ`, `LA`, `LB`, `LC`, `LI`, `LK`, `LR`, `LS`, `LT`, `LU`, `LV`, `LY`, `MA`, `MC`, `MD`, `ME`, `MF`, `MG`, `MH`, `MK`, `ML`, `MM`, `MN`, `MO`, `MP`, `MQ`, `MR`, `MS`, `MT`, `MU`, `MV`, `MW`, `MX`, `MY`, `MZ`, `NA`, `NC`, `NE`, `NF`, `NG`, `NI`, `NL`, `NO`, `NP`, `NR`, `NU`, `NZ`, `OM`, `PA`, `PE`, `PF`, `PG`, `PH`, `PK`, `PL`, `PM`, `PN`, `PR`, `PS`, `PT`, `PW`, `PY`, `QA`, `QO`, `RE`, `RO`, `RS`, `RU`, `RW`, `SA`, `SB`, `SC`, `SD`, `SE`, `SG`, `SH`, `SI`, `SJ`, `SK`, `SL`, `SM`, `SN`, `SO`, `SR`, `SS`, `ST`, `SV`, `SX`, `SY`, `SZ`, `TA`, `TC`, `TD`, `TF`, `TG`, `TH`, `TJ`, `TK`, `TL`, `TM`, `TN`, `TO`, `TR`, `TT`, `TV`, `TW`, `TZ`, `UA`, `UG`, `UM`, `UN`, `US`, `UY`, `UZ`, `VA`, `VC`, `VE`, `VG`, `VI`, `VN`, `VU`, `WF`, `WS`, `XK`, `YE`, `YT`, `ZA`, `ZM`, `ZW`): The two-letter code repesenting the country the location is in - `latitude` (number): The latitude of the location in decimal degrees, will be derived from the postcode unless explicitly set - `longitude` (number): The longitude of the location in decimal degrees, will be derived from the postcode unless explicitly set - `name` (string): The name of the location - `postcode` (string, required): The postcode of the location ### Responses **200** Success Same fields as `OrgUnitLocation` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) **503** Service Unavailable — the postcode lookup timed out and nothing was saved; retry the same request — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/org_units//location" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "address_line_1": "1 Bridge Street", "city": "London", "country_code": "GB", "latitude": 51.5007, "longitude": -0.1246, "name": "Head Office", "postcode": "SW1A0AA" }' ``` ## Delete the org unit's location `DELETE /api/v1/org_units/{org_unit_id}/location` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the org unit | ### Responses **200** Success Same fields as `OrgUnitLocation` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity: the `org_unit_id` is not a valid UUID (`{"errors": {"org_units_id": ["is invalid"]}}`). — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/org_units//location" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # OrgUnits Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of Org Units `GET /api/v1/org_units` An org unit in Sona represents a single organisation unit of a business. So for example if a business is structured as Head Office -> Regions -> Locations -> Departments then an org unit could be any one of those things. Which one it is can be identified from the `org_unit_type` field. Every org unit but one for an organisation will have a `parent_org_unit` which can be used to reconstruct the hierarchy. **Important**: By default, only base entities (typically the lowest-level units such as Sites or Departments) are returned. To retrieve the full hierarchy including parent org units (e.g. Regions, Areas), you must pass `base_only=false`. Example: `GET /api/v1/org_units?base_only=false` **Pagination**: Results are paginated. Use the `page` and `page_size` query parameters to navigate. The default page size is 10. Note: the parameter name is `page` (not `page_number`). The response includes `page_number`, `page_size`, `total_pages`, and `total_entries` metadata to assist with iteration. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `external_ids` | query | array of string | no | The external ids to filter by. Only org units with a matching external connection are returned. | | `base_only` | query | boolean | no | Whether only base entities should be returned. **Defaults to `true`**, which means only the lowest-level org units (e.g. Sites, Departments) are returned. Set to `false` to include all org units in the hierarchy, including parent units such as Regions and Areas. This is required if you need to resolve `parent_org_unit` references. | ### Responses **200** Success - `entries` (array of `OrgUnit`) - `accounting_cost_code` (string): Accounting cost code - `average_hourly_rate` (number, float): Average hourly rate - `external_connections` (array of object) - `external_id` (string): External identifier of the org unit - `id` (string): Sona unique identifier - `is_base_entity` (boolean): Whether it's a base entity - `name` (string): The name of the org unit - `org_unit_type` (object) - `id` (string): Sona unique identifier of the org unit type - `name` (string): The name of the org unit type - `parent_org_unit` (object) - `id` (string): Sona unique identifier of the parent org unit, or null for a top-level org unit - `paycaptain_company_id` (string): Pay captain company id - `public_holidays` (array of `PublicHoliday`) - `country_code` (string): Country code - `date` (string, date): Holiday date - `day_of_week` (string): Day of week - `id` (string): Sona unique identifier - `is_substitute` (boolean): Substitute? - `name` (string): Public holiday name - `regions` (array of string): Country regions - `timezone` (string): The timezone of the org unit - `page_number` (integer): The current page number (1-indexed) - `page_size` (integer): The number of items per page (default: 10) - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/org_units" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create an Org Unit `POST /api/v1/org_units` ### Request body The org unit parameters - `accounting_cost_code` (string): Accounting cost code - `average_hourly_rate` (number, float): Average hourly rate - `external_connections` (array of object) - `external_id` (string): External identifier of the org unit - `is_base_entity` (boolean, required): Whether it's a base entity - `name` (string, required): The name of the org unit - `org_unit_type_id` (string, required): Sona unique identifier of the org unit type - `parent_id` (string): Sona unique identifier of the parent org unit - `paycaptain_company_id` (string): Pay captain company id - `public_holidays` (array of string) ### Responses **200** Success Same fields as `OrgUnit` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/org_units" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "accounting_cost_code": "MW-001", "average_hourly_rate": 14.5, "external_connections": [ { "external_id": "EXT-MW-001" } ], "is_base_entity": true, "name": "Maple Ward", "org_unit_type_id": "c9d80598-8223-4836-875d-f6dd9552a77f", "parent_id": "e27ad021-af0e-4ccf-95e8-1c7c84656c93", "paycaptain_company_id": "PC-1234", "public_holidays": [ "a1b2c3d4-e5f6-7890-abcd-ef1234567890" ] }' ``` ## Get an individual org unit `GET /api/v1/org_units/{org_unit_id}` Get an individual org unit. See the org unit listing endpoint for a description of org units ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the org_unit | ### Responses **200** Success Same fields as `OrgUnit` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/org_units/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update an Org Unit `PUT /api/v1/org_units/{org_unit_id}` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | The Sona unique identifier of org unit to update | ### Request body The org unit parameters - `accounting_cost_code` (string): Accounting cost code - `average_hourly_rate` (number, float): Average hourly rate - `external_connections` (array of object): List of external connections for update. Existing external connections will be removed unless included in this list. - `external_id` (string): External identifier of the org unit - `is_base_entity` (boolean): Whether it's a base entity - `name` (string): The name of the org unit - `org_unit_type_id` (string): Sona unique identifier of the org unit type - `parent_id` (string): Sona unique identifier of the parent org unit - `paycaptain_company_id` (string): Pay captain company id ### Responses **200** Success Same fields as `OrgUnit` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/org_units/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "accounting_cost_code": "OW-002", "average_hourly_rate": 15.75, "external_connections": [ { "external_id": "EXT-OW-002" } ], "is_base_entity": true, "name": "Oak Ward", "org_unit_type_id": "c9d80598-8223-4836-875d-f6dd9552a77f", "parent_id": "e27ad021-af0e-4ccf-95e8-1c7c84656c93", "paycaptain_company_id": "PC-5678" }' ``` ## Set public holidays for the org unit `POST /api/v1/org_units/{org_unit_id}/public_holidays` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the org unit | ### Request body Public holidays parameters - `public_holidays` (array of string) ### Responses **200** Success Same fields as `OrgUnit` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** No org unit ID was provided — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/org_units//public_holidays" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "public_holidays": [ "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "b2c3d4e5-f6a7-8901-bcde-f12345678901" ] }' ``` ## Remove selected public holidays from the org unit public holidays list `DELETE /api/v1/org_units/{org_unit_id}/public_holidays` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the org unit | ### Request body Public holidays IDs to remove Same fields as `OrgUnitPublicHolidaysParams` above. ### Responses **200** Success Same fields as `OrgUnit` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/org_units//public_holidays" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "public_holidays": [ "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "b2c3d4e5-f6a7-8901-bcde-f12345678901" ] }' ``` --- # OrgUnitTypes Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of Org Unit Types `GET /api/v1/org_unit_types` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `OrgUnitType`) - `id` (string, required): Sona unique identifier - `name` (string, required): The name of the org unit type - `parent_org_unit_type` (object) - `id` (string): Sona unique identifier of the parent org unit type, or null for a top-level type - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/org_unit_types" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create an Org Unit Type `POST /api/v1/org_unit_types` ### Request body The org unit type parameters - `name` (string, required): The name of the org unit type - `parent_id` (string): Sona unique identifier of the parent org unit type ### Responses **200** Success Same fields as `OrgUnitType` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/org_unit_types" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Department", "parent_id": "c9d80598-8223-4836-875d-f6dd9552a77f" }' ``` ## Get an individual org unit type `GET /api/v1/org_unit_types/{org_unit_type_id}` Get an individual org unit type. See the listing endpoint for a description of org unit types ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_type_id` | path | string | yes | Sona unique identifier of the org_unit_type | ### Responses **200** Success Same fields as `OrgUnitType` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/org_unit_types/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # PayGroupPayPeriods Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## List pay periods for a pay group `GET /api/v1/pay_groups/{pay_group_id}/pay_periods` Returns the pay schedule (pay periods) for the given pay group, ordered by start date descending. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `pay_group_id` | path | string | yes | The Sona unique identifier of the pay group. | | `from_date` | query | string | no | Filter pay periods with start_date on or after this date (ISO 8601, e.g. 2026-01-01) | | `to_date` | query | string | no | Filter pay periods with start_date on or before this date (ISO 8601, e.g. 2026-12-31) | | `state` | query | string; one of `open`, `closed` | no | Filter by pay period state: "open" or "closed" | ### Responses **200** Success - `entries` (array of `PayPeriod`) - `cutoff_datetime` (string): The payroll cutoff datetime (ISO 8601) - `end_date` (string): The last day of the pay period, inclusive, in the organisation's timezone (YYYY-MM-DD) - `end_datetime` (string): The end datetime of the pay period (ISO 8601) - `id` (string): The unique identifier of the pay period - `is_parallel` (boolean): Whether this is a parallel-run period, not the live payroll - `name` (string): The name of the pay period - `pay_datetime` (string): The pay date (ISO 8601) - `pay_group_id` (string): The unique identifier of the pay group - `payslip_issue_datetime` (string): When payslips become available (ISO 8601) - `period_number` (integer): The period number within the tax year - `reference_period_end_date` (string): The last day of the reference period used for shift-based pay, inclusive (YYYY-MM-DD) - `reference_period_end_datetime` (string): The end datetime of the reference period (ISO 8601) - `reference_period_start_date` (string): The first day of the reference period used for shift-based pay, inclusive (YYYY-MM-DD) - `reference_period_start_datetime` (string): The start datetime of the reference period (ISO 8601) - `start_date` (string): The first day of the pay period, inclusive, in the organisation's timezone (YYYY-MM-DD) - `start_datetime` (string): The start datetime of the pay period (ISO 8601) - `state` (string): The state of the pay period (open or closed) - `tax_year` (integer): The tax year this period belongs to - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The pay group ID or a filter (`from_date`, `to_date`, `state`) is invalid. `message` is an object mapping each invalid parameter to a single error message (a string), e.g. `{"state": "invalid value, allowed values are: open, closed"}`. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/pay_groups//pay_periods" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # PayGroups Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of pay groups `GET /api/v1/pay_groups` List pay groups for the current organisation ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `PayGroup`) - `id` (string): The unique identifier of the pay group - `name` (string): The name of the pay group - `pay_frequency` (string; one of `weekly`, `fortnightly`, `four_weekly`, `monthly`, `quarterly`, `bi_annual`, `annual`, `calendar_monthly`): The type of pay period - `payroll_entity_name` (string): The name of the payroll entity - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/pay_groups" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get a single pay group `GET /api/v1/pay_groups/{id}` Get a single pay group data. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | The Sona unique identifier of the pay group. | ### Responses **200** Success Same fields as `PayGroup` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The `id` is not a valid UUID. `message` maps the parameter to its error, e.g. `{"id": "invalid format"}` — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/pay_groups/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # PayLines Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of pay lines `GET /api/v1/pay_lines` List all pay lines for the current organisation. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `order_by_date` | query | string; one of `asc`, `desc` | no | order results by date (asc or desc). By default is asc. | | `start_datetime` | query | string | no | The start date time in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) | | `end_datetime` | query | string | no | The end date time in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) | | `entry_type` | query | string; one of `regular`, `amendment` | no | Type of pay line | | `user_id` | query | string | no | Filter by user id | ### Responses **200** Success - `entries` (array of `PayLine`) - `comment` (string): Pay line comment - `duration_in_minutes` (integer): Duration in minutes - `end_datetime` (string, date-time): The end datetime in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `entry_type` (string; one of `regular`, `amendment`): Type of pay line - `id` (string): Sona pay line unique identifier - `pay_type` (string): Pay type - `rate_of_pay` (object): The rate of pay the line was paid at, or null when there is none - `id` (string): Rate of pay id - `pay_type` (string): How it is paid, e.g. hourly / salaried - `rate` (string): The payment rate - `shift_id` (string): Sona unique identifier of the shift - `shift_type_id` (string): Sona unique identifier of the shift type - `shift_type_name` (string): Name of the shift type - `start_datetime` (string): The start date time in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `user_id` (string): The unique Sona identifier of the user entity - `variant` (string): The name of the line's pay variant - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/pay_lines" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # PayrollPaymentDeductionTypes Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## List payroll payment/deduction types `GET /api/v1/payroll_payment_deduction_types` Returns a paginated list of payroll payment and deduction types for the current organisation. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `PayrollPaymentDeductionType`) - `additional_properties` (object): Additional properties map - `cr_code` (string): Credit cost code - `cr_cost_centre` (string): Credit cost centre - `dr_code` (string): Debit cost code - `dr_cost_centre` (string): Debit cost centre - `exclude_from_wage_cost` (boolean): Excluded from wage cost reporting - `external_type` (string): Category as used by external payroll system - `gender_pay_gap` (string): Gender pay gap reporting category - `holidayable` (boolean): Included in holiday pay calculations - `id` (string, required): Unique Sona identifier - `min_wage` (boolean): Included in minimum wage calculations - `name` (string): Descriptive name - `niable` (boolean): Included in NI calculation - `pay_code` (string, required): Unique pay code within the organisation - `payslip_text` (string): Text shown on employee payslip - `pensionable` (boolean): Included in pension calculation - `system_pay_code` (string): System-level pay code (final_holiday_pay, holiday_pay, ssp) - `taxable` (boolean): Included in tax calculation - `type` (string, required): Whether this is a payment, deduction or cost. Free text: `Payment`, `Deduction` and `Cost` are the conventional values, but records may hold others (e.g. `addition`). - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/payroll_payment_deduction_types" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a payroll payment/deduction type `POST /api/v1/payroll_payment_deduction_types` Create a new payroll payment or deduction type for the current organisation. The `type` field distinguishes payments from deductions: "Payment", "Deduction", or "Cost". The `external_type` field must match the company pay elements in Shape when Sona Pay sync is enabled for the organisation; it becomes a required field in that case. The `system_pay_code` field is optional and restricted to: final_holiday_pay, holiday_pay, ssp. Only one final_holiday_pay type and one ssp type are allowed per organisation. WARNING: Changing a pay_code after TGP rules reference it may cause errors in running or future TGP jobs. ### Request body The payroll payment/deduction type parameters - `additional_properties` (object): Additional properties map. Known keys: is_tip, is_regular_pay, is_hours, is_overtime, is_additional_payment (all booleans). TGP rules engine uses is_tip and is_regular_pay. - `cr_code` (string): Credit cost code - `cr_cost_centre` (string): Credit cost centre - `dr_code` (string): Debit cost code - `dr_cost_centre` (string): Debit cost centre - `exclude_from_wage_cost` (boolean): Excluded from wage cost reporting - `external_type` (string): Category as used by external payroll system (must match Shape company pay elements). Required when Sona Pay sync is enabled. - `gender_pay_gap` (string; one of `not_set`, `not_included`, `excluded`, `ordinary`, `bonus`): Gender pay gap reporting category - `holidayable` (boolean): Included in holiday pay calculations - `min_wage` (boolean): Included in minimum wage calculations - `name` (string): Descriptive name - `niable` (boolean): Included in NI calculation - `pay_code` (string, required): Unique pay code within the organisation - `payslip_text` (string): Text shown on employee payslip - `pensionable` (boolean): Included in pension calculation - `system_pay_code` (string; one of `final_holiday_pay`, `holiday_pay`, `ssp`): System-level pay code - `taxable` (boolean): Included in tax calculation - `type` (string, required): Whether this is a payment, deduction or cost. Free text: the API stores any string. `Payment`, `Deduction` and `Cost` are the conventional values, but existing records may hold others (e.g. `addition`). ### Responses **201** Created Same fields as `PayrollPaymentDeductionType` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Validation failed. `message` maps each invalid field to its error messages, e.g. `{"pay_code": ["can't be blank"]}` — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/payroll_payment_deduction_types" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "additional_properties": { "is_overtime": true, "is_regular_pay": false }, "exclude_from_wage_cost": false, "external_type": "Overtime", "gender_pay_gap": "ordinary", "holidayable": true, "min_wage": false, "name": "Overtime 1.5x", "niable": true, "pay_code": "OT150", "payslip_text": "Overtime at time and a half", "pensionable": false, "taxable": true, "type": "Payment" }' ``` ## Get a single payroll payment/deduction type `GET /api/v1/payroll_payment_deduction_types/{id}` Get a single payroll payment or deduction type by its Sona unique identifier. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | The Sona unique identifier of the payroll payment/deduction type. | ### Responses **200** Success Same fields as `PayrollPaymentDeductionType` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The `id` is not a valid UUID — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/payroll_payment_deduction_types/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update a payroll payment/deduction type `PUT /api/v1/payroll_payment_deduction_types/{id}` Update an existing payroll payment or deduction type. The `additional_properties` field uses merge semantics: only the keys you provide will be updated; existing keys not included in the request are preserved. Omitting `additional_properties` entirely leaves the current value unchanged. WARNING: Changing a pay_code after TGP rules reference it may cause errors in running or future TGP jobs. Changing system_pay_code may affect holiday pay or SSP calculations. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | The Sona unique identifier of the payroll payment/deduction type. | ### Request body The payroll payment/deduction type parameters - `additional_properties` (object): Additional properties map (merge semantics - existing keys preserved). Known keys: is_tip, is_regular_pay, is_hours, is_overtime, is_additional_payment (all booleans). - `cr_code` (string): Credit cost code - `cr_cost_centre` (string): Credit cost centre - `dr_code` (string): Debit cost code - `dr_cost_centre` (string): Debit cost centre - `exclude_from_wage_cost` (boolean): Excluded from wage cost reporting - `external_type` (string): Category as used by external payroll system (must match Shape company pay elements) - `gender_pay_gap` (string; one of `not_set`, `not_included`, `excluded`, `ordinary`, `bonus`): Gender pay gap reporting category - `holidayable` (boolean): Included in holiday pay calculations - `min_wage` (boolean): Included in minimum wage calculations - `name` (string): Descriptive name - `niable` (boolean): Included in NI calculation - `pay_code` (string): Unique pay code within the organisation - `payslip_text` (string): Text shown on employee payslip - `pensionable` (boolean): Included in pension calculation - `system_pay_code` (string; one of `final_holiday_pay`, `holiday_pay`, `ssp`): System-level pay code - `taxable` (boolean): Included in tax calculation - `type` (string): Whether this is a payment, deduction or cost. Free text: the API stores any string. `Payment`, `Deduction` and `Cost` are the conventional values, but existing records may hold others (e.g. `addition`). ### Responses **200** Success Same fields as `PayrollPaymentDeductionType` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The `id` is not a valid UUID (`message` is the string `"Invalid id format"`), or validation failed (`message` maps each invalid field to its error messages) — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/payroll_payment_deduction_types/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "additional_properties": { "is_overtime": true }, "name": "Overtime 2x", "payslip_text": "Overtime at double time", "taxable": true }' ``` --- # PayrollSubmissionStatuses Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get payroll submission statuses for worked shifts `GET /api/v1/payroll_submission_statuses` Returns worked shifts with their payroll submission status and, for shifts still pending submission, whether they need attention ("unreconciled") and why. This is the programmatic equivalent of the *Review Payroll Records* / *Pending Submission* view. `submission_status` is `pending` (not yet approved for payroll) or `submitted` (approved for payroll). `reconciliation_status` is `needs_attention` when the shift has an unresolved payroll issue, otherwise `resolved`. The `exceptions` array lists the reasons a shift needs attention: `pay_query` (an exception flagged on the payroll record), `missing_pay_variant`, `shift_in_progress` (payroll record has no end time), or `missing_pay_record` (no payroll record while time & attendance is required). `min_start_datetime` and `max_start_datetime` are required and may span at most 31 days. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `min_start_datetime` | query | string | yes | Only include shifts starting on or after this time in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) | | `max_start_datetime` | query | string | yes | Only include shifts starting on or before this time in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) | | `user_id` | query | string | no | Filter by the Sona user id of the employee | | `org_unit_ids` | query | string | no | Filter by one or more org unit (location) ids, comma-separated | | `submission_status` | query | string; one of `pending`, `submitted` | no | Filter by submission status | ### Responses **200** Success - `entries` (array of `PayrollSubmissionStatus`) - `approved_for_payroll_at` (string, date-time): When the shift was approved (submitted) for payroll; null when still pending - `exceptions` (array of string; one of `pay_query`, `missing_pay_variant`, `shift_in_progress`, `missing_pay_record`): Reasons the shift needs attention (empty when resolved) - `finish_datetime` (string, date-time): Shift finish in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `internal_id` (string): Your own external identifier for the shift, if one was set - `org_unit` (object, required): The org unit (location) of the shift - `reconciliation_status` (string; one of `resolved`, `needs_attention`, required): Whether the shift needs attention before it can be submitted - `shift_id` (string, required): Sona unique identifier of the shift - `shift_type` (object, required): The shift type (role) of the shift - `start_datetime` (string, date-time): Shift start in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `submission_status` (string; one of `pending`, `submitted`, required): Submission status - `user` (object, required): The employee who worked the shift - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/payroll_submission_statuses?min_start_datetime=&max_start_datetime=" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # PayVariants Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Paginated list of pay variants `GET /api/v1/absence_types/{absence_type_id}/pay_variants` Lists all pay variants associated with an absence type. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `absence_type_id` | path | string | yes | Sona unique identifier of the absence type | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `PayVariant`) - `id` (string): Sona pay variant unique identifier - `name` (string): The name of the variant - `paid` (boolean): Whether the variant is paid or not - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The absence type ID is not a valid UUID, or the pagination parameters are invalid — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/absence_types//pay_variants" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get a list of pay variants `GET /api/v1/pay_variants` List all pay variants for the organisation ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `PayVariant`) - Same fields as `PayVariant` above. - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity: pagination or sorting parameters are invalid. `message` maps parameter names to errors. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/pay_variants" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Paginated list of pay variants `GET /api/v1/shift_types/{shift_type_id}/pay_variants` List all pay variants associated with a shift type. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_type_id` | path | string | yes | Sona unique identifier of the shift type | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `ShiftTypePayVariant`) - `counts_towards_annualised_hours` (boolean): Whether counts towards annualised hours or not - `default` (boolean): Whether the variant is the default for the shift type or not - `derives_annualised_hours_from_shifts` (boolean): Whether derives annualised hours from shifts or not - `id` (string): Sona pay variant unique identifier - `name` (string): The name of the pay variant - `paid` (boolean): Whether the pay variant is paid or not - `subject_to_annualised_hours_bonus` (boolean): Whether it's subject to annualised hours bonus or not - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not found — `ErrorResponse` (see the overview) **422** Unprocessable entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/shift_types//pay_variants" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Creates a pay variant `POST /api/v1/shift_types/{shift_type_id}/pay_variants` Creates a pay variant associated with a shift type. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_type_id` | path | string | yes | Sona unique identifier of the shift type | ### Request body Pay variant creation parameters for the shift type - `counts_towards_annualised_hours` (boolean): Whether counts towards annualised hours or not - `default` (boolean): Whether the variant is the default for the shift type or not - `derives_annualised_hours_from_shifts` (boolean): Whether derives annualised hours from shifts or not - `name` (string): The name of the pay variant - `paid` (boolean): Whether the pay variant is paid or not - `subject_to_annualised_hours_bonus` (boolean): Whether it's subject to annualised hours bonus or not ### Responses **201** Success Same fields as `ShiftTypePayVariant` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not found — `ErrorResponse` (see the overview) **422** A shift type or pay variant ID is not a valid UUID (`{error, message}`), or the pay variant fails validation (`{errors}`) — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/shift_types//pay_variants" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "counts_towards_annualised_hours": true, "default": false, "derives_annualised_hours_from_shifts": false, "name": "Bank Holiday Premium", "paid": true, "subject_to_annualised_hours_bonus": false }' ``` ## Gets a pay variant `GET /api/v1/shift_types/{shift_type_id}/pay_variants/{pay_variant_id}` Gets a pay variant associated with a shift type. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_type_id` | path | string | yes | Sona unique identifier of the shift type | | `pay_variant_id` | path | string | yes | Sona unique identifier of the pay variant | ### Responses **200** Success Same fields as `ShiftTypePayVariant` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not found — `ErrorResponse` (see the overview) **422** Unprocessable entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/shift_types//pay_variants/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Updates a pay variant `PUT /api/v1/shift_types/{shift_type_id}/pay_variants/{pay_variant_id}` Updates a pay variant associated with a shift type. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_type_id` | path | string | yes | Sona unique identifier of the shift type | | `pay_variant_id` | path | string | yes | Sona unique identifier of the pay variant | ### Request body Pay variant updatable parameters for the shift type Same fields as `ShiftTypePayVariantParams` above. ### Responses **200** Success Same fields as `ShiftTypePayVariant` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not found — `ErrorResponse` (see the overview) **422** A shift type or pay variant ID is not a valid UUID (`{error, message}`), or the pay variant fails validation (`{errors}`) — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/shift_types//pay_variants/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "counts_towards_annualised_hours": true, "default": false, "derives_annualised_hours_from_shifts": false, "name": "Bank Holiday Premium", "paid": true, "subject_to_annualised_hours_bonus": false }' ``` ## Deletes a pay variant `DELETE /api/v1/shift_types/{shift_type_id}/pay_variants/{pay_variant_id}` Deletes a pay variant associated with a shift type. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_type_id` | path | string | yes | Sona unique identifier of the shift type | | `pay_variant_id` | path | string | yes | Sona unique identifier of the pay variant | ### Responses **204** Success **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/shift_types//pay_variants/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Links a pay variant to a shift type `POST /api/v1/shift_types/{shift_type_id}/pay_variants/{pay_variant_id}/link` Links a pay variant to a shift type. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_type_id` | path | string | yes | Sona unique identifier of the shift type | | `pay_variant_id` | path | string | yes | Sona unique identifier of the pay variant | ### Responses **204** Success **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/shift_types//pay_variants//link" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Unlinks a pay variant from a shift type `DELETE /api/v1/shift_types/{shift_type_id}/pay_variants/{pay_variant_id}/link` Unlinks a pay variant from a shift type. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_type_id` | path | string | yes | Sona unique identifier of the shift type | | `pay_variant_id` | path | string | yes | Sona unique identifier of the pay variant | ### Responses **204** Success **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/shift_types//pay_variants//link" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # PeopleWeSupport Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of people we support `GET /api/v1/people_we_support` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `org_unit_id` | query | string | no | Only people in this org unit (Sona id) | | `upload_id` | query | string | no | Exact match on the external id (`upload_id`) | | `integration_idx` | query | string | no | Exact match on `integration_idx` | | `name` | query | string | no | Case-insensitive 'contains' match on name | | `status` | query | string | no | Current status: `active` or `inactive`. `active` includes people with no status record (`status: null`). | | `updated_at` | query | string | no | Only people updated on or after this UTC datetime (ISO 8601, trailing Z). Only the person's own record is considered, not status or care package changes. | | `order` | query | string | no | `asc` or `desc`, by name. When omitted the order is unspecified, as before. | ### Responses **200** Success - `entries` (array of `PeopleWeSupportData`) - `current_care_package` (object): The care package in force today (UTC date): started, and not yet ended. Null when there is none. - `id` (string): The unique Sona identifier of the care package - `idx` (string): Package code - `name` (string): Package name - `start_date` (string): Start date in ISO 8601 standard - `end_date` (string): Start date in ISO 8601 standard - `id` (string): The unique Sona identifier of the person we support - `integration_idx` (string): Integration idx - `location` (object) - `address_line_1` (string): Address line 1 - `address_line_2` (string): Address line 2 - `address_line_3` (string): Address line 3 - `address_line_4` (string): Address line 4 - `city` (string): City - `country` (string): Country name - `id` (string): The unique Sona identifier of the location - `name` (string): Location name - `postcode` (string): Postal code - `name` (string): Person full name - `nhs_number` (string): NHS number - `org_unit_id` (string): The unique Sona identifier of the org unit - `start_date` (string): Start date in ISO 8601 standard - `status` (string): Current status, `active` or `inactive`. Null when no status record exists (shown as Unknown in the admin screen), which is the case for anyone never logged out of service. The `status=active` filter counts them as active. - `updated_at_utc` (string): Last update of the person record, UTC ISO 8601 - `upload_id` (string): External id (`idx`) - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/people_we_support" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create person we support `POST /api/v1/people_we_support` ### Request body Person we support parameters - `end_date` (string): Start date in ISO 8601 standard - `integration_idx` (string): Integration idx - `location` (object) - `address_line_1` (string): Address line 1 - `address_line_2` (string): Address line 2 - `address_line_3` (string): Address line 3 - `address_line_4` (string): Address line 4 - `city` (string): City - `country` (string): Country name - `name` (string): Location name - `postcode` (string): Postal code - `name` (string, required): Person full name - `nhs_number` (string): NHS number - `org_unit_id` (string, required): The unique Sona identifier of the org unit - `start_date` (string): Start date in ISO 8601 standard - `upload_id` (string): External id (`idx`), unique within your organisation ### Responses **201** Success Same fields as `PeopleWeSupportData` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** The org unit was not found in the organisation. A missing `org_unit_id` is also returned as 404 (`org_unit can't be blank`; known issue: it is a validation error and should be 422). — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/people_we_support" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "end_date": "2025-01-15", "integration_idx": "PWS-EXT-001", "location": { "address_line_1": "42 Elm Street", "address_line_2": "Westbury", "city": "Bristol", "country": "United Kingdom", "name": "Rosewood House", "postcode": "BS1 4DJ" }, "name": "Margaret Thompson", "nhs_number": "943 476 5919", "org_unit_id": "e27ad021-af0e-4ccf-95e8-1c7c84656c93", "start_date": "2024-01-15" }' ``` ## Get a person we support `GET /api/v1/people_we_support/{person_we_support_id}` Get one person we support by Sona id, or by `upload_id` when `is_idx=true`. 404 if the person is not in your organisation. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `person_we_support_id` | path | string | yes | Sona id, or `upload_id` when `is_idx=true` | | `is_idx` | query | boolean | no | Set to true to look the person up by `upload_id` | ### Responses **200** Success Same fields as `PeopleWeSupportData` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/people_we_support/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update person we support `PUT /api/v1/people_we_support/{person_we_support_id}` Update person we support, by Sona id or by `upload_id` when `is_idx=true` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `person_we_support_id` | path | string | yes | Sona id, or `upload_id` when `is_idx=true` | | `is_idx` | query | boolean | no | Set to true to look the person up by `upload_id` | ### Request body People we support update parameters - `end_date` (string): Start date in ISO 8601 standard - `integration_idx` (string): Integration idx - `location` (object) - `address_line_1` (string): Address line 1 - `address_line_2` (string): Address line 2 - `address_line_3` (string): Address line 3 - `address_line_4` (string): Address line 4 - `city` (string): City - `country` (string): Country name - `name` (string): Location name - `postcode` (string): Postal code - `name` (string): Person full name - `nhs_number` (string): NHS number - `start_date` (string): Start date in ISO 8601 standard - `upload_id` (string): External id (`idx`), unique within your organisation ### Responses **200** Success Same fields as `PeopleWeSupportData` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/people_we_support/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "end_date": "2025-06-30", "integration_idx": "PWS-EXT-001", "location": { "address_line_1": "42 Elm Street", "address_line_2": "Westbury", "city": "Bristol", "country": "United Kingdom", "name": "Rosewood House", "postcode": "BS1 4DJ" }, "name": "Margaret Thompson", "nhs_number": "943 476 5919", "start_date": "2024-01-15" }' ``` ## List a person's status history `GET /api/v1/people_we_support/{person_we_support_id}/status_records` When the person was active or inactive, and why, newest first. Read only. The current status is also on the person (`status`). Requires demand-based rostering to be enabled for your organisation (403 otherwise). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `person_we_support_id` | path | string | yes | Sona id, or `upload_id` when `is_idx=true` | | `is_idx` | query | boolean | no | Set to true to look the person up by `upload_id` | | `status` | query | string | no | `active` or `inactive` | | `from` | query | string | no | Records that started on or after this date (YYYY-MM-DD, UTC) | | `to` | query | string | no | Records that started on or before this date (YYYY-MM-DD, UTC) | | `updated_at` | query | string | no | Records created or changed on or after this time, YYYY-MM-DDThh:mm:ssZ in UTC (e.g. 2026-01-15T10:00:00Z) | ### Responses **200** Success - `entries` (array of `StatusRecordData`) - `actioned_by_source` (string): Who recorded it, for example `user_action` - `budget_impact` (string): `charged` or `not_charged`, for inactive records - `end_at` (string): When it ended, UTC ISO 8601. Null while it is the current record. - `hold_allocation` (boolean): Whether the person's allocation is held - `hold_expiry` (string): Date the hold expires - `id` (string): Record id - `inactive_reason` (string): Why they were inactive - `inserted_at_utc` (string): When the record was created, UTC ISO 8601 - `notes` (string): Notes - `notice_date` (string): Date notice was given - `pws_id` (string): Sona id of the person we support - `start_at` (string): When it started, UTC ISO 8601 - `status` (string): `active` or `inactive` - `updated_at_utc` (string): When the record last changed, for example when it was ended, UTC ISO 8601 - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/people_we_support//status_records" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## List status records across your organisation `GET /api/v1/status_records` Every person's status history, newest first, for keeping a copy in sync: pass `updated_at` to get only records created or changed since your last sync, including a record being ended by a newer one. Read only. Requires demand-based rostering to be enabled for your organisation (403 otherwise). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `status` | query | string | no | `active` or `inactive` | | `from` | query | string | no | Records that started on or after this date (YYYY-MM-DD, UTC) | | `to` | query | string | no | Records that started on or before this date (YYYY-MM-DD, UTC) | | `updated_at` | query | string | no | Records created or changed on or after this time, YYYY-MM-DDThh:mm:ssZ in UTC (e.g. 2026-01-15T10:00:00Z) | ### Responses **200** Success - `entries` (array of `StatusRecordData`) - Same fields as `StatusRecordData` above. - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/status_records" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # PeopleWeSupportBudgets Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get list of people we support budget entries `GET /api/v1/people_we_support/{person_we_support_id}/budgets` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `person_we_support_id` | path | string | yes | Sona unique identifier of the person we support | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `PeopleWeSupportBudget`) - `end_date` (string, date): End date - `hours` (number, float): Hours (instances for sleep) - `id` (string): The unique identifier of PeopleWeSupport budget entry - `people_we_support_id` (string): The unique identifier of person we support - `start_date` (string, date): Start date - `type_of_work_id` (string): The unique Sona identifier of the type of care - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/people_we_support//budgets" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create people we support budget entry `POST /api/v1/people_we_support/{person_we_support_id}/budgets` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `person_we_support_id` | path | string | yes | Sona unique identifier of the person we support | ### Request body People we support budget parameters - `end_date` (string, date): End date - `hours` (number, float, required): Hours (instances for sleep) - `start_date` (string, date, required): Start date - `type_of_work_id` (string, required): The unique Sona identifier of the type of care ### Responses **201** Success Same fields as `PeopleWeSupportBudget` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity: `{"errors": ...}` when a budget field fails validation; `{"error", "message"}` for an invalid request. — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/people_we_support//budgets" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "end_date": "2024-08-01", "hours": 10.0, "start_date": "2024-07-31", "type_of_work_id": "b2b09422-c909-4198-a915-f66af3be2ec9" }' ``` ## Update people we support budget entry `PUT /api/v1/people_we_support/{person_we_support_id}/budgets/{budget_id}` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `person_we_support_id` | path | string | yes | Sona unique identifier of the person we support | | `budget_id` | path | string | yes | Sona unique identifier of the budget entry | ### Request body People we support budget parameters - `end_date` (string, date): End date - `hours` (number, float, required): Hours (instances for sleep) - `start_date` (string, date, required): Start date ### Responses **200** Success Same fields as `PeopleWeSupportBudget` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity: `{"errors": ...}` when a budget field fails validation; `{"error", "message"}` for an invalid request. — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/people_we_support//budgets/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "end_date": "2024-08-01", "hours": 10.0, "start_date": "2024-07-31" }' ``` --- # PermissionRoles Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of permission roles `GET /api/v1/permission_roles` Permission Roles ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `PermissionRole`) - `description` (string, required): Description of the role - `id` (string, required): Sona unique identifier - `location_specific` (boolean): Is the role location specific? - `name` (string, required): Name of the role - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/permission_roles" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update permission roles for an employment period `PUT /api/v1/permission_roles/employment_periods/{employment_period_id_or_idx}` Update permission roles for an employment period based on employment_period_id OR the external idx ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `employment_period_id_or_idx` | path | string | yes | Sona unique identifier of the employment period or external id | ### Request body The permission roles to update for the employment period Array of object: - `org_unit_ids` (array of string): The unique Sona identifiers of the org units - `permission_role_id` (string): The unique Sona identifier of the permission role ### Responses **200** Success, employment_period permission roles updated - `permission_roles` (array of `PermissionRole`): The updated permission roles for the employment period - Same fields as `PermissionRole` above. - `permission_roles_locations` (array of object): Mapping of location specific permission roles to org units - `org_unit_ids` (array of string): The unique Sona identifiers of the org units - `permission_role_id` (string): The unique Sona identifier of the permission role **400** Invalid parameters — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** The employment period, permission role or org unit was not found. A failed validation is also returned as 404, with a `ValidationErrors` body — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/permission_roles/employment_periods/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '[ { "org_unit_ids": [ "" ], "permission_role_id": "" } ]' ``` ## Add permission roles for an employment period `PUT /api/v1/permission_roles/{permission_role_id}/employment_periods/{employment_period_id_or_idx}` Add permission roles for an employment period based on employment_period_id OR the external idx ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `employment_period_id_or_idx` | path | string | yes | Sona unique identifier of the employment period or external id | | `permission_role_id` | path | string | yes | Sona unique identifier of the permission role to add | ### Request body The org unit IDs to associate with the permission role - `org_unit_ids` (array of string): The unique Sona identifiers of the org units ### Responses **200** Success, employment_period permission roles added Same fields as `EmploymentPeriodPermissionRolesUpdateResponse` above. **400** Invalid parameters — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** The employment period, permission role or org unit was not found. A failed validation is also returned as 404, with a `ValidationErrors` body — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/permission_roles//employment_periods/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "org_unit_ids": [ "a1b2c3d4-5678-4def-abcd-111111111111", "d4e5f6a7-8901-4bcd-efab-333333333333" ] }' ``` ## Remove permission roles for an employment period `DELETE /api/v1/permission_roles/{permission_role_id}/employment_periods/{employment_period_id_or_idx}` Remove permission roles for an employment period based on employment_period_id OR the external idx ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `employment_period_id_or_idx` | path | string | yes | Sona unique identifier of the employment period or external id | | `permission_role_id` | path | string | yes | Sona unique identifier of the permission role to remove | ### Responses **200** Success, employment_period permission roles removed Same fields as `EmploymentPeriodPermissionRolesUpdateResponse` above. **400** Invalid parameters — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** The employment period, permission role or org unit was not found. A failed validation is also returned as 404, with a `ValidationErrors` body — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/permission_roles//employment_periods/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Add org_unit to permission role for an employment period `PUT /api/v1/permission_roles/{permission_role_id}/employment_periods/{employment_period_id_or_idx}/org_units/{org_unit_id}` Add org_unit to permission role for an employment period based on employment_period_id OR the external idx ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `employment_period_id_or_idx` | path | string | yes | Sona unique identifier of the employment period or external id | | `permission_role_id` | path | string | yes | Sona unique identifier of the permission role to add org unit to | | `org_unit_id` | path | string | yes | Sona unique identifier of the org unit to add to the permission role | ### Responses **200** Success, employment_period permission roles updated Same fields as `EmploymentPeriodPermissionRolesUpdateResponse` above. **400** Invalid parameters — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** The employment period, permission role or org unit was not found. A failed validation is also returned as 404, with a `ValidationErrors` body — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/permission_roles//employment_periods//org_units/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Remove org_unit from permission role for an employment period `DELETE /api/v1/permission_roles/{permission_role_id}/employment_periods/{employment_period_id_or_idx}/org_units/{org_unit_id}` Remove org_unit from permission role for an employment period based on employment_period_id OR the external idx ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `employment_period_id_or_idx` | path | string | yes | Sona unique identifier of the employment period or external id | | `permission_role_id` | path | string | yes | Sona unique identifier of the permission role to update | | `org_unit_id` | path | string | yes | Sona unique identifier of the org unit to remove from the permission role | ### Responses **200** Success, employment_period permission roles updated Same fields as `EmploymentPeriodPermissionRolesUpdateResponse` above. **400** Invalid parameters — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** The employment period, permission role or org unit was not found. A failed validation is also returned as 404, with a `ValidationErrors` body — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/permission_roles//employment_periods//org_units/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # ProductivityCalculate Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Calculate productivity demand for an organisational unit `POST /api/v1/org_units/{org_unit_id}/productivity/calculate` Runs the productivity engine calculator with the supplied shifts, rules, metrics, and roles. Returns staffing demand per role per time interval. **Metrics behaviour:** - If `metrics` is omitted, metric data is auto-loaded from the database using `metric_id` values referenced in demand rules. The fallback order for metric values is: actual, then human forecasted, then system forecasted. - If `metrics` is provided, the supplied data is used directly (useful for what-if scenarios). **Rounding behaviour:** - If `rounding` is omitted, raw float values are returned. - Supported modes: `none`, `ceil`, `floor`, `threshold`. - `threshold` mode requires a `threshold` value between 0 and 1. **Date range limit:** `to_utc - from_utc` must not exceed 7 days. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | UUID of the organisational unit | ### Request body Calculation parameters - `from_utc` (string, required): Start of calculation window (ISO8601 UTC) - `interval_minutes` (integer): Time interval granularity in minutes (default: 30) - `metrics` (array of object): Metric data with id, name, and transactions (optional) - `roles` (array of object): Roles with id and name - `rounding` (object): Rounding configuration with mode and optional threshold - `rules` (object): Map of rule type to array of rules (7 rule types supported) - `shifts` (array of object): Shifts with role_id, start_datetime_utc, end_datetime_utc - `strategy_id` (string): Optional UUID of a labour strategy. When set, demand is calculated from the strategy's group assignments for the org unit (status-agnostic — draft, live, and archived strategies are all supported). Mutually exclusive with `rules`. - `timezone` (string): Timezone for interpreting rule local times (default: Etc/UTC) - `to_utc` (string, required): End of calculation window (ISO8601 UTC) ### Responses **200** Success - `data` (object, required): Calculation results containing role_demand_list and calculation_result **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Org unit not found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/org_units//productivity/calculate" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "from_utc": "2024-06-15T06:00:00Z", "interval_minutes": 30, "roles": [ { "id": "d3e4f5a6-7890-4abc-bcde-444444444444", "name": "Registered Nurse" } ], "rounding": { "mode": "threshold", "threshold": 0.5 }, "shifts": [ { "end_datetime_utc": "2024-06-15T15:00:00Z", "role_id": "d3e4f5a6-7890-4abc-bcde-444444444444", "start_datetime_utc": "2024-06-15T07:00:00Z" } ], "timezone": "Europe/London", "to_utc": "2024-06-15T22:00:00Z" }' ``` --- # ProductivityConfig Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Gets the organisational unit productivity config `GET /api/v1/org_units/{org_unit_id}/productivity_config` Returns the productivity configuration for the organisational unit, including rule sets and rules. Returns null if no config exists. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the organisational unit | ### Responses **200** Success - `productivity_config` (`ProductivityConfig`): The productivity config or null - `org_unit_id` (string, uuid, required): UUID of the org unit - `rounding_mode` (string; one of `none`, `ceil`, `floor`, `threshold`, required): Rounding mode for required workers - `rounding_threshold` (number, required): Threshold value for rounding (0-1) - `rule_sets` (array of `ProductivityRuleSet`, required): List of rule sets - `id` (string, uuid, required): UUID of the rule set - `name` (string, required): Name of the rule set - `rules` (array of `ProductivityRule`, required): List of rules in this set - `master_shift_type_id` (string, uuid, required): UUID of the master shift type - `metric_id` (string, uuid): UUID of the metric - `rule_params` (object, required): Rule-specific parameters - `rule_type` (string; one of `inflexible_demand_linear`, `inflexible_demand_specified`, `inflexible_cumulative_demand_fixed_tasks`, `flexible_cumulative_demand_fixed_tasks`, `inflexible_fixed_tasks`, `inflexible_cumulative_demand_minimum_staffing`, `inflexible_minimum_staffing`, `inflexible_maximum_staffing`, `flexible_fixed_tasks`, required): Type of the rule **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/org_units//productivity_config" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # PublicHolidays Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get list of public holidays `GET /api/v1/public_holidays` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `PublicHoliday`) - `country_code` (string): Country code - `date` (string, date): Holiday date - `day_of_week` (string): Day of week - `id` (string): Sona unique identifier - `is_substitute` (boolean): Substitute? - `name` (string): Public holiday name - `regions` (array of string): Country regions - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/public_holidays" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # RatesOfPay Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of rates of pay `GET /api/v1/rates_of_pay` A rate of pay defines the monetary compensation for a specific user, type of work, and organisational unit over a date range. Rates can be hourly or salaried and are linked to an employment period. Use the query parameters to filter by user, type of work, org unit, or employment period. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `user_id` | query | string | no | The user_id to filter by | | `type_of_work_id` | query | string | no | The type_of_work_id to filter results by | | `org_unit_id` | query | string | no | The org_unit_id to filter results by | | `employment_period_id` | query | string | no | The employment_period_id to filter results by | ### Responses **200** Success - `entries` (array of `RateOfPay`) - `employment_period_id` (string): The unique Sona identifier of the employment period - `employment_period_idx` (string): External identifier of the employment_period - `finish_date` (string, date): The date from when this rate of pay is no longer applicable. Expects YYYY-MM-DD format - `id` (string, required): The unique Sona identifier of the rate of pay - `idx` (string): An external identifier of the rate of pay. - `is_default` (boolean): Whether this is a default rate of pay - `label` (string): A label for the rate of pay - `org_unit_id` (string, required): The unique Sona identifier of the org_unit - `organisation_user_id` (string, required): Sona unique identifier of the user in the organisation - `pay_type` (string; one of `hourly`, `salaried`): How it is paid, e.g. hourly / salaried - `rate` (string, required): The payment rate - `start_date` (string, date, required): The date from when this rate of pay is applicable. Expects YYYY-MM-DD format - `type_of_work_id` (string, required): The unique Sona identifier of the type_of_work - `user_id` (string, required): Sona unique identifier of the user for this rate of pay - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/rates_of_pay" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a new rate of pay `POST /api/v1/rates_of_pay` Create a new rate of pay. See the RatesOfPay listing endpoint for a description of a rate of pay. One of the employment_period_id or employment_period_idx parameters is required. For organisations with UK minimum wage validation enabled, a GBP rate below the apprentice-rate hard floor (£7.55, £8.00 from April 2026) is rejected with a 422 naming the required amount on the rate field. This floor is not age-banded, so a rate that is legal for an apprentice but below the National Living Wage for the employee's age is still accepted. A salaried rate is compared on its hourly equivalent, derived from the contracted hours stored against the employment spec for the rate's start date; where none are stored the comparison is skipped. A currency that is not a recognised three-letter ISO 4217 code is also rejected with a 422. ### Request body The rate of pay parameters - `amount` (integer, required): Amount represented in lowest value in given currency, pennies/cents (e.g. amount: 1241 represents 12.41, 500 represents 5.00) - `currency` (string): The currency used - `employment_period_id` (string): The unique Sona identifier of the employment period. At least one of employment_period_id or employment_period_idx is required. - `employment_period_idx` (string): External identifier of the employment_period. At least one of employment_period_id or employment_period_idx is required. - `finish_date` (string, date): The date from when this rate of pay is no longer applicable. Expects YYYY-MM-DD format - `idx` (string): An external identifier of the rate of pay. - `is_default` (boolean): Whether this is a default rate of pay - `label` (string): A label for the rate of pay - `org_unit_id` (string): The unique Sona identifier of the org_unit - `pay_type` (string; one of `hourly`, `salaried`, required): How it is paid, e.g. hourly / salaried - `start_date` (string, date, required): The date from when this rate of pay is applicable. Expects YYYY-MM-DD format - `type_of_work_id` (string): The unique Sona identifier of the type_of_work ### Responses **200** Success Same fields as `RateOfPay` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Invalid parameters (e.g. bad date or amount, overlapping dates). `message` is a string, or for field validation failures an object of field names to arrays of error messages. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/rates_of_pay" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "amount": 1750, "currency": "GBP", "employment_period_id": "159600fb-3fa9-4687-bbc9-196375de6ee3", "employment_period_idx": "159600fb-3fa9-4687-bbc9-196375de6ee3", "finish_date": "2020-04-20", "idx": "external id", "is_default": false, "label": "Standard Rate", "org_unit_id": "c65bf5ec-5e82-4207-90cd-29caca7eae75", "pay_type": "hourly", "start_date": "2019-04-20", "type_of_work_id": "159600fb-3fa9-4687-bbc9-196375de6ee3" }' ``` ## Get an individual rate_of_pay `GET /api/v1/rates_of_pay/{rate_of_pay_id_or_idx}` Get an individual rate_of_pay. See the rate_of_pay listing endpoint for a description of rate_of_pay ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `rate_of_pay_id_or_idx` | path | string | yes | Sona unique identifier or external id of the rate of pay | | `is_idx` | query | boolean | no | If rate_of_pay_id_or_idx parameter is an external id this parameter should be set to true | ### Responses **200** Success Same fields as `RateOfPay` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/rates_of_pay/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update an existing rate of pay `PUT /api/v1/rates_of_pay/{rate_of_pay_id_or_idx}` Update an existing rate of pay. See the RatesOfPay listing endpoint for a description of a rate of pay. One of the id or idx parameters is required. To change the rate of pay's external identifier, send the new value as idx in the request body; rate_of_pay_id_or_idx in the path identifies the rate being updated. If both employment_period_id and employment_period_idx are given employment_period_id has a higher precedence. For organisations with UK minimum wage validation enabled, a GBP rate below the apprentice-rate hard floor (£7.55, £8.00 from April 2026) is rejected with a 422 naming the required amount on the rate field. This floor is not age-banded, so a rate that is legal for an apprentice but below the National Living Wage for the employee's age is still accepted. A salaried rate is compared on its hourly equivalent, derived from the contracted hours stored against the employment spec for the rate's start date; where none are stored the comparison is skipped. A currency that is not a recognised three-letter ISO 4217 code is also rejected with a 422. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `rate_of_pay_id_or_idx` | path | string | yes | Sona unique identifier or external id of the rate of pay | ### Request body The rate of pay parameters - `amount` (integer): Amount represented in lowest value in given currency, pennies/cents (e.g. amount: 1241 represents 12.41, 500 represents 5.00) - `currency` (string): The currency used - `finish_date` (string, date): The date from when this rate of pay is no longer applicable. Expects YYYY-MM-DD format - `idx` (string): A new external identifier for the rate of pay. Replaces the current idx; use the new value for later lookups by idx. - `is_default` (boolean): Whether this is a default rate of pay - `is_idx` (boolean): If rate_of_pay_id_or_idx parameter is an external id this parameter should be set to true - `label` (string): A label for the rate of pay - `org_unit_id` (string): The unique Sona identifier of the org_unit - `pay_type` (string; one of `hourly`, `salaried`): How it is paid, e.g. hourly / salaried - `start_date` (string, date): The date from when this rate of pay is applicable. Expects YYYY-MM-DD format - `type_of_work_id` (string): The unique Sona identifier of the type_of_work ### Responses **200** Success Same fields as `RateOfPay` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Invalid parameters (e.g. bad date or amount, overlapping dates). `message` is a string, or for field validation failures an object of field names to arrays of error messages. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/rates_of_pay/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "amount": 1750, "currency": "GBP", "finish_date": "2020-04-20", "idx": "new external id", "is_default": false, "is_idx": true, "label": "Standard Rate", "org_unit_id": "c65bf5ec-5e82-4207-90cd-29caca7eae75", "pay_type": "hourly", "start_date": "2019-04-20", "type_of_work_id": "159600fb-3fa9-4687-bbc9-196375de6ee3" }' ``` ## Delete a rate of pay `DELETE /api/v1/rates_of_pay/{rate_of_pay_id_or_idx}` Delete a rate of pay. This applies the same rules as deleting a rate in the Sona web client: the base (or default) rate of pay cannot be deleted, and a rate that is used by a shift or a payroll time to gross run cannot be deleted. Both are rejected with a 422 explaining why. A token restricted to specific endpoints must allow `DELETE /api/v1/rates_of_pay/:id`, otherwise the response is a 403. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `rate_of_pay_id_or_idx` | path | string | yes | Sona unique identifier or external id of the rate of pay | | `is_idx` | query | boolean | no | IF rate_of_pay_id_or_idx is an external id, this parameter should be set to true | ### Responses **204** Deleted **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity. For the base or default rate, `message` is a plain string. For a rate used by a shift or payroll run, `message` is an object of field errors, e.g. `{"id": ["Can't be deleted because it is used by one or more shifts"]}`. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/rates_of_pay/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # RightToWorkDocuments Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of right to work documents `GET /api/v1/users/{user_id}/standard_documents/right_to_work` List right to work documents for the current user ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `user_id` | path | string | yes | The Sona unique identifier of the user. | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `RightToWorkDocument`) - `cos_field_1` (string): COS field 1 - `cos_field_2` (string): COS field 2 - `country_of_issue` (string): ISO 3166 Alpha-2 code for the country of issue of the document - `date_checked` (string, date): The date when document was checked - `document_type` (string; one of `passport`, `visa`, `dbs`, `driving_license`, `eu_settlement`, `ilr`, `sia`, `share_code`, `student_visa`, `work_permit`, `external_id_check`, `skilled_worker_visa`): The type of the document - `expiry_date` (string, date): The expiration date of the document - `id` (string): The unique identifier of the right to work document entry - `issue_date` (string, date): The issue date of the document - `nationality` (string): The nationality in the document - `notes` (string): Additional notes about document - `share_code` (string): Share code - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** An ID in the path is not a valid UUID. `message` is an object mapping each invalid parameter to an error string, e.g. `{"id": "Invalid UUID format: 'abc'."}`. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/users//standard_documents/right_to_work" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a new right to work document `POST /api/v1/users/{user_id}/standard_documents/right_to_work/` Create a new right to work document entry ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `user_id` | path | string | yes | The Sona unique identifier of the user. | | `file_uploads` | formData | file | no | List of files to upload. The total upload size must not exceed 100 MB. | | `expiry_date` | formData | string, date | no | The expiration date of the document | | `issue_date` | formData | string, date | no | The issue date of the document | | `date_checked` | formData | string, date | no | The date when document was checked | | `document_type` | formData | string; one of `passport`, `visa`, `dbs`, `driving_license`, `eu_settlement`, `ilr`, `sia`, `share_code`, `student_visa`, `work_permit`, `external_id_check`, `skilled_worker_visa` | yes | The type of the document | | `country_of_issue` | formData | string | no | ISO 3166 Alpha-2 code for the country of issue of the document | | `nationality` | formData | string | no | ISO 3166 Alpha-2 code for the nationality in the document | | `share_code` | formData | string | no | Share code | | `cos_field_1` | formData | string | no | COS field 1 | | `cos_field_2` | formData | string | no | COS field 2 | | `notes` | formData | string | no | Additional notes about document | ### Responses **201** Created Same fields as `RightToWorkDocument` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** No user with this ID exists in the organisation — `ErrorResponse` (see the overview) **422** Validation failed. Field validation errors (for example an unknown `country_of_issue` or a missing date) return `{"errors": {...}}`. An ID that is not a valid UUID returns `{"error", "message"}` with `message` mapping each invalid parameter to an error string. An expiry date before the issue date or a failed file upload returns `{"error", "message"}` with a string `message`. — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/users//standard_documents/right_to_work/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -F "file_uploads=@" \ -F "expiry_date=EXPIRY_DATE" \ -F "issue_date=ISSUE_DATE" \ -F "date_checked=DATE_CHECKED" \ -F "document_type=DOCUMENT_TYPE" \ -F "country_of_issue=COUNTRY_OF_ISSUE" \ -F "nationality=NATIONALITY" \ -F "share_code=SHARE_CODE" \ -F "cos_field_1=COS_FIELD_1" \ -F "cos_field_2=COS_FIELD_2" \ -F "notes=NOTES" ``` ## Get a single right to work document. `GET /api/v1/users/{user_id}/standard_documents/right_to_work/{id}` Get data about a single right to work document for a user. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | The Sona unique identifier of the right to work document. | | `user_id` | path | string | yes | The Sona unique identifier of the user. | ### Responses **200** Success Same fields as `RightToWorkDocument` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** An ID in the path is not a valid UUID. `message` is an object mapping each invalid parameter to an error string, e.g. `{"id": "Invalid UUID format: 'abc'."}`. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/users//standard_documents/right_to_work/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update a right to work document `PUT /api/v1/users/{user_id}/standard_documents/right_to_work/{id}` Update a right to work document entry ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `user_id` | path | string | yes | The Sona unique identifier of the user. | | `id` | path | string | yes | Sona unique identifier of the right to work document entry | | `file_uploads` | formData | file | no | List of files to upload. The total upload size must not exceed 20 MB. Will overwrite existing files. | | `expiry_date` | formData | string, date | no | The expiration date of the document | | `issue_date` | formData | string, date | no | The issue date of the document | | `date_checked` | formData | string, date | no | The date when document was checked | | `document_type` | formData | string; one of `passport`, `visa`, `dbs`, `driving_license`, `eu_settlement`, `ilr`, `sia`, `share_code`, `student_visa`, `work_permit`, `external_id_check`, `skilled_worker_visa` | no | The type of the document | | `country_of_issue` | formData | string | no | ISO 3166 Alpha-2 code for the country of issue of the document | | `nationality` | formData | string | no | ISO 3166 Alpha-2 code for the nationality in the document | | `share_code` | formData | string | no | Share code | | `cos_field_1` | formData | string | no | COS field 1 | | `cos_field_2` | formData | string | no | COS field 2 | | `notes` | formData | string | no | Additional notes about document | ### Responses **200** Success Same fields as `RightToWorkDocument` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** No right to work document with this ID exists for the user — `ErrorResponse` (see the overview) **422** Validation failed. Field validation errors return `{"errors": {...}}`. An ID that is not a valid UUID returns `{"error", "message"}` with `message` mapping each invalid parameter to an error string. A failed file upload returns `{"error", "message"}` with a string `message`. — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/users//standard_documents/right_to_work/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -F "file_uploads=@" \ -F "expiry_date=EXPIRY_DATE" \ -F "issue_date=ISSUE_DATE" \ -F "date_checked=DATE_CHECKED" \ -F "document_type=DOCUMENT_TYPE" \ -F "country_of_issue=COUNTRY_OF_ISSUE" \ -F "nationality=NATIONALITY" \ -F "share_code=SHARE_CODE" \ -F "cos_field_1=COS_FIELD_1" \ -F "cos_field_2=COS_FIELD_2" \ -F "notes=NOTES" ``` ## Delete a right to work document entry `DELETE /api/v1/users/{user_id}/standard_documents/right_to_work/{id}` Delete a right to work document entry. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `user_id` | path | string | yes | The Sona unique identifier of the user. | | `id` | path | string | yes | Sona unique identifier of the right to work document entry | ### Responses **204** No Content **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** An ID in the path is not a valid UUID. `message` is an object mapping each invalid parameter to an error string. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/users//standard_documents/right_to_work/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # Services Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## List services `GET /api/v1/services` Org units with their demand-based rostering setup. Requires demand-based rostering to be enabled for your organisation (403 otherwise). `config` is the config in force today, or null. `updated_at` counts a change to the org unit, any of its configs or any of its demand periods, including a demand period being removed. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `configured` | query | boolean | no | `true` only configured services, `false` only unconfigured | | `name` | query | string | no | Case-insensitive 'contains' match on the service name | | `idx` | query | string | no | Exact match on the service `idx` (the org unit `upload_id`) | | `updated_at` | query | string | no | Only services changed on or after this UTC datetime (ISO 8601, trailing Z) | ### Responses **200** Success - `entries` (array of `ServiceData`) - `config` (`ServiceConfigData`) - `end_date` (string): ISO 8601 date - `id` (string): Config id - `inactive_reasons` (array of string): Stored out-of-service reasons. The admin screen falls back to a default list when none is set. - `max_assignment_capacity` (integer): Maximum people supported - `non_contact_label` (string): Non-contact label - `start_date` (string): ISO 8601 date - `target_assignment_count` (integer): Target people supported - `target_staff_count` (integer): Target staff count - `updated_at_utc` (string): UTC ISO 8601 - `configured` (boolean): Whether a config is in force today - `demand_periods` (array of `DemandPeriodData`): Demand periods in order - `end_time` (string): HH:MM - `id` (string): Demand period id - `name` (string): Name - `position` (integer): Order, from 0 - `start_time` (string): HH:MM - `updated_at_utc` (string): UTC ISO 8601 - `id` (string): The Sona identifier of the service (org unit) - `idx` (string): The external id (the org unit `upload_id`) - `name` (string): Service name - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/services" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get a service `GET /api/v1/services/{service_id}` One service by Sona id, or by `idx` (its `upload_id`) when `is_idx=true`. 404 if it is not in your organisation. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `service_id` | path | string | yes | Sona id, or `idx` when `is_idx=true` | | `is_idx` | query | boolean | no | Set to true to look the service up by `idx` | ### Responses **200** Success Same fields as `ServiceData` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/services/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get a service's audit log `GET /api/v1/services/{service_id}/audit_log` Field-level changes to the service's capacity, target ratio and demand periods, newest first (entries made within the same second have no defined order). `actioned_by_source` is `api` for changes made through this API. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `service_id` | path | string | yes | Sona id, or `idx` when `is_idx=true` | | `is_idx` | query | boolean | no | Set to true to look the service up by `idx` | ### Responses **200** Success - `entries` (array of `ServiceAuditEntry`) - `actioned_by_source` (string): `api`, `user`, `csv_import` and so on - `author_id` (string): The user, when a user made the change - `field_label` (string): What changed - `id` (string): Entry id - `inserted_at_utc` (string): UTC ISO 8601 - `new_value` (string): New value - `previous_value` (string): Previous value - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/services//audit_log" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create or update a service's config `PUT /api/v1/services/{service_id}/config` Creates the config (201) when the service has no open-ended one, otherwise updates it in place (200) using **only the fields you send**, so other fields are never cleared. Send an explicit null to clear an optional field. `start_date` is required when creating. To end demand-based rostering for a service, set `end_date`; there is no delete. This never creates a service. Note: the config in force today is the one returned by GET; this endpoint acts on the open-ended config (no `end_date`), which can be a config that starts in the future. Simultaneous calls for one service are handled one after another, so only the first can create. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `service_id` | path | string | yes | Sona id, or `idx` when `is_idx=true` | | `is_idx` | query | boolean | no | Set to true to look the service up by `idx` | ### Request body Config fields - `end_date` (string): ISO 8601 date, not before start_date. Ends the config. - `inactive_reasons` (array of string): Out-of-service reasons. Blank entries are ignored. - `max_assignment_capacity` (integer): Maximum people supported, greater than 0 - `non_contact_label` (string): Label for non-contact time - `start_date` (string): ISO 8601 date. Required when creating. - `target_assignment_count` (integer): Target people supported count, greater than 0 - `target_staff_count` (integer): Target staff count, greater than 0 ### Responses **200** Updated Same fields as `ServiceData` above. **201** Created Same fields as `ServiceData` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorOrValidationErrors` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/services//config" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "inactive_reasons": [ "Hospital admission", "Respite" ], "max_assignment_capacity": 12, "start_date": "2026-01-01" }' ``` ## Replace a service's demand periods `PUT /api/v1/services/{service_id}/demand_periods` **This replaces the whole list.** Any existing demand period you leave out is deleted. To keep or change an existing period, send it with its `id`; an item without an `id` is created as a new period. Periods are never matched by name, and an `id` that is not one of this service's periods is a 422. Array order sets `position`. Overnight periods are fine (`start_time` after `end_time`). Sending the same list twice changes nothing. Demand period names are unique within a service, and periods are saved one at a time in list order, so a request that gives one period a name another period still holds (for example swapping two names) is refused with a 422. Do that in two requests. A period used by a care package hour line cannot be removed (409, with the period ids in `references`). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `service_id` | path | string | yes | Sona id, or `idx` when `is_idx=true` | | `is_idx` | query | boolean | no | Set to true to look the service up by `idx` | ### Request body The full list of demand periods - `demand_periods` (array of `DemandPeriodInput`, required): All demand periods for the service - `end_time` (string, required): HH:MM. May be earlier than start_time for an overnight period. - `id` (string): Id of an existing period of this service. Omit to create a new one. - `name` (string, required): Period name, unique within the service - `start_time` (string, required): HH:MM ### Responses **200** Success Same fields as `ServiceData` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** Feature not enabled — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **409** A period is still in use — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorOrValidationErrors` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/services//demand_periods" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "demand_periods": [ { "end_time": "19:00", "id": "7a1f0c9e-2b3d-4c5e-8f60-1a2b3c4d5e6f", "name": "Day", "start_time": "07:00" }, { "end_time": "07:00", "name": "Night", "start_time": "19:00" } ] }' ``` --- # Shifts Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of shifts `GET /api/v1/shifts` A shift represents a method of accounting for a specific block of time. The most common usage is to represent a period of work to be performed. The vast majority of shifts created in the Sona platform are represented by the category `open_shifts`. The `shift_type` represents the sort of work being performed which is in turn linked to `type_of_work` which governs which users are able to see and work the shift. `min_start_datetime` and `max_start_datetime` are required and the longest timespan shifts can be listed for is 7 days. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `org_unit_ids` | query | string | no | Comma separated list of org unit IDs | | `min_start_datetime` | query | string | yes | The earliest shift start date time to return in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) | | `max_start_datetime` | query | string | yes | The latest shift start date time to return in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) | | `user_id` | query | string | no | The user_id to filter shifts by | ### Responses **200** Success - `entries` (array of `ShiftForIndex`) - `agency` (object) - `id` (string): The Sona unique identifier of the agency the shift is assigned to. This will be nil unless the shift has been explicitly assigned to an agency. - `category` (string; one of `open_shift`, `externally_managed`, `manual_assignment_only`, `assigned_to_unknown_agency_worker`, `deleted_draft`, `externally_managed_annual_leave`, `gap`, required): The type of shift being returned - `description` (string, required): The text description of the shift, or null when none is set - `draft_of_shift` (object) - `id` (string): Sona's unique identifier of the original shift that this shift is a draft of, or null when it isn't a draft - `employment_period` (object): The employment period the shift belongs to. Null when the shift has none. - `id` (string, required): Sona unique identifier of the employment period - `filling_way` (string; one of `booked`, `assigned`): How the worker was placed onto this shift once it exists as a row in Sona. Integrations can join pay lines (or other payroll artefacts) to shifts using `shift_id` and use this field to distinguish open-shift pool pickups from manager-rostered work. **booked** — The worker claimed this shift from the open shift pool (self-service). Use this value when a policy must apply only to hours that originated from the pool rather than from direct roster assignment. **assigned** — The worker was placed on the shift via manager-led rostering or another workflow Sona records as assignment (not self-service pool booking). **null** — No fill provenance is stored. This may indicate legacy data, an unclaimed shift slot, imports, or other paths that never set the field. **Do not treat `null` as evidence of either booked or assigned behaviour**; agree an explicit default with the customer where this matters (for example payroll or drawdown rules). - `finish_datetime` (string, required): The finish date time of the shift in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `id` (string, required): Sona unique identifier - `internal_id` (string): The client specific internal ID of a shift, or null when not set - `is_shift_run` (boolean, required): Indicates whether this shift is a shift run parent (true) or a regular shift (false) - `is_travel_gap` (boolean, required): Indicates whether this shift is a travel gap between shifts in a shift run. Will be true only when category is 'gap' - `org_unit` (object, required) - `id` (string, required): Sona unique identifier of the org unit - `people_we_support` (array of object): People we support (service users/assignments) linked to this shift. Returns an empty list if none are linked. - `id` (string, required): The Sona unique identifier of the person we support - `integration_idx` (string): The external integration identifier - `name` (string, required): The name of the person we support - `shift_breaks` (array of object): Breaks defined for this Shift. Returns an empty list if Shift has no breaks - `break` (object) - `duration` (integer): The duration of a break represented in minutes - `id` (string, required): The Sona unique identifier for the shift break - `shift_run_id` (string): The Sona unique identifier of the shift run parent this shift belongs to. Will be nil for regular shifts and shift run parents - `shift_type` (object, required) - `id` (string, required): The Sona unique identifier of the shift type - `name` (string): The shift type name - `start_datetime` (string, required): The start date time of the shift in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `user_working_shift` (object) - `id` (string, required): the Sona unique identifier of the user working the shift, will be nil if no user is assigned - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/shifts?min_start_datetime=&max_start_datetime=" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a new shift `POST /api/v1/shifts` Create a new shift. When overlapping employment periods are enabled, a regular worker's shift belongs to one employment period. Submit it as `employment_period.id` (find IDs with `GET /api/v1/employment_periods`). When it is omitted and exactly one of the worker's employment periods covers the shift and holds the shift type's type of work, that one is stored. When several do, the request fails with an error on `employment_period_id`. ### Request body The shift parameters - `category` (string; one of `open_shift`, `externally_managed`, `manual_assignment_only`, required): The type of shift to be created - `description` (string): The text description of the shift - `employment_period` (object): The employment period the shift belongs to. Optional. Used only for regular workers when overlapping employment periods are enabled. Find IDs with GET /api/v1/employment_periods. - `id` (string, required): Sona unique identifier of the employment period - `finish_datetime` (string, required): The finish date time of the shift in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `internal_id` (string): The client specific internal ID of a shift - `org_unit` (object, required) - `id` (string, required): Sona unique identifier of the org unit - `shift_type` (object, required) - `id` (string, required): The Sona unique identifier of the shift type - `start_datetime` (string, required): The start date time of the shift in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `user_working_shift` (object): The user assigned to work the shift. Optional — omit for open shifts with no assigned user. - `id` (string): the Sona unique identifier of the user working the shift, will be nil if no user is assigned ### Responses **200** Success, shift created - `agency` (object) - `id` (string): The Sona unique identifier of the agency the shift is assigned to. This will be nil unless the shift has been explicitly assigned to an agency. - `category` (string; one of `open_shift`, `externally_managed`, `manual_assignment_only`, `assigned_to_unknown_agency_worker`, `deleted_draft`, `externally_managed_annual_leave`, `gap`, required): The type of shift being returned - `description` (string, required): The text description of the shift, or null when none is set - `draft_of_shift` (object) - `id` (string): Sona's unique identifier of the original shift that this shift is a draft of, or null when it isn't a draft - `employment_period` (object): The employment period the shift belongs to. Null when the shift has none. - `id` (string, required): Sona unique identifier of the employment period - `filling_way` (string; one of `booked`, `assigned`): How the worker was placed onto this shift once it exists as a row in Sona. Integrations can join pay lines (or other payroll artefacts) to shifts using `shift_id` and use this field to distinguish open-shift pool pickups from manager-rostered work. **booked** — The worker claimed this shift from the open shift pool (self-service). Use this value when a policy must apply only to hours that originated from the pool rather than from direct roster assignment. **assigned** — The worker was placed on the shift via manager-led rostering or another workflow Sona records as assignment (not self-service pool booking). **null** — No fill provenance is stored. This may indicate legacy data, an unclaimed shift slot, imports, or other paths that never set the field. **Do not treat `null` as evidence of either booked or assigned behaviour**; agree an explicit default with the customer where this matters (for example payroll or drawdown rules). - `finish_datetime` (string, required): The finish date time of the shift in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `id` (string, required): Sona unique identifier - `internal_id` (string): The client specific internal ID of a shift, or null when not set - `is_shift_run` (boolean, required): Indicates whether this shift is a shift run parent (true) or a regular shift (false) - `is_travel_gap` (boolean, required): Indicates whether this shift is a travel gap between shifts in a shift run. Will be true only when category is 'gap' - `org_unit` (object, required) - `id` (string, required): Sona unique identifier of the org unit - `people_we_support` (array of object): People we support (service users/assignments) linked to this shift. Returns an empty list if none are linked. - `id` (string, required): The Sona unique identifier of the person we support - `integration_idx` (string): The external integration identifier - `name` (string, required): The name of the person we support - `rate_of_pay` (object) - `id` (string): Sona's unique identifier of the rate of pay associated with this shift, or null when none is calculated - `shift_breaks` (array of object): Breaks defined for this Shift. Returns an empty list if Shift has no breaks - `break` (object) - `duration` (integer): The duration of a break represented in minutes - `id` (string, required): The Sona unique identifier for the shift break - `shift_run_id` (string): The Sona unique identifier of the shift run parent this shift belongs to. Will be nil for regular shifts and shift run parents - `shift_type` (object, required) - `id` (string, required): The Sona unique identifier of the shift type - `name` (string): The shift type name - `start_datetime` (string, required): The start date time of the shift in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `user_working_shift` (object) - `id` (string, required): the Sona unique identifier of the user working the shift, will be nil if no user is assigned **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unable to create a shift with these parameters. `message` is an object mapping field names to lists of messages for field validation failures, or a string for rejections such as an unsupported category or an onboarding user. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/shifts" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "category": "open_shift", "description": "Description of the shift", "employment_period": { "id": "3b1e8f02-6c4d-4f7a-9d51-2a8c0e7b4f19" }, "finish_datetime": "2019-04-20T15:47:58Z", "internal_id": "RGM15RMC", "org_unit": { "id": "9f8bda89-002a-45e0-8660-46e2fff714d2" }, "shift_type": { "id": "68f1a80c-3e72-4b7e-9542-c81b964600c7" }, "start_datetime": "2019-04-20T07:47:58Z", "user_working_shift": { "id": "159600fb-3fa9-4687-bbc9-196375de6ee3" } }' ``` ## Get an individual shift `GET /api/v1/shifts/{shift_id}` Get an individual shift. See the shifts listing endpoint for more details on shifts. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_id` | path | string | yes | Sona unique identifier of the shift | ### Responses **200** Success Same fields as `Shift` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/shifts/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update an individual shift `PUT /api/v1/shifts/{shift_id}` Update an individual shift. When overlapping employment periods are enabled, an update keeps the shift's employment period while it still covers the shift and holds the shift type's type of work. Otherwise, or when a shift without one changes its dates, worker or shift type, the employment period is resolved as on create: submit `employment_period.id` when several employment periods could hold the shift. The employment period of a shift in a shift run cannot be changed through the API. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_id` | path | string | yes | Sona unique identifier of the shift | ### Request body The shift parameters Same fields as `ShiftParameters` above. ### Responses **200** Success Same fields as `Shift` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** A shift with that ID was not found — `ErrorResponse` (see the overview) **422** Could not update a shift with these parameters. `message` is an object mapping field names to lists of messages for field validation failures, or a string for rejections such as an unsupported category or an onboarding user. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/shifts/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "category": "open_shift", "description": "Description of the shift", "employment_period": { "id": "3b1e8f02-6c4d-4f7a-9d51-2a8c0e7b4f19" }, "finish_datetime": "2019-04-20T15:47:58Z", "internal_id": "RGM15RMC", "org_unit": { "id": "9f8bda89-002a-45e0-8660-46e2fff714d2" }, "shift_type": { "id": "68f1a80c-3e72-4b7e-9542-c81b964600c7" }, "start_datetime": "2019-04-20T07:47:58Z", "user_working_shift": { "id": "159600fb-3fa9-4687-bbc9-196375de6ee3" } }' ``` ## Assign an individual shift to an agency `PUT /api/v1/shifts/{shift_id}/assign_to_agency` Assign an individual shift to an agency. If none of `agency_worker_id`, `agency_worker_first_name` or `agency_worker_last_name` are provided (omitted, null or empty), the shift is assigned to the agency without a specific worker. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_id` | path | string | yes | Sona unique identifier of the shift | ### Request body Details of the worker and agency to assign the shift to - `agency_id` (string, required): The Sona unique identifier of the agency - `agency_worker_first_name` (string): If no agency worker id is provided we will try and find a worker with the given first name and last name at the provided agency, if no such user is found, they will be created. Ignored if `agency_worker_id` is provided. - `agency_worker_id` (string): The Sona unique identifier of the agency worker. If omitted, null or empty along with `agency_worker_first_name` and `agency_worker_last_name`, the shift is assigned to the agency without a specific worker yet. - `agency_worker_last_name` (string): If no agency worker id is provided we will try and find a worker with the given first name and last name at the provided agency, if no such user is found, they will be created. Ignored if `agency_worker_id` is provided. ### Responses **200** Success Same fields as `Shift` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** The shift or agency was not found (`{error, message}`). Validation failures while assigning are also returned as 404, with a `{errors}` body (known issue: they should be 422). — `ErrorOrValidationErrors` (see the overview) **422** Could not assign shift to agency — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/shifts//assign_to_agency" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "agency_id": "", "agency_worker_first_name": "", "agency_worker_id": "", "agency_worker_last_name": "" }' ``` ## Cancel an individual shift `PUT /api/v1/shifts/{shift_id}/cancel` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_id` | path | string | yes | Sona unique identifier of the shift | ### Responses **200** Success Same fields as `Shift` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** A shift with that ID was not found — `ErrorResponse` (see the overview) **422** Could not cancel a shift with these parameters — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/shifts//cancel" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Unassign an individual shift from an agency `POST /api/v1/shifts/{shift_id}/unassign_from_agency` Unassign an individual shift from an agency. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_id` | path | string | yes | Sona unique identifier of the shift | ### Responses **200** Success Same fields as `Shift` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** A shift with that ID was not found — `ErrorResponse` (see the overview) **422** Could not unassign shift from agency — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/shifts//unassign_from_agency" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # ShiftTypes Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of Shift Types `GET /api/v1/shift_types` A shift type is an org unit specific definition of a type of work. It is link to a type of work which defines which users are qualified to claim or be assigned to the shift. Shift types can have default descriptions and start + finish times. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `org_unit_id` | query | string | no | The org unit to return shift types for | ### Responses **200** Success - `entries` (array of `ShiftType`) - `color` (string; one of `amber`, `blue`, `emerald`, `fuchsia`, `green`, `indigo`, `lime`, `orange`, `pink`, `purple`, `red`, `rose`, `teal`, `violet`, `yellow`): Shift color - `default_end_time` (string): Default end time - `default_shift_break_duration` (integer): Default shift break duration - `default_start_time` (string): Default start time - `description` (string): Shift type description - `id` (string): The unique Sona identifier of the shift type - `is_absence` (boolean): Whether it's a absence shift type - `is_sleep` (boolean): Makes sure that shifts belonging to a shift_type with is_sleep aren't counted towards the contracted hours - `is_unpaid_absence_with_shifts` (boolean): Whether organisation wants to have unpaid holiday shifts on the roster - `max_shift_length_minutes` (integer): Optional maximum expected shift length in minutes (null means no rule) - `min_shift_length_minutes` (integer): Optional minimum expected shift length in minutes (null means no rule) - `name` (string): Shift type name - `night_shift` (boolean): Whether it's a night shift - `org_unit` (object): The org unit the shift type belongs to - `id` (string): The unique Sona identifier of the org_unit - `peak_shift` (boolean): Whether it's a peak shift - `shift_tags` (array of string): Shift type tag - `type_of_work` (object): The type of work of the shift type - `id` (string): The unique Sona identifier of the type of work - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/shift_types" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a Shift Type `POST /api/v1/shift_types` ### Request body The shift type parameters - `color` (string; one of `amber`, `blue`, `emerald`, `fuchsia`, `green`, `indigo`, `lime`, `orange`, `pink`, `purple`, `red`, `rose`, `teal`, `violet`, `yellow`): Shift color - `default_end_time` (string): Default end time - `default_shift_break_duration` (integer): Default shift break duration - `default_start_time` (string): Default start time - `description` (string): Shift type description - `is_absence` (boolean): Whether it's a absence shift type - `is_sleep` (boolean): Makes sure that shifts belonging to a shift_type with is_sleep aren't counted towards the contracted hours - `is_unpaid_absence_with_shifts` (boolean): Whether organisation wants to have unpaid holiday shifts on the roster - `max_shift_length_minutes` (integer): Optional maximum expected shift length in minutes (null means no rule) - `min_shift_length_minutes` (integer): Optional minimum expected shift length in minutes (null means no rule) - `name` (string, required): Shift type name - `night_shift` (boolean): Whether it's a night shift - `org_unit_id` (string, required): The unique Sona identifier of the org_unit - `peak_shift` (boolean): Whether it's a peak shift - `shift_tags` (array of string): Shift type tag - `staffing_model` (string; one of `shared`, `specified`): Staffing model - `type_of_work_id` (string, required): The unique Sona identifier of the types_of_work ### Responses **200** Success Same fields as `ShiftType` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity: `{"errors": ...}` when a field fails validation; `{"error", "message"}` with `org_unit not found` or `type_of_work not found` when `org_unit_id` or `type_of_work_id` is missing or not in your organisation. — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/shift_types" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "color": "green", "default_end_time": "21:50:00", "default_shift_break_duration": 100, "default_start_time": "07:50:00", "description": "Shift description", "is_absence": true, "is_sleep": true, "is_unpaid_absence_with_shifts": true, "max_shift_length_minutes": 720, "min_shift_length_minutes": 480, "name": "An example shift type", "night_shift": true, "org_unit_id": "e27ad021-aa0e-4ccf-91e8-1c7c84656c93", "peak_shift": true, "shift_tags": [ "tag1", "tag2" ], "staffing_model": "shared", "type_of_work_id": "e27ad021-aa0e-4ccf-91e8-1c3c84626c91" }' ``` ## Get an individual shift type `GET /api/v1/shift_types/{shift_type_id}` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_type_id` | path | string | yes | Sona unique identifier of the shift_type | ### Responses **200** Success Same fields as `ShiftType` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/shift_types/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update a Shift Type `PUT /api/v1/shift_types/{shift_type_id}` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_type_id` | path | string | yes | The Sona unique identifier of the shift type | ### Request body The shift type parameters - `color` (string; one of `amber`, `blue`, `emerald`, `fuchsia`, `green`, `indigo`, `lime`, `orange`, `pink`, `purple`, `red`, `rose`, `teal`, `violet`, `yellow`): Shift color - `default_end_time` (string): Default end time - `default_shift_break_duration` (integer): Default shift break duration - `default_start_time` (string): Default start time - `description` (string): Shift type description - `is_sleep` (boolean): Makes sure that shifts belonging to a shift_type with is_sleep aren't counted towards the contracted hours - `max_shift_length_minutes` (integer): Optional maximum expected shift length in minutes (null means no rule) - `min_shift_length_minutes` (integer): Optional minimum expected shift length in minutes (null means no rule) - `name` (string): Shift type name - `night_shift` (boolean): Whether it's a night shift - `shift_tags` (array of string): Shift type tag - `staffing_model` (string; one of `shared`, `specified`): Staffing model ### Responses **200** Success Same fields as `ShiftType` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found: the shift type doesn't exist in your organisation, or the ID is not a valid UUID. — `ErrorResponse` (see the overview) **422** Unprocessable Entity: `{"errors": ...}` when a field fails validation; `{"error", "message"}` with `org_unit not found` when `org_unit_id` is given but not in your organisation. `org_unit_id` and `type_of_work_id` can't be changed by this endpoint. — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/shift_types/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "color": "green", "staffing_model": "shared" }' ``` --- # SkillsAndAttributes Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of skills and attributes `GET /api/v1/skills_and_attributes` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of object) - `externally_managed` (boolean): Whether the skill or attribute is managed externally (e.g. by an LMS). - `id` (string): The identifier of the skill or attribute - `name` (string): Skill or attribute name - `type` (string; one of `skill`, `attribute`): Item type - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/skills_and_attributes" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create new skills and attributes `POST /api/v1/skills_and_attributes` ### Request body The skill attribute parameters - `externally_managed` (boolean): Whether the skill or attribute is managed externally (e.g. by an LMS). Defaults to false. Externally-managed entries cannot be assigned, edited or removed via the Sona UI unless the user has the appropriate admin permissions. - `idx` (string): An external identifier for the skill or attribute - `name` (string, required): Skill or attribute name - `org_unit_ids` (array of string): The identifiers of the org units the skill or attribute is applicable to. Pass an empty list to make the skill or attribute applicable to all org units. - `organisation_id` (string, required): The identifier of the organisation - `required_by_default` (boolean): Whether the skill or attribute is required by default - `show_on_roster` (boolean): Whether the skill or attribute should be shown on the roster - `show_on_shifts` (boolean): Whether the skill or attribute should be shown on shifts - `type` (string; one of `skill`, `attribute`, required): Item type ### Responses **200** Success - `externally_managed` (boolean): Whether the skill or attribute is managed externally (e.g. by an LMS). Externally-managed entries cannot be assigned, edited or removed via the Sona UI unless the user has the appropriate admin permissions. - `id` (string, required): The identifier of the skill or attribute - `idx` (string): An external identifier for the skill or attribute - `name` (string, required): Skill or attribute name - `org_unit_ids` (array of string): The identifiers of the org units the skill or attribute is applicable to - `organisation_id` (string, required): The identifier of the organisation - `required_by_default` (boolean): Whether the skill or attribute is required by default - `show_on_roster` (boolean): Whether the skill or attribute should be shown on the roster - `show_on_shifts` (boolean): Whether the skill or attribute should be shown on shifts - `type` (string; one of `skill`, `attribute`, required): Item type **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity: `{"error", "message"}`, where `message` is an object of field errors when validation fails, or a string for other rejections. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/skills_and_attributes" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "externally_managed": false, "idx": "SKILL-MH-001", "name": "Manual Handling", "org_unit_ids": [ "fefc811b-f372-4472-9f28-31fb42add989" ], "organisation_id": "278612aa-4fb2-4697-bbc9-196375de6ef5", "required_by_default": true, "show_on_roster": true, "show_on_shifts": false, "type": "skill" }' ``` ## Assigns a skill or an attribute to a user `POST /api/v1/skills_and_attributes/{skill_attribute_id_or_idx}/assign_to_user` Use this endpoint to assign a skill or an attribute to a user. Skills can be given optional start and end dates. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `skill_attribute_id_or_idx` | path | string | yes | A Sona unique identifier or an external unique identifier of the skill or attribute that is being assigned. | ### Request body The parameters of the request - `end_date` (string): The optional date on which the skill ends. Can't be used for attributes. - `start_date` (string): The optional date on which the skill starts. Can't be used for attributes. - `user_id` (string, required): The identifier of the user the skill or attribute should be assigned to ### Responses **201** Success - `end_date` (string): The optional date the skill remains valid to - `skill_attribute_id` (string): The identifier of the skill or attribute that was assigned - `start_date` (string): The optional date the skill starts - `user_id` (string): The identifier of the user the skill or attribute was assigned to **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found: `{"errors": {"user_id": ["not found"]}}` or `{"errors": {"skill_attribute_id": ["not found"]}}` when the user or skill/attribute doesn't exist in the organisation (field errors are deliberately returned as 404); `{"error", "message"}` for other missing resources. — `ErrorOrValidationErrors` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/skills_and_attributes//assign_to_user" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "end_date": "2025-06-30", "start_date": "2025-01-01", "user_id": "b9d1a3ad-a3db-4150-81c1-33c8c613271a" }' ``` ## Take back assigned skill or attribute from the user `POST /api/v1/skills_and_attributes/{skill_attribute_id_or_idx}/retract` Use this endpoint to remove a skill or an attribute from the user. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `skill_attribute_id_or_idx` | path | string | yes | A Sona unique identifier or an external unique identifier of the skill or attribute that is being retracted. | ### Request body The parameters of the request - `user_id` (string, required): The identifier of the user the skill or attribute should be removed from ### Responses **200** Success - `end_date` (string): The optional date the skill remains valid to - `skill_attribute_id` (string): The identifier of the skill or attribute that was removed - `start_date` (string): The optional date the skill starts - `user_id` (string): The identifier of the user the skill or attribute was removed **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/skills_and_attributes//retract" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "user_id": "b9d1a3ad-a3db-4150-81c1-33c8c613271a" }' ``` ## Update an assigned skill or attribute for a user `PUT /api/v1/skills_and_attributes/{skill_attribute_id_or_idx}/update_assignment` Use this endpoint to update the start date and/or end date of an existing skill or attribute assignment for a user. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `skill_attribute_id_or_idx` | path | string | yes | A Sona unique identifier or an external unique identifier of the skill or attribute. | ### Request body The parameters of the request - `end_date` (string): The new end date for the skill assignment - `start_date` (string): The new start date for the skill assignment - `user_id` (string, required): The identifier of the user whose skill or attribute assignment should be updated ### Responses **200** Success - `end_date` (string): The updated date the skill remains valid to - `skill_attribute_id` (string): The identifier of the skill or attribute that was updated - `start_date` (string): The date the skill starts - `user_id` (string): The identifier of the user **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found: `{"errors": {"user_id": ["not found"]}}` or `{"errors": {"skill_attribute_id": ["not found"]}}` when the user or skill/attribute doesn't exist in the organisation (field errors are deliberately returned as 404); `{"error", "message"}` for other missing resources. — `ErrorOrValidationErrors` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/skills_and_attributes//update_assignment" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "end_date": "2025-06-30", "start_date": "2025-01-01", "user_id": "b9d1a3ad-a3db-4150-81c1-33c8c613271a" }' ``` --- # TerminationReasons Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of Termination Reasons `GET /api/v1/termination_reasons` List the termination reasons configured for the organisation. These are the values accepted by the `termination_reason_id` field on the employment periods endpoints, so this endpoint is the source of truth for mapping a termination reason name to its Sona id. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `archived` | query | string | no | Filter by whether the termination reason is archived. If omitted, both archived and live termination reasons are returned. | ### Responses **200** Success - `entries` (array of `TerminationReason`) - `archived_at` (string, date-time): When the termination reason was archived, or null if it is still live. Sona's own interface no longer offers archived reasons when recording a new termination, but the API does not reject them: `POST /api/v1/employment_periods` accepts an archived `termination_reason_id`. Filter them out client-side if you do not want them offered. They remain readable because historic employment periods may still reference them. in the following ISO 8601 UTC format: 2019-04-20T07:47:58Z (`YYYY-MM-DDTHH:mm:SSZ`) - `id` (string, required): Sona unique identifier - `name` (string, required): The name of the termination reason - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/termination_reasons" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get an individual termination reason `GET /api/v1/termination_reasons/{termination_reason_id}` Get an individual termination reason. See the termination reason listing endpoint for a description of termination reasons. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `termination_reason_id` | path | string | yes | Sona unique identifier of the termination reason | ### Responses **200** Success Same fields as `TerminationReason` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/termination_reasons/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # TimeAndAttendancePolicy Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Gets a organisational unit's time and attendance policy `GET /api/v1/org_units/{org_unit_id}/time_and_attendance_policy` Returns the time and attendance policy of a organisational unit. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the organisational unit | ### Responses **200** Success - `attendance_enforced_from` (string, datetime, required): UTC date-time from which the T&A policy will be enforced. - `clock_in_allowed_before_in_minutes` (integer, required): The amount of minutes allowed before the clock-in. - `clock_in_and_clock_out_required` (boolean, required): Whether or not clocking in and out is required. - `clock_out_allowed_after_in_minutes` (integer, required): The amount of minutes allowed after the clock-out. - `clocking_requires_location_permission` (boolean, required): Whether or not clocking requires the worker to permit location sharing when clocking in/out. - `early_clock_in_tolerance_in_minutes` (integer, required): What's the tolerance (in minutes) for clocking in early. - `early_clock_out_tolerance_in_minutes` (integer, required): What's the tolerance (in minutes) for clocking out early. - `geotag_required` (boolean, required): Whether or not the geotag is required when clocking in/out. - `geotag_required_for_shift_runs` (boolean, required): Whether or not the geotag is required when clocking in/out of a shift run. - `late_clock_in_tolerance_in_minutes` (integer, required): What's the tolerance (in minutes) for clocking in late. - `late_clock_out_tolerance_in_minutes` (integer, required): What's the tolerance (in minutes) for clocking out late. - `location_tolerance_in_metres` (integer, required): The geotag location tolerance (in meters) when clocking in/out. - `shift_break_attendance_enabled` (boolean, required): Whether or not shift-break attendance should be enabled. - `time_and_attendance_required` (boolean, required): Whether or not the T&A is mandatory. - `unplanned_attendances_enabled` (boolean, required): Whether or not unplanned attendances should be enabled. **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/org_units//time_and_attendance_policy" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Creates an organisational unit's time and attendance policy `POST /api/v1/org_units/{org_unit_id}/time_and_attendance_policy` Creates the time and attendance policy for an organisational unit. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the organisational unit | ### Request body The organisational unit's T&A policy parameters - `attendance_enforced_from` (string, datetime, required): UTC date-time from which the T&A policy will be enforced. - `clock_in_allowed_before_in_minutes` (integer): The amount of minutes allowed before the clock-in. It will default to the orgnisation's T&A policy if it has one. Otherwise, it will be set to 15 minutes. - `clock_in_and_clock_out_required` (boolean): Whether or not clocking in and out is required. It will default to the organisation's T&A policy if it has one. Otherwise, it will be set to true. - `clock_out_allowed_after_in_minutes` (integer): The amount of minutes allowed after the clock-out. It will default to the orgnisation's T&A policy if it has one. Otherwise, it will be set to 720 minutes. - `clocking_requires_location_permission` (boolean): Whether or not clocking requires the worker to permit location sharing when clocking in/out. It will default to the organisation's T&A policy if it has one. Otherwise, it will be set to false. - `early_clock_in_tolerance_in_minutes` (integer): What's the tolerance (in minutes) for clocking in early. It will default to the organisation's T&A policy if it has one. Otherwise, it will be set to 0 minutes. - `early_clock_out_tolerance_in_minutes` (integer): What's the tolerance (in minutes) for clocking out early. It will default to the organisation's T&A policy if it has one. Otherwise, it will be set to 0 minutes. - `geotag_required` (boolean): Whether or not the geotag is required when clocking in/out. It will default to the organisation's T&A policy if it has one. Otherwise, it will be set to false. - `geotag_required_for_shift_runs` (boolean): Whether or not the geotag is required when clocking in/out of a shift run. If omitted, the existing policy's value is kept on update; on create it comes from the organisation's T&A policy, or false if there is none. - `late_clock_in_tolerance_in_minutes` (integer): What's the tolerance (in minutes) for clocking in late. It will default to the organisation's T&A policy if it has one. Otherwise, it will be set to 0 minutes. - `late_clock_out_tolerance_in_minutes` (integer): What's the tolerance (in minutes) for clocking out late. It will default to the organisation's T&A policy if it has one. Otherwise, it will be set to 0 minutes. - `location_tolerance_in_metres` (integer): The geotag location tolerance (in meters) when clocking in/out. It will default to the organisation's T&A policy if it has one. Otherwise, it will be set to 500 meters. - `shift_break_attendance_enabled` (boolean): Whether or not shift-break attendance should be enabled. It will default to the organisation's T&A policy if it has one. Otherwise, it will be set to false. - `time_and_attendance_required` (boolean): Whether or not the T&A is mandatory. It will default to the organisation's T&A policy if it has one. Otherwise, it will be set to false. - `unplanned_attendances_enabled` (boolean): Whether or not unplanned attendances should be enabled. It will default to the organisation's T&A policy if it has one. Otherwise, it will be set to false. ### Responses **200** Success Same fields as `TimeAndAttendancePolicy` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **409** Conflict — `ErrorResponse` (see the overview) **422** Unprocessable Entity: `{"errors": ...}` when a policy field fails validation; `{"error", "message"}` for an invalid `org_unit_id` ("Invalid org_unit_id"), a policy that is not an object ("Invalid time_and_attendance_policy") or missing parameters. — `ErrorOrValidationErrors` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/org_units//time_and_attendance_policy" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "attendance_enforced_from": "2025-02-27T12:34:44.932935Z", "clock_in_allowed_before_in_minutes": 15, "clock_in_and_clock_out_required": true, "clock_out_allowed_after_in_minutes": 720, "clocking_requires_location_permission": false, "early_clock_in_tolerance_in_minutes": 0, "early_clock_out_tolerance_in_minutes": 0, "geotag_required": true, "geotag_required_for_shift_runs": false, "late_clock_in_tolerance_in_minutes": 0, "late_clock_out_tolerance_in_minutes": 0, "location_tolerance_in_metres": 500, "shift_break_attendance_enabled": false, "time_and_attendance_required": false, "unplanned_attendances_enabled": false }' ``` ## Updates an organisational unit's time and attendance policy `PUT /api/v1/org_units/{org_unit_id}/time_and_attendance_policy` Updates the time and attendance policy for an organisational unit. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the organisational unit | ### Request body The organisational unit's T&A policy parameters Same fields as `TimeAndAttendancePolicyParams` above. ### Responses **200** Success Same fields as `TimeAndAttendancePolicy` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity: `{"errors": ...}` when a policy field fails validation; `{"error", "message"}` for an invalid `org_unit_id` ("Invalid org_unit_id"), a policy that is not an object ("Invalid time_and_attendance_policy") or missing parameters. — `ErrorOrValidationErrors` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/org_units//time_and_attendance_policy" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "attendance_enforced_from": "2025-02-27T12:34:44.932935Z", "clock_in_allowed_before_in_minutes": 15, "clock_in_and_clock_out_required": true, "clock_out_allowed_after_in_minutes": 720, "clocking_requires_location_permission": false, "early_clock_in_tolerance_in_minutes": 0, "early_clock_out_tolerance_in_minutes": 0, "geotag_required": true, "geotag_required_for_shift_runs": false, "late_clock_in_tolerance_in_minutes": 0, "late_clock_out_tolerance_in_minutes": 0, "location_tolerance_in_metres": 500, "shift_break_attendance_enabled": false, "time_and_attendance_required": false, "unplanned_attendances_enabled": false }' ``` ## Deletes an organisational unit's time and attendance policy `DELETE /api/v1/org_units/{org_unit_id}/time_and_attendance_policy` Deletes the time and attendance policy for an organisational unit. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `org_unit_id` | path | string | yes | Sona unique identifier of the organisational unit | ### Responses **204** Success **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity: the `org_unit_id` is not a valid UUID ("Invalid org_unit_id"). — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/org_units//time_and_attendance_policy" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Gets a shift type's time and attendance policy `GET /api/v1/shift_types/{shift_type_id}/time_and_attendance_policy` Returns the time and attendance policy of a shift type. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_type_id` | path | string | yes | Sona unique identifier of the shift type | ### Responses **200** Success Same fields as `TimeAndAttendancePolicy` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/shift_types//time_and_attendance_policy" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Creates a shift type's time and attendance policy `POST /api/v1/shift_types/{shift_type_id}/time_and_attendance_policy` Creates the time and attendance policy for a shift type. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_type_id` | path | string | yes | Sona unique identifier of the shift type | ### Request body The shift type's T&A policy parameters Same fields as `TimeAndAttendancePolicyParams` above. ### Responses **200** Success Same fields as `TimeAndAttendancePolicy` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **409** Conflict — `ErrorResponse` (see the overview) **422** Unprocessable Entity: `{"errors": ...}` when a policy field fails validation; `{"error", "message"}` for an invalid `shift_type_id` ("Invalid shift_type_id"), a policy that is not an object ("Invalid time_and_attendance_policy") or missing parameters. — `ErrorOrValidationErrors` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/shift_types//time_and_attendance_policy" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "attendance_enforced_from": "2025-02-27T12:34:44.932935Z", "clock_in_allowed_before_in_minutes": 15, "clock_in_and_clock_out_required": true, "clock_out_allowed_after_in_minutes": 720, "clocking_requires_location_permission": false, "early_clock_in_tolerance_in_minutes": 0, "early_clock_out_tolerance_in_minutes": 0, "geotag_required": true, "geotag_required_for_shift_runs": false, "late_clock_in_tolerance_in_minutes": 0, "late_clock_out_tolerance_in_minutes": 0, "location_tolerance_in_metres": 500, "shift_break_attendance_enabled": false, "time_and_attendance_required": false, "unplanned_attendances_enabled": false }' ``` ## Updates a shift type's time and attendance policy `PUT /api/v1/shift_types/{shift_type_id}/time_and_attendance_policy` Updates the time and attendance policy for a shift type. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_type_id` | path | string | yes | Sona unique identifier of the shift type | ### Request body The shift type's T&A policy parameters Same fields as `TimeAndAttendancePolicyParams` above. ### Responses **200** Success Same fields as `TimeAndAttendancePolicy` above. **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity: `{"errors": ...}` when a policy field fails validation; `{"error", "message"}` for an invalid `shift_type_id` ("Invalid shift_type_id"), a policy that is not an object ("Invalid time_and_attendance_policy") or missing parameters. — `ErrorOrValidationErrors` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/shift_types//time_and_attendance_policy" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "attendance_enforced_from": "2025-02-27T12:34:44.932935Z", "clock_in_allowed_before_in_minutes": 15, "clock_in_and_clock_out_required": true, "clock_out_allowed_after_in_minutes": 720, "clocking_requires_location_permission": false, "early_clock_in_tolerance_in_minutes": 0, "early_clock_out_tolerance_in_minutes": 0, "geotag_required": true, "geotag_required_for_shift_runs": false, "late_clock_in_tolerance_in_minutes": 0, "late_clock_out_tolerance_in_minutes": 0, "location_tolerance_in_metres": 500, "shift_break_attendance_enabled": false, "time_and_attendance_required": false, "unplanned_attendances_enabled": false }' ``` ## Deletes a shift type's time and attendance policy `DELETE /api/v1/shift_types/{shift_type_id}/time_and_attendance_policy` Deletes the time and attendance policy for a shift type. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `shift_type_id` | path | string | yes | Sona unique identifier of the shift type | ### Responses **204** Success **401** Unauthorised — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity: the `shift_type_id` is not a valid UUID ("Invalid shift_type_id"). — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/shift_types//time_and_attendance_policy" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # TrainingCourseAttendances Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of Training Course Attendances `GET /api/v1/training_course_attendances` List all training course attendances for the current organisation. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `user_id` | query | string | no | The Sona unique identifier of the user | | `user_idx` | query | string | no | The external id of the user | | `training_course_id` | query | string | no | The Sona unique identifier of the training course | | `training_course_idx` | query | string | no | The external id of the training course | ### Responses **200** Success - `entries` (array of `TrainingCourseAttendance`) - `end_date` (string): The end date of the training course attendance - `expiry_date` (string): The expiry date of the training course attendance - `id` (string): The unique identifier of the training course attendance - `idx` (string): An external unique identifier for the training course attendance - `start_date` (string): The start date of the training course attendance - `training_course_id` (string): The unique identifier of the training course - `training_course_idx` (string): The external id of the training course - `user_id` (string): The unique identifier of the user - `user_idx` (string): The external id of the user - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **400** Bad Request — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/training_course_attendances" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a new training course attendance `POST /api/v1/training_course_attendances` Create a new training course attendance. ### Request body The course attendance parameters - `end_date` (string, required): The end date of the training course attendance - `expiry_date` (string): The expiry date of the training course attendance - `idx` (string): An external unique identifier for the training course attendance - `start_date` (string, required): The start date of the training course attendance - `training_course_id` (string): The Sona unique identifier of the training course. Required if training_course_idx is not provided. - `training_course_idx` (string): The external identifier of the training course. Required if training_course_id is not provided. - `user_id` (string): The unique identifier of the user. Required if user_idx is not provided. - `user_idx` (string): The external identifier of the user. Required if user_id is not provided. ### Responses **201** Created Same fields as `TrainingCourseAttendance` above. **400** Bad Request — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** The user or training course could not be resolved: `user_id`/`user_idx` or `training_course_id`/`training_course_idx` is missing, invalid, or matches no record (or the id and idx match different records): `{"error", "message"}` with a string `message`. Field validation failures return `{"errors": {...}}` instead. — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/training_course_attendances" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "end_date": "2023-01-31", "expiry_date": "2023-12-31", "idx": "external-idx-123", "start_date": "2023-01-01", "training_course_id": "e27ad021-af0e-4ccf-95e8-1c7c84656c93", "user_id": "e27ad021-af0e-4ccf-95e8-1c7c84656c93" }' ``` ## Get an individual training course attendance `GET /api/v1/training_course_attendances/{attendance_id_or_idx}` Get an individual training course attendance. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `attendance_id_or_idx` | path | string | yes | The Sona unique identifier or external id of the training course attendance | ### Responses **200** Success Same fields as `TrainingCourseAttendance` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/training_course_attendances/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update a training course attendance `PUT /api/v1/training_course_attendances/{attendance_id_or_idx}` Update a training course attendance. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `attendance_id_or_idx` | path | string | yes | The Sona unique identifier or external id of the training course attendance | ### Request body The course attendance parameters - `end_date` (string): The end date of the training course attendance - `expiry_date` (string): The expiry date of the training course attendance - `start_date` (string): The start date of the training course attendance ### Responses **200** Success Same fields as `TrainingCourseAttendance` above. **400** Bad Request — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** No training course attendance with this id or idx — `ErrorResponse` (see the overview) **422** The attendance parameters failed validation — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/training_course_attendances/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "end_date": "2023-01-31", "expiry_date": "2023-12-31", "start_date": "2023-01-01" }' ``` ## Delete a training course attendance `DELETE /api/v1/training_course_attendances/{attendance_id_or_idx}` Delete a training course attendance. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `attendance_id_or_idx` | path | string | yes | The Sona unique identifier or external id of the training course attendance | ### Responses **204** No Content **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/training_course_attendances/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # TrainingCourses Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of Training Courses `GET /api/v1/training_courses` List all training courses for the current organisation. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `mandatory` | query | string | no | Filter by whether courses are mandatory or not. If omitted all courses are returned. | | `name` | query | string | no | Filter by course name. If omitted all courses are returned. | | `archived` | query | string | no | Filter by course status (archived or live). If omitted all courses are returned. | ### Responses **200** Success - `entries` (array of `TrainingCourse`) - `archived` (boolean): Whether the course is archived - `archived_at` (string): Course archival date, or null when the course is not archived - `description` (string): Course description, or null - `frequency` (string; one of `once`, `weekly`, `bi_weekly`, `monthly`, `quarterly`, `bi_annually`, `annually`, `every_2_years`, `every_3_years`, `every_4_years`, `every_5_years`): Course frequency, or null when not set - `id` (string): The unique identifier of the training course - `idx` (string): An external unique identifier for the training course, or null - `mandatory` (boolean): Whether the course is mandatory for employees in the organisation - `name` (string): Course name - `provider` (string): Course provider, or null - `type` (string): Course type - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **400** Bad Request — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** `mandatory` or `archived` is not 'true' or 'false' — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/training_courses" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a new training course `POST /api/v1/training_courses` Create a new training course. ### Request body The course parameters - `description` (string): The course description - `frequency` (string; one of `once`, `weekly`, `bi_weekly`, `monthly`, `quarterly`, `bi_annually`, `annually`, `every_2_years`, `every_3_years`, `every_4_years`, `every_5_years`): Course frequency - `idx` (string): External unique identifier for the course - `mandatory` (string, required): Whether the course is mandatory. Must be 'true' or 'false'. - `name` (string, required): The name of the course - `provider` (string): The course provider - `type` (string; one of `online`, `classroom`, `workshop`, `elearning`, `on_site`, `other`): Course type ### Responses **201** Created Same fields as `TrainingCourse` above. **400** Bad Request — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Invalid parameters. `mandatory` not 'true' or 'false', or a `frequency` or `type` outside the allowed values, return `{error, message}`; missing or invalid fields caught by validation return `{errors}`. — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/training_courses" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "description": "In-depth guide to safety in the workplace", "frequency": "annually", "idx": "safety-training-101", "mandatory": "true", "name": "Safety Training", "provider": "Safety Solutions Inc.", "type": "online" }' ``` ## Get an individual training course `GET /api/v1/training_courses/{course_id_or_idx}` Get an individual training course. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `course_id_or_idx` | path | string | yes | The Sona unique identifier or external id of the training course | ### Responses **200** Success Same fields as `TrainingCourse` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/training_courses/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update a training course `PUT /api/v1/training_courses/{course_id_or_idx}` Update a training course. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `course_id_or_idx` | path | string | yes | Sona unique identifier or external id of the course to update | ### Request body The course parameters - `description` (string): The course description - `frequency` (string; one of `once`, `weekly`, `bi_weekly`, `monthly`, `quarterly`, `bi_annually`, `annually`, `every_2_years`, `every_3_years`, `every_4_years`, `every_5_years`): Course frequency - `mandatory` (string): Whether the course is mandatory. Must be 'true' or 'false'. - `name` (string): The name of the course - `provider` (string): The course provider - `type` (string; one of `online`, `classroom`, `workshop`, `elearning`, `on_site`, `other`): Course type ### Responses **200** Success Same fields as `TrainingCourse` above. **400** Bad Request — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** No training course with this id or idx — `ErrorResponse` (see the overview) **422** The course parameters failed validation — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/training_courses/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "description": "Get up to date with the latest safety legislation", "frequency": "annually", "mandatory": "true", "name": "Safety Trainiing", "provider": "Safety Solutions Inc.", "type": "online" }' ``` ## Archive a training course `PUT /api/v1/training_courses/{course_id_or_idx}/archive` Archive a training course. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `course_id_or_idx` | path | string | yes | Sona unique identifier or external id of the course to archive | ### Responses **200** Success Same fields as `TrainingCourse` above. **400** Bad Request — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** No training course with this id or idx — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/training_courses//archive" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Turn an archived course back to live `PUT /api/v1/training_courses/{course_id_or_idx}/unarchive` Unarchive a training course that was previously archived. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `course_id_or_idx` | path | string | yes | Sona unique identifier or external id of the course to unarchive | ### Responses **200** Success Same fields as `TrainingCourse` above. **400** Bad Request — `ErrorResponse` (see the overview) **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** No training course with this id or idx — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/training_courses//unarchive" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # TrainingVenues Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Create a training venue `POST /api/v1/training_venues` Creates a training venue: a rosterable org unit under the given parent with the organisation's 'Training venue' org unit type, its location (coordinates from latitude and longitude when given, else converted from a what3words reference when given, else derived from the postcode; sending both coordinates and a reference is refused), the organisation-wide 'Training' type of work (reused when one exists) and a 'Training' shift type at the venue linked to it. Everything is created in one transaction: an address that fails validation leaves nothing behind. ### Request body The training venue parameters - `address_line_1` (string, required): The first line of address - `address_line_2` (string): The second line of address - `address_line_3` (string): The third line of address - `address_line_4` (string): The fourth line of address - `city` (string, required): The city the venue is in - `country_code` (string; one of `AC`, `AD`, `AE`, `AF`, `AG`, `AI`, `AL`, `AM`, `AO`, `AQ`, `AR`, `AS`, `AT`, `AU`, `AW`, `AX`, `AZ`, `BA`, `BB`, `BD`, `BE`, `BF`, `BG`, `BH`, `BI`, `BJ`, `BL`, `BM`, `BN`, `BO`, `BQ`, `BR`, `BS`, `BT`, `BV`, `BW`, `BY`, `BZ`, `CA`, `CC`, `CD`, `CF`, `CG`, `CH`, `CI`, `CK`, `CL`, `CM`, `CN`, `CO`, `CP`, `CQ`, `CR`, `CU`, `CV`, `CW`, `CX`, `CY`, `CZ`, `DE`, `DG`, `DJ`, `DK`, `DM`, `DO`, `DZ`, `EA`, `EC`, `EE`, `EG`, `EH`, `ER`, `ES`, `ET`, `EU`, `EZ`, `FI`, `FJ`, `FK`, `FM`, `FO`, `FR`, `GA`, `GB`, `GD`, `GE`, `GF`, `GG`, `GH`, `GI`, `GL`, `GM`, `GN`, `GP`, `GQ`, `GR`, `GS`, `GT`, `GU`, `GW`, `GY`, `HK`, `HM`, `HN`, `HR`, `HT`, `HU`, `IC`, `ID`, `IE`, `IL`, `IM`, `IN`, `IO`, `IQ`, `IR`, `IS`, `IT`, `JE`, `JM`, `JO`, `JP`, `KE`, `KG`, `KH`, `KI`, `KM`, `KN`, `KP`, `KR`, `KW`, `KY`, `KZ`, `LA`, `LB`, `LC`, `LI`, `LK`, `LR`, `LS`, `LT`, `LU`, `LV`, `LY`, `MA`, `MC`, `MD`, `ME`, `MF`, `MG`, `MH`, `MK`, `ML`, `MM`, `MN`, `MO`, `MP`, `MQ`, `MR`, `MS`, `MT`, `MU`, `MV`, `MW`, `MX`, `MY`, `MZ`, `NA`, `NC`, `NE`, `NF`, `NG`, `NI`, `NL`, `NO`, `NP`, `NR`, `NU`, `NZ`, `OM`, `PA`, `PE`, `PF`, `PG`, `PH`, `PK`, `PL`, `PM`, `PN`, `PR`, `PS`, `PT`, `PW`, `PY`, `QA`, `QO`, `RE`, `RO`, `RS`, `RU`, `RW`, `SA`, `SB`, `SC`, `SD`, `SE`, `SG`, `SH`, `SI`, `SJ`, `SK`, `SL`, `SM`, `SN`, `SO`, `SR`, `SS`, `ST`, `SV`, `SX`, `SY`, `SZ`, `TA`, `TC`, `TD`, `TF`, `TG`, `TH`, `TJ`, `TK`, `TL`, `TM`, `TN`, `TO`, `TR`, `TT`, `TV`, `TW`, `TZ`, `UA`, `UG`, `UM`, `UN`, `US`, `UY`, `UZ`, `VA`, `VC`, `VE`, `VG`, `VI`, `VN`, `VU`, `WF`, `WS`, `XK`, `YE`, `YT`, `ZA`, `ZM`, `ZW`): The two-letter code representing the country the venue is in - `latitude` (number): The latitude of the venue in decimal degrees, derived from the postcode unless explicitly set - `longitude` (number): The longitude of the venue in decimal degrees, derived from the postcode unless explicitly set - `name` (string, required): The name of the venue - `parent_id` (string, required): Sona unique identifier of the parent org unit, which must be an active org unit of the organisation - `postcode` (string, required): The postcode of the venue - `timezone` (string): The timezone of the venue. Defaults to Europe/London - `what3words` (string): A what3words reference (e.g. filled.count.soap) converted to the venue's coordinates; cannot be sent together with latitude and longitude. The three words are not stored or returned ### Responses **200** Success - `id` (string): Sona unique identifier of the venue org unit - `is_base_entity` (boolean): Always true: a venue is a rosterable site - `location` (`OrgUnitLocation`) - `address_line_1` (string): The first line of address - `address_line_2` (string): The second line of address - `address_line_3` (string): The third line of address - `address_line_4` (string): The fourth line of address - `city` (string): The city the location is in - `country` (string): The full country name, derived from the country code - `country_code` (string; one of `AC`, `AD`, `AE`, `AF`, `AG`, `AI`, `AL`, `AM`, `AO`, `AQ`, `AR`, `AS`, `AT`, `AU`, `AW`, `AX`, `AZ`, `BA`, `BB`, `BD`, `BE`, `BF`, `BG`, `BH`, `BI`, `BJ`, `BL`, `BM`, `BN`, `BO`, `BQ`, `BR`, `BS`, `BT`, `BV`, `BW`, `BY`, `BZ`, `CA`, `CC`, `CD`, `CF`, `CG`, `CH`, `CI`, `CK`, `CL`, `CM`, `CN`, `CO`, `CP`, `CQ`, `CR`, `CU`, `CV`, `CW`, `CX`, `CY`, `CZ`, `DE`, `DG`, `DJ`, `DK`, `DM`, `DO`, `DZ`, `EA`, `EC`, `EE`, `EG`, `EH`, `ER`, `ES`, `ET`, `EU`, `EZ`, `FI`, `FJ`, `FK`, `FM`, `FO`, `FR`, `GA`, `GB`, `GD`, `GE`, `GF`, `GG`, `GH`, `GI`, `GL`, `GM`, `GN`, `GP`, `GQ`, `GR`, `GS`, `GT`, `GU`, `GW`, `GY`, `HK`, `HM`, `HN`, `HR`, `HT`, `HU`, `IC`, `ID`, `IE`, `IL`, `IM`, `IN`, `IO`, `IQ`, `IR`, `IS`, `IT`, `JE`, `JM`, `JO`, `JP`, `KE`, `KG`, `KH`, `KI`, `KM`, `KN`, `KP`, `KR`, `KW`, `KY`, `KZ`, `LA`, `LB`, `LC`, `LI`, `LK`, `LR`, `LS`, `LT`, `LU`, `LV`, `LY`, `MA`, `MC`, `MD`, `ME`, `MF`, `MG`, `MH`, `MK`, `ML`, `MM`, `MN`, `MO`, `MP`, `MQ`, `MR`, `MS`, `MT`, `MU`, `MV`, `MW`, `MX`, `MY`, `MZ`, `NA`, `NC`, `NE`, `NF`, `NG`, `NI`, `NL`, `NO`, `NP`, `NR`, `NU`, `NZ`, `OM`, `PA`, `PE`, `PF`, `PG`, `PH`, `PK`, `PL`, `PM`, `PN`, `PR`, `PS`, `PT`, `PW`, `PY`, `QA`, `QO`, `RE`, `RO`, `RS`, `RU`, `RW`, `SA`, `SB`, `SC`, `SD`, `SE`, `SG`, `SH`, `SI`, `SJ`, `SK`, `SL`, `SM`, `SN`, `SO`, `SR`, `SS`, `ST`, `SV`, `SX`, `SY`, `SZ`, `TA`, `TC`, `TD`, `TF`, `TG`, `TH`, `TJ`, `TK`, `TL`, `TM`, `TN`, `TO`, `TR`, `TT`, `TV`, `TW`, `TZ`, `UA`, `UG`, `UM`, `UN`, `US`, `UY`, `UZ`, `VA`, `VC`, `VE`, `VG`, `VI`, `VN`, `VU`, `WF`, `WS`, `XK`, `YE`, `YT`, `ZA`, `ZM`, `ZW`): The two-letter code repesenting the country the location is in - `id` (string): Sona unique identifier - `latitude` (number): The latitude of the location in decimal degrees, derived from the postcode unless explicitly set - `longitude` (number): The longitude of the location in decimal degrees, derived from the postcode unless explicitly set - `name` (string): The name of the location - `postcode` (string): The postcode of the location - `name` (string): The name of the venue - `org_unit_type` (object) - `id` (string): Sona unique identifier of the "Training venue" org unit type - `name` (string): The name of the org unit type - `parent_org_unit` (object) - `id` (string): Sona unique identifier of the parent org unit - `timezone` (string): The timezone of the venue - `training_shift_type` (object) - `id` (string): Sona unique identifier of the venue's "Training" shift type - `name` (string): The name of the shift type - `training_type_of_work` (object) - `id` (string): Sona unique identifier of the organisation's "Training" type of work - `name` (string): The name of the type of work **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) **503** Service Unavailable — the postcode lookup timed out and nothing was saved; retry the same request — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/training_venues" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "address_line_1": "1 Training Street", "city": "Leeds", "country_code": "GB", "latitude": 53.796, "longitude": -1.547, "name": "Leeds Training Centre", "parent_id": "e27ad021-aa0e-4ccf-91e8-1c7c84656c93", "postcode": "LS1 4AP", "timezone": "Europe/London" }' ``` --- # TypesOfWork Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of Types Of Work `GET /api/v1/types_of_work` A type of work defines a kind of work a user may be qualified to do or not. The users who can claim or be assigned to a shift are defined based on matching the type of work associated with the shift type to the types of work a user has associated with them ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `TypeOfWork`) - `id` (string, required): Sona unique identifier - `idx` (string): An external ID for this type of work - `name` (string, required): The name of the type of work - `properties` (object): Additional key-value properties for this type of work - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/types_of_work" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get an individual type of work `GET /api/v1/types_of_work/{type_of_work_id}` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `type_of_work_id` | path | string | yes | Sona unique identifier of the type of work | ### Responses **200** Success Same fields as `TypeOfWork` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/types_of_work/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # User lookup Resolve a single user from an external identifier (National Insurance number, employee number, external employee id, or email) and return bundled related data. Additional endpoints in this group may be added over time. Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Lookup user and periods `GET /api/v1/users/lookup` Resolve **exactly one** user using **exactly one** of: `insurance_number`, `employee_number`, `external_id`, `contact_email`, `email`. Returns that user, their employment periods (up to 200), and `lookup.by`. HTTP status codes are documented under **Responses**. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `insurance_number` | query | string | no | UK NI number (case-insensitive). One of five identity params; use only one. | | `employee_number` | query | string | no | Employment period employee number. One of five identity params; use only one. | | `external_id` | query | string | no | Organisation user `external_employee_id`. One of five identity params; use only one. | | `contact_email` | query | string | no | User contact email. One of five identity params; use only one. | | `email` | query | string | no | Contact or work email. One of five identity params; use only one. | ### Responses **200** Success - `employment_periods` (array of `EmploymentPeriodSimple`, required): Periods for this user (start date ascending) - `active` (boolean, required): Whether or not the user is currently active - `continuous_employment_start_date` (string, date, required) - `contract_id` (string): The unique Sona identifier of the contract this employment period belongs to. Employment periods sharing a contract_id are versions of the same contract. - `employee_number` (string, required): Unique number of the employee who is working this employment_period - `employment_specs` (array of `EmploymentSpec`): related employment specs - `contract_types` (array of `ContractType`): Related contract types. Use `api/v1/contract_types` endpoint to retrieve available Contract Types. Each contract type includes an `employment_type` field with values: `full_time`, `part_time`, or `flexible`. - `booking_granularity` (string; one of `full_day`, `half_day`, `full_hour`, `half_hour`, `quarter_hour`, `minute`): Granularity for annual leave booking - `denomination` (string; one of `hours`, `days`): The denomination for holiday allowances - `employment_basis` (string): Free-text employment basis label, or null when not set - `employment_type` (string; one of `full_time`, `part_time`, `flexible`): The employment type classification for this contract. This field is a property of the contract type, not a top-level field on the employment period. - `holiday_allowance_rounding` (string; one of `full_day`, `half_day`, `full_hour`, `half_hour`, `quarter_hour`, `minute`): Rounding method for calculating holiday allowance - `id` (string, required): Sona unique identifier - `include_absence_cost` (boolean): Whether paid absence days contribute to wage cost calculations - `include_absence_hours` (boolean): Whether absence hours count towards the wage cost hours metric. Only has an effect when include_absence_cost is true - `name` (string, required): The name of the contract type - `only_worked_days` (boolean): Whether salary distributions should consider only days worked - `pay_period_start` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`, required): The day of the week the pay period starts - `pay_period_type` (string; one of `weekly`, `monthly`, required): The type of pay period - `payroll_rule` (string; one of `hourly`, `fixed_salary`, required): How the pay is paid - `worked_days_in_year` (number): Number of worked days in the year to distribute salary across when only worked days are enabled (supports decimals) - `worked_weeks_in_year` (number): Number of worked weeks in the year to distribute salary across for weekly salary distribution (supports decimals) - `contracted_hours` (array of `ContractedHour`): related contracted hours - `colour_coding` (boolean): Whether to specially highlight contracted hours on roster - `days` (number, required): Total number of contracted days in a period, or null when not set - `employment_period_id` (string): Sona unique identifier of the employment period these hours belong to. Only returned by the `/api/v1/contracted_hours` endpoints. - `employment_period_idx` (string): External identifier of the employment period these hours belong to. Only returned by the `/api/v1/contracted_hours` endpoints. - `finish_date` (string, date, required): The date from when these contracted hours are no longer applicable, or null when open-ended - `hours` (number, required): Total number of contracted hours in a period - `id` (string, required): The unique Sona identifier of the contracted hours record - `idx` (string): An external identifier of the contracted hours. - `monthly_period_starts_at` (integer, required): The day of the month a monthly period starts at - `org_unit_id` (string): The Sona unique identifier of the org unit these hours apply to - `org_units` (array of object): The org units these hours apply to. Only returned by the `/api/v1/contracted_hours` endpoints. - `period` (string; one of `weekly`, `monthly`, `rolling_sequence`, `calendar_month_daily`): The type of period - `rolling_period_sequence_length_days` (integer): The length of rolling sequence in days - `rolling_period_sequence_start` (string, date): When the rolling sequence starts - `start_date` (string, date, required): The date from when these contracted hours are applicable - `type_of_work_id` (string): The Sona unique identifier of the type of work these hours apply to - `types_of_work` (array of object): The types of work these hours apply to. Only returned by the `/api/v1/contracted_hours` endpoints. - `weekly_period_starts_at` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`, required): The day of the week a weekly period starts at - `id` (string, required): The unique Sona identifier of the employment_spec - `rates_of_pay` (array of `EmploymentPeriodRateOfPay`): related rates of pay. Use `api/v1/rates_of_pay endpoint to retrieve available Rates of Pay - `finish_date` (string, date, required): The date from when this rate of pay is no longer applicable, or null when open-ended - `id` (string, required): The unique Sona identifier of the rate of pay - `idx` (string): An external identifier of the rate of pay. - `is_default` (boolean): Whether this is a default rate of pay - `label` (string): A label for the rate of pay - `org_unit_id` (string): The unique Sona identifier of the org_unit - `pay_type` (string; one of `hourly`, `salaried`): How it is paid, e.g. hourly / salaried - `rate` (string, required): The payment rate, formatted with its currency symbol (e.g. "£12.50") - `start_date` (string, date, required): The date from when this rate of pay is applicable - `type_of_work_id` (string): The unique Sona identifier of the type_of_work - `finish_date` (string, date, required): The finish date for this period of employment, or null when it is open-ended - `forces_new_holiday_breakdown` (boolean): Whether to create a new holiday breakdown starting with this employment period. Defaults to true if the new employment period is for a rehire. - `id` (string, required): The unique Sona identifier of the employment_period - `idx` (string): External identifier of the employment_period - `job_grade_id` (string, required): The unique Sona identifier of the job grade - `job_id` (string, required): The unique Sona identifier of the job - `level_id` (string, required): The unqiue Soan identifier of the level - `organisation_id` (string): Sona unique identifier of the organisation - `organisation_user_id` (string, required): The unique Sona identifier of the organisation user record - `permission_roles` (array of `PermissionRole`): Related permission roles - `description` (string, required): Description of the role - `id` (string, required): Sona unique identifier - `location_specific` (boolean): Is the role location specific? - `name` (string, required): Name of the role - `permission_roles_locations` (array of object) - `org_unit_ids` (array of string): The unique Sona identifiers of the org units - `permission_role_id` (string): The unique Sona identifier of the permission role - `primary_org_unit_id` (string, required): The Sona unique identifier of the org unit - `raffy_tier` (string, required): The user's current Raffy credit tier: raffy_crew, raffy_gm, or raffy_hq - `reports_to_org_user_id` (string, required): [DEPRECATED: use reports_to_user_id] The unique Sona identifier of the organisation user to whom the employee reports - `reports_to_user_id` (string, required): The unique Sona identifier of the user to whom the employee reports - `start_date` (string, date, required): The start date for this period of employment - `starter_declaration` (string, required): Starter declaration (https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1106535/Starter_checklist.pdf) - `student_loan_plan_five` (boolean, required): Employee has student loan plan five - `student_loan_plan_four` (boolean, required): Employee has student loan plan four - `student_loan_plan_one` (boolean, required): Employee has student loan plan one - `student_loan_plan_two` (boolean, required): Employee has student loan plan two - `student_loan_post_graduate` (boolean, required): Employee has post graduate student loan plan - `termination_reason` (`EmploymentPeriodTerminationReason`): The reason this users employment was terminated - `details` (string): Additional context for the termination - `id` (string, required): The unique Sona identifier of the termination reason record, or null when only `details` were recorded - `name` (string): The reason, or null when only `details` were recorded - `termination_would_rehire` (boolean): Whether or not this person is eligible for rehiring following termination - `tupe` (boolean): Whether the employee has transferred under TUPE regulations - `types_of_work` (array of `TypeOfWork`): related types of work - `id` (string, required): Sona unique identifier - `idx` (string): An external ID for this type of work - `name` (string, required): The name of the type of work - `properties` (object): Additional key-value properties for this type of work - `updated_at_utc` (string, date-time): When the employment period was last updated, in UTC - `user_id` (string, required): The unique Sona identifier of the user record - `employment_periods_truncated` (boolean, required): Additional periods exist beyond this response - `lookup` (object): Echo of which query parameter was used to resolve the user - `by` (string, required): One of: insurance_number, employee_number, external_id, contact_email, email - `user` (`User`, required): Resolved user - `active` (boolean): [DEPRECATED - use `state` instead] Whether the user is considered active. - `administrative_org_unit` (object): The org unit the user will be associated with for reporting purposes - `id` (string, required): The Sona unique identifier of the org unit - `agency` (object): If the user is an agency worker, the agency they are associated with - `id` (string, required): The Sona unique identifier of the agency - `appears_on_roster_for_org_units` (array of object): Org units for which the user should appear on the roster even if they don't have shifts - `id` (string, required): The Sona unique identifier of the org unit - `assignable_org_units` (array of object): Org units for which this user can be assigned shifts by the manager - `id` (string, required): The Sona unique identifier of the org unit - `auth_strategies` (array of object): Available methods for logging into the system - `id` (string, required): The Sona unique identifier for the auth strategy - `key` (string, required): The unique key used to identify the user for this strategy - `type` (string; one of `email`, `mobile_number`, `okta`, `microsoft`, required): The strategy type - `bank_account` (object): Bank Account details. Send as null to remove. - `account_name` (string, required): Name of the Account - `account_number` (string, required): Account number - `bank_name` (string): Name of the Bank - `country_code` (string; one of `GB`, required): Country Code - only GB is supported - `sort_code` (string, required): Sort code of the bank account - `base_roles` (array of object): The underlying roles the user has in the system (independent of org unit specific roles - `name` (string; one of `manager`, `read_only_manager`, `hr_manager`, `content_manager`, `hr_and_content_manager`, `payroll_manager`, `organisation_admin`, `organisation_user_admin`, `payroll_reports_admin`, `management_dashboard_viewer`, `roster_approvals`): The name of the role - `blocked_for_org_units` (array of object): Org units for which the user is blocked. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string, required): The Sona unique identifier of the org unit - `idx` (string): External ID of the org unit - `contact_email` (string): The contact email for the user - `contact_mobile_number` (string): The contact mobile number for the user - `contact_phone_number` (string): The contact phone number for the user - `date_of_birth` (string): The DOB of the user - `exclude_from_payroll` (boolean): Boolean flag to exclude a user from payroll integration - `external_id` (string, required): The client specific external ID of a user - `final_date_of_employment` (string): User's final date of employment - `first_name` (string, required): The first name of the user - `home_address` (object): The home address of the user - `address_line_1` (string): First line the user's address - `address_line_2` (string): Second line the user's address - `address_line_3` (string): Third line the user's address - `address_line_4` (string): Fourth line the user's address - `city` (string): The city the user's address - `country` (string): The country of the user's address - `postcode` (string): The postcode of the user's address - `home_tier_org_units` (array of object): Org units for which the user appears in the primary tier (e.g. see's available shifts first) - `id` (string, required): The Sona unique identifier of the org unit - `id` (string, required): The unique Sona identifier of the user - `identifies_as` (string, required): The gender the user identifies as - `insurance_number` (string): The user's National Insurance number - `last_name` (string, required): The last name of the user - `legal_first_name` (string): The legal first name of the user - `manager_org_units` (array of object): Org units the user has full manager (write) access to - `id` (string, required): The id of the org unit - `idx` (string): External ID of the org unit - `marital_status` (string, required): Marital status of the user - `middle_name` (string, required): The middle name of the user - `next_of_kin` (array of object): Next of kinds of the user - `first_name` (string, required): The first name of the next of kin - `id` (string, required): The id of the next of kin - `last_name` (string, required): The last name of the next of kin - `location` (`NextOfKinLocation`) - `address_line_1` (string): First line of the next of kin address - `address_line_2` (string): Second line of the next of kin address - `address_line_3` (string): Third line of the next of kin address - `address_line_4` (string): Fourth line of the next of kin address - `city` (string): The city of the next of kin address - `country` (string): The country of the next of kin address - `id` (string): The id of location - `postcode` (string): The postcode of the next of kin address - `phone` (string, required): The phone number of the next of kin - `relationship` (string; one of `spouse`, `sibling`, `parent`, `child`, `sibling_of_parent`, `child_of_sibling`, `cousin`, `grandparent`, `step_sibling`, `step_parent`, `stepchild`, `sibling_in_law`, `parent_in_law`, `child_in_law`, `friend`, `partner`, `other`, required): The relationship type of the next of kin - `organisation_user_id` (string): The Sona unique identifier of the user's membership of the organisation - `pay_group_ids` (array of string): List of Sona unique identifiers of pay groups - `preferred_name` (string, required): The preferred name of the user - `read_only_org_units` (array of object): Org units the user has read only access to - `id` (string, required): The id of the org unit - `idx` (string): External ID of the org unit - `receives_content_for_org_units` (array of object): Org units for which the user receives Newsfeed content. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string, required): The Sona unique identifier of the org unit - `idx` (string): External ID of the org unit - `sex` (string, required): The sex of the user - `skills_and_attributes` (array of object): Skills and attributes for the user. The user_updated webhook currently includes only skill_and_attribute_id, start_date, and end_date for each entry; value is excluded. - `end_date` (string): The optional date on which the skill ends. Can't be used for attributes. - `name` (string): The name of the skill or attribute - `skill_and_attribute_id` (string, required): The Sona unique identifier of skill or attribute - `start_date` (string): The optional date on which the skill starts. Can't be used for attributes. - `state` (string; one of `active`, `deactivated`, `leaver`, `left`, `starting_soon`, `onboarding`): The current user state in the organisation - `title` (string): Title to refer to the user - `types_of_work` (array of object): Types of work the user is qualified to do, used for shift matching - `id` (string, required): The Sona unique identifier of the type of work - `to_remove` (boolean): Pass true to remove association - `updated_at_utc` (string, date-time): When the user or their organisation membership was last updated, whichever is later, as an ISO 8601 UTC datetime - `work_email` (string): The work email of the user - `work_phone_number` (string): The work phone number for the user **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/users/lookup" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # Users Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of users `GET /api/v1/users` A user in Sona typically represents an entity that can login to one or more components of the system. For example the manager portal or the mobile application. Additionally there are some headless users who cannot login, for example agency workers. Users by default have no access to anything and must be explicitly granted access to roles and organisational units. **Pagination**: Results are paginated. Use the `page` and `page_size` query parameters to navigate. The default page size is 10. Note: the parameter name is `page` (not `page_number`). The response includes `page_number`, `page_size`, `total_pages`, and `total_entries` metadata to assist with iteration. Example: `GET /api/v1/users?page=2&page_size=100` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `external_id` | query | string | no | The external id to filter by, useful for checking if a user with the given external id exists before deciding whether to create or update | | `employee_number` | query | string | no | The employee number to filter by (often a payroll id) | | `contact_email` | query | string | no | The employee contact email to filter by | | `email` | query | string | no | The employee email to filter by, returns a matching employee for either contact or work email | | `type` | query | string; one of `agency`, `non_agency` | no | The type of users to return (`agency` or `non_agency`), if omitted all users are returned | | `state` | query | string; one of `active`, `deactivated`, `leaver`, `left`, `starting_soon`, `onboarding` | no | The state of users to return (one of the values below), if omitted all users are returned | | `order` | query | string | no | Specify ordering with format 'field[direction]' where direction is 'asc' or 'desc' (e.g., 'updated_at[desc]'). If omitted, users are ordered by first_name, last_name for deterministic results | | `last_day_of_employment` | query | string | no | Users who have their last day of employment on this date, if omitted the default order is returned | | `insurance_number` | query | string | no | The insurance number to filter by | | `org_unit_ids` | query | string | no | Comma separated list of org unit identifiers (Sona internal identifiers or external identifiers). Only returns users with an administrative site relationship to the specified org units. | | `updated_at` | query | string | no | UTC datetime to filter users updated on or after this timestamp. Use format YYYY-MM-DDThh:mm:ssZ in UTC (e.g. 2024-01-15T10:00:00Z). | ### Responses **200** Success - `entries` (array of `User`) - `active` (boolean): [DEPRECATED - use `state` instead] Whether the user is considered active. - `administrative_org_unit` (object): The org unit the user will be associated with for reporting purposes - `id` (string, required): The Sona unique identifier of the org unit - `agency` (object): If the user is an agency worker, the agency they are associated with - `id` (string, required): The Sona unique identifier of the agency - `appears_on_roster_for_org_units` (array of object): Org units for which the user should appear on the roster even if they don't have shifts - `id` (string, required): The Sona unique identifier of the org unit - `assignable_org_units` (array of object): Org units for which this user can be assigned shifts by the manager - `id` (string, required): The Sona unique identifier of the org unit - `auth_strategies` (array of object): Available methods for logging into the system - `id` (string, required): The Sona unique identifier for the auth strategy - `key` (string, required): The unique key used to identify the user for this strategy - `type` (string; one of `email`, `mobile_number`, `okta`, `microsoft`, required): The strategy type - `bank_account` (object): Bank Account details. Send as null to remove. - `account_name` (string, required): Name of the Account - `account_number` (string, required): Account number - `bank_name` (string): Name of the Bank - `country_code` (string; one of `GB`, required): Country Code - only GB is supported - `sort_code` (string, required): Sort code of the bank account - `base_roles` (array of object): The underlying roles the user has in the system (independent of org unit specific roles - `name` (string; one of `manager`, `read_only_manager`, `hr_manager`, `content_manager`, `hr_and_content_manager`, `payroll_manager`, `organisation_admin`, `organisation_user_admin`, `payroll_reports_admin`, `management_dashboard_viewer`, `roster_approvals`): The name of the role - `blocked_for_org_units` (array of object): Org units for which the user is blocked. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string, required): The Sona unique identifier of the org unit - `idx` (string): External ID of the org unit - `contact_email` (string): The contact email for the user - `contact_mobile_number` (string): The contact mobile number for the user - `contact_phone_number` (string): The contact phone number for the user - `date_of_birth` (string): The DOB of the user - `exclude_from_payroll` (boolean): Boolean flag to exclude a user from payroll integration - `external_id` (string, required): The client specific external ID of a user - `final_date_of_employment` (string): User's final date of employment - `first_name` (string, required): The first name of the user - `home_address` (object): The home address of the user - `address_line_1` (string): First line the user's address - `address_line_2` (string): Second line the user's address - `address_line_3` (string): Third line the user's address - `address_line_4` (string): Fourth line the user's address - `city` (string): The city the user's address - `country` (string): The country of the user's address - `postcode` (string): The postcode of the user's address - `home_tier_org_units` (array of object): Org units for which the user appears in the primary tier (e.g. see's available shifts first) - `id` (string, required): The Sona unique identifier of the org unit - `id` (string, required): The unique Sona identifier of the user - `identifies_as` (string, required): The gender the user identifies as - `insurance_number` (string): The user's National Insurance number - `last_name` (string, required): The last name of the user - `legal_first_name` (string): The legal first name of the user - `manager_org_units` (array of object): Org units the user has full manager (write) access to - `id` (string, required): The id of the org unit - `idx` (string): External ID of the org unit - `marital_status` (string, required): Marital status of the user - `middle_name` (string, required): The middle name of the user - `next_of_kin` (array of object): Next of kinds of the user - `first_name` (string, required): The first name of the next of kin - `id` (string, required): The id of the next of kin - `last_name` (string, required): The last name of the next of kin - `location` (`NextOfKinLocation`) - `address_line_1` (string): First line of the next of kin address - `address_line_2` (string): Second line of the next of kin address - `address_line_3` (string): Third line of the next of kin address - `address_line_4` (string): Fourth line of the next of kin address - `city` (string): The city of the next of kin address - `country` (string): The country of the next of kin address - `id` (string): The id of location - `postcode` (string): The postcode of the next of kin address - `phone` (string, required): The phone number of the next of kin - `relationship` (string; one of `spouse`, `sibling`, `parent`, `child`, `sibling_of_parent`, `child_of_sibling`, `cousin`, `grandparent`, `step_sibling`, `step_parent`, `stepchild`, `sibling_in_law`, `parent_in_law`, `child_in_law`, `friend`, `partner`, `other`, required): The relationship type of the next of kin - `organisation_user_id` (string): The Sona unique identifier of the user's membership of the organisation - `pay_group_ids` (array of string): List of Sona unique identifiers of pay groups - `preferred_name` (string, required): The preferred name of the user - `read_only_org_units` (array of object): Org units the user has read only access to - `id` (string, required): The id of the org unit - `idx` (string): External ID of the org unit - `receives_content_for_org_units` (array of object): Org units for which the user receives Newsfeed content. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string, required): The Sona unique identifier of the org unit - `idx` (string): External ID of the org unit - `sex` (string, required): The sex of the user - `skills_and_attributes` (array of object): Skills and attributes for the user. The user_updated webhook currently includes only skill_and_attribute_id, start_date, and end_date for each entry; value is excluded. - `end_date` (string): The optional date on which the skill ends. Can't be used for attributes. - `name` (string): The name of the skill or attribute - `skill_and_attribute_id` (string, required): The Sona unique identifier of skill or attribute - `start_date` (string): The optional date on which the skill starts. Can't be used for attributes. - `state` (string; one of `active`, `deactivated`, `leaver`, `left`, `starting_soon`, `onboarding`): The current user state in the organisation - `title` (string): Title to refer to the user - `types_of_work` (array of object): Types of work the user is qualified to do, used for shift matching - `id` (string, required): The Sona unique identifier of the type of work - `to_remove` (boolean): Pass true to remove association - `updated_at_utc` (string, date-time): When the user or their organisation membership was last updated, whichever is later, as an ISO 8601 UTC datetime - `work_email` (string): The work email of the user - `work_phone_number` (string): The work phone number for the user - `page_number` (integer): The current page number (1-indexed) - `page_size` (integer): The number of items per page (default: 10) - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/users" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a new user `POST /api/v1/users` Create a new user. Some profile fields may become mandatory depending on your organisation's configuration. For example, `contact_email` is required when the `make_fields_mandatory` setting is enabled for your organisation. ### Request body The user parameters - `administrative_org_unit` (object): The org unit the user will be associated with for reporting purposes. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. Required when create_employment_period is true. - `id` (string): The Sona unique identifier of the org unit. Provide `id` or `idx`. - `idx` (string): External ID of the org unit. Provide `id` or `idx`. - `appears_on_roster_for_org_units` (array of object): Org units for which the user should appear on the roster even if they don't have shifts. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string): The Sona unique identifier of the org unit. Provide `id` or `idx`. - `idx` (string): External ID of the org unit. Provide `id` or `idx`. - `assignable_org_units` (array of object): Org units for which this user can be assigned shifts by the manager. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string): The Sona unique identifier of the org unit. Provide `id` or `idx`. - `idx` (string): External ID of the org unit. Provide `id` or `idx`. - `auth_strategies` (array of object): Available methods for logging into the system - `id` (string): Ignored on creation — there is no existing strategy to match against yet. - `key` (string, required): The unique key used to identify the user for this strategy - `type` (string; one of `email`, `mobile_number`, `okta`, `microsoft`, required): The strategy type - `bank_account` (object): Bank Account details. Send as null to remove. - `account_name` (string, required): Name of the Account - `account_number` (string, required): Account number - `bank_name` (string): Name of the Bank - `country_code` (string; one of `GB`, required): Country Code - only GB is supported - `sort_code` (string, required): Sort code of the bank account - `base_roles` (array of object): The underlying roles the user has in the system (independent of org unit specific roles). If omitted and `manager_org_units` is provided, `manager` is assigned automatically so the user can log in to the manager desktop. If omitted and only `read_only_org_units` is provided, `read_only_manager` is assigned. Pass `base_roles` explicitly to override this default (including `[]` for a user with no manager login access). - `name` (string; one of `manager`, `read_only_manager`, `hr_manager`, `content_manager`, `hr_and_content_manager`, `payroll_manager`, `organisation_admin`, `organisation_user_admin`, `payroll_reports_admin`, `management_dashboard_viewer`, `roster_approvals`): The name of the role - `blocked_for_org_units` (array of object): Org units for which the user is blocked. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string): The Sona unique identifier of the org unit. Provide `id` or `idx`. - `idx` (string): External ID of the org unit. Provide `id` or `idx`. - `contact_email` (string): The contact email of the user. Optional by default. Required on creation when the `make_fields_mandatory` setting is enabled for your organisation. - `contact_mobile_number` (string): The contact mobile number for the user - `contact_phone_number` (string): The contact phone number for the user - `create_employment_period` (boolean): When true, creates a minimal employment period for the user. Requires administrative_org_unit to be set. - `date_of_birth` (string): The DOB of the user - `employment_period` (object): Employment period configuration. Only used when create_employment_period is true. - `continuous_employment_start_date` (string, date): ISO8601 date for continuous employment start. Defaults to start_date. - `level_id` (string): The Sona unique identifier of the level. Defaults to lowest level in org hierarchy. - `start_date` (string, date): ISO8601 date for employment period start. Defaults to 9999-01-01. - `exclude_from_payroll` (boolean): Whether to exclude the user from payroll integration - `external_id` (string): The client specific external ID of a user - `first_name` (string, required): The first name of the user - `home_address` (object): The home address of the user - `address_line_1` (string): First line the user's address - `address_line_2` (string): Second line the user's address - `address_line_3` (string): Third line the user's address - `address_line_4` (string): Fourth line the user's address - `city` (string): The city the user's address - `country` (string): The country of the user's address - `postcode` (string): The postcode of the user's address - `home_tier_org_units` (array of object): Org units for which the user appears in the primary tier (e.g. see's available shifts first). To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string): The Sona unique identifier of the org unit. Provide `id` or `idx`. - `idx` (string): External ID of the org unit. Provide `id` or `idx`. - `identifies_as` (string; one of `female`, `male`, `non_binary`, `not_entered`, `other`): The gender the user identifies as - `insurance_number` (string): The UK National Insurance number of the user - `last_name` (string, required): The last name of the user - `legal_first_name` (string): The legal first name of the user - `manager_org_units` (array of object): Org units the user has full manager (write) access to - `id` (string, required): The id of the org unit - `idx` (string): External ID of the org unit - `marital_status` (string; one of `single`, `in_a_relationship`, `engaged`, `married`, `civil_partnership`, `separated`, `divorced`, `widowed`, `other`): The marital status of the user - `middle_name` (string): The middle name of the user - `next_of_kin` (array of object): User's next of kin - `first_name` (string): First Name of the User's kin - `id` (string): The id of the next of kin - `last_name` (string): Last Name of the User's kin - `location` (object) - `address_line_1` (string): First line of the next of kin address - `address_line_2` (string): Second line of the next of kin address - `address_line_3` (string): Third line of the next of kin address - `address_line_4` (string): Fourth line of the next of kin address - `city` (string): The city of the next of kin address - `country` (string): The country of the next of kin address - `postcode` (string): The postcode of the next of kin address - `phone` (string): UK Phone of next of kin - `relationship` (string; one of `spouse`, `sibling`, `parent`, `child`, `sibling_of_parent`, `child_of_sibling`, `cousin`, `grandparent`, `step_sibling`, `step_parent`, `stepchild`, `sibling_in_law`, `parent_in_law`, `child_in_law`, `friend`, `partner`, `other`): Type of relationship - `pay_group_ids` (array of string): List containing a single pay group ID to assign the user to. Omit or pass an empty list to leave the user unassigned. Only one pay group per user is supported. - `preferred_name` (string): The preferred name of the user - `read_only_org_units` (array of object): Org units the user has read only access to - `id` (string, required): The id of the org unit - `idx` (string): External ID of the org unit - `receives_content_for_org_units` (array of object): Org units for which the user receives Newsfeed content. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string): The Sona unique identifier of the org unit. Provide `id` or `idx`. - `idx` (string): External ID of the org unit. Provide `id` or `idx`. - `sex` (string; one of `female`, `male`): The sex of the user - `title` (string; one of `mr`, `mrs`, `miss`, `dr`, `ms`, `mx`): Title to refer to the user - `types_of_work` (array of object): Types of work the user is qualified to do, used for shift matching - `id` (string, required): The Sona unique identifier of the type of work - `work_email` (string): The work email of the user - `work_phone_number` (string): The work phone number for the user ### Responses **200** Success, user created Same fields as `User` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unable to create a user with these parameters. `message` is usually an array of error strings (e.g. `["insurance_number Must be a valid national insurance number."]`), or a single string for request-level errors. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/users" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "types_of_work": [ { "id": "fefc811b-f372-4472-9f28-31fb42add989" } ], "sex": "male", "pay_group_ids": [ "2de697e7-29b6-498b-b324-00aa5de7c526" ], "contact_email": "jamie_watson_smith@example.com", "insurance_number": "NI8682", "home_address": { "address_line_1": "24 Seddon Road", "address_line_2": "Manchester", "address_line_3": "", "address_line_4": "", "postcode": "MX1 2BR" }, "base_roles": [ { "name": "manager" } ], "read_only_org_units": [ { "id": "fefc811b-f372-4472-9f28-31fb42add989" } ], "home_tier_org_units": [ { "id": "fefc811b-f372-4472-9f28-31fb42add989" }, { "idx": "external_id" } ], "next_of_kin": [ { "first_name": "Sherlock", "last_name": "Holmes" }, { "first_name": "Mary", "last_name": "Morstan", "relationship": "spouse" } ], "work_phone_number": "07496756541", "work_email": "jamie_watson_smith@example.com", "exclude_from_payroll": false, "identifies_as": "non_binary", "receives_content_for_org_units": [ { "id": "fefc811b-f372-4472-9f28-31fb42add989" } ], "external_id": "123700", "auth_strategies": [ { "key": "user@example.org", "type": "email" } ], "date_of_birth": "01/02/1978", "marital_status": "single", "administrative_org_unit": { "id": "fefc811b-f379-4472-9f28-31cb41add989", "idx": "some external id" }, "middle_name": "Watson", "last_name": "Smith", "bank_account": { "account_name": "Personal", "account_number": "12345678", "bank_name": "Bank", "country_code": "GB", "sort_code": "12-34-56" }, "manager_org_units": [ { "id": "fefc811b-f379-4472-9f28-31cb41add989" }, { "id": "fefc811b-f379-4472-9f28-31fb42add989" } ], "legal_first_name": "James", "title": "dr", "contact_phone_number": "07496756541", "first_name": "James", "appears_on_roster_for_org_units": [ { "id": "fefc811b-f372-4472-9f28-31fb42add989" }, { "idx": "external_id" } ], "blocked_for_org_units": [ { "id": "4ba2cb25-2d7e-4658-95a6-1b6316a8cdd9" }, { "idx": "external_id" } ], "assignable_org_units": [ { "id": "fefc811b-f372-4472-9f28-31fb42add989" }, { "idx": "external_id" } ], "preferred_name": "Jamie", "contact_mobile_number": "07901234440" }' ``` ## Terminates an individual user's employment `PUT /api/v1/users/{user_id_or_idx}/terminate` Terminate a user's employment. This is the correct endpoint for triggering the full termination workflow — simply setting `final_date_of_employment` via `PUT /users/{id}` will not change the user's state. After termination, the user's state is derived automatically based on `final_date_of_employment`: `leaver` if the date is today or in the future, `left` if in the past. **Organisations with the overlapping employment periods feature:** termination ends every contract running on `final_date_of_employment`. Each of those employment periods gets that date as its `finish_date` and the same termination fields. A contract that already ended before that date keeps its own `finish_date`. If any of the person's employment periods starts after `final_date_of_employment`, the termination is rejected and nothing changes. To end one contract while the others continue, set a `finish_date` on its current employment period with `PUT /api/v1/employment_periods/{employment_period_id}` instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `user_id_or_idx` | path | string | yes | Sona unique identifier of the user or external id | ### Request body Termination parameters - `final_date_of_employment` (string, required): The final date of employment - `termination_reason_details` (string): Details of the termination - `termination_reason_id` (string): The Sona unique identifier of the reason for termination - `termination_would_rehire` (boolean): Would rehire the user ### Responses **200** Success - `active` (boolean): [DEPRECATED - use `state` instead] Whether the user is considered active. - `external_id` (string, required): The client specific external ID of a user - `final_date_of_employment` (string): User's final date of employment - `first_name` (string, required): The first name of the user - `id` (string, required): The unique Sona identifier of the user - `last_employment_period_id` (string): The Sona unique identifier of the user's latest employment period (the one terminated) - `last_name` (string, required): The last name of the user - `legal_first_name` (string): The legal first name of the user - `middle_name` (string, required): The middle name of the user - `state` (string; one of `active`, `deactivated`, `leaver`, `left`, `starting_soon`): The current user state. After termination: `leaver` if `final_date_of_employment` is today or in the future, `left` if it is in the past. - `termination_reason_details` (string): Details of the termination - `termination_reason_id` (string): The Sona unique identifier of the reason for termination, from the latest employment period - `termination_would_rehire` (boolean): Whether the organisation would rehire the user **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** API token does not have access to this endpoint — contact Sona to update permissions — `ErrorResponse` (see the overview) **404** A user with that ID or external ID was not found — `ErrorResponse` (see the overview) **422** Termination failed — invalid parameters, missing final_date_of_employment, or leaver workflow not enabled. Returns `ErrorResponse`, except field-level validation failures, which return `ValidationErrors`. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/users//terminate" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "final_date_of_employment": "2022-01-01", "termination_reason_details": "User resigned", "termination_reason_id": "fefc811b-f372-4472-9f28-31fb42add989", "termination_would_rehire": true }' ``` ## Undo termination of an individual user's employment `PUT /api/v1/users/{user_id_or_idx}/undo_termination` Undo a previously processed termination, restoring the user to active status. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `user_id_or_idx` | path | string | yes | Sona unique identifier of the user or external id | ### Responses **200** Success Same fields as `User` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** API token does not have access to this endpoint — contact Sona to update permissions — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/users//undo_termination" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Get an individual user `GET /api/v1/users/{user_id}` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `user_id` | path | string | yes | Sona unique identifier of the user | ### Responses **200** Success Same fields as `User` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/users/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update an individual user `PUT /api/v1/users/{user_id}` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `user_id` | path | string | yes | Sona unique identifier of the user | ### Request body The user parameters - `administrative_org_unit` (object): The org unit the user will be associated with for reporting purposes. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string): The Sona unique identifier of the org unit. Provide `id` or `idx`. - `idx` (string): External ID of the org unit. Provide `id` or `idx`. - `appears_on_roster_for_org_units` (array of object): Org units for which the user should appear on the roster even if they don't have shifts. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string, required): The Sona unique identifier of the org unit - `idx` (string): External ID of the org unit - `to_remove` (boolean): Pass true to remove association - `assignable_org_units` (array of object): Org units for which this user can be assigned shifts by the manager. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string, required): The Sona unique identifier of the org unit - `idx` (string): External ID of the org unit - `to_remove` (boolean): Pass true to remove association - `auth_strategies` (array of object): Available methods for logging into the system - `id` (string): The Sona unique identifier of an existing auth strategy. When supplied, that strategy is updated in place instead of a new one being created. - `key` (string, required): The unique key used to identify the user for this strategy - `type` (string; one of `email`, `mobile_number`, `okta`, `microsoft`, required): The strategy type - `bank_account` (object): Bank Account details. Send as null to remove. - `account_name` (string, required): Name of the Account - `account_number` (string, required): Account number - `bank_name` (string): Name of the Bank - `country_code` (string; one of `GB`, required): Country Code - only GB is supported - `sort_code` (string, required): Sort code of the bank account - `base_roles` (array of object): The underlying roles the user has in the system (independent of org unit specific roles). If omitted and the user currently has no roles, Sona will default `roles` to `manager` when `manager_org_units` are being granted, or `read_only_manager` when only `read_only_org_units` are being granted, so that the user can log in to the manager desktop. Existing roles are never overwritten — pass `base_roles` explicitly to change them. - `name` (string; one of `manager`, `read_only_manager`, `hr_manager`, `content_manager`, `hr_and_content_manager`, `payroll_manager`, `organisation_admin`, `organisation_user_admin`, `payroll_reports_admin`, `management_dashboard_viewer`, `roster_approvals`): The name of the role - `blocked_for_org_units` (array of object): Org units for which the user is blocked. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string): The Sona unique identifier of the org unit. Provide `id` or `idx`. - `idx` (string): External ID of the org unit. Provide `id` or `idx`. - `to_remove` (boolean): Pass true to remove association - `contact_email` (string): The contact email of the user - `contact_mobile_number` (string): The contact mobile number for the user - `contact_phone_number` (string): The contact phone number for the user - `date_of_birth` (string): The DOB of the user - `exclude_from_payroll` (boolean): Whether to exclude the user from payroll integration - `external_id` (string): The client specific external ID of a user - `final_date_of_employment` (string): User's final date of employment - `first_name` (string): The first name of the user - `home_address` (object): The home address of the user - `address_line_1` (string): First line the user's address - `address_line_2` (string): Second line the user's address - `address_line_3` (string): Third line the user's address - `address_line_4` (string): Fourth line the user's address - `city` (string): The city the user's address - `country` (string): The country of the user's address - `postcode` (string): The postcode of the user's address - `home_tier_org_units` (array of object): Org units for which the user appears in the primary tier (e.g. see's available shifts first). To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string, required): The Sona unique identifier of the org unit - `idx` (string): External ID of the org unit - `to_remove` (boolean): Pass true to remove association - `identifies_as` (string; one of `female`, `male`, `non_binary`, `not_entered`, `other`): The gender the user identifies as - `insurance_number` (string): The UK National Insurance number of the user - `last_name` (string): The last name of the user - `legal_first_name` (string): The legal first name of the user - `manager_org_units` (array of object): Org units the user has full manager (write) access to - `id` (string, required): The id of the org unit - `idx` (string): External ID of the org unit - `to_remove` (boolean): Pass true to remove association - `marital_status` (string; one of `single`, `in_a_relationship`, `engaged`, `married`, `civil_partnership`, `separated`, `divorced`, `widowed`, `other`): The marital status of the user - `middle_name` (string): The middle name of the user - `next_of_kin` (array of object): User's next of kin - `first_name` (string): First Name of the User's kin - `id` (string): The id of the next of kin. If empty new Next of Kin will be added - `last_name` (string): Last Name of the User's kin - `location` (object) - `address_line_1` (string): First line of the next of kin address - `address_line_2` (string): Second line of the next of kin address - `address_line_3` (string): Third line of the next of kin address - `address_line_4` (string): Fourth line of the next of kin address - `city` (string): The city of the next of kin address - `country` (string): The country of the next of kin address - `id` (string, required): The id of location - `postcode` (string): The postcode of the next of kin address - `phone` (string): UK Phone of next of kin - `relationship` (string; one of `spouse`, `sibling`, `parent`, `child`, `sibling_of_parent`, `child_of_sibling`, `cousin`, `grandparent`, `step_sibling`, `step_parent`, `stepchild`, `sibling_in_law`, `parent_in_law`, `child_in_law`, `friend`, `partner`, `other`): Type of relationship - `to_remove` (boolean): Pass true with id to remove association - `pay_group_ids` (array of string): List containing a single pay group ID to assign the user to. Omit or pass an empty list to leave the current assignment unchanged. Only one pay group per user is supported. - `preferred_name` (string): The preferred name of the user - `read_only_org_units` (array of object): Org units the user has read only access to - `id` (string, required): The id of the org unit - `idx` (string): External ID of the org unit - `to_remove` (boolean): Pass true to remove association - `receives_content_for_org_units` (array of object): Org units for which the user receives Newsfeed content. To pass a Sona identifier, pass it as %{id => sona_org_id} and to pass an external id pass it as %{idx => external_id}. - `id` (string): The Sona unique identifier of the org unit. Provide `id` or `idx`. - `idx` (string): External ID of the org unit. Provide `id` or `idx`. - `sex` (string; one of `female`, `male`): The sex of the user - `title` (string; one of `mr`, `mrs`, `miss`, `dr`, `ms`, `mx`): Title to refer to the user - `types_of_work` (array of object): Types of work the user is qualified to do, used for shift matching - `id` (string, required): The Sona unique identifier of the type of work - `to_remove` (boolean): Pass true to remove association - `work_email` (string): The work email of the user - `work_phone_number` (string): The work phone number for the user ### Responses **200** Success, user updated Same fields as `User` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** A user with that ID was not found — `ErrorResponse` (see the overview) **422** Could not update a user with these parameters. `message` is usually an array of error strings, or a single string for request-level errors. — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/users/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "administrative_org_unit": { "id": "fefc811b-f379-4472-9f28-31cb41add989", "idx": "some external ID" }, "appears_on_roster_for_org_units": [ { "id": "fefc811b-f372-4472-9f28-31fb42add989" } ], "assignable_org_units": [ { "id": "fefc811b-f372-4472-9f28-31fb42add989" } ], "auth_strategies": [ { "key": "user@example.org", "type": "email" } ], "bank_account": { "account_name": "Personal", "account_number": "12345678", "bank_name": "Bank", "country_code": "GB", "sort_code": "12-34-56" }, "base_roles": [ { "name": "manager" } ], "blocked_for_org_units": [ { "id": "4ba2cb25-2d7e-4658-95a6-1b6316a8cdd9" } ], "contact_email": "jamie_watson_smith@example.com", "contact_mobile_number": "07901234440", "contact_phone_number": "07496756541", "date_of_birth": "01/02/1978", "exclude_from_payroll": false, "external_id": "123700", "first_name": "James", "home_address": { "address_line_1": "24 Seddon Road", "address_line_2": "Manchester", "address_line_3": "", "address_line_4": "", "postcode": "MX1 2BR" }, "home_tier_org_units": [ { "id": "fefc811b-f372-4472-9f28-31fb42add989" } ], "identifies_as": "non_binary", "insurance_number": "NI8682", "last_name": "Smith", "legal_first_name": "James", "manager_org_units": [ { "id": "fefc811b-f379-4472-9f28-31cb41add989", "to_remove": true }, { "id": "435a7e93-3d41-4de2-90af-d125357aa360" } ], "marital_status": "single", "middle_name": "Watson", "next_of_kin": [ { "id": "fefc811b-f379-4472-9f28-31cb41add989", "to_remove": true }, { "first_name": "Sherlock", "id": "fefc811b-f379-4472-9f28-31cb41add989", "last_name": "Holmes" }, { "first_name": "Mary", "last_name": "Morstan", "relationship": "spouse" } ], "preferred_name": "Jamie", "read_only_org_units": [ { "id": "fefc811b-f379-4472-9f28-31cb41add989", "to_remove": true }, { "id": "435a7e93-3d41-4de2-90af-d125357aa360" } ], "receives_content_for_org_units": [ { "id": "fefc811b-f372-4472-9f28-31fb42add989" } ], "sex": "male", "title": "dr", "types_of_work": [ { "id": "fefc811b-f372-4472-9f28-31fb42add989" } ], "work_email": "jamie_watson_smith@example.com", "work_phone_number": "07496756541" }' ``` ## Activate an individual user `PUT /api/v1/users/{user_id}/activate` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `user_id` | path | string | yes | Sona unique identifier of the user | ### Responses **200** Success Same fields as `User` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** A user with that ID was not found — `ErrorResponse` (see the overview) **422** Could not activate a user with these parameters — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/users//activate" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Deactivate an individual user `PUT /api/v1/users/{user_id}/deactivate` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `user_id` | path | string | yes | Sona unique identifier of the user | ### Responses **200** Success Same fields as `User` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** A user with that ID was not found — `ErrorResponse` (see the overview) **422** Could not deactivate a user with these parameters — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/users//deactivate" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # Webhook Subscriptions Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of subscriptions `GET /api/v1/webhooks/subscriptions` A Webhook Subscription allows API clients to get notified when an event occurs. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | ### Responses **200** Success - `entries` (array of `Subscription`) - `event_type` (string; one of `shift_updated`, `user_updated`, `absence_period_created`, `absence_period_deleted`, `absence_period_updated`, `employment_period_created`, `employment_period_updated`, `employment_period_started`, `user_leaving_today`, `employment_terminated`, `user_deactivated`, `user_activated`, `absence_periods_merged`, `amendment_created`, `agency_shift_available`, `agency_shift_assigned`, `agency_shift_cancelled_by_agency`, `agency_shift_cancelled_by_client`, `agency_shift_updated`, `user_permissions_changed`, `job_created`, `job_updated`, `job_archived`, `job_restored`, `job_deleted`, `roster_period_locked`, `roster_period_unlocked`, `absence_shift_created`, `absence_shift_updated`, `absence_shift_deleted`, `roster_approval_requested`, `roster_approved`, `roster_not_approved`, `roster_site_submitted`): The event type this subscription is for - `id` (string, required): The unique Sona identifier of the subscription - `secret_key` (string, required): The secret key used to create signature, and must be used by client to verify authenticity of the request. The hash generated with HMAC using SHA256 algorithm on combination of "{timestamp}.{body}" must match with the signature in the request header. - `url` (string, required): The client API URL, where the event will be pushed to - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/webhooks/subscriptions" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a new subscription `POST /api/v1/webhooks/subscriptions` Create a new subscription for supplied event type ### Request body The subscription parameters - `event_type` (string; one of `shift_updated`, `user_updated`, `absence_period_created`, `absence_period_deleted`, `absence_period_updated`, `employment_period_created`, `employment_period_updated`, `employment_period_started`, `user_leaving_today`, `employment_terminated`, `user_deactivated`, `user_activated`, `absence_periods_merged`, `amendment_created`, `agency_shift_available`, `agency_shift_assigned`, `agency_shift_cancelled_by_agency`, `agency_shift_cancelled_by_client`, `agency_shift_updated`, `user_permissions_changed`, `job_created`, `job_updated`, `job_archived`, `job_restored`, `job_deleted`, `roster_period_locked`, `roster_period_unlocked`, `absence_shift_created`, `absence_shift_updated`, `absence_shift_deleted`, `roster_approval_requested`, `roster_approved`, `roster_not_approved`, `roster_site_submitted`, required): The event type this subscription is for - `url` (string, required): The client API URL, where the event will be pushed to ### Responses **201** Success, subscription created Same fields as `Subscription` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Unable to create a subscription with these parameters — `object` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/webhooks/subscriptions" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "event_type": "shift_updated", "url": "https://example.com/api/v1/webhook_events" }' ``` ## Delete a subscription `DELETE /api/v1/webhooks/subscriptions/{id}` Delete a webhook subscription by its ID ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | The subscription ID | ### Responses **204** Success, subscription deleted **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Subscription not found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X DELETE "https://api.sona.is/api/v1/webhooks/subscriptions/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # WorkingPreferences Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of working preferences `GET /api/v1/working_preferences` Returns a list of working preferences sorted by creation date. Allows filtering by `worker_id`. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `worker_id` | query | string | no | The worker ID to filter by | ### Responses **200** Success - `entries` (array of `WorkingPreference`) - `days` (array of object) - `available` (boolean): Whether the worker is available to work on this day - `name` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`): The name of the day of the week - `id` (string): The identifier of the working preference - `worker_id` (string): The ID of the worker the preference is for - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Invalid query parameters. A `worker_id` that isn't a UUID, or a non-integer `page`/`page_size`, returns `{errors}`; pagination values outside the allowed range return `{error, message}` with `message` as an object of field errors. — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/working_preferences" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a working preference `POST /api/v1/working_preferences` Creates a working preference ### Request body Working preference parameters - `days` (array of object, required): Days of the week the worker prefers to work - `available` (boolean): Whether the worker is available to work on this day - `name` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`): The name of the day of the week - `is_under_ewtd` (boolean): [DEPRECATED - use /api/v1/work_restrictions instead] Whether the worker is under the European Working Time Directive. Written to the worker's work restriction. - `is_under_student_visa` (boolean): [DEPRECATED - use /api/v1/work_restrictions instead] Whether the worker is under a student visa. Written to the worker's work restriction. - `worker_id` (string, required): The ID of the worker the preference is for ### Responses **200** Created. The status is 200, not 201. Same fields as `WorkingPreference` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** The worker was not found: `{"errors": {"worker_id": ["not found"]}}` — `ValidationErrors` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/working_preferences" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "days": [ { "available": true, "name": "monday" }, { "available": false, "name": "tuesday" }, { "available": true, "name": "wednesday" }, { "available": true, "name": "thursday" }, { "available": true, "name": "friday" }, { "available": false, "name": "saturday" }, { "available": false, "name": "sunday" } ], "worker_id": "b9d1a3ad-a3db-4150-81c1-33c8c613271a" }' ``` ## Update a working preference `PUT /api/v1/working_preferences/{worker_id}` Updates a working preference for the specified worker. The `worker_id` parameter should be the worker's user ID. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `worker_id` | path | string | yes | The ID of the worker whose working preference to update | ### Request body Working preference parameters - `days` (array of object): Days of the week the worker prefers to work - `available` (boolean): Whether the worker is available to work on this day - `name` (string; one of `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`): The name of the day of the week - `is_under_ewtd` (boolean): [DEPRECATED - use /api/v1/work_restrictions instead] Whether the worker is under the European Working Time Directive. Written to the worker's work restriction. - `is_under_student_visa` (boolean): [DEPRECATED - use /api/v1/work_restrictions instead] Whether the worker is under a student visa. Written to the worker's work restriction. ### Responses **200** Success Same fields as `WorkingPreference` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** The worker has no working preference yet; create one with POST. Unlike `ErrorResponse`, the body has only `error`. — `object` (see the overview) **422** Unprocessable Entity — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/working_preferences/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "days": [ { "available": true, "name": "monday" }, { "available": false, "name": "tuesday" } ] }' ``` ## Get a working preference by ID `GET /api/v1/working_preferences/{working_preference_id}` Returns a working preference by ID ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `working_preference_id` | path | string | yes | The ID of the working preference to get | ### Responses **200** Success Same fields as `WorkingPreference` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The working preference ID is invalid (for example not a UUID) — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/working_preferences/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` --- # WorkRestrictions Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a list of work restrictions `GET /api/v1/work_restrictions` Returns a list of work restrictions sorted by creation date. Allows filtering by `worker_id`. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_size` | query | integer | no | Number of elements per page | | `page` | query | integer | no | Number of the page | | `worker_id` | query | string | no | The worker ID to filter by | ### Responses **200** Success - `entries` (array of `WorkRestriction`) - `id` (string): The identifier of the work restriction - `is_night_worker` (boolean): Whether the worker is designated as a night worker (EWTD Reg 6) - `is_under_ewtd` (boolean): Whether the worker is under the European Working Time Directive - `is_under_student_visa` (boolean): Whether the worker is under a student visa - `worker_id` (string): The ID of the worker the restriction is for - `page_number` (integer): The page number - `page_size` (integer): The number of items per page - `total_entries` (integer): The total number of entities available - `total_pages` (integer): The total number of pages available **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **422** Invalid pagination or filter parameters. `message` maps each invalid parameter to its error messages — `DetailedErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/work_restrictions" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Create a work restriction `POST /api/v1/work_restrictions` Creates a work restriction for a worker ### Request body Work restriction parameters - `is_night_worker` (boolean): Whether the worker is designated as a night worker (EWTD Reg 6) - `is_under_ewtd` (boolean): Whether the worker is under the European Working Time Directive - `is_under_student_visa` (boolean): Whether the worker is under a student visa - `worker_id` (string, required): The ID of the worker the restriction is for ### Responses **200** Created, or updated if the worker already has work restrictions (this endpoint upserts and returns 200, not 201) Same fields as `WorkRestriction` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** No worker with this `worker_id` in the organisation. Unusually, the body is the field-validation shape: `{"errors": {"worker_id": ["not found"]}}` — `ValidationErrors` (see the overview) **422** `worker_id` is missing or not a valid UUID, or a flag is not a boolean — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/work_restrictions" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "is_night_worker": false, "is_under_ewtd": true, "is_under_student_visa": false, "worker_id": "b9d1a3ad-a3db-4150-81c1-33c8c613271a" }' ``` ## Get a work restriction by ID `GET /api/v1/work_restrictions/{work_restriction_id}` Returns a work restriction by ID ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `work_restriction_id` | path | string | yes | The ID of the work restriction to get | ### Responses **200** Success Same fields as `WorkRestriction` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The `id` is not a valid UUID — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/work_restrictions/" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update a work restriction `PUT /api/v1/work_restrictions/{worker_id}` Updates a work restriction for the specified worker. The `worker_id` parameter should be the worker's user ID. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `worker_id` | path | string | yes | The ID of the worker whose work restriction to update | ### Request body Work restriction parameters - `is_night_worker` (boolean): Whether the worker is designated as a night worker (EWTD Reg 6) - `is_under_ewtd` (boolean): Whether the worker is under the European Working Time Directive - `is_under_student_visa` (boolean): Whether the worker is under a student visa ### Responses **200** Success Same fields as `WorkRestriction` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** The worker has no work restrictions yet (create them with POST). Unusually, the body has only `error` and no `message`: `{"error": "Work restriction not found for this worker. Use POST to create."}` — `MetricsErrorResponse` (see the overview) **422** The worker ID is not a valid UUID, or a flag is not a boolean — `ValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X PUT "https://api.sona.is/api/v1/work_restrictions/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "is_night_worker": false, "is_under_ewtd": false, "is_under_student_visa": true }' ``` --- # YapsterMigration Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get a user's Yapster migration info `POST /api/v1/yapster/users/{external_employee_id}` Returns all the information required by the Yapster client to migrate a user from the Yapster app to the Sona app. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `external_employee_id` | path | string | yes | Customer employee ID | ### Request body Yapster User confirmed phone numbers - `yapster_confirmed_phones` (array of string) ### Responses **200** Success - `migration_info` (object) - `auth_strategy_phones` (array of string): List of authentication phone numbers - `external_employee_id` (string): Customer employee ID - `iat` (string): Issued at timestamp - `matched_phones` (array of string): List of Yapster confirmed phone numbers matched on Sona user - `migration_token` (string): Migration token - `organisation_id` (string): Sona Organisation ID - `user_id` (string): Sona User ID **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/yapster/users/" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "yapster_confirmed_phones": [ "+447700900123", "+447700900456" ] }' ``` --- # YearlyHolidayAllowance Base URL: `https://api.sona.is`. All requests need an `Authorization: Bearer ` header. ## Get user's holiday allowance for a year `GET /api/v1/yearly_holiday_allowance` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `user_id` | query | string | yes | Sona unique identifier of the user to get the holiday allowance | | `start_date` | query | string | no | The start date holiday year for which to set the entitlement in the following ISO 8601 UTC format: 2019-04-20 (`YYYY-MM-DD`) | | `contract_id` | query | string | no | The Sona unique identifier of the contract the request is about, from `contract_id` on `GET /api/v1/employment_periods`. Only used by organisations with overlapping employment periods enabled. Required when the user holds employment periods on several contracts covering the requested date; without it the only contract covering that date is used. A contract whose employment periods have ended still reads its own absence year covering the date. | ### Responses **200** Success Array of `YearlyHolidayAllowance`: - `contract_id` (string): The unique Sona identifier of the contract of the absence year, or null for an absence year without a contract - `denomination` (string; one of `hours`, `days`): Absence year denomination - `id` (string): The unique identifier of the absence year - `start_date` (string): The start date of the holiday year - `user_id` (string, required): The unique Sona identifier of the user entity - `yearly_holiday_allowance` (integer): The total yearly holiday allowance for the worker **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** Unprocessable Entity — `ErrorResponse` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X GET "https://api.sona.is/api/v1/yearly_holiday_allowance?user_id=" \ -H "Authorization: Bearer $SONA_API_TOKEN" ``` ## Update a user's holiday allowance for a year `POST /api/v1/yearly_holiday_allowance` ### Request body The holiday allowance parameters - `contract_id` (string): The Sona unique identifier of the contract the request is about, from `contract_id` on `GET /api/v1/employment_periods`. Only used by organisations with overlapping employment periods enabled. Required when the user holds employment periods on several contracts covering the requested date; without it the only contract covering that date is used. A contract whose employment periods have ended still reads its own absence year covering the date. - `start_date` (string, required): The start date holiday year for which to set the entitlement in the following ISO 8601 UTC format: 2019-04-20 (`YYYY-MM-DD`) - `user_id` (string, required): Sona unique identifier of the user for the holiday allowance - `yearly_allowance` (integer, required): The total yearly holiday allowance for the worker for a year ### Responses **200** Success, absence updated Same fields as `YearlyHolidayAllowance` above. **401** The Bearer token was missing or not valid. Returned by every endpoint before the request is processed. — `ErrorResponse` (see the overview) **403** The API token is valid but restricted to other endpoints. Ask Sona to add this endpoint to the token's allowed list. — `ErrorResponse` (see the overview) **404** Not Found — `ErrorResponse` (see the overview) **422** The allowance can't be set. Request-level problems return `{error, message}` with a string `message` (e.g. the user isn't employed on `start_date`, `contract_id` doesn't apply, or `start_date` isn't a date); field validation failures return `{errors}` — `ErrorOrValidationErrors` (see the overview) **500** Server Error — `ErrorResponse` (see the overview) ### Example ```sh curl -X POST "https://api.sona.is/api/v1/yearly_holiday_allowance" \ -H "Authorization: Bearer $SONA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "contract_id": "4f5c2a36-8f6e-4a0a-9d8e-2b1c7f3e6a51", "start_date": "2019-04-20", "user_id": "9f8bda89-002a-45e0-8660-46e2fff714d2", "yearly_allowance": 28 }' ```