Sign in with Google

Gomi API v1.0

Waste collection management platform β€” driver tracking, dispatch, and administration.

Base URL https://getgomi.xyz
Driver Dispatch Admin Public

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:

HTTP Headers
Authorization: Bearer <your-jwt-token-or-api-key>
Content-Type: application/json
Auth Methods β€” Which Token To Use
MethodToken FormatAccess LevelUse Case
JWTeyJhbGciOi... (long base64)βœ… All endpointsMobile apps, SPAs
API Keygomi_d0ea77fe... (starts with gomi_)⚠️ /api/driver/* onlySimple scripts, fallback

Both go in the same header: Authorization: Bearer <token>

API Client Helper (React Native / JavaScript)

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

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

Bash
# 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

TierHow You Get ItExpiresEndpoint Access
JWTPOST /auth/token (exchange session cookie)24 hoursAll endpoints
Session CookieSet automatically after Google OAuth login30 daysAll endpoints (browser only)
API KeyPOST /api/keysConfigurable/api/driver/* only
⚠️ API Key Limitations
API keys can only access /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.

1. Open WebView
/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>
React Native β€” Complete Login Flow
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

GET /auth/login Redirect to Google OAuth β–Ύ

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).

Note
Open this URL in a browser or WebView β€” do not call via fetch/AJAX.
Query Parameters
ParamTypeDescription
redirectstringOptional URL to redirect after login (default: welcome page)
POST /auth/token Exchange session cookie β†’ JWT β–Ύ

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.

πŸ“± Mobile apps β€” this is your most important endpoint
After Google login sets the 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.
Request
HTTP
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.

Response β€” 200 OK
JSON
// 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"
}
Fields
FieldTypeDescription
tokenstringJWT token β€” use this in Authorization: Bearer <token>
expires_inintSeconds until expiry (86400 = 24 hours)
token_typestringAlways "Bearer"
api_keystring?Only present on first call if user had no key. Raw API key for /api/driver/*. Save it β€” never shown again.
api_key_notestring?Explanation (present only when api_key is included)
Error Responses
StatusBodyMeaning
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)
POST /auth/refresh Refresh an expiring JWT β–Ύ

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.

Request
HTTP
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).

Response β€” 200 OK
JSON
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vy...",
  "expires_in": 86400,
  "token_type": "Bearer"
}
Error Responses
StatusBodyMeaning
401{"error": "not authenticated"}No token or token expired
403{"error": "JWT required for refresh"}Used session/API key instead of JWT
πŸ’‘ Tip
Refresh proactively when there's <1 hour left (check expires_in). If refresh fails, redirect the user back to the login WebView.
GET /auth/me Get current user β–Ύ

Returns the currently authenticated user's profile. Use this to verify the token works and get user info after login.

Request
HTTP
GET /auth/me HTTP/1.1
Host: getgomi.xyz
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Response β€” 200 OK
JSON
{
  "user": {
    "id": "c88d57e3b2c5a460e194c5a398a26b2c",
    "email": "driver@example.com",
    "first_name": "Harold",
    "last_name": "M",
    "avatar_url": "https://lh3.googleusercontent.com/...",
    "role": "driver",
    "status": "offline"
  }
}
POST /auth/logout End session β–Ύ

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.

Response β€” 200 OK
JSON
{
  "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).

How to send an API key

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...
POST /api/keys Create API key β–Ύ

Requires JWT or session cookie auth (API key auth cannot create new keys).

⚠️ Important
The raw key value is returned only once. Store it securely immediately.
Request Body
JSON
{
  "name": "phone",
  "expires_in": "90d"
}
Response β€” 201 Created
JSON
{
  "key": "gomi_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
  "api_key": {
    "id": "key-uuid-1234",
    "name": "phone",
    "key_prefix": "gomi_a1b2...s9t0"
  }
}
GET /api/keys List your API keys β–Ύ
Response β€” 200 OK
JSON
{
  "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"
    }
  ]
}
DELETE /api/keys/{id} Revoke an API key β–Ύ

Permanently revokes the API key. Any requests using this key will immediately fail with 401.

Response β€” 200 OK
JSON
{
  "message": "api key revoked"
}

Profile

Manage the authenticated user's own profile. Requires JWT or session cookie. Auth Required

GET /api/profile Get your profile β–Ύ

Returns the full profile of the authenticated user including location and timestamps.

Response β€” 200 OK
JSON
{
  "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"
  }
}
PUT /api/profile Update your profile β–Ύ
Request Body
JSON
{
  "first_name": "Harold",
  "last_name": "Mukuru"
}
Response β€” 200 OK
JSON
{
  "user": { ... },
  "message": "profile updated"
}

Driver: Shifts & Location

Drivers must clock in before accepting jobs. Location updates keep the dispatch dashboard current. Driver

PUT /api/driver/clock-in Start shift β–Ύ

Clocks the driver in and sets their status to online. Requires current GPS coordinates.

Request Body
JSON
{
  "latitude": 35.6812,
  "longitude": 139.7671
}
Response β€” 200 OK
JSON
{
  "user": {
    "id": "driver-uuid",
    "status": "online",
    "latitude": 35.6812,
    "longitude": 139.7671
  },
  "message": "clocked in"
}
PUT /api/driver/clock-out End shift β–Ύ
Request Body
JSON
{
  "notes": "Finished all scheduled routes"
}
Response β€” 200 OK
JSON
{
  "user": { "id": "driver-uuid", "status": "offline" },
  "message": "clocked out"
}
PUT /api/driver/location Update location β–Ύ

Pushes a background location update outside of active trips. Use this while online but not on an active trip.

Request Body
JSON
{
  "latitude": 35.6812,
  "longitude": 139.7671
}
GET /api/driver/dashboard Driver overview β–Ύ

Returns the driver's current status, active shift, and job summary.

Response β€” 200 OK
JSON
{
  "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
  }
}
GET /api/driver/shifts Shift history β–Ύ

Returns the driver's shift event history (clock-in / clock-out pairs). Filter by date range.

Query Parameters
ParamTypeDescription
fromstringStart date (RFC 3339 or YYYY-MM-DD)
tostringEnd date (RFC 3339 or YYYY-MM-DD)
Response β€” 200 OK
JSON
{
  "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"
    }
  ]
}
PUT /api/driver/status Set availability β–Ύ

Set the driver's availability status without a full clock-in/clock-out cycle.

Request Body
JSON
{
  "status": "online"
}

Accepted values: online, offline.

Response β€” 200 OK
JSON
{
  "user": {
    "id": "driver-uuid",
    "status": "online",
    "first_name": "Tanaka",
    "last_name": "Yuki"
  },
  "message": "status updated"
}
GET /api/driver/nearby-jobs Find nearby unassigned jobs β–Ύ

Returns unassigned jobs near the driver's current location. The driver must be online with a known location.

Query Parameters
ParamTypeDescription
limitintMax results (default 10)
Response β€” 200 OK
JSON
{
  "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

GET /api/driver/jobs List jobs β–Ύ
Query Parameters
ParamTypeDescription
statusstringFilter: assigned, in_progress, completed, pending
limitintMax results (default 50)
offsetintPagination offset
Response β€” 200 OK
JSON
{
  "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
}
GET /api/driver/jobs/{id} Get job detail β–Ύ
Response β€” 200 OK
JSON
{
  "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"
  }
}
POST /api/driver/jobs/{id}/start Start a job β–Ύ
Request Body
JSON
{
  "latitude": 35.6812,
  "longitude": 139.7671
}
Response β€” 200 OK
JSON
{
  "job": { "id": "job-uuid-001", "status": "in_progress" },
  "message": "job started"
}
POST /api/driver/jobs/{id}/complete Complete a job β–Ύ
Request Body
JSON
{
  "notes": "Collected 3 bags from front gate",
  "actual_volume": 3.0
}
Response β€” 200 OK
JSON
{
  "job": { "id": "job-uuid-001", "status": "completed" },
  "message": "job completed",
  "status": "completed"
}
GET /api/driver/stats Driver statistics β–Ύ
Response β€” 200 OK
JSON
{
  "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

POST /api/driver/jobs/{id}/photos Upload a job photo β–Ύ

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 upload
This endpoint uses multipart/form-data, not JSON. Send the image as a form file field.
Request β€” Multipart Form
FieldTypeRequiredDescription
filebinaryβœ…Image file (JPEG, PNG, WebP, HEIC). Max 50MB.
phasestring❌before or after (default: after)
latitudefloat❌Phone GPS latitude at upload time
longitudefloat❌Phone GPS longitude at upload time
cURL
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
React Native
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
Response β€” 201 Created
JSON
{
  "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
}
Error Responses
StatusMeaning
400Missing file, bad phase, unsupported image type
403Job not assigned to you
404Job not found
409Max 5 photos per phase reached, or upload window closed (48h)
413File too large (max 50MB)
503Photo uploads not configured (GCS not set up)
⚠️ EXIF Data Preserved
The original image is stored unmodified. EXIF GPS, timestamp, camera make/model are extracted and indexed in the database. Both phone GPS (from request) and EXIF GPS (from image) are stored separately for audit comparison.
GET /api/driver/jobs/{id}/photos List photos for a job β–Ύ

Returns all photos for a job, grouped by phase (before, then after).

Response β€” 200 OK
JSON
{
  "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

start β†’ en_route β†’ ping Γ— N β†’ arrive β†’ arrived β†’ complete
POST /api/driver/trips/start Start a new trip β–Ύ

Initiates a trip linked to a job. The driver's status changes to on_job.

Request Body
JSON
{
  "job_id": "job-uuid-001",
  "latitude": 35.6812,
  "longitude": 139.7671
}
Response β€” 201 Created
JSON
{
  "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
}
POST /api/driver/trips/{id}/ping Send GPS telemetry ping β–Ύ

Compact GPS telemetry payload. Send at the interval specified by next_ping_ms in the response. Short field names minimize bandwidth on mobile networks.

Request Body
JSON
{
  "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
}
Response β€” 200 OK
JSON
{
  "ok": true,
  "ping_count": 42,
  "stored": true,
  "next_ping_ms": 3000
}

Ping Field Reference

FieldTypeReqDescription
latfloatβœ…Latitude (-90 to 90)
lngfloatβœ…Longitude (-180 to 180)
spdfloatSpeed in meters per second
hdgfloatHeading 0–360Β° (0=North, clockwise)
accfloatGPS accuracy in meters (lower = better)
altfloatAltitude in meters above sea level
battfloatDevice battery level 0–100
tsstringISO 8601 timestamp from client clock
seqintMonotonic sequence number (for ordering)

Adaptive Ping Frequency

The server returns next_ping_ms but the client should also adapt based on speed:

SpeedInterval
> 60 km/h2 seconds
30–60 km/h3 seconds
10–30 km/h5 seconds
< 10 km/h10 seconds
< 2 km/h (stopped)15 seconds
POST /api/driver/trips/{id}/ping/batch Batch send pings β–Ύ

Send multiple queued pings at once. Useful when the device was offline or to reduce HTTP overhead. Accepts Content-Encoding: gzip for compressed payloads.

πŸ’‘ Compression
For large batches (50+ pings), gzip the request body and set Content-Encoding: gzip to reduce bandwidth by ~80%.
Request Body
JSON
{
  "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" }
  ]
}
Response β€” 200 OK
JSON
{
  "ok": true,
  "ping_count": 45,
  "stored": 3,
  "skipped": 0,
  "next_ping_ms": 3000
}
POST /api/driver/trips/{id}/arrive Mark arrival at destination β–Ύ
Request Body
JSON
{
  "latitude": 35.6580,
  "longitude": 139.7016
}
Response β€” 200 OK
JSON
{
  "trip": {
    "id": "trip-uuid-001",
    "status": "arrived",
    "arrived_at": "2026-04-14T08:32:00Z"
  }
}
POST /api/driver/trips/{id}/complete Complete trip β–Ύ
Request Body
JSON
{
  "latitude": 35.6580,
  "longitude": 139.7016,
  "notes": "All collected, area clean"
}
Response β€” 200 OK
JSON
{
  "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
  }
}
POST /api/driver/trips/{id}/cancel Cancel trip β–Ύ
Response β€” 200 OK
JSON
{
  "trip": {
    "id": "trip-uuid-001",
    "status": "cancelled",
    "cancelled_at": "2026-04-14T08:20:00Z"
  }
}
GET /api/driver/trips List driver's trips β–Ύ
Response β€” 200 OK
JSON
{
  "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
    }
  ]
}
GET /api/driver/trips/{id} Get trip detail β–Ύ
Response β€” 200 OK
JSON
{
  "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
  }
}
GET /api/driver/trips/{id}/pings Get trip pings β–Ύ

Returns all stored GPS pings for a trip, ordered by sequence number.

Response β€” 200 OK
JSON
{
  "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:

React Native / TypeScript
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

GET /api/dispatch/trips/live EventSource SSE stream β–Ύ

Opens a persistent SSE connection. Authenticate via query parameter since EventSource doesn't support custom headers.

Query Parameters
ParamTypeDescription
keystringAPI key: gomi_xxx...

Event Types

init
Connection established. Payload: { "active_trips": [...] } β€” current state snapshot.
trip_start
A driver started a new trip. Payload: { "trip": {...}, "driver": {...}, "job": {...} }
ping
GPS telemetry update. Payload: { "trip_id", "lat", "lng", "spd", "hdg", "ts" }
trip_arrive
Driver arrived at destination. Payload: { "trip": {...} }
trip_complete
Trip completed. Payload: { "trip": {...}, "stats": {...} }
trip_cancel
Trip cancelled. Payload: { "trip": {...} }

React Native EventSource Example

React Native / TypeScript
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

POST /api/dispatch/jobs Create job β–Ύ
Request Body
JSON
{
  "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"
}
Response β€” 201 Created
JSON
{
  "job": {
    "id": "job-uuid-002",
    "title": "Meguro Ward - Recyclables",
    "status": "pending",
    "created_at": "2026-04-14T10:00:00Z"
  }
}
GET /api/dispatch/jobs List all jobs β–Ύ
Query Parameters
ParamTypeDescription
statusstringFilter by status
driver_idstringFilter by assigned driver
datestringFilter by scheduled date (YYYY-MM-DD)
limitintMax results (default 50)
offsetintPagination offset
Response β€” 200 OK
JSON
{
  "jobs": [ { "id": "...", "title": "...", "status": "pending", ... } ],
  "total": 47
}
GET /api/dispatch/jobs/{id} Get job detail β–Ύ

Returns full job details including assigned driver information.

PUT /api/dispatch/jobs/{id} Update job β–Ύ

Update any mutable job fields (title, address, coordinates, notes, priority, scheduled_date, estimated_volume).

POST /api/dispatch/jobs/{id}/assign Assign driver β–Ύ
Request Body
JSON
{
  "driver_id": "driver-uuid"
}
Response β€” 200 OK
JSON
{
  "job": { "id": "job-uuid-002", "status": "assigned", "driver_id": "driver-uuid" },
  "message": "job assigned"
}
POST /api/dispatch/jobs/{id}/cancel Cancel job β–Ύ
Request Body
JSON
{
  "notes": "Customer cancelled pickup"
}
POST /api/dispatch/jobs/{id}/reassign Reassign job to different driver β–Ύ

Reassign an existing job to a different driver. The job must not be completed or cancelled.

Request Body
JSON
{
  "driver_id": "new-driver-uuid"
}
Response β€” 200 OK
JSON
{
  "job": {
    "id": "job-uuid-002",
    "status": "assigned",
    "driver_id": "new-driver-uuid"
  },
  "message": "job reassigned"
}
POST /api/dispatch/jobs/auto-assign Auto-assign pending jobs β–Ύ

Automatically assign all pending jobs to the nearest available online drivers based on location proximity.

Response β€” 200 OK
JSON
{
  "assigned": 12,
  "skipped": 3,
  "message": "auto-assign complete"
}
GET /api/dispatch/jobs/{id}/events Job event audit log β–Ύ

Returns the full event history for a job β€” creation, assignment, status changes, reassignment, completion.

Response β€” 200 OK
JSON
{
  "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

GET /api/dispatch/trips/active List active trips β–Ύ

Returns all currently active trips with embedded driver and job info for the dispatch map.

Response β€” 200 OK
JSON
{
  "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"
      }
    }
  ]
}
GET /api/dispatch/trips/{id}/pings Get trip pings (dispatch) β–Ύ

Same as the driver endpoint but accessible to dispatch users. Returns all pings for route visualization.

Response β€” 200 OK
JSON
{
  "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

GET /api/dispatch/collection-points List collection points β–Ύ

Returns a paginated list of collection points. Use search to filter by name or address.

Query Parameters
ParamTypeDescription
searchstringFilter by name or address
pageintPage number (default 1)
limitintResults per page (default 20)
Response β€” 200 OK
JSON
{
  "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
}
GET /api/dispatch/collection-points/{id} Get collection point β–Ύ

Returns a single collection point by ID.

Response β€” 200 OK
JSON
{
  "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"
  }
}
POST /api/dispatch/collection-points Create collection point β–Ύ

Create a new collection point (pickup location).

Request Body
JSON
{
  "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"
}
Response β€” 201 Created
JSON
{
  "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"
  }
}
PUT /api/dispatch/collection-points/{id} Update collection point β–Ύ

Update an existing collection point. Accepts the same fields as create.

Request Body
JSON
{
  "name": "Meguro Apartment Complex (Updated)",
  "contact_phone": "+81-3-1111-2222",
  "notes": "New gate code: 4521"
}
Response β€” 200 OK
JSON
{
  "collection_point": {
    "id": "cp-uuid-002",
    "name": "Meguro Apartment Complex (Updated)",
    "notes": "New gate code: 4521",
    "...": "..."
  }
}
DELETE /api/dispatch/collection-points/{id} Delete collection point β–Ύ

Permanently delete a collection point. Fails if the point is currently used as a stop on an active route.

Response β€” 200 OK
JSON
{
  "message": "collection point deleted"
}

Dispatch: Routes

Create and manage collection routes with ordered stops. Routes can be scheduled and assigned to drivers. Dispatch

POST /api/dispatch/routes Create route β–Ύ

Create a new collection route.

Request Body
JSON
{
  "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"
}
Response β€” 201 Created
JSON
{
  "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"
  }
}
GET /api/dispatch/routes List routes β–Ύ

Returns all collection routes.

Response β€” 200 OK
JSON
{
  "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"
    }
  ]
}
GET /api/dispatch/routes/{id} Get route with stops β–Ύ

Returns a single route including its ordered list of stops (collection points).

Response β€” 200 OK
JSON
{
  "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"
  }
}
PUT /api/dispatch/routes/{id} Update route β–Ύ

Update route metadata. Accepts the same fields as create (name, area_name, country_code, schedule_days, schedule_time, color, notes).

Request Body
JSON
{
  "name": "Shibuya Morning Route (Revised)",
  "schedule_days": ["monday", "tuesday", "thursday"],
  "schedule_time": "07:30"
}
Response β€” 200 OK
JSON
{
  "route": {
    "id": "route-uuid-001",
    "name": "Shibuya Morning Route (Revised)",
    "schedule_days": ["monday", "tuesday", "thursday"],
    "schedule_time": "07:30",
    "...": "..."
  }
}
DELETE /api/dispatch/routes/{id} Delete route β–Ύ

Permanently delete a route and its stop associations.

Response β€” 200 OK
JSON
{
  "message": "route deleted"
}
POST /api/dispatch/routes/{id}/clone Clone route β–Ύ

Create a copy of an existing route including all its stops. The cloned route name is suffixed with " (Copy)".

Response β€” 201 Created
JSON
{
  "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"
  }
}
PUT /api/dispatch/routes/{id}/stops Set route stops β–Ύ

Replace the ordered list of stops for a route. Each stop references a collection point and has an explicit order.

Request Body
JSON
{
  "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 }
  ]
}
Response β€” 200 OK
JSON
{
  "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" } }
    ]
  }
}
POST /api/dispatch/routes/{id}/optimize Optimize stop order β–Ύ

Automatically reorder the route's stops to minimize total travel distance.

Response β€” 200 OK
JSON
{
  "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

GET /api/dispatch/route-assignments List route assignments β–Ύ

Returns route assignments for a given date, including assigned drivers and completion status.

Query Parameters
ParamTypeDescription
datestringDate to query (YYYY-MM-DD, default today)
Response β€” 200 OK
JSON
{
  "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"
    }
  ]
}
PUT /api/dispatch/route-assignments/{id} Update route assignment β–Ύ

Assign or reassign a driver to a route assignment, or update its status.

Request Body
JSON
{
  "driver_id": "driver-uuid",
  "status": "in_progress"
}
Response β€” 200 OK
JSON
{
  "assignment": {
    "id": "ra-uuid-001",
    "route_id": "route-uuid-001",
    "driver_id": "driver-uuid",
    "status": "in_progress"
  },
  "message": "assignment updated"
}
DELETE /api/dispatch/route-assignments/{id} Delete route assignment β–Ύ

Remove a route assignment. The route itself is not affected.

Response β€” 200 OK
JSON
{
  "message": "assignment deleted"
}
POST /api/dispatch/routes/generate-schedule Generate daily schedule β–Ύ

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.

Request Body
JSON
{
  "date": "2026-01-15"
}
Response β€” 201 Created
JSON
{
  "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"
}
POST /api/dispatch/routes/auto-assign Auto-assign drivers to routes β–Ύ

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.

Response β€” 200 OK
JSON
{
  "assigned": 4,
  "unassigned": 1,
  "message": "auto-assign complete"
}

Admin: Users

Manage platform users, roles, and access. Admin

GET /api/admin/users List all users β–Ύ
Response β€” 200 OK
JSON
{
  "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
}
GET /api/admin/users/{id} Get user detail β–Ύ

Returns full user profile including activity history summary.

PATCH /api/admin/users/{id}/role Change user role β–Ύ
Request Body
JSON
{
  "role": "dispatch"
}

Valid roles: driver, dispatch, admin

PATCH /api/admin/users/{id}/status Change user status β–Ύ
Request Body
JSON
{
  "status": "suspended"
}

Valid statuses: offline, online, suspended

Admin: Invites

POST /api/admin/invites Send invite β–Ύ
Request Body
JSON
{
  "email": "newdriver@example.com",
  "role": "driver"
}
Response β€” 201 Created
JSON
{
  "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"
  }
}
GET /api/admin/invites List invites β–Ύ

Returns all pending and expired invites.

DELETE /api/admin/invites/{id} Revoke invite β–Ύ

Revokes a pending invite so it can no longer be used.

Admin: Photo Search

GET /api/admin/photos Search all job photos β–Ύ

Search and filter photos across all jobs and drivers. Returns EXIF metadata, driver info, and job title.

Query Parameters
ParamTypeDescription
job_idstringFilter by job ID
driver_idstringFilter by driver ID
phasestringbefore or after
taken_afterISO 8601EXIF date lower bound
taken_beforeISO 8601EXIF date upper bound
pageintPage number (default 1)
limitintResults per page (default 50)
Example
# 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
Response β€” 200 OK
JSON
{
  "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

GET /api/admin/audit/auth Auth audit log β–Ύ

Returns authentication events (login, logout, key usage) for all users.

Response β€” 200 OK
JSON
{
  "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
}
GET /api/admin/keys List all API keys β–Ύ

Returns all API keys across all users. Admins can see key metadata but not raw key values.

DELETE /api/admin/keys/{id} Revoke any API key β–Ύ

Admins can revoke any user's API key. The key is immediately invalidated.

POST /api/admin/users/{id}/create-key Create API key for any user β–Ύ

Admins can create an API key on behalf of any user. The raw key is returned only once β€” store it securely.

Response β€” 201 Created
JSON
{
  "key": "gomi_d0ea77fe9a3b4c1e8f2d6a7b5c0e1f3a",
  "id": "key-uuid-001",
  "prefix": "gomi_d0ea",
  "user_id": "user-uuid",
  "user_name": "Tanaka Yuki"
}

Reference: Waste Types

Public

GET /api/waste-types List waste types β–Ύ
Response β€” 200 OK
JSON
{
  "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

offline β†’ online β†’ on_job β†’ online β†’ offline

Job Status

pending β†’ assigned β†’ in_progress β†’ completed

Trip Status

en_route β†’ arrived β†’ completed

Or from any state β†’ cancelled

Error Handling

All errors return a consistent JSON structure with an appropriate HTTP status code:

Error Response
{
  "error": "Human-readable error message"
}
StatusMeaningExample
400Bad RequestMissing required field, invalid format
401UnauthorizedMissing or invalid API key / session
403ForbiddenInsufficient role (driver accessing admin endpoint)
404Not FoundResource doesn't exist
409ConflictAlready clocked in, trip already active
500Server ErrorInternal server error
Error Handling Example
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

GET /health Service health β–Ύ

Public endpoint for monitoring. No authentication required.

Response β€” 200 OK
JSON
{
  "status": "ok"
}

Gomi API Documentation β€” Built for waste collection in Japan πŸ‡―πŸ‡΅

Β© 2026 getgomi.xyz