API conventions
Everything under /api/v1 follows the same rules. Learn them once and the rest of the surface is
predictable.
Base URL
https://evonia-gate.annatarhe.com/api/v1The OpenAPI document is at /api/v1/openapi.json and the interactive explorer at
/docs/api-reference.
Authentication
Two credentials, and the order they are considered matters.
-
X-API-Keyheader. If present, Gate authenticates a service account. The session is never consulted. -
Session cookie. Only when there is no
X-API-Keyheader does Gate readbetter-auth.session_token.
Some endpoints require a user session specifically and reject service accounts with
403 A user session is required. See
Choose an integration mode.
Success responses
Every successful response wraps its payload in data. There is no envelope variation and no
top-level array.
{
"data": {
"id": "6f1c9e7a-2b44-4a0f-9f1a-2c9d2b6ac1a3",
"name": "ShellTime",
"slug": "shelltime"
}
}Creation endpoints return 201. Everything else returns 200.
Error responses
Errors are problem-style JSON with a stable type URI.
{
"type": "https://evonia-gate.annatarhe.com/problems/validation",
"title": "Request validation failed",
"status": 400,
"errors": [{ "path": ["body", "scopes"], "message": "Array must contain at least 1 element(s)" }]
}| Field | Present |
|---|---|
type | Always |
title | Always |
status | Always |
errors | Validation failures only — an array of Zod issues |
requestId | Errors raised after the request id is assigned |
The full list of problem types is in the error catalog.
Request ids
Send x-request-id and Gate echoes it back on the response. Omit it and Gate generates a UUID and
echoes that. Either way, the response carries the header and audit rows record it.
curl -sSi "https://evonia-gate.annatarhe.com/api/v1/health" \
-H "x-request-id: 018f3a2c-1d4e-7c3a-9b21-6f9c2d1e4a55"Log the id on your side. It is the fastest way to correlate a failure with Gate's audit log.
Idempotency
POST /projects/{projectId}/billing/checkout requires an idempotency-key header of 16–255
characters. Without it the request fails validation with 400.
curl -sS -X POST \
"https://evonia-gate.annatarhe.com/api/v1/projects/$PROJECT_ID/billing/checkout" \
-H "content-type: application/json" \
-H "idempotency-key: checkout-$SUBJECT_ID-$(date +%s)" \
-H "x-api-key: $GATE_API_KEY" \
-d '{ "environmentId": "...", "subjectId": "...", "planKey": "pro",
"successUrl": "https://shelltime.xyz/done", "cancelUrl": "https://shelltime.xyz/cancel" }'Usage events use a different mechanism: they carry eventId in the body and are unique on
(projectId, eventId). A replay returns 200 with {"accepted": false, "duplicate": true} —
not a 409. Treat that as success.
Archive and restore
Organizations, projects, and config entries are archived rather than deleted. DELETE sets a
timestamp; the row and its history remain.
List endpoints take a state query parameter:
| Value | Returns |
|---|---|
active | Default. Non-archived records only |
archived | Archived records only |
all | Both |
curl -sS "https://evonia-gate.annatarhe.com/api/v1/organizations?state=archived" \
-b "better-auth.session_token=$SESSION"Restoring uses a dedicated route — POST .../restore — and requires the same permission that
archiving did.
CORS
Credentialed browser requests are accepted only from the origins in src/server/origins.ts:
GATE_BASE_URL, https://shelltime.xyz, https://rawback.app, https://athena.annatarhe.com,
and https://athena.annatarhe.cn.
| Setting | Value |
|---|---|
| Methods | GET, POST, PUT, PATCH, DELETE, OPTIONS |
| Headers | content-type, authorization, x-api-key, x-request-id, idempotency-key |
| Credentials | Allowed |
Health
/healthRuns select 1 against Postgres, so it reports database reachability rather than just process
liveness. It is the only unauthenticated route besides the Stripe webhook.