---
updatedAt: 2026-05-19T10:58:12.000Z
---

Fetch the complete documentation index at: https://developer.connecteam.com/llms.txt. Use this file to discover all available pages before exploring further.

# Shift IDs and Response Shapes

On a JS Vision-migrated company, the same underlying shift is identified and represented differently in V1 and V2. Understanding this is the single biggest unlock for getting V2 right.

> 📘 Why this matters
>
> The data is the same in both versions. What changes is how shifts are **identified** (slot ID vs base ID) and how groups are **shaped** in the response (multiple objects vs one). Get those two ideas right and the rest of V2 falls into place.

***

## Shift IDs: slot ID vs base ID

On a migrated company:

* **V1 returns and accepts slot IDs:**

  ```text
  507f1f77bcf86cd799439011:550e8400-e29b-41d4-a716-446655440000
  ```

  * The 24-char BSON ObjectId, then a colon `:`, then a UUID4 identifying the specific user's slot.
  * One ID per user assignment. A group shift with 3 users has 3 different slot IDs sharing the same prefix.

* **V2 returns and accepts base IDs:**

  ```text
  507f1f77bcf86cd799439011
  ```

  * The plain 24-char BSON ObjectId — the part **before** the colon.
  * One ID per base shift, regardless of how many users are on it.

You can convert a V1 slot ID to its V2 base ID by stripping everything from the first `:` onward:

```python
base_id = slot_id.split(":")[0] if ":" in slot_id else slot_id
```

> ⚠️ The reverse conversion is not possible directly
>
> A base ID maps to multiple slot IDs (one per user). To go from a base ID back to specific slot IDs, you have to list the group with `GET /v1/.../shifts/{shiftId}` (or fetch via list) and read the slot IDs from the response.

> ⚠️ V2 rejects slot IDs
>
> Sending a slot ID (anything with `:`) to a V2 endpoint — in the path, in `PUT` body, or in a `DELETE` body — returns `400 Bad Request` with `"shift id is invalid"`. Always strip the `:UUID` suffix before calling V2.

> 👍 V1 keeps accepting your pre-migration shift IDs
>
> If your integration stored 24-char BSON shift IDs **before** the company migrated to Vision, V1 will continue to accept those IDs unchanged. The server keeps a mapping (`legacyShiftId`) from the old ID to the new slot it now lives in, and looks it up automatically. You don't need to convert or re-fetch anything for existing references.
>
> The catch: bare BSON IDs created **after** migration — e.g. base shift IDs you pulled from a V2 response — are **not** valid V1 inputs. V1 returns `404 Not Found` if you pass one (because there's no `legacyShiftId` match). V1 strictly expects either a slot ID (`bson:uuid`) for post-migration shifts or a pre-migration legacy ID.

***

## ID validation rules at a glance

| Input                                           | V1 (non-Vision)    | V1 (Vision)                                                                              | V2 (Vision)          |
| ----------------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------- | -------------------- |
| Plain BSON ID (no `:`)                          | ✅ Accepted         | ✅ Accepted as a **legacy** ID; resolves to the pre-migration shift, or `404` if no match | ✅ Accepted (base ID) |
| Slot ID (`bson:uuid`)                           | ❌ Rejected (`400`) | ✅ Accepted (post-migration shift)                                                        | ❌ Rejected (`400`)   |
| Mixed types (legacy + slot) in one bulk request | n/a                | ❌ Rejected (`400`)                                                                       | n/a                  |

> 📘 V2 always requires Vision
>
> V2 on a non-Vision company returns `403` before ID validation even runs. The table above only applies to Vision-migrated accounts.

***

## What happens to your existing V1 shift IDs after migration?

This is the single most important reassurance for integrations that were built **before** JS Vision:

> 👍 Your stored shift IDs keep working
>
> Every 24-char BSON shift ID your system already stored maps to a specific slot in the new architecture, and V1 looks them up automatically. `GET /scheduler/v1/.../shifts/{oldId}`, `PUT`, `DELETE` — all continue to work on those IDs after migration. No code change needed for existing references.

But two things **do** change, and they're worth handling before migration day:

### 1. New shifts come back with longer IDs

Any shift **created** via V1 after migration is returned with a slot ID:

```text
6784dacb3c07733b0a849f49:7a4e8a18-9d4f-4a3d-b9e1-2b1f6a8c5e21
```

That's \~61 characters and contains a `:`. If your storage column is a fixed 24-char width (`CHAR(24)`, `VARCHAR(24)`, or a BSON-typed field), it will silently truncate or reject these IDs.

> ⚠️ Action item: widen your shift-ID storage to at least 64 characters of `VARCHAR` / `TEXT` before the company is migrated. This applies even if you stay on V1.

### 2. Don't mix legacy and slot IDs in one bulk request

Bulk endpoints like `GET /scheduler/v1/.../shifts?shiftIds=...` and `DELETE /scheduler/v1/.../shifts` accept a list of IDs. After migration, your stored list might contain both:

* Pre-migration BSON IDs (e.g. `507f1f77bcf86cd799439011`)
* Post-migration slot IDs (e.g. `6784dacb3c07733b0a849f49:7a4e8a18-...`)

V1 rejects mixed lists with `400 Bad Request` and `"must be all new format ids ... or all legacy ids"`. **Batch them by type** — send one request per ID format.

### Quick sanity check

```bash
ID="507f1f77bcf86cd799439011"  # an ID you stored before migration

curl -X GET "https://api.connecteam.com/scheduler/v1/schedulers/<schedulerId>/shifts/${ID}" \
  -H "X-API-KEY: <your-key>"
```

Expected on a migrated company:

* ✅ `200 OK` with the shift's current state — if `${ID}` was real and existed before migration.
* ❌ `404 Not Found` — if `${ID}` is a base ID of a shift created **after** migration, or simply doesn't exist.

***

## Response shape: side-by-side examples

The same underlying shift renders differently in V1 and V2. The examples below all assume a migrated company.

### Example A — Group shift with 2 assigned users

The shift was created with `assignedUserIds: [9170357, 9170358]` and `isOpenShift: false`.

**`GET /scheduler/v1/schedulers/{id}/shifts`** returns **two** objects:

```json
{
  "requestId": "...",
  "data": {
    "shifts": [
      {
        "id": "6784dacb3c07733b0a849f49:7a4e8a18-...",
        "title": "Morning Shift",
        "assignedUserIds": [9170357],
        "startTime": 1736924400,
        "endTime": 1736953200,
        "isOpenShift": false,
        "isPublished": true
      },
      {
        "id": "6784dacb3c07733b0a849f49:1b2f55de-...",
        "title": "Morning Shift",
        "assignedUserIds": [9170358],
        "startTime": 1736924400,
        "endTime": 1736953200,
        "isOpenShift": false,
        "isPublished": true
      }
    ]
  },
  "paging": { "offset": 2 }
}
```

Both objects share the same base prefix in their IDs (`6784dacb3c07733b0a849f49`), but each has its own slot UUID after the colon. **There is no field that explicitly links them as a group** in the V1 response — clients have to detect that by comparing the BSON prefixes.

**`GET /scheduler/v2/schedulers/{id}/shifts`** returns **one** object:

```json
{
  "requestId": "...",
  "data": {
    "shifts": [
      {
        "id": "6784dacb3c07733b0a849f49",
        "title": "Morning Shift",
        "assignedUserIds": [9170357, 9170358],
        "startTime": 1736924400,
        "endTime": 1736953200,
        "isOpenShift": false,
        "isPublished": true
      }
    ]
  },
  "paging": { "offset": 1 }
}
```

One object. The `id` is the BSON prefix from the V1 slot IDs. `assignedUserIds` lists everyone.

***

### Example B — Open shift with 5 spots, 2 claimed

Created with `isOpenShift: true, openSpots: 5`. Two users have claimed it; 3 spots remain.

**V1** returns **one shift per slot**: 2 assigned objects (each with the claiming user in `assignedUserIds`) plus 3 unassigned objects with `openSpots: 1` each — all five sharing the same BSON prefix in their IDs.

**V2** returns **one shift**:

```json
{
  "id": "6784dacb3c07733b0a849f49",
  "title": "Weekend Coverage",
  "isOpenShift": true,
  "openSpots": 3,
  "assignedUserIds": [9170357, 9170358],
  "startTime": 1736924400,
  "endTime": 1736953200,
  "isPublished": true
}
```

> 📘 What each field means here
>
> * `openSpots` is the **remaining** number of unclaimed slots.
> * `assignedUserIds` lists every user who has already claimed.
> * The shift's `id` stays the same as spots get claimed or released — see "Open shift ID stability" below.

***

### Example C — Regular single-user shift

For a single-user shift, V1 and V2 return the **same data**. The only difference is the `id` format: V1 returns a slot ID, V2 returns a base ID. There's no aggregation happening because there's only one slot.

***

## Aggregated fields in V2

V2's aggregation isn't limited to `assignedUserIds`. The following fields are also rolled up to the base shift:

| Field                                             | V1 (one row per slot)                                             | V2 (aggregated)                          |
| ------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------- |
| `assignedUserIds`                                 | At most one user                                                  | All assigned users in the group          |
| `openSpots`                                       | Each unassigned slot has `1` (or `0` if hidden)                   | Sum of remaining open spots across slots |
| `statuses`                                        | Only the slot's user's statuses                                   | Every slot's statuses concatenated       |
| `notes`, `breaks`, `customFields`, `shiftDetails` | Stored once on the base shift; returned identically on every slot | Returned once on the aggregated shift    |

***

## Open shift ID stability

This is a subtle but important upgrade in V2.

> 🚧 V1 (legacy / non-Vision) loses IDs when all spots fill
>
> When all spots on an open shift are claimed, the "root" open shift is soft-deleted. If spots become free again later, the root may be re-created with a **different** ID. Integrations that store shift IDs to track open shifts can silently lose references this way.

> 👍 V2 (Vision) keeps the same ID forever
>
> The base shift ID stays the same across the whole lifecycle. As spots are claimed, `openSpots` goes down. If all spots are claimed, the shift becomes "hidden" (still present in the database, returned only with admin-only flags, with `openSpots: 0`). If a spot becomes free again, the same ID reappears in regular listings. Webhooks and integrations that key off shift IDs stay stable.

***

[Next: Endpoint-by-Endpoint Reference →](https://developer.connecteam.com/docs/scheduler-shifts-vision-endpoints)