Section 4

4. Getting started

4.1 Sign up

bash
curl -X POST "$BASE/v1/auth/signup" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"email": "you@example.com", "password": "…", "display_name": "You"}'

Returns an access_token (a session JWT) and a refresh token. Signup creates a user — not yet a place to put things.

4.2 Create your workspace

bash
curl -X POST "$BASE/v1/tenants" \
  -H "Authorization: Bearer $SESSION_JWT" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name": "Sapelo AI, Inc.", "project_name": "first-project"}'

One call, one transaction: a tenant, its first organization with you enrolled as owner, and a first project. Returns all three.

The slug is derived from the name — Sapelo AI, Inc. becomes sapelo-ai-inc — and collisions get a random suffix. Pass slug explicitly to choose your own, which you must do if the name has nothing sluggable in it or derives to a reserved word.

This is deliberately separate from signup: a user arriving by invitation is joining someone else's organization and should call POST /v1/invitations/accept instead, not receive an empty tenant they never asked for.

Further projects go under the organization, and all three fields are required — a body carrying only name is a typed 400:

bash
curl -X POST "$BASE/v1/organizations/$ORG_ID/projects" \
  -H "Authorization: Bearer $SESSION_JWT" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"slug": "my-app-staging", "name": "My App (staging)", "environment": "staging"}'

environment is a closed set: production, staging, development. There is no test value — a sandbox project is development with a pk_test_ key.

4.3 Pick a plan

A new tenant lands on free, which carries zero monthly credits — so inference is refused with 429 budget.exceeded until you choose a plan.

bash
curl -X POST "$BASE/v1/tenants/$TENANT_ID/billing/entitlement/change-plan" \
  -H "Authorization: Bearer $SESSION_JWT" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"plan_id": "pro"}'

Self-serve payment is not built — Stripe checkout is a typed 501. Changing plans works and grants the allowance immediately, so today this is an internal/trusted operation rather than a purchase.

4.4 Mint a key

bash
curl -X POST "$BASE/v1/projects/$PROJECT_ID/keys" \
  -H "Authorization: Bearer $SESSION_JWT" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "my-app-production",
    "environment": "live",
    "scopes": ["inference:call", "catalog:read", "usage:read"],
    "rate_limit_rpm": 600
  }'

The response carries token — the only time the plaintext key is ever returned. Store it immediately.

4.5 Call a model

bash
curl -X POST "$BASE/v1/chat/completions" \
  -H "Authorization: Bearer pk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4.1-mini",
    "messages": [{"role": "user", "content": "Hello"}]
  }'

That's it. Any OpenAI SDK works by pointing base_url at $BASE/v1.

python
from openai import OpenAI
client = OpenAI(api_key="pk_live_…", base_url="https://api.opennozzle.com/v1")
client.chat.completions.create(model="gpt-4.1-mini",
                               messages=[{"role": "user", "content": "Hello"}])