Files
Krow-workspace/docs/BACKEND/API_GUIDES/V2/unified-api.md
2026-03-18 12:56:14 +01:00

14 KiB

Unified API V2

Frontend should use this service as the single base URL:

  • https://krow-api-v2-933560802882.us-central1.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-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/orders/view
  • 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

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.

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:

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

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

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

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

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/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
  • 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
  • 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

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

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