Skip to main content

RoutelyOS API Reference

Complete documentation for the RoutelyOS field service management platform API. 40 endpoints across 10 categories. All request bodies are validated with Zod schemas.

Base URL: https://routelyos.comOpenAPI 3.1 Spec

Authentication

Public endpoints require no authentication but are IP rate-limited.

HQ endpoints (requireHQAuth) require a valid Supabase access token sent as Authorization: Bearer <token>. A session cookie is accepted as a fallback, but this app does not issue one — the client keeps the session in localStorage.

Cron endpoints require Authorization: Bearer $CRON_SECRET header.

Stripe webhook verifies the stripe-signature header against STRIPE_WEBHOOK_SECRET.

Error Format

All errors follow a consistent JSON structure:

{
  "ok": false,
  "error": "Human-readable error message",
  "details": ["optional", "validation", "errors"]
}

HTTP status codes: 400 (validation), 401 (unauthorized), 404 (not found), 429 (rate limited), 500 (server error), 502/504 (upstream).

Public

No authentication required. Rate-limited by IP.

GET/api/geocode
Public20 req/min per IP

Forward geocode an address query via Nominatim. Returns lat/lng results.

Request

Query params:
  q: string  (1-500 chars, required)

Response

{
  "ok": true,
  "data": {
    "results": [
      { "lat": 38.88, "lng": -77.10, "label": "..." }
    ]
  }
}

Example

curl "https://routelyos.com/api/geocode?q=Alexandria+VA"
POST/api/coverage
Public15 req/min per IP

Generate drive-time isochrone polygons via Valhalla. Returns real GeoJSON coverage areas.

Request

{
  "lat": number       // -90 to 90 (required)
  "lng": number       // -180 to 180 (required)
  "maxDrive": number  // 1-180 minutes (default: 60)
  "contours": number[] // optional custom contour values
}

Response

{
  "ok": true,
  "data": {
    "polygons": [{ "minutes": 60, "coords": [[lat,lng],...] }],
    "source": "valhalla",
    "generatedAt": "ISO string",
    "requestedMinutes": 60,
    "actualMinutes": 60
  }
}

Example

curl -X POST https://routelyos.com/api/coverage \
  -H "Content-Type: application/json" \
  -d '{"lat":38.88,"lng":-77.10,"maxDrive":45}'
POST/api/isochrone
Public30 req/min per IP

Raw Valhalla isochrone proxy. Returns GeoJSON feature collection for drive-time contours.

Request

{
  "lat": number        // -90 to 90 (required)
  "lng": number        // -180 to 180 (required)
  "time": number       // 1-180 minutes (default: 60)
  "contours": [{       // optional, 1-10 items
    "time": number,
    "color": string
  }]
}

Response

GeoJSON FeatureCollection from Valhalla

Example

curl -X POST https://routelyos.com/api/isochrone \
  -H "Content-Type: application/json" \
  -d '{"lat":38.88,"lng":-77.10,"time":30}'
POST/api/vitals
Public60 req/min per IP

Store web vitals metrics (LCP, FID, CLS, etc.) to Supabase.

Request

{
  "metric": string  // max 100 chars (default: "unknown")
  "value": number   // (default: 0)
  "page": string    // max 500 chars (default: "/")
  "id": string?     // max 200 chars, nullable
}

Response

{ "ok": true, "data": { "received": true } }
GET/api/vitals
Public

Health check. Returns status and version.

Response

{
  "ok": true,
  "data": {
    "status": "ok",
    "timestamp": "ISO string",
    "version": "1.0.0"
  }
}

Example

curl https://routelyos.com/api/vitals
GET/api/reports
Public30 req/min per IP

Generate business reports (P&L, jobs, workers, revenue, coverage).

Request

Query params:
  type: "pnl" | "jobs" | "workers" | "revenue" | "coverage" (default: "pnl")
  franchise: string?  (max 200 chars)

Response

{
  "ok": true,
  "data": {
    "type": "pnl",
    "franchise": "all",
    "generatedAt": "ISO string",
    "data": { ... }
  }
}

Customer Portal

Customer-facing endpoints for email verification and booking cancellation.

POST/api/customer/verify
Public5 req/min per IP

Two-step email verification. First call (email only) sends a 6-digit code. Second call (email + code) verifies it.

Request

Step 1 — Send code:
{
  "email": string  // valid email, max 254 chars
}

Step 2 — Verify code:
{
  "email": string,
  "code": string   // exactly 6 digits
}

Response

// Send: { "ok": true, "data": { "sent": true } }
// Verify: { "ok": true, "data": { "verified": true } }

Example

# Send code
curl -X POST https://routelyos.com/api/customer/verify \
  -H "Content-Type: application/json" \
  -d '{"email":"customer@example.com"}'

# Verify code
curl -X POST https://routelyos.com/api/customer/verify \
  -H "Content-Type: application/json" \
  -d '{"email":"customer@example.com","code":"123456"}'
POST/api/customer/cancel
Public5 req/min per IP

Cancel a booking. Processes Stripe refund if payment was charged, cancels setup intent for card-on-file. Sends email/SMS notification.

Request

{
  "jobId": string          // required, max 200 chars
  "paymentId": string?     // Stripe payment intent or setup intent ID
  "paymentStatus": string? // "charged" | "card_on_file" | "pending"
  "amount": number?        // 0-99999
  "email": string?         // for cancellation notification
  "phone": string?         // for SMS notification
  "type": string?          // service type label
  "dateISO": string?       // scheduled date
}

Response

{
  "ok": true,
  "data": {
    "refunded": boolean,
    "refundId": string?,
    "setupIntentCancelled": boolean,
    "message": string
  }
}

Example

curl -X POST https://routelyos.com/api/customer/cancel \
  -H "Content-Type: application/json" \
  -d '{"jobId":"abc-123","paymentId":"pi_xxx","paymentStatus":"charged"}'

Jobs

Job creation endpoint. Inserts into Supabase and triggers welcome email.

POST/api/jobs/create
Public10 req/min per IP

Create a new service job. Inserts into Supabase, logs activity, fires welcome email to customer.

Request

{
  "customerName": string    // required, max 200
  "customerEmail": string?  // valid email, max 200
  "customerPhone": string?  // max 50, digits/spaces/dashes
  "address": string         // required, max 500
  "type": string            // required, service type, max 200
  "price": number           // 0-99999, required
  "preferredDate": string?  // ISO date string, max 30
  "preferredTime": string?  // e.g. "9:00 AM - 12:00 PM", max 30
  "addOns": string[]        // optional array of add-on labels
  "franchiseId": string?    // max 100
  "notes": string?          // max 1000
  "paymentId": string?      // Stripe payment intent ID, max 200
  "paymentStatus": string?  // max 50
  "gutterLength": number?
  "stories": number?
}

Response

{
  "ok": true,
  "data": {
    "job": { /* full Supabase row */ },
    "message": "Job created successfully"
  }
}

Example

curl -X POST https://routelyos.com/api/jobs/create \
  -H "Content-Type: application/json" \
  -d '{
    "customerName": "John Doe",
    "address": "123 Main St, Alexandria VA",
    "type": "Gutter Cleaning",
    "price": 249
  }'

Stripe Payments

Checkout sessions, setup intents, and webhook handling.

POST/api/stripe/checkout
Public10 req/min per IP

Create a Stripe Checkout Session to pay for an existing booking. Returns the session URL for redirect. Never creates a job.

Request

{
  "amount": number          // 2500-999999 (in cents), required; must match the job's price
  "jobId": string           // uuid of an existing, priced job, required
  "customerName": string?   // max 200
  "customerEmail": string?  // valid email, max 254
  "jobDescription": string? // max 500
  "returnUrl": string?      // max 2000, must be same-origin
}

Response

{
  "ok": true,
  "data": {
    "url": "https://checkout.stripe.com/...",
    "sessionId": "cs_xxx"
  }
}

Example

curl -X POST https://routelyos.com/api/stripe/checkout \
  -H "Content-Type: application/json" \
  -d '{"amount":24900,"jobId":"<job uuid>","customerEmail":"customer@example.com","jobDescription":"Gutter Cleaning"}'
POST/api/stripe-webhook
Stripe Signature60 req/min per IP

Stripe webhook receiver. Verifies signature, processes events (payment_intent.succeeded, payment_intent.payment_failed, etc.), triggers franchise payouts, emails, and invoice generation.

Request

Raw Stripe event JSON (max 64 KB).
Requires stripe-signature header.

Response

{ "received": true, "eventId": "evt_xxx" }

Stripe Connect

Franchise onboarding, payout status, and transfer dashboard. All require HQ authentication.

POST/api/stripe/connect/onboard
requireHQAuth10 req/min per IP

Initiate Stripe Connect onboarding for a franchise. Creates a Standard account and returns the onboarding URL.

Request

{
  "franchiseId": string  // required, max 100
}

Response

{
  "ok": true,
  "data": { "url": "https://connect.stripe.com/setup/..." }
}
GET/api/stripe/connect/status
requireHQAuth30 req/min per IP

Check Stripe Connect onboarding status for a franchise. Syncs status back to Supabase.

Request

Query params:
  franchiseId: string  (required, max 100)

Response

{
  "ok": true,
  "data": {
    "connected": boolean,
    "accountId": string | null,
    "chargesEnabled": boolean,
    "payoutsEnabled": boolean,
    "detailsSubmitted": boolean,
    "onboardingComplete": boolean
  }
}
GET/api/stripe/connect/dashboard
requireHQAuth30 req/min per IP

Franchise transfer history and payout summary: completed and pending transfers. No platform-fee total — HQ's share is set in Revenue splits (franchise_splits).

Request

Query params:
  franchiseId: string  (required, max 100)

Response

{
  "ok": true,
  "data": {
    "transfers": [...],
    "totalPaid": number,
    "totalPending": number,
    "balance": number
  }
}
GET/api/stripe/connect/return
Public20 req/min per IP

OAuth return handler after Stripe Connect onboarding. Updates franchise status in Supabase and redirects to admin dashboard.

Request

Query params:
  account_id: string

Response

302 Redirect to /admin?tab=franchise&connect={status}

Invoices

Invoice generation and delivery. Requires HQ authentication.

POST/api/invoices/generate
requireHQAuth20 req/min per IP

Generate an invoice from a job ID. Fetches job from Supabase, builds line items, stores invoice record.

Request

{
  "jobId": string  // required, max 200
}

Response

{
  "ok": true,
  "data": {
    "invoice": { /* Supabase row */ },
    "invoiceData": { /* structured invoice */ },
    "html": "<div>...",
    "message": "Invoice generated"
  }
}
POST/api/invoices/send
requireHQAuth15 req/min per IP

Send an existing invoice via email or SMS. Updates invoice status to 'sent'.

Request

{
  "invoiceId": string  // required, max 200
  "method": "email" | "sms"
}

Response

{
  "ok": true,
  "data": {
    "invoice": { /* updated row */ },
    "message": "Invoice sent via email"
  }
}

Billing

Recurring billing schedule management. Requires HQ authentication.

GET/api/billing/recurring
requireHQAuth30 req/min per IP

List recurring billing schedules. Filter by franchise and active status.

Request

Query params:
  franchiseId: string?  (max 200)
  active: "true" | "false"?

Response

{
  "ok": true,
  "data": {
    "schedules": [...],
    "count": number
  }
}
POST/api/billing/recurring
requireHQAuth30 req/min per IP

Create a recurring billing schedule for a customer. Links to a recurring job template.

Request

{
  "recurringScheduleId": string  // required, max 200
  "franchiseId": string?         // max 200
  "customerId": string           // required, max 200
  "customerName": string         // required, max 200
  "customerEmail": string?       // valid email, max 200
  "serviceType": string          // required, max 200
  "intervalMonths": number       // 1-24 integer
  "price": number                // 0-99999
  "autoCharge": boolean          // default: false
  "stripeCustomerId": string?    // max 200
  "nextBillingDate": string?     // ISO date, max 30
}

Response

{
  "ok": true,
  "data": {
    "schedule": { /* Supabase row */ },
    "message": "Recurring billing schedule created"
  }
}

Campaigns

Bulk messaging to customers. Requires HQ authentication.

POST/api/campaigns/send
requireHQAuth5 req/min per IP

Send bulk email/SMS campaign to customers from completed jobs. Supports audience targeting (all, new, returning).

Request

{
  "type": "email" | "sms" | "both"
  "targetAudience": "all" | "new" | "returning"  // default: "all"
  "messageTemplate": string   // required, max 5000 chars
                               // supports {name} and {email} placeholders
  "subject": string?          // email subject, max 200
  "franchiseId": string?      // filter by franchise, max 100
}

Response

{
  "ok": true,
  "data": {
    "sent": number,
    "failed": number,
    "total": number,
    "errors": string[]?
  }
}

Cron Jobs

Scheduled tasks triggered by Vercel Cron. Authenticated via CRON_SECRET Bearer token.

GET/api/cron/auto-dispatch
CRON_SECRET

Auto-dispatch unscheduled jobs to best-fit workers. Runs every 15 min, Mon-Sat 7 AM - 6 PM.

Request

Authorization: Bearer {CRON_SECRET}

Response

{
  "ok": true,
  "data": {
    "success": true,
    "assigned": number,
    "escalated": number,
    "skipped": number,
    "actions": [{ "type": "assigned", "jobId": "...", "worker": "..." }],
    "timestamp": "ISO string"
  }
}
GET/api/cron/onboarding-emails
CRON_SECRET2 req/min

Send onboarding email sequences. Franchise welcome, setup guide, first-job guide, customer follow-up. Daily at 9 AM UTC.

Response

{
  "ok": true,
  "data": {
    "success": true,
    "emailsSent": number,
    "emailsFailed": number,
    "details": [{ "type": "...", "to": "...", "status": "sent" }],
    "timestamp": "ISO string"
  }
}
GET/api/cron/recurring-jobs
CRON_SECRET

Generate upcoming jobs from recurring templates. Creates invoices for auto-billed schedules. Daily at 6 AM UTC.

Request

Authorization: Bearer {CRON_SECRET}
Optional POST body:
{
  "templates": any[]     // recurring job templates
  "existingJobs": any[]  // current jobs for dedup
}

Response

{
  "ok": true,
  "data": {
    "success": true,
    "generated": number,
    "jobs": [...],
    "skipped": number,
    "projectedRevenue": number,
    "invoicesCreated": number,
    "timestamp": "ISO string"
  }
}
GET/api/cron/webhook-retry
CRON_SECRET

Retry failed Stripe webhook events from the persistent queue. Every 5 minutes.

Response

{
  "ok": true,
  "data": {
    "success": true,
    "processed": number,
    "succeeded": number,
    "failed": number,
    "results": [{ "id": "...", "type": "...", "success": boolean }],
    "timestamp": "ISO string"
  }
}

Admin / Dev

Internal developer tools. All require HQ authentication (Supabase session).

GET/api/dev/health
Public

System health check. Tests Supabase, Stripe, email, Sentry, GitHub, Vercel connectivity with latency measurements. No auth required.

Response

{
  "ok": true,
  "data": {
    "status": "healthy" | "degraded" | "unhealthy",
    "timestamp": "ISO string",
    "version": "1.0.0",
    "checks": {
      "database": { "status": "up", "latencyMs": 45 },
      "stripe": { "status": "configured" },
      "email": { "status": "configured" },
      "webhookQueue": { "depth": 0 },
      "memory": { "heapUsedMB": 32 },
      ...
    }
  }
}

Example

curl https://routelyos.com/api/dev/health
GET/api/dev/metrics
requireHQAuth30 req/min per IP

Platform metrics dashboard. Job counts by status, worker availability, total revenue.

Response

{
  "ok": true,
  "data": {
    "jobs": { "total": 150, "pending": 12, "completed": 98, ... },
    "workers": { "total": 8, "available": 5 },
    "revenue": { "total": 24500, "completedJobs": 98 }
  }
}
GET/api/dev/insights
requireHQAuth10 req/min per IP

System improvement recommendations. Analyzes API speed, data quality, config, cancellation trends. Returns scored insights.

Response

{
  "ok": true,
  "data": {
    "insights": [
      {
        "severity": "high" | "medium" | "low",
        "category": "performance" | "data" | "security" | "config",
        "title": "...",
        "detail": "...",
        "action": "..."
      }
    ],
    "score": 85,
    "analyzedAt": "ISO string"
  }
}
GET/api/dev/stripe
requireHQAuth15 req/min per IP

Stripe financial summary. Balance, revenue (today/week/all-time), recent events.

Response

{
  "ok": true,
  "data": {
    "balance": { "available": [...], "pending": [...] },
    "revenue": { "today": 4900, "week": 24500, "allTime": 128000 },
    "recentEvents": [...],
    "chargeCount": 20
  }
}
GET/api/dev/github
requireHQAuth15 req/min per IP

GitHub repo summary. Recent commits, pull requests, repo stats.

Response

{
  "ok": true,
  "data": {
    "commits": [{ "sha": "abc1234", "message": "...", "author": "..." }],
    "pulls": [{ "number": 42, "title": "...", "state": "open" }],
    "repo": { "name": "...", "stars": 0, "openIssues": 3 }
  }
}
GET/api/dev/vercel
requireHQAuth15 req/min per IP

Recent Vercel deployments. Includes production URL, commit messages, deployment state.

Response

{
  "ok": true,
  "data": {
    "deployments": [{ "id": "...", "url": "...", "state": "READY" }],
    "productionUrl": "https://..."
  }
}
POST/api/dev/activity
requireHQAuth30 req/min per IP

Log a developer activity event to Supabase.

Request

{
  "type": string    // max 500
  "actor": string   // max 500
  "summary": string // max 500
  "metadata": {}?   // optional key-value pairs
}

Response

{ "ok": true, "data": { "ok": true, "stored": true } }
GET/api/dev/activity
requireHQAuth30 req/min per IP

Retrieve last 50 developer activity events.

Response

{ "ok": true, "data": { "events": [...], "stored": true } }
POST/api/dev/errors
requireHQAuth30 req/min per IP

Log a client-side error to Supabase.

Request

{
  "message": string    // max 500
  "stack": string?     // max 5000
  "page": string?      // max 500
  "userAgent": string? // max 500
  "metadata": {}?
}

Response

{ "ok": true, "data": { "ok": true, "stored": true } }
GET/api/dev/errors
requireHQAuth30 req/min per IP

Retrieve recent errors with grouping by message.

Response

{
  "ok": true,
  "data": {
    "errors": [...],
    "grouped": [{ "message": "...", "count": 5, "firstSeen": "...", "lastSeen": "..." }],
    "total": 42
  }
}
POST/api/dev/email-test
requireHQAuth3 req/min per IP

Send a test email to verify delivery pipeline. Detects provider (Resend/SendGrid).

Request

{
  "to": string               // valid email, max 254
  "template": "booking" | "completion" | "test"  // default: "test"
}

Response

{
  "ok": true,
  "data": {
    "provider": "resend",
    "status": "sent",
    "messageId": "...",
    "to": "...",
    "template": "test"
  }
}
GET/api/dev/email-test
Public

Check which email provider is configured.

Response

{
  "ok": true,
  "data": {
    "provider": "resend" | "sendgrid" | "demo",
    "configured": boolean,
    "fromEmail": "noreply@routelyos.com"
  }
}
POST/api/dev/notify
requireHQAuth5 req/min per IP

Send a styled notification (handoff, deploy, error, revenue) via email or SMS.

Request

{
  "type": "handoff" | "deploy" | "error" | "revenue"
  "to": string          // email or phone number, max 254
  "data": {}            // template-specific data
  "channel": "email" | "sms"  // default: "email"
}

Response

{
  "ok": true,
  "data": {
    "success": boolean,
    "messageId": string?,
    "channel": "email",
    "type": "deploy"
  }
}
POST/api/dev/invoice
requireHQAuth15 req/min per IP

Generate an invoice preview from job data. Optionally emails it.

Request

{
  "job": { "id": string | number, ... }  // job data (passthrough)
  "franchiseName": string?               // max 200
  "sendEmail": boolean?                  // default: false
}

Response

{
  "ok": true,
  "data": {
    "invoice": { /* structured invoice data */ },
    "html": "<div>...",
    "emailed": boolean
  }
}
POST/api/dev/location
requireHQAuth30 req/min per IP

Update a worker's real-time GPS location.

Request

{
  "workerId": string   // max 100
  "lat": number        // -90 to 90
  "lng": number        // -180 to 180
  "accuracy": number?  // 0-100000
  "heading": number?   // 0-360
  "speed": number?     // 0-500
}

Response

{ "ok": true, "data": { "ok": true } }
GET/api/dev/location
requireHQAuth30 req/min per IP

Get all worker GPS locations for the dispatch map.

Response

{
  "ok": true,
  "data": { "locations": [{ "worker_id": "...", "lat": 38.88, "lng": -77.10, ... }] }
}

Routely API v1.0.0 — 40 endpoints — Generated from Zod schemas