ownroute :: agents api.ownroute.org/api/v1 // v1.0 stable
json machine-primary $0.001/optimize $0.0001/write 5% rebate as OWN 60 rpm default

// agents.read.this

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.

0_BOOTSTRAP

# 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": {...} }

1_DOMAIN_MODEL

resourceid_prefixcreatelistupdatedelete
accountacct_POST /agent_signupGET /accountPATCH /account-
driverdrv_POST /driversGET /driversPATCH /drivers/:idDELETE /drivers/:id
jobjob_POST /jobsGET /jobsPATCH /jobs/:idPOST /jobs/:id/cancel
routerte_(via /optimize)GET /routes--
webhook_endpointwhe_POST /webhook_endpointsGET /webhook_endpointsPATCH /webhook_endpoints/:idDELETE /webhook_endpoints/:id
api_keyork_POST /api_keysGET /api_keys-DELETE /api_keys/:id

2_PRIMARY_FLOW

  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)

3_OPTIMIZE

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 }
}

4_WRITES

create job

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"
}

create driver

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
}

5_WEBHOOKS

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)

6_ERRORS

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" } }
codehttpretryableaction
invalid_request400nofix payload
unauthorized401norotate key
insufficient_balance402notop up via top_up_url
not_found404nocheck id
rate_limited429yesbackoff per Retry-After
internal_error500yesexponential backoff, max 5
service_unavailable503yesretry with jitter

7_BILLING_AND_OWN

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 } }

8_MCP

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." }
  ]
}

9_OPENAPI

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.

10_RATE_LIMITS

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", ... } }

11_IDEMPOTENCY

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.

12_DETERMINISM

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.

13_SANDBOX

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.

14_TERMS_FOR_AGENTS


EXAMPLE_AGENT_LOOP

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