# Dianthan > Managed backend infrastructure. Systems: Identity (sign-in, accounts and teams), Billing (plans, checkout and entitlements), Metering (usage, credits and model cost), Data (records, queries, search and files), Messaging (email, push, inbox and realtime), Automation (jobs, schedules and webhooks), Observe (analytics, errors and alerts), Flags (feature flags and rollouts), Models (one gateway to every model). API base URL: https://api.dianthan.com # Dianthan documentation Dianthan is a backend you share across all of your apps. It handles sign-in, billing, usage metering, data, messaging, background work, analytics and feature flags over one HTTP API, so each new product starts with all of that already in place. ## How it fits together Your account owns any number of **apps**. An app is one product: it has its own users, domains, keys, data and limits. You turn **systems** on for the apps that need them. Every system is reached through the same base URL, with the same keys and the same error format. ```text your sites api.dianthan.com systems ────────── ──────────────── ─────── fieldnotes.app ──┐ ┌── identity billing ├── X-App-Id + key ──► one API ──┼── metering data tally.so ──┘ └── messaging automation observe flags ``` A person who signs in to two of your apps has one Dianthan identity and two memberships. Their data in each app is kept separate. You see both memberships in your dashboard. ## Where to start - **New to Dianthan:** follow the [Quickstart](/docs/quickstart). It takes about ten minutes to get from no account to a signed-in user. - **Planning an integration:** read [Concepts](/docs/concepts) for apps, keys, sessions and how isolation works. - **Working with an agent:** point it at [Agents and MCP](/docs/agents), or give it [`/llms.txt`](/llms.txt), the whole documentation as one plain-text file. ## Conventions in these docs Examples use `curl` against `https://api.dianthan.com`, which works from any language. Where a TypeScript SDK call is shorter, it appears next to the HTTP version. Values you replace look like `app_…` and `dn_…`. > **In preview** marks a feature that you can design around now but that isn't generally available yet. Its API may still change before it is. # Quickstart Create an app, give it a key, and sign a person in with an emailed link. You need a terminal and a site you can run locally. ## 1. Create an account and an app Sign in at [app.dianthan.com](https://app.dianthan.com) with your email. You don't set a password. Then create your first app from the CLI: ```bash npx dianthan login npx dianthan apps create "Field Notes" --domain fieldnotes.localhost --signup open ``` The domain tells Dianthan where sign-in links may send people back to. `--signup open` lets anyone who signs in become a customer of this app; leave it off to allow invited people only. > Use `.localhost` for local development. Browsers resolve it to your own machine, and unlike plain `localhost`, it gives each app a domain of its own. ## 2. Create a server key ```bash npx dianthan keys create --app fieldnotes --name web-server ``` ```text DIANTHAN_URL=https://api.dianthan.com DIANTHAN_APP_ID=app_8Jq2… DIANTHAN_KEY=dn_live_4f9c… ``` The key is shown once. Put these three lines in your site's server environment (for example `.env.local`). Never send the key to a browser; it can act for any user of the app. ## 3. Send a sign-in link From your server, ask Dianthan to email a link: ```bash curl https://api.dianthan.com/v1/auth/magic-link \ -H "Content-Type: application/json" \ -d '{ "email": "ada@example.com", "app_id": "app_8Jq2…", "return_to": "http://fieldnotes.localhost:3000/auth/callback" }' ``` The response is always `202`, whether or not the address has an account, so the endpoint can't be used to find out who your users are. ## 4. Exchange the grant When Ada clicks the link, she confirms on a Dianthan page and lands on your `return_to` URL with a single-use `grant` parameter. Your callback route exchanges it for a session: ```ts--app/auth/callback/route.ts export async function GET(req: Request) { const grant = new URL(req.url).searchParams.get("grant"); const res = await fetch(`${process.env.DIANTHAN_URL}/v1/auth/grants/exchange`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ grant }), }); const session = await res.json(); // session.access_token (short-lived JWT), session.refresh_token (opaque) const out = Response.redirect(new URL("/", req.url)); out.headers.append("Set-Cookie", `dn_refresh=${session.refresh_token}; HttpOnly; Secure; SameSite=Lax; Path=/`); return out; } ``` Grants last a few minutes and work once. Store the refresh token in an `httpOnly` cookie on your own domain. Keep the access token in memory or in a short-lived cookie, and refresh it with `POST /v1/auth/refresh`. ## 5. Ask who is signed in ```bash curl https://api.dianthan.com/v1/me \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` ```json { "id": "usr_x81…", "email": "ada@example.com", "name": null } ``` That's a working sign-in. From here: - Add Google and GitHub sign-in in [Identity](/docs/identity). - Put a feature behind a plan with [Billing](/docs/billing). - Record what people do with [Observe](/docs/observe). # Concepts A few ideas explain most of how Dianthan behaves. Read this once and the rest of the API becomes predictable. ## Accounts and apps Your **account** is you, or your team. It holds billing, the dashboard and any number of apps. An **app** is one product. Every piece of app data (records, files, logs, subscriptions, flags, jobs) carries the app's ID, and every query filters by it. One app can never read another app's data, even when both belong to you. A test suite checks this on every release. App IDs look like `app_8Jq2…`. Every ID in Dianthan has a prefix that names its type: `usr_` for users, `org_` for organizations, `sub_` for subscriptions, and so on. If you find an ID in a log, the prefix tells you what it refers to. ## Users and memberships A **user** is a person. They have one identity across Dianthan, so signing in to a second app of yours doesn't create a second person. A **membership** is what that person is inside one app: a `customer`, a `member` or an `admin`. Customers can use self-service routes only, such as their own profile, entitlements and billing portal. They can never list other users or read app-wide data. Each app chooses whether **sign-up is open** (anyone who signs in becomes a customer) or **closed** (only people you invite or create from your server). ## Organizations An **organization** groups users who work together. It has members, roles, invitations, its own API keys and, optionally, single sign-on. You can bill an organization instead of a person, and its entitlements then apply to every member. ## The three ways to authenticate | Who | Credential | Sent as | |---|---|---| | A person using your app | Access token from a sign-in | `Authorization: Bearer ` | | Your server | App key (`dn_live_…`) | `X-API-Key` and `X-App-Id` headers | | You, in the dashboard or CLI | Your own session or a personal key | `Authorization: Bearer ` | Server keys act for the whole app. Keep them on your server and pass the visitor's IP in `X-Client-IP`, so rate limits apply to the visitor and not to your server. ## Systems A **system** is one area of the API: identity, billing, metering, data, messaging, automation, observe or flags. Systems are switched on per app. A request to a system that's switched off returns `403 module_disabled` and a link to switch it on. Systems work on their own and share three things: - **One identity.** Every system knows the same users and organizations. - **One event stream.** Changes in any system appear in the app's event feed and can trigger webhooks. - **One audit log.** Every change records who made it, using which credential, and from where. ## Environments Use separate apps for production and development (for example `fieldnotes` and `fieldnotes-dev`). They cost nothing extra on paid plans, and keeping them apart means a development key can never touch production data. # Identity Passwordless sign-in, sessions, organizations, roles, single sign-on and two-factor authentication. There are no passwords anywhere in Dianthan, so there is nothing to hash, reset or leak. ## Sign-in with an emailed link ```bash curl https://api.dianthan.com/v1/auth/magic-link \ -H "Content-Type: application/json" \ -d '{"email":"ada@example.com","app_id":"app_…","return_to":"https://fieldnotes.app/auth/callback"}' ``` - `return_to` must be on a domain registered to the app. Any other URL is refused. - Links last 15 minutes and work once. - The link opens a confirmation page first, so the security scanners in mail clients that open every link can't use it up. - The response is `202` whether or not the address is known. After confirmation, the person lands on `return_to?grant=…`. Exchange the grant from your server with `POST /v1/auth/grants/exchange`, as shown in the [Quickstart](/docs/quickstart#4-exchange-the-grant). ## Sign-in with Google or GitHub ```bash curl "https://api.dianthan.com/v1/auth/oauth/google?app_id=app_…&return_to=https://fieldnotes.app/auth/callback" ``` The response contains the provider URL. Redirect the browser to it. The person comes back to `return_to` with a grant, exactly as with an emailed link, so one callback route handles both. Accounts are matched by verified email: someone who signed in with a link and later uses Google with the same address is the same user. ## Sessions An exchange returns two tokens: | Token | Lifetime | Keep it | |---|---|---| | `access_token` | Minutes | In memory or a short-lived cookie. Send as `Authorization: Bearer`. | | `refresh_token` | Days | In an `httpOnly`, `Secure` cookie on your own domain. | Refresh with `POST /v1/auth/refresh`. The refresh token rotates each time, and reusing an old one revokes the session. List sessions with `GET /v1/auth/sessions` and revoke one with `DELETE /v1/auth/sessions/:id`, or all of them with `DELETE /v1/auth/sessions`. ## Creating users from your server When your own sign-up flow already knows a person, create them directly with your app key: ```bash curl https://api.dianthan.com/v1/auth/register \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{"email":"jane@customer.com","name":"Jane"}' ``` ## Organizations and roles ```bash # Create a team curl -X POST https://api.dianthan.com/v1/organizations \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"name":"Acme"}' # Invite someone curl -X POST https://api.dianthan.com/v1/organizations/org_…/invitations \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"email":"grace@acme.com","role":"member"}' ``` Invitations are accepted with `POST /v1/invitations/:id/accept`. Define custom roles with `POST /v1/organizations/:id/roles` and attach permissions from `GET /v1/permissions`. ## Two-factor authentication and step-up People can add an authenticator app with `POST /v1/me/mfa/totp/setup` and confirm it with `/verify`. They get ten recovery codes, each usable once. For sensitive actions such as deleting an organization or changing a payout account, ask for a fresh check with `POST /v1/auth/step-up`. The access token it returns carries a recent-authentication claim that your server can require. ## Single sign-on Organizations can sign in through their own identity provider. Create a connection with `POST /v1/organizations/:id/sso`, then send members to `GET /v1/auth/sso/:connection_id/start`. Members of that organization can then be required to use it. > **In preview:** passkeys, as a third way to sign in alongside links and OAuth. # Billing Plans, checkout, subscriptions, invoices and entitlements. Your code checks what a person is allowed to do, not which price they pay. ## Plans and entitlements A **plan** has a price and a set of **entitlements**: named features and limits. ```json { "id": "plan_pro", "name": "Pro", "interval": "month", "amount": 1200, "currency": "usd", "entitlements": { "export.pdf": true, "seats": 5, "ai.summaries.per_month": 500 } } ``` Change a plan's entitlements in the dashboard and every subscriber gets the change on their next request, without a deploy. Plans can belong to one app or be shared by all of them. ## Checking entitlements ```bash curl https://api.dianthan.com/v1/me/entitlements \ -H "Authorization: Bearer $TOKEN" ``` ```json { "entitlements": { "export.pdf": true, "seats": 5, "ai.summaries.per_month": 500 } } ``` Check one with `GET /v1/me/entitlements/export.pdf`. Organizations have their own at `GET /v1/organizations/:id/entitlements`. A person who belongs to a paying organization gets its entitlements as well as their own. ## Checkout ```bash curl -X POST https://api.dianthan.com/v1/billing/checkout \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "plan_id": "plan_pro", "success_url": "https://fieldnotes.app/welcome", "cancel_url": "https://fieldnotes.app/pricing" }' ``` The response contains a hosted checkout `url`; redirect to it. Pass `"organization_id"` to bill a team, or `"mode": "payment"` for a one-off charge. Success and cancel URLs must be on the app's registered domains. When the payment completes, Dianthan records the subscription and updates entitlements, usually before the person's browser reaches `success_url`. ## The customer portal `POST /v1/billing/portal` returns a link to the hosted self-service portal, where customers update cards, download invoices and cancel. You can also cancel and resume from your own interface with `POST /v1/billing/subscriptions/:id/cancel` and `/resume`. ## The local ledger Customers, subscriptions and invoices are recorded from verified payment events, and each event is applied exactly once, even if it is delivered twice. Reads such as `GET /v1/billing/subscriptions` and `GET /v1/billing/invoices` are served from this ledger, so they are fast and consistent. ## Grants and overrides From the dashboard, or with `POST /v1/billing/subscriptions` using an admin credential, you can give someone a plan without charging them. Entitlement overrides switch on one feature for one account without changing their plan, which is useful for a beta or a support case. ## Settlement Payments settle to your merchant account. Dianthan takes no share of them; card processing fees apply as usual. > **In preview:** connecting your merchant account from the dashboard. Until then, accounts are connected by our team on request. # Metering Record each unit of work against the person or team that caused it, then turn usage into limits, credits and margins. This matters most for products that pay for every model call. ## Recording usage ```bash curl -X POST https://api.dianthan.com/v1/usage/events \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{ "metric": "ai.summary", "quantity": 1, "user_id": "usr_x81…", "metadata": { "document": "rec_9a…" } }' ``` - `metric` is any name you choose: lowercase letters, digits and `_ . : -`, starting with a letter. - `quantity` defaults to 1 and can be any non-negative number, such as a count of tokens or seconds. - Attach usage to an organization with `organization_id`. Record usage **after** the work succeeds. A failed request shouldn't cost your customer anything. ## Reading usage ```bash curl "https://api.dianthan.com/v1/usage?user_id=usr_x81…&since=2026-11-01" \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" ``` ```json { "app_id": "app_…", "items": [ { "metric": "ai.summary", "total": 212, "events": 212 }, { "metric": "ai.tokens.output", "total": 381904, "events": 212 } ] } ``` `GET /v1/usage/:metric` narrows to one metric. Organization totals are at `GET /v1/organizations/:id/usage`. ## Limits from plans > **In preview.** Plans will carry quotas as entitlements, for example `"ai.summaries.per_month": 500`. Ask before running expensive work: > > `GET /v1/usage/ai.summary/remaining` returns `{ "limit": 500, "used": 212, "remaining": 288, "resets_at": "…" }`, and recording past the limit returns `402 quota_exceeded` with the same body. ## Credits > **In preview.** A prepaid balance per user or organization, kept in an append-only ledger. Credits come from plans (monthly allowances), top-ups (`POST /v1/credits/top-up` opens checkout), and grants. They're spent by usage events that carry a price. Each entry records why the balance changed, so a customer's statement adds up. ## Model cost pass-through > **In preview.** Send the model and token counts with a usage event: > > ```json > { "metric": "ai.summary", "model": "claude-sonnet-5-5", "input_tokens": 1830, "output_tokens": 412 } > ``` > > Dianthan prices the call at the provider's current rates, applies your markup (say, 3×), and deducts credits. Your dashboard shows revenue against model spend for each customer and each plan, so an unprofitable account stands out before the invoice arrives. ## Patterns - **Count requests, not attempts.** Wrap the model call and record usage in the success branch only. - **Use several metrics.** Record `ai.summary` (one per request) and `ai.tokens.output` (tokens). One is what you sell; the other is what you pay. - **Name metrics by feature.** `ai.summary` and `ai.chat` let you price features differently later, which a single `ai.calls` metric wouldn't. # Data JSON records grouped by resource, structured queries, search and file storage, all scoped to one app. Use it for an app's own data when you don't want to design and migrate tables yet. ## Records A record is a JSON document under a resource name you choose. ```bash curl -X POST https://api.dianthan.com/v1/records \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{"resource":"contacts","data":{"name":"Acme Ltd","status":"active","owner":"usr_x81…"}}' ``` ```json { "id": "rec_3kd…", "resource": "contacts", "data": { "name": "Acme Ltd", "status": "active", "owner": "usr_x81…" }, "created_at": "…" } ``` Read with `GET /v1/records/:id`, change fields with `PATCH /v1/records/:id` (fields you send are merged), and delete with `DELETE /v1/records/:id`. ## Queries Queries are JSON. You never build SQL strings, so nothing can be injected. ```bash curl -X POST https://api.dianthan.com/v1/query \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{ "resource": "contacts", "filter": { "status": "active" }, "sort": "-created_at", "limit": 50 }' ``` - `filter` matches fields by equality. Every field in it must match. - `sort` names a field, with a leading `-` for descending. - `limit` and `offset` page through results. ## Search ```bash curl -X POST https://api.dianthan.com/v1/search \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{"query":"acme","resources":["contacts"],"limit":20}' ``` Leave out `resources` to search every searchable resource in the app. ## Declaring a resource Once you know a resource's shape, register it to say which fields are allowed and whether it appears in search: ```bash curl -X POST https://api.dianthan.com/v1/resources \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{"resource":"contacts","description":"People and companies we work with","allowed_fields":["name","status","owner"],"searchable":true}' ``` ## Files Browsers upload files straight to Dianthan, so the bytes never pass through your server. Your server only starts the upload: ```bash # 1. Your server asks for an upload URL curl -X POST https://api.dianthan.com/v1/files/upload-url \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"filename":"report.pdf","mime_type":"application/pdf"}' # → { "file_id": "fil_…", "upload_url": "/v1/files/fil_…/content", "method": "PUT" } # 2. The browser PUTs the bytes to upload_url # 3. Mark it complete curl -X POST https://api.dianthan.com/v1/files/fil_…/complete -H "Authorization: Bearer $TOKEN" ``` Get a short-lived download link with `GET /v1/files/:id/download-url`. Files belong to the user who uploaded them unless you're using an app key. ## Isolation Every record, query, search and file is filtered by app. A key for one app gets `403` from every other app. Within an app, people using their own access token can only reach routes meant for them. Listing records requires an app key or an admin. > **In preview:** exporting all of an app's records and files as one archive. # Messaging Transactional email, web push, an in-app inbox and realtime channels. Every message is logged with its delivery status. ## Email ```bash curl -X POST https://api.dianthan.com/v1/messages/email \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{ "to": "ada@example.com", "subject": "Your export is ready", "text": "Download it here: https://fieldnotes.app/exports/42", "html": "

Download it here.

" }' ``` - `text` is required (up to 50 KB). `html` is optional (up to 100 KB). Mail clients that can't show HTML show the text. - Mail is sent from your app's verified domain. Add the domain and its DNS records in the dashboard first. - A provider failure returns `502 email_failed`, and the attempt is still logged. `GET /v1/messages` lists deliveries with recipient, subject, status and error, so you can check whether an email was sent before anyone asks. To send later or retry automatically, queue an `email` job instead. See [Automation](/docs/automation). ## Web push 1. Fetch the public key: `GET /v1/push/vapid-public-key`. 2. In the browser, subscribe with the Push API and send the subscription to `POST /v1/push/subscriptions`. 3. From your server, notify a user: ```bash curl -X POST https://api.dianthan.com/v1/push/send \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{"user_id":"usr_x81…"}' ``` Pushes wake the service worker without a payload. The worker then fetches what's new from your app, so message content never passes through push services. ## In-app inbox Each user has a notification list at `GET /v1/notifications`, with read state. Show it in a menu, mark items read, and use the unread count for a badge. ## Realtime channels Publish from your server: ```bash curl -X POST https://api.dianthan.com/v1/channels/publish \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{"channel":"user:usr_x81…","data":{"type":"export.ready","url":"/exports/42"}}' ``` Subscribe from the browser with server-sent events: ```ts const events = new EventSource(`/api/live?channel=user:${userId}`); // proxied to /v1/channels/:channel/subscribe events.onmessage = (e) => render(JSON.parse(e.data)); ``` Channels deliver from server to browser. To send from the browser, call your own API, which publishes. ## Preferences `GET /v1/me/notifications` and `PATCH /v1/me/notifications` store which messages a person wants and where. Read them before you send. Dianthan stores the choices, and your code decides how to respect them. > **In preview:** versioned email templates with variables and previews in the dashboard. # Automation Background jobs with retries, cron schedules, signed webhooks, an event feed and an audit log. ## Jobs A job is a piece of work that runs outside the request: now, after a delay, or at a set time. ```bash curl -X POST https://api.dianthan.com/v1/jobs \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{ "type": "http", "payload": { "url": "https://fieldnotes.app/api/rebuild-index", "body": { "full": true } }, "delay_seconds": 60, "max_attempts": 5 }' ``` | Type | Does | |---|---| | `http` | Calls an HTTPS URL on one of the app's registered domains, with optional `headers` and `body`. Any non-2xx response counts as a failure. | | `email` | Sends an email. The payload is the same as `POST /v1/messages/email`. | | `webhook` | Delivers an event to one of your webhook endpoints. | | `log` | Writes an analytics event. Useful for scheduled markers. | - Schedule with `delay_seconds` or `run_at` (ISO 8601). Leave both out to run as soon as possible. - `max_attempts` is 1 to 25. Failed attempts are retried with exponential backoff. - Inspect with `GET /v1/jobs/:id`, cancel with `POST /v1/jobs/:id/cancel`, run a failed job again with `POST /v1/jobs/:id/retry`. ## Schedules ```bash curl -X POST https://api.dianthan.com/v1/schedules \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{ "name": "nightly-digest", "cron": "0 6 * * *", "type": "http", "payload": { "url": "https://fieldnotes.app/api/digest" } }' ``` A schedule creates a job at each time it fires. Pause it with `PATCH /v1/schedules/:id` and `{"enabled": false}`. Schedules are checked every few minutes, so use a job with `run_at` when you need exact timing. ## Webhooks Subscribe an endpoint to events: ```bash curl -X POST https://api.dianthan.com/v1/webhooks \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{"url":"https://fieldnotes.app/api/hooks","events":["user.created","subscription.updated"]}' ``` Each delivery is signed with the webhook's secret. Check the signature before you trust the body. Deliveries are kept, so you can inspect them with `GET /v1/webhooks/:id/deliveries` and retry any of them with `POST /v1/webhooks/:id/deliveries/:delivery_id/retry`. Send a test event with `POST /v1/webhooks/:id/test`. ## Events and the audit log - `GET /v1/events` is the app's append-only stream of what happened: users created, files uploaded, subscriptions changed. - `GET /v1/audit-log` records who did what, with which credential, from which IP. Filter it by actor, action or resource. Neither can be edited or deleted through the API. > **In preview:** workflows. These are multi-step jobs that wait for each other, for a period of time or for an event, and resume after a failure. # Observe Product analytics, error tracking, request traces and alerts for every app, in one dashboard. ## Recording events ```bash curl -X POST https://api.dianthan.com/v1/logs \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{ "type": "export.completed", "actor_user_id": "usr_x81…", "body": { "format": "pdf", "pages": 14, "ms": 812 } }' ``` - `type` names the event. Use `noun.verb` in the past tense: `export.completed`, `invite.sent`. - `actor_user_id` is optional. It must be a user of this app, and it links the event to that person. - `body` holds whatever you want to break down by later. Read events back with `GET /v1/logs`, filtered by `type` or `actor_user_id`. That makes a "your activity" page a single request. `GET /v1/logs/counts` returns daily and total counts per type. ## The dashboard Your dashboard lists every app with sign-ups, sign-ins, active users and your own events, each compared with the previous period. Open an app for daily charts and breakdowns by any field in an event's body, such as success rate by format or failures by host. Sign-in uses a single-use emailed link. The dashboard sets no third-party cookies and loads no third-party assets. ## Errors and traces Report errors from your server or browser: ```bash curl -X POST https://api.dianthan.com/v1/telemetry/errors \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{"message":"Cannot read properties of undefined (reading \"id\")","stack":"…","url":"/exports/42"}' ``` Errors are grouped by cause. Dianthan also traces a sample of its own requests for each app, so `GET /v1/telemetry/traces` shows latency percentiles for each route without any extra setup. ## Alerts ```bash curl -X POST https://api.dianthan.com/v1/alerts \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{"name":"Slow API","metric":"p95_latency_ms","threshold":800,"window_minutes":15,"webhook_id":"whk_…"}' ``` Two metrics are available: `new_errors` (errors in the window above the threshold) and `p95_latency_ms` (latency above the threshold). Alerts are checked every few minutes. When one fires, it's delivered to the webhook you named, which can forward it to email, Slack or anything else. > **In preview:** a weekly digest across all your apps, and uptime checks against your public URLs with a status page you can share. ## Undo Every record change is written to the audit log with the key or person that made it and a copy of the record as it was before. That makes one credential's changes reversible: give each agent its own key, and if one does something it shouldn't, roll back everything it changed in a window. ```bash curl -X POST https://api.dianthan.com/v1/audit-log/revert \ -H "Authorization: Bearer $OWNER_TOKEN" -H "X-App-Id: app_…" -H "Content-Type: application/json" \ -d '{ "api_key_id": "key_…", "since": "2026-11-13T02:00:00Z" }' ``` The first call is a preview (`dry_run` defaults to `true`) and lists each change with how it would be undone: records the key created are deleted, changed records get their earlier version back, deleted records are brought back with their original id. Send `"dry_run": false` to apply it. A record that someone else has changed since is skipped and listed, never overwritten. The undo is itself written to the audit log as `audit.reverted`. Undo needs an owner or admin of the app, and covers records. The console has the same thing under **Operations, Audit and undo**. # Flags Feature flags with rules and stable percentage rollouts. Change who sees a feature without a deploy. ## Creating a flag ```bash curl -X POST https://api.dianthan.com/v1/flags \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{ "key": "editor.v2", "description": "The new editor", "default_value": false, "rules": [ { "conditions": [{ "attr": "plan", "op": "eq", "value": "pro" }], "value": true }, { "conditions": [{ "attr": "user_id", "op": "percent", "value": 20 }], "value": true } ] }' ``` Rules are checked in order, and the first one whose conditions all match decides the value. If none match, the flag returns `default_value`. Values can be any JSON up to 4 KB, not just `true` and `false`. ## Operators | `op` | Matches when the attribute | |---|---| | `eq`, `neq` | equals, or doesn't equal, `value` | | `in`, `not_in` | is, or isn't, in the list `value` (up to 100 items) | | `contains` | contains the string `value` | | `gt`, `gte`, `lt`, `lte` | compares as a number | | `percent` | falls in the first `value` percent of a stable hash | A flag can have up to 50 rules with up to 10 conditions each. ## Stable rollouts `percent` hashes the flag key together with the attribute's value. The same user always gets the same answer for the same flag. Raising a rollout from 20 to 50 keeps the first 20% and adds more, so nobody switches back and forth between versions. Different flags hash differently, so the same people aren't always the early group. ## Evaluating Evaluate every flag a page needs in one request: ```bash curl -X POST https://api.dianthan.com/v1/flags/evaluate \ -H "X-API-Key: dn_live_…" -H "X-App-Id: app_…" \ -H "Content-Type: application/json" \ -d '{"keys":["editor.v2","export.batch"],"context":{"user_id":"usr_x81…","plan":"pro","country":"PT"}}' ``` ```json { "flags": { "editor.v2": true, "export.batch": false } } ``` Pass up to 100 keys, or a single `key`. The `context` holds up to 50 attributes to target on. Dianthan doesn't store them. ## Kill switches To turn a feature off for everyone, set its first rule to an empty condition list with value `false`, or change `default_value` and remove the rules. `PATCH /v1/flags/:key` takes effect on the next evaluation. > **In preview:** experiments that assign variants and compare them using the events Observe already collects. # Models One endpoint for every major model. Models speaks the chat completions format that most libraries already use, so adopting it is a change of base URL. Each call is checked against your caps, priced, and logged against the customer who caused it. ## Making a call Point any OpenAI-compatible client at `https://api.dianthan.com/v1/ai` and use an app API key. ```ts import OpenAI from "openai"; const ai = new OpenAI({ baseURL: "https://api.dianthan.com/v1/ai", apiKey: process.env.DIANTHAN_KEY, }); const reply = await ai.chat.completions.create({ model: "anthropic/claude-sonnet-5.5", messages: [{ role: "user", content: "Summarise this note in one line." }], user: session.userId, }); ``` Or with plain HTTP: ```bash curl https://api.dianthan.com/v1/ai/chat/completions \ -H "Authorization: Bearer dn_live_…" \ -H "Content-Type: application/json" \ -d '{ "model": "anthropic/claude-sonnet-5.5", "messages": [{ "role": "user", "content": "Hello" }], "user": "usr_x81…", "metric": "ai.summary" }' ``` - `model` is a model id in `provider/model` form. `GET /v1/ai/models` lists every id available to the app, with its context length and price per token. - `user` names the customer the call is for. It is used for per-customer caps and cost, and it is never sent on to the model provider. - `metric` is an optional label of your own, such as `ai.summary`, so you can see cost per feature. - Everything else (`temperature`, `tools`, `response_format` and so on) is passed through unchanged. The response is the standard completion object. The price of the call in US dollars is in the `X-Dianthan-Cost-Usd` header and in `usage.cost`. ## Streaming Set `"stream": true`. Server-sent events arrive exactly as the provider sends them, and the last event before `[DONE]` carries `usage`, including the cost. Usage is recorded when the stream ends. ## Fallbacks List alternatives in `models`, in order of preference. If the first model is unavailable, rate-limited or refuses the request, the next one answers it. ```json { "model": "anthropic/claude-sonnet-5.5", "models": ["openai/gpt-5", "google/gemini-2.5-pro"], "messages": [{ "role": "user", "content": "Hello" }] } ``` The `model` field of the response tells you which one answered, and that is the one you are billed for. ## Allow-lists and caps Set them in the app's settings, from the dashboard or the API: ```bash curl -X PATCH https://api.dianthan.com/v1/apps/app_…/settings \ -H "Authorization: Bearer " -H "Content-Type: application/json" \ -d '{ "ai": { "models": ["anthropic/claude-sonnet-5.5", "openai/gpt-5"], "monthly_cap_usd": 200, "user_monthly_cap_usd": 2 } }' ``` | Setting | Effect | | --- | --- | | `models` | Only these ids may be called, including as fallbacks. Others get `403 model_not_allowed`. | | `monthly_cap_usd` | Model spend for the whole app this calendar month (UTC). Past it, calls get `402 spend_cap_reached`. | | `user_monthly_cap_usd` | The same, per `user`. Past it, that customer's calls get `402 user_cap_reached`. | Caps are checked before each call, so a single call can finish slightly over a cap; the next one is refused. ## Usage and cost ```bash curl "https://api.dianthan.com/v1/ai/usage?user=usr_x81…" -H "Authorization: Bearer dn_live_…" ``` ```json { "month": "2026-11", "user": "usr_x81…", "spent_usd": 0.4182, "cap_usd": 2 } ``` Leave out `user` for the whole app. Every call is also written to the app's event log as `ai.call` with the model, tokens, cost, latency, `user` and `metric`, so it can be read with `GET /v1/logs?name=ai.call` and appears in Observe. ## Margin per customer Dianthan takes your customers' payments through [Billing](/docs/billing) and routes their model calls, so it can set one against the other. Send the customer's id as `user` on each call: a Dianthan user id, a billing customer id, or the customer's email all match. ```bash curl "https://api.dianthan.com/v1/ai/margin?month=2026-11" -H "Authorization: Bearer dn_live_…" ``` ```json { "month": "2026-11", "totals": { "revenue_usd": 346, "model_cost_usd": 141.55, "margin_usd": 204.45, "margin_percent": 59.1, "customers": 5, "unprofitable": 2, "unattributed_cost_usd": 0.27 }, "customers": [ { "user": "usr_lumen…", "email": "ops@lumen.ai", "plan": "Pro", "revenue_usd": 49, "credits_used_usd": 0, "model_cost_usd": 71.2, "margin_usd": -22.2, "margin_percent": -45.3 } ] } ``` Customers come least profitable first. Revenue is what each customer paid in the month: paid invoices where there are any, otherwise the monthly price of their live subscription, plus credits they used. Cost is the provider price of their calls. Calls made without a `user` are counted in `unattributed_cost_usd`. The same view is in the console under **Customers, Margin**. ## Credits Credits let customers prepay for model use, so heavy use pays for itself. Turn them on for an app with a markup: ```bash curl -X PATCH https://api.dianthan.com/v1/apps/app_…/settings \ -H "Authorization: Bearer dn_live_…" -H "Content-Type: application/json" \ -d '{ "ai": { "credits": { "enabled": true, "markup_percent": 20 } } }' ``` Then grant credits from your own payment flow, for example the webhook your checkout sends when a top-up succeeds. Negative amounts correct a balance. ```bash curl -X POST https://api.dianthan.com/v1/ai/credits \ -H "Authorization: Bearer dn_live_…" -H "Content-Type: application/json" \ -d '{ "user": "usr_x81…", "amount_usd": 10, "note": "Top-up" }' ``` With credits on, every call must name a `user`. A call is refused with `402 insufficient_credits` once that user's balance is spent; otherwise it is charged at provider cost plus your markup, returned in the `x-dianthan-charged-usd` header, and counted as revenue in the margin view. Read a balance with `GET /v1/ai/credits?user=…`. ## Errors | Status | `error` | Meaning | | --- | --- | --- | | 400 | `bad_request` | Missing `model` or `messages`, or an invalid `user`. | | 400 | `upstream_error` | The provider rejected the request; `message` says why. | | 402 | `spend_cap_reached` / `user_cap_reached` | A monthly cap has been reached. | | 402 | `insufficient_credits` | Credits are on and this user's balance is spent. | | 403 | `model_not_allowed` | The model isn't in the app's allow-list. | | 429 | `upstream_error` | The provider is rate-limiting; retry with backoff or add a fallback. | | 502 | `upstream_error` | The provider failed. Add a fallback to absorb this. | ## Pricing Each call costs the provider's published price for the tokens it used, plus a fee stated on the [pricing page](/pricing). There is no separate subscription for Models. ## In preview - **Your own provider keys.** Use existing provider contracts while keeping caps, logs and fallbacks here. # CLI Every dashboard action as a command. Output is readable by default and JSON with `--json`, and the exit codes mean something, so the CLI works the same in your terminal, in CI and for an agent. > **In preview.** The commands below are the planned interface. Names and flags may change before general availability. ## Install ```bash npx dianthan --help # no install needed npm i -g dianthan # or install it globally ``` ## Start a project ```bash npx dianthan init ``` Run it in a project directory. It signs you in, creates an app (or links an existing one), asks which systems you want, and writes the app's environment to `.env.local`. Every step below can also be run on its own. ## Sign in ```bash dianthan login # opens a browser; no password dianthan whoami ``` In CI, set `DIANTHAN_TOKEN` to a personal key instead of running `login`. ## Apps ```bash dianthan apps create "Field Notes" --domain fieldnotes.app --signup open dianthan apps list dianthan apps domains add fieldnotes www.fieldnotes.app dianthan link fieldnotes # remembers the app for this directory ``` ## Systems ```bash dianthan add identity billing metering dianthan systems list dianthan remove flags ``` Removing a system stops new requests to it. Its data is kept for 30 days in case you turn it back on. ## Keys and environment ```bash dianthan keys create --name web-server dianthan keys create --name ci --scopes logs:read,flags:write --expires 30d dianthan keys rotate key_… dianthan env pull # writes DIANTHAN_URL, DIANTHAN_APP_ID and DIANTHAN_KEY to .env.local ``` ## Billing ```bash dianthan plans create pro --price 12/month --entitle export.pdf --entitle seats=5 dianthan plans list ``` ## Flags ```bash dianthan flags create editor.v2 --default false dianthan flags rollout editor.v2 20 dianthan flags off editor.v2 ``` ## Observing ```bash dianthan logs tail --type export.completed dianthan errors list --since 24h ``` ## Exit codes | Code | Meaning | |---|---| | `0` | Success | | `1` | The request failed. The message says why. | | `2` | Invalid usage, such as a missing argument or an unknown flag | | `3` | Not signed in, or the key lacks the scope | | `4` | Not found | # Agents and MCP Dianthan is designed to be operated by coding agents as well as by people. This page covers the MCP server, the files written for models, and how to give an agent access you can trust. > **In preview.** The MCP server and the agent skill are in preview. `/llms.txt` is available now. ## Documentation for models [`/llms.txt`](/llms.txt) is this entire documentation as one plain-text file. Paste its URL into any agent and it has everything it needs to integrate an app. ## The MCP server Install it for Claude Code, Cursor or any client that speaks the Model Context Protocol: ```bash npx dianthan mcp install ``` Or add it by hand: ```json { "mcpServers": { "dianthan": { "url": "https://api.dianthan.com/mcp", "headers": { "Authorization": "Bearer dn_agent_…" } } } } ``` ### Tools | Tool | Does | |---|---| | `apps.list`, `apps.create` | List and create apps, register domains | | `systems.add`, `systems.remove` | Switch systems on and off | | `keys.create` | Create a scoped server key and write it to the local environment | | `plans.create`, `plans.update` | Define plans and their entitlements | | `flags.set`, `flags.rollout` | Create flags and change rollouts | | `records.query` | Read an app's records with structured filters | | `logs.query`, `errors.list` | Read events, errors and traces to debug | | `docs.search` | Search this documentation | ## Giving an agent a key Create a key for the agent, not for yourself: ```bash dianthan keys create \ --name "claude-code on ada's laptop" \ --scopes records:read,records:write,logs:read \ --expires 7d ``` - **Scope it.** Give it only the scopes the task needs. A key that lists scopes can use only the routes they cover: `records:read`, `records:write`, `files:read`, `files:write`, `logs:read`, `logs:write`, `usage:write`, `models:call` and `messages:send`. Anything else is refused with `403 insufficient_scope`. - **Limit it to one app** with `--app` when the work concerns one product. - **Make it expire.** A key that ends after the week's work can't be forgotten in a dotfile. Every change the key makes is written to the audit log with the key that made it, and its record changes can be [undone in one step](/docs/observe#undo). Revoke it from the console and its access ends on the next request. ## The agent skill A skill file teaches an agent the conventions in these docs: keep keys on the server, store refresh tokens in `httpOnly` cookies, record usage only after success, check entitlements rather than prices. Install it into a project: ```bash npx dianthan skill install # writes .claude/skills/dianthan/SKILL.md ``` ## Prompts that work well - "Add Dianthan sign-in with magic links and Google to this Next.js app." - "Create a Pro plan at $12 a month with PDF export, and gate the export button on it." - "Record a usage event for every summary we generate, and stop at 500 a month on the free plan." - "Look at this app's errors from the last day and fix the most frequent one." # API reference Conventions shared by every endpoint, followed by an index of routes grouped by system. Each system's guide covers request and response bodies in detail. ## Base URL and versioning ```text https://api.dianthan.com/v1 ``` The version is part of the path. Within `v1`, changes are additive: new fields, new endpoints and new optional parameters. Write clients that ignore fields they don't recognise. Anything that would break existing clients ships as `v2`, and `v1` keeps working for at least twelve months after that. ## Authentication | Header | When | |---|---| | `Authorization: Bearer ` | Acting as a signed-in person, or as yourself | | `X-API-Key: dn_live_…` | Acting as your server, for one app | | `X-App-Id: app_…` | Selects the app. Required with a server key; when sent with a token, it takes precedence over the app in the token | | `X-Client-IP: ` | Your visitor's IP when your server calls on their behalf. Only honoured with server keys | ## Requests and responses Bodies are JSON with `Content-Type: application/json`. Field names are `snake_case`. Timestamps are ISO 8601 in UTC. Money is an integer number of the currency's smallest unit, with a separate `currency` field. IDs carry a prefix for their type: | Prefix | Type | Prefix | Type | |---|---|---|---| | `usr_` | User | `sub_` | Subscription | | `app_` | App | `fil_` | File | | `org_` | Organization | `job_` | Job | | `key_` | API key | `whk_` | Webhook | ## Errors Every error has the same shape: ```json { "error": "quota_exceeded", "message": "This app has used its 1,000,000 usage events for November." } ``` `error` is a stable machine-readable code you can branch on. `message` is written for a developer reading logs and may be reworded at any time. | Status | Meaning | |---|---| | `400` | The request is invalid. `message` names the field and the rule it broke. | | `401` | No valid credential | | `403` | The credential is valid but can't do this, for example a key for another app or a disabled system | | `404` | Not found, or not visible to this credential | | `409` | Conflicts with existing state, such as a duplicate key | | `410` | The endpoint has been removed. `message` says what replaces it. | | `429` | Rate limited. Wait for the number of seconds in `Retry-After`. | | `501` | The feature needs configuration first, such as connecting a merchant account before checkout | ## Rate limits Each app has a per-minute quota, and sign-in endpoints have stricter per-IP limits. Responses include `RateLimit-Limit` and `RateLimit-Remaining`, and a `429` includes `Retry-After`. If you send `X-Client-IP` from your server, per-IP limits apply to your visitors individually, not to your server as a whole. ## Route index ### Identity ```text POST /v1/auth/magic-link GET /v1/auth/oauth/:provider POST /v1/auth/grants/exchange GET /v1/auth/sso/:connection_id/start POST /v1/auth/refresh POST /v1/auth/logout GET /v1/auth/sessions DELETE /v1/auth/sessions[/:id] POST /v1/auth/register POST /v1/auth/step-up GET /v1/me PATCH /v1/me GET /v1/me/mfa POST /v1/me/mfa/totp/setup POST /v1/me/mfa/totp/verify POST /v1/me/mfa/recovery GET /v1/users GET /v1/users/:id POST /v1/organizations GET /v1/organizations/:id/members POST /v1/organizations/:id/invitations POST /v1/invitations/:id/accept POST /v1/organizations/:id/roles POST /v1/organizations/:id/sso GET /v1/permissions ``` ### Billing ```text GET /v1/billing/plans GET /v1/billing/plans/:id POST /v1/billing/checkout POST /v1/billing/portal GET /v1/billing/subscriptions POST /v1/billing/subscriptions/:id/cancel POST /v1/billing/subscriptions/:id/resume GET /v1/billing/invoices GET /v1/me/entitlements[/:key] GET /v1/organizations/:id/entitlements ``` ### Metering ```text POST /v1/usage/events GET /v1/usage[/:metric] GET /v1/organizations/:id/usage GET /v1/apps/:id/usage ``` ### Data ```text POST /v1/records GET /v1/records[/:id] PATCH /v1/records/:id DELETE /v1/records/:id POST /v1/query POST /v1/search POST /v1/resources GET /v1/resources POST /v1/files/upload-url PUT /v1/files/:id/content POST /v1/files/:id/complete GET /v1/files/:id/download-url ``` ### Messaging ```text POST /v1/messages/email GET /v1/messages GET /v1/push/vapid-public-key POST /v1/push/subscriptions POST /v1/push/send GET /v1/notifications POST /v1/notifications/:id/read POST /v1/notifications/read-all POST /v1/channels/publish GET /v1/channels/:channel/subscribe ``` ### Automation ```text POST /v1/jobs GET /v1/jobs[/:id] POST /v1/jobs/:id/cancel POST /v1/jobs/:id/retry POST /v1/schedules PATCH /v1/schedules/:id POST /v1/webhooks GET /v1/webhooks/:id/deliveries POST /v1/webhooks/:id/test POST /v1/webhooks/:id/deliveries/:delivery_id/retry GET /v1/events GET /v1/audit-log ``` ### Observe ```text POST /v1/logs GET /v1/logs[/:id] GET /v1/logs/counts POST /v1/telemetry/errors GET /v1/telemetry/traces POST /v1/alerts GET /v1/audit-log POST /v1/audit-log/revert ``` ### Flags ```text POST /v1/flags GET /v1/flags[/:key] PATCH /v1/flags/:key DELETE /v1/flags/:key POST /v1/flags/evaluate ``` ### Models ```text POST /v1/ai/chat/completions GET /v1/ai/models GET /v1/ai/usage GET /v1/ai/margin POST /v1/ai/credits GET /v1/ai/credits ``` ### System ```text GET /health GET /v1/system/status GET /v1/system/capabilities ```