
## Workday history

`GET /v1/workday/records`

Returns an employee's workday history: start and end times, seconds worked, break durations, and an offset calculated for each record from the current IANA time zone (`TIME_ZONE`) in the employee profile. Records are selected by the required employee identifier and a period; without a period the window covers the last 7 days.

This endpoint requires both `timeman` and one of `user_brief`, `user_basic`, or `user`. An empty page is returned without an additional profile lookup. If the current IANA profile zone cannot be retrieved or its historical rules cannot yield an offset for a non-empty page, the endpoint returns `502 BITRIX_UNAVAILABLE` with no partial data. The API cannot detect a zone reassignment after record creation, so that case does not produce a 502.

## Parameters

| Parameter | Type | Required | Description |
|----------|-----|--------------|---------|
| `userId` (query) | number | yes | Employee identifier, an integer greater than zero. Your own identifier — [`GET /v1/users/me`](/docs/entities/users/me), the employee list — [`GET /v1/users`](/docs/entities/users/list) |
| `from` (query) | string | no | Start of the period, ISO-8601 with an explicit offset or `Z` — for example `2026-06-01T00:00:00Z`. Filters by the day start time. A date without a time returns `400`. Encode the plus sign in the offset as `%2B`; otherwise the value arrives with a space instead of the sign and is not recognized |
| `to` (query) | string | no | End of the period in the same format. A `from` later than `to` returns `400` |
| `limit` (query) | number | no | Records per page, from 1 to 50. Defaults to `50` |
| `offset` (query) | number | no | Offset from the start of the selection, a non-negative integer. Defaults to `0`. Cannot be combined with `page` |
| `page` (query) | number | no | Page number starting from one, equivalent to `offset` with the value `(page − 1) × limit`. Cannot be combined with `offset` |
| `order` (query) | string | no | Order by the day start time: `desc` — newest first, `asc` — oldest first. Defaults to `desc` |

Both ends of the period are optional: if neither `from` nor `to` is passed, the selection is limited to the last 7 days from the moment of the request. A query parameter outside this list returns `400 INVALID_PARAMS` with the name of the passed key and the list of accepted ones.

## Examples

### curl — personal key

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://vibecode.bitrix24.com/v1/workday/records?userId=1&from=2020-01-01T00:00:00Z&limit=3&order=asc"
```

### curl — another employee's records

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://vibecode.bitrix24.com/v1/workday/records?userId=503&from=2020-01-01T00:00:00Z&limit=3&order=asc"
```

### curl — OAuth application

```bash
curl -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  "https://vibecode.bitrix24.com/v1/workday/records?userId=1&from=2020-01-01T00:00:00Z&limit=3&order=asc"
```

### JavaScript — personal key

```javascript
const params = new URLSearchParams({
  userId: '1',
  from: '2020-01-01T00:00:00Z',
  limit: '3',
  order: 'asc',
})
const res = await fetch(`https://vibecode.bitrix24.com/v1/workday/records?${params}`, {
  headers: { 'X-Api-Key': 'YOUR_API_KEY' },
})
const { data, meta } = await res.json()
for (const record of data) {
  console.log(record.startTime, '— seconds worked:', record.duration)
}
console.log('More records available:', meta.hasMore)
```

### JavaScript — OAuth application

```javascript
const params = new URLSearchParams({
  userId: '1',
  from: '2020-01-01T00:00:00Z',
  limit: '3',
  order: 'asc',
})
const res = await fetch(`https://vibecode.bitrix24.com/v1/workday/records?${params}`, {
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
  },
})
const { data, meta } = await res.json()
```

## Response fields

| Field | Type | Description |
|------|-----|---------|
| `success` | boolean | `true` on a successful response |
| `data` | array | Array of workday records. An empty array when no record falls within the period |
| `data[].id` | number | Workday record identifier |
| `data[].userId` | number | Identifier of the employee the record belongs to |
| `data[].startTime` | string | Day start time, ISO-8601. The string keeps the account offset and is not rewritten to the employee's time zone |
| `data[].endTime` | string \| null | Day end time in the same format. On a day that is still open the field is absent or arrives as `null` |
| `data[].duration` | number \| null | Time worked during the day, in seconds. On a day that is still open the field is absent or arrives as `null` |
| `data[].breakLength` | number \| null | Total break duration for the day, in seconds. On a day that is still open the field is absent or arrives as `null` |
| `data[].state.status` | string | Record state in lowercase. On a finished day it is `closed` |
| `data[].state.recommendedCloseTime` | string \| null | Recommended day close time. For the records checked, this value is `null` |
| `data[].isApproved` | boolean \| null | Whether the workday record is approved. On a day that is still open the field is absent or arrives as `null` |
| `data[].tzOffset` | number | Required offset in seconds east of UTC, calculated for the `startTime` instant using the historical rules of the current IANA time zone (`TIME_ZONE`) in the employee profile. It is not proof that this zone was assigned when the record was created |
| `meta.hasMore` | boolean | `true` when the page is filled up to `limit` and more records follow |
| `meta.total` | number | Number of records in the selection — the sum of `offset` and the page length. Arrives only when the page is shorter than `limit` |
| `meta.from` | string | The lower bound of the applied period. Present when that bound is set — either by the `from` passed in or by the substituted last-7-days window |
| `meta.to` | string | The upper bound of the applied period. Absent for a period with no upper bound (only `from` is passed) |

## Response example

The first three records from oldest to newest, `?userId=1&from=2020-01-01T00:00:00Z&limit=3&order=asc`:

```json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "userId": 1,
      "startTime": "2020-04-23T13:42:10+00:00",
      "endTime": "2020-04-23T17:53:26+00:00",
      "duration": 12999,
      "breakLength": 2077,
      "state": { "status": "closed", "recommendedCloseTime": null },
      "isApproved": true,
      "tzOffset": 7200
    },
    {
      "id": 5,
      "userId": 1,
      "startTime": "2020-04-24T09:16:12+00:00",
      "endTime": "2020-04-24T16:30:13+00:00",
      "duration": 26041,
      "breakLength": 0,
      "state": { "status": "closed", "recommendedCloseTime": null },
      "isApproved": true,
      "tzOffset": 7200
    },
    {
      "id": 7,
      "userId": 1,
      "startTime": "2020-04-27T09:04:11+00:00",
      "endTime": "2020-04-27T18:00:00+00:00",
      "duration": 32149,
      "breakLength": 0,
      "state": { "status": "closed", "recommendedCloseTime": null },
      "isApproved": true,
      "tzOffset": 7200
    }
  ],
  "meta": { "hasMore": true, "from": "2020-01-01T00:00:00Z" }
}
```

The last page of the same selection, `?userId=1&from=2020-01-01T00:00:00Z&offset=40&order=asc` — fewer records than `limit` arrived, so `total` appears in `meta`:

```json
{
  "success": true,
  "data": [
    {
      "id": 163,
      "userId": 1,
      "startTime": "2024-07-24T17:34:51+00:00",
      "endTime": "2024-07-25T02:35:00+00:00",
      "duration": 32409,
      "breakLength": 0,
      "state": { "status": "closed", "recommendedCloseTime": null },
      "isApproved": true,
      "tzOffset": 7200
    },
    {
      "id": 165,
      "userId": 1,
      "startTime": "2024-08-26T12:34:47+00:00",
      "endTime": "2024-08-26T12:58:15+00:00",
      "duration": 1408,
      "breakLength": 0,
      "state": { "status": "closed", "recommendedCloseTime": null },
      "isApproved": true,
      "tzOffset": 7200
    },
    {
      "id": 167,
      "userId": 1,
      "startTime": "2024-09-05T12:07:30+00:00",
      "endTime": "2025-03-27T14:53:08+00:00",
      "duration": 17548827,
      "breakLength": 311,
      "state": { "status": "closed", "recommendedCloseTime": null },
      "isApproved": true,
      "tzOffset": 7200
    },
    {
      "id": 171,
      "userId": 1,
      "startTime": "2026-05-05T09:52:51+00:00",
      "endTime": "2026-05-05T09:53:37+00:00",
      "duration": 20,
      "breakLength": 26,
      "state": { "status": "closed", "recommendedCloseTime": null },
      "isApproved": true,
      "tzOffset": 7200
    },
    {
      "id": 173,
      "userId": 1,
      "startTime": "2026-05-05T09:55:38+00:00",
      "endTime": "2026-05-05T09:55:39+00:00",
      "duration": 1,
      "breakLength": 0,
      "state": { "status": "closed", "recommendedCloseTime": null },
      "isApproved": true,
      "tzOffset": 7200
    },
    {
      "id": 175,
      "userId": 1,
      "startTime": "2026-05-05T10:07:02+00:00",
      "endTime": "2026-05-05T10:07:06+00:00",
      "duration": 2,
      "breakLength": 2,
      "state": { "status": "closed", "recommendedCloseTime": null },
      "isApproved": true,
      "tzOffset": 7200
    }
  ],
  "meta": { "hasMore": false, "total": 46, "from": "2020-01-01T00:00:00Z" }
}
```

## Error response example

400 — a required parameter is missing:

```json
{
  "success": false,
  "error": {
    "code": "MISSING_REQUIRED_PARAMS",
    "message": "userId is required. Bitrix24 refuses this method without it; get the id from GET /v1/users/me."
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|---------|
| 400 | `MISSING_REQUIRED_PARAMS` | `userId` is missing |
| 400 | `INVALID_PARAMS` | Parameter validation failed: an invalid `userId`, `limit` outside 1..50, a date without an explicit offset or one that does not exist in the calendar, `from` later than `to`, `offset` together with `page`, a parameter passed twice, an offset above 999,999,999, or an unknown query parameter |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header is missing |
| 401 | `INVALID_API_KEY` | The key is not recognized — no such key exists on the platform |
| 401 | `KEY_EXPIRED` | The API key has expired |
| 401 | `TOKEN_MISSING` | The key has no OAuth tokens configured for the portal |
| 402 | `ACCOUNT_FROZEN` | The portal balance is frozen |
| 403 | `SCOPE_DENIED` | The key does not satisfy the compound requirement: it needs `timeman` plus at least one of `user_brief`, `user_basic`, or `user` |
| 403 | `BITRIX_ACCESS_DENIED` | Bitrix24 denied access — including when the key is not allowed to read the specified employee's time sheet |
| 409 | `TIMEMAN_MODULE_NOT_ENABLED` | Bitrix24 did not accept the method. Call [`GET /v1/workday/status`](/docs/workday/status): the same refusal means the time tracking module is disabled on the portal; a `200` response means the portal does not return workday history while the current day state stays available |
| 422 | `BITRIX_ERROR` | Other Bitrix24 refusals — the text is in `message` |
| 429 | `RATE_LIMITED` | The rate limit of this endpoint is exceeded — 30 requests per minute per key. The current limit is returned in the `x-ratelimit-limit` header. For an application key the count is kept separately for each employee |
| 429 | `RATE_LIMITED` | The Bitrix24 portal request limit is exceeded |
| 502 | `BITRIX_UNAVAILABLE` | The Bitrix24 portal is unavailable, or the current IANA profile zone or the record offset derived from it cannot be determined reliably; an unknown former zone does not cause this error |

For the full list of common API errors, see [Errors](/docs/errors).

## Known specifics

- **The key does not restrict permissions to the caller's identity.** A personal key `vibe_api_…` accesses the Bitrix24 account through an inbound webhook and reads the time sheet with the rights of the Bitrix24 employee the webhook was issued under — that is, the rights of the key creator, not of whoever uses the key. The owner of such a key can enumerate `userId` and export the clock-in and clock-out times of the whole company. An application key `vibe_app_…` works differently — the request goes with the rights of the employee who opened the session. Bitrix24, not the platform, decides who may read another employee's time sheet: the Bitrix24 account administrator or the employee's direct manager.
- **The response names the period it applied.** `meta.from` and `meta.to` are present even when no period was passed: otherwise a `meta.total` of `0` cannot be told apart from "this employee has no records at all", though it only means "none in the last 7 days". For a period left open on one side, only the bound that is set comes back.
- **A parameter must not be passed twice.** `?userId=1&userId=2` returns `400` instead of silently picking one of the values.
- **`meta.total` arrives only on a short page.** On a full page the field is absent from the response entirely, so a handler that reads it unconditionally gets `undefined`. The value is correct at the time of the response: with `order=desc` a record added between two requests shifts the output window.
- **The offset in `startTime` and `endTime` is not the employee's time zone.** Both strings keep the account offset and are not rewritten. `tzOffset` is calculated for the absolute `startTime` instant using the historical rules of the current IANA time zone (`TIME_ZONE`) in the employee profile. To get local time in that zone, parse the absolute instant from `startTime` and apply `tzOffset`. If another IANA zone was assigned after record creation, the API does not know the former assignment: the returned value belongs to the current profile zone and is not proof of the zone assigned at that time.
- **Durations arrive in seconds.** `duration` and `breakLength` are integer numbers of seconds. The related [`GET /v1/workday/status`](/docs/workday/status) returns durations as a string of the form `HH:MM:SS`, so parsing does not carry over from one page to the other.
- **The selection is limited to the employee and the day start time.** The endpoint supports no other filters: you cannot filter by end time, duration or approval status, and sorting is by start time only. Filter the returned records locally to obtain the subset you need.
- **The whole history is traversed in pages of 50 records.** The endpoint does not return more than 50 records per request; deeper pages are reached with `offset` or `page`. The end of the output is signaled by `meta.hasMore` with the value `false`. A request with an offset beyond the end of the selection returns an empty array, and `meta.total` does not arrive in such a response.

## See also

- [Current status](/docs/workday/status)
- [Open a workday](/docs/workday/open)
- [Close a workday](/docs/workday/close)
- [Pause](/docs/workday/pause)
- [Tracking settings](/docs/workday/settings)
- [Work schedule](/docs/workday/schedule)
- [Workday](/docs/workday)

## Work and daily reports

[Work reports](/docs/workday/work-reports)

[Daily reports](/docs/workday/daily-reports)
