API conventions

Everything under /api/v1 follows the same rules. Learn them once and the rest of the surface is predictable.

Base URL

Text
https://evonia-gate.annatarhe.com/api/v1

The 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.

  1. X-API-Key header. If present, Gate authenticates a service account. The session is never consulted.

  2. Session cookie. Only when there is no X-API-Key header does Gate read better-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.

JSON
{
  "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.

JSON
{
  "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)" }]
}
FieldPresent
typeAlways
titleAlways
statusAlways
errorsValidation failures only — an array of Zod issues
requestIdErrors 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
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
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:

ValueReturns
activeDefault. Non-archived records only
archivedArchived records only
allBoth
curl
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.

SettingValue
MethodsGET, POST, PUT, PATCH, DELETE, OPTIONS
Headerscontent-type, authorization, x-api-key, x-request-id, idempotency-key
CredentialsAllowed

Health

GET/healthnone

Runs select 1 against Postgres, so it reports database reachability rather than just process liveness. It is the only unauthenticated route besides the Stripe webhook.