# Introduction
> Welcome to the Untether Developer Docs
Get started with the Untether Labs API
## Using the API
Build robust applications using the Untether Labs API.
Handle large response sets with pagination.
Respond to common and unexpected errors in your API requests.
A comprehensive list of all expected API errors and their meanings.
Learn how to create historical corrections for signed-off pay periods.
## Guides
Familiarize yourself with common tasks and workflows.
Learn how to create and delete timecard punches.
## API Versioning
Breaking changes are introduced as new versioned endpoints. Before any endpoint is sunset, we'll publish a deprecation notice and reach out to affected customers with a migration timeline.
Every response includes an `X-API-Version` header identifying the build that served it, useful for correlating issues against your own logs:
```
X-API-Version: 2026-05-14.a3f2c1d
```
You can also query the current deployment at `GET /version` (unauthenticated):
```json
{
"version": "2026-05-14.a3f2c1d",
"deployedAt": "2026-05-14T18:23:47Z"
}
```
`version` matches the header; `deployedAt` is an ISO 8601 UTC timestamp of when the build was deployed.
## Need Help?
Have questions or need assistance? We're here to help!
[Shoot us an email](mailto:support@untetherlabs.com)
---
# Accessing the API
> Learn how to access the Untether Labs API
The Untether Labs API uses OAuth 2.0 `client_credentials` grant for machine to machine authentication.
You may read more information on this flow [here](https://www.oauth.com/oauth2-servers/access-tokens/client-credentials/).
## Obtaining Credentials
Please [reach out](mailto:support@untetherlabs.com) to create a bot account for your company.
Once approved and created, you will receieve two credentials:
- **Client ID**: A public identifier for your application.
- **Client Secret**: A secret known only to your application and the authorization server.
## Access Token Exchange
The Client ID and Client Secret do not provide access to the API by themselves, you must
exchange them for a short-lived access token. This is accomplished by sending them to the
`/oauth/token` endpoint:
```bash Token Request icon=file-terminal
curl -X POST --location "https://app.untetherlabs.com/api/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'grant_type=client_credentials&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}'
```
```json Token Response icon=braces
{
"access_token": "{ACCESS_TOKEN}",
"token_type": "Bearer",
"expires_at": "{ISO TIMESTAMP}"
}
```
Machine access tokens are short lived (1 hour by default). You may repeat the token
exchange at any time to obtain a new access token.
## Your First Request
With an access token, you are now free to make requests to the API any time before `expires_at`.
For example, we can fetch our own user information:
```bash First API Request icon=file-terminal
curl -X GET --location "https://app.untetherlabs.com/api/v1/users/{CLIENT_ID}" \
-H "Authorization: Bearer {ACCESS_TOKEN}"
```
```json Get User Response icon=braces
{
"id": "{CLIENT_ID}",
"type": "bot",
"name": "My API Client",
"email": "api@untetherlabs.com",
"status": "active"
}
```
For more information about this endpoint, see `GET /v1/users/{userId}` in the API
reference.
---
# Pagination
> Learn how to handle large response sets with pagination.
The Untether Labs API uses cursor-based pagination for most endpoints which return lists of items.
This approach provides efficient and consistent navigation through result sets.
## Pagination Structure
All paginated endpoints return responses in the following format:
```json Paginated Response Structure icon=braces
{
"items": [
// Array of returned items
],
"cursor": ""
}
```
### Response Fields
An array containing the requested data items for the current page
A string token for fetching the next page, or `null` if this is the last page
The cursor value should be treated as an opaque string. The format is subject to
change without notice, clients should not attempt to decode or modify it.
## Making Paginated Requests
### Initial Request
Start by making a request to any paginated endpoint without a cursor parameter:
```bash Initial Paginated Request icon=file-terminal
curl -X GET --location "https://app.untetherlabs.com/api/v1/pay-codes" \
-H "Authorization: Bearer {ACCESS_TOKEN}"
```
```json Initial Response icon=braces
{
"items": [
{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "Overtime",
"shorthandName": "OT",
"hexColor": "#f97316",
"type": "hours",
"value": 1.5
}
// ... more items
],
"cursor": "my-opaque-cursor"
}
```
### Subsequent Requests
To fetch the next page, include the previous cursor value as a query parameter:
```bash Next Page Request icon=file-terminal
curl -X GET --location "https://app.untetherlabs.com/api/v1/timekeeping/timecards?cursor=my-opaque-cursor" \
-H "Authorization: Bearer {ACCESS_TOKEN}"
```
### Last Page Detection
When you reach the last page, the cursor will be `null`:
```json Final Page Response icon=braces
{
"items": [
{
"id": "79208585-e4d2-4142-aafd-e76b851ffb88",
"name": "Regular",
"shorthandName": "REG",
"hexColor": "#f97316",
"type": "hours",
"value": 1
}
],
"cursor": null
}
```
## Best Practices
- Always treat cursor values as opaque strings
- Process items in batches rather than loading all pages at once for large datasets
- Consider implementing pagination limits in your application to avoid memory issues
Page counts are not currently provided in pagination responses. This may change
(per-endpoint) in the future.
## Implementation Examples
Below are basic examples of how to iterate through pages of a paginated endpoint in JavaScript and Python.
```javascript fetchAllPages.js lines
async function fetchAllItems(endpoint, accessToken) {
const allItems = [];
let cursor = null;
do {
const url = new URL(`https://app.untetherlabs.com/api${endpoint}`);
if (cursor) {
url.searchParams.set('cursor', cursor);
}
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const data = await response.json();
allItems.push(...data.items);
cursor = data.cursor;
} while (cursor !== null);
return allItems;
}
// Usage
const allPayCodes = await fetchAllItems('/v1/pay-codes', accessToken);
```
```python fetch_all_pages.py lines
import requests
from urllib.parse import urljoin
def fetch_all_items(endpoint, access_token):
"""Fetch all items from a paginated endpoint."""
all_items = []
cursor = None
base_url = "https://app.untetherlabs.com/api"
while True:
url = urljoin(base_url, endpoint)
headers = {"Authorization": f"Bearer {access_token}"}
params = {"cursor": cursor} if cursor else {}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
all_items.extend(data["items"])
cursor = data.get("cursor")
if cursor is None:
break
return all_items
# Usage
all_pay_codes = fetch_all_items("/v1/pay-codes", access_token)
```
---
# Query Parameters
> Learn how to use common query parameters in API requests.
This documentation is coming soon...
---
# Error Handling
> Learn how to respond to errors in your API requests.
The Untether Labs API follows the [RFC-9457](https://www.rfc-editor.org/rfc/rfc9457.html)
Problem Detail specification for error responses. This standardized format provides consistent,
machine-readable error information to help you build robust applications.
A complete list of expected errors can be found in the [Error Reference](/errors).
Each endpoint in the API Reference documents the specific errors that it may return.
## Error Response Structure
All API errors return a JSON response with the following structure:
```json Error Response Structure icon=braces
{
"type": "https://developers.untetherlabs.com/errors#user-not-found",
"status": 404,
"title": "A specified user does not exist in the system.",
"detail": "User with ID '123e4567-e89b-12d3-a456-426614174000' was not found."
}
```
### Response Fields
Standard HTTP response codes are always returned in addition to the error body.
A URI that uniquely identifies the error type. Always use this field to identify and
handle specific errors programmatically.
The HTTP status code of the response (e.g., 400, 404, 500). This is always set in the
actual HTTP response also.
A short, human-readable summary of the problem type.
A human-readable explanation specific to this occurrence of the problem.
Additional fields (extensions) may be present depending on the specific error type.
See the response documentation for specific endpoints for details on any extensions.
## Error Identification
Always use the `type` field to identify errors programmatically. The `type` contains the full URL path to the specific error documentation:
```javascript Error Identification Example icon=code
if (error.type === 'https://developers.untetherlabs.com/errors#user-not-found') {
// Handle user not found
console.log('User does not exist');
} else if (error.type === 'https://developers.untetherlabs.com/errors#validation') {
// Handle validation error
console.log('Input validation failed:', error.message);
}
```
---
# Historical Corrections
> Learn how to create historical corrections for signed-off pay periods.
Historical corrections are required when modifying a resource in a closed pay period,
or which may impact a closed pay period. This safety mechanism ensures that changes to
historical data are intentional and properly documented for payroll and audit purposes.
## When Historical Corrections Are Required
Some examples of when a historical correction is required include:
- Creating, updating, or deleting timecard punches in signed-off pay periods
- Modifying shifts in signed-off pay periods
- Updating pay codes or other pay-related system configuration in the past
## Historical Correction Structure
To make a historical correction, include the `historicalCorrection` object in your request body:
```json Historical Correction Structure icon=braces
{
// Your normal request data
"startTime": "2024-01-15T08:00:00Z",
"endTime": "2024-01-15T17:00:00Z",
// Historical correction acknowledgment
"historicalCorrection": {
"note": "Correcting missed punch from employee timesheet review"
}
}
```
### Fields
Required when modifying historical data that may affect pay.
An optional note explaining the reason for the historical correction. While
not required, providing a clear explanation is strongly recommended for audit
purposes.
## Error Handling
### Missing Historical Correction
If you attempt to modify historical data without the required acknowledgment:
```json Missing Historical Correction Error icon=braces
{
"type": "https://developers.untetherlabs.com/errors#missing-historical-correction-acknowledgement",
"status": 400,
"title": "A historical correction acknowledgement is required to create a historical correction.",
"detail": "This action would create one or more historical corrections, but no historical correction acknowledgement was provided."
}
```
You may handle this before sending the request, or as a retry after receiving the error.
For additional help with historical corrections, [contact our support team](mailto:support@untetherlabs.com).
---
# Managing Timecard Punches
> Learn how to create and delete timecard punches.
The Untether Labs API provides endpoints for managing timecard punches, allowing you to create and delete time tracking entries for your workforce.
## Terminology
Before working with timecard punches, it's important to understand the key terms:
- **Punch**: A single time entry that records when a provider performs an action (clock in, clock out, break start, break end).
Typically timecard punches follow a logical sequence. You'll have pairs of clock_in/clock_out punches, with optional break_start/break_end punches in between.
- **Timecard**: A collection of punches that represent a complete work session for a provider
- **Action**: The type of punch being recorded (`clock_in`, `clock_out`, `break_start`, `break_end`)
- **State**: The admin approval status of a punch (`pending`, `approved`, `rejected`)
## Creating Punches
Use the `POST /v1/timekeeping/timecard/punches` endpoint to create new timecard punches.
```bash Clock In Request icon=file-terminal
curl -X POST --location "https://app.untetherlabs.com/api/v1/timekeeping/timecard/punches" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"provider": "123e4567-e89b-12d3-a456-426614174000",
"action": "clock_in",
"time": "2025-09-10T08:00:00Z",
"state": "pending",
"providerNote": "Starting my shift"
}'
```
### Other Actions (Clock Out, Breaks)
```bash Clock Out Request icon=file-terminal
curl -X POST --location "https://app.untetherlabs.com/api/v1/timekeeping/timecard/punches" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"provider": "123e4567-e89b-12d3-a456-426614174000",
"action": "clock_out",
"time": "2025-09-10T17:00:00Z",
"state": "pending",
"providerNote": "Finished my shift"
}'
```
### Response
```json Create Punch Response icon=braces
{
"id": "789e1234-e89b-12d3-a456-426614174002",
"timecard": "456e7890-e89b-12d3-a456-426614174001",
"provider": "123e4567-e89b-12d3-a456-426614174000",
"action": "clock_in",
"state": "pending",
"time": "2025-09-10T08:00:00Z",
"generated": false,
"createdAt": "2025-09-10T08:00:01Z",
"updatedAt": "2025-09-10T08:00:01Z",
"providerNote": "Starting my shift"
}
```
## Deleting Punches
Use the `DELETE /v1/timekeeping/timecard/punches/{punchId}` endpoint to remove a specific punch.
```bash Delete Punch Request icon=file-terminal
curl -X DELETE --location "https://app.untetherlabs.com/api/v1/timekeeping/timecard/punches/789e1234-e89b-12d3-a456-426614174002" \
-H "Authorization: Bearer {ACCESS_TOKEN}"
```
When deleting the last punch in a timecard, the entire timecard will be automatically
deleted. To re-add punches you must start over with a new `clock_in`.
### Response
The delete operation returns the deleted punch object:
```json Delete Punch Response icon=braces
{
"id": "789e1234-e89b-12d3-a456-426614174002",
"timecard": "456e7890-e89b-12d3-a456-426614174001",
"provider": "123e4567-e89b-12d3-a456-426614174000",
"action": "clock_in",
"state": "pending",
"time": "2025-09-10T08:00:00Z",
"generated": false,
"createdAt": "2025-09-10T08:00:01Z",
"updatedAt": "2025-09-10T08:00:01Z",
"providerNote": "Starting my shift"
}
```
---
# Using Skills
> Model provider attributes and enforce scheduling constraints with a flexible, composable skill system.
## Overview
Skills are a generic abstraction for any provider attribute your organization cares about — licensed regions, enrollments, spoken languages, clinical specialties, and more. Rather than building separate systems for each attribute type, you define **skills**, assign **values** to providers, and enforce **skill requirements**.
This gives you a single, composable mechanism that covers a wide range of scheduling constraints without custom logic for each one.
## Core concepts
### Skills
A skill is a named category of provider attribute. Each skill has an `id` and a set of possible **values**.
| Example skill | Possible values |
| ------------------- | -------------------------------------------------- |
| Licensed Region | `US-KY`, `US-OH`, `US-IN`, … |
| Hospital Enrollment | `st_mary`, `general_east`, `childrens_central`, … |
| Language | `en`, `es`, `fr`, `zh`, … |
| Specialty | `cardiology`, `radiology`, `emergency_medicine`, … |
### Skill assignments
A provider can hold zero or more values for each skill, as well as a valid range for the assignment:
| Provider | Skill | Value | Range |
| -------- | --------------- | ------- | ------------------ |
| Dr. Reed | Licensed Region | `US-KY` | — |
| Dr. Reed | Licensed Region | `US-OH` | 2026-01-10 onwards |
| Dr. Reed | Licensed Region | `US-WI` | until 2028-12-31 |
### Skill requirements
A skill requirement is a constraint — typically placed on a shift — that a provider must satisfy in order to be eligible. Requirements are **composable**: you can combine simple checks with `and` / `or` logic to express complex eligibility rules.
## Requirement schema
Skill requirements use a recursive, tree-structured schema. There are two node types: **leaf** nodes that check a single skill value, and **logical** nodes that combine other requirements.
### `equal` — leaf requirement
Asserts that a provider holds a specific value for a specific skill.
```json
{
"type": "equal",
"skill": "licensed-region-id",
"value": "US-KY"
}
```
### `and` — all must match
```json
{
"type": "and",
"entries": [
{ "type": "equal", "skill": "licensed-region-id", "value": "US-KY" },
{ "type": "equal", "skill": "enrollment-id", "value": "st_mary" }
]
}
```
### `or` — at least one must match
```json
{
"type": "or",
"entries": [
{ "type": "equal", "skill": "licensed-region-id", "value": "US-KY" },
{ "type": "equal", "skill": "licensed-region-id", "value": "US-OH" }
]
}
```
### Full type definition
The schema is recursive — `and` and `or` nodes can contain any mix of `equal`, `and`, and `or` children, allowing arbitrary nesting depth.
```typescript TypeScript
type SkillRequirement =
| {
type: 'equal';
skill: string;
value: string;
}
| {
type: 'and' | 'or';
entries: SkillRequirement[];
};
```
```python Python (Pydantic)
from pydantic import BaseModel
from typing import Union, Literal
class EqualSkillRequirement(BaseModel):
type: Literal['equal']
skill: str
value: str
class LogicalSkillRequirement(BaseModel):
type: Literal['and', 'or']
entries: list['SkillRequirement']
SkillRequirement = Union[EqualSkillRequirement, LogicalSkillRequirement]
```
## Demand forecasts by skill
A demand forecast says how many staff a role needs, and when. For some roles it can also say which skills those staff need, such as a language or a licensed region, so the forecast asks for "3 Spanish speakers licensed in Kentucky" rather than just "3 people".
### Which columns a role takes
Untether sets up which roles are broken down by which skills. Each of those skills becomes one extra column when you [upload demand](/reference/v1/forecasting/upload-demand-forecast), and each column accepts a fixed list of values. [`GET /v1/forecasting/demand-config/{teamId}`](/reference/v1/forecasting/get-demand-config) returns every role's columns and values. Use the column names exactly as given. They are based on the skill's name (`Licensed Region` is `licensed_region`).
A role that is not broken down by any skill takes only the four standard columns: `role`, `startDate`, `endDate` and `demand`.
### An example
Say a role needs, from 08:00 to 16:00: 3 Spanish-speaking staff licensed in Kentucky, 2 English-speaking staff licensed in Ohio, and 1 more person whose language and licence don't matter. That is three rows over the same hours:
```csv
role,startDate,endDate,demand,language,licensed_region
,2026-06-01T08:00:00-04:00,2026-06-01T16:00:00-04:00,3,es,US-KY
,2026-06-01T08:00:00-04:00,2026-06-01T16:00:00-04:00,2,en,US-OH
,2026-06-01T08:00:00-04:00,2026-06-01T16:00:00-04:00,1,,
```
The same rows as JSON. For the row with no requirement, leave the keys out (or send `null` or an empty string):
```json
[
{
"role": "",
"startDate": "2026-06-01T08:00:00-04:00",
"endDate": "2026-06-01T16:00:00-04:00",
"demand": 3,
"language": "es",
"licensed_region": "US-KY"
},
{
"role": "",
"startDate": "2026-06-01T08:00:00-04:00",
"endDate": "2026-06-01T16:00:00-04:00",
"demand": 2,
"language": "en",
"licensed_region": "US-OH"
},
{
"role": "",
"startDate": "2026-06-01T08:00:00-04:00",
"endDate": "2026-06-01T16:00:00-04:00",
"demand": 1
}
]
```
The rows add up: the role needs 6 staff in total over those hours. A view filtered to Spanish speakers counts the Spanish row plus the row with no requirement.
### Rules worth knowing
- **Overlapping rows.** Rows for the same role may cover the same hours as long as their skill values differ, as above. Two rows with the same values, both empty included, must not overlap.
- **What an upload replaces.** For each role, everything already stored between the role's earliest start and latest end is replaced, for every language and region, including stretches inside that span that no row covers. Demand outside the span is untouched. A row covers its start up to, but not including, its end.
- **Replacing a whole week.** Because the span runs from the first row to the last, a week of weekday shifts leaves the weekend as it was. To replace the full week, start and end it with an untagged row of demand 0, such as Monday 00:00 to 08:00 and Friday 16:00 to the next Monday 00:00. The weeknight gaps in between are cleared because they fall inside the span.
- **Unknown columns and bad values.** A column the role doesn't take is ignored and listed in `ignoredColumns` in the response. A value that is not in the column's list rejects the whole request, nothing is saved, and the error names the row: `Row 3: "de" is not a value of the language column (expected one of: en, fr)`.
---
# Webhooks
> Receive real-time notifications when data changes in Untether.
Webhooks allow you to build or configure integrations which subscribe to certain events in the Untether app. When one of those events is triggered, we'll send a HTTP POST payload to the webhook's configured URL.
**Getting Started**
We haven't launched the self-service Webhook UI yet. To set up your first endpoint or update your destination URL, please reach out to our team.
## Delivery Guarantees
Untether uses a robust queuing system to ensure your events are delivered reliably.
* **At-least-once delivery**: We guarantee that every event will be delivered to your endpoint at least once.
* **Retries**: If your server returns a non-2xx status code, we will retry delivery with exponential backoff.
* **Idempotency**: The `id` field in the payload is consistent across re-deliveries. You should use this ID to prevent processing the same event twice.
* **Ordering**: **Message ordering is NOT guaranteed.** Because of the distributed nature of our system, a later event might arrive before an earlier one. Always use the `timestamp` field to determine the correct sequence of events.
---
## Event Payload
All webhook events follow a standardized JSON structure.
Unique identifier for the event. Consistent across re-deliveries.
The type of event triggered (e.g., `v1.user.created`).
The ISO-8601 timestamp of when the event occurred.
The ID of the user who triggered the event. Will be `system` for automated actions.
The display name of the actor.
The actual event data. The schema depends on the `type`.
## Verifying Webhook Authenticity
To ensure that webhooks are genuinely coming from Untether, you must verify the signature included in the request headers.
Every request sent to your server will include the following headers:
| Header | Description |
| --- | --- |
| `user-agent` | `Untether-Webhook-Agent/1.0` |
| `x-unt-webhook-id` | Unique ID for the delivery attempt. |
| `x-unt-webhook-signature` | The HMAC SHA-256 signature used for verification. |
The `x-unt-webhook-signature` header contains a timestamp and one or more signatures. The timestamp is prefixed by `t=`, and each signature is prefixed by a scheme (e.g., `v1=`).
You can verify the signature by comparing it against the expected signature computed using your signing secret. If any of the signatures match, the request is authentic.
**Format:**
`t=1709575424,v1=6a29...v1=7b30...`
### Language Examples
```javascript JavaScript
const crypto = require('crypto');
function verifyWebhook(header, body, secret) {
const parts = header.split(',');
const timestamp = parts.find(p => p.startsWith('t=')).slice(2);
const signatures = parts.filter(p => p.startsWith('v1=')).map(p => p.slice(3));
const message = `${timestamp}.${body}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
return signatures.includes(expectedSignature);
}
```
### Secret Rotation
We support zero-downtime secret rotation using a dual-key (Alpha/Beta) system.
1. **Preparation**: We generate a **Beta** secret and begin signing webhooks with *both* the Alpha and Beta secrets. The header will contain two `v1` signatures.
2. **Update**: You update your application to recognize the new secret.
3. **Completion**: We promote the Beta secret to Alpha and remove the old secret. We return to signing with only one key.
---
# Error Reference
> Every error the Untether Labs API returns.
Errors are identified by their `type`, which is this page's URL with the error's
code as the fragment — an error listed here as `shift-already-claimed` arrives as
`https://developers.untetherlabs.com/errors#shift-already-claimed`. Match on that
rather than on the status code or the message, both of which are shared by many
errors.
Every response carries the standard [RFC-9457 members](/using-the-api/errors). The
extension members listed below are additional to those.
## 400 Bad Request
### batch-size-exceeded
The request exceeded the maximum allowed batch size.
Extension members:
- `batchSize` (number, required)
- `maxBatchSize` (number, required)
### inactive-integration
The requested operation requires an active integration.
### invalid-balance-reset-rows
The requested balance reset rows are invalid.
### invalid-punch-action
The requested punch action is invalid.
### missing-historical-correction-acknowledgement
A historical correction acknowledgement is required to create a historical correction.
For more information on historical corrections, see the [dedicated guide](/using-the-api/historical-corrections).
### negative-range
Time range end date must come after start date.
Extension members:
- `path` (string)
### provider-not-in-policy
The specified provider is not a member of the specified time off policy.
### read-only-field
The request attempted to modify a read-only field.
Extension members:
- `paths` (string[], required)
### recurring-change-in-past
A recurring schedule change must take effect today or later in the company timezone.
Extension members:
- `fromDate` (string, required)
- `today` (string, required)
### recurring-schedule-closed
The requested recurring schedule is closed and cannot be modified.
### report-parameter-validation
A report parameter did not pass validation, see message.
Extension members:
- `path` (string)
- `message` (string, required)
### rippling-validation
The time off request failed Rippling validation requirements.
Extension members:
- `message` (string, required)
- `errorCode` (string)
### schedule-batch-item-failed
One staged change could not be applied, so nothing in the batch was saved.
Extension members:
- `index` (integer, required) — Position of the failed item in the queue, counting from 0.
- `itemType` (string, required)
- `cause` (object, required)
- `type` (string, required)
- `title` (string, required)
- `detail` (string)
### skill-assignment-overlap
The requested skill assignment overlaps with an existing assignment for the same provider and skill.
### team-not-configured-for-autoscheduling
The specified team has no AutoScheduler configuration, so a run cannot be created for it.
### validation
Input validation failed.
Extension members:
- `path` (string)
- `message` (string, required)
## 403 Forbidden
### unauthorized
You do not have permission to perform this action.
Extension members:
- `action` (string, required)
- `resource` (string, required)
- `reason` (string)
## 404 Not Found
### appointment-not-found
A specified appointment does not exist in the system.
### appointment-type-not-found
A specified appointmenttype does not exist in the system.
### auto-scheduler-job-not-found
A specified auto scheduler job does not exist in the system.
### auto-scheduler-snapshot-not-found
A specified auto scheduler snapshot does not exist in the system.
### availability-exception-not-found
A specified availability exception does not exist in the system.
### availability-lock-not-found
A specified availability lock does not exist in the system.
### availability-not-found
A specified availability does not exist in the system.
### balance-reset-upload-not-found
A specified balance reset upload does not exist in the system.
### billing-document-not-found
A specified billing document does not exist in the system.
### billing-invoice-not-found
A specified billing invoice does not exist in the system.
### billing-not-enabled
Billing is not enabled for this company.
### conversation-not-found
A specified conversation does not exist in the system.
### cron-not-found
A specified cron does not exist in the system.
### custom-field-not-found
A specified custom field does not exist in the system.
### data-source-not-found
A specified data source does not exist in the system.
### help-article-not-found
A specified help article does not exist in the system.
### help-collection-not-found
A specified help collection does not exist in the system.
### help-draft-not-found
A specified help draft does not exist in the system.
### integration-not-found
A specified integration does not exist in the system.
### job-not-found
A specified job does not exist in the system.
### label-not-found
A specified label does not exist in the system.
### message-not-found
A specified message does not exist in the system.
### no-assigned-team
The authenticated user is not assigned to a team.
### notification-not-found
A specified notification does not exist in the system.
### overtime-rule-not-found
A specified overtime rule does not exist in the system.
### pay-code-not-found
A specified pay code does not exist in the system.
### pay-period-not-found
A specified pay period does not exist in the system.
### policy-not-found
A specified policy does not exist in the system.
### policy-relationship-not-found
A specified policy relationship does not exist in the system.
### provider-not-found
A specified provider does not exist in the system.
### punch-warnings-not-found
A specified punch warnings does not exist in the system.
### query-result-not-found
A specified query result does not exist in the system.
### quick-link-not-found
A specified quick link does not exist in the system.
### rbac-role-not-found
A specified rbac role does not exist in the system.
### recurring-schedule-not-found
A specified recurring schedule does not exist in the system.
### report-not-found
A specified report does not exist in the system.
### report-run-not-found
A specified report run does not exist in the system.
### role-not-found
A specified role does not exist in the system.
### rounding-rule-not-found
A specified rounding rule does not exist in the system.
### schedule-group-not-found
A specified schedule group does not exist in the system.
### shift-not-found
A specified shift does not exist in the system.
### shift-swap-application-not-found
A specified shift swap application does not exist in the system.
### shift-swap-listing-not-found
A specified shift swap listing does not exist in the system.
### skill-assignment-not-found
A specified skill assignment does not exist in the system.
### skill-not-found
A specified skill does not exist in the system.
### skill-requirement-template-not-found
A specified skill requirement template does not exist in the system.
### team-not-found
A specified team does not exist in the system.
### time-off-policy-not-found
A specified time off policy does not exist in the system.
### time-off-request-not-found
A specified time off request does not exist in the system.
### timecard-not-found
A specified timecard does not exist in the system.
### timecard-punch-not-found
A specified timecard punch does not exist in the system.
### user-not-found
A specified user does not exist in the system.
### vacation-bid-not-found
A specified vacation bid does not exist in the system.
### vacation-bid-period-not-found
A specified vacation bid period does not exist in the system.
### webhook-not-found
A specified webhook does not exist in the system.
### work-rule-not-found
A specified work rule does not exist in the system.
### workflow-not-found
A specified workflow does not exist in the system.
### workflow-run-not-found
A specified workflow run does not exist in the system.
## 409 Conflict
### availability-has-scheduled-shift
This availability overlaps a scheduled shift, so it cannot be changed. Contact your scheduler to change the shift first.
### availability-imported
The availability was imported from a connected calendar and can only be changed there.
### availability-locked
Availability for these dates is locked while a schedule is being drafted.
### client-timezone-pinned
The company shows providers its own time zone, so a provider's time zone cannot be changed.
### duplicate-help-path
A help collection or article already exists at the specified path.
Extension members:
- `fullPath` (string, required)
### duplicate-shift-template
A shift template with the same name already exists for this team.
### duplicate-skill
A skill with the same name and type already exists.
### duplicate-skill-collection
A skill collection with the same name already exists for this skill.
### duplicate-skill-value
A value with the same name already exists for this skill.
### email-in-use
The specified email address is already in use by another user.
### policy-relationship-already-exists
One or more time off policies are already linked to another vacation bid.
### shift-already-claimed
The requested shift has already been claimed by another provider.
### shift-already-started
The requested shift has already started and cannot be claimed.
### shift-claim-window-closed
The requested shift cannot be claimed within the configured minimum notice window.
Extension members:
- `shiftId` (string, required)
- `minimumNoticeMinutes` (integer, required, min 0)
- `startsAt` (string, required, example "2024-11-20T07:43:34+0000")
### shift-missing-skills
The requested shift requires skills the provider does not have.
Extension members:
- `shiftId` (string, required)
- `missingSkills` (object[], required)
- `name` (string, required)
- `value` (string)
- `type` (enum, required, one of `licensed_region`, `language`, `enrollment`, `custom`)
### shift-schedule-conflict
The requested shift overlaps with an existing shift for this provider.
Extension members:
- `shiftId` (string, required)
- `conflicts` (object[], required)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
### version-conflict
This object has been changed. Refresh to see the latest version.
Extension members:
- `currentVersion` (number, required)
## 503 Service Unavailable
### maintenance-mode
The requested company is temporarily read-only.
---
# API Reference
> Every endpoint in the Untether Labs API.
## V1
### Appointment Types
- [Get Appointment Types](https://developers.untetherlabs.com/reference/v1/appointment-types/get-appointment-types.md): GET /v1/appointment-types
- [Create Appointment Type](https://developers.untetherlabs.com/reference/v1/appointment-types/create-appointment-type.md): POST /v1/appointment-types
- [Update Appointment Type](https://developers.untetherlabs.com/reference/v1/appointment-types/update-appointment-type.md): PATCH /v1/appointment-types/{appointmentTypeId}
- [Archive Appointment Type](https://developers.untetherlabs.com/reference/v1/appointment-types/archive-appointment-type.md): DELETE /v1/appointment-types/{appointmentTypeId}
### Appointments
- [Get Appointments](https://developers.untetherlabs.com/reference/v1/appointments/get-appointments.md): GET /v1/appointments
- [Get Appointment](https://developers.untetherlabs.com/reference/v1/appointments/get-appointment.md): GET /v1/appointments/{appointmentId}
- [Create Appointment](https://developers.untetherlabs.com/reference/v1/appointments/create-appointment.md): POST /v1/appointments
- [Search Appointment Slots](https://developers.untetherlabs.com/reference/v1/appointments/search-appointment-slots.md): POST /v1/appointments/slots
- [Update Appointment](https://developers.untetherlabs.com/reference/v1/appointments/update-appointment.md): PATCH /v1/appointments/{appointmentId}
- [Delete Appointment](https://developers.untetherlabs.com/reference/v1/appointments/delete-appointment.md): DELETE /v1/appointments/{appointmentId}
### Employment Types
- [Get Employment Types](https://developers.untetherlabs.com/reference/v1/employment-types/get-employment-types.md): GET /v1/employment-types
- [Get Employment Type](https://developers.untetherlabs.com/reference/v1/employment-types/get-employment-type.md): GET /v1/employment-types/{id}
### Forecasting
- [Get Demand Config](https://developers.untetherlabs.com/reference/v1/forecasting/get-demand-config.md): GET /v1/forecasting/demand-config/{teamId}
- [Upload Demand Forecast](https://developers.untetherlabs.com/reference/v1/forecasting/upload-demand-forecast.md): POST /v1/forecasting/demand
### Providers
- [Get Providers](https://developers.untetherlabs.com/reference/v1/providers/get-providers.md): GET /v1/providers
- [Get Providers Count](https://developers.untetherlabs.com/reference/v1/providers/get-providers-count.md): GET /v1/providers/count
### Reporting
- [Get Report Runs](https://developers.untetherlabs.com/reference/v1/reporting/get-report-runs.md): GET /v1/reporting/reports/runs
- [Get Report Run](https://developers.untetherlabs.com/reference/v1/reporting/get-report-run.md): GET /v1/reporting/reports/runs/{reportRunId}
- [Create Report Run](https://developers.untetherlabs.com/reference/v1/reporting/create-report-run.md): POST /v1/reporting/reports/runs
### Roles
- [Get Roles](https://developers.untetherlabs.com/reference/v1/roles/get-roles.md): GET /v1/roles
- [Get Role](https://developers.untetherlabs.com/reference/v1/roles/get-role.md): GET /v1/roles/{roleId}
### Settings
#### Labels
- [Get Labels](https://developers.untetherlabs.com/reference/v1/settings/labels/get-labels.md): GET /v1/settings/labels
- [Get Label](https://developers.untetherlabs.com/reference/v1/settings/labels/get-label.md): GET /v1/settings/labels/{labelId}
- [Create Label](https://developers.untetherlabs.com/reference/v1/settings/labels/create-label.md): POST /v1/settings/labels
- [Update Label](https://developers.untetherlabs.com/reference/v1/settings/labels/update-label.md): PUT /v1/settings/labels/{labelId}
- [Delete Label](https://developers.untetherlabs.com/reference/v1/settings/labels/delete-label.md): DELETE /v1/settings/labels/{labelId}
#### Pay Codes
- [Get Pay Codes](https://developers.untetherlabs.com/reference/v1/settings/pay-codes/get-pay-codes.md): GET /v1/settings/pay-codes
- [Get Pay Code](https://developers.untetherlabs.com/reference/v1/settings/pay-codes/get-pay-code.md): GET /v1/settings/pay-codes/{payCodeId}
### Shifts
- [Get Shifts](https://developers.untetherlabs.com/reference/v1/shifts/get-shifts.md): GET /v1/shifts
### Teams
- [Get Teams](https://developers.untetherlabs.com/reference/v1/teams/get-teams.md): GET /v1/teams
- [Get Team](https://developers.untetherlabs.com/reference/v1/teams/get-team.md): GET /v1/teams/{teamId}
### Time Off
- [Get Policies](https://developers.untetherlabs.com/reference/v1/time-off/get-policies.md): GET /v1/time-off/policies
- [Get Policy](https://developers.untetherlabs.com/reference/v1/time-off/get-policy.md): GET /v1/time-off/policies/{policyId}
- [Get Requests](https://developers.untetherlabs.com/reference/v1/time-off/get-requests.md): GET /v1/time-off/requests
- [Get Request](https://developers.untetherlabs.com/reference/v1/time-off/get-request.md): GET /v1/time-off/requests/{requestId}
- [Get Time Off Requests Count](https://developers.untetherlabs.com/reference/v1/time-off/get-time-off-requests-count.md): GET /v1/time-off/requests/count
### Timekeeping
- [Get Timecard](https://developers.untetherlabs.com/reference/v1/timekeeping/get-timecard.md): GET /v1/timekeeping/timecard/{provider}
- [Get Timesheets](https://developers.untetherlabs.com/reference/v1/timekeeping/get-timesheets.md): GET /v1/timekeeping/timesheet
- [Create Timecard Punch](https://developers.untetherlabs.com/reference/v1/timekeeping/create-timecard-punch.md): POST /v1/timekeeping/timecard/punches
- [Delete Timecard Punch](https://developers.untetherlabs.com/reference/v1/timekeeping/delete-timecard-punch.md): DELETE /v1/timekeeping/timecard/punches/{punchId}
### Users
- [Get Users](https://developers.untetherlabs.com/reference/v1/users/get-users.md): GET /v1/users
- [Get User](https://developers.untetherlabs.com/reference/v1/users/get-user.md): GET /v1/users/{userId}
---
# Get Appointment Types
> GET /v1/appointment-types
`GET https://app.untetherlabs.com/api/v1/appointment-types`
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/appointment-types' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (AppointmentType[], required)
- `name` (string, required)
- `length` (integer, required) — in minutes
- `allowedRoles` (uuid[] | null, required) — Roles which are considered for this appointment type. Null means any role is allowed.
- `skillRequirement` (NullableSkillRequirement)
- Option 1: SkillRequirementCondition
- `type` (enum, required, one of `equal`)
- `skill` (string, required, min length 1)
- `value` (string, required)
- Option 2: SkillRequirementGroup
- `type` (enum, required, one of `and`, `or`)
- `entries` (one of[], required, min items 1)
- `cursor` (string)
## Errors
- `validation` (400) — Input validation failed.
---
# Create Appointment Type
> POST /v1/appointment-types
`POST https://app.untetherlabs.com/api/v1/appointment-types`
## Request
```bash
curl -X POST 'https://app.untetherlabs.com/api/v1/appointment-types' \
-H 'Authorization: Bearer $UNTETHER_API_KEY' \
-H 'Content-Type: application/json' \
-d @body.json
```
## Request body (application/json)
- `name` (string, required, min length 2)
- `length` (integer, required, min 5) — in minutes
- `allowedRoles` (uuid[] | null) — Roles which are considered for this appointment type. Null means any role is allowed.
- `skillRequirement` (NullableSkillRequirement)
- Option 1: SkillRequirementCondition
- `type` (enum, required, one of `equal`)
- `skill` (string, required, min length 1)
- `value` (string, required)
- Option 2: SkillRequirementGroup
- `type` (enum, required, one of `and`, `or`)
- `entries` (one of[], required, min items 1)
- Option 1: SkillRequirementCondition
- Option 2: SkillRequirementNestedGroup
## Response 201
- `name` (string, required)
- `length` (integer, required) — in minutes
- `allowedRoles` (uuid[] | null, required) — Roles which are considered for this appointment type. Null means any role is allowed.
- `skillRequirement` (NullableSkillRequirement)
- Option 1: SkillRequirementCondition
- `type` (enum, required, one of `equal`)
- `skill` (string, required, min length 1)
- `value` (string, required)
- Option 2: SkillRequirementGroup
- `type` (enum, required, one of `and`, `or`)
- `entries` (one of[], required, min items 1)
- Option 1: SkillRequirementCondition
- Option 2: SkillRequirementNestedGroup
## Errors
- `validation` (400) — Input validation failed.
---
# Update Appointment Type
> PATCH /v1/appointment-types/{appointmentTypeId}
`PATCH https://app.untetherlabs.com/api/v1/appointment-types/{appointmentTypeId}`
## Request
```bash
curl -X PATCH 'https://app.untetherlabs.com/api/v1/appointment-types/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY' \
-H 'Content-Type: application/json' \
-d @body.json
```
## Path parameters
- `appointmentTypeId` (uuid, required)
## Request body (application/json)
- `name` (string, min length 2)
- `length` (integer, min 5) — in minutes
- `allowedRoles` (uuid[] | null) — Roles which are considered for this appointment type. Null means any role is allowed.
- `skillRequirement` (NullableSkillRequirement)
- Option 1: SkillRequirementCondition
- `type` (enum, required, one of `equal`)
- `skill` (string, required, min length 1)
- `value` (string, required)
- Option 2: SkillRequirementGroup
- `type` (enum, required, one of `and`, `or`)
- `entries` (one of[], required, min items 1)
- Option 1: SkillRequirementCondition
- Option 2: SkillRequirementNestedGroup
## Response 200
- `name` (string, required)
- `length` (integer, required) — in minutes
- `allowedRoles` (uuid[] | null, required) — Roles which are considered for this appointment type. Null means any role is allowed.
- `skillRequirement` (NullableSkillRequirement)
- Option 1: SkillRequirementCondition
- `type` (enum, required, one of `equal`)
- `skill` (string, required, min length 1)
- `value` (string, required)
- Option 2: SkillRequirementGroup
- `type` (enum, required, one of `and`, `or`)
- `entries` (one of[], required, min items 1)
- Option 1: SkillRequirementCondition
- Option 2: SkillRequirementNestedGroup
## Errors
- `validation` (400) — Input validation failed.
- `appointment-type-not-found` (404) — A specified appointmenttype does not exist in the system.
---
# Archive Appointment Type
> DELETE /v1/appointment-types/{appointmentTypeId}
`DELETE https://app.untetherlabs.com/api/v1/appointment-types/{appointmentTypeId}`
## Request
```bash
curl -X DELETE 'https://app.untetherlabs.com/api/v1/appointment-types/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `appointmentTypeId` (uuid, required)
## Response 200
- `name` (string, required)
- `length` (integer, required) — in minutes
- `allowedRoles` (uuid[] | null, required) — Roles which are considered for this appointment type. Null means any role is allowed.
- `skillRequirement` (NullableSkillRequirement)
- Option 1: SkillRequirementCondition
- `type` (enum, required, one of `equal`)
- `skill` (string, required, min length 1)
- `value` (string, required)
- Option 2: SkillRequirementGroup
- `type` (enum, required, one of `and`, `or`)
- `entries` (one of[], required, min items 1)
- Option 1: SkillRequirementCondition
- Option 2: SkillRequirementNestedGroup
## Errors
- `validation` (400) — Input validation failed.
- `appointment-type-not-found` (404) — A specified appointmenttype does not exist in the system.
---
# Get Appointments
> GET /v1/appointments
`GET https://app.untetherlabs.com/api/v1/appointments`
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/appointments' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `teamId` (uuid)
- `providerId` (uuid)
- `memberId` (string)
- `startDate` (string, example "2024-11-20T07:43:34+0000")
- `endDate` (string, example "2024-11-20T07:43:34+0000")
- `status` (AppointmentStatus, one of `upcoming`, `occurred`, `cancelled`, `no-show`, `re-scheduled`)
- `type` (string)
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (Appointment[], required)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `type` (string, required, example "video") — Appointment type, company specific
- `status` (AppointmentStatus, required, one of `upcoming`, `occurred`, `cancelled`, `no-show`, `re-scheduled`)
- `providerId` (uuid, required)
- `memberId` (string | null, required)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `actualStartDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `actualEndDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `location` (string | null, required)
- `cancellationDate` (string | null, required, example "2024-11-20T07:43:34+0000") — Only present when status = `cancelled`
- `cancellationReason` (string | null, required) — Only present when status = `cancelled`
- `cursor` (string)
## Errors
- `negative-range` (400) — Time range end date must come after start date.
- `validation` (400) — Input validation failed.
---
# Get Appointment
> GET /v1/appointments/{appointmentId}
`GET https://app.untetherlabs.com/api/v1/appointments/{appointmentId}`
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/appointments/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `appointmentId` (uuid, required)
## Response 200
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `type` (string, required, example "video") — Appointment type, company specific
- `status` (AppointmentStatus, required, one of `upcoming`, `occurred`, `cancelled`, `no-show`, `re-scheduled`)
- `providerId` (uuid, required)
- `memberId` (string | null, required)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `actualStartDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `actualEndDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `location` (string | null, required)
- `cancellationDate` (string | null, required, example "2024-11-20T07:43:34+0000") — Only present when status = `cancelled`
- `cancellationReason` (string | null, required) — Only present when status = `cancelled`
## Errors
- `validation` (400) — Input validation failed.
- `appointment-not-found` (404) — A specified appointment does not exist in the system.
---
# Create Appointment
> POST /v1/appointments
`POST https://app.untetherlabs.com/api/v1/appointments`
## Request
```bash
curl -X POST 'https://app.untetherlabs.com/api/v1/appointments' \
-H 'Authorization: Bearer $UNTETHER_API_KEY' \
-H 'Content-Type: application/json' \
-d @body.json
```
## Request body (application/json)
- `createdAt` (string, example "2024-11-20T07:43:34+0000")
- `type` (string, required, example "video") — Appointment type, company specific
- `status` (AppointmentStatus, default "upcoming", one of `upcoming`, `occurred`, `cancelled`, `no-show`, `re-scheduled`)
- `providerId` (uuid, required)
- `memberId` (string | null)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `actualStartDate` (string | null, example "2024-11-20T07:43:34+0000")
- `actualEndDate` (string | null, example "2024-11-20T07:43:34+0000")
- `location` (string | null)
- `cancellationDate` (string | null, example "2024-11-20T07:43:34+0000") — Only present when status = `cancelled`
- `cancellationReason` (string | null) — Only present when status = `cancelled`
## Response 201
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `type` (string, required, example "video") — Appointment type, company specific
- `status` (AppointmentStatus, required, one of `upcoming`, `occurred`, `cancelled`, `no-show`, `re-scheduled`)
- `providerId` (uuid, required)
- `memberId` (string | null, required)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `actualStartDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `actualEndDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `location` (string | null, required)
- `cancellationDate` (string | null, required, example "2024-11-20T07:43:34+0000") — Only present when status = `cancelled`
- `cancellationReason` (string | null, required) — Only present when status = `cancelled`
## Errors
- `validation` (400) — Input validation failed.
---
# Search Appointment Slots
> POST /v1/appointments/slots
`POST https://app.untetherlabs.com/api/v1/appointments/slots`
This endpoint is in beta.
This is a beta endpoint. Breaking changes may occur without notice, use at your own risk.
Generate appointment slot suggestions for a set of available providers in the given time range.
Slots are based on working-type shifts in the period, filtered & ranked by the given requirements/preferences.
On ranking,
* Skills are ranked by the closest match. For example, a skill requirement for specialty X will rank providers
with only specialty X highly, and providers with multiple specialties including X lower.
* Slots which do not create unusable gaps are preferred.
Skills are the primary mechanism for selecting providers, see [Using Skills](/guides/skills) for more information.
## Request
```bash
curl -X POST 'https://app.untetherlabs.com/api/v1/appointments/slots' \
-H 'Authorization: Bearer $UNTETHER_API_KEY' \
-H 'Content-Type: application/json' \
-d @body.json
```
## Request body (application/json)
- `appointmentTypeId` (string, required)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `requirements` (object) — Hard filters for possible slots
- `providerIds` (string[])
- `skill` (SkillRequirement)
- Option 1: SkillRequirementCondition
- `type` (enum, required, one of `equal`)
- `skill` (string, required, min length 1)
- `value` (string, required)
- Option 2: SkillRequirementGroup
- `type` (enum, required, one of `and`, `or`)
- `entries` (one of[], required, min items 1)
- `preferences` (object) — Soft filters for possible slots
- `providerIds` (string[])
- `skill` (SkillRequirement)
- Option 1: SkillRequirementCondition
- `type` (enum, required, one of `equal`)
- `skill` (string, required, min length 1)
- `value` (string, required)
- Option 2: SkillRequirementGroup
- `type` (enum, required, one of `and`, `or`)
- `entries` (one of[], required, min items 1)
- `limit` (number, default 10) — Maximum number of slots to return
## Response 201
- `results` (AppointmentSlot[], required)
- `score` (number, required) — Arbitrary number for ranking slots relative to each other. Should not be compared to anything besides the appointments returned by this request.
- `appointmentTypeId` (string, required)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `providerId` (string, required)
## Errors
- `validation` (400) — Input validation failed.
- `appointment-type-not-found` (404) — A specified appointmenttype does not exist in the system.
---
# Update Appointment
> PATCH /v1/appointments/{appointmentId}
`PATCH https://app.untetherlabs.com/api/v1/appointments/{appointmentId}`
## Request
```bash
curl -X PATCH 'https://app.untetherlabs.com/api/v1/appointments/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY' \
-H 'Content-Type: application/json' \
-d @body.json
```
## Path parameters
- `appointmentId` (uuid, required)
## Request body (application/json)
- `createdAt` (string, example "2024-11-20T07:43:34+0000")
- `type` (string, example "video") — Appointment type, company specific
- `status` (AppointmentStatus, one of `upcoming`, `occurred`, `cancelled`, `no-show`, `re-scheduled`)
- `providerId` (uuid)
- `memberId` (uuid | null)
- `startDate` (string, example "2024-11-20T07:43:34+0000")
- `endDate` (string, example "2024-11-20T07:43:34+0000")
- `actualStartDate` (string | null, example "2024-11-20T07:43:34+0000")
- `actualEndDate` (string | null, example "2024-11-20T07:43:34+0000")
- `location` (string | null)
- `cancellationDate` (string | null, example "2024-11-20T07:43:34+0000") — Only present when status = `cancelled`
- `cancellationReason` (string | null) — Only present when status = `cancelled`
- `version` (integer) — Object version to change, will return a version conflict error if out of date
## Response 200
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `type` (string, required, example "video") — Appointment type, company specific
- `status` (AppointmentStatus, required, one of `upcoming`, `occurred`, `cancelled`, `no-show`, `re-scheduled`)
- `providerId` (uuid, required)
- `memberId` (string | null, required)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `actualStartDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `actualEndDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `location` (string | null, required)
- `cancellationDate` (string | null, required, example "2024-11-20T07:43:34+0000") — Only present when status = `cancelled`
- `cancellationReason` (string | null, required) — Only present when status = `cancelled`
## Errors
- `validation` (400) — Input validation failed.
- `appointment-not-found` (404) — A specified appointment does not exist in the system.
- `version-conflict` (409) — This object has been changed. Refresh to see the latest version.
---
# Delete Appointment
> DELETE /v1/appointments/{appointmentId}
`DELETE https://app.untetherlabs.com/api/v1/appointments/{appointmentId}`
## Request
```bash
curl -X DELETE 'https://app.untetherlabs.com/api/v1/appointments/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `appointmentId` (uuid, required)
## Response 200
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `type` (string, required, example "video") — Appointment type, company specific
- `status` (AppointmentStatus, required, one of `upcoming`, `occurred`, `cancelled`, `no-show`, `re-scheduled`)
- `providerId` (uuid, required)
- `memberId` (string | null, required)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `actualStartDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `actualEndDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `location` (string | null, required)
- `cancellationDate` (string | null, required, example "2024-11-20T07:43:34+0000") — Only present when status = `cancelled`
- `cancellationReason` (string | null, required) — Only present when status = `cancelled`
## Errors
- `validation` (400) — Input validation failed.
- `appointment-not-found` (404) — A specified appointment does not exist in the system.
- `version-conflict` (409) — This object has been changed. Refresh to see the latest version.
---
# Get Employment Types
> GET /v1/employment-types
`GET https://app.untetherlabs.com/api/v1/employment-types`
Retrieve all employment types
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/employment-types' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (EmploymentType[], required)
- `name` (string, required)
- `type` (enum, required, one of `contractor`, `employee`)
- `compensationTimePeriod` (enum, required, one of `hourly`, `salaried`)
- `amountWorked` (enum, required, one of `part_time`, `full_time`, `temporary`)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `updatedAt` (string, required, example "2024-11-20T07:43:34+0000")
- `cursor` (string)
## Errors
- `validation` (400) — Input validation failed.
---
# Get Employment Type
> GET /v1/employment-types/{id}
`GET https://app.untetherlabs.com/api/v1/employment-types/{id}`
Retrieve a single employment type by ID
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/employment-types/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `id` (uuid, required)
## Response 200
- `name` (string, required)
- `type` (enum, required, one of `contractor`, `employee`)
- `compensationTimePeriod` (enum, required, one of `hourly`, `salaried`)
- `amountWorked` (enum, required, one of `part_time`, `full_time`, `temporary`)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `updatedAt` (string, required, example "2024-11-20T07:43:34+0000")
## Errors
- `validation` (400) — Input validation failed.
---
# Get Demand Config
> GET /v1/forecasting/demand-config/{teamId}
`GET https://app.untetherlabs.com/api/v1/forecasting/demand-config/{teamId}`
Get a [team](/reference/v1/teams/get-team)'s demand configuration: for
each [role](/reference/v1/roles/get-role), the extra columns
[POST /v1/forecasting/demand](/reference/v1/forecasting/upload-demand-forecast)
accepts and the values each allows.
- An axis with `mode: "column"` is an upload column. `column` is its
CSV header or JSON key, and `values` are the values a cell accepts,
matched exactly. An empty cell means any value.
- An axis with `mode: "ratio"` is a fixed split of the role's demand
across the values of a licensed-region [skill](/guides/skills), used
when demand is filtered by region. It is not an upload column.
`roleAxis` is how the team's demand maps to its roles.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/forecasting/demand-config/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `teamId` (uuid, required) — The id of a [team](/reference/v1/teams/get-team).
## Response 200
- `teamId` (uuid, required) — The [team](/reference/v1/teams/get-team).
- `roleAxis` (one of, required) — The team's role axis.
- Option 1: RoleAxisColumn
- `mode` (enum, required, one of `column`)
- Option 2: RoleAxisNotUsed
- `mode` (enum, required, one of `not_used`)
- Option 3: RoleAxisRatio
- `mode` (enum, required, one of `ratio`)
- `ratios` (object, required) — The fraction of the team total each role receives, keyed by role id. Every key must be a role of this team and the fractions must sum to 1.
- `roles` (DemandConfigRole[], required) — Every role of the team, by name.
- `roleId` (uuid, required) — The [role](/reference/v1/roles/get-role), as the `role` column of an upload names it.
- `roleName` (string, required) — The name of the role.
- `axes` (SkillAxis[], required) — The role's skill axes. Empty when its demand is not broken down by any skill.
- Option 1: SkillAxisColumn
- `skillId` (uuid, required) — The [skill](/guides/skills) the axis breaks demand down by.
- `skillName` (string, required) — The name of that skill.
- `mode` (enum, required, one of `column`)
- `column` (string, required) — The CSV column (or JSON key) that carries this skill in an upload for the role.
- `values` (string[], required) — The values a cell in the column may hold, matched exactly.
- Option 2: SkillAxisRatio
- `skillId` (uuid, required) — The [skill](/guides/skills) the axis breaks demand down by.
- `skillName` (string, required) — The name of that skill.
- `mode` (enum, required, one of `ratio`)
- `ratios` (object, required) — The fraction of the role's demand in each of the skill's values, keyed by value. Every key must be one of the skill's values and the fractions must sum to 1.
## Errors
- `validation` (400) — Input validation failed.
- `team-not-found` (404) — A specified team does not exist in the system.
---
# Upload Demand Forecast
> POST /v1/forecasting/demand
`POST https://app.untetherlabs.com/api/v1/forecasting/demand`
Upload demand: the number of staff each role needs over a time range.
Each row has a [role](/reference/v1/roles/get-role) id, a startDate, an
endDate and a demand, the number of staff needed throughout the range.
Times are ISO-8601 with an explicit offset, on a quarter hour (:00, :15,
:30 or :45). A row covers its start up to, but not including, its end.
Rows may come in any order.
For each role in the request, everything stored between the role's
earliest start and latest end is replaced by the rows sent, for every
skill value, including stretches no row covers. Demand outside that span
is unchanged.
A role broken down by [skills](/guides/skills) accepts one extra column
per skill. A cell holds one of the skill's values; an empty cell, or in
JSON a missing key, null or "", means any value. Column names and values
are matched exactly.
[`GET /v1/forecasting/demand-config/{teamId}`](/reference/v1/forecasting/get-demand-config)
lists the columns and values each role accepts. Rows for the same role may
overlap when their skill values differ, and overlapping demand adds up.
Rows with the same role and skill values must not overlap.
The body is application/json, an array of rows with skill columns as extra
keys, or text/csv with a header row: role, startDate, endDate, demand,
then any skill columns, in any order.
Nothing is stored if any row is invalid; the response is a 400. A field in
the wrong format, such as a time without an offset, a role that is not an
id, or a missing or negative demand, is reported with the field and its
row. A row that breaks an upload rule, such as an end before its start, a
time off the quarter hour, a value a skill column does not allow, or an
overlap, is reported as "Row 3: ...", counting from 1 and, in CSV, without
the header. A role id that does not exist returns RoleNotFound. A column
the role does not accept is ignored and listed in ignoredColumns.
## Request
```bash
curl -X POST 'https://app.untetherlabs.com/api/v1/forecasting/demand' \
-H 'Authorization: Bearer $UNTETHER_API_KEY' \
-H 'Content-Type: application/json' \
-d @body.json
```
## Request body (application/json)
Type: `UploadDemandForecastRow[]`
- `role` (uuid, required) — The id of the [role](/reference/v1/roles/get-role) the demand is for.
- `startDate` (string, required, example "2024-11-20T07:43:34+0000") — Start of the range, included. ISO-8601 with an explicit offset, on a quarter hour.
- `endDate` (string, required, example "2024-11-20T07:43:34+0000") — End of the range, not included. ISO-8601 with an explicit offset, on a quarter hour.
- `demand` (number, required, min 0) — The number of staff needed throughout the range. 0 or more.
## Request body (text/csv)
Type: `string` — CSV text with a header row containing the columns: role, startDate, endDate, demand.
## Response 201
- `ignoredColumns` (string[], required)
## Errors
- `validation` (400) — Input validation failed.
- `role-not-found` (404) — A specified role does not exist in the system.
---
# Get Providers
> GET /v1/providers
`GET https://app.untetherlabs.com/api/v1/providers`
Retrieve providers with optional related data and pagination
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/providers' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `status` (one of, default ["onboarding","active"])
- Option 1: EmploymentStatus
- Option 2: EmploymentStatus[]
- `email` (string) — Filter by (case-insensitive) email, * wildcards allowed
- `teamId` (one of) — Filter providers by team ID(s)
- Option 1: uuid
- Option 2: uuid[]
- `includeEnrollments` (boolean, default false) — Include enrollment data for each provider
- `includeSkills` (boolean, default false) — Include skills data for each provider
- `includeManager` (boolean, default false) — Include manager data for each provider
- `includeTeam` (boolean, default false) — Include team data for each provider
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (ProviderWithRelations[], required)
- `name` (string | null, required, example "Bruce Wayne")
- `email` (string, required, example "bruce.wayne@goodhealth.com")
- `hireDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `jobTitle` (string | null, required, example "Registered Nurse") — The provider's job title.
- `status` (enum, required, example "active", one of `onboarding`, `active`, `dismissed`, `unmanaged`)
- `teamId` (uuid | null, required)
- `licensedRegions` (string[], required)
- `employmentType` (uuid | null, required)
- `enrollments` (ProviderEnrollment[], required)
- `name` (string, required)
- `skills` (ProviderSkill[], required)
- `skillName` (string, required)
- `values` (string[], required)
- `skillLicensedRegion` (string[])
- `skillEnrollment` (string[])
- `manager` (object | null, required)
- `name` (string | null, required)
- `team` (object | null, required)
- `name` (string, required)
- `shorthandName` (string | null, required)
- `cursor` (string)
## Errors
- `validation` (400) — Input validation failed.
---
# Get Providers Count
> GET /v1/providers/count
`GET https://app.untetherlabs.com/api/v1/providers/count`
Retrieve the total provider count for the current filters
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/providers/count' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `status` (one of, default ["onboarding","active"])
- Option 1: EmploymentStatus
- Option 2: EmploymentStatus[]
- `email` (string) — Filter by (case-insensitive) email, * wildcards allowed
- `teamId` (one of) — Filter providers by team ID(s)
- Option 1: uuid
- Option 2: uuid[]
- `includeEnrollments` (boolean, default false) — Include enrollment data for each provider
- `includeSkills` (boolean, default false) — Include skills data for each provider
- `includeManager` (boolean, default false) — Include manager data for each provider
- `includeTeam` (boolean, default false) — Include team data for each provider
## Response 200
- `count` (number, required)
## Errors
- `validation` (400) — Input validation failed.
---
# Get Report Runs
> GET /v1/reporting/reports/runs
`GET https://app.untetherlabs.com/api/v1/reporting/reports/runs`
This endpoint is in beta.
This is a beta endpoint. Breaking changes may occur without notice, use at your own risk.
Search through previously executed report runs.
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/reporting/reports/runs' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `status` (ReportStatus[], default ["running","completed","failed"], one of `running`, `completed`, `failed`)
- `user` (uuid) — Filter to only runs generated by this user
- `report` (string, example "employee-contact-info")
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (ReportRun[], required)
- `reportId` (string, required, example "payroll-report")
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `createdBy` (ReportUser, required)
- `name` (string, required, example "Bruce Wayne")
- `status` (ReportStatus, required, one of `running`, `completed`, `failed`)
- `file` (object) — Only present if the report has been completed.
- `name` (string, required, example "payroll-2025-01-01.csv")
- `mediaType` (string, required, example "text/csv")
- `sizeBytes` (number, required, min 0)
- `cursor` (string)
## Errors
- `validation` (400) — Input validation failed.
---
# Get Report Run
> GET /v1/reporting/reports/runs/{reportRunId}
`GET https://app.untetherlabs.com/api/v1/reporting/reports/runs/{reportRunId}`
This endpoint is in beta.
This is a beta endpoint. Breaking changes may occur without notice, use at your own risk.
Retrieve metadata about a specific report run, including a temporary download link if the report has been completed.
The download link will expire after one hour, at the given `expiresAt` time.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/reporting/reports/runs/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `reportRunId` (uuid, required)
## Response 200
- `reportId` (string, required, example "payroll-report")
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `createdBy` (ReportUser, required)
- `name` (string, required, example "Bruce Wayne")
- `status` (ReportStatus, required, one of `running`, `completed`, `failed`)
- `file` (object) — Only present if the report has been completed.
- `name` (string, required, example "payroll-2025-01-01.csv")
- `mediaType` (string, required, example "text/csv")
- `sizeBytes` (number, required, min 0)
- `download` (ReportDownload)
- `url` (uri, required)
- `expiresAt` (string, required, example "2024-11-20T07:43:34+0000")
## Errors
- `validation` (400) — Input validation failed.
- `report-run-not-found` (404) — A specified report run does not exist in the system.
---
# Create Report Run
> POST /v1/reporting/reports/runs
`POST https://app.untetherlabs.com/api/v1/reporting/reports/runs`
This endpoint is in beta.
This is a beta endpoint. Breaking changes may occur without notice, use at your own risk.
Create a new report run for the given report ID.
The report parameters depend on the report being executed,
see the report documentation for more info.
Reports are generated asynchronously. The returned object
will always be marked as `running`, you will have to
periodically poll for status changes.
## Request
```bash
curl -X POST 'https://app.untetherlabs.com/api/v1/reporting/reports/runs' \
-H 'Authorization: Bearer $UNTETHER_API_KEY' \
-H 'Content-Type: application/json' \
-d @body.json
```
## Request body (application/json)
- `reportId` (string, required, example "payroll-report")
- `parameters` (object, example {"startDate":"2025-01-01","endDate":"2025-01-31"})
## Response 201
- `reportId` (string, required, example "payroll-report")
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `createdBy` (ReportUser, required)
- `name` (string, required, example "Bruce Wayne")
- `status` (ReportStatus, required, one of `running`, `completed`, `failed`)
- `file` (object) — Only present if the report has been completed.
- `name` (string, required, example "payroll-2025-01-01.csv")
- `mediaType` (string, required, example "text/csv")
- `sizeBytes` (number, required, min 0)
## Errors
- `report-parameter-validation` (400) — A report parameter did not pass validation, see message.
- `validation` (400) — Input validation failed.
- `report-not-found` (404) — A specified report does not exist in the system.
- `user-not-found` (404) — A specified user does not exist in the system.
---
# Get Roles
> GET /v1/roles
`GET https://app.untetherlabs.com/api/v1/roles`
Fetch Roles
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/roles' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `team` (uuid)
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (Role[], required)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `name` (string, required, example "Care Coordinator")
- `team` (uuid, required)
- `teamName` (string, required)
- `hexColor` (string, required, example "#6941c6")
- `cursor` (string)
## Errors
- `validation` (400) — Input validation failed.
---
# Get Role
> GET /v1/roles/{roleId}
`GET https://app.untetherlabs.com/api/v1/roles/{roleId}`
Fetch Role
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/roles/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `roleId` (uuid, required)
## Response 200
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `name` (string, required, example "Care Coordinator")
- `team` (uuid, required)
- `teamName` (string, required)
- `hexColor` (string, required, example "#6941c6")
## Errors
- `validation` (400) — Input validation failed.
- `role-not-found` (404) — A specified role does not exist in the system.
---
# Get Labels
> GET /v1/settings/labels
`GET https://app.untetherlabs.com/api/v1/settings/labels`
Read all configured labels
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/settings/labels' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (Label[], required)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `updatedAt` (string, required, example "2024-11-20T07:43:34+0000")
- `name` (string, required, example "Video")
- `description` (string | null, required, example "Shift segment time is only available for video calls")
- `color` (string, required, example "#6941c6")
- `cursor` (string)
## Errors
- `validation` (400) — Input validation failed.
---
# Get Label
> GET /v1/settings/labels/{labelId}
`GET https://app.untetherlabs.com/api/v1/settings/labels/{labelId}`
Get an existing label by its ID
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/settings/labels/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `labelId` (uuid, required)
## Response 200
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `updatedAt` (string, required, example "2024-11-20T07:43:34+0000")
- `name` (string, required, example "Video")
- `description` (string | null, required, example "Shift segment time is only available for video calls")
- `color` (string, required, example "#6941c6")
## Errors
- `validation` (400) — Input validation failed.
- `label-not-found` (404) — A specified label does not exist in the system.
---
# Create Label
> POST /v1/settings/labels
`POST https://app.untetherlabs.com/api/v1/settings/labels`
Create a new label
## Request
```bash
curl -X POST 'https://app.untetherlabs.com/api/v1/settings/labels' \
-H 'Authorization: Bearer $UNTETHER_API_KEY' \
-H 'Content-Type: application/json' \
-d @body.json
```
## Request body (application/json)
- `name` (string, required, min length 1, example "Video")
- `description` (string, example "Shift segment time is only available for video calls")
- `color` (string, example "#6941c6")
## Response 201
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `updatedAt` (string, required, example "2024-11-20T07:43:34+0000")
- `name` (string, required, example "Video")
- `description` (string | null, required, example "Shift segment time is only available for video calls")
- `color` (string, required, example "#6941c6")
## Errors
- `validation` (400) — Input validation failed.
---
# Update Label
> PUT /v1/settings/labels/{labelId}
`PUT https://app.untetherlabs.com/api/v1/settings/labels/{labelId}`
Update an existing label
## Request
```bash
curl -X PUT 'https://app.untetherlabs.com/api/v1/settings/labels/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY' \
-H 'Content-Type: application/json' \
-d @body.json
```
## Path parameters
- `labelId` (uuid, required)
## Request body (application/json)
- `name` (string, min length 1, example "Video")
- `description` (string | null, example "Shift segment time is only available for video calls")
- `color` (string, example "#6941c6")
## Response 200
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `updatedAt` (string, required, example "2024-11-20T07:43:34+0000")
- `name` (string, required, example "Video")
- `description` (string | null, required, example "Shift segment time is only available for video calls")
- `color` (string, required, example "#6941c6")
## Errors
- `validation` (400) — Input validation failed.
- `label-not-found` (404) — A specified label does not exist in the system.
---
# Delete Label
> DELETE /v1/settings/labels/{labelId}
`DELETE https://app.untetherlabs.com/api/v1/settings/labels/{labelId}`
Delete a label
## Request
```bash
curl -X DELETE 'https://app.untetherlabs.com/api/v1/settings/labels/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `labelId` (uuid, required)
## Response 200
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `updatedAt` (string, required, example "2024-11-20T07:43:34+0000")
- `name` (string, required, example "Video")
- `description` (string | null, required, example "Shift segment time is only available for video calls")
- `color` (string, required, example "#6941c6")
## Errors
- `validation` (400) — Input validation failed.
- `label-not-found` (404) — A specified label does not exist in the system.
---
# Get Pay Codes
> GET /v1/settings/pay-codes
`GET https://app.untetherlabs.com/api/v1/settings/pay-codes`
Retrieve all defined pay codes.
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/settings/pay-codes' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (PayCode[], required)
- `name` (string, required, example "Overtime")
- `shorthandName` (string, required, example "OT")
- `hexColor` (string, required, example "#f97316")
- `type` (enum, required, one of `hours`, `days`, `money`)
- `value` (number, required, example 1.5)
- `cursor` (string)
## Errors
- `validation` (400) — Input validation failed.
---
# Get Pay Code
> GET /v1/settings/pay-codes/{payCodeId}
`GET https://app.untetherlabs.com/api/v1/settings/pay-codes/{payCodeId}`
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/settings/pay-codes/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `payCodeId` (uuid, required)
## Response 200
- `name` (string, required, example "Overtime")
- `shorthandName` (string, required, example "OT")
- `hexColor` (string, required, example "#f97316")
- `type` (enum, required, one of `hours`, `days`, `money`)
- `value` (number, required, example 1.5)
## Errors
- `validation` (400) — Input validation failed.
- `pay-code-not-found` (404) — A specified pay code does not exist in the system.
---
# Get Shifts
> GET /v1/shifts
`GET https://app.untetherlabs.com/api/v1/shifts`
Get shifts within a given time range.
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/shifts' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `provider` (uuid)
- `team` (uuid)
- `startDate` (string, example "2024-11-20T07:43:34+0000") — Used to inclusively filter shifts whose startDate is later than the provided start time, in yyyy-MM-dd format. Defaults to current date - 1 month.
- `endDate` (string, example "2024-11-20T07:43:34+0000") — Used to exclusively filter shifts whose endDate is earlier than the provided end time, in yyyy-MM-dd format. Defaults to current date + 1 month.
- `includeOpen` (boolean, default false)
- `includeDisplayNames` (boolean, default false)
- `sort` (string) — Available fields: `startDate` See [Query Parameters](/using-the-api/query-parameters) for more information on sorting.
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (Shift[], required)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `version` (number) — Version number for optimistic locking. Include this when updating to detect conflicts.
- `provider` (uuid | null, required) — The provider of the shift. If null, the shift is an open shift.
- `providerName` (string | null) — The display name of the provider. Only populated when the request opts in via includeDisplayNames.
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `segments` (ShiftSegment[], required) — The segments that make up a shift, ordered by startDate.
- `importedId` (string | null, required) — If a segment was created in an external system, this is the ID of the segment in that system.
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `role` (uuid | null, required) — The role of the shift. Can be null if the shift is pulled from an external system.
- `roleName` (string | null) — The display name of the segment's role. Only populated when the request opts in via includeDisplayNames.
- `roleColor` (string | null) — The hex color of the segment's role. Only populated when the request opts in via includeDisplayNames.
- `roleImportedId` (string) — *DEPRECATED* The ID of the role in the external system. Once all roles are migrated to the system, this field will be removed.
- `additionalInfo` (any) — *DEPRECATED* Additional information about the segment.
- `isBreak` (boolean, required, example false) — Used to denote whether or not the provider will be on break during this segment.
- `isOnCall` (boolean, required, example false)
- `type` (enum, required, one of `working`, `paid_break`, `unpaid_break`, `on_call`) — The type of the segment.
- `startDate` (string, required, example "2024-11-20T07:43:34+0000") — The timestamp of when the shift starts.
- `endDate` (string, required, example "2024-11-20T07:43:34+0000") — The timestamp of when the shift ends.
- `skillRequirement` (NullableSkillRequirement)
- Option 1: SkillRequirementCondition
- Option 2: SkillRequirementGroup
- `labels` (string[], required) — The label IDs associated with this shift segment.
- `payCodes` (ShiftPayCode[], max items 1)
- `payCodeId` (uuid, required) — The pay code ID associated with this shift.
- `value` (number) — Only present for variable pay codes. The value of the pay code for this shift.
- `team` (uuid | null, required) — The team that the shift is associated with.
- `note` (string | null, required) — A note about the shift. Visible to providers.
- `isCallout` (boolean, required) — Whether the shift is marked as a callout. Callout shifts are hidden from reads unless an endpoint explicitly includes them.
- `workRuleOverride` (uuid | null, required)
- `cursor` (string)
## Errors
- `validation` (400) — Input validation failed.
- `unauthorized` (403) — You do not have permission to perform this action.
---
# Get Teams
> GET /v1/teams
`GET https://app.untetherlabs.com/api/v1/teams`
Retrieve all teams
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/teams' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (Team[], required)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000") — The timestamp of when the team was created.
- `name` (string, required) — The name of the team.
- `shorthandName` (string | null) — The shorthand name of the team.
- `cursor` (string)
## Errors
- `validation` (400) — Input validation failed.
---
# Get Team
> GET /v1/teams/{teamId}
`GET https://app.untetherlabs.com/api/v1/teams/{teamId}`
Get team information by ID
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/teams/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `teamId` (uuid, required)
## Response 200
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000") — The timestamp of when the team was created.
- `name` (string, required) — The name of the team.
- `shorthandName` (string | null) — The shorthand name of the team.
## Errors
- `validation` (400) — Input validation failed.
- `team-not-found` (404) — A specified team does not exist in the system.
---
# Get Policies
> GET /v1/time-off/policies
`GET https://app.untetherlabs.com/api/v1/time-off/policies`
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/time-off/policies' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `expand` (string) — Available expansions: `properties`, `payCode` See [Query Parameters](/using-the-api/query-parameters) for more information on expansions.
- `providerId` (uuid) — Filter to only policies assigned to the given provider.
- `defaultsForProviderId` (uuid) — Annotate every policy with `enrolled`, `suggested` and `teammateCount` for the given provider, derived from what their active teammates hold. Does not filter.
- `publishedOnly` (boolean, default false) — Filter to only policies with `properties.published = true`.
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (TimeOffPolicy[], required)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `type` (enum, required, example "vacation", one of `bereavement`, `floating-holiday`, `holiday-pay`, `jury-duty`, `learning-and-development`, `paid-time-off`, `vacation`, `parental-leave`, `personal-day`, `sick`, `volunteer`, `weather`, `wellness`, `flex-day`, `marriage-code`, `unpaid-day`, and 2 more)
- `name` (string, required, example "QC Vacation")
- `description` (string | null, required, example "Vacation policy for Quebec-based employees.")
- `accrualUnit` (one of, required)
- Option 1: AccrualUnit
- `yearlyAccrual` (number | null) — Allowance granted per accrual period, in `accrualUnit`.
- `accrualPeriodRRule` (string | null) — RRULE describing when the balance resets.
- `properties` (any)
- `payCodeId` (uuid | null, required) — The pay code for which associated time will be paid out.
- `payCode` (one of)
- Option 1: PayCode
- `name` (string, required, example "Overtime")
- `shorthandName` (string, required, example "OT")
- `hexColor` (string, required, example "#f97316")
- `type` (enum, required, one of `hours`, `days`, `money`)
- `value` (number, required, example 1.5)
- `enrolled` (boolean) — Only with `defaultsForProviderId`: whether that provider is enrolled in this policy.
- `suggested` (boolean) — Only with `defaultsForProviderId`: most of the provider’s active teammates hold this policy. For vacation policies, only the single most common one is suggested.
- `teammateCount` (integer) — Only with `defaultsForProviderId`: number of the provider’s active teammates enrolled in this policy.
- `cursor` (string)
## Errors
- `validation` (400) — Input validation failed.
---
# Get Policy
> GET /v1/time-off/policies/{policyId}
`GET https://app.untetherlabs.com/api/v1/time-off/policies/{policyId}`
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/time-off/policies/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `policyId` (uuid, required)
## Query parameters
- `expand` (string) — Available expansions: `properties`, `payCode` See [Query Parameters](/using-the-api/query-parameters) for more information on expansions.
## Response 200
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `type` (enum, required, example "vacation", one of `bereavement`, `floating-holiday`, `holiday-pay`, `jury-duty`, `learning-and-development`, `paid-time-off`, `vacation`, `parental-leave`, `personal-day`, `sick`, `volunteer`, `weather`, `wellness`, `flex-day`, `marriage-code`, `unpaid-day`, and 2 more)
- `name` (string, required, example "QC Vacation")
- `description` (string | null, required, example "Vacation policy for Quebec-based employees.")
- `accrualUnit` (one of, required)
- Option 1: AccrualUnit
- `yearlyAccrual` (number | null) — Allowance granted per accrual period, in `accrualUnit`.
- `accrualPeriodRRule` (string | null) — RRULE describing when the balance resets.
- `properties` (any)
- `payCodeId` (uuid | null, required) — The pay code for which associated time will be paid out.
- `payCode` (one of)
- Option 1: PayCode
- `name` (string, required, example "Overtime")
- `shorthandName` (string, required, example "OT")
- `hexColor` (string, required, example "#f97316")
- `type` (enum, required, one of `hours`, `days`, `money`)
- `value` (number, required, example 1.5)
- `enrolled` (boolean) — Only with `defaultsForProviderId`: whether that provider is enrolled in this policy.
- `suggested` (boolean) — Only with `defaultsForProviderId`: most of the provider’s active teammates hold this policy. For vacation policies, only the single most common one is suggested.
- `teammateCount` (integer) — Only with `defaultsForProviderId`: number of the provider’s active teammates enrolled in this policy.
## Errors
- `validation` (400) — Input validation failed.
- `policy-not-found` (404) — A specified policy does not exist in the system.
---
# Get Requests
> GET /v1/time-off/requests
`GET https://app.untetherlabs.com/api/v1/time-off/requests`
Get all time off requests. Can be filtered if necessary.
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/time-off/requests' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `providerId` (uuid) — Filter to requests for a specific provider.
- `teamId` (one of) — Filter to requests for providers in specific teams.
- Option 1: uuid
- Option 2: uuid[]
- `policyId` (one of) — Filter to requests for specific policies.
- Option 1: uuid
- Option 2: uuid[]
- `startDate` (string, example "2024-11-20T07:43:34+0000") — Filter to requests whose start date is on or after the given timestamp.
- `endDate` (string, example "2024-11-20T07:43:34+0000") — Filter to requests whose end date is earlier than the provided timestamp.
- `status` (TimeOffRequestStatus, one of `pending`, `approved`, `rejected`, `cancelled`, `pending_sync`, `sync_failed`)
- `query` (string)
- `expand` (string) — Available expansions: `policy`, `provider`, `rejectedBy` See [Query Parameters](/using-the-api/query-parameters) for more information on expansions.
- `sort` (string) — Available fields: `createdAt`, `startDate` See [Query Parameters](/using-the-api/query-parameters) for more information on sorting.
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (TimeOffRequest[], required)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `status` (TimeOffRequestStatus, required, one of `pending`, `approved`, `rejected`, `cancelled`, `pending_sync`, `sync_failed`)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `policyId` (uuid, required)
- `policy` (TimeOffPolicy)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `type` (enum, required, example "vacation", one of `bereavement`, `floating-holiday`, `holiday-pay`, `jury-duty`, `learning-and-development`, `paid-time-off`, `vacation`, `parental-leave`, `personal-day`, `sick`, `volunteer`, `weather`, `wellness`, `flex-day`, `marriage-code`, `unpaid-day`, and 2 more)
- `name` (string, required, example "QC Vacation")
- `description` (string | null, required, example "Vacation policy for Quebec-based employees.")
- `accrualUnit` (one of, required)
- Option 1: AccrualUnit
- `yearlyAccrual` (number | null) — Allowance granted per accrual period, in `accrualUnit`.
- `accrualPeriodRRule` (string | null) — RRULE describing when the balance resets.
- `properties` (any)
- `payCodeId` (uuid | null, required) — The pay code for which associated time will be paid out.
- `payCode` (one of)
- Option 1: PayCode
- `enrolled` (boolean) — Only with `defaultsForProviderId`: whether that provider is enrolled in this policy.
- `suggested` (boolean) — Only with `defaultsForProviderId`: most of the provider’s active teammates hold this policy. For vacation policies, only the single most common one is suggested.
- `teammateCount` (integer) — Only with `defaultsForProviderId`: number of the provider’s active teammates enrolled in this policy.
- `providerId` (uuid, required)
- `provider` (Provider)
- `name` (string | null, required, example "Bruce Wayne")
- `email` (string, required, example "bruce.wayne@goodhealth.com")
- `hireDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `jobTitle` (string | null, required, example "Registered Nurse") — The provider's job title.
- `status` (enum, required, example "active", one of `onboarding`, `active`, `dismissed`, `unmanaged`)
- `teamId` (uuid | null, required)
- `licensedRegions` (string[], required)
- `employmentType` (uuid | null, required)
- `note` (string | null, required, example "Going on vacation with my family.")
- `dailyHours` (DailyHours, required)
- `date` (string, required, example "2024-10-06T00:00:00.000Z") — ISO timestamp of the date for the daily hours.
- `duration` (number, required, example 8) — Number of hours requested off for this day.
- `paidHours` (number, required, example 32) — The total number of hours that will be paid out for this time off request.
- `rejectedById` (uuid) — Only present for rejected time off requests, the user who rejected the request.
- `rejectedBy` (User)
- `type` (UserType, required, one of `user`, `bot`)
- `name` (string, required)
- `email` (string, required)
- `status` (EmploymentStatus, required, one of `onboarding`, `active`, `dismissed`, `unmanaged`)
- `isAdmin` (boolean, required)
- `rejectionReason` (string | null) — Only present for rejected time off requests, reason may be null for unspecified.
- `syncError` (object | null) — Present for pending_sync requests, contains the latest Rippling sync status and error message.
- `status` (enum, required, one of `success`, `failed`)
- `message` (string | null, required)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `conflictResolution` (enum | null, one of `delete`, `deleteWithOpen`, `callout`, `calloutWithOpen`, `keep`, `keepWithOpen`, `null`) — The shift conflict resolution strategy selected when this request was approved.
- `isFullDay` (boolean | null) — Whether the request blocks whole calendar days rather than the dailyHours windows. Null for requests created before this field existed; treated as full-day when the policy accrues in days.
- `cursor` (string)
## Errors
- `negative-range` (400) — Time range end date must come after start date.
- `validation` (400) — Input validation failed.
---
# Get Request
> GET /v1/time-off/requests/{requestId}
`GET https://app.untetherlabs.com/api/v1/time-off/requests/{requestId}`
Get a specific time off request
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/time-off/requests/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `requestId` (uuid, required)
## Query parameters
- `expand` (string) — Available expansions: `policy`, `provider`, `rejectedBy` See [Query Parameters](/using-the-api/query-parameters) for more information on expansions.
## Response 200
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `status` (TimeOffRequestStatus, required, one of `pending`, `approved`, `rejected`, `cancelled`, `pending_sync`, `sync_failed`)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `policyId` (uuid, required)
- `policy` (TimeOffPolicy)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `type` (enum, required, example "vacation", one of `bereavement`, `floating-holiday`, `holiday-pay`, `jury-duty`, `learning-and-development`, `paid-time-off`, `vacation`, `parental-leave`, `personal-day`, `sick`, `volunteer`, `weather`, `wellness`, `flex-day`, `marriage-code`, `unpaid-day`, and 2 more)
- `name` (string, required, example "QC Vacation")
- `description` (string | null, required, example "Vacation policy for Quebec-based employees.")
- `accrualUnit` (one of, required)
- Option 1: AccrualUnit
- `yearlyAccrual` (number | null) — Allowance granted per accrual period, in `accrualUnit`.
- `accrualPeriodRRule` (string | null) — RRULE describing when the balance resets.
- `properties` (any)
- `payCodeId` (uuid | null, required) — The pay code for which associated time will be paid out.
- `payCode` (one of)
- Option 1: PayCode
- `name` (string, required, example "Overtime")
- `shorthandName` (string, required, example "OT")
- `hexColor` (string, required, example "#f97316")
- `type` (enum, required, one of `hours`, `days`, `money`)
- `value` (number, required, example 1.5)
- `enrolled` (boolean) — Only with `defaultsForProviderId`: whether that provider is enrolled in this policy.
- `suggested` (boolean) — Only with `defaultsForProviderId`: most of the provider’s active teammates hold this policy. For vacation policies, only the single most common one is suggested.
- `teammateCount` (integer) — Only with `defaultsForProviderId`: number of the provider’s active teammates enrolled in this policy.
- `providerId` (uuid, required)
- `provider` (Provider)
- `name` (string | null, required, example "Bruce Wayne")
- `email` (string, required, example "bruce.wayne@goodhealth.com")
- `hireDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `jobTitle` (string | null, required, example "Registered Nurse") — The provider's job title.
- `status` (enum, required, example "active", one of `onboarding`, `active`, `dismissed`, `unmanaged`)
- `teamId` (uuid | null, required)
- `licensedRegions` (string[], required)
- `employmentType` (uuid | null, required)
- `note` (string | null, required, example "Going on vacation with my family.")
- `dailyHours` (DailyHours, required)
- `date` (string, required, example "2024-10-06T00:00:00.000Z") — ISO timestamp of the date for the daily hours.
- `duration` (number, required, example 8) — Number of hours requested off for this day.
- `paidHours` (number, required, example 32) — The total number of hours that will be paid out for this time off request.
- `rejectedById` (uuid) — Only present for rejected time off requests, the user who rejected the request.
- `rejectedBy` (User)
- `type` (UserType, required, one of `user`, `bot`)
- `name` (string, required)
- `email` (string, required)
- `status` (EmploymentStatus, required, one of `onboarding`, `active`, `dismissed`, `unmanaged`)
- `isAdmin` (boolean, required)
- `rejectionReason` (string | null) — Only present for rejected time off requests, reason may be null for unspecified.
- `syncError` (object | null) — Present for pending_sync requests, contains the latest Rippling sync status and error message.
- `status` (enum, required, one of `success`, `failed`)
- `message` (string | null, required)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `conflictResolution` (enum | null, one of `delete`, `deleteWithOpen`, `callout`, `calloutWithOpen`, `keep`, `keepWithOpen`, `null`) — The shift conflict resolution strategy selected when this request was approved.
- `isFullDay` (boolean | null) — Whether the request blocks whole calendar days rather than the dailyHours windows. Null for requests created before this field existed; treated as full-day when the policy accrues in days.
## Errors
- `validation` (400) — Input validation failed.
- `time-off-request-not-found` (404) — A specified time off request does not exist in the system.
---
# Get Time Off Requests Count
> GET /v1/time-off/requests/count
`GET https://app.untetherlabs.com/api/v1/time-off/requests/count`
Get the total count of time off requests for the current filters.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/time-off/requests/count' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `providerId` (uuid) — Filter to requests for a specific provider.
- `teamId` (one of) — Filter to requests for providers in specific teams.
- Option 1: uuid
- Option 2: uuid[]
- `policyId` (one of) — Filter to requests for specific policies.
- Option 1: uuid
- Option 2: uuid[]
- `startDate` (string, example "2024-11-20T07:43:34+0000") — Filter to requests whose start date is on or after the given timestamp.
- `endDate` (string, example "2024-11-20T07:43:34+0000") — Filter to requests whose end date is earlier than the provided timestamp.
- `status` (TimeOffRequestStatus, one of `pending`, `approved`, `rejected`, `cancelled`, `pending_sync`, `sync_failed`)
- `query` (string)
- `expand` (string) — Available expansions: `policy`, `provider`, `rejectedBy` See [Query Parameters](/using-the-api/query-parameters) for more information on expansions.
- `sort` (string) — Available fields: `createdAt`, `startDate` See [Query Parameters](/using-the-api/query-parameters) for more information on sorting.
## Response 200
- `count` (number, required)
## Errors
- `negative-range` (400) — Time range end date must come after start date.
- `validation` (400) — Input validation failed.
---
# Get Timecard
> GET /v1/timekeeping/timecard/{provider}
`GET https://app.untetherlabs.com/api/v1/timekeeping/timecard/{provider}`
Get the current timecard for the given provider.
Returns no punches + clock_in if there is no active timecard.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/timekeeping/timecard/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `provider` (uuid, required)
## Response 200
- `punches` (TimecardPunch[], required)
- `timecardId` (uuid, required)
- `providerId` (uuid, required)
- `generated` (boolean, default false)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `updatedAt` (string, required, example "2024-11-20T07:43:34+0000")
- `action` (string, required)
- `state` (TimecardPunchState, required, one of `pending`, `approved`, `rejected`)
- `time` (string, required, example "2024-11-20T07:43:34+0000")
- `providerNote` (string | null, required, example "Felt sick, had to leave early.")
- `adminNote` (string | null, required, example "Adjusted to schedule.")
- `nextActions` (string[], required)
## Errors
- `validation` (400) — Input validation failed.
- `provider-not-found` (404) — A specified provider does not exist in the system.
---
# Get Timesheets
> GET /v1/timekeeping/timesheet
`GET https://app.untetherlabs.com/api/v1/timekeeping/timesheet`
Allows reading computed timesheet data for one or more providers.
Timesheets are returned for any matching provider who was active during the
specified date range even if they worked zero hours. These values are impacted
by historical corrections, you are advised to review the [Historical
Corrections](/using-the-api/historical-corrections) documentation.
* **provider**: Includes the entire provider object for each timesheet.
* **paySegments**: Pay segments describe each individually worked span
during the timesheet. They are the primitive unit of timekeeping data.
* **scheduledPaySegments**: Scheduled pay segments represent the expected
set of worked segments based on the schedule only, not punch data.
* **problems**: Includes any warnings and errors for the timesheet.
* **approval**: Includes whether the entire range is approved, and the
exact sub ranges that have been approved.
* **signOff**: Includes whether the entire range is signed off, and the
exact sub ranges that have been signed off.
* **historicalCorrections**: Includes any historical corrections which
are applied to the time range of the timesheet. These are historical
corrections which were made _to_ this time range as opposed to applied
to/paid out in this range.
* **totals**: The computed totals broken down by pay code and role for the
requested range.
* **historicalCorrectionTotals**: The totals from any historical corrections
which are applied _to_ this timesheet (as opposed to corrections made
to this timesheet). Historical correction totals are only available
when requesting a particular pay period (by offset or exact reference).
This endpoint may have to do significant computation to generate timesheet data on-the-fly,
you are advised to set appropriate timeout and consider caching the results if applicable.
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/timekeeping/timesheet' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `teamId` (uuid)
- `providerId` (uuid)
- `payPeriodId` (uuid)
- `payPeriodOffset` (integer) — Optional offset from current pay period. 0 = current, 1 = next, -1 = previous.
- `startDate` (string, example "2024-11-20T07:43:34+0000")
- `endDate` (string, example "2024-11-20T07:43:34+0000")
- `expand` (string) — Available expansions: `provider`, `paySegments`, `scheduledPaySegments`, `problems`, `approval`, `signOff`, `historicalCorrections`, `totals`, `historicalCorrectionTotals` See [Query Parameters](/using-the-api/query-parameters) for more information on expansions.
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (Timesheet[], required)
- `providerId` (uuid, required)
- `provider` (Provider)
- `name` (string | null, required, example "Bruce Wayne")
- `email` (string, required, example "bruce.wayne@goodhealth.com")
- `hireDate` (string | null, required, example "2024-11-20T07:43:34+0000")
- `jobTitle` (string | null, required, example "Registered Nurse") — The provider's job title.
- `status` (enum, required, example "active", one of `onboarding`, `active`, `dismissed`, `unmanaged`)
- `teamId` (uuid | null, required)
- `licensedRegions` (string[], required)
- `employmentType` (uuid | null, required)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `payPeriodId` (uuid | null, required) — The matching pay period, or null if the range does not align to a single pay period.
- `scheduledShifts` (Shift[], required)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `version` (number) — Version number for optimistic locking. Include this when updating to detect conflicts.
- `provider` (uuid | null, required) — The provider of the shift. If null, the shift is an open shift.
- `providerName` (string | null) — The display name of the provider. Only populated when the request opts in via includeDisplayNames.
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `segments` (ShiftSegment[], required) — The segments that make up a shift, ordered by startDate.
- `importedId` (string | null, required) — If a segment was created in an external system, this is the ID of the segment in that system.
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `role` (uuid | null, required) — The role of the shift. Can be null if the shift is pulled from an external system.
- `roleName` (string | null) — The display name of the segment's role. Only populated when the request opts in via includeDisplayNames.
- `roleColor` (string | null) — The hex color of the segment's role. Only populated when the request opts in via includeDisplayNames.
- `roleImportedId` (string) — *DEPRECATED* The ID of the role in the external system. Once all roles are migrated to the system, this field will be removed.
- `additionalInfo` (any) — *DEPRECATED* Additional information about the segment.
- `isBreak` (boolean, required, example false) — Used to denote whether or not the provider will be on break during this segment.
- `isOnCall` (boolean, required, example false)
- `type` (enum, required, one of `working`, `paid_break`, `unpaid_break`, `on_call`) — The type of the segment.
- `startDate` (string, required, example "2024-11-20T07:43:34+0000") — The timestamp of when the shift starts.
- `endDate` (string, required, example "2024-11-20T07:43:34+0000") — The timestamp of when the shift ends.
- `skillRequirement` (NullableSkillRequirement)
- `labels` (string[], required) — The label IDs associated with this shift segment.
- `payCodes` (ShiftPayCode[], max items 1)
- `payCodeId` (uuid, required) — The pay code ID associated with this shift.
- `value` (number) — Only present for variable pay codes. The value of the pay code for this shift.
- `team` (uuid | null, required) — The team that the shift is associated with.
- `note` (string | null, required) — A note about the shift. Visible to providers.
- `isCallout` (boolean, required) — Whether the shift is marked as a callout. Callout shifts are hidden from reads unless an endpoint explicitly includes them.
- `workRuleOverride` (uuid | null, required)
- `punches` (TimecardPunch[], required)
- `timecardId` (uuid, required)
- `providerId` (uuid, required)
- `generated` (boolean, default false)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `updatedAt` (string, required, example "2024-11-20T07:43:34+0000")
- `action` (string, required)
- `state` (TimecardPunchState, required, one of `pending`, `approved`, `rejected`)
- `time` (string, required, example "2024-11-20T07:43:34+0000")
- `providerNote` (string | null, required, example "Felt sick, had to leave early.")
- `adminNote` (string | null, required, example "Adjusted to schedule.")
- `paySegments` (PaySegment[])
- `type` (PaySegmentType, required, one of `paid`, `unpaid`, `on_call`)
- `itemId` (string | null)
- `shiftId` (string | null)
- `timecardId` (string | null)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000") — The start date of the pay segment.
- `endDate` (string, required, example "2024-11-20T07:43:34+0000") — The end date of the pay segment.
- `payCodes` (string[], required)
- `overtime` (string[], required)
- `timeRanges` (string[], required)
- `workRuleOverride` (string | null)
- `role` (string | null)
- `scheduledPaySegments` (PaySegment[])
- `type` (PaySegmentType, required, one of `paid`, `unpaid`, `on_call`)
- `itemId` (string | null)
- `shiftId` (string | null)
- `timecardId` (string | null)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000") — The start date of the pay segment.
- `endDate` (string, required, example "2024-11-20T07:43:34+0000") — The end date of the pay segment.
- `payCodes` (string[], required)
- `overtime` (string[], required)
- `timeRanges` (string[], required)
- `workRuleOverride` (string | null)
- `role` (string | null)
- `warnings` (TimesheetWarning[])
- `type` (TimesheetWarningType, required, one of `clock_in_early`, `clock_out_early`, `clock_in_late`, `clock_out_late`, `missing_shift`)
- `punchId` (uuid, required)
- `errors` (TimesheetError[])
- `type` (TimesheetErrorType, required, one of `no_clock_out`, `invalid_shift`, `missing_timecard`, `missing_pay_period_template`)
- `shiftId` (uuid)
- `timecardId` (uuid)
- `approval` (TimesheetApproval)
- `approved` (boolean, required)
- `ranges` (TimeRange[], required)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `signOff` (TimesheetSignOff)
- `signedOff` (boolean, required)
- `ranges` (TimeRange[], required)
- `startDate` (string, required, example "2024-11-20T07:43:34+0000")
- `endDate` (string, required, example "2024-11-20T07:43:34+0000")
- `historicalCorrections` (HistoricalCorrection[])
- `status` (HistoricalCorrectionStatus, required, one of `pending`, `completed`)
- `changes` (HistoricalCorrectionChange[], required)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `actor` (object | null, required) — A null actor indicates a system-generated change
- `note` (string)
- `auditLogReference` (uuid | null, required) — A null audit log reference indicates a manually triggered historical correction
- `auditLogAction` (string | null, required) — The action associated with the audit log reference. Will be null if there is no audit log reference.
- `payCodeDifference` (HistoricalCorrectionPayCodeDifference[], required)
- `date` (string, required, example "2024-11-20T07:43:34+0000") — Always represents the start of the day.
- `payCode` (uuid, required)
- `role` (string | null, required)
- `value` (number, required) — The difference in **minutes** (may be negative)
- `totals` (PayCodeTotal[])
- `payCodeId` (uuid, required)
- `roleId` (uuid | null, required) — The role worked during this time, or null for non-working time.
- `scheduled` (number, required) — The number of minutes scheduled for this pay code and role.
- `actual` (number, required) — The number of minutes actually worked for this pay code and role.
- `historicalCorrectionTotals` (PayCodeTotal[])
- `payCodeId` (uuid, required)
- `roleId` (uuid | null, required) — The role worked during this time, or null for non-working time.
- `scheduled` (number, required) — The number of minutes scheduled for this pay code and role.
- `actual` (number, required) — The number of minutes actually worked for this pay code and role.
- `cursor` (string)
## Errors
- `negative-range` (400) — Time range end date must come after start date.
- `validation` (400) — Input validation failed.
---
# Create Timecard Punch
> POST /v1/timekeeping/timecard/punches
`POST https://app.untetherlabs.com/api/v1/timekeeping/timecard/punches`
Create a new punch for a specific timecard
## Request
```bash
curl -X POST 'https://app.untetherlabs.com/api/v1/timekeeping/timecard/punches' \
-H 'Authorization: Bearer $UNTETHER_API_KEY' \
-H 'Content-Type: application/json' \
-d @body.json
```
## Request body (application/json)
- `provider` (uuid, required)
- `action` (string, required)
- `time` (string, example "2024-11-20T07:43:34+0000") — Punch timestamp, or now if not provided
- `state` (TimecardPunchState, default "pending", one of `pending`, `approved`, `rejected`)
- `providerNote` (string, example "Felt sick, had to leave early.")
- `adminNote` (string, example "Adjusted to schedule.")
## Response 201
- `timecardId` (uuid, required)
- `providerId` (uuid, required)
- `generated` (boolean, default false)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `updatedAt` (string, required, example "2024-11-20T07:43:34+0000")
- `action` (string, required)
- `state` (TimecardPunchState, required, one of `pending`, `approved`, `rejected`)
- `time` (string, required, example "2024-11-20T07:43:34+0000")
- `providerNote` (string | null, required, example "Felt sick, had to leave early.")
- `adminNote` (string | null, required, example "Adjusted to schedule.")
## Errors
- `validation` (400) — Input validation failed.
- `timecard-not-found` (404) — A specified timecard does not exist in the system.
---
# Delete Timecard Punch
> DELETE /v1/timekeeping/timecard/punches/{punchId}
`DELETE https://app.untetherlabs.com/api/v1/timekeeping/timecard/punches/{punchId}`
Delete a punch
## Request
```bash
curl -X DELETE 'https://app.untetherlabs.com/api/v1/timekeeping/timecard/punches/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `punchId` (uuid, required)
## Response 200
- `timecardId` (uuid, required)
- `providerId` (uuid, required)
- `generated` (boolean, default false)
- `createdAt` (string, required, example "2024-11-20T07:43:34+0000")
- `updatedAt` (string, required, example "2024-11-20T07:43:34+0000")
- `action` (string, required)
- `state` (TimecardPunchState, required, one of `pending`, `approved`, `rejected`)
- `time` (string, required, example "2024-11-20T07:43:34+0000")
- `providerNote` (string | null, required, example "Felt sick, had to leave early.")
- `adminNote` (string | null, required, example "Adjusted to schedule.")
## Errors
- `validation` (400) — Input validation failed.
- `timecard-punch-not-found` (404) — A specified timecard punch does not exist in the system.
---
# Get Users
> GET /v1/users
`GET https://app.untetherlabs.com/api/v1/users`
Search for registered users in the Untether app.
Users are a superset of [Providers](/reference/v1/providers/get-providers),
but providers are used for most platform operations. Users and providers share
the same IDs, but not all users have an associated provider.
This endpoint is paginated. See /using-the-api/pagination.
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/users' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Query parameters
- `type` (UserType[], default ["user"], one of `user`, `bot`)
- `status` (EmploymentStatus[], default ["onboarding","active"], one of `onboarding`, `active`, `dismissed`, `unmanaged`)
- `name` (string, max length 50) — Case-insensitive partial matching name search string
- `pageSize` (number, default 20, min 1, max 200)
- `cursor` (string)
## Response 200
- `items` (User[], required)
- `type` (UserType, required, one of `user`, `bot`)
- `name` (string, required)
- `email` (string, required)
- `status` (EmploymentStatus, required, one of `onboarding`, `active`, `dismissed`, `unmanaged`)
- `isAdmin` (boolean, required)
- `cursor` (string)
## Errors
- `validation` (400) — Input validation failed.
---
# Get User
> GET /v1/users/{userId}
`GET https://app.untetherlabs.com/api/v1/users/{userId}`
## Request
```bash
curl -X GET 'https://app.untetherlabs.com/api/v1/users/3fa85f64-5717-4562-b3fc-2c963f66afa6' \
-H 'Authorization: Bearer $UNTETHER_API_KEY'
```
## Path parameters
- `userId` (uuid, required)
## Response 200
- `type` (UserType, required, one of `user`, `bot`)
- `name` (string, required)
- `email` (string, required)
- `status` (EmploymentStatus, required, one of `onboarding`, `active`, `dismissed`, `unmanaged`)
- `isAdmin` (boolean, required)
## Errors
- `validation` (400) — Input validation failed.
- `user-not-found` (404) — A specified user does not exist in the system.