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 and require authentication via Bearer token (session cookie or API key).
API Client Helper
Use this minimal wrapper for all API calls:
const BASE = 'https://getgomi.xyz';
let API_KEY = '';
async function api(path, opts = {}) {
const res = await fetch(BASE + path, {
...opts,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'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();
}
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 }),
});
Authentication
Gomi uses Google OAuth 2.0 for user login. After authentication, you receive a session cookie. For programmatic access, create API keys.
Redirects the user to Google's OAuth consent screen. After authorization, the user is redirected back with a session cookie set.
| Param | Type | Description |
|---|---|---|
redirect | string | Optional URL to redirect after login |
Returns the currently authenticated user's profile information.
{
"user": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "driver@getgomi.xyz",
"first_name": "Tanaka",
"last_name": "Yuki",
"avatar_url": "https://lh3.googleusercontent.com/...",
"role": "driver",
"status": "online"
}
}
Destroys the current session and clears the authentication cookie.
{
"message": "logged out"
}
API Keys
API keys allow headless/mobile access. The raw key is shown only once at creation time.
{
"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"
}
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
}
}
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: 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"
}
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" }
}
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: 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.
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