# Unified API V2 Frontend should use this service as the single base URL: - `https://krow-api-v2-e3g6witsvq-uc.a.run.app` The gateway keeps backend services separate internally, but frontend should treat it as one API. ## 1) Auth routes Full auth behavior, including staff phone flow and refresh rules, is documented in [Authentication](./authentication.md). ### Client auth - `POST /auth/client/sign-in` - `POST /auth/client/sign-up` - `POST /auth/client/sign-out` ### Staff auth - `POST /auth/staff/phone/start` - `POST /auth/staff/phone/verify` - `POST /auth/staff/sign-out` ### Shared auth - `GET /auth/session` - `POST /auth/sign-out` ## 2) Client routes ### Client reads - `GET /client/session` - `GET /client/dashboard` - `GET /client/reorders` - `GET /client/billing/accounts` - `GET /client/billing/invoices/pending` - `GET /client/billing/invoices/history` - `GET /client/billing/current-bill` - `GET /client/billing/savings` - `GET /client/billing/spend-breakdown` - `GET /client/coverage` - `GET /client/coverage/stats` - `GET /client/coverage/core-team` - `GET /client/coverage/incidents` - `GET /client/coverage/blocked-staff` - `GET /client/coverage/swap-requests` - `GET /client/coverage/dispatch-teams` - `GET /client/coverage/dispatch-candidates` - `GET /client/hubs` - `GET /client/cost-centers` - `GET /client/vendors` - `GET /client/vendors/:vendorId/roles` - `GET /client/hubs/:hubId/managers` - `GET /client/team-members` - `GET /client/shifts/scheduled` - `GET /client/orders/view` deprecated compatibility alias - `GET /client/orders/:orderId/reorder-preview` - `GET /client/reports/summary` - `GET /client/reports/daily-ops` - `GET /client/reports/spend` - `GET /client/reports/coverage` - `GET /client/reports/forecast` - `GET /client/reports/performance` - `GET /client/reports/no-show` ### Client writes - `POST /client/devices/push-tokens` - `DELETE /client/devices/push-tokens` - `POST /client/orders/one-time` - `POST /client/orders/recurring` - `POST /client/orders/permanent` - `POST /client/orders/:orderId/edit` - `POST /client/orders/:orderId/cancel` - `POST /client/shift-managers` - `POST /client/hubs` - `PUT /client/hubs/:hubId` - `DELETE /client/hubs/:hubId` - `POST /client/hubs/:hubId/assign-nfc` - `POST /client/hubs/:hubId/managers` - `POST /client/billing/invoices/:invoiceId/approve` - `POST /client/billing/invoices/:invoiceId/dispute` - `POST /client/coverage/reviews` - `POST /client/coverage/late-workers/:assignmentId/cancel` - `POST /client/coverage/swap-requests/:swapRequestId/resolve` - `POST /client/coverage/swap-requests/:swapRequestId/cancel` - `POST /client/coverage/dispatch-teams/memberships` - `DELETE /client/coverage/dispatch-teams/memberships/:membershipId` Timeline route naming: - `GET /client/shifts/scheduled` is the canonical client timeline route - it returns shift-level scheduled items, not order headers - `GET /client/orders/view` still returns the same payload for compatibility, but now emits a deprecation header Coverage-review request payload may also send: ```json { "staffId": "uuid", "assignmentId": "uuid", "rating": 2, "feedback": "Worker left the shift early without approval", "markAsFavorite": false, "issueFlags": ["LEFT_EARLY"], "markAsBlocked": true } ``` If `markAsFavorite` is `true`, backend adds that worker to the business favorites list. If `markAsFavorite` is `false`, backend removes them from that list. If `markAsBlocked` is `true`, backend adds that staff member to the business-level blocked list and future apply or assign attempts are rejected until a later review sends `markAsBlocked: false`. `GET /client/coverage` response notes: - each shift item includes `locationName` and `locationAddress` - each assigned worker item includes `hasReview` Swap-review routes: - `GET /client/coverage/swap-requests?status=OPEN` - `POST /client/coverage/swap-requests/:swapRequestId/resolve` - `POST /client/coverage/swap-requests/:swapRequestId/cancel` Resolve example: ```json { "applicationId": "uuid", "note": "Dispatch selected the strongest replacement candidate" } ``` Dispatch-team routes: - `GET /client/coverage/dispatch-teams` - `GET /client/coverage/dispatch-candidates?shiftId=uuid&roleId=uuid` - `POST /client/coverage/dispatch-teams/memberships` - `DELETE /client/coverage/dispatch-teams/memberships/:membershipId` Dispatch-team membership example: ```json { "staffId": "uuid", "hubId": "uuid", "teamType": "CORE", "notes": "Preferred lead barista for this location" } ``` Dispatch priority order is: 1. `CORE` 2. `CERTIFIED_LOCATION` 3. `MARKETPLACE` Shift-manager creation example: ```json { "firstName": "Nora", "lastName": "Lead", "email": "nora.lead@example.com", "phone": "+15550001234", "hubId": "uuid" } ``` The manager is created as an invited business membership. If `hubId` is present, backend also links the manager to that hub. ## 3) Staff routes ### Staff reads - `GET /staff/session` - `GET /staff/dashboard` - `GET /staff/profile/stats` - `GET /staff/profile-completion` - `GET /staff/availability` - `GET /staff/clock-in/shifts/today` - `GET /staff/clock-in/status` - `GET /staff/payments/summary` - `GET /staff/payments/history` - `GET /staff/payments/chart` - `GET /staff/orders/available` - `GET /staff/shifts/assigned` - `GET /staff/shifts/open` - `GET /staff/shifts/pending` - `GET /staff/shifts/cancelled` - `GET /staff/shifts/completed` - `GET /staff/shifts/:shiftId` - `GET /staff/profile/sections` - `GET /staff/profile/personal-info` - `GET /staff/profile/industries` - `GET /staff/profile/skills` - `GET /staff/profile/documents` - `GET /staff/profile/attire` - `GET /staff/profile/tax-forms` - `GET /staff/profile/emergency-contacts` - `GET /staff/profile/certificates` - `GET /staff/profile/bank-accounts` - `GET /staff/profile/benefits` - `GET /staff/profile/benefits/history` - `GET /staff/profile/time-card` - `GET /staff/profile/privacy` - `GET /staff/faqs` - `GET /staff/faqs/search` Example `GET /staff/clock-in/shifts/today` item: ```json { "assignmentId": "uuid", "shiftId": "uuid", "title": "Assigned espresso shift", "clientName": "Google Mountain View Cafes", "hourlyRate": 23, "roleName": "Barista", "location": "Google MV Cafe Clock Point", "locationAddress": "1600 Amphitheatre Pkwy, Mountain View, CA", "latitude": 37.4221, "longitude": -122.0841, "startTime": "2026-03-17T13:48:23.482Z", "endTime": "2026-03-17T21:48:23.482Z", "clockInMode": "GEO_REQUIRED", "allowClockInOverride": true, "geofenceRadiusMeters": 120, "nfcTagId": "NFC-DEMO-ANA-001", "attendanceStatus": "NOT_CLOCKED_IN", "clockInAt": null } ``` Example `GET /staff/profile/stats` response: ```json { "staffId": "uuid", "totalShifts": 12, "averageRating": 4.8, "ratingCount": 7, "onTimeRate": 91.7, "noShowCount": 1, "cancellationCount": 0, "reliabilityScore": 92.3 } ``` Order booking route notes: - `GET /staff/orders/available` is the canonical order-level marketplace feed for recurring and grouped work - `GET /staff/shifts/open` remains available for shift-level opportunities and swap coverage - `POST /staff/orders/:orderId/book` books the future shifts of an order atomically for one role - the `roleId` returned by `GET /staff/orders/available` is the role catalog id for the order booking flow - the `roleId` returned by `GET /staff/shifts/open` is still the concrete `shift_roles.id` for shift-level apply ### Staff writes - `POST /staff/profile/setup` - `POST /staff/devices/push-tokens` - `DELETE /staff/devices/push-tokens` - `POST /staff/clock-in` - `POST /staff/clock-out` - `POST /staff/location-streams` - `PUT /staff/availability` - `POST /staff/availability/quick-set` - `POST /staff/orders/:orderId/book` - `POST /staff/shifts/:shiftId/apply` - `POST /staff/shifts/:shiftId/accept` - `POST /staff/shifts/:shiftId/decline` - `POST /staff/shifts/:shiftId/request-swap` - `POST /staff/shifts/:shiftId/submit-for-approval` - `PUT /staff/profile/personal-info` - `PUT /staff/profile/experience` - `PUT /staff/profile/locations` - `POST /staff/profile/emergency-contacts` - `PUT /staff/profile/emergency-contacts/:contactId` - `PUT /staff/profile/tax-forms/:formType` - `POST /staff/profile/tax-forms/:formType/submit` - `POST /staff/profile/bank-accounts` - `PUT /staff/profile/privacy` ## 4) Upload and verification routes These are exposed as direct unified aliases even though they are backed by `core-api-v2`. ### Generic core aliases - `POST /upload-file` - `POST /create-signed-url` - `POST /invoke-llm` - `POST /rapid-orders/transcribe` - `POST /rapid-orders/parse` - `POST /rapid-orders/process` - `POST /verifications` - `GET /verifications/:verificationId` - `POST /verifications/:verificationId/review` - `POST /verifications/:verificationId/retry` ### Staff upload aliases - `POST /staff/profile/photo` - `POST /staff/profile/documents/:documentId/upload` - `PUT /staff/profile/documents/:documentId/upload` - `POST /staff/profile/attire/:documentId/upload` - `PUT /staff/profile/attire/:documentId/upload` - `POST /staff/profile/certificates` - `DELETE /staff/profile/certificates/:certificateId` ## 5) Notes that matter for frontend - `roleId` on `POST /staff/shifts/:shiftId/apply` is the concrete `shift_roles.id` for that shift, not the catalog role definition id. - `accountType` on `POST /staff/profile/bank-accounts` accepts either lowercase or uppercase and is normalized by the backend. - Document routes now return only document rows. They do not mix in attire items anymore. - Tax-form data should come from `GET /staff/profile/tax-forms`, not `GET /staff/profile/documents`. - Staff benefit activity should come from `GET /staff/profile/benefits/history`; the summary card should keep using `GET /staff/profile/benefits`. - File upload routes return a storage path plus a signed URL. Frontend uploads the file directly to storage using that URL. - The frontend upload contract for documents, attire, and certificates is: 1. `POST /upload-file` 2. `POST /create-signed-url` 3. `POST /verifications` 4. finalize with: - `PUT /staff/profile/documents/:documentId/upload` - `PUT /staff/profile/attire/:documentId/upload` - `POST /staff/profile/certificates` - Finalization requires `verificationId`. Frontend may still send `fileUri` or `photoUrl`, but the backend treats the verification-linked file as the source of truth. - `POST /rapid-orders/process` is the single-call route for "transcribe + parse". - `POST /client/orders/:orderId/edit` builds a replacement order from future shifts only. - `POST /client/orders/:orderId/cancel` cancels future shifts only on the mobile surface and leaves historical shifts intact. - Verification upload and review routes are live and were validated through document, attire, and certificate flows. Do not rely on long-lived verification history durability until the dedicated persistence slice is landed in `core-api-v2`. - Attendance policy is explicit. Reads now expose `clockInMode` and `allowClockInOverride`. - `clockInMode` values are: - `NFC_REQUIRED` - `GEO_REQUIRED` - `EITHER` - all source-of-truth timestamps are UTC ISO 8601 values. Frontend should convert them to local time for display. - For `POST /staff/clock-in` and `POST /staff/clock-out`: - send `nfcTagId` when clocking with NFC - send `latitude`, `longitude`, and `accuracyMeters` when clocking with geolocation - send `proofNonce` and `proofTimestamp` for attendance-proof logging; these are most important on NFC paths - send `attestationProvider` and `attestationToken` only when the device has a real attestation result to forward - send `overrideReason` only when the worker is bypassing a geofence failure and the shift/hub allows overrides - if the worker is already clocked in, backend returns `409` with code `ALREADY_CLOCKED_IN` - `POST /staff/location-streams` is for the background tracking loop after a worker is already clocked in. - `GET /client/coverage/incidents` is the review feed for geofence breaches, missing-location batches, and clock-in overrides. - `GET /client/coverage/blocked-staff` is the review feed for workers currently blocked by that business. - `POST /client/coverage/late-workers/:assignmentId/cancel` is the client-side recovery action when lateness is confirmed by incident evidence or elapsed grace time. - `GET /client/coverage/swap-requests` is the manager/ops review feed for swap requests, candidate applications, and status. - `GET /client/coverage/dispatch-candidates` returns ranked candidates with the dispatch-team priority already applied. - swap auto-cancellation is backend-driven. If a swap request expires without a replacement, backend cancels the original assignment, marks the swap request `AUTO_CANCELLED`, and alerts both the manager path and the original worker. - Raw location stream payloads are stored in the private v2 bucket; SQL only stores the summary and incident index. - Push delivery is backed by: - SQL token registry in `device_push_tokens` - durable queue in `notification_outbox` - per-attempt delivery records in `notification_deliveries` - private Cloud Run worker service `krow-notification-worker-v2` - Cloud Scheduler job `krow-notification-dispatch-v2` ### Push token request example ```json { "provider": "FCM", "platform": "IOS", "pushToken": "expo-or-fcm-device-token", "deviceId": "iphone-15-pro-max", "appVersion": "2.0.0", "appBuild": "2000", "locale": "en-US", "timezone": "America/Los_Angeles" } ``` Push-token delete requests may send `tokenId` or `pushToken` either: - as JSON in the request body - or as query params on the `DELETE` URL Using query params is safer when the client stack or proxy is inconsistent about forwarding `DELETE` bodies. ### Clock-in request example ```json { "shiftId": "uuid", "sourceType": "GEO", "deviceId": "iphone-15-pro", "latitude": 37.4221, "longitude": -122.0841, "accuracyMeters": 12, "proofNonce": "nonce-generated-on-device", "proofTimestamp": "2026-03-16T09:00:00.000Z", "overrideReason": "Parking garage entrance is outside the marked hub geofence", "capturedAt": "2026-03-16T09:00:00.000Z" } ``` ### Location-stream batch example ```json { "shiftId": "uuid", "sourceType": "GEO", "deviceId": "iphone-15-pro", "points": [ { "capturedAt": "2026-03-16T09:15:00.000Z", "latitude": 37.4221, "longitude": -122.0841, "accuracyMeters": 12 }, { "capturedAt": "2026-03-16T09:30:00.000Z", "latitude": 37.4301, "longitude": -122.0761, "accuracyMeters": 20 } ], "metadata": { "source": "background-workmanager" } } ``` ### Coverage incidents response shape ```json { "items": [ { "incidentId": "uuid", "assignmentId": "uuid", "shiftId": "uuid", "staffName": "Ana Barista", "incidentType": "OUTSIDE_GEOFENCE", "severity": "CRITICAL", "status": "OPEN", "clockInMode": "GEO_REQUIRED", "overrideReason": null, "message": "Worker drifted outside hub geofence during active monitoring", "distanceToClockPointMeters": 910, "withinGeofence": false, "occurredAt": "2026-03-16T09:30:00.000Z" } ], "requestId": "uuid" } ``` ## 6) Why this shape - frontend gets one host - backend keeps reads, writes, and service helpers separated - routing can change internally later without forcing frontend rewrites