Gomi API v1.0
Waste collection management platform β driver tracking, dispatch, and administration.
https://getgomi.xyz
Quick Start
Get up and running with the Gomi API in minutes. All endpoints return JSON. Every request must include an Authorization header with a Bearer token (JWT or API key).
Required Headers
Every API request needs these headers:
Authorization: Bearer <your-jwt-token-or-api-key>
Content-Type: application/json
| Method | Token Format | Access Level | Use Case |
|---|---|---|---|
| JWT | eyJhbGciOi... (long base64) | β All endpoints | Mobile apps, SPAs |
| API Key | gomi_d0ea77fe... (starts with gomi_) | β οΈ /api/driver/* only | Simple scripts, fallback |
Both go in the same header: Authorization: Bearer <token>
API Client Helper (React Native / JavaScript)
const BASE = 'https://getgomi.xyz';
// Store the JWT after login (see Mobile App Flow below)
let jwtToken = null;
async function api(path, opts = {}) {
const res = await fetch(BASE + path, {
...opts,
headers: {
'Authorization': `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
...opts.headers,
},
});
if (res.status === 401) {
// Token expired β try refreshing
const refreshed = await refreshToken();
if (refreshed) return api(path, opts); // retry once
throw new Error('Session expired, please log in again');
}
if (!res.ok) {
const e = await res.json().catch(() => ({}));
throw new Error(e.error || res.statusText);
}
return res.json();
}
async function refreshToken() {
try {
const res = await fetch(BASE + '/auth/refresh', {
method: 'POST',
headers: {
'Authorization': `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
});
if (!res.ok) return false;
const data = await res.json();
jwtToken = data.token; // save new token
return true;
} catch {
return false;
}
}
Usage Examples
// Get current user
const { user } = await api('/auth/me');
// Clock in as driver
await api('/api/driver/clock-in', {
method: 'PUT',
body: JSON.stringify({ latitude: 35.68, longitude: 139.77 }),
});
// Fetch assigned jobs
const { jobs } = await api('/api/driver/jobs?status=assigned');
// Start a trip
const { trip } = await api('/api/driver/trips/start', {
method: 'POST',
body: JSON.stringify({ job_id: 'job-uuid', latitude: 35.68, longitude: 139.77 }),
});
cURL Examples
# With JWT token
curl -H "Authorization: Bearer eyJhbGciOi..." \
https://getgomi.xyz/auth/me
# With API key (driver endpoints only)
curl -H "Authorization: Bearer gomi_d0ea77fe..." \
https://getgomi.xyz/api/driver/jobs
# POST with JSON body
curl -X PUT \
-H "Authorization: Bearer eyJhbGciOi..." \
-H "Content-Type: application/json" \
-d '{"latitude": -1.286, "longitude": 36.817}' \
https://getgomi.xyz/api/driver/clock-in
Authentication
Gomi uses Google OAuth 2.0 for login. The API supports three auth methods, all sent via the same Authorization: Bearer <token> header.
Auth Tiers
| Tier | How You Get It | Expires | Endpoint Access |
|---|---|---|---|
| JWT | POST /auth/token (exchange session cookie) | 24 hours | All endpoints |
| Session Cookie | Set automatically after Google OAuth login | 30 days | All endpoints (browser only) |
| API Key | POST /api/keys | Configurable | /api/driver/* only |
/api/driver/* endpoints. For all other endpoints (profile, dispatch, admin), you need a JWT or session cookie. This prevents privilege escalation if a key is compromised.
π± Mobile App Login Flow (React Native)
This is the recommended flow for mobile apps. Open Google OAuth in a WebView, extract the session cookie, exchange it for a JWT, then use the JWT for all subsequent API calls.
/auth/login β 2. User signs in
Google OAuth β 3. Cookie set
gomi_session β 4. Exchange for JWT
POST /auth/token β 5. Use JWT
Authorization: Bearer <jwt>
import { WebView } from 'react-native-webview';
import AsyncStorage from '@react-native-async-storage/async-storage';
import CookieManager from '@react-native-cookies/cookies';
const BASE = 'https://getgomi.xyz';
// Step 1-3: Open WebView for Google OAuth login
function LoginScreen({ onLoginSuccess }) {
const handleNavigationChange = async (navState) => {
// After OAuth callback, the URL returns to the app
// and the session cookie "gomi_session" is now set
if (navState.url.includes('/welcome') || navState.url === BASE + '/') {
// Step 4: Exchange the session cookie for a JWT
const cookies = await CookieManager.get(BASE);
const sessionCookie = cookies.gomi_session?.value;
if (sessionCookie) {
const res = await fetch(`${BASE}/auth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cookie': `gomi_session=${sessionCookie}`,
},
});
const data = await res.json();
// data = { token: "eyJhbG...", expires_in: 86400, token_type: "Bearer",
// api_key: "gomi_..." (first login only) }
// Step 5: Store JWT for all future API calls
await AsyncStorage.setItem('jwt_token', data.token);
await AsyncStorage.setItem('jwt_expires_at',
String(Date.now() + data.expires_in * 1000));
// Store auto-provisioned API key (first login only)
if (data.api_key) {
await AsyncStorage.setItem('api_key', data.api_key);
}
onLoginSuccess(data.token);
}
}
};
return (
<WebView
source={{ uri: `${BASE}/auth/login` }}
onNavigationStateChange={handleNavigationChange}
sharedCookiesEnabled={true}
/>
);
}
// Step 5: Use JWT for all API calls
async function api(path, opts = {}) {
let token = await AsyncStorage.getItem('jwt_token');
const expiresAt = await AsyncStorage.getItem('jwt_expires_at');
// Auto-refresh if expiring within 1 hour
if (expiresAt && Date.now() > Number(expiresAt) - 3600000) {
token = await doRefresh(token);
}
const res = await fetch(BASE + path, {
...opts,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...opts.headers,
},
});
if (!res.ok) {
const e = await res.json().catch(() => ({}));
throw new Error(e.error || res.statusText);
}
return res.json();
}
async function doRefresh(currentToken) {
const res = await fetch(`${BASE}/auth/refresh`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${currentToken}`,
'Content-Type': 'application/json',
},
});
if (!res.ok) throw new Error('Token refresh failed β re-login required');
const data = await res.json();
await AsyncStorage.setItem('jwt_token', data.token);
await AsyncStorage.setItem('jwt_expires_at',
String(Date.now() + data.expires_in * 1000));
return data.token;
}
Auth Endpoints
Redirects the user to Google's OAuth consent screen. After authorization, the user is redirected back with a gomi_session cookie set (30-day expiry, HttpOnly, Secure).
| Param | Type | Description |
|---|---|---|
redirect | string | Optional URL to redirect after login (default: welcome page) |
Exchanges a valid session cookie for a JWT. This is the key step for mobile apps β call this right after the OAuth WebView flow completes.
gomi_session cookie, call this endpoint with that cookie to get a JWT. Store the JWT and use it for all subsequent API calls.
Auto-provisioned API key: On the first call, if the user has no API key yet, one is automatically created and returned in the
api_key field. Save it β itβs only shown once. Use the JWT for all endpoints; the API key is a fallback for /api/driver/* only.
POST /auth/token HTTP/1.1
Host: getgomi.xyz
Cookie: gomi_session=<session-id-from-oauth-login>
Content-Type: application/json
No request body needed.
// First call (no API key yet β one is auto-created):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYzg4...",
"expires_in": 86400,
"token_type": "Bearer",
"api_key": "gomi_a1b2c3d4e5f6...",
"api_key_note": "Auto-generated API key for /api/driver/* endpoints. Save it β it won't be shown again."
}
// Subsequent calls (user already has key):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYzg4...",
"expires_in": 86400,
"token_type": "Bearer"
}
| Field | Type | Description |
|---|---|---|
token | string | JWT token β use this in Authorization: Bearer <token> |
expires_in | int | Seconds until expiry (86400 = 24 hours) |
token_type | string | Always "Bearer" |
api_key | string? | Only present on first call if user had no key. Raw API key for /api/driver/*. Save it β never shown again. |
api_key_note | string? | Explanation (present only when api_key is included) |
| Status | Body | Meaning |
|---|---|---|
| 401 | {"error": "not authenticated"} | No valid session cookie |
| 403 | {"error": "session or JWT authentication required"} | API key auth used (not allowed β must use session cookie) |
Exchanges a still-valid JWT for a fresh one with a new 24-hour expiry. Call this before the current token expires to keep the user logged in without re-doing OAuth.
POST /auth/refresh HTTP/1.1
Host: getgomi.xyz
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
No request body needed. Must use a JWT (not a session cookie or API key).
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vy...",
"expires_in": 86400,
"token_type": "Bearer"
}
| Status | Body | Meaning |
|---|---|---|
| 401 | {"error": "not authenticated"} | No token or token expired |
| 403 | {"error": "JWT required for refresh"} | Used session/API key instead of JWT |
expires_in). If refresh fails, redirect the user back to the login WebView.
Returns the currently authenticated user's profile. Use this to verify the token works and get user info after login.
GET /auth/me HTTP/1.1
Host: getgomi.xyz
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"user": {
"id": "c88d57e3b2c5a460e194c5a398a26b2c",
"email": "driver@example.com",
"first_name": "Harold",
"last_name": "M",
"avatar_url": "https://lh3.googleusercontent.com/...",
"role": "driver",
"status": "offline"
}
}
Destroys the server-side session and clears the gomi_session cookie. JWTs already issued will remain valid until they expire (24h), but new token exchanges will fail.
{
"message": "logged out"
}
API Keys
API keys provide simple auth for driver endpoints. They only work on /api/driver/* routes. For full access, use JWT (see above).
Use the same Authorization header β or a query parameter as a fallback:
Authorization: Bearer gomi_d0ea77fefcf5f813... # OR as query parameter: GET /api/driver/jobs?key=gomi_d0ea77fefcf5f813...
Requires JWT or session cookie auth (API key auth cannot create new keys).
{
"name": "phone",
"expires_in": "90d"
}
{
"key": "gomi_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
"api_key": {
"id": "key-uuid-1234",
"name": "phone",
"key_prefix": "gomi_a1b2...s9t0"
}
}
{
"api_keys": [
{
"id": "key-uuid-1234",
"name": "phone",
"key_prefix": "gomi_a1b2...s9t0",
"created_at": "2026-04-01T00:00:00Z",
"expires_at": "2026-07-01T00:00:00Z",
"last_used_at": "2026-04-14T03:22:00Z"
}
]
}
Permanently revokes the API key. Any requests using this key will immediately fail with 401.
{
"message": "api key revoked"
}
Profile
Manage the authenticated user's own profile. Requires JWT or session cookie. Auth Required
Returns the full profile of the authenticated user including location and timestamps.
{
"user": {
"id": "c88d57e3...",
"email": "driver@example.com",
"first_name": "Harold",
"last_name": "M",
"avatar_url": "https://lh3.googleusercontent.com/...",
"role": "driver",
"status": "online",
"last_known_lat": -1.286,
"last_known_lng": 36.817,
"created_at": "2026-06-01T00:00:00Z"
}
}
{
"first_name": "Harold",
"last_name": "Mukuru"
}
{
"user": { ... },
"message": "profile updated"
}
Driver: Shifts & Location
Drivers must clock in before accepting jobs. Location updates keep the dispatch dashboard current. Driver
Clocks the driver in and sets their status to online. Requires current GPS coordinates.
{
"latitude": 35.6812,
"longitude": 139.7671
}
{
"user": {
"id": "driver-uuid",
"status": "online",
"latitude": 35.6812,
"longitude": 139.7671
},
"message": "clocked in"
}
{
"notes": "Finished all scheduled routes"
}
{
"user": { "id": "driver-uuid", "status": "offline" },
"message": "clocked out"
}
Pushes a background location update outside of active trips. Use this while online but not on an active trip.
{
"latitude": 35.6812,
"longitude": 139.7671
}
Returns the driver's current status, active shift, and job summary.
{
"user": { "id": "driver-uuid", "first_name": "Tanaka", "status": "online" },
"status": "online",
"active_shift": {
"clocked_in_at": "2026-04-14T08:00:00Z",
"latitude": 35.6812,
"longitude": 139.7671
},
"active_jobs": [],
"upcoming_jobs": [
{ "id": "job-1", "title": "Shibuya pickup", "status": "assigned" }
],
"stats": {
"active": 0,
"upcoming": 1,
"completed_today": 5,
"total": 142
}
}
Returns the driver's shift event history (clock-in / clock-out pairs). Filter by date range.
| Param | Type | Description |
|---|---|---|
from | string | Start date (RFC 3339 or YYYY-MM-DD) |
to | string | End date (RFC 3339 or YYYY-MM-DD) |
{
"events": [
{
"id": "evt-uuid-001",
"type": "clock_in",
"latitude": 35.6812,
"longitude": 139.7671,
"created_at": "2026-01-15T08:00:00Z"
},
{
"id": "evt-uuid-002",
"type": "clock_out",
"notes": "End of shift",
"created_at": "2026-01-15T17:00:00Z"
}
]
}
Set the driver's availability status without a full clock-in/clock-out cycle.
{
"status": "online"
}
Accepted values: online, offline.
{
"user": {
"id": "driver-uuid",
"status": "online",
"first_name": "Tanaka",
"last_name": "Yuki"
},
"message": "status updated"
}
Returns unassigned jobs near the driver's current location. The driver must be online with a known location.
| Param | Type | Description |
|---|---|---|
limit | int | Max results (default 10) |
{
"jobs": [
{
"id": "job-uuid-010",
"title": "Shibuya Ward - Burnable",
"address": "1-2-3 Shibuya, Shibuya-ku, Tokyo",
"latitude": 35.6580,
"longitude": 139.7016,
"distance_km": 1.3,
"priority": "normal",
"waste_type": "Burnable",
"scheduled_date": "2026-04-15"
}
]
}
Driver: Jobs
View and manage assigned collection jobs. Driver
| Param | Type | Description |
|---|---|---|
status | string | Filter: assigned, in_progress, completed, pending |
limit | int | Max results (default 50) |
offset | int | Pagination offset |
{
"jobs": [
{
"id": "job-uuid-001",
"title": "Shibuya Ward - Burnable",
"address": "1-2-3 Shibuya, Shibuya-ku, Tokyo",
"latitude": 35.6580,
"longitude": 139.7016,
"status": "assigned",
"priority": "normal",
"waste_type_name": "Burnable",
"waste_type_icon": "π₯"
}
],
"total": 1
}
{
"job": {
"id": "job-uuid-001",
"title": "Shibuya Ward - Burnable",
"address": "1-2-3 Shibuya, Shibuya-ku, Tokyo",
"latitude": 35.6580,
"longitude": 139.7016,
"status": "assigned",
"priority": "normal",
"scheduled_date": "2026-04-14",
"estimated_volume": 2.5,
"waste_type_name": "Burnable",
"waste_type_icon": "π₯",
"notes": "Gate code: 1234",
"assigned_at": "2026-04-14T07:00:00Z"
}
}
{
"latitude": 35.6812,
"longitude": 139.7671
}
{
"job": { "id": "job-uuid-001", "status": "in_progress" },
"message": "job started"
}
{
"notes": "Collected 3 bags from front gate",
"actual_volume": 3.0
}
{
"job": { "id": "job-uuid-001", "status": "completed" },
"message": "job completed",
"status": "completed"
}
{
"stats": {
"total": 142,
"pending": 0,
"assigned": 3,
"in_progress": 1,
"completed": 138,
"completed_today": 5
}
}
Driver: Job Photos πΈ
Drivers can upload up to 5 photos per phase (before/after) as proof-of-work. Photos are stored in Google Cloud Storage with full EXIF metadata preserved for auditing. Uploads allowed while job is in-progress or within 48 hours after completion. Driver
Upload a photo for a job. The original file is stored as-is (no compression, no EXIF stripping). EXIF metadata is extracted and indexed for admin search.
multipart/form-data, not JSON. Send the image as a form file field.
| Field | Type | Required | Description |
|---|---|---|---|
file | binary | β | Image file (JPEG, PNG, WebP, HEIC). Max 50MB. |
phase | string | β | before or after (default: after) |
latitude | float | β | Phone GPS latitude at upload time |
longitude | float | β | Phone GPS longitude at upload time |
curl -X POST \
-H "Authorization: Bearer eyJhbG..." \
-F "file=@photo.jpg" \
-F "phase=after" \
-F "latitude=-1.286" \
-F "longitude=36.817" \
https://getgomi.xyz/api/driver/jobs/{job_id}/photos
const formData = new FormData();
formData.append('file', {
uri: photo.uri, // from camera/gallery
type: photo.type || 'image/jpeg',
name: photo.fileName || 'photo.jpg',
});
formData.append('phase', 'after');
formData.append('latitude', String(location.latitude));
formData.append('longitude', String(location.longitude));
const res = await fetch(
`https://getgomi.xyz/api/driver/jobs/${jobId}/photos`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData,
// Do NOT set Content-Type β fetch sets it with boundary
}
);
const data = await res.json();
// data.photo.public_url β display this
// data.remaining β photos left for this phase
{
"photo": {
"id": "a1b2c3d4e5f6...",
"job_id": "c88d57e3...",
"driver_id": "d9f8e7c6...",
"phase": "after",
"gcs_path": "jobs/c88d57e3/after/a1b2c3d4.jpg",
"public_url": "https://storage.googleapis.com/gomi-job-photos/jobs/c88d57e3/after/a1b2c3d4.jpg",
"content_type": "image/jpeg",
"size_bytes": 4521983,
"latitude": -1.286,
"longitude": 36.817,
"exif_taken_at": "2026-07-07T10:30:00Z",
"exif_lat": -1.2861,
"exif_lng": 36.8172,
"exif_make": "Samsung",
"exif_model": "Galaxy S24",
"exif_orientation": 1,
"image_width": 4000,
"image_height": 3000,
"uploaded_at": "2026-07-07T10:31:15Z"
},
"remaining": 4
}
| Status | Meaning |
|---|---|
| 400 | Missing file, bad phase, unsupported image type |
| 403 | Job not assigned to you |
| 404 | Job not found |
| 409 | Max 5 photos per phase reached, or upload window closed (48h) |
| 413 | File too large (max 50MB) |
| 503 | Photo uploads not configured (GCS not set up) |
Returns all photos for a job, grouped by phase (before, then after).
{
"photos": [
{
"id": "a1b2c3d4...",
"phase": "before",
"public_url": "https://storage.googleapis.com/gomi-job-photos/jobs/.../before/a1b2.jpg",
"exif_taken_at": "2026-07-07T10:00:00Z",
"exif_make": "Apple",
"exif_model": "iPhone 16 Pro",
"uploaded_at": "2026-07-07T10:01:00Z"
},
{
"id": "e5f6g7h8...",
"phase": "after",
"public_url": "https://storage.googleapis.com/gomi-job-photos/jobs/.../after/e5f6.jpg",
"uploaded_at": "2026-07-07T10:45:00Z"
}
]
}
Driver: Trip Tracking β
The trip tracking system captures high-frequency GPS telemetry from driver devices during active collections. Trips link to jobs and provide real-time visibility for dispatch. Driver
Trip Lifecycle
Initiates a trip linked to a job. The driver's status changes to on_job.
{
"job_id": "job-uuid-001",
"latitude": 35.6812,
"longitude": 139.7671
}
{
"trip": {
"id": "trip-uuid-001",
"job_id": "job-uuid-001",
"driver_id": "driver-uuid",
"status": "en_route",
"start_latitude": 35.6812,
"start_longitude": 139.7671,
"started_at": "2026-04-14T08:15:00Z"
},
"next_ping_ms": 3000
}
Compact GPS telemetry payload. Send at the interval specified by next_ping_ms in the response. Short field names minimize bandwidth on mobile networks.
{
"lat": 35.6815,
"lng": 139.7680,
"spd": 13.9,
"hdg": 225,
"acc": 8.5,
"alt": 35,
"batt": 92,
"ts": "2026-04-14T05:00:00Z",
"seq": 42
}
{
"ok": true,
"ping_count": 42,
"stored": true,
"next_ping_ms": 3000
}
Ping Field Reference
| Field | Type | Req | Description |
|---|---|---|---|
lat | float | β | Latitude (-90 to 90) |
lng | float | β | Longitude (-180 to 180) |
spd | float | Speed in meters per second | |
hdg | float | Heading 0β360Β° (0=North, clockwise) | |
acc | float | GPS accuracy in meters (lower = better) | |
alt | float | Altitude in meters above sea level | |
batt | float | Device battery level 0β100 | |
ts | string | ISO 8601 timestamp from client clock | |
seq | int | Monotonic sequence number (for ordering) |
Adaptive Ping Frequency
The server returns next_ping_ms but the client should also adapt based on speed:
| Speed | Interval |
|---|---|
| > 60 km/h | 2 seconds |
| 30β60 km/h | 3 seconds |
| 10β30 km/h | 5 seconds |
| < 10 km/h | 10 seconds |
| < 2 km/h (stopped) | 15 seconds |
Send multiple queued pings at once. Useful when the device was offline or to reduce HTTP overhead. Accepts Content-Encoding: gzip for compressed payloads.
{
"pings": [
{ "lat": 35.6812, "lng": 139.7671, "spd": 0, "seq": 1, "ts": "2026-04-14T08:15:00Z" },
{ "lat": 35.6815, "lng": 139.7680, "spd": 8.2, "seq": 2, "ts": "2026-04-14T08:15:05Z" },
{ "lat": 35.6820, "lng": 139.7695, "spd": 13.1, "seq": 3, "ts": "2026-04-14T08:15:08Z" }
]
}
{
"ok": true,
"ping_count": 45,
"stored": 3,
"skipped": 0,
"next_ping_ms": 3000
}
{
"latitude": 35.6580,
"longitude": 139.7016
}
{
"trip": {
"id": "trip-uuid-001",
"status": "arrived",
"arrived_at": "2026-04-14T08:32:00Z"
}
}
{
"latitude": 35.6580,
"longitude": 139.7016,
"notes": "All collected, area clean"
}
{
"trip": {
"id": "trip-uuid-001",
"status": "completed",
"completed_at": "2026-04-14T08:45:00Z",
"distance_meters": 4250,
"duration_seconds": 1800
},
"stats": {
"ping_count": 312,
"avg_speed": 8.5
}
}
{
"trip": {
"id": "trip-uuid-001",
"status": "cancelled",
"cancelled_at": "2026-04-14T08:20:00Z"
}
}
{
"trips": [
{
"id": "trip-uuid-001",
"job_id": "job-uuid-001",
"status": "completed",
"started_at": "2026-04-14T08:15:00Z",
"completed_at": "2026-04-14T08:45:00Z",
"distance_meters": 4250
}
]
}
{
"trip": {
"id": "trip-uuid-001",
"job_id": "job-uuid-001",
"driver_id": "driver-uuid",
"status": "completed",
"start_latitude": 35.6812,
"start_longitude": 139.7671,
"end_latitude": 35.6580,
"end_longitude": 139.7016,
"distance_meters": 4250,
"duration_seconds": 1800,
"started_at": "2026-04-14T08:15:00Z",
"arrived_at": "2026-04-14T08:32:00Z",
"completed_at": "2026-04-14T08:45:00Z"
},
"stats": {
"ping_count": 312,
"avg_speed": 8.5
}
}
Returns all stored GPS pings for a trip, ordered by sequence number.
{
"pings": [
{ "lat": 35.6812, "lng": 139.7671, "spd": 0, "hdg": 0, "acc": 5.2, "seq": 1, "ts": "2026-04-14T08:15:00Z" },
{ "lat": 35.6815, "lng": 139.7680, "spd": 8.2, "hdg": 45, "acc": 6.1, "seq": 2, "ts": "2026-04-14T08:15:05Z" }
],
"trip": {
"id": "trip-uuid-001",
"status": "completed"
}
}
React Native TripTracker
Complete implementation using expo-location with adaptive frequency, offline queuing, and batch uploads:
import * as Location from 'expo-location';
const BASE = 'https://getgomi.xyz';
interface Ping {
lat: number; lng: number; spd: number; hdg: number;
acc: number; alt: number; batt: number; ts: string; seq: number;
}
class TripTracker {
private tripId: string | null = null;
private apiKey: string;
private seq = 0;
private queue: Ping[] = [];
private intervalId: ReturnType<typeof setTimeout> | null = null;
private locationSub: Location.LocationSubscription | null = null;
private nextPingMs = 3000;
private lastPing: Ping | null = null;
private sending = false;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
private async request(path: string, body?: object) {
const res = await fetch(BASE + path, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
const e = await res.json().catch(() => ({}));
throw new Error(e.error || res.statusText);
}
return res.json();
}
private getIntervalMs(speedMs: number): number {
const kmh = speedMs * 3.6;
if (kmh > 60) return 2000;
if (kmh > 30) return 3000;
if (kmh > 10) return 5000;
if (kmh > 2) return 10000;
return 15000;
}
async start(jobId: string): Promise<void> {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') throw new Error('Location permission denied');
const loc = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High,
});
const data = await this.request('/api/driver/trips/start', {
job_id: jobId,
latitude: loc.coords.latitude,
longitude: loc.coords.longitude,
});
this.tripId = data.trip.id;
this.nextPingMs = data.next_ping_ms || 3000;
this.seq = 0;
this.queue = [];
// Start watching location
this.locationSub = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.High,
distanceInterval: 5,
timeInterval: 1000,
},
(location) => {
this.lastPing = {
lat: location.coords.latitude,
lng: location.coords.longitude,
spd: location.coords.speed || 0,
hdg: location.coords.heading || 0,
acc: location.coords.accuracy || 0,
alt: location.coords.altitude || 0,
batt: 0, // integrate battery API separately
ts: new Date(location.timestamp).toISOString(),
seq: ++this.seq,
};
},
);
// Start ping loop
this.schedulePing();
}
private schedulePing() {
if (this.intervalId) clearTimeout(this.intervalId);
this.intervalId = setTimeout(async () => {
await this.sendPing();
this.schedulePing();
}, this.nextPingMs);
}
private async sendPing(): Promise<void> {
if (!this.tripId || !this.lastPing || this.sending) return;
this.sending = true;
const ping = { ...this.lastPing };
this.queue.push(ping);
// Adaptive interval based on speed
this.nextPingMs = this.getIntervalMs(ping.spd);
try {
if (this.queue.length > 1) {
// Batch send queued pings
const data = await this.request(
`/api/driver/trips/${this.tripId}/ping/batch`,
{ pings: this.queue },
);
this.nextPingMs = data.next_ping_ms || this.nextPingMs;
this.queue = []; // Clear queue on success
} else {
// Single ping
const data = await this.request(
`/api/driver/trips/${this.tripId}/ping`,
ping,
);
this.nextPingMs = data.next_ping_ms || this.nextPingMs;
this.queue = [];
}
} catch (err) {
// Keep pings in queue for next batch attempt
console.warn('Ping failed, queued:', this.queue.length);
} finally {
this.sending = false;
}
}
async arrive(): Promise<any> {
if (!this.tripId || !this.lastPing) throw new Error('No active trip');
return this.request(`/api/driver/trips/${this.tripId}/arrive`, {
latitude: this.lastPing.lat,
longitude: this.lastPing.lng,
});
}
async complete(notes?: string): Promise<any> {
if (!this.tripId || !this.lastPing) throw new Error('No active trip');
// Flush remaining pings
if (this.queue.length > 0) {
await this.sendPing();
}
const result = await this.request(
`/api/driver/trips/${this.tripId}/complete`,
{
latitude: this.lastPing.lat,
longitude: this.lastPing.lng,
notes: notes || '',
},
);
this.cleanup();
return result;
}
async cancel(): Promise<any> {
if (!this.tripId) throw new Error('No active trip');
const result = await this.request(
`/api/driver/trips/${this.tripId}/cancel`,
);
this.cleanup();
return result;
}
private cleanup() {
if (this.intervalId) clearTimeout(this.intervalId);
if (this.locationSub) this.locationSub.remove();
this.tripId = null;
this.lastPing = null;
this.queue = [];
this.seq = 0;
}
get isActive(): boolean {
return this.tripId !== null;
}
get currentTripId(): string | null {
return this.tripId;
}
}
export default TripTracker;
// --- Usage ---
// const tracker = new TripTracker('gomi_xxx...');
// await tracker.start('job-uuid-001');
// ... driving ...
// await tracker.arrive();
// ... collecting waste ...
// const result = await tracker.complete('All done');
// console.log(result.trip.distance_meters);
SSE Real-Time (Dispatch)
Server-Sent Events stream for live trip updates on the dispatch dashboard. Dispatch
Opens a persistent SSE connection. Authenticate via query parameter since EventSource doesn't support custom headers.
| Param | Type | Description |
|---|---|---|
key | string | API key: gomi_xxx... |
Event Types
React Native EventSource Example
import { useEffect, useRef, useCallback, useState } from 'react';
interface TripPing {
trip_id: string;
lat: number;
lng: number;
spd: number;
hdg: number;
ts: string;
}
interface Trip {
id: string;
driver_id: string;
job_id: string;
status: string;
start_latitude: number;
start_longitude: number;
}
const BASE = 'https://getgomi.xyz';
export function useDispatchSSE(apiKey: string) {
const esRef = useRef<EventSource | null>(null);
const [trips, setTrips] = useState<Map<string, Trip>>(new Map());
const [latestPing, setLatestPing] = useState<TripPing | null>(null);
const connect = useCallback(() => {
if (esRef.current) esRef.current.close();
const es = new EventSource(
`${BASE}/api/dispatch/trips/live?key=${apiKey}`
);
esRef.current = es;
es.addEventListener('init', (e: MessageEvent) => {
const data = JSON.parse(e.data);
const map = new Map<string, Trip>();
data.active_trips.forEach((t: Trip) => map.set(t.id, t));
setTrips(map);
});
es.addEventListener('trip_start', (e: MessageEvent) => {
const { trip } = JSON.parse(e.data);
setTrips(prev => new Map(prev).set(trip.id, trip));
});
es.addEventListener('ping', (e: MessageEvent) => {
const ping: TripPing = JSON.parse(e.data);
setLatestPing(ping);
});
es.addEventListener('trip_arrive', (e: MessageEvent) => {
const { trip } = JSON.parse(e.data);
setTrips(prev => {
const next = new Map(prev);
next.set(trip.id, { ...next.get(trip.id)!, ...trip });
return next;
});
});
es.addEventListener('trip_complete', (e: MessageEvent) => {
const { trip } = JSON.parse(e.data);
setTrips(prev => {
const next = new Map(prev);
next.delete(trip.id);
return next;
});
});
es.addEventListener('trip_cancel', (e: MessageEvent) => {
const { trip } = JSON.parse(e.data);
setTrips(prev => {
const next = new Map(prev);
next.delete(trip.id);
return next;
});
});
es.onerror = () => {
es.close();
// Reconnect after 3 seconds
setTimeout(connect, 3000);
};
}, [apiKey]);
useEffect(() => {
connect();
return () => esRef.current?.close();
}, [connect]);
return { trips, latestPing };
}
// --- Usage in a component ---
// const { trips, latestPing } = useDispatchSSE('gomi_xxx...');
// trips is a Map<tripId, Trip> of all active trips
// latestPing updates on every GPS telemetry event
Dispatch: Jobs
Create, manage, and assign collection jobs to drivers. Dispatch
{
"title": "Meguro Ward - Recyclables",
"address": "4-5-6 Meguro, Meguro-ku, Tokyo",
"latitude": 35.6339,
"longitude": 139.7154,
"waste_type_id": "wt-uuid-002",
"priority": "high",
"scheduled_date": "2026-04-15",
"estimated_volume": 5.0,
"notes": "Large apartment complex, use rear entrance"
}
{
"job": {
"id": "job-uuid-002",
"title": "Meguro Ward - Recyclables",
"status": "pending",
"created_at": "2026-04-14T10:00:00Z"
}
}
| Param | Type | Description |
|---|---|---|
status | string | Filter by status |
driver_id | string | Filter by assigned driver |
date | string | Filter by scheduled date (YYYY-MM-DD) |
limit | int | Max results (default 50) |
offset | int | Pagination offset |
{
"jobs": [ { "id": "...", "title": "...", "status": "pending", ... } ],
"total": 47
}
Returns full job details including assigned driver information.
Update any mutable job fields (title, address, coordinates, notes, priority, scheduled_date, estimated_volume).
{
"driver_id": "driver-uuid"
}
{
"job": { "id": "job-uuid-002", "status": "assigned", "driver_id": "driver-uuid" },
"message": "job assigned"
}
{
"notes": "Customer cancelled pickup"
}
Reassign an existing job to a different driver. The job must not be completed or cancelled.
{
"driver_id": "new-driver-uuid"
}
{
"job": {
"id": "job-uuid-002",
"status": "assigned",
"driver_id": "new-driver-uuid"
},
"message": "job reassigned"
}
Automatically assign all pending jobs to the nearest available online drivers based on location proximity.
{
"assigned": 12,
"skipped": 3,
"message": "auto-assign complete"
}
Returns the full event history for a job β creation, assignment, status changes, reassignment, completion.
{
"events": [
{
"id": "evt-uuid-001",
"event_type": "created",
"actor_id": "dispatch-user-uuid",
"created_at": "2026-04-14T10:00:00Z"
},
{
"id": "evt-uuid-002",
"event_type": "assigned",
"actor_id": "dispatch-user-uuid",
"data": { "driver_id": "driver-uuid" },
"created_at": "2026-04-14T10:05:00Z"
},
{
"id": "evt-uuid-003",
"event_type": "completed",
"actor_id": "driver-uuid",
"created_at": "2026-04-14T14:30:00Z"
}
]
}
Dispatch: Active Trips
Returns all currently active trips with embedded driver and job info for the dispatch map.
{
"trips": [
{
"id": "trip-uuid-001",
"status": "en_route",
"started_at": "2026-04-14T08:15:00Z",
"latest_lat": 35.6820,
"latest_lng": 139.7695,
"latest_spd": 13.1,
"driver": {
"id": "driver-uuid",
"first_name": "Tanaka",
"last_name": "Yuki",
"avatar_url": "..."
},
"job": {
"id": "job-uuid-001",
"title": "Shibuya Ward - Burnable",
"address": "1-2-3 Shibuya, Shibuya-ku, Tokyo"
}
}
]
}
Same as the driver endpoint but accessible to dispatch users. Returns all pings for route visualization.
{
"pings": [
{ "lat": 35.6812, "lng": 139.7671, "spd": 0, "seq": 1, "ts": "..." },
{ "lat": 35.6815, "lng": 139.7680, "spd": 8.2, "seq": 2, "ts": "..." }
],
"trip": { "id": "trip-uuid-001", "status": "en_route" }
}
Dispatch: Collection Points
Manage pickup locations that can be added as stops on routes. Dispatch
Returns a paginated list of collection points. Use search to filter by name or address.
| Param | Type | Description |
|---|---|---|
search | string | Filter by name or address |
page | int | Page number (default 1) |
limit | int | Results per page (default 20) |
{
"collection_points": [
{
"id": "cp-uuid-001",
"name": "Shibuya Station Drop-off",
"address": "2-24-12 Shibuya, Shibuya-ku, Tokyo",
"area_name": "Shibuya",
"latitude": 35.6580,
"longitude": 139.7016,
"country_code": "JP",
"contact_name": "Sato Kenji",
"contact_phone": "+81-3-1234-5678",
"waste_type_id": "wt-uuid-001",
"notes": "Rear loading bay",
"created_at": "2026-03-01T09:00:00Z"
}
],
"total": 34
}
Returns a single collection point by ID.
{
"collection_point": {
"id": "cp-uuid-001",
"name": "Shibuya Station Drop-off",
"address": "2-24-12 Shibuya, Shibuya-ku, Tokyo",
"area_name": "Shibuya",
"latitude": 35.6580,
"longitude": 139.7016,
"country_code": "JP",
"contact_name": "Sato Kenji",
"contact_phone": "+81-3-1234-5678",
"waste_type_id": "wt-uuid-001",
"notes": "Rear loading bay",
"created_at": "2026-03-01T09:00:00Z"
}
}
Create a new collection point (pickup location).
{
"name": "Meguro Apartment Complex",
"address": "4-5-6 Meguro, Meguro-ku, Tokyo",
"area_name": "Meguro",
"latitude": 35.6339,
"longitude": 139.7154,
"country_code": "JP",
"contact_name": "Yamamoto Aiko",
"contact_phone": "+81-3-9876-5432",
"waste_type_id": "wt-uuid-002",
"notes": "Use rear entrance, buzzer #203"
}
{
"collection_point": {
"id": "cp-uuid-002",
"name": "Meguro Apartment Complex",
"address": "4-5-6 Meguro, Meguro-ku, Tokyo",
"area_name": "Meguro",
"latitude": 35.6339,
"longitude": 139.7154,
"country_code": "JP",
"contact_name": "Yamamoto Aiko",
"contact_phone": "+81-3-9876-5432",
"waste_type_id": "wt-uuid-002",
"notes": "Use rear entrance, buzzer #203",
"created_at": "2026-04-14T12:00:00Z"
}
}
Update an existing collection point. Accepts the same fields as create.
{
"name": "Meguro Apartment Complex (Updated)",
"contact_phone": "+81-3-1111-2222",
"notes": "New gate code: 4521"
}
{
"collection_point": {
"id": "cp-uuid-002",
"name": "Meguro Apartment Complex (Updated)",
"notes": "New gate code: 4521",
"...": "..."
}
}
Permanently delete a collection point. Fails if the point is currently used as a stop on an active route.
{
"message": "collection point deleted"
}
Dispatch: Routes
Create and manage collection routes with ordered stops. Routes can be scheduled and assigned to drivers. Dispatch
Create a new collection route.
{
"name": "Shibuya Morning Route",
"area_name": "Shibuya",
"country_code": "JP",
"schedule_days": ["monday", "wednesday", "friday"],
"schedule_time": "08:00",
"color": "#22c55e",
"notes": "Start from station, work north"
}
{
"route": {
"id": "route-uuid-001",
"name": "Shibuya Morning Route",
"area_name": "Shibuya",
"country_code": "JP",
"schedule_days": ["monday", "wednesday", "friday"],
"schedule_time": "08:00",
"color": "#22c55e",
"notes": "Start from station, work north",
"created_at": "2026-04-14T09:00:00Z"
}
}
Returns all collection routes.
{
"routes": [
{
"id": "route-uuid-001",
"name": "Shibuya Morning Route",
"area_name": "Shibuya",
"schedule_days": ["monday", "wednesday", "friday"],
"schedule_time": "08:00",
"color": "#22c55e",
"stop_count": 8,
"created_at": "2026-04-14T09:00:00Z"
}
]
}
Returns a single route including its ordered list of stops (collection points).
{
"route": {
"id": "route-uuid-001",
"name": "Shibuya Morning Route",
"area_name": "Shibuya",
"country_code": "JP",
"schedule_days": ["monday", "wednesday", "friday"],
"schedule_time": "08:00",
"color": "#22c55e",
"notes": "Start from station, work north",
"stops": [
{
"order": 1,
"collection_point": {
"id": "cp-uuid-001",
"name": "Shibuya Station Drop-off",
"address": "2-24-12 Shibuya, Shibuya-ku, Tokyo",
"latitude": 35.6580,
"longitude": 139.7016
}
},
{
"order": 2,
"collection_point": {
"id": "cp-uuid-003",
"name": "Harajuku Office Building",
"address": "1-1-1 Jingumae, Shibuya-ku, Tokyo",
"latitude": 35.6702,
"longitude": 139.7027
}
}
],
"created_at": "2026-04-14T09:00:00Z"
}
}
Update route metadata. Accepts the same fields as create (name, area_name, country_code, schedule_days, schedule_time, color, notes).
{
"name": "Shibuya Morning Route (Revised)",
"schedule_days": ["monday", "tuesday", "thursday"],
"schedule_time": "07:30"
}
{
"route": {
"id": "route-uuid-001",
"name": "Shibuya Morning Route (Revised)",
"schedule_days": ["monday", "tuesday", "thursday"],
"schedule_time": "07:30",
"...": "..."
}
}
Permanently delete a route and its stop associations.
{
"message": "route deleted"
}
Create a copy of an existing route including all its stops. The cloned route name is suffixed with " (Copy)".
{
"route": {
"id": "route-uuid-002",
"name": "Shibuya Morning Route (Copy)",
"area_name": "Shibuya",
"schedule_days": ["monday", "wednesday", "friday"],
"stops": [ ... ],
"created_at": "2026-04-14T12:00:00Z"
}
}
Replace the ordered list of stops for a route. Each stop references a collection point and has an explicit order.
{
"stops": [
{ "collection_point_id": "cp-uuid-001", "order": 1 },
{ "collection_point_id": "cp-uuid-003", "order": 2 },
{ "collection_point_id": "cp-uuid-005", "order": 3 }
]
}
{
"route": {
"id": "route-uuid-001",
"stops": [
{ "order": 1, "collection_point": { "id": "cp-uuid-001", "name": "Shibuya Station Drop-off" } },
{ "order": 2, "collection_point": { "id": "cp-uuid-003", "name": "Harajuku Office Building" } },
{ "order": 3, "collection_point": { "id": "cp-uuid-005", "name": "Yoyogi Park Bins" } }
]
}
}
Automatically reorder the route's stops to minimize total travel distance.
{
"route": {
"id": "route-uuid-001",
"name": "Shibuya Morning Route",
"stops": [
{ "order": 1, "collection_point": { "id": "cp-uuid-003", "name": "Harajuku Office Building" } },
{ "order": 2, "collection_point": { "id": "cp-uuid-005", "name": "Yoyogi Park Bins" } },
{ "order": 3, "collection_point": { "id": "cp-uuid-001", "name": "Shibuya Station Drop-off" } }
]
}
}
Dispatch: Route Scheduling
Schedule routes for specific dates and assign drivers. Generate daily schedules from route templates. Dispatch
Returns route assignments for a given date, including assigned drivers and completion status.
| Param | Type | Description |
|---|---|---|
date | string | Date to query (YYYY-MM-DD, default today) |
{
"assignments": [
{
"id": "ra-uuid-001",
"route_id": "route-uuid-001",
"route_name": "Shibuya Morning Route",
"driver_id": "driver-uuid",
"driver_name": "Tanaka Yuki",
"date": "2026-01-15",
"status": "pending",
"created_at": "2026-01-14T18:00:00Z"
}
]
}
Assign or reassign a driver to a route assignment, or update its status.
{
"driver_id": "driver-uuid",
"status": "in_progress"
}
{
"assignment": {
"id": "ra-uuid-001",
"route_id": "route-uuid-001",
"driver_id": "driver-uuid",
"status": "in_progress"
},
"message": "assignment updated"
}
Remove a route assignment. The route itself is not affected.
{
"message": "assignment deleted"
}
Generate route assignments for a specific date based on each route's schedule_days configuration. Routes whose schedule includes the target day-of-week will have assignments created.
{
"date": "2026-01-15"
}
{
"assignments": [
{
"id": "ra-uuid-010",
"route_id": "route-uuid-001",
"route_name": "Shibuya Morning Route",
"date": "2026-01-15",
"status": "pending"
}
],
"created": 5,
"message": "schedule generated"
}
Automatically assign available online drivers to unassigned route assignments for today (or a specified date). Drivers are matched based on proximity to the route's area.
{
"assigned": 4,
"unassigned": 1,
"message": "auto-assign complete"
}
Admin: Users
Manage platform users, roles, and access. Admin
{
"users": [
{
"id": "user-uuid",
"email": "driver@getgomi.xyz",
"first_name": "Tanaka",
"last_name": "Yuki",
"role": "driver",
"status": "online",
"created_at": "2026-01-15T00:00:00Z",
"last_login_at": "2026-04-14T08:00:00Z"
}
],
"total": 24
}
Returns full user profile including activity history summary.
{
"role": "dispatch"
}
Valid roles: driver, dispatch, admin
{
"status": "suspended"
}
Valid statuses: offline, online, suspended
Admin: Invites
{
"email": "newdriver@example.com",
"role": "driver"
}
{
"invite": {
"id": "invite-uuid",
"email": "newdriver@example.com",
"role": "driver",
"invited_by": "admin-uuid",
"created_at": "2026-04-14T10:00:00Z",
"expires_at": "2026-04-21T10:00:00Z"
}
}
Returns all pending and expired invites.
Revokes a pending invite so it can no longer be used.
Admin: Photo Search
Search and filter photos across all jobs and drivers. Returns EXIF metadata, driver info, and job title.
| Param | Type | Description |
|---|---|---|
job_id | string | Filter by job ID |
driver_id | string | Filter by driver ID |
phase | string | before or after |
taken_after | ISO 8601 | EXIF date lower bound |
taken_before | ISO 8601 | EXIF date upper bound |
page | int | Page number (default 1) |
limit | int | Results per page (default 50) |
# All photos by Harold from last week
GET /api/admin/photos?driver_id=c88d57e3&taken_after=2026-06-30T00:00:00Z
# All completion photos for a specific job
GET /api/admin/photos?job_id=abc123&phase=after
{
"photos": [
{
"id": "a1b2c3d4...",
"job_id": "c88d57e3...",
"job_title": "Shibuya pickup",
"driver_id": "d9f8e7c6...",
"driver_first_name": "Harold",
"driver_last_name": "M",
"driver_email": "mukuruharold94@gmail.com",
"phase": "after",
"public_url": "https://storage.googleapis.com/...",
"exif_taken_at": "2026-07-07T10:30:00Z",
"exif_lat": -1.2861,
"exif_lng": 36.8172,
"exif_make": "Samsung",
"exif_model": "Galaxy S24",
"size_bytes": 4521983,
"uploaded_at": "2026-07-07T10:31:15Z"
}
]
}
Admin: Audit & Keys
Returns authentication events (login, logout, key usage) for all users.
{
"events": [
{
"id": "evt-uuid",
"user_id": "user-uuid",
"email": "driver@getgomi.xyz",
"event_type": "login",
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0 ...",
"created_at": "2026-04-14T08:00:00Z"
}
],
"total": 1502
}
Returns all API keys across all users. Admins can see key metadata but not raw key values.
Admins can revoke any user's API key. The key is immediately invalidated.
Admins can create an API key on behalf of any user. The raw key is returned only once β store it securely.
{
"key": "gomi_d0ea77fe9a3b4c1e8f2d6a7b5c0e1f3a",
"id": "key-uuid-001",
"prefix": "gomi_d0ea",
"user_id": "user-uuid",
"user_name": "Tanaka Yuki"
}
Reference: Waste Types
Public
{
"waste_types": [
{
"id": "wt-uuid-001",
"name": "Burnable",
"name_local": "ηγγγ΄γ",
"color": "#ef4444",
"icon": "π₯",
"country_code": "JP"
},
{
"id": "wt-uuid-002",
"name": "Recyclable",
"name_local": "θ³ζΊγ΄γ",
"color": "#3b82f6",
"icon": "β»οΈ",
"country_code": "JP"
},
{
"id": "wt-uuid-003",
"name": "Non-Burnable",
"name_local": "ηγγͺγγ΄γ",
"color": "#6b7280",
"icon": "πͺ¨",
"country_code": "JP"
},
{
"id": "wt-uuid-004",
"name": "Oversized",
"name_local": "η²ε€§γ΄γ",
"color": "#f59e0b",
"icon": "π¦",
"country_code": "JP"
}
]
}
Status Flows
User Status
Job Status
Trip Status
Or from any state β cancelled
Error Handling
All errors return a consistent JSON structure with an appropriate HTTP status code:
{
"error": "Human-readable error message"
}
| Status | Meaning | Example |
|---|---|---|
400 | Bad Request | Missing required field, invalid format |
401 | Unauthorized | Missing or invalid API key / session |
403 | Forbidden | Insufficient role (driver accessing admin endpoint) |
404 | Not Found | Resource doesn't exist |
409 | Conflict | Already clocked in, trip already active |
500 | Server Error | Internal server error |
try {
await api('/api/driver/clock-in', {
method: 'PUT',
body: JSON.stringify({ latitude: 35.68, longitude: 139.77 }),
});
} catch (err) {
if (err.message === 'already clocked in') {
// Handle 409 conflict
console.log('Driver is already on shift');
} else {
console.error('Clock-in failed:', err.message);
}
}
Health Check
Public endpoint for monitoring. No authentication required.
{
"status": "ok"
}
Gomi API Documentation β Built for waste collection in Japan π―π΅
Β© 2026 getgomi.xyz