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.
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.
Execute Collection Queries
| Endpoint | What it does | Example |
|---|---|---|
| `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` |
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
orderByinvalidates 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...
Handle the Unified Error Envelope
| Code | HTTP status | When it fires |
|---|---|---|
| `BAD_REQUEST` | 400 / 422 | Zod schema rejected the input, or required field missing |
| `UNAUTHORIZED` | 401 | No valid token, or token expired |
| `FORBIDDEN` | 403 | Token valid but lacks required permissions |
| `NOT_FOUND` | 404 | Record doesn't exist or RLS hides it from this user |
| `CONFLICT` | 409 | Unique constraint violation, version mismatch |
| `INTERNAL_ERROR` | 500 | Server-side error (always logged for ops review) |
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.
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.