4. Getting started
4.1 Sign up
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
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:
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.
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
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
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.
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"}])