Quickstart: Connect Your App

Authenticate, query with Filter AST, paginate via cursor, handle errors uniformly, register webhooks, generate native SDKs — the full integration in 6 steps, in under 25 minutes.

1

Authenticate (Two-Step Flow)

The Global API uses Two-Step Auth — first authenticate as a user (cross-tenant), then select a specific tenant context.

Step 1a — System-level login:

```typescript const loginRes = await fetch("https://api.aeionos.com/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "user@acme.com", password: "super-secret", }), });

const { data: { refreshToken }, } = await loginRes.json(); // refreshToken: stateful — usable across tenants you belong to ```

Step 1b — Tenant selection:

```typescript const tenantRes = await fetch( "https://api.aeionos.com/api/auth/select-tenant", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken, tenantId: "tenant_acme" }), }, );

const { data: { accessToken }, } = await tenantRes.json(); // accessToken: stateless JWT scoped to this tenant ```

Why two steps? A single user might belong to multiple tenants (employee at acme.com + customer at competitor.com). The refresh token authenticates them as a person; the access token authenticates them WITHIN a specific tenant. The kernel uses the access token's tenantId claim for all subsequent operations.

Token refresh: When the access token expires (~1 hour), use the refresh token to get a new one without re-authenticating.

2

Execute Collection Queries

EndpointWhat it doesExample
`GET /api/v1/{slug}`List with filter + pagination`/api/v1/contacts`
`GET /api/v1/{slug}/:id`Single record by ID`/api/v1/contacts/ct_xyz`
`POST /api/v1/{slug}`Create new record`/api/v1/contacts`
`PATCH /api/v1/{slug}/:id`Update existing record`/api/v1/contacts/ct_xyz`
`DELETE /api/v1/{slug}/:id`Delete record`/api/v1/contacts/ct_xyz`
3

Implement Cursor Pagination

For predictable performance at any scale, use cursor pagination instead of offset:

typescript // First page const page1 = await fetch("/api/v1/contacts?limit=50", { headers: { Authorization: Bearer ${accessToken}`, "x-tenant-id": tenantId }, }); const page1Body = await page1.json(); // page1Body.data: array of records // page1Body.meta.hasMore: boolean // page1Body.meta.cursor: string (use for next request)

// Next page using cursor if (page1Body.meta.hasMore) { const page2 = await fetch( /api/v1/contacts?limit=50&cursor=${encodeURIComponent(page1Body.meta.cursor)}, { headers: { Authorization: Bearer ${accessToken}, "x-tenant-id": tenantId, }, }, ); } ```

Why cursor vs offset?

  • Cursor pagination: O(1) per page — DB jumps directly to the next page boundary
  • Offset pagination: O(N) — DB scans through N rows to reach the offset (slow on deep pages)

Cursor characteristics:

  • Opaque (URL-safe encoded internal state)
  • Tied to your sort order (changing orderBy invalidates cursors)
  • Stable — re-using a cursor returns the same next page
  • Time-limited — cursors expire after 1 hour by default (configurable)

Sort + cursor:

GET /api/v1/contacts?orderBy[lastActivityAt]=desc&limit=50 GET /api/v1/contacts?orderBy[lastActivityAt]=desc&limit=50&cursor=eyJ...

4

Handle the Unified Error Envelope

CodeHTTP statusWhen it fires
`BAD_REQUEST`400 / 422Zod schema rejected the input, or required field missing
`UNAUTHORIZED`401No valid token, or token expired
`FORBIDDEN`403Token valid but lacks required permissions
`NOT_FOUND`404Record doesn't exist or RLS hides it from this user
`CONFLICT`409Unique constraint violation, version mismatch
`INTERNAL_ERROR`500Server-side error (always logged for ops review)
5

Register Signed Webhooks

For server-to-server notifications (order created, payment completed, etc.), register webhooks:

typescript // Register a webhook endpoint (admin API) await fetch("/api/admin/webhooks", { method: "POST", headers: { Authorization: Bearer ${accessToken}, "x-tenant-id": tenantId, "Content-Type": "application/json", }, body: JSON.stringify({ url: "https://your-server.com/webhooks/aeion", events: ["commerce.order.completed", "commerce.refund.processed"], secret: "your-shared-secret", // Never sent to Aeion API; stored on your end active: true, }), });

HMAC-SHA256 signature verification on your side:

```typescript import crypto from "crypto";

app.post("/webhooks/aeion", (req, res) => { const signature = req.headers["x-aeion-signature"]; const timestamp = req.headers["x-aeion-timestamp"]; const body = req.rawBody; // Get raw bytes, not parsed JSON

// Verify signature const expectedSig = crypto .createHmac("sha256", "your-shared-secret") .update(${timestamp}.${body}) .digest("hex");

if (signature !== expectedSig) { return res.status(401).send("Invalid signature"); }

// Verify timestamp (prevent replay attacks) const now = Date.now() / 1000; const requestTime = parseInt(timestamp); if (now - requestTime > 300) { // 5-minute window return res.status(401).send("Timestamp too old"); }

// Process the event const event = JSON.parse(body); // ...

res.status(200).send("OK"); }); ```

Reliability: Aeion retries failed deliveries with exponential backoff (1s, 5s, 25s, 2min, 10min, 1h, 6h, 24h). After max retries, the delivery is parked in the DLQ; you can manually replay via Admin → Webhooks → DLQ.

Per-tenant rate limits — webhooks subject to per-recipient throttling to prevent overwhelming your endpoint.

6

Generate Native SDKs from OpenAPI

Aeion auto-generates an OpenAPI 3.1 spec from your registered collections.

Fetch the spec:

bash curl https://api.aeionos.com/api/docs/openapi.json > aeion-openapi.json

Generate native SDKs:

```bash # TypeScript npx openapi-typescript aeion-openapi.json -o aeion-types.ts

# Python openapi-generator-cli generate -i aeion-openapi.json -g python -o ./aeion-sdk-python

# Go openapi-generator-cli generate -i aeion-openapi.json -g go -o ./aeion-sdk-go

# Java openapi-generator-cli generate -i aeion-openapi.json -g java -o ./aeion-sdk-java

# Swift openapi-generator-cli generate -i aeion-openapi.json -g swift5 -o ./aeion-sdk-swift

# Rust openapi-generator-cli generate -i aeion-openapi.json -g rust -o ./aeion-sdk-rust ```

Generated SDKs include:

  • Typed request/response models for every endpoint
  • Built-in retry logic + exponential backoff
  • Configurable auth (token in env var, secret manager, etc.)
  • Error envelope handling with typed error codes
  • Streaming support for large response bodies
  • Webhook signature verification helpers

Stainless option (for enterprise SDKs with branding):

  • Premium SDKs with hand-tuned ergonomics
  • Stable interfaces guaranteed across versions
  • Branded under your product name (e.g., "ACME API SDK" instead of "Aeion API SDK")
  • Single-source SDKs for TS / Python / Go / Java / Kotlin / Swift / Ruby

Integration Recipes

Mobile app integration. Generate Swift / Kotlin SDKs; integrate the JWT refresh flow + auto-retry on failed requests. Tenant selection happens once at sign-up; thereafter the JWT carries the tenant context.

Backend-to-backend integration. Server-side credentials use API keys (long-lived, scoped) instead of JWTs. Pass Authorization: Bearer aeion_key_.... Webhooks notify your backend of events.

Cross-app sync via webhooks. Webhook to your CRM when Aeion creates a contact; bidirectional sync without complex polling.

MCP integration. Aeion exposes 89 MCP tools (dynamically filtered per-tenant) alongside the REST API, plus 180+ WebMCP browser tools for in-app AI. Same auth, same RBAC — MCP tools are auto-scoped to the user's permissions.

GraphQL alongside REST. Both protocols expose the same business logic. Use REST for CRUD, GraphQL for complex graph queries that hydrate relationships in one round-trip.

WebMCP for browser AI. Browser-side AI tools (via Aeion WebMCP) execute under the user's JWT — same auth, same RBAC as REST.

Troubleshooting

401 Unauthorized despite valid token. Token may have expired (default 1-hour TTL). Use the refresh token to get a new access token + retry. Or check the JWT is being sent with the correct Authorization: Bearer ... prefix.

403 Forbidden but I should have access. RBAC permission missing. Check the user's roles in Admin → Users → [user]. Verify the role has the required module:action permission.

404 on a record I know exists. Either the record doesn't exist or RLS filters it out. Try with admin token — if it works, you've identified an RLS issue.

Cursor pagination returning duplicates. The data changed between requests, shifting the cursor's position. Use orderBy with a stable secondary sort (e.g., id) to make cursors stable across concurrent modifications.

Webhook signature verification failing. Common causes: (1) you're hashing the parsed JSON, not the raw body, (2) timestamp window too tight, (3) wrong secret. Test with the verification tool at Admin → Webhooks → Test.

Rate limited unexpectedly. Check rate limit headers in the response. Default per-tenant API limit: 1000 req/sec. If consistently rate-limited, contact support — may need a higher rate-limit tier.

Frequently Asked Questions

Yes — the Global API relies on strict multi-tenant isolation. Unless your deployment routes traffic via distinct subdomains (`tenant1.aeionos.com`), the `x-tenant-id` header is required by the kernel router to execute the query.

The protocol defines a unified error envelope. Common codes are `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, `INTERNAL_ERROR`. Domain middleware can add specific codes (e.g., `FEATURE_NOT_ENABLED`, `CSRF_TOKEN_MISSING`) — always switch on `error.code` rather than HTTP status. Never parse unstructured HTML or custom JSON.

No. File uploads strictly use `multipart/form-data` at `/api/v1/files/upload`. Base64-encoding into JSON triggers `VALIDATION_ERROR` — destroys payload optimization + creates problems with binary data.

Default: 1000 req/sec per tenant, burst up to 2000 req/sec. Higher limits available on Enterprise. Per-endpoint limits also apply (auth endpoints lower, search endpoints higher). Rate-limit headers in every response.

Specific public endpoints don't require auth — typically content delivery + healthchecks. Business data always requires auth.

Aeion uses URL versioning (`/api/v1/`). New major versions add new URL prefixes; old versions stay supported for at least 12 months after deprecation. Backward-compatible additions go into v1 without version bump.

Yes. GraphQL endpoint at `/api/graphql`. Single endpoint, schema introspected from registered collections. Aliased nested queries hydrate relationships in one round-trip. Subscriptions for real-time updates piggyback on WebSocket.

Yes. Per-endpoint `Cache-Control` headers + ETag support. For real-time use cases, subscribe to webhooks instead of polling.

Exponential backoff: 1s, 5s, 25s, 2min, 10min, 1h, 6h, 24h. Max attempts 8 by default. After max, parked in DLQ for manual replay. Custom retry policies configurable per-webhook.

Fetch `/api/docs/openapi.json` and filter the `paths` object client-side by prefix (`/api/v1/crm_*`, `/api/v1/commerce_*`, etc.) to generate scoped SDKs. Same auth + tenant headers apply when consuming the resulting endpoints.

Ready to scale your architecture?