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:

    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:

    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:

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

InputV1 (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 requestn/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:

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

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:

{
  "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:

{
  "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:

{
  "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:

FieldV1 (one row per slot)V2 (aggregated)
assignedUserIdsAt most one userAll assigned users in the group
openSpotsEach unassigned slot has 1 (or 0 if hidden)Sum of remaining open spots across slots
statusesOnly the slot's user's statusesEvery slot's statuses concatenated
notes, breaks, customFields, shiftDetailsStored once on the base shift; returned identically on every slotReturned 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 →


What’s Next

Continue