15 KiB
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.
Client auth
POST /auth/client/sign-inPOST /auth/client/sign-upPOST /auth/client/sign-out
Staff auth
POST /auth/staff/phone/startPOST /auth/staff/phone/verifyPOST /auth/staff/sign-out
Shared auth
GET /auth/sessionPOST /auth/sign-out
2) Client routes
Client reads
GET /client/sessionGET /client/dashboardGET /client/reordersGET /client/billing/accountsGET /client/billing/invoices/pendingGET /client/billing/invoices/historyGET /client/billing/current-billGET /client/billing/savingsGET /client/billing/spend-breakdownGET /client/coverageGET /client/coverage/statsGET /client/coverage/core-teamGET /client/coverage/incidentsGET /client/coverage/blocked-staffGET /client/coverage/swap-requestsGET /client/coverage/dispatch-teamsGET /client/coverage/dispatch-candidatesGET /client/hubsGET /client/cost-centersGET /client/vendorsGET /client/vendors/:vendorId/rolesGET /client/hubs/:hubId/managersGET /client/team-membersGET /client/shifts/scheduledGET /client/orders/viewdeprecated compatibility aliasGET /client/orders/:orderId/reorder-previewGET /client/reports/summaryGET /client/reports/daily-opsGET /client/reports/spendGET /client/reports/coverageGET /client/reports/forecastGET /client/reports/performanceGET /client/reports/no-show
Client writes
POST /client/devices/push-tokensDELETE /client/devices/push-tokensPOST /client/orders/one-timePOST /client/orders/recurringPOST /client/orders/permanentPOST /client/orders/:orderId/editPOST /client/orders/:orderId/cancelPOST /client/shift-managersPOST /client/hubsPUT /client/hubs/:hubIdDELETE /client/hubs/:hubIdPOST /client/hubs/:hubId/assign-nfcPOST /client/hubs/:hubId/managersPOST /client/billing/invoices/:invoiceId/approvePOST /client/billing/invoices/:invoiceId/disputePOST /client/coverage/reviewsPOST /client/coverage/late-workers/:assignmentId/cancelPOST /client/coverage/swap-requests/:swapRequestId/resolvePOST /client/coverage/swap-requests/:swapRequestId/cancelPOST /client/coverage/dispatch-teams/membershipsDELETE /client/coverage/dispatch-teams/memberships/:membershipId
Timeline route naming:
GET /client/shifts/scheduledis the canonical client timeline route- it returns shift-level scheduled items, not order headers
GET /client/orders/viewstill returns the same payload for compatibility, but now emits a deprecation header
Coverage-review request payload may also send:
{
"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
locationNameandlocationAddress - each assigned worker item includes
hasReview
Swap-review routes:
GET /client/coverage/swap-requests?status=OPENPOST /client/coverage/swap-requests/:swapRequestId/resolvePOST /client/coverage/swap-requests/:swapRequestId/cancel
Resolve example:
{
"applicationId": "uuid",
"note": "Dispatch selected the strongest replacement candidate"
}
Dispatch-team routes:
GET /client/coverage/dispatch-teamsGET /client/coverage/dispatch-candidates?shiftId=uuid&roleId=uuidPOST /client/coverage/dispatch-teams/membershipsDELETE /client/coverage/dispatch-teams/memberships/:membershipId
Dispatch-team membership example:
{
"staffId": "uuid",
"hubId": "uuid",
"teamType": "CORE",
"notes": "Preferred lead barista for this location"
}
Dispatch priority order is:
CORECERTIFIED_LOCATIONMARKETPLACE
Shift-manager creation example:
{
"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/sessionGET /staff/dashboardGET /staff/profile/statsGET /staff/profile-completionGET /staff/availabilityGET /staff/clock-in/shifts/todayGET /staff/clock-in/statusGET /staff/payments/summaryGET /staff/payments/historyGET /staff/payments/chartGET /staff/orders/availableGET /staff/shifts/assignedGET /staff/shifts/openGET /staff/shifts/pendingGET /staff/shifts/cancelledGET /staff/shifts/completedGET /staff/shifts/:shiftIdGET /staff/profile/sectionsGET /staff/profile/personal-infoGET /staff/profile/industriesGET /staff/profile/skillsGET /staff/profile/documentsGET /staff/profile/attireGET /staff/profile/tax-formsGET /staff/profile/emergency-contactsGET /staff/profile/certificatesGET /staff/profile/bank-accountsGET /staff/profile/benefitsGET /staff/profile/benefits/historyGET /staff/profile/time-cardGET /staff/profile/privacyGET /staff/faqsGET /staff/faqs/search
Example GET /staff/clock-in/shifts/today item:
{
"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:
{
"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/availableis the canonical order-level marketplace feed for recurring and grouped workGET /staff/shifts/openremains available for shift-level opportunities and swap coveragePOST /staff/orders/:orderId/bookbooks the future shifts of an order atomically for one role- the
roleIdreturned byGET /staff/orders/availableis the role catalog id for the order booking flow - the
roleIdreturned byGET /staff/shifts/openis still the concreteshift_roles.idfor shift-level apply
Staff writes
POST /staff/profile/setupPOST /staff/devices/push-tokensDELETE /staff/devices/push-tokensPOST /staff/clock-inPOST /staff/clock-outPOST /staff/location-streamsPUT /staff/availabilityPOST /staff/availability/quick-setPOST /staff/orders/:orderId/bookPOST /staff/shifts/:shiftId/applyPOST /staff/shifts/:shiftId/acceptPOST /staff/shifts/:shiftId/declinePOST /staff/shifts/:shiftId/request-swapPOST /staff/shifts/:shiftId/submit-for-approvalPUT /staff/profile/personal-infoPUT /staff/profile/experiencePUT /staff/profile/locationsPOST /staff/profile/emergency-contactsPUT /staff/profile/emergency-contacts/:contactIdPUT /staff/profile/tax-forms/:formTypePOST /staff/profile/tax-forms/:formType/submitPOST /staff/profile/bank-accountsPUT /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-filePOST /create-signed-urlPOST /invoke-llmPOST /rapid-orders/transcribePOST /rapid-orders/parsePOST /rapid-orders/processPOST /verificationsGET /verifications/:verificationIdPOST /verifications/:verificationId/reviewPOST /verifications/:verificationId/retry
Staff upload aliases
POST /staff/profile/photoPOST /staff/profile/documents/:documentId/uploadPUT /staff/profile/documents/:documentId/uploadPOST /staff/profile/attire/:documentId/uploadPUT /staff/profile/attire/:documentId/uploadPOST /staff/profile/certificatesDELETE /staff/profile/certificates/:certificateId
5) Notes that matter for frontend
roleIdonPOST /staff/shifts/:shiftId/applyis the concreteshift_roles.idfor that shift, not the catalog role definition id.accountTypeonPOST /staff/profile/bank-accountsaccepts 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, notGET /staff/profile/documents. - Staff benefit activity should come from
GET /staff/profile/benefits/history; the summary card should keep usingGET /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:
POST /upload-filePOST /create-signed-urlPOST /verifications- finalize with:
PUT /staff/profile/documents/:documentId/uploadPUT /staff/profile/attire/:documentId/uploadPOST /staff/profile/certificates
- Finalization requires
verificationId. Frontend may still sendfileUriorphotoUrl, but the backend treats the verification-linked file as the source of truth. POST /rapid-orders/processis the single-call route for "transcribe + parse".POST /client/orders/:orderId/editbuilds a replacement order from future shifts only.POST /client/orders/:orderId/cancelcancels 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
clockInModeandallowClockInOverride. clockInModevalues are:NFC_REQUIREDGEO_REQUIREDEITHER
- all source-of-truth timestamps are UTC ISO 8601 values. Frontend should convert them to local time for display.
- For
POST /staff/clock-inandPOST /staff/clock-out:- send
nfcTagIdwhen clocking with NFC - send
latitude,longitude, andaccuracyMeterswhen clocking with geolocation - send
proofNonceandproofTimestampfor attendance-proof logging; these are most important on NFC paths - send
attestationProviderandattestationTokenonly when the device has a real attestation result to forward - send
overrideReasononly when the worker is bypassing a geofence failure and the shift/hub allows overrides - if the worker is already clocked in, backend returns
409with codeALREADY_CLOCKED_IN
- send
POST /staff/location-streamsis for the background tracking loop after a worker is already clocked in.GET /client/coverage/incidentsis the review feed for geofence breaches, missing-location batches, and clock-in overrides.GET /client/coverage/blocked-staffis the review feed for workers currently blocked by that business.POST /client/coverage/late-workers/:assignmentId/cancelis the client-side recovery action when lateness is confirmed by incident evidence or elapsed grace time.GET /client/coverage/swap-requestsis the manager/ops review feed for swap requests, candidate applications, and status.GET /client/coverage/dispatch-candidatesreturns 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
- SQL token registry in
Push token request example
{
"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
DELETEURL
Using query params is safer when the client stack or proxy is inconsistent about forwarding DELETE bodies.
Clock-in request example
{
"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
{
"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
{
"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