{"openapi":"3.1.0","info":{"title":"Soon External API","version":"v1","description":"External REST API for customers. The v1 documentation covers boards, shifts, assignments, users, leave, and system metadata. Team-scoped business endpoints use API tokens. Rate limits: authenticated requests draw from token buckets per API token and per team. Reads: 600/min (burst 30) per token, 1200/min (burst 60) per team. Writes (POST/PATCH/PUT/DELETE): 120/min (burst 30) per token, 300/min (burst 60) per team. A refused request is not charged. Authenticated responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`; a 429 also carries `Retry-After`. Requests without credentials are limited per IP, and an IP that repeatedly fails authentication is refused for up to a minute."},"servers":[{"url":"https://api.soon.works/v1","description":"Soon API v1"}],"tags":[{"name":"System","description":"Health and API metadata endpoints"},{"name":"Boards","description":"Scheduling boards"},{"name":"Shifts","description":"Shift/event operations"},{"name":"Assignments","description":"Shift assignments"},{"name":"Users","description":"Team members"},{"name":"Leaves","description":"Leave (time off) of team members"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Token","description":"Use `Authorization: Bearer sk_live_...`"}},"schemas":{"Meta":{"type":"object","properties":{"requestId":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["requestId","timestamp"]},"ErrorPayload":{"type":"object","properties":{"code":{"type":"string","description":"Stable, machine-readable error code (UPPER_SNAKE_CASE). Branch on this, not on `message`. New codes may be added; handle an unknown code by its HTTP status. 400: VALIDATION_ERROR, CLIENT_UNAVAILABLE. 401: UNAUTHORIZED, TOKEN_EXPIRED, TOKEN_REVOKED. 403: FORBIDDEN, INSUFFICIENT_SCOPE, FEATURE_NOT_ENABLED. 404: <RESOURCE>_NOT_FOUND, for example SHIFT_NOT_FOUND, SHIFT_STAFFING_NOT_FOUND, BOARD_NOT_FOUND, ROLE_NOT_FOUND (`details.roleId`), USER_NOT_FOUND, ASSIGNMENT_NOT_FOUND, ENDPOINT_NOT_FOUND. 409: EXTERNAL_ID_CONFLICT (`details.existingShiftId`), SHIFT_NOT_PUBLISHABLE (`details.status`), SHIFT_STATE_CHANGED. 429: RATE_LIMITED (`details` names the bucket). 500: INTERNAL_ERROR. 503: RATE_LIMIT_UNAVAILABLE."},"message":{"type":"string","description":"Human-readable explanation. Its wording may change."},"status":{"type":"integer"},"details":{"type":"object","additionalProperties":{},"description":"Machine-readable context for some error codes, e.g. `status` on `SHIFT_NOT_PUBLISHABLE`."}},"required":["code","message","status"]},"ErrorResponse":{"type":"object","properties":{"error":{"$ref":"#/components/schemas/ErrorPayload"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["error","meta"]},"Pagination":{"type":"object","properties":{"cursor":{"type":["string","null"]},"hasMore":{"type":"boolean"}},"required":["cursor","hasMore"]},"PingData":{"type":"object","properties":{"ok":{"type":"boolean"},"teamId":{"type":"string"},"teamName":{"type":["string","null"],"description":"Display name of the team this API key belongs to. Null only when that team row no longer exists."},"scopes":{"type":"array","items":{"type":"string"}}},"required":["ok","teamId","teamName","scopes"]},"Board":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"description":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"teamId":{"type":"string"}},"required":["id","name","description","createdAt","updatedAt","teamId"]},"RoleSummary":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]}},"required":["id","name"]},"ShiftLocation":{"type":"object","properties":{"name":{"type":"string"},"sublocation":{"type":["string","null"]}},"required":["name"]},"ShiftStaffing":{"type":"object","properties":{"id":{"type":"string"},"maximumRequiredCount":{"type":["integer","null"]},"minimumRequiredCount":{"type":["integer","null"]},"type":{"type":["string","null"]},"role":{"allOf":[{"$ref":"#/components/schemas/RoleSummary"},{"type":["object","null"]}]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","maximumRequiredCount","minimumRequiredCount","type","role","createdAt","updatedAt"]},"ShiftSettings":{"type":"object","properties":{"selfSchedulingActivities":{"type":"boolean"},"selfSchedulingWfh":{"type":"boolean"},"calendarAvailability":{"type":"string","enum":["busy","free"]},"autoCloseOpenSeats":{"type":"object","properties":{"mode":{"type":"string","enum":["enabled","disabled","boardDefault"]},"days":{"type":"integer","minimum":1,"maximum":30},"trigger":{"type":"string","enum":["afterPublish","beforeStart"]}},"required":["mode","days","trigger"]},"defaultWorkFromHome":{"type":"string","enum":["enabled","disabled","boardDefault"]}}},"Shift":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":["string","null"]},"description":{"type":["string","null"]},"startTime":{"type":"string","format":"date-time"},"endTime":{"type":"string","format":"date-time"},"breakDurationMinutes":{"type":["integer","null"]},"timezone":{"type":["string","null"]},"status":{"type":"string","enum":["PUBLISHED","UNPUBLISHED","DELETED","CLOSED"]},"color":{"type":["string","null"]},"location":{"allOf":[{"$ref":"#/components/schemas/ShiftLocation"},{"type":["object","null"]}]},"boardId":{"type":"string"},"clientId":{"type":["string","null"],"description":"The client this shift is worked for (Clients, available to selected teams only). Null when the shift has no client, the team does not have Clients, or the client was deleted."},"externalId":{"type":["string","null"],"description":"Your own identifier for the shift: 1 to 512 characters, not blank, stored and matched exactly as sent (case, accents and spaces count). Unique among the team's shifts that are not deleted; a deleted shift keeps showing its externalId, but the value can be reused by another shift. Null when no externalId was set, for example for shifts created in the Soon app."},"settings":{"$ref":"#/components/schemas/ShiftSettings"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","title","description","startTime","endTime","breakDurationMinutes","timezone","status","color","location","boardId","clientId","externalId","settings","createdAt","updatedAt"]},"User":{"type":"object","properties":{"id":{"type":"string"},"firstName":{"type":["string","null"]},"lastName":{"type":["string","null"]},"name":{"type":["string","null"]},"email":{"type":["string","null"],"format":"email"},"avatar":{"type":["string","null"]},"timezone":{"type":["string","null"]},"language":{"type":["string","null"]},"isVerified":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"lastSeen":{"type":["string","null"],"format":"date-time"},"teamRole":{"type":["string","null"]}},"required":["id","firstName","lastName","name","email","avatar","timezone","language","isVerified","createdAt","updatedAt","lastSeen","teamRole"]},"BoardAccess":{"type":"object","properties":{"boardId":{"type":"string"},"role":{"type":"string","enum":["OWNER","ADMIN","MEMBER","READ_ONLY"]},"readOnly":{"type":"boolean"},"visible":{"type":"boolean"}},"required":["boardId","role","readOnly","visible"]},"Assignment":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["PENDING","ASSIGNED","ACCEPTED","CANCELLED","COVERED","SWAPPED"]},"userId":{"type":["string","null"]},"staffingId":{"type":["string","null"]},"shiftId":{"type":["string","null"]},"roleName":{"type":["string","null"]},"actualStartTime":{"type":["string","null"],"format":"date-time"},"actualEndTime":{"type":["string","null"],"format":"date-time"},"actualBreakDurationMinutes":{"type":["integer","null"]},"shiftStartTime":{"type":["string","null"],"format":"date-time"},"shiftEndTime":{"type":["string","null"],"format":"date-time"},"shiftBreakDurationMinutes":{"type":["integer","null"]},"timezone":{"type":["string","null"]},"locationType":{"type":["string","null"]},"selfScheduled":{"type":["boolean","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","status","userId","staffingId","shiftId","roleName","actualStartTime","actualEndTime","actualBreakDurationMinutes","shiftStartTime","shiftEndTime","shiftBreakDurationMinutes","timezone","locationType","selfScheduled","createdAt","updatedAt"]},"ShiftStaffingWithAssignments":{"allOf":[{"$ref":"#/components/schemas/ShiftStaffing"},{"type":"object","properties":{"assignments":{"type":"array","items":{"$ref":"#/components/schemas/Assignment"}}}}]},"ShiftWithStaffing":{"allOf":[{"$ref":"#/components/schemas/Shift"},{"type":"object","properties":{"staffing":{"type":"array","items":{"$ref":"#/components/schemas/ShiftStaffingWithAssignments"}}}}]},"LeaveCategory":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"icon":{"type":["string","null"]}},"required":["id","name","icon"]},"Leave":{"type":"object","properties":{"id":{"type":"string"},"startTime":{"type":"string","format":"date-time"},"endTime":{"type":"string","format":"date-time"},"timezone":{"type":["string","null"]},"status":{"type":"string","enum":["APPROVED","REJECTED","CANCELLATION_REQUESTED","PENDING","CANCELLED"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"userId":{"type":"string"},"teamId":{"type":"string"},"categoryId":{"type":["string","null"]},"category":{"allOf":[{"$ref":"#/components/schemas/LeaveCategory"},{"type":["object","null"]}]}},"required":["id","startTime","endTime","timezone","status","createdAt","updatedAt","userId","teamId","categoryId","category"]},"OverlappingShift":{"type":"object","properties":{"shiftId":{"type":"string"},"assignmentId":{"type":"string"},"boardId":{"type":["string","null"]},"title":{"type":"string"},"shiftStatus":{"type":"string","enum":["PUBLISHED","UNPUBLISHED"]},"assignmentStatus":{"type":["string","null"]},"startTime":{"type":"string","format":"date-time"},"endTime":{"type":"string","format":"date-time"},"outcome":{"type":"string","enum":["kept","removed","cancelled"],"description":"Only on an approval's response: what happened to this assignment."}},"required":["shiftId","assignmentId","boardId","title","shiftStatus","assignmentStatus","startTime","endTime"]},"BulkEditShiftsAccepted":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"type":"string","description":"pending | running | completed | failed"},"acceptedAt":{"type":"string"},"replayed":{"type":"boolean","description":"True when an idempotency key was replayed"},"progress":{"type":"object","properties":{"total":{"type":"number"},"processed":{"type":"number"}},"required":["total","processed"]},"result":{"type":["object","null"],"additionalProperties":{},"description":"Per-shift outcomes and counts. Individual shifts may be skipped or fail."},"dryRun":{"type":"boolean","enum":[false]}},"required":["jobId","status","acceptedAt","replayed","progress","result","dryRun"]},"BulkEditShiftsDryRun":{"type":"object","properties":{"dryRun":{"type":"boolean","enum":[true]},"plan":{"type":"object","additionalProperties":{},"description":"Per-shift outcomes, diffs and warnings"},"notFoundShiftIds":{"type":"array","items":{"type":"string"}},"staleShiftIds":{"type":"array","items":{"type":"string"}},"issues":{"type":"array","items":{"type":"object","additionalProperties":{}}},"previewRevision":{"type":"string","description":"Pass this back as previewRevision on the apply to tie it to this preview."}},"required":["dryRun","plan","notFoundShiftIds","staleShiftIds","issues","previewRevision"]},"BulkEditShiftsRequest":{"type":"object","properties":{"boardId":{"type":"string","description":"Board every selected shift belongs to"},"shiftIds":{"type":"array","items":{"type":"string"},"description":"Shifts to edit; ids outside this team resolve to notFound"},"operations":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Ordered operations. Each has a kind, an operationId and its own fields. Settings use this API's public spelling (boardDefault, afterPublish)."},"expectedVersions":{"type":"object","additionalProperties":{"type":"string"},"description":"Per shift, the version token the plan was built against. Required on a dry run as well as an apply."},"idempotencyKey":{"type":"string","description":"Replaying a key returns the original outcome and writes nothing. Required on a dry run as well as an apply; a dry run never consumes it."},"dryRun":{"type":"boolean","description":"True plans and writes nothing; omitted or false applies"},"previewRevision":{"type":"string","description":"The revision a dry run returned. When present, the whole request is refused with 409 if the board changed since that preview, so an apply can only commit the outcome that was actually reviewed. Omit it to keep per-shift staleness handling alone."}},"required":["boardId","shiftIds","operations","expectedVersions","idempotencyKey"]}},"parameters":{}},"paths":{"/ping":{"get":{"tags":["System"],"summary":"Ping authenticated API","operationId":"ping_api","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Ping response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PingData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/boards":{"get":{"tags":["Boards"],"summary":"List boards","operationId":"list_boards","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Boards list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Board"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","pagination","meta"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/boards/{boardId}":{"get":{"tags":["Boards"],"summary":"Get board by id","operationId":"get_board","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"boardId","in":"path"}],"responses":{"200":{"description":"Board","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Board"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Board not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/shifts":{"get":{"tags":["Shifts"],"summary":"List shifts","description":"Lists the team's shifts, newest first. Deleted shifts are left out by default; pass `includeDeleted=true` to include them or `status=DELETED` for deleted shifts only. `GET /shifts/{shiftId}` still returns a deleted shift.","operationId":"list_shifts","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"string","format":"date-time"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string","format":"date-time"},"required":false,"name":"to","in":"query"},{"schema":{"type":"string"},"required":false,"name":"boardId","in":"query"},{"schema":{"type":"string","enum":["PUBLISHED","UNPUBLISHED","DELETED","CLOSED"],"description":"Return only shifts with this status. Overrides `includeDeleted`: `status=DELETED` returns only deleted shifts."},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":512,"description":"Return only shifts with exactly this externalId. Deleted shifts are left out unless you pass `includeDeleted=true`. To tell whether a planner deleted a shift you created, look it up with `includeDeleted=true`: a deleted shift keeps showing its externalId, and the value can be reused by a live shift."},"required":false,"name":"externalId","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"`true` also returns deleted shifts (status `DELETED`). Default `false`. Only published or closed shifts are kept as deleted; a deleted unpublished draft is removed and never listed. Ignored when `status` is set."},"required":false,"name":"includeDeleted","in":"query"},{"schema":{"type":"array","items":{"type":"string","enum":["staffing","staffing.assignments"]},"description":"Optional relationship expansions. Use `staffing` or `staffing.assignments`; `staffing.assignments` implies `staffing`."},"required":false,"name":"include","in":"query"}],"responses":{"200":{"description":"Shifts list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ShiftWithStaffing"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","pagination","meta"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["Shifts"],"summary":"Create shift","operationId":"create_shift","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"boardId":{"type":"string"},"title":{"type":["string","null"],"maxLength":255,"description":"Optional. Omitted, null, or blank titles default to \"Untitled\"."},"startTime":{"type":"string","format":"date-time"},"endTime":{"type":"string","format":"date-time"},"timezone":{"type":"string","maxLength":100,"description":"IANA timezone. Defaults to the team's timezone, falling back to UTC."},"description":{"type":"string","maxLength":5000},"location":{"allOf":[{"$ref":"#/components/schemas/ShiftLocation"},{"type":["object","null"]}]},"color":{"type":"string","maxLength":32},"breakDurationMinutes":{"type":"integer","minimum":0},"settings":{"allOf":[{"$ref":"#/components/schemas/ShiftSettings"},{"description":"Optional event-level settings. Omitted fields do not change anything; unsupported or invalid values are ignored."}]},"staffing":{"type":"array","items":{"type":"object","properties":{"roleId":{"type":"string"},"type":{"type":"string","enum":["OPEN"]},"maximumRequiredCount":{"type":"integer","minimum":0,"default":0},"minimumRequiredCount":{"type":"integer","minimum":0}},"additionalProperties":false},"maxItems":25},"clientId":{"type":["string","null"],"maxLength":64,"description":"Optional client this shift is worked for (Clients, available to selected teams only). The client must belong to the team and not be archived or deleted (400 CLIENT_UNAVAILABLE); teams without Clients get 403 FEATURE_NOT_ENABLED."},"externalId":{"type":["string","null"],"minLength":1,"maxLength":512,"description":"Your own identifier for the shift: 1 to 512 characters, not blank, stored and matched exactly as sent (case, accents and spaces count). Unique among the team's shifts that are not deleted; a deleted shift keeps showing its externalId, but the value can be reused by another shift. Optional. A value already used by another live shift is refused with 409 EXTERNAL_ID_CONFLICT and `error.details.existingShiftId`, and no shift is created."}},"required":["boardId","startTime","endTime"],"additionalProperties":false}}}},"responses":{"201":{"description":"Shift created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ShiftWithStaffing"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"EXTERNAL_ID_CONFLICT: another shift in this team already uses this externalId (`error.details.existingShiftId`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/shifts/{shiftId}":{"get":{"tags":["Shifts"],"summary":"Get shift by id","operationId":"get_shift","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"shiftId","in":"path"},{"schema":{"type":"array","items":{"type":"string","enum":["staffing","staffing.assignments"]},"description":"Optional relationship expansions. Use `staffing` or `staffing.assignments`; `staffing.assignments` implies `staffing`."},"required":false,"name":"include","in":"query"}],"responses":{"200":{"description":"Shift","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ShiftWithStaffing"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Shift not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Shifts"],"summary":"Update shift","description":"Partial update: omitted fields stay unchanged. Behaves like editing the shift in the Soon app: each changed field is recorded in the shift's history, and when a published shift's title, description, times, break, timezone, date or location change, the people assigned to it are notified and their synced calendars are updated. Send only fields that actually changed.","operationId":"update_shift","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"shiftId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":["string","null"],"maxLength":255,"description":"Optional. Omitted, null, or blank titles default to \"Untitled\"."},"startTime":{"type":"string","format":"date-time","description":"The shift's calendar day follows from startTime in the shift's timezone. Send startTime and endTime together when the times change."},"endTime":{"type":"string","format":"date-time"},"timezone":{"type":"string","maxLength":100},"description":{"type":"string","maxLength":5000},"location":{"allOf":[{"$ref":"#/components/schemas/ShiftLocation"},{"type":["object","null"]}]},"color":{"type":"string","maxLength":32},"breakDurationMinutes":{"type":"integer","minimum":0},"settings":{"allOf":[{"$ref":"#/components/schemas/ShiftSettings"},{"description":"Optional event-level settings. Omitted fields do not change anything; unsupported or invalid values are ignored."}]},"clientId":{"type":["string","null"],"maxLength":64,"description":"Change the shift's client (Clients, available to selected teams only). Omit to keep it, null to clear it. The client must belong to the team and not be archived or deleted (400 CLIENT_UNAVAILABLE); teams without Clients get 403 FEATURE_NOT_ENABLED."},"externalId":{"type":["string","null"],"minLength":1,"maxLength":512,"description":"Your own identifier for the shift: 1 to 512 characters, not blank, stored and matched exactly as sent (case, accents and spaces count). Unique among the team's shifts that are not deleted; a deleted shift keeps showing its externalId, but the value can be reused by another shift. Omit to keep it, send a value to set or replace it, null to remove it. A value already used by another live shift is refused with 409 EXTERNAL_ID_CONFLICT and `error.details.existingShiftId`."}},"additionalProperties":false}}}},"responses":{"200":{"description":"Shift updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Shift"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Shift not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"EXTERNAL_ID_CONFLICT: another shift in this team already uses this externalId (`error.details.existingShiftId`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["Shifts"],"summary":"Delete shift","description":"Uses the same deletion semantics as the planner app: unpublished drafts may be removed, while published and closed shifts are marked deleted because they have already been shared. Published-shift cancellation side effects are applied. Idempotent for published and closed shifts: deleting a shift that is already deleted returns 200 with the shift and repeats no side effects.","operationId":"delete_shift","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"shiftId","in":"path"}],"responses":{"200":{"description":"Shift deleted (`meta.outcome: deleted`), or it was already deleted (`meta.outcome: already_deleted`, no side effects repeated).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Shift"},"meta":{"allOf":[{"$ref":"#/components/schemas/Meta"},{"type":"object","properties":{"outcome":{"type":"string","enum":["deleted","already_deleted"]}},"required":["outcome"]}]}},"required":["data","meta"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Shift not found. A deleted unpublished draft is removed, so a retried delete of a draft also returns 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"SHIFT_STATE_CHANGED: the shift changed while it was being deleted; read it again and retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/shifts/{shiftId}/staffing":{"get":{"tags":["Shifts"],"summary":"List staffing for shift","operationId":"list_shift_staffing","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"shiftId","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Staffing list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ShiftStaffing"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","pagination","meta"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Shift not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["Shifts"],"summary":"Create staffing for shift","operationId":"create_shift_staffing","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"shiftId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"roleId":{"type":["string","null"],"description":"Optional active team role id to connect. Omit or pass null for staffing without a role."},"type":{"type":["string","null"],"enum":["OPEN"],"description":"Pass OPEN to make the staffing row self-schedulable."},"maximumRequiredCount":{"type":"integer","minimum":0,"default":0},"minimumRequiredCount":{"type":["integer","null"],"minimum":0}},"additionalProperties":false,"description":"Creates one staffing row on an existing shift. Assignments are managed separately through assignment endpoints."}}}},"responses":{"201":{"description":"Staffing created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ShiftStaffing"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/shifts/{shiftId}/staffing/{staffingId}":{"get":{"tags":["Shifts"],"summary":"Get shift staffing by id","operationId":"get_shift_staffing","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"shiftId","in":"path"},{"schema":{"type":"string"},"required":true,"name":"staffingId","in":"path"}],"responses":{"200":{"description":"Staffing","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ShiftStaffing"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Shift staffing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Shifts"],"summary":"Update shift staffing","operationId":"update_shift_staffing","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"shiftId","in":"path"},{"schema":{"type":"string"},"required":true,"name":"staffingId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"roleId":{"type":["string","null"],"description":"Active team role id to connect, or null to disconnect the current role."},"type":{"type":["string","null"],"enum":["OPEN"]},"maximumRequiredCount":{"type":"integer","minimum":0},"minimumRequiredCount":{"type":["integer","null"],"minimum":0}},"additionalProperties":false,"description":"Updates one staffing row. At least one field must be provided."}}}},"responses":{"200":{"description":"Staffing updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ShiftStaffing"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["Shifts"],"summary":"Delete shift staffing","description":"Uses planner staffing deletion semantics. Unpublished shift staffing is removed with its draft requests; published and closed shift staffing is hidden and active assignments are cancelled or deleted according to their status.","operationId":"delete_shift_staffing","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"shiftId","in":"path"},{"schema":{"type":"string"},"required":true,"name":"staffingId","in":"path"}],"responses":{"200":{"description":"Staffing deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ShiftStaffing"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Shift staffing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/shifts/bulk/jobs/{jobId}":{"get":{"tags":["Shifts"],"summary":"Get a bulk edit job","description":"A bulk edit job's state and, once it has ended (completed or failed), its result with per-shift outcomes. Reads one row: poll this after `Prefer: respond-async` or a lost response rather than replaying the apply, which repeats the preview and is charged to the team's budget. Only jobs this key submitted are visible.","operationId":"get_bulk_edit_job","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"The jobId a bulk edit apply returned"},"required":true,"name":"jobId","in":"path"}],"responses":{"200":{"description":"The job","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/BulkEditShiftsAccepted"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Bulk edit job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/shifts/bulk":{"post":{"tags":["Shifts"],"summary":"Bulk edit shifts","description":"Apply one or more operations to many shifts in one request. Best effort by design: each shift — or each pair of shifts linked by a pending swap or cover — is committed in its own transaction, so a stale, deleted, locked or failing shift becomes its own outcome row while every other shift still commits — read the per-shift outcomes rather than the status code. Pass dryRun to plan without writing. Reuse an idempotencyKey to replay a submission safely. People operations need the assignments:write scope as well as shifts:write.","operationId":"bulk_edit_shifts","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Send `respond-async` to be answered as soon as the job is accepted (202, status running, result null, a Location header and `Preference-Applied: respond-async`) instead of when it ends. Follow the job on GET /shifts/bulk/jobs/{jobId}. Without it the response waits for the job, which a large apply can take longer than an HTTP timeout to finish.","example":"respond-async"},"required":false,"name":"prefer","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkEditShiftsRequest"}}}},"responses":{"200":{"description":"Dry run plan; nothing was written","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/BulkEditShiftsDryRun"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"202":{"description":"Accepted and executed. Per-shift outcomes are in the result; some shifts may be skipped or failed. With `Prefer: respond-async`: accepted and still running, result null — follow it on GET /shifts/bulk/jobs/{jobId}.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/BulkEditShiftsAccepted"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"description":"Validation error (BULK_EDIT_VALIDATION), or a request that costs more than the team's whole hourly budget (BULK_EDIT_TOO_LARGE) — split it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Bulk edit is not enabled for this team (BULK_EDIT_DISABLED), or a people operation without the assignments:write scope (INSUFFICIENT_SCOPE)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Board not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Idempotency key reused with a different body, or the preview this apply names has been superseded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"The team has used its hourly bulk edit budget (BULK_EDIT_BUDGET_EXCEEDED), or too many bulk edits are running at once for this team or on this server (BULK_EDIT_BUSY). Nothing was accepted; Retry-After says when to try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/shifts/{shiftId}/publish":{"post":{"tags":["Shifts"],"summary":"Publish shift","description":"Publishes an unpublished shift and notifies its assigned people. Idempotent: publishing a shift that is already published returns 200 with the shift and changes nothing.","operationId":"publish_shift","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"shiftId","in":"path"}],"responses":{"200":{"description":"Shift published (`meta.outcome: published`), or it was already published (`meta.outcome: already_published`, no notifications or calendar sync repeated).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Shift"},"meta":{"allOf":[{"$ref":"#/components/schemas/Meta"},{"type":"object","properties":{"outcome":{"type":"string","enum":["published","already_published"]}},"required":["outcome"]}]}},"required":["data","meta"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Shift not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"SHIFT_NOT_PUBLISHABLE: the shift is closed or deleted (`error.details.status`); SHIFT_STATE_CHANGED: the shift changed while it was being published","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/users":{"get":{"tags":["Users"],"summary":"List users","operationId":"list_users","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"array","items":{"type":"string"}},"required":false,"name":"boardId","in":"query"},{"schema":{"type":"string","format":"email","description":"Exact email address of one user on this team. Leading and trailing spaces are ignored and the match is case-insensitive. Combined with boardId. An address that belongs only to another team returns an empty list."},"required":false,"name":"email","in":"query"}],"responses":{"200":{"description":"Users list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/User"},{"type":"object","properties":{"boardAccess":{"type":"array","items":{"$ref":"#/components/schemas/BoardAccess"}}}}]}},"pagination":{"$ref":"#/components/schemas/Pagination"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","pagination","meta"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/users/{userId}":{"get":{"tags":["Users"],"summary":"Get user by id","operationId":"get_user","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"userId","in":"path"}],"responses":{"200":{"description":"User","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/User"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/assignments":{"get":{"tags":["Assignments"],"summary":"List assignments","operationId":"list_assignments","security":[{"bearerAuth":[]}],"description":"Team assignment-management route. Returns assignments on non-deleted shifts, including unpublished draft shifts.","parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"string","format":"date-time"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string","format":"date-time"},"required":false,"name":"to","in":"query"},{"schema":{"type":"string"},"required":false,"name":"shiftId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"userId","in":"query"},{"schema":{"type":"string","enum":["PENDING","ASSIGNED","ACCEPTED","CANCELLED","COVERED","SWAPPED"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Assignments list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Assignment"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","pagination","meta"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["Assignments"],"summary":"Create assignment","operationId":"create_assignment","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"staffingId":{"type":"string"},"status":{"type":"string","enum":["PENDING","ASSIGNED"],"default":"PENDING"}},"required":["userId","staffingId"],"additionalProperties":false}}}},"responses":{"201":{"description":"Assignment created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Assignment"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/assignments/{assignmentId}":{"get":{"tags":["Assignments"],"summary":"Get assignment by id","operationId":"get_assignment","security":[{"bearerAuth":[]}],"description":"Team assignment-management route. Can return assignments on unpublished draft shifts.","parameters":[{"schema":{"type":"string"},"required":true,"name":"assignmentId","in":"path"}],"responses":{"200":{"description":"Assignment","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Assignment"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Assignment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Assignments"],"summary":"Update assignment status","operationId":"update_assignment","security":[{"bearerAuth":[]}],"description":"Team assignment-management route. Can update assignments on unpublished draft shifts.","parameters":[{"schema":{"type":"string"},"required":true,"name":"assignmentId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ACCEPTED","CANCELLED","ASSIGNED"]}},"required":["status"],"additionalProperties":false}}}},"responses":{"200":{"description":"Assignment updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Assignment"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Assignment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/leaves":{"get":{"tags":["Leaves"],"summary":"List leaves","operationId":"list_leaves","security":[{"bearerAuth":[]}],"description":"Every leave in the team, newest start first. `from`/`to` return each leave that overlaps the range, including one that starts before `from`. Times are ISO 8601; a leave's own `timezone` is the zone its days are read in.","parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"string","format":"date-time"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string","format":"date-time"},"required":false,"name":"to","in":"query"},{"schema":{"type":"string"},"required":false,"name":"userId","in":"query"},{"schema":{"type":"string","enum":["APPROVED","REJECTED","CANCELLATION_REQUESTED","PENDING","CANCELLED"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Leaves list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Leave"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","pagination","meta"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["Leaves"],"summary":"Create leave","operationId":"create_leave","security":[{"bearerAuth":[]}],"description":"Creates a leave request (PENDING) and notifies the person's leave managers, or the team admins when the team has none.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"startTime":{"type":"string","format":"date-time"},"endTime":{"type":"string","format":"date-time"},"timezone":{"type":"string","maxLength":100},"categoryId":{"type":"string"},"status":{"type":"string","enum":["PENDING"],"default":"PENDING","description":"Leave is always created as PENDING. Approve it with PATCH /leaves/{leaveId}, which asks what happens to overlapping shifts."}},"required":["userId","startTime","endTime"],"additionalProperties":false}}}},"responses":{"201":{"description":"Leave created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Leave"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/leaves/{leaveId}":{"get":{"tags":["Leaves"],"summary":"Get leave by id","operationId":"get_leave","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"leaveId","in":"path"}],"responses":{"200":{"description":"Leave","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Leave"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Leave not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Leaves"],"summary":"Update leave status","operationId":"update_leave","security":[{"bearerAuth":[]}],"description":"Moves the leave to a new status and notifies the person. Approving a PENDING leave needs `overlappingShifts`; check `GET /leaves/{leaveId}/overlapping-shifts` first. The decision and any shift removals commit together. The status changes only from the status it was in when read, so a concurrent decision returns `LEAVE_CHANGED`.","parameters":[{"schema":{"type":"string"},"required":true,"name":"leaveId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["APPROVED","REJECTED","CANCELLATION_REQUESTED","CANCELLED"]},"overlappingShifts":{"type":"string","enum":["cancel","keep"],"description":"Required when approving a PENDING leave, refused for any other change. \"cancel\" takes the person off every shift the leave overlaps (cancelled on published shifts, removed from draft ones), in the same transaction as the approval. \"keep\" leaves the schedule as it is."}},"required":["status"],"additionalProperties":false}}}},"responses":{"200":{"description":"Leave updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"allOf":[{"$ref":"#/components/schemas/Leave"},{"type":"object","properties":{"overlappingShifts":{"type":"object","properties":{"choice":{"type":"string","enum":["cancel","keep"]},"truncated":{"type":"boolean"},"shifts":{"type":"array","items":{"$ref":"#/components/schemas/OverlappingShift"}}},"required":["choice","truncated","shifts"],"description":"Present when a PENDING leave was approved."}}}]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"description":"Validation error: `VALIDATION_ERROR` (including a transition that is not allowed), `LEAVE_OVERLAPPING_SHIFTS_REQUIRED`, `LEAVE_OVERLAPPING_SHIFTS_NOT_APPLICABLE` or `LEAVE_TOO_MANY_OVERLAPPING_SHIFTS`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Leave not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"`LEAVE_CHANGED` or `LEAVE_SHIFTS_CHANGED`: something changed during the decision. Read the leave again and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/leaves/{leaveId}/overlapping-shifts":{"get":{"tags":["Leaves"],"summary":"List shifts a leave overlaps","operationId":"list_leave_overlapping_shifts","security":[{"bearerAuth":[]}],"description":"The person's assignments (pending, assigned, accepted or cancellation requested) on published and draft shifts that overlap the leave. A person's own times on an assignment win over the shift's. Requires `leaves:read` and `assignments:read`.","parameters":[{"schema":{"type":"string"},"required":true,"name":"leaveId","in":"path"}],"responses":{"200":{"description":"Overlapping shifts","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"leaveId":{"type":"string"},"truncated":{"type":"boolean","description":"True when more than 200 shifts overlap; the list stops there."},"shifts":{"type":"array","items":{"$ref":"#/components/schemas/OverlappingShift"}}},"required":["leaveId","truncated","shifts"]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Leave not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (`RATE_LIMITED`). Wait the number of seconds in `Retry-After`, then retry. `error.details` names the bucket that refused the request.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"Size (burst) of the bucket closest to empty.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in that bucket right now.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that bucket is full again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"webhooks":{}}