Path-versioned routing optimization API. Bearer-token auth. Prepaid metered. Stable contract: /api/v1/* will not break in incompatible ways without 90 days deprecation notice via Deprecation header.
# 1. Provision a key. Prepay via card, USDC on Base, or OWN credits. curl -X POST https://api.ownroute.org/api/v1/agent_signup \ -H 'Content-Type: application/json' \ -d '{"agent_id": "my-bot-v1", "contact": "ops@example.com", "prepay_usd": 10}' # -> { "api_key": "ork_live_...", "balance_usd": 10.00, "rate_per_optimize": 0.001 } # 2. Stash the key. Echo it back to verify. export ORK=ork_live_... curl https://api.ownroute.org/api/v1/account -H "Authorization: Bearer $ORK" # -> { "id": "acct_...", "balance_usd": 10.00, "balance_own": 0, "limits": {...} }
| resource | id_prefix | create | list | update | delete |
|---|---|---|---|---|---|
| account | acct_ | POST /agent_signup | GET /account | PATCH /account | - |
| driver | drv_ | POST /drivers | GET /drivers | PATCH /drivers/:id | DELETE /drivers/:id |
| job | job_ | POST /jobs | GET /jobs | PATCH /jobs/:id | POST /jobs/:id/cancel |
| route | rte_ | (via /optimize) | GET /routes | - | - |
| webhook_endpoint | whe_ | POST /webhook_endpoints | GET /webhook_endpoints | PATCH /webhook_endpoints/:id | DELETE /webhook_endpoints/:id |
| api_key | ork_ | POST /api_keys | GET /api_keys | - | DELETE /api_keys/:id |
agent ──(POST /jobs)──────► [job_a, job_b, ...]
agent ──(POST /drivers)──► [drv_x, drv_y, ...]
agent ──(POST /optimize)─► VROOM + OSRM
│
▼
┌───────────────────────────────┐
│ routes = [ │
│ { driver: drv_x, stops:[]} │
│ { driver: drv_y, stops:[]} │
│ ] │
└───────────────┬───────────────┘
│
webhook ◄───── job.assigned, job.completed, ...
(HMAC-SHA256 over body, X-OwnRoute-Signature)
Single endpoint. Synchronous up to 200 jobs. Async with ?async=true beyond that — returns 202 + job_id; poll /optimization_runs/:id or wait for optimization.completed webhook.
POST /api/v1/optimize Authorization: Bearer ork_live_... Content-Type: application/json { "job_ids": ["job_a1", "job_a2", "job_a3"], "driver_ids": ["drv_x", "drv_y"], "options": { "shift_start": "2026-05-08T08:00:00-07:00", "shift_end": "2026-05-08T18:00:00-07:00", "max_stops_per_driver": 25, "objective": "min_duration" // or "min_distance" | "balanced" } } // 200 OK { "run_id": "opt_8f3a...", "summary": { "cost": 2841, "unassigned": [], "duration_s": 14102 }, "routes": [ { "driver_id": "drv_x", "stops": [ { "job_id": "job_a2", "arrival": "2026-05-08T08:21:14-07:00", "duration_s": 300 }, { "job_id": "job_a1", "arrival": "2026-05-08T09:02:51-07:00", "duration_s": 300 } ], "polyline": "_p~iF~ps|U_ulLnnqC..." } ], "billing": { "cost_usd": 0.001, "own_minted": 0.05, "balance_usd": 9.999 } }
POST /api/v1/jobs { "external_id": "order-1234", "dropoff_lat": 37.7749, "dropoff_lng": -122.4194, "dropoff_address": "123 Mission St, SF, CA", "service_minutes": 5, "priority": 1, "window_start": "2026-05-08T09:00:00-07:00", "window_end": "2026-05-08T17:00:00-07:00" }
POST /api/v1/drivers { "external_id": "emp-42", "name": "D. Driver", "phone": "+14155550001", "start_lat": 37.7858, "start_lng": -122.4065, "max_concurrent_jobs": 8 }
POST /api/v1/webhook_endpoints { "url": "https://my-agent.example.com/hooks", "events": ["job.assigned", "job.completed", "job.failed", "optimization.completed"] } // -> { id: "whe_...", secret: "whsec_..." } // store secret, not shown again // Inbound delivery payload: // Headers: X-OwnRoute-Event, X-OwnRoute-Delivery, X-OwnRoute-Signature: t=..,v1=hex(hmac_sha256(secret, t + "." + body)) { "event": "job.completed", "data": { "job_id": "job_a1", "completed_at": "2026-05-08T09:08:02-07:00" } } // Verify: // want = hmac_sha256(secret, t + "." + raw_body) // abs(now - t) < 300s AND hmac_eq(want, sig.v1)
4xx body shape:
{ "error": { "code": "insufficient_balance",
"message": "top up required",
"top_up_url": "https://billing.ownstack.org/p/topup?acct=...",
"docs_url": "https://ownroute.org/agents#errors" } }
| code | http | retryable | action |
|---|---|---|---|
| invalid_request | 400 | no | fix payload |
| unauthorized | 401 | no | rotate key |
| insufficient_balance | 402 | no | top up via top_up_url |
| not_found | 404 | no | check id |
| rate_limited | 429 | yes | backoff per Retry-After |
| internal_error | 500 | yes | exponential backoff, max 5 |
| service_unavailable | 503 | yes | retry with jitter |
Prepaid balance. $0.001/optimize, $0.0001/write, $0 for reads. Every $1 spent mints $0.05 of OWN credits. OWN can be redeemed for future calls ($1 OWN = $1 USD spend) or transferred to another OwnStack account by signed message.
GET /api/v1/account { "id": "acct_...", "balance_usd": 9.842, "balance_own": 0.158, "top_up_url": "https://billing.ownstack.org/p/topup?acct=...", "redeem_own_url": "https://billing.ownstack.org/p/redeem?acct=...", "limits": { "rpm": 60, "burst": 240, "max_jobs_per_optimize": 1000 } }
OwnRoute exposes an MCP server at https://api.ownroute.org/mcp. Stdio shim available via npx @ownroute/mcp. Manifest:
GET /.well-known/mcp.json { "schema_version": "v1", "name": "ownroute", "description": "Vehicle routing optimization. Assign N jobs to M drivers.", "server": { "url": "https://api.ownroute.org/mcp", "transport": "http" }, "auth": { "type": "bearer", "header": "Authorization" }, "tools": [ { "name": "ownroute.optimize", "description": "Optimize routes." }, { "name": "ownroute.create_job", "description": "Create a delivery job." }, { "name": "ownroute.list_jobs", "description": "List jobs." }, { "name": "ownroute.cancel_job", "description": "Cancel a job." }, { "name": "ownroute.create_driver","description": "Create a driver." }, { "name": "ownroute.list_drivers","description": "List drivers." } ] }
OpenAPI 3.1 schema lives at /.well-known/openapi.json. Use it to generate a client. Last-changed header on every response: X-Schema-Version.
headers on every response: X-RateLimit-Limit: 60 X-RateLimit-Remaining: 47 X-RateLimit-Reset: 1745799000 # unix seconds X-RateLimit-Burst: 240 429 response: Retry-After: 12 # seconds body: { error: { code: "rate_limited", ... } }
POST writes accept: Idempotency-Key: any-stable-string-up-to-128-chars Same key + same body within 24h -> identical 2xx response, no double-write. Same key + different body within 24h -> 409 conflict.
Optimization is bounded but not bit-deterministic — VROOM's local search is seeded but driver/job order in input affects results. To pin a result, set options.seed: <u32> and freeze input order.
Test keys (ork_test_...) hit a frozen California snapshot, never charge USD, mint test-OWN that doesn't redeem against live balance, and emit synthetic webhooks immediately. Identical surface to live.
User-Agent: agent-name/version (+contact-url). We page humans on agents that don't.POST /api_keys to mint per-agent keys.# pseudo-code; same shape works in any language function dispatch_loop(agent_state): jobs = agent_state.collect_pending_jobs() drivers = agent_state.collect_active_drivers() for j in jobs: ownroute.create_job(j, idempotency_key=j.id) for d in drivers: ownroute.create_driver(d, idempotency_key=d.id) result = ownroute.optimize( job_ids=[j.id for j in jobs], driver_ids=[d.id for d in drivers], options={"objective": "min_duration"} ) for route in result.routes: agent_state.apply_assignment(route) # consume webhook stream for status updates for evt in webhook_stream(): if evt.event == "job.completed": agent_state.close(evt.data.job_id)