Local development
How to run the Ultra API locally and exercise it from your code, agent, or CLI without burning production keys.
The production API at https://ultranetwork.co/api/v1 is the easiest target — point your ULTRA_API_KEY at it and you're done. This guide is for the cases where you want a self-contained loop: contributing to Ultra itself, building an integration on a flaky network, working offline, or running a CI suite that shouldn't touch prod.
Prerequisites
- Node.js 20+ (the API ships from a Next.js monorepo)
- A local Ultra workspace running (Ultra is open-source; clone instructions are out of scope for this guide — contact us if you're building against a forked or self-hosted instance)
- One of:
curl, the Ultra CLI, the Ultra MCP server, or your own client
The dev-bootstrap key
When you run the Ultra API locally for the first time, the production key table is empty — there's nothing to authenticate against. To unblock this, the server checks for a ULTRA_API_DEV_KEY environment variable at boot. If set, that string is accepted as a wildcard-scope, cross-org master key for the local instance only.
# In the Ultra server's .env.local
ULTRA_API_DEV_KEY=ulk_dev_localonly_pickyourown
Then on the client side:
export ULTRA_API_KEY=ulk_dev_localonly_pickyourown
export ULTRA_API_BASE_URL=http://localhost:3000/api/v1 # adjust port to your local
curl -H "Authorization: Bearer $ULTRA_API_KEY" \
"$ULTRA_API_BASE_URL/trips?limit=3"
The dev-bootstrap key bypasses the agent-onboarding agreement gate and grants ["*"] scopes. It is the equivalent of root access on the local instance. Never set ULTRA_API_DEV_KEY on a deployed (non-local) server — it is intended only for the empty-key-table bootstrap problem on first run.
Pointing the CLI at local
export ULTRA_API_KEY=ulk_dev_localonly_pickyourown
export ULTRA_API_SPEC=http://localhost:3000/api/v1/openapi.json
ultra list_trips --limit=5
ULTRA_API_SPEC tells the CLI to load operations from the local OpenAPI document. ULTRA_API_BASE_URL is inferred from the spec's servers[0].url, so as long as the local server emits the right servers[0] (it does — see apps/web/app/api/v1/openapi.json/route.ts in the Ultra codebase), one env var is enough.
Pointing the MCP server at local
{
"mcpServers": {
"ultra-local": {
"command": "npx",
"args": ["-y", "@ultra-network/mcp"],
"env": {
"ULTRA_API_KEY": "ulk_dev_localonly_pickyourown",
"ULTRA_API_SPEC": "http://localhost:3000/api/v1/openapi.json"
}
}
}
}
Restart Claude Code; /mcp should show ultra-local connected. Now your LLM can read and write against the local instance without touching production.
Seeding test data
A bare Ultra instance has no trips, no suppliers, no bookings. Two paths to populate it:
- Use the trip builder UI to create a trip, add a client, run a search against a sandbox adapter. This produces real
plan_itemrows you can book against. - Use the API directly to mint trips, items, and bookings. See the examples directory for runnable scripts.
The supplier adapter list (hbt, drivado, …) requires per-adapter credentials. In local dev most adapters fall back to sandbox or mocked responses if their credentials aren't configured; check apps/web/app/api/v1/bookings/route.ts in the Ultra codebase for the current behaviour of each.
CI patterns
For CI that exercises the API:
- Provision the
ULTRA_API_DEV_KEYas a CI secret pointed at a CI-only Ultra instance (never at production). - Run the Ultra server as a Docker service alongside your tests, or spin it up with
next devin a background job. - Wait for
GET /api/v1/openapi.jsonto return 200 before running tests — this confirms the server is ready and the spec is generatable. - Reset state between runs by either (a) using a fresh database container per run, or (b) hitting destructive cleanup endpoints if your fork exposes them.
When local diverges from prod
Two known sources of drift:
- Supplier adapters: real adapters hit real supplier APIs. In local you'll typically run with mocked or sandbox adapters that return synthetic data. Behaviour on the adapter happy path is identical; error cases may differ.
- Rate limits: the local server enforces the same rate-limit code path as prod, but the in-memory bucket store resets on every restart. Don't write tests that depend on rate-limit state surviving across server restarts.
For anything else, the production API and the local API run the same code from the same OpenAPI spec — that's the point of the spec-driven design.
Next
- Authentication — how the dev key relates to org-bound and cross-org keys
- API reference — every operation, identical against local or prod
- Errors — the envelope is the same; the codes are the same