# For AI assistants and agents Source: https://docs.orvlin.com/ai-agents Use these docs with ChatGPT, Claude, Cursor, or your own coding agent, and the facts they need to get Orvlin right These docs are written so that people and AI tools can both use them. This page explains how to give them to an assistant, and lists the rules an assistant most often gets wrong. ## Give the docs to your assistant | Format | URL | Use it for | | - | - | - | | Index | [/llms.txt](/llms.txt) | A list of every page with a one-line summary. Good for choosing pages. | | Full text | [/llms-full.txt](/llms-full.txt) | Every page in one file. Good for a large context window. | | One page | Add `.md` to any page URL, such as [/quickstart.md](/quickstart.md) | A single page as Markdown, for a focused question. | Every page also has **Copy** and **Open in** actions at the top, which send the page to ChatGPT, Claude, or Cursor. For integration work, give the assistant [/guides/integrate-your-app.md](/guides/integrate-your-app.md) and the [API reference overview](/api-reference/overview.md). That covers most questions. ## Facts to get right Assistants frequently get the points below wrong. Check generated code against them. ### Authentication * Server requests send `Authorization: Bearer `. Keys start with `orv_`. * The React SDK and browser code use a **portal token** from `POST /v1/portal/sessions`. Never put an API key in browser code. * Creating or revoking API keys, and managing webhooks, requires a **signed-in user session**, not an API key. ### Identifiers * Subscriptions take `planId`, the plan's ID. They don't take the plan name. * Customer IDs are yours, or generated as `cust_…`. Letters, digits, and `_ . : -` only, up to 128 characters. * Events, grants, and purchases accept `idempotencyKey`. Reusing a key for an event counts it once. Reusing a key for a grant or purchase returns the original record. ### Usage * Send events with `POST /v1/events`. A single object, an array, or `{ "events": [...] }` are all accepted. * `eventName` must match the metric's events, or the usage isn't billed. * Track usage after the work succeeds. Check entitlements before the work starts. ### Entitlements and credits * `check(customerId, featureId, { amount })` returns `hasAccess`, `remaining`, and `source`. `remaining` is `null` for no cap. * `checkCredits` (Node) and `check_credits` (Python) check a balance without spending it. * Credit grants and purchases use `POST /v1/customers/:id/wallets/:type/grants` and `/purchases`. Test clocks only work in sandbox. ### Webhooks * Signature: HMAC-SHA256 over `X-Orvlin-Timestamp + "." + raw body`, using the endpoint secret that starts with `whsec_`. * Verify the raw body before parsing. Reject timestamps older than a few minutes. * Deliveries can repeat. Make handlers idempotent on the event `id`. * Endpoints must use HTTPS in production. ### Errors | Status | Meaning | | - | - | | `400` | Invalid input. Don't retry unchanged. | | `401` | Key missing, invalid, or revoked. | | `403` | Key's role can't do this. | | `404` | Not found in this workspace. | | `429` | Rate limited. Retry with backoff. | | `500` / `503` | Retry with backoff and the same idempotency key. | ### Environments * New workspaces are sandbox. Sandbox payments never reach a real processor. * Use separate API keys for sandbox and live. Never use a live key in tests. ## Ask good questions Assistants answer better with the specifics. Include: 1. The language and framework (for example, Next.js with the Node SDK, or FastAPI with the Python SDK). 2. The billable action and how you charge for it. 3. Whether the code runs on a server or in a browser. 4. The exact error response you got, including the status code. ## Confirm before relying on it Where a page says to confirm something with your account team, don't let an assistant state it as fact. Check it in your workspace first. # API keys Source: https://docs.orvlin.com/api-reference/api-keys Create, list, and revoke API keys, and choose the right role for each one API keys let your servers call the Orvlin API. Each key has a name, a role, and a secret. A key's secret is shown once, when it is created. Store it in your secret manager. If you lose it, create a new key and revoke the old one. ## Create a key ```http theme={null} POST /v1/api-keys ``` Creating a key requires a signed-in user session, so you can't create a key with another key. ```json theme={null} { "name": "Production backend", "role": "developer" } ``` | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | A label so you can recognise the key later. | | `role` | string | No | One of `owner`, `admin`, `developer`, `viewer`, or `billing`. Defaults to `admin`. | Response (`201 Created`): ```json theme={null} { "data": { "id": "key_01JX…", "name": "Production backend", "role": "developer" }, "secret": "orv_xxxxxxxx_your_secret" } ``` `secret` appears only in this response. ## Roles Give each key the smallest role that does its job. | Role | Typical use | | - | - | | `viewer` | Read-only. Can check entitlements and balances. | | `developer` | Sends usage events and manages day-to-day customer data. | | `billing` | Works with invoices and payments. | | `admin` | Manages the catalog, customers, and settings. | | `owner` | Full control of the workspace. | A key can't be created with a higher role than the person creating it. A `developer` user can't create an `admin` key; the key is created with the `developer` role instead. ## List keys ```http theme={null} GET /v1/api-keys ``` Returns each key's `id`, `name`, and `role`. The secret is never returned after creation. ## Revoke a key ```http theme={null} DELETE /v1/api-keys/:id ``` Revoked keys stop working immediately. Requests with them return `401`. ```json theme={null} { "success": true } ``` Revoking requires a signed-in user session. ## Rotate a key 1. Create a new key with the same role. 2. Deploy your application with the new key. 3. Revoke the old key. # Approvals Source: https://docs.orvlin.com/api-reference/approvals Read and change approval policies, and approve or reject requests Approvals hold money-moving actions until a second person decides on them. For how the feature works, see [Approvals](/enterprise/approvals). ## Policies ### List policies ```http theme={null} GET /v1/approvals/policies ``` Returns one policy per action, with a `label` and whether the action `hasAmount`: ```json theme={null} { "data": [ { "action": "credit.grant", "label": "Grant a credit", "hasAmount": true, "enabled": true, "thresholdAmount": 500, "thresholdCurrency": "USD", "updatedAt": "2026-10-13T09:00:00.000Z" } ] } ``` ### Update a policy ```http theme={null} PUT /v1/approvals/policies/:action ``` Only workspace admins can change policies. Unknown actions return `404`. | Field | Type | Required | Description | | - | - | - | - | | `enabled` | boolean | Yes | Whether the action needs approval. | | `thresholdAmount` | number or `null` | No | Only amounts at or above this need approval. `null` applies to every amount. | | `thresholdCurrency` | string or `null` | No | Three-letter ISO code. | The `action` must be one of `credit.grant`, `invoice.void`, `invoice.credit_note`, or `subscription.cancel`. ## Requests ### List requests ```http theme={null} GET /v1/approvals ``` Returns pending and past requests, most recent first. ### Get a request ```http theme={null} GET /v1/approvals/:id ``` Returns `404` if the request isn't in your workspace. ### Approve ```http theme={null} POST /v1/approvals/:id/approve ``` ```json theme={null} { "note": "Matches the signed credit agreement" } ``` `note` is optional, up to 1000 characters. ### Reject ```http theme={null} POST /v1/approvals/:id/reject ``` ```json theme={null} { "note": "Amount doesn't match the order" } ``` `note` is optional, up to 1000 characters. A rejected request doesn't run. ## Status values `pending`, `approved`, `executing`, `executed`, `failed`, `rejected`, `expired`. See the [lifecycle](/enterprise/approvals#request-lifecycle). # Audit logs Source: https://docs.orvlin.com/api-reference/audit-logs List and export audit log entries for your workspace For what is recorded and how to use the log, see [Audit logs](/enterprise/audit-logs). ## List entries ```http theme={null} GET /v1/audit ``` Requires a signed-in user session with access to audit logs. | Parameter | Type | Default | Description | | - | - | - | - | | `action` | string | | An exact action, or a prefix ending in `*`, such as `customer.*`. | | `resourceType` | string | | The kind of resource, such as `invoice`. | | `resourceId` | string | | One specific resource ID. | | `since` | string | | ISO 8601 start, inclusive. | | `until` | string | | ISO 8601 end, inclusive. | | `limit` | integer | `50` | Entries per page. Maximum `1000` for JSON, `10000` for CSV. | | `offset` | integer | `0` | Entries to skip. | | `format` | string | `json` | Set to `csv` to download a CSV file. | A malformed `since` or `until` returns `400`. ### Example response ```json theme={null} { "data": [ { "action": "invoice.void", "resourceType": "invoice", "resourceId": "inv_01JX…", "createdAt": "2026-10-12T14:05:00.000Z" } ] } ``` ## Export as CSV ```bash theme={null} curl -s "$ORVLIN_API/v1/audit?since=2026-10-01T00:00:00Z&format=csv" \ -H "Authorization: Bearer $ADMIN_SESSION_TOKEN" -o audit.csv ``` A CSV export returns up to `limit` entries, with a maximum of `10000` per request. Page with `offset` for larger exports. # Authentication Source: https://docs.orvlin.com/api-reference/authentication Authenticate API requests with API keys, and manage dashboard sessions Your application authenticates with an **API key**. Dashboard users sign in with a **session**. The two are separate: a key is for servers, and a session is for people. ## API keys Send your key as a Bearer token on every request: ```http theme={null} Authorization: Bearer orv_xxxxxxxx_your_secret ``` The `x-api-key` header is also accepted: ```http theme={null} x-api-key: orv_xxxxxxxx_your_secret ``` Keys start with `orv_`. Create and revoke them with the [API keys endpoints](/api-reference/api-keys) or in the dashboard. A key's role decides what it can do. See [API keys](/api-reference/api-keys#roles). Keep keys on your server. Never put them in browser code or mobile apps. For the browser, use a [customer portal](/api-reference/portal) session. ## Sessions Dashboard users sign in and receive an access token and a refresh token. Send the access token as a Bearer token too. ### Register Creates a user and a workspace. ```http theme={null} POST /v1/auth/register ``` | Field | Type | Required | Description | | - | - | - | - | | `email` | string | Yes | The user's email address. Used to sign in. | | `password` | string | Yes | The password. At least 8 characters. | | `name` | string | Yes | The user's name. | | `companyName` | string | No | The workspace name. | ### Log in ```http theme={null} POST /v1/auth/login ``` | Field | Type | Required | Description | | - | - | - | - | | `email` | string | Yes | The user's email address. | | `password` | string | Yes | The password. | Response: ```json theme={null} { "user": { "id": "usr_...", "email": "you@example.com", "name": "Ada", "accountId": "acc_...", "role": "admin", "status": "active" }, "tokens": { "accessToken": "eyJhbG...", "refreshToken": "786370...", "expiresIn": 900 } } ``` The access token expires after `expiresIn` seconds (900 by default). ### Refresh Exchange a refresh token for a new access token: ```http theme={null} POST /v1/auth/refresh ``` ```json theme={null} { "refreshToken": "786370..." } ``` ### Log out ```http theme={null} POST /v1/auth/logout ``` ### Current user ```http theme={null} GET /v1/users/me ``` ### Other sign-in methods * `POST /v1/auth/magic-link` with `{ "email": "…" }` sends a sign-in link. `POST /v1/auth/magic-link/verify` completes it. * `POST /v1/auth/forgot-password` with `{ "email": "…" }` sends a reset link. `POST /v1/auth/reset-password` sets the new password. * Single sign-on: `POST /v1/auth/sso/start` begins a sign-in, and `POST /v1/auth/sso/exchange` completes it. See [Single sign-on](/guides/sso). ## Errors A missing or invalid credential returns `401`. A valid credential without the right role returns `403`. See [Errors](/api-reference/errors). # Credits Source: https://docs.orvlin.com/api-reference/credits Define credit types, and grant, sell, and track credits in customer wallets Credits are prepaid balances. A credit type defines what the credits are for. Each customer has a wallet for each credit type. For how credits fit together, see [Prepaid credits](/guides/prepaid-credits). ## Credit types | Method and path | Description | | - | - | | `GET /v1/credit-types` | List credit types. | | `POST /v1/credit-types` | Create a credit type. | | `GET /v1/credit-types/:type` | Get one credit type. | | `PATCH /v1/credit-types/:type` | Update it. | | `GET /v1/credit-types/:type/rates` | List the credit type's rates. | | `POST /v1/credit-types/:type/rates` | Add a rate. | | `DELETE /v1/credit-types/:type/rates/:rateId` | Remove a rate. | | `GET /v1/credit-types/:type/allowances` | List allowances. | | `POST /v1/credit-types/:type/allowances` | Add an allowance. | | `DELETE /v1/credit-types/:type/allowances/:allowanceId` | Remove an allowance. | A credit type's `overage` setting decides what happens at zero: `block` stops usage, and `allow` lets it continue and bills the overage. ## Wallets Each path is per customer and credit type. | Method and path | Description | | - | - | | `GET /v1/customers/:id/wallets` | Balances for every credit type. | | `GET /v1/customers/:id/wallets/:type` | The balance for one credit type. | | `POST /v1/customers/:id/wallets/:type/grants` | Grant credits. | | `POST /v1/customers/:id/wallets/:type/purchases` | Sell credits. | | `POST /v1/customers/:id/wallets/:type/blocks/:blockId/void` | Void a block of credits. | | `GET /v1/customers/:id/wallets/:type/ledger` | The history of grants, purchases, and draw-downs. | | `GET /v1/customers/:id/wallets/:type/auto-top-up` | Read the auto top-up settings. | | `PUT /v1/customers/:id/wallets/:type/auto-top-up` | Set the auto top-up settings. | | `DELETE /v1/customers/:id/wallets/:type/auto-top-up` | Turn off auto top-up. | ### Grant body | Field | Type | Required | Description | | - | - | - | - | | `amount` | number | Yes | How many credits to grant. Must be positive. | | `source` | string | No | `grant`, `promotional`, or `purchase`. | | `priority` | integer | No | 0 to 1000. Lower numbers are used first. | | `effectiveAt` | string | No | When the credits become available. | | `expiresAt` | string | No | When the credits expire. | | `costBasis` | number | No | What you paid for the credits, for reporting. | | `currency` | string | No | Three-letter ISO code. | | `description` | string | No | A note for the ledger. | | `idempotencyKey` | string | No | Makes a retry grant the credits once. | ### Purchase body | Field | Type | Required | Description | | - | - | - | - | | `credits` | number | Yes | How many credits to sell. | | `unitPrice` | number | No | Price per credit. Defaults to the credit type's price. | | `currency` | string | No | Three-letter ISO code. | | `expiresAt` | string | No | When the purchased credits expire. | | `collection` | string | No | `charge` (card on file, default) or `send_invoice`. | | `description` | string | No | A note for the ledger. | | `idempotencyKey` | string | No | Makes a retry sell the credits once. | A purchase starts as `pending_payment`. It becomes `active` when paid. ### Auto top-up body | Field | Type | Required | Description | | - | - | - | - | | `threshold` | number | Yes | Top up when the balance is at or below this. | | `credits` | number | Yes | How many credits to buy each time. | | `unitPrice` | number | No | Price per credit. | | `maxPerMonth` | integer | No | Most top-ups per calendar month. Default 5. | | `enabled` | boolean | No | Turn the rule on or off. | ## Check a balance ```http theme={null} GET /v1/wallets/check?customerId=cust_acme&creditType=ai_images&quantity=1 ``` | Parameter | Description | | - | - | | `customerId` | Required. The customer to check. | | `creditType` | The credit type to check. | | `eventName` | The event being checked, when the credit rate depends on the event. | | `quantity` | How many units are being used. Must be zero or more. | | `amount` | An amount to check instead of a quantity. Must be zero or more. | Returns whether the balance covers the request, and the balance itself. The [Node](/sdks/node#credits) and [Python](/sdks/python#credits) SDKs wrap this call as `checkCredits` and `check_credits`. # Customers Source: https://docs.orvlin.com/api-reference/customers Create, read, update, and delete customers, and read their entitlements A customer is the party you bill. Every subscription, invoice, event, and entitlement belongs to one. ## Create a customer ```http theme={null} POST /v1/customers ``` ```json theme={null} { "id": "cust_acme", "name": "Acme Corp", "email": "billing@acme.example", "billingEmail": "ap@acme.example", "currency": "USD", "externalId": "crm_0042", "metadata": { "region": "eu-west" } } ``` | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | The customer's name. | | `id` | string | No | Your identifier. Letters, digits, and `_ . : -`, up to 128 characters. Generated as `cust_…` if omitted. | | `email` | string | No | The customer's contact email. | | `billingEmail` | string | No | Where invoices are sent, if different from `email`. | | `currency` | string | No | Three-letter ISO code. Defaults to `USD`. | | `externalId` | string | No | An ID from another system, such as your CRM. | | `metadata` | object | No | Any key-value pairs you want to store. | Responds `201 Created` with the customer. ## List customers ```http theme={null} GET /v1/customers?limit=100&offset=0 ``` | Parameter | Default | Description | | - | - | - | | `limit` | `100` | How many customers to return. | | `offset` | `0` | How many to skip, for paging. | Response: `{ "data": [ … ] }`. ## Get a customer ```http theme={null} GET /v1/customers/:id ``` ## Update a customer ```http theme={null} PATCH /v1/customers/:id ``` Send only the fields you want to change. ## Delete a customer ```http theme={null} DELETE /v1/customers/:id ``` ## Entitlements Entitlements show what a customer can use and how much of each allowance remains. ### All entitlements ```http theme={null} GET /v1/customers/:id/entitlements ``` ```json theme={null} { "customerId": "cust_acme", "entitlements": [ { "featureId": "api_calls", "featureName": "API calls", "type": "numeric", "hasAccess": true, "allocated": 100000, "consumed": 42000, "remaining": 58000, "pricingModel": "unit" } ] } ``` `allocated` and `remaining` are `null` for a feature with no cap. ### One feature ```http theme={null} GET /v1/customers/:id/entitlements/:featureName ``` ### Entitlement grants Grant a customer a feature or a higher limit outside their plan. | Method and path | Description | | - | - | | `GET /v1/customers/:id/entitlement-grants` | List the customer's grants. | | `POST /v1/customers/:id/entitlement-grants` | Create a grant. | | `DELETE /v1/entitlement-grants/:grantId` | Remove a grant. | Create a grant with `featureId` (required), and optionally `limit`, `reason`, and `expiresAt`. See [Entitlements](/guides/entitlements#grant-access-directly). ## Usage limits Limits cap a customer's usage in real time, separately from entitlements. Manage them under `/v1/usage-limits`, and check one with `POST /v1/usage-limits/check`. A customer's own limits are at `/v1/customers/:id/usage-limits`. ## Privacy requests * `GET /v1/customers/:id/data-export` exports everything stored about a customer. * `POST /v1/customers/:id/erase` deletes a customer's personal data, as required by privacy law. This cannot be undone. # Errors Source: https://docs.orvlin.com/api-reference/errors Error responses and how to handle them When a request fails, Orvlin returns a non-2xx status code and a JSON body. ```json theme={null} { "error": "Invalid input", "details": [ { "path": ["name"], "message": "Required" } ] } ``` * `error` is a message for developers. Don't show it to end users. * `details` is present for validation failures. It lists each problem with its field path. ## Status codes | Status | Meaning | What to do | | - | - | - | | `400 Bad Request` | The request is invalid. For example, a required field is missing or a value is out of range. | Fix the request. Don't retry it unchanged. | | `401 Unauthorized` | The API key is missing, invalid, or revoked. | Check the key. See [Authentication](/api-reference/authentication). | | `403 Forbidden` | The key is valid but its role can't do this. | Use a key with a higher role, or check whether the action is allowed for your plan. | | `404 Not Found` | The resource doesn't exist in your workspace. | Check the ID. Resources in other workspaces also return `404`. | | `429 Too Many Requests` | You've sent too many requests. | Wait, then retry with exponential backoff. | | `500 Internal Server Error` | Something went wrong on our side. The message is hidden. | Retry with backoff. If it continues, contact support with the request time. | | `503 Service Unavailable` | Orvlin is temporarily unavailable, for example during maintenance. | Retry after a few minutes. Responses with `"code": "maintenance"` mean changes are paused. | ## Retrying safely Retry `429`, `500`, and `503` responses with exponential backoff and a random jitter. Don't retry `400`, `401`, `403`, or `404`; fix the request first. When you retry a request that creates a record, send the same idempotency key so the record isn't created twice. See [Idempotency](/api-reference/overview#idempotency). ## Handling errors in the SDKs The [Node.js](/sdks/node#errors) and [Python](/sdks/python#errors) SDKs raise an error with the HTTP status on it. Check the status to decide whether to retry. # Events Source: https://docs.orvlin.com/api-reference/events Send usage events and list them Events are the raw usage records that metrics aggregate. Send them as customers do things in your product. See [Usage metering](/guides/usage-metering) for how to design them. ## Send events ```http theme={null} POST /v1/events ``` Send one event, an array of events, or an object with an `events` array: ```json theme={null} { "events": [ { "customerId": "cust_acme", "eventName": "api_call", "timestamp": "2026-10-12T09:30:00Z", "idempotencyKey": "req_8f2c1a", "properties": { "endpoint": "/v1/search", "method": "POST", "statusCode": 200, "latencyMs": 142 } } ] } ``` ### Event fields | Field | Type | Required | Description | | - | - | - | - | | `customerId` | string | Yes | The customer who did the action. | | `eventName` | string | Yes | The kind of event, such as `api_call`. Matches the name your metrics use. | | `timestamp` | string | No | When it happened, in ISO 8601. Defaults to the time of ingestion. | | `idempotencyKey` | string | No | An ID for this action. Events with the same key are counted once. | | `properties` | object | No | Values that metrics aggregate, such as `latencyMs` or `tokens`. | ### Response ```json theme={null} { "accepted": ["01KWMD..."], "rejected": [] } ``` `accepted` lists the IDs of stored events. `rejected` lists events that failed validation, with the reason, so you can fix and resend them. ## List events ```http theme={null} GET /v1/events ``` | Parameter | Type | Description | | - | - | - | | `customer_id` | string | Only events for this customer. | | `event_name` | string | Only events with this name. | | `start_time` | string | Events at or after this ISO 8601 time. | | `end_time` | string | Events before this ISO 8601 time. Defaults to now. | | `limit` | number | How many to return. Defaults to 100. | When no time range is given, the list covers the last 30 days. ## Backfill events To load historical usage, send the events to `POST /v1/events/backfill`. Use it for past periods, not for live traffic. Send live events to `POST /v1/events`. ## Query events `POST /v1/events/query` runs a query over stored events. Use it to explore your data. Billing is computed from [metrics](/api-reference/metrics), not from ad-hoc queries. # Invoices Source: https://docs.orvlin.com/api-reference/invoices List, approve, pay, void, and credit invoices, and read their payment history Invoices are issued for subscription periods, credit purchases, and manual charges. For the lifecycle and what each action means, see [Invoicing and collection](/guides/invoicing-and-collection). ## List invoices ```http theme={null} GET /v1/invoices?status=open&customer_id=cust_acme&page=1&page_size=20 ``` | Parameter | Default | Description | | - | - | - | | `status` | | Only invoices with this status: `draft`, `open`, `paid`, `void`, or `uncollectible`. | | `customer_id` | | Only invoices for this customer. | | `page` | `1` | Which page to return. | | `page_size` | `20` | Invoices per page. Maximum 100. | Response: ```json theme={null} { "data": [ { "id": "inv_01JX…", "customerId": "cust_acme", "status": "paid", "total": 42.5, "currency": "USD", "dueDate": "2026-11-01T00:00:00Z" } ], "pagination": { "total": 1, "page": 1, "pageSize": 20 } } ``` ## Get an invoice ```http theme={null} GET /v1/invoices/:id ``` ## Approve Moves a `draft` invoice to `open`. ```http theme={null} POST /v1/invoices/:id/approve ``` ## Pay Charges the customer's card on file now. ```http theme={null} POST /v1/invoices/:id/pay ``` Returns `400` if the invoice is already `paid` or has been `void`. In a sandbox workspace the charge is simulated and never reaches a payment processor. ## Void Cancels an invoice. A voided invoice can't be paid. ```http theme={null} POST /v1/invoices/:id/void ``` ## Credit note Issues a credit against a paid invoice. ```http theme={null} POST /v1/invoices/:id/credit-note ``` ## PDF and hosted link | Method and path | Description | | - | - | | `GET /v1/invoices/:id/pdf` | The invoice as a PDF. | | `GET /v1/invoices/:id/link` | A hosted page where the customer can view and pay. | The hosted page is also available without authentication at `/i/:token`, using the token from the link. ## Payment attempts ```http theme={null} GET /v1/invoices/:id/payment-attempts ``` Lists each charge attempt for the invoice, including failed ones. ## Payment retries Configure how failed charges are retried: | Method and path | Description | | - | - | | `GET /v1/payment-retry/settings` | The current retry schedule. | | `PUT /v1/payment-retry/settings` | Change the schedule. | | `GET /v1/payment-retry/summary` | Counts of invoices being retried. | | `GET /v1/dunning/processes` | Customer reminders sent during collection. | | `GET /v1/dunning/policies` | Reminder policies. | ## Invoice settings Billing settings, including invoice details, are read with `GET /v1/billing-settings` and changed with `PUT /v1/billing-settings`. ## Revenue Recognised revenue for invoices is available from: * `POST /v1/revenue/generate`: create the recognition schedule. * `GET /v1/revenue/summary`: totals by period. * `GET /v1/revenue/waterfall`: how deferred revenue changes over time. * `GET /v1/revenue/contracts`: revenue per contract. # Licenses Source: https://docs.orvlin.com/api-reference/licenses Allocate and deactivate per-seat licenses for customers Licenses are seats a customer holds under a `license` price. Allocate one for each seat you sell, and deactivate it when the seat is freed. ## Allocate a license ```http theme={null} POST /v1/licenses ``` ```json theme={null} { "customerId": "cust_acme", "licenseTypeId": "seats" } ``` | Field | Type | Required | Description | | - | - | - | - | | `customerId` | string | Yes | The customer who holds the seat. | | `licenseTypeId` | string | Yes | The license type, matching the `licenseTypeId` of a `license` price, such as `seats`. | `snake_case` (`customer_id`, `license_type_id`) is also accepted. Response: ```json theme={null} { "id": "lic_01JX…", "accountId": "acc_…", "customerId": "cust_acme", "licenseTypeId": "seats", "status": "active", "startedAt": "2026-10-12T09:30:00.000Z" } ``` ## Deactivate a license Frees the seat. The customer stops being billed for it from the next period. ```http theme={null} POST /v1/licenses/:id/deactivate ``` Response: ```json theme={null} { "success": true, "deactivatedAt": "2026-10-12T10:00:00.000Z" } ``` # Metrics Source: https://docs.orvlin.com/api-reference/metrics Define how events are aggregated into billable quantities A metric turns events into a number for each billing period. Prices charge for metrics. ## Create a metric ```http theme={null} POST /v1/metrics ``` ```json theme={null} { "name": "api_calls_total", "description": "Total API calls", "aggregation": { "type": "count", "filter": {}, "groupBy": [] } } ``` | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | The metric's name. Prices refer to it. | | `description` | string | No | What the metric measures. | | `aggregation.type` | string | Yes | `count`, `sum`, `max`, `count_distinct`, or `sql`. | | `aggregation.property` | string | For `sum`, `max`, `count_distinct` | The event property to aggregate. Dot paths such as `usage.tokens` are allowed. | | `aggregation.filter` | object | No | Only count events whose properties match. | | `aggregation.groupBy` | string\[] | No | Properties to group by. | | `sql` | string | For `sql` | A custom SQL expression. It is checked on creation, and an invalid one is rejected with `400`. | Responds `201 Created`. ### Examples Count calls that succeeded: ```json theme={null} { "name": "successful_calls", "aggregation": { "type": "count", "filter": { "statusCode": 200 } } } ``` Sum tokens: ```json theme={null} { "name": "tokens_total", "aggregation": { "type": "sum", "property": "tokens" } } ``` ## List metrics ```http theme={null} GET /v1/metrics ``` Response: `{ "data": [ … ] }`. ## Update a metric ```http theme={null} PATCH /v1/metrics/:id ``` Send only the fields to change. ## Delete a metric ```http theme={null} DELETE /v1/metrics/:id ``` # API overview Source: https://docs.orvlin.com/api-reference/overview Base URL, versioning, authentication, pagination, and how the API is organised The Orvlin API is a JSON API over HTTPS. Every endpoint lives under `/v1`. ``` https://api.orvlin.com/v1 ``` ## Making requests * Send and receive JSON. Set `Content-Type: application/json` on requests with a body. * Authenticate with an API key (see [Authentication](/api-reference/authentication)). * Dates and times are ISO 8601 strings, with a timezone offset or `Z`. * Money amounts are decimal numbers in the currency named on the object. Currencies are three-letter ISO codes, such as `USD`. * IDs are strings. Customer IDs are yours or `cust_…`. Other objects get ULID-style IDs. ## Pagination Lists return `{ "data": [ … ] }`. Some also accept paging parameters: | Endpoint | Parameters | Maximum page size | | - | - | - | | `GET /v1/customers` | `limit` (default 100), `offset` | Set by `limit` | | `GET /v1/invoices` | `page` (default 1), `page_size` (default 20) | 100 | Invoice lists also return a `pagination` object with `total`, `page`, and `pageSize`. ## Idempotency Requests that create billable records take an idempotency key, so a retry can't create a duplicate: * Events: set `idempotencyKey` on each event. * Credit grants and purchases: set `idempotencyKey` in the body. Reusing a key returns the original result instead of creating a second record. ## Errors and rate limits Errors return a JSON body with an `error` message. See [Errors](/api-reference/errors). Requests are rate limited. If you receive `429 Too Many Requests`, wait and retry with exponential backoff. ## Usage Usage summaries and time series for the dashboard are available from: * `GET /v1/usage/summary`: totals across the workspace. * `GET /v1/usage/customers/:customerId/timeseries`: consumption for one customer over time. ## Health | Endpoint | Description | | - | - | | `GET /v1/health` | Confirms the API is running. | | `GET /v1/health/ready` | Confirms the API is ready to serve requests. | ## Resource index | Area | Resource | Reference | | - | - | - | | Access | Authentication, sessions | [Authentication](/api-reference/authentication) | | Access | API keys | [API keys](/api-reference/api-keys) | | Usage | Events | [Events](/api-reference/events) | | Catalog | Metrics | [Metrics](/api-reference/metrics) | | Catalog | Prices | [Prices](/api-reference/prices) | | Catalog | Plans | [Plans](/api-reference/plans) | | Customers | Customers, entitlements | [Customers](/api-reference/customers) | | Billing | Subscriptions | [Subscriptions](/api-reference/subscriptions) | | Billing | Invoices, payment retries | [Invoices](/api-reference/invoices) | | Billing | Licenses | [Licenses](/api-reference/licenses) | | Credits | Credit types, wallets | [Credits](/api-reference/credits) | | Integrations | Webhooks | [Webhooks](/api-reference/webhooks) | | Customer-facing | Customer portal | [Customer portal](/api-reference/portal) | Some endpoints are for the dashboard and aren't covered here. They include coupons, contracts, dunning, forecasts, and revenue recognition, which are documented in the guides where they apply. # Plans Source: https://docs.orvlin.com/api-reference/plans Bundle prices into plans that customers subscribe to, and publish new versions A plan is a named bundle of prices. Customers subscribe to plans, not to individual prices. ## Create a plan ```http theme={null} POST /v1/plans ``` ```json theme={null} { "name": "starter", "description": "Starter plan", "prices": [ { "priceId": "01JX0F8K4W3Z9YQ6V7M2T5R8B1" } ] } ``` | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | The plan's name. | | `prices` | array | Yes | The prices in the plan. Each entry is an object with a `priceId`, or a bare price ID. | | `description` | string | No | What the plan is for. | Every price must exist in your workspace. Responds `201 Created`. ## List plans ```http theme={null} GET /v1/plans ``` Response: `{ "data": [ … ] }`. ## Update a plan ```http theme={null} PATCH /v1/plans/:id ``` A change creates a new version of the plan. Existing subscriptions keep their version until they are migrated. ## Versions | Method and path | Description | | - | - | | `GET /v1/plans/:id/versions` | List the plan's versions. | | `POST /v1/plans/:id/publish` | Publish the current draft so new subscriptions can use it. | | `POST /v1/plans/:id/versions/:version/restore` | Make an earlier version current again. | Publishing makes a version available for new subscriptions. ## Delete a plan ```http theme={null} DELETE /v1/plans/:id ``` # Customer portal API Source: https://docs.orvlin.com/api-reference/portal Create portal sessions and call customer-facing endpoints with a portal token The customer portal API lets a signed-in customer see their own plan, usage, invoices, and credits, and change plans where you allow it. Your backend creates a session. The browser then calls the portal endpoints with that session's token. For how the pieces fit together, see [Customer portal](/guides/customer-portal). ## Create a session Call this from your backend with your API key. ```http theme={null} POST /v1/portal/sessions Authorization: Bearer orv_xxxxxxxx_your_secret ``` ```json theme={null} { "customerId": "cust_acme", "expiresInSeconds": 3600, "allowPlanChanges": true, "allowCreditPurchases": false } ``` | Field | Type | Required | Description | | - | - | - | - | | `customerId` | string | Yes | The customer the session is for. | | `expiresInSeconds` | integer | No | How long the session lasts, from 1 to 86,400. | | `allowPlanChanges` | boolean | No | Lets the customer switch to another published plan. | | `allowCreditPurchases` | boolean | No | Lets the customer buy credits, invoiced to them. | Pass the session token to the browser. Don't pass your API key. ## Call portal endpoints Send the portal token as a Bearer token: ```http theme={null} GET /v1/portal/me Authorization: Bearer ``` A portal token can only read and act on its own customer. Requests with a token for another customer, or an expired one, are refused. ## Endpoints | Method and path | Description | | - | - | | `GET /v1/portal/me` | The customer. | | `GET /v1/portal/config` | Which portal features are enabled for this session. | | `GET /v1/portal/plans` | Published plans the customer can switch to. | | `GET /v1/portal/plan-change/preview?planId=…` | What switching to a plan would cost. Changes nothing. | | `POST /v1/portal/plan-change` | Switch the customer to a plan. | | `GET /v1/portal/usage` | Usage for the current period. | | `GET /v1/portal/entitlements` | Features and remaining allowances. | | `GET /v1/portal/wallets` | Credit balances. | | `GET /v1/portal/credits` | Credit history. | | `POST /v1/portal/wallets/:type/purchase` | Buy credits, invoiced to the customer. Requires `allowCreditPurchases`. | | `GET /v1/portal/invoices` | The customer's invoices. | | `GET /v1/portal/upcoming` | The next invoice. | Show the plan-change preview before the customer confirms a change. # Prices Source: https://docs.orvlin.com/api-reference/prices Charge for a metric or a flat fee, using one of the pricing models A price says how much to charge. It refers to a metric (for usage-based charges) or to nothing (for flat fees). For the pricing models, see [Pricing models](/pricing-models). ## Create a price ```http theme={null} POST /v1/prices ``` ```json theme={null} { "name": "api_call_price", "description": "Per API call", "metricId": "01JX0F8K4W3Z9YQ6V7M2T5R8B1", "config": { "model": "unit", "unitPrice": 0.001, "currency": "USD" }, "cadence": "monthly", "timing": "in_arrears" } ``` | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | The price's name. Plans refer to it. | | `config` | object | Yes | The pricing model and its settings. See [Pricing models](/pricing-models). | | `description` | string | No | What the price is for. | | `metricId` | string | No | The metric to charge for. Leave it empty for a flat fee. | | `cadence` | string | No | `monthly` (default), `quarterly`, `annual`, `one_time`, or `usage_based`. | | `timing` | string | No | `in_arrears` (default) charges at the end of the period. `in_advance` charges at the start. | A price with no `metricId` must use a flat-fee cadence. A usage-based price needs a metric. Responds `201 Created`. ## List prices ```http theme={null} GET /v1/prices ``` Response: `{ "data": [ … ] }`. ## Update a price ```http theme={null} PATCH /v1/prices/:id ``` Changing a price creates a new version. Subscriptions keep the version they started on until they are migrated. See [Subscriptions](/api-reference/subscriptions#change-a-plan). ## Delete a price ```http theme={null} DELETE /v1/prices/:id ``` # Subscriptions Source: https://docs.orvlin.com/api-reference/subscriptions Subscribe customers to plans, change them, and cancel them A subscription connects a customer to a plan from a start date. Subscriptions drive billing: each billing period produces an invoice for the prices in the plan. ## Create a subscription ```http theme={null} POST /v1/subscriptions ``` ```json theme={null} { "customerId": "cust_acme", "planId": "01JX0F8K4W3Z9YQ6V7M2T5R8B1" } ``` | Field | Type | Required | Description | | - | - | - | - | | `customerId` | string | Yes | The customer to subscribe. Must exist in your workspace. | | `planId` | string | Yes | The plan's `id` (not its name). Must exist in your workspace. | Responds `201 Created`. Returns `404` if the customer or plan doesn't exist in your workspace. ## List subscriptions ```http theme={null} GET /v1/subscriptions GET /v1/subscriptions?status=active ``` With `status=active`, only active subscriptions are returned. Response: `{ "data": [ … ] }`. ## Get a subscription ```http theme={null} GET /v1/subscriptions/:id ``` ## Status | Status | Meaning | | - | - | | `trialing` | In a trial period. | | `active` | Billing normally. | | `past_due` | A payment failed and retries are underway. | | `paused` | Not billing. | | `canceled` | Ended. | ## Cancel ```http theme={null} POST /v1/subscriptions/:id/cancel ``` ```json theme={null} { "immediately": false } ``` | Field | Default | Description | | - | - | - | | `immediately` | `false` | `true` ends the subscription now. `false` ends it at the end of the current period. | ## Change a plan Amend a subscription to change its prices or plan: | Method and path | Description | | - | - | | `POST /v1/subscriptions/:id/amend` | Apply a change now. | | `POST /v1/subscriptions/:id/schedule-amendment` | Apply a change at a future date. | | `GET /v1/subscriptions/:id/amendments` | List changes made to the subscription. | | `POST /v1/subscriptions/:id/migrate` | Move the subscription to a newer version of its plan. | To see what a change costs before applying it, compute the next invoice with `POST /v1/subscriptions/:id/invoices/compute`. To issue an invoice for the subscription now, use `POST /v1/subscriptions/:id/invoices`. ## Coupons | Method and path | Description | | - | - | | `POST /v1/subscriptions/:id/coupons` | Apply a coupon. | | `DELETE /v1/subscriptions/:id/coupons/:redemptionId` | Remove a coupon. | Create and deactivate coupons with `POST /v1/coupons` and `POST /v1/coupons/:id/deactivate`. ## Billing runs Orvlin bills subscriptions on a schedule. To run billing yourself, use `POST /v1/billing/run`. To see the schedule and past runs, use `GET /v1/billing/schedule` and `GET /v1/billing/history`. # Webhooks Source: https://docs.orvlin.com/api-reference/webhooks Receive signed notifications when invoices, subscriptions, and credits change Webhooks send you a message when something happens in your workspace, such as an invoice being paid or a subscription changing. Use them to update your own systems without polling the API. ## Register an endpoint ```http theme={null} POST /v1/webhooks ``` Managing webhooks requires a signed-in user session. ```json theme={null} { "url": "https://example.com/hooks/orvlin", "events": ["invoice.paid", "subscription.created"] } ``` | Field | Type | Required | Description | | - | - | - | - | | `url` | string | Yes | Where to send events. Must use `https` in production. | | `events` | string\[] | No | The events to receive. Defaults to `invoice.paid` and `subscription.created`. | Responds `201 Created` with the webhook, including its `secret`. The secret starts with `whsec_` and is used to verify signatures. Store it securely. ## Manage endpoints | Method and path | Description | | - | - | | `GET /v1/webhooks` | List endpoints. | | `PATCH /v1/webhooks/:id` | Turn an endpoint on or off with `{ "enabled": false }`. | | `DELETE /v1/webhooks/:id` | Delete an endpoint. | | `POST /v1/webhooks/:id/test` | Send a test event to the endpoint. | | `GET /v1/webhooks/:id/deliveries?limit=50` | Recent deliveries, with their status. | ## Events These are some of the events you can subscribe to: | Event | Sent when | | - | - | | `invoice.created` | An invoice is issued. | | `invoice.paid` | An invoice is paid. | | `invoice.voided` | An invoice is voided. | | `invoice.credit_note` | A credit note is issued. | | `invoice.payment_failed` | A charge fails. | | `invoice.payment_action_required` | The customer must act to complete payment. | | `invoice.payment_recovered` | A retry succeeds. | | `invoice.marked_uncollectible` | Retries are exhausted. | | `subscription.created` | A subscription starts. | | `subscription.cancel` | A subscription is canceled. | | `subscription.plan_change` | A subscription moves to another plan. | | `credit.grant` | Credits are granted to a customer. | | `usage.limit` | A customer reaches a usage limit. | ## Payload Every delivery is a `POST` with a JSON body: ```json theme={null} { "id": "evt_01JX…", "event": "invoice.paid", "timestamp": "2026-10-12T09:30:00.000Z", "data": { "id": "inv_01JX…", "customerId": "cust_acme", "total": 42.5, "currency": "USD" } } ``` `id` stays the same across retries, so use it to ignore duplicates. ## Headers | Header | Value | | - | - | | `X-Orvlin-Event` | The event name, such as `invoice.paid`. | | `X-Orvlin-Timestamp` | When the delivery was signed, in ISO 8601. | | `X-Orvlin-Signature` | The signature, as a hex string. | | `User-Agent` | `Orvlin-Webhook/1.0`. | ## Verify signatures The signature is an HMAC-SHA256 of the timestamp, a dot, and the raw request body, using the endpoint's secret: ``` signature = HMAC_SHA256(secret, X-Orvlin-Timestamp + "." + raw_body) ``` Compare your computed value with `X-Orvlin-Signature` using a constant-time comparison. Verify the signature against the raw body before parsing the JSON. ```ts Node.js theme={null} import crypto from 'node:crypto'; export function verifyOrvlinWebhook(rawBody: string, headers: Record, secret: string) { const timestamp = headers['x-orvlin-timestamp']; const received = headers['x-orvlin-signature']; if (!timestamp || !received) return false; const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${rawBody}`) .digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(received); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` ```python Python theme={null} import hmac import hashlib def verify_orvlin_webhook(raw_body: bytes, headers: dict, secret: str) -> bool: timestamp = headers.get("x-orvlin-timestamp") received = headers.get("x-orvlin-signature") if not timestamp or not received: return False signed = f"{timestamp}.".encode() + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, received) ``` Also check that `X-Orvlin-Timestamp` is recent, for example within five minutes, so a captured delivery can't be replayed later. Orvlin signs the timestamp, but your endpoint has to check it. ## Responding Return any `2xx` status within 8 seconds to acknowledge the delivery. Do the slow work after you respond, for example by queuing a job. ## Retries If your endpoint doesn't return `2xx`, Orvlin retries with increasing delays, for about a day. After the last attempt, the delivery is marked as failed. Each attempt is listed under `GET /v1/webhooks/:id/deliveries`. If your endpoint returns errors repeatedly, fix it and then re-enable it with `PATCH /v1/webhooks/:id`. # Core concepts Source: https://docs.orvlin.com/core-concepts The objects in Orvlin and how they relate Orvlin separates what your product does (events) from what you charge for it (prices and plans). That separation lets you change pricing without changing your instrumentation. ```mermaid theme={null} graph TD Event[Event: usage from your app] -->|Aggregated by| Metric[Metric] Metric -->|Charged by| Price[Price] Price -->|Bundled into| Plan[Plan] Plan -->|Subscribed to by| Subscription[Subscription] Subscription -->|Produces| Invoice[Invoice] ``` ## Customers A customer is the party you bill. Each customer has an ID (yours or Orvlin's), a name, an optional billing email, a currency, and free-form `metadata`. Every other object refers to a customer by ID. Set `id` to your own identifier when you create a customer. Then you never have to store a mapping between the two systems. ## Events An event records something a customer did: an API call, a generated image, a row written. Each event has: * `customerId`: who did it. * `eventName`: what happened, such as `api_call`. * `timestamp`: when it happened. Defaults to the time of ingestion. * `properties`: details a metric can aggregate, such as `latencyMs` or `tokens`. * `idempotencyKey`: optional. Events with the same key are counted once. Events are the only input to metering. See [Usage metering](/guides/usage-metering). ## Metrics A metric turns events into a number per billing period. The aggregation types are: | Type | Result | | - | - | | `count` | Number of matching events. | | `sum` | Sum of a numeric `property`. | | `max` | Largest value of a numeric `property` in the period. | | `count_distinct` | Number of distinct values of a `property`, such as unique users. | | `sql` | A custom SQL expression over events, for advanced cases. | Metrics can filter events and group them by a property. ## Prices A price charges for a metric (or for a flat fee, when it has no metric). Each price has: * a `config` with a pricing `model` (unit, tiered, bulk, package, percentage, or license). See [Pricing models](/pricing-models). * a `cadence`: `monthly`, `quarterly`, `annual`, `one_time`, or `usage_based`. * a `timing`: `in_advance` (charged at the start of the period) or `in_arrears` (charged at the end). ## Plans A plan is a named bundle of prices. Customers subscribe to plans, not to individual prices. Editing a plan creates a new version, so existing subscriptions keep the terms they signed up for until you migrate them. ## Subscriptions A subscription connects a customer to a plan from a start date. Its `status` is one of `active`, `trialing`, `past_due`, `paused`, or `canceled`. You can cancel a subscription immediately or at the end of the period, amend it, schedule an amendment, or migrate it to a newer plan version. ## Invoices Orvlin creates invoices for each billing period, for prepaid credit purchases, and for manual charges. An invoice moves through these statuses: | Status | Meaning | | - | - | | `draft` | Created but not finalized. Can be approved or edited. | | `open` | Finalized and waiting for payment. | | `paid` | Payment received. | | `void` | Cancelled. It cannot be paid. | | `uncollectible` | Written off after payment attempts failed. | ## Credits and wallets Credits are a prepaid balance a customer spends as they use your product. Each credit type (for example `ai_images`) has its own wallet per customer. Usage can draw down a wallet automatically, and a wallet can top itself up when it falls below a threshold. See [Prepaid credits](/guides/prepaid-credits). ## Entitlements An entitlement answers "may this customer use this feature right now, and how much is left?" It is derived from the customer's subscription, plan, credits, and any grants you have added. See [Entitlements](/guides/entitlements). ## Webhooks Webhooks deliver events such as `invoice.paid` and `subscription.created` to an HTTPS endpoint you own, with a signature you can verify. See [Webhooks](/api-reference/webhooks). ## Next steps Unit, tiered, bulk, package, percentage, and license pricing. Declare your catalog in a `pricing.yaml` file. # Accounting periods Source: https://docs.orvlin.com/enterprise/accounting-periods Mark closed months in your close process, and see how period status is recorded When your finance team closes a month, record that close on the accounting period. The period status is the sign-off point for your close process. ## Close a period ```bash theme={null} curl -s -X POST "$ORVLIN_API/v1/accounting/periods/9/2026/lock" \ -H "Authorization: Bearer $ADMIN_SESSION_TOKEN" ``` The path takes the month as `:month` (1 to 12) and the year as `:year`. The example closes September 2026. The response is the updated period, with `status: "closed"` and `closed_at`. ## Reopen a period Reopen a period only for corrections, and record the reason in your own change log. ```bash theme={null} curl -s -X POST "$ORVLIN_API/v1/accounting/periods/9/2026/unlock" \ -H "Authorization: Bearer $ADMIN_SESSION_TOKEN" ``` The response shows `status: "open"`. ## What the status means today * The period's status is recorded as `closed` or `open`. Your reporting and close checklists should read it before finalising figures. * The status does not yet stop changes to a closed period through the API. Check the status before you make corrections to a period. Until enforcement is in place, the status is a record of your close, not a lock. Treat changes to closed periods as exceptions, and log them. ## Recommended close process 1. Confirm all usage for the month has been ingested. Late events are the most common cause of surprises. 2. Review the revenue summary: `GET /v1/revenue/summary`. 3. Close the period. 4. Export the audit log for the period, and store it with your close file. # Approvals Source: https://docs.orvlin.com/enterprise/approvals Require a second person to approve high-impact billing actions Approvals add a review step ("four eyes") before actions that move money. When a policy applies, the action waits for an approver instead of running right away. ## Which actions can require approval | Action | What it does | | - | - | | `credit.grant` | Grants credits to a customer. | | `invoice.void` | Voids an invoice. | | `invoice.credit_note` | Issues a credit note. | | `subscription.cancel` | Cancels a subscription. | ## Policies Each action has a policy with three settings: | Field | Type | Description | | - | - | - | | `enabled` | boolean | Whether this action needs approval. | | `thresholdAmount` | number or `null` | Only amounts at or above this need approval. `null` means every amount does. | | `thresholdCurrency` | string or `null` | The currency the threshold applies to, as a three-letter ISO code. | Only workspace admins can change policies. Policies are read with `GET /v1/approvals/policies`, and updated with `PUT /v1/approvals/policies/:action`. ### Example: approve any credit grant over \$500 ```bash theme={null} curl -s -X PUT "$ORVLIN_API/v1/approvals/policies/credit.grant" \ -H "Authorization: Bearer $ADMIN_SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "enabled": true, "thresholdAmount": 500, "thresholdCurrency": "USD" }' ``` ## Request lifecycle When an action that needs approval is requested, Orvlin creates an approval request instead of running it. ```mermaid theme={null} graph LR Req[Action requested] --> Pending[pending] Pending -->|approved| Approved[approved] Approved --> Executing[executing] Executing --> Executed[executed] Executing --> Failed[failed] Pending -->|rejected| Rejected[rejected] Pending -->|no decision in time| Expired[expired] ``` | Status | Meaning | | - | - | | `pending` | Waiting for a decision. | | `approved` | Approved. The action is about to run. | | `executing` | The action is running. | | `executed` | The action completed. | | `failed` | The action was approved but couldn't complete. Check the error and retry. | | `rejected` | Declined. The action does not run. | | `expired` | No decision was made in time. The action does not run. | ## Working with requests * List requests: `GET /v1/approvals`. * Read one: `GET /v1/approvals/:id`. * Approve: `POST /v1/approvals/:id/approve` with an optional `note`. * Reject: `POST /v1/approvals/:id/reject` with an optional `note`. Decisions and policy changes are recorded in the [audit log](/enterprise/audit-logs). Orvlin sends webhooks when a request is created (`approval.requested`) and when it's decided (`approval.decided`), so your team can route requests to Slack or a ticketing system. See [Webhooks](/api-reference/webhooks). Start with `thresholdAmount` set to a value that catches only the risky cases. Review the queue for a week before you tighten it. ## Related * [API reference: approvals](/api-reference/approvals) * [Audit logs](/enterprise/audit-logs) # Audit logs Source: https://docs.orvlin.com/enterprise/audit-logs Find and export a record of every change made in your workspace The audit log records changes in your workspace: who did what, to which resource, and when. Use it for investigations, access reviews, and compliance evidence. ## What is recorded Changes to billing, catalog, customers, keys, webhooks, approvals, and privacy actions are recorded. Each entry includes: * the **action**, such as `invoice.void`, `api_key.create`, or `credits.grant` * the **resource type** and **resource ID** the action applied to * the time it happened * details of the change, where they apply Examples of recorded actions include `api_key.create`, `api_key.revoke`, `webhook.create`, `webhook.delete`, `credits.grant`, `invoice.void`, and `invoice.credit_note`. ## Search the log ```bash theme={null} curl -s "$ORVLIN_API/v1/audit?action=api_key.*&since=2026-10-01T00:00:00Z&limit=100" \ -H "Authorization: Bearer $ADMIN_SESSION_TOKEN" | jq ``` Filter by: | Parameter | Description | | - | - | | `action` | An exact action name, or a prefix ending in `*`, such as `customer.*`. | | `resourceType` | The kind of resource, such as `invoice`. | | `resourceId` | One specific resource. | | `since`, `until` | ISO 8601 bounds, inclusive. | | `limit`, `offset` | Paging. Default 50 per page, maximum 1000 for JSON. | ## Export Add `format=csv` to download the matching entries as a CSV file for your records or SIEM. A CSV export can contain up to 10,000 entries per request, so page with `offset` for larger exports: ```bash theme={null} curl -s "$ORVLIN_API/v1/audit?since=2026-10-01T00:00:00Z&format=csv&limit=10000" \ -H "Authorization: Bearer $ADMIN_SESSION_TOKEN" -o audit-2026-10.csv ``` ## Retention Keep exports in your own storage, under your own retention policy. Confirm with your account team how long Orvlin keeps audit entries for your plan. If you need a longer period, export on a schedule. Audit entries can't be edited or deleted through the API. Treat exports as evidence: store them somewhere write-protected. ## Related * [API reference: audit logs](/api-reference/audit-logs) * [Approvals](/enterprise/approvals) # Data privacy Source: https://docs.orvlin.com/enterprise/data-privacy Export or erase a customer's data to meet access and deletion requests Customers can ask what data you hold about them, and ask you to delete it. Orvlin provides an export for access requests, an erasure for deletion requests, and retention settings for data you keep. ## Who can do this Erasure and retention changes destroy data, so only **workspace admins and API keys** can run them. Exports are read-only, and are limited to people and keys with access to the customer's data. Every erasure and retention change is recorded in the [audit log](/enterprise/audit-logs). ## Export a customer's data ```bash theme={null} curl -s -OJ "$ORVLIN_API/v1/customers/cust_acme/data-export" \ -H "Authorization: Bearer $ADMIN_SESSION_TOKEN" ``` The response is a JSON file (`customer-cust_acme-data-export.json`) sent as an attachment. Each export carries an `x-data-subject-request` header with the request ID, so you can link the file to your ticket. Store the file in encrypted storage, and don't send it by unencrypted email. ## Erase a customer Erasure requires you to type the customer ID back in the request, so you can't erase the wrong customer by accident. ```bash theme={null} curl -s -X POST "$ORVLIN_API/v1/customers/cust_acme/erase" \ -H "Authorization: Bearer $ADMIN_SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "confirm": "cust_acme", "usageEvents": "retain" }' ``` | Field | Required | Values | Description | | - | - | - | - | | `confirm` | Yes | The customer ID | Must match the ID in the URL. Otherwise the request returns `400`. | | `usageEvents` | No | `retain` (default), `delete` | Whether the customer's usage events are kept or removed. | Erasure cannot be undone. Export the customer first if you need a copy, and confirm the request with your legal team, especially for customers with open invoices. Check your own retention duties before you erase. Some invoice and tax records may need to be kept by law. ## Retention Retention controls how long Orvlin keeps usage events. Read the current setting with `GET /v1/privacy/retention`, and change it with `PUT /v1/privacy/retention` and a `usageEventRetentionDays` value (a whole number of days, or `null` to clear it). Only workspace admins and API keys can change retention, and each change is recorded in the audit log. ## Responding to a request 1. Verify the requester's identity in your own system. 2. Export the data, and send it through your secure channel. 3. For a deletion request, confirm the request, export if needed, then erase with `"confirm"` set to the customer ID. 4. Keep the request ID and your response in your compliance records. # Environments and sandbox Source: https://docs.orvlin.com/enterprise/environments Keep test and live billing separate, and promote changes safely Billing mistakes are expensive, so keep environments apart. Orvlin workspaces start in **sandbox**, where payments never reach a real processor and test clocks are available. ## Recommended setup | Environment | Workspace | Keys | Used for | | - | - | - | - | | Development | Sandbox | `developer` key per developer | Building and experimenting. | | Staging | Sandbox | One `developer` key, used by staging | Integration tests and load tests. | | Production | Live | Separate `developer` and `viewer` keys | Real customers. | Give each environment its own keys. A revoked staging key then can't affect production. ## What sandbox does * Payments never reach a real payment processor, even if one is connected. * Test clocks can move customers' time forward to test renewals and retries. * Data is kept apart from live customers. ## Promote a change Change the catalog in sandbox first, then apply the same change to live: 1. Make the change in sandbox, for example a new price or a plan version. 2. Run through it with a test clock: see [Test billing with test clocks](/tutorials/test-billing-with-test-clocks). 3. If you manage the catalog as code, deploy the same [pricing manifest](/pricing-manifest) to live with `orvlin deploy --dry-run` first, then without it. 4. Check the live plan and price in the dashboard before you route customers to it. Never point a test script at a live key. Keep live keys in production secret storage only, and don't check them into source control. ## Checking which environment you're in Use the health endpoint and your own configuration to show the environment in your logs, so an engineer can tell at a glance which workspace a request went to. # Reliability Source: https://docs.orvlin.com/enterprise/reliability How webhooks, retries, idempotency, and failures behave, and what your systems should do This page explains what Orvlin does when things go wrong, so you can design your integration around it. ## Webhook delivery Webhooks are stored in a durable outbox before they're sent, so a restart or deploy doesn't lose them. * Each attempt times out after 8 seconds. * A failed attempt (anything other than a `2xx`) is retried with increasing delays. The schedule runs from 30 seconds to 12 hours, for about a day in total. * After the last attempt, the delivery is marked as failed. It stays in the delivery log. * A disabled or deleted endpoint stops receiving events. A rotated secret is used on the next attempt. * The event `id` and payload stay the same across retries, so your handler can deduplicate. Design your handler to be idempotent and to respond fast. See [Receive webhooks securely](/tutorials/receive-webhooks-securely). ## Idempotency Anything that creates money-related records accepts an idempotency key: * Events, keyed on `idempotencyKey`. A duplicate event isn't counted twice. * Credit grants and purchases, keyed on `idempotencyKey`. A retry grants or sells once. Reuse the same key when you retry the same logical operation. Use a new key for a new operation. ## Usage ingestion * Event batches can contain a mix of accepted and rejected events. The response lists both, so retry only the rejected ones after you fix them. * Entitlement checks in the SDK keep answering during short outages, using cached answers for up to 24 hours by default. Choose `onUnavailable` deliberately: see [Entitlements](/guides/entitlements#how-answers-are-cached). ## Rate limits and maintenance * If you're rate limited, the API returns `429`. Back off and retry. * During maintenance, changes are paused and the API returns `503` with `"code": "maintenance"`. Read-only checks may still work. Retry after a few minutes. ## Health checks | Endpoint | Use | | - | - | | `GET /v1/health` | Confirms the API is up. Use it for uptime monitoring. | | `GET /v1/health/ready` | Confirms the API is ready for traffic. Use it before routing requests. | ## Failure modes and what to do | If this happens | Your system should | | - | - | | A webhook endpoint is down | Keep returning `5xx` until fixed. Orvlin retries. Replay from the delivery log if needed. | | Orvlin is slow or unreachable for a check | Use the SDK's cached answer. Log the `source` field. | | An event is rejected | Fix the payload, then resend with the same `idempotencyKey`. | | A credit purchase is pending payment | Don't deliver credits yet. Wait for `invoice.paid`. | | A request returns `500` | Retry with backoff, using the same idempotency key. Contact support with the time of the request if it continues. | ## Support For incidents, include the workspace subdomain, the request time (in UTC), and the `x-request-id` or event `id` involved. # Security model Source: https://docs.orvlin.com/enterprise/security-model How Orvlin authenticates people and services, protects credentials, and signs webhooks This page describes how Orvlin protects your workspace, what you control, and what to check during a security review. ## Trust boundaries | Actor | Authenticates with | Can do | | - | - | - | | Your servers | API key (`orv_…`) | Calls the API, within the key's role. | | Your customers' browsers | Portal token, scoped to one customer | Reads and acts on that customer's billing only. | | Your team | Session (email and password, magic link, or SSO) | Manages the workspace, keys, and webhooks. | | Orvlin | Signs webhooks and invoices | Sends events to endpoints you registered. | Keep API keys on servers. Never ship them to browsers or mobile apps. Portal tokens are the browser credential. ## API keys * Each key has a **role**. Grant the smallest role that does the job. See [API keys](/api-reference/api-keys#roles). * A key can't be created with a higher role than the person creating it. * Keys are shown once, when created. They can't be retrieved afterwards. * Revoking a key takes effect immediately; requests with it return `401`. * Creating and revoking keys requires a signed-in user session, so a leaked key can't mint new keys. Rotate keys on a schedule and after any staff change. The [rotation steps](/api-reference/api-keys#rotate-a-key) create the new key before revoking the old one, so there's no downtime. ## Sessions * Access tokens expire after 15 minutes (`expiresIn: 900`). Refresh tokens are exchanged for new access tokens. * Sign-in through SSO returns a single-use code that is valid for 60 seconds. Tokens never appear in a URL. * Enterprise workspaces can [require SSO](/guides/sso), which turns off password and magic-link sign-in for the verified domains. ## Credentials at rest * Integration credentials for tax, CRM, accounting, and warehouse providers are stored encrypted with AES-256-GCM. * Client secrets for SSO connections are stored encrypted. * Save webhook secrets in your own secret manager when you create the endpoint. If a secret leaks, rotate it by recreating the endpoint and updating your receiver. ## Webhook signatures Every webhook delivery is signed with HMAC-SHA256 over the timestamp and the raw body, using a per-endpoint secret. Receivers verify the signature and reject old timestamps. See [Receive webhooks securely](/tutorials/receive-webhooks-securely). ## Access control in the workspace * Money-moving actions can require a second approver. See [Approvals](/enterprise/approvals). * Every change is recorded in the audit log. See [Audit logs](/enterprise/audit-logs). * Accounting periods can be locked so closed months can't change. See [Accounting periods](/enterprise/accounting-periods). ## Transport All API traffic uses HTTPS. Webhook endpoints must use HTTPS in production. ## Reporting a vulnerability Email security reports to `support@orvlin.com` with the subject line "Security report" and steps to reproduce. Please don't test against other customers' data, and use a sandbox workspace for testing. Confirm the security contact with your account team before you publish this page for customers. A dedicated security address is recommended. # Glossary Source: https://docs.orvlin.com/glossary Definitions of the billing terms used across Orvlin How a metric combines matching events into one number: `count`, `sum`, `max`, `count_distinct`, or `sql`. The amount of a feature a customer can use in a period, set by their plan or a grant. A pending action that waits for a second person to approve or reject it. See [Approvals](/enterprise/approvals). A record of changes in the workspace, searchable and exportable. See [Audit logs](/enterprise/audit-logs). A rule that buys more credits when a balance falls to a threshold, up to a monthly cap. The span of time a subscription's usage and fees are billed together, such as a calendar month. How often a price is charged: `monthly`, `quarterly`, `annual`, `one_time`, or `usage_based`. A set of credits with its own amount, source, and expiry, held in a wallet. A document that reduces what a customer owes against a paid invoice. What a set of credits is for, such as `image_credits`, and how the balance behaves at zero. The party you bill. Identified by your own ID, or by a generated `cust_…` ID. The reminders and retries sent when an invoice isn't paid. Whether a customer can use a feature right now, and how much of it remains. One record of something a customer did, sent by your application. A value that makes a retried request count or create only once. Whether a price is charged at the end (`in_arrears`) or the start (`in_advance`) of a period. A bill for a period, a purchase, or a manual charge. See the [lifecycle](/guides/invoicing-and-collection). A seat a customer holds under a `license` price. A rule that turns events into a billable quantity. See [Metrics](/api-reference/metrics). A named bundle of prices that customers subscribe to. A short-lived, customer-scoped token that lets a customer's browser read their billing. See [Customer portal](/guides/customer-portal). What to charge for a metric or a flat fee, with a pricing model. See [Pricing models](/pricing-models). A workspace where payments never reach a real processor and test clocks are available. See [Environments](/enterprise/environments). A customer's enrolment in a plan from a start date. A simulated time for a sandbox customer, moved forward to test billing. See [Test billing](/tutorials/test-billing-with-test-clocks). A customer's balance for one credit type. A signed notification sent to your endpoint when something happens. See [Webhooks](/api-reference/webhooks). # Customer portal Source: https://docs.orvlin.com/guides/customer-portal Let customers see usage, invoices, and plans, and change their own plan The customer portal is a set of endpoints your application can put in front of its own customers. A customer can see their plan, usage, credits, and invoices, and can change plans or buy credits if you allow it. You control access through short-lived portal sessions, so customers never use your API key. ## How it works 1. Your backend creates a **portal session** for a customer with your API key. 2. Your backend passes the session token to the browser. 3. The browser calls the portal endpoints with the session token as a Bearer token. Sessions are scoped to one customer and expire after `expiresInSeconds` (up to 24 hours). ## Create a session ```bash theme={null} curl -X POST "$ORVLIN_API/v1/portal/sessions" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "customerId": "cust_acme", "expiresInSeconds": 3600, "allowPlanChanges": true, "allowCreditPurchases": false }' ``` | Field | Required | Description | | - | - | - | | `customerId` | Yes | The customer the session is for. | | `expiresInSeconds` | No | How long the session lasts, from 1 to 86,400. | | `allowPlanChanges` | No | Lets the customer switch to another published plan. | | `allowCreditPurchases` | No | Lets the customer buy credits, paid by invoice. | Create the session on your server, and only when the customer is signed in to your application. ## What the customer can see With the session token, the portal calls return: * `GET /v1/portal/me`: the customer's own profile. * `GET /v1/portal/config`: which portal features are enabled. * `GET /v1/portal/plans`: plans the customer can switch to. * `GET /v1/portal/usage`: consumption for the current period. * `GET /v1/portal/entitlements`: features and remaining allowances. * `GET /v1/portal/wallets` and `GET /v1/portal/credits`: credit balances. * `GET /v1/portal/invoices`: invoices. * `GET /v1/portal/upcoming`: the next invoice. To change plans, call `GET /v1/portal/plan-change/preview?planId=…` to show the customer what will happen, then `POST /v1/portal/plan-change` to apply it. Preview doesn't change anything, so show it before the customer confirms. To buy credits when `allowCreditPurchases` is on, call `POST /v1/portal/wallets/:type/purchase`, where `:type` is the credit type. The purchase is invoiced to the customer. ## Build your own UI The portal is a set of plain HTTP endpoints, so you can build the screens in your own application. The [React SDK](/sdks/react) provides hooks for entitlements and credits that you can use alongside them. # Entitlements Source: https://docs.orvlin.com/guides/entitlements Gate features and enforce limits in your application without slowing requests An entitlement tells your application whether a customer can use a feature right now, and how much of its allowance remains. Plans define the entitlements; Orvlin keeps the consumption up to date as usage arrives. ## Check an entitlement Call the SDK before the work you want to gate: ```ts theme={null} const { hasAccess, remaining } = await orvlin.check('cust_acme', 'api_calls', { amount: 1 }); if (!hasAccess) { return reply.status(402).send({ error: 'Upgrade your plan to continue.' }); } ``` The `amount` is how much the request will consume. `remaining` is `null` when the feature has no cap. ## Read all entitlements ```bash theme={null} curl "$ORVLIN_API/v1/customers/cust_acme/entitlements" \ -H "Authorization: Bearer $ORVLIN_API_KEY" ``` ```json theme={null} { "customerId": "cust_acme", "entitlements": [ { "featureId": "seats", "featureName": "Plan Seats", "type": "numeric", "hasAccess": true, "allocated": 10, "consumed": 3, "remaining": 7, "pricingModel": "license" } ] } ``` ## How answers are cached Entitlement checks sit on your request path, so the SDKs answer from a local cache: | Situation | What the SDK returns | `source` | | - | - | - | | Answer is fresh (within `cacheTtlMs`, default 30 s) | The cached answer. | `cache` | | Answer is stale but Orvlin is slow or unreachable | The stale answer, while a refresh runs in the background. | `stale` | | Orvlin is unreachable and the customer has never been seen | The `onUnavailable` fallback. Default `deny`. | `fallback` | | Fresh fetch | The live answer. | `live` | A stale answer is used for up to `staleTtlMs` (default 24 hours). After that, the SDK falls back to `onUnavailable`. `onUnavailable: 'allow'` lets unknown customers use features while Orvlin is down. Only use it where a short outage is cheaper than blocking paying customers. ## Warm the cache Prefetch the customers you expect to see, such as the ones active at deploy time, so the first request does not wait: ```ts theme={null} await orvlin.prefetch(['cust_acme', 'cust_globex']); ``` Save the cache with `snapshot()` and restore it with `restore()` to warm a new process from a previous one. ## Invalidate after a change When you change a customer's plan or grant them something, drop the cached answer so the next check is live: ```ts theme={null} orvlin.invalidate('cust_acme'); // one customer orvlin.invalidate(); // everyone ``` Orvlin also sends `subscription.plan_change` webhooks. Invalidate on those if you cache outside the SDK. ## Grant access directly To give a customer a feature or a higher limit outside their plan, add an entitlement grant: ```bash theme={null} curl -X POST "$ORVLIN_API/v1/customers/cust_acme/entitlement-grants" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "featureId": "api_calls", "limit": 50000, "reason": "Pilot agreement", "expiresAt": "2027-03-31T00:00:00Z" }' ``` `featureId` is required. `limit`, `reason`, and `expiresAt` are optional. `expiresAt` must be in the future. Grants are invalidated in the SDK cache automatically when the API creates them. Remove a grant with `DELETE /v1/entitlement-grants/:grantId`. ## React In a React app, wrap components with `` to show or hide them by entitlement. See [React SDK](/sdks/react). # Integrate Orvlin into your app Source: https://docs.orvlin.com/guides/integrate-your-app Step-by-step: connect your application to Orvlin for metering, entitlements, credits, webhooks, and a customer portal This guide takes an existing application from no billing to full integration. Each step builds on the one before, and each step has a check you can run before moving on. **Time:** 1–2 hours for a typical server · **You need:** a sandbox workspace, a `developer` API key, and a server application (Node.js or Python). ## How the pieces fit ```mermaid theme={null} graph LR App[Your server] -->|track usage| Orvlin[Orvlin API] App -->|check entitlement| Orvlin Orvlin -->|signed webhooks| App Browser[Customer browser] -->|portal token| Orvlin App -->|create portal session| Orvlin ``` | Piece | Where it runs | Credential | | - | - | - | | Usage tracking and entitlement checks | Your server | API key | | Webhooks | Your server (receives calls) | Webhook secret | | Customer portal | Browser, with a session your server creates | Portal token | Never put the API key in a browser or mobile app. *** ## Step 1: Map your product to billing terms Before writing code, decide what you charge for. Write down three things: 1. **The billable action**, such as an API call, an image generated, or a seat used. This becomes an `eventName`. 2. **What you count**, such as the number of calls, or the sum of tokens. This becomes a [metric](/api-reference/metrics). 3. **How you charge**, such as per unit, in tiers, or by credits. This becomes a [price](/pricing-models). You can state your billable action in one sentence, for example: "Every successful search request is one `search_request` event." *** ## Step 2: Create the catalog Create the metric, price, and plan in your sandbox workspace. The [Meter your first API](/tutorials/meter-your-first-api) tutorial shows each call. You can also keep the catalog in a `pricing.yaml` file and deploy it with the [CLI](/sdks/cli). `GET /v1/plans` returns your plan, and the plan lists the price you created. *** ## Step 3: Store your credentials Create one API key per environment, with the smallest role that works. See [API keys](/api-reference/api-keys#roles). | Environment | Workspace | Key role | | - | - | - | | Local development | Sandbox | `developer` | | Staging | Sandbox | `developer` | | Production | Live | `developer` (and `viewer` if a service only checks access) | Add the key to your secret manager, then expose it to your server as environment variables: ```bash theme={null} ORVLIN_API_URL=https://api.orvlin.com ORVLIN_API_KEY=orv_xxxxxxxx_your_secret ``` Never commit these values, and never log them. Run `curl -s -o /dev/null -w "%{http_code}" "$ORVLIN_API_URL/v1/health"`. It returns `200`. *** ## Step 4: Install the SDK ```bash Node.js theme={null} npm install @orvlin/node ``` ```bash Python theme={null} pip install orvlin ``` For a browser app, also install the React SDK with `npm install @orvlin/react`. It uses portal tokens, not API keys. See Step 9. *** ## Step 5: Create one client per process Create the client once at startup and reuse it. The client caches entitlement answers, so reusing it keeps checks fast. ```ts Node.js theme={null} // orvlin.ts import { Orvlin } from '@orvlin/node'; export const orvlin = new Orvlin({ apiKey: process.env.ORVLIN_API_KEY!, baseUrl: process.env.ORVLIN_API_URL, cacheTtlMs: 30_000, timeoutMs: 2_000, onUnavailable: 'deny', }); ``` ```python Python theme={null} # orvlin_client.py import os from orvlin import Orvlin orvlin = Orvlin( api_key=os.environ["ORVLIN_API_KEY"], base_url=os.environ["ORVLIN_API_URL"], cache_ttl=30.0, timeout=2.0, on_unavailable="deny", ) ``` Import the client in one route and call `orvlin.check("cust_test", "search_requests")`. It returns an answer without throwing. A `source` value of `live` shows the first call reached Orvlin. *** ## Step 6: Connect customers Each customer in your app needs a matching Orvlin customer. Use your own user or account ID as the Orvlin customer ID, so you never store a mapping. ```bash curl theme={null} curl -s -X POST "$ORVLIN_API_URL/v1/customers" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "acct_4821", "name": "Acme Corp", "email": "billing@acme.example" }' ``` Create the customer when they sign up, and subscribe them to a plan at the same time: ```bash theme={null} curl -s -X POST "$ORVLIN_API_URL/v1/subscriptions" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "customerId": "acct_4821", "planId": "" }' ``` `GET /v1/customers/acct_4821` returns the customer, and the subscription has `"status": "active"` or `"trialing"`. *** ## Step 7: Track usage Record one event per billable action, after the action succeeds. Key each event to the request, so retries don't count twice. ```ts Node.js theme={null} import { orvlin } from './orvlin'; export async function afterSearch(customerId: string, requestId: string) { await orvlin.track({ customerId, eventName: 'search_request', idempotencyKey: requestId, properties: { statusCode: 200 }, }); } ``` ```python Python theme={null} from orvlin_client import orvlin def after_search(customer_id: str, request_id: str) -> None: orvlin.track({ "customer_id": customer_id, "event_name": "search_request", "idempotency_key": request_id, "properties": {"status_code": 200}, }) ``` The `eventName` must match the event your metric counts. Check it against the metric definition in Step 2. A mismatch records usage that is never billed. `GET /v1/events?customer_id=acct_4821&event_name=search_request` lists the event. Sending it again with the same `idempotencyKey` does not add a second one. *** ## Step 8: Gate features and spend Before doing paid work, check the entitlement. Return a clear response when the customer is out of allowance. ```ts Node.js theme={null} const check = await orvlin.check(customerId, 'search_requests', { amount: 1 }); if (!check.hasAccess) { return res.status(402).json({ error: 'Allowance used up', upgradeUrl: '/billing' }); } ``` ```python Python theme={null} check = orvlin.check(customer_id, "search_requests", amount=1) if not check.has_access: return Response({"error": "Allowance used up", "upgrade_url": "/billing"}, status=402) ``` For prepaid credits, check the balance the same way with `checkCredits` (Node) or `check_credits` (Python), using `creditType` and `quantity`. See [Prepaid credits](/guides/prepaid-credits). A customer with allowance gets through. A customer without it gets `402`. See the [entitlements tutorial](/tutorials/gate-features-with-entitlements) for the full middleware. *** ## Step 9: Receive webhooks Webhooks tell your app when an invoice is paid, a subscription changes, or an approval is decided. Register an HTTPS endpoint, then verify every delivery before you act on it. Register the endpoint with a signed-in user session (not an API key): ```bash theme={null} curl -s -X POST "$ORVLIN_API_URL/v1/webhooks" \ -H "Authorization: Bearer $USER_SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.acme.example/webhooks/orvlin", "events": ["invoice.paid", "subscription.plan_change"] }' ``` Save the `secret` (it starts with `whsec_`) in your secret manager. Verify each delivery as shown in [Webhooks](/api-reference/webhooks#verify-signatures). Then handle each event once, keyed on its `id`. The [Receive webhooks securely](/tutorials/receive-webhooks-securely) tutorial builds this end to end. `POST /v1/webhooks/:id/test` delivers a test event, and your endpoint returns `200`. *** ## Step 10: Add a customer portal (optional) If customers should see their usage, invoices, and plans in your app, create a portal session from your server, and use the React SDK in the browser. Your server creates the session for the signed-in user's customer only: ```bash theme={null} curl -s -X POST "$ORVLIN_API_URL/v1/portal/sessions" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "customerId": "acct_4821", "expiresInSeconds": 900, "allowPlanChanges": true }' ``` The browser then uses the React SDK with that token. See [Launch a customer portal](/tutorials/launch-customer-portal). The portal endpoint `GET /v1/portal/me` returns the customer when called with the portal token. *** ## Go-live checklist * [ ] Sandbox tests pass for tracking, gating, and webhooks. * [ ] A test clock shows the expected first invoice ([tutorial](/tutorials/test-billing-with-test-clocks)). * [ ] Production uses a live workspace and its own API key, stored in your secret manager. * [ ] Webhook endpoint uses HTTPS and verifies signatures. * [ ] Every event and grant uses an idempotency key. * [ ] Entitlement checks log the `source` field, so you can see fallbacks. * [ ] Your team knows where the [audit log](/enterprise/audit-logs) is and who can approve money-moving actions ([approvals](/enterprise/approvals)). ## Troubleshooting | Symptom | Likely cause | Fix | | - | - | - | | `401 Unauthorized` | Key missing, mistyped, or revoked. | Check the environment variable. Create a new key if this one was revoked. | | `403 Forbidden` | The key's role can't do this action. | Use a key with a higher role, such as `developer` for tracking. | | `404 Not Found` on a customer or plan | The ID doesn't exist in this workspace. | Check you're using the sandbox or live key that matches the ID. | | Usage recorded but not billed | The `eventName` doesn't match the metric. | Compare the event name with the metric definition. | | Every check returns `fallback` | Orvlin isn't reachable from your network. | Check outbound HTTPS to `api.orvlin.com`, and the timeout setting. | | Webhooks not arriving | Endpoint returns non-`2xx`, or isn't HTTPS. | Check the [delivery log](/api-reference/webhooks#manage-endpoints) and fix the endpoint. | ## Next steps Build the catalog and first usage record end to end. What happens during failures, and what your systems should do. # Integrations Source: https://docs.orvlin.com/guides/integrations Connect tax, CRM, accounting, data warehouses, and cloud marketplaces Integrations connect Orvlin to the systems around billing: tax engines, your CRM, your accounting books, your data warehouse, and cloud marketplaces. Each workspace connects its own accounts from the **Integrations** page. Credentials are stored encrypted, and a connection only shows as **Connected** after Orvlin has verified it with the provider. ## Tax **Avalara AvaTax** and **Anrok** calculate sales tax on invoices. * Avalara needs an AvaTax account, a license key, and a company code. * Anrok needs an API key. Optionally, pick the Anrok product that carries your tax category. Issued invoices are committed to the provider, so they appear in your tax returns. Voided invoices are voided there too. Previews use an uncommitted quote and create no liability. Customers need a country on their billing address for tax to be calculated. Customers without one are skipped with a warning rather than taxed as if they were somewhere else. The default Avalara tax code is `SW054000`, which is commonly used for SaaS. Avalara distinguishes codes by delivery method and by business or personal use. Confirm the right code with your tax advisor. Nexus, exemption certificates, and tax filing stay in Avalara's or Anrok's own dashboards. Orvlin sends them transactions; it does not replace their compliance tooling. ## CRM Customers sync first, then their invoices. Syncs are idempotent: unchanged records are skipped and changed ones are updated in place. Failures are retried with backoff and listed at `GET /v1/integrations/failures`. | Provider | Customers become | Invoices become | You provide | | - | - | - | - | | HubSpot | Companies | Deals, associated to the company | A private app with `crm.objects.companies.write` and `crm.objects.deals.write`. | | Salesforce | Accounts | Opportunities | A Connected App with the Client Credentials flow, a run-as user, and your My Domain host. | | NetSuite | Customers | Invoices | Token-based authentication, an integration record, an access token, and one non-inventory item for invoice lines. | ## Accounting **QuickBooks Online** and **Xero** sync customers, invoices, payments, and voids. Connect with the **Connect** button; you sign in to the vendor and approve access. What syncs, every 10 minutes: * **Customers** become QuickBooks Customers or Xero Contacts. An existing customer or contact with the same name is linked, not duplicated. * **Invoices** (open, paid, and void, never drafts) are numbered with the Orvlin invoice number. Tax Orvlin calculated is sent as its own line. * **Payments** are recorded against the invoice when it is paid in Orvlin. * **Voids** are voided in the books too. After connecting, set the connection's settings: * **QuickBooks:** the item that invoice lines post to. Until it is set, only customers sync. * **Xero:** the revenue account code (default 200), and the bank account code that payments are recorded against. Sync is one-way, from Orvlin to your books. A payment recorded directly in QuickBooks or Xero does not mark the Orvlin invoice paid. ## Data warehouse Invoices, line items, customers, subscriptions, credits, and usage events are exported every 10 minutes, or on demand with **Export now**. | Destination | What you provide | | - | - | | Amazon S3 (or any S3-compatible store) | A bucket, a region, and a key with `s3:PutObject`. Optional prefix and endpoint. | | BigQuery | A project, a dataset, and a service account with BigQuery Data Editor. | | Snowflake | An account, a user with a registered RSA key, a warehouse, a database, and a schema. | Every exported row has `id`, `account_id`, `exported_at`, and `data` (the full record). Exports are append-only, so take the latest row per `id`: ```sql theme={null} -- BigQuery SELECT * EXCEPT (rn) FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY id ORDER BY exported_at DESC) AS rn FROM billing.orvlin_invoices ) WHERE rn = 1; -- Snowflake SELECT * FROM ORVLIN_INVOICES QUALIFY ROW_NUMBER() OVER (PARTITION BY ID ORDER BY EXPORTED_AT DESC) = 1; ``` Each run exports up to 5,000 rows per dataset and continues on the next run, so the first export of a large account takes several runs. Check progress on the card, or at `GET /v1/integrations/warehouse/status`. ## Cloud marketplaces Connect AWS Marketplace, Azure Marketplace, or Google Cloud Marketplace to sell through a marketplace listing. Each one needs: 1. **A listing** with metered dimensions. Note the dimension names. 2. **A dimension mapping** from your Orvlin metric names to the marketplace's dimensions, such as `{"api_calls_total": "api_calls"}`. 3. **A fulfilment step.** When a buyer subscribes, the marketplace sends them to your sign-up URL with a token. Forward that token to Orvlin so it can link or create the customer. | Marketplace | Fulfilment endpoint | Notes | | - | - | - | | AWS | `POST /v1/marketplace/aws/resolve` with `registrationToken` | Orvlin resolves the buyer with AWS. | | Azure | `POST /v1/marketplace/azure/resolve` with `token` | Resolving also activates the subscription. Microsoft does not bill until activation. | | Google Cloud | `POST /v1/marketplace/gcp/signup` with `token` and `audience` | Once the entitlement is active, attach its usage reporting ID with `POST /v1/marketplace/gcp/entitlement`. | Usage is reported to each marketplace once per hour. Events that arrive after an hour has been reported are not re-reported, because marketplaces reject duplicate records for a reported window. Google's entitlement changes (plan changes and cancellations) arrive on Google's Pub/Sub topic. Subscribe to it and call the entitlement endpoint as needed. ## Troubleshooting * **Connected, but nothing syncs:** check the card's error message, then press **Reconnect** if the provider revoked access. * **Failed records:** list them at `GET /v1/integrations/failures` and retry with `POST /v1/integrations/failures/retry`. * **Tax is missing on an invoice:** check that the customer has a country on their billing address. # Invoicing and collection Source: https://docs.orvlin.com/guides/invoicing-and-collection How invoices are issued, paid, retried, voided, and credited Orvlin turns subscriptions and usage into invoices, collects payment through your payment provider, and retries failed charges. This guide covers the lifecycle and the actions you can take at each step. ## The invoice lifecycle ```mermaid theme={null} graph LR Draft[draft] -->|approve| Open[open] Open -->|payment succeeds| Paid[paid] Open -->|payment fails| Retry[retries] Retry -->|recovered| Paid Retry -->|gives up| Uncollectible[uncollectible] Open -->|void| Void[void] Paid -->|credit note| Credited[credit note issued] ``` Invoices that need review wait as `draft` until someone approves them. Others are finalized as `open` when they are issued. ## Approve, pay, and void | Action | Endpoint | Notes | | - | - | - | | Approve a draft | `POST /v1/invoices/:id/approve` | Moves the invoice to `open`. | | Charge now | `POST /v1/invoices/:id/pay` | Charges the card on file. Fails for `paid` or `void` invoices. | | Void | `POST /v1/invoices/:id/void` | Cancels the invoice. A voided invoice cannot be paid. | | Credit | `POST /v1/invoices/:id/credit-note` | Issues a credit note against a paid invoice. | | Send a link | `GET /v1/invoices/:id/link` | A hosted page where the customer can pay. | | Download | `GET /v1/invoices/:id/pdf` | The invoice as a PDF. | ## Payment retries When a charge fails, Orvlin retries it on a schedule. Configure the schedule with the payment retry endpoints (`/v1/payment-retry/settings`) or in the dashboard. Each attempt is recorded at `GET /v1/invoices/:id/payment-attempts`. Webhooks tell you what happened: * `invoice.payment_failed`: a charge failed. * `invoice.payment_action_required`: the customer must act, such as confirming a card. * `invoice.payment_recovered`: a retry succeeded. * `invoice.marked_uncollectible`: retries are exhausted. ## Connect a payment provider Orvlin charges cards through your own Stripe account. Connect it from the dashboard's payment settings using the Stripe OAuth flow. Stripe sends its webhooks to Orvlin at `/v1/payments/webhooks`. Each workspace connects its own Stripe account, so payouts go directly to you. Sandbox workspaces never reach a real payment processor, whatever the configuration. Use sandbox to test the flow end to end, then switch to a live workspace when you are ready to charge customers. ## Tax Orvlin can calculate tax through Avalara or Anrok. Customers need a country on their billing address before tax can be calculated; customers without one are skipped with a warning rather than taxed in the wrong place. See [Integrations](/guides/integrations#tax). ## Revenue recognition Orvlin can schedule recognition of invoice revenue over the service period (ASC 606). Generate schedules with `POST /v1/revenue/generate`, then read `GET /v1/revenue/summary`, `GET /v1/revenue/waterfall`, and `GET /v1/revenue/contracts`. ## Accounting periods Lock a month once its books are closed so invoices in that period cannot change. Use `POST /v1/accounting/periods/:month/:year/lock` and the matching `unlock` endpoint. # Prepaid credits Source: https://docs.orvlin.com/guides/prepaid-credits Sell credit packs, grant credits, and top up balances automatically Credits are a prepaid balance a customer spends as they use your product. Each credit type (for example `ai_images` or `tokens`) has a wallet per customer. Usage events draw the wallet down as they arrive. ## Concepts * **Credit type**: what the credits are for, with a name, a unit label, and an `overage` rule. `block` stops usage when the balance reaches zero. `allow` lets usage continue and bills the overage. * **Grant**: credits given to a customer for free, as a promotion, or as part of a plan. Grants can expire. * **Purchase**: credits the customer buys. They are paid for by the card on file, or by an emailed invoice. * **Auto top-up**: buy more credits automatically when the balance falls to a threshold. ## Grant credits ```bash theme={null} curl -X POST "$ORVLIN_API/v1/customers/cust_acme/wallets/ai_images/grants" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 500, "source": "promotional", "expiresAt": "2027-01-31T00:00:00Z", "description": "Launch promotion", "idempotencyKey": "promo_launch_acme" }' ``` `source` is `grant`, `promotional`, or `purchase`. Reusing an `idempotencyKey` grants the credits once. ## Sell a credit pack ```bash theme={null} curl -X POST "$ORVLIN_API/v1/customers/cust_acme/wallets/ai_images/purchases" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "credits": 1000, "collection": "charge", "idempotencyKey": "order_20261012_001" }' ``` `collection` is `charge` (the card on file, the default) or `send_invoice` (the customer pays an emailed invoice). If you omit `unitPrice`, the credit type's price is used. A purchase starts as `pending_payment` and becomes `active` once paid. ## Configure auto top-up ```bash theme={null} curl -X PUT "$ORVLIN_API/v1/customers/cust_acme/wallets/ai_images/auto-top-up" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "threshold": 100, "credits": 1000, "maxPerMonth": 5, "enabled": true }' ``` When the balance is at or below `threshold`, Orvlin buys `credits` against the card on file. `maxPerMonth` (default 5) caps the number of top-ups per calendar month. ## Check a balance before work Use the SDK's `checkCredits` to confirm a customer can pay for an action: ```ts theme={null} const { allowed, balance } = await orvlin.checkCredits('cust_acme', { eventName: 'image', quantity: 1, }); ``` Usage you then report with `track()` is drawn from the wallet as it arrives. ## Read balances and history * `GET /v1/customers/:id/wallets`: balances for every credit type. * `GET /v1/customers/:id/wallets/:type/ledger`: every grant, purchase, and draw-down. See the [Credits API](/api-reference/credits) for every field. # Single sign-on Source: https://docs.orvlin.com/guides/sso Sign your team in through your identity provider with OIDC or SAML Each workspace can sign its people in through its own identity provider, over **OIDC** or **SAML 2.0**. Set it up in **Settings → Single sign-on**. ## Setup 1. **Create a connection.** Choose the protocol, give it a name, and list the email domains it covers, such as `acme.com`. 2. **Verify your domain.** Add the DNS record shown on the connection, for example `TXT _orvlin-sso.acme.com = orvlin-sso=…`, then press **Check DNS**. Until a domain is verified, nobody can sign in through the connection for it. Public mail domains such as gmail.com cannot be claimed. 3. **Configure your identity provider** with the values shown on the connection card. 4. **Test it.** Sign out, choose **Sign in with SSO** on the login page, and enter a work email. 5. **Optional: require SSO.** This turns off password and magic-link sign-in for people on those domains. ## Identity provider settings | Provider | Protocol | What to enter | | - | - | - | | Okta | OIDC (Web app, Authorization Code, PKCE) | Sign-in redirect URI = the *Redirect URI* on the card. Copy the issuer, client ID, and client secret into Orvlin. | | Microsoft Entra ID | OIDC (App registration, Web platform) | Redirect URI = the *Redirect URI*. Add a client secret. Issuer: `https://login.microsoftonline.com//v2.0`. Add the optional `email` claim. | | Microsoft Entra ID | SAML (Enterprise application) | Identifier = the *SP entity ID*. Reply URL = the *ACS URL*. Copy the Login URL and the Base64 signing certificate into Orvlin. | | Google Workspace | SAML (custom SAML app) | ACS URL and Entity ID from the card. Name ID format `EMAIL`. Copy the SSO URL and certificate. | | Auth0, OneLogin, Keycloak, and others | OIDC or SAML | The same fields. | ## How sign-in works * People without an Orvlin user are created on their first sign-in. Turn off **Create users on first sign-in** to require invitations instead. SSO users get the standard `user` role. * After the identity provider responds, the browser receives a single-use code valid for 60 seconds, and the dashboard exchanges it for a normal session. Tokens never appear in a URL. ## Security model * An identity provider is trusted only for its workspace's verified domains. If it asserts an email on any other domain, sign-in is refused. * A person who already belongs to a different Orvlin workspace cannot sign in through another workspace's connection. * **OIDC:** authorization code flow with PKCE (S256). The state and nonce are single-use. The ID token signature is checked against the provider's published keys, and the issuer, audience, expiry, and nonce are enforced. `email_verified: false` is refused. * **SAML:** the assertion signature must verify against the configured certificate. The audience, recipient, and validity window are enforced. A response can be used once. Sign-in must start from Orvlin; IdP-initiated responses are refused. * Client secrets are stored encrypted. # Usage metering Source: https://docs.orvlin.com/guides/usage-metering Send events reliably and design them so metrics stay accurate Usage metering is how your product tells Orvlin what customers did. Send one event per billable action, with enough properties to price it. ## Send events `POST /v1/events` accepts one event, an array of events, or an object with an `events` array: ```bash theme={null} curl -X POST "$ORVLIN_API/v1/events" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "events": [ { "customerId": "cust_acme", "eventName": "api_call", "timestamp": "2026-10-12T09:30:00Z", "idempotencyKey": "req_8f2c1a", "properties": { "endpoint": "/v1/search", "method": "POST", "statusCode": 200, "latencyMs": 142 } }, { "customerId": "cust_acme", "eventName": "api_call", "idempotencyKey": "req_8f2c1b", "properties": { "endpoint": "/v1/search", "method": "GET", "statusCode": 200, "latencyMs": 88 } } ] }' ``` The response lists the IDs of accepted events and any that were rejected, with the reason. ## Use idempotency keys Networks fail and workers retry. Give every billable action a stable `idempotencyKey`, such as the request ID or the ID of the job that produced it. If the same key arrives twice, Orvlin counts it once. Derive the key from the thing that happened, not from the time you sent the event. A retry should produce the same key. ## Timestamps `timestamp` is an ISO 8601 string. If you omit it, Orvlin uses the time of ingestion. Set it yourself when you send events in batches later, so they land in the correct billing period. ## Design event names and properties * **Name events for actions**, such as `api_call`, `image_generated`, or `token_usage`. Don't name them after prices. * **Put the numbers you bill on in `properties`**, such as `tokens`, `bytes`, or `seconds`. A `sum` or `max` metric reads these. * **Keep properties flat and typed.** Use the same name and type for the same field across every event. * **Use a `cost` or `amount` property only when you know the cost.** Orvlin also attributes it to budgets that cover the event. ## Batching Batch events to reduce request overhead. The Node and Python SDKs queue and send events for you. See [Node SDK](/sdks/node#track-usage). ## Verify what was counted List recent events for a customer and event name: ```bash theme={null} curl "$ORVLIN_API/v1/events?customer_id=cust_acme&event_name=api_call&limit=20" \ -H "Authorization: Bearer $ORVLIN_API_KEY" ``` Check a customer's consumption against a metric with the [usage endpoints](/api-reference/overview#usage). # Introduction Source: https://docs.orvlin.com/introduction What Orvlin is, what it does, and where to start Orvlin is a hosted billing platform for SaaS and AI products. It meters the usage your product generates, turns it into prices and plans, checks what each customer is entitled to, and produces the invoices and payments that follow. You send Orvlin events from your application. Orvlin aggregates them into metrics, applies your prices and plans, and handles subscriptions, credits, invoicing, and collection. Your application asks Orvlin whether a customer can use a feature before it does the work. ## What you can build with it Meter API calls, tokens, seats, or any custom event, and bill for consumption. Gate features and enforce limits in your request path with a local cache. Sell credit packs, grant credits, and top up automatically when balances run low. Issue invoices, collect payment, retry failed charges, and void or credit as needed. ## How it fits together ```mermaid theme={null} graph LR App[Your application] -->|POST /v1/events| Orvlin[Orvlin] App -->|GET entitlements| Orvlin Orvlin -->|Subscriptions, invoices, payments| Stripe[Payment provider] Orvlin -->|webhooks| App ``` ## Where to go next Create a customer, define a price, and send your first event in a few minutes. Learn the objects: events, metrics, prices, plans, subscriptions, and invoices. Every endpoint, with request and response fields. Start in a sandbox workspace. Sandbox payments never reach a real payment processor, so you can test invoices and collection safely. # Pricing as code Source: https://docs.orvlin.com/pricing-manifest Declare metrics, prices, and plans in a pricing.yaml file You can define your catalog in a `pricing.yaml` file and keep it in version control with the rest of your code. Deploy it to your workspace with the [CLI](/sdks/cli). Deploys are idempotent: running a deploy again does not duplicate plans, prices, or metrics. Use the manifest for the catalog. Use the API for customers, subscriptions, and usage. ## Structure The file lists `resources`. Each resource has a `kind`: | Kind | Declares | | - | - | | `Event` | The shape of an event: its name and expected properties. | | `Metric` | How events are aggregated. | | `Price` | How a metric (or a flat fee) is charged. | | `Plan` | A bundle of prices. | ## Example ```yaml theme={null} version: "1.0" accountId: "00000000-0000-0000-0000-000000000000" resources: # 1. The event your application sends - kind: Event name: api_call eventName: api_call description: API call event properties: endpoint: string method: string statusCode: number latencyMs: number requiredProperties: - endpoint - method # 2. Aggregate the events into a metric - kind: Metric name: api_calls_total description: Total API calls aggregation: type: count filter: {} groupBy: [] eventNames: - api_call # 3. Charge for the metric - kind: Price name: api_call_price description: Per API call pricing metric: api_calls_total config: model: unit unitPrice: 0.001 currency: USD cadence: monthly timing: in_arrears # 4. Bundle the price into a plan - kind: Plan name: starter description: Starter plan prices: - price: api_call_price isActive: true ``` Each `Price` references a `Metric` by name, and each `Plan` references `Price` resources by name. Keep names stable: they are how resources are matched between deploys. ## Validate before you deploy Check the manifest without changing anything: ```bash theme={null} orvlin deploy --manifest pricing.yaml --validate-only ``` Then preview the changes against your workspace: ```bash theme={null} orvlin deploy --manifest pricing.yaml --environment sandbox --dry-run ``` See the [CLI reference](/sdks/cli) for every option. ## Changing prices Editing a price or plan in the manifest creates a new version. Existing subscriptions keep the version they started on. To move customers to the new terms, use a price migration or a subscription migration, as described in [Subscriptions](/api-reference/subscriptions). # Pricing models Source: https://docs.orvlin.com/pricing-models Choose how a price turns usage into an amount Every price has a `config` object whose `model` decides how the metric's value becomes an amount. Amounts are in the price's `currency` (three-letter ISO code, default `USD`). ## Unit Charge a fixed amount per unit. ```json theme={null} { "model": "unit", "unitPrice": 0.001, "currency": "USD" } ``` 100,000 calls cost 100,000 × 0.001 = \$100. ## Tiered Charge different unit prices for different ranges. Each tier has an `upTo` limit (`null` means no limit), a `unitPrice`, and an optional `flatFee` charged once when the tier is reached. ```json theme={null} { "model": "tiered", "currency": "USD", "tiers": [ { "upTo": 1000, "unitPrice": 0.02 }, { "upTo": 10000, "unitPrice": 0.01 }, { "upTo": null, "unitPrice": 0.005 } ] } ``` Tiers are graduated: the first 1,000 units use the first tier's price, the next 9,000 use the second, and the rest use the third. ## Bulk Like tiered, but the tier a total falls into sets the unit price for every unit. There are no splits across tiers. ```json theme={null} { "model": "bulk", "currency": "USD", "tiers": [ { "upTo": 1000, "unitPrice": 0.02 }, { "upTo": null, "unitPrice": 0.01 } ] } ``` With 1,500 units, all 1,500 are billed at \$0.01. ## Package Charge per block of units. A `size` and a `unitPrice` define each package; the amount is the number of packages needed, rounded up, multiplied by the package price. ```json theme={null} { "model": "package", "currency": "USD", "packages": [{ "size": 1000, "unitPrice": 5 }] } ``` 1,001 units bill two packages, \$10. ## Percentage Charge a percentage of another metric's value. Useful for payment-style fees on transaction volume. ```json theme={null} { "model": "percentage", "basisMetricId": "", "percentage": 2.5, "currency": "USD" } ``` ## License Charge per seat for a license type you allocate to customers with [`POST /v1/licenses`](/api-reference/licenses). ```json theme={null} { "model": "license", "licenseTypeId": "seats", "unitPrice": 12, "currency": "USD" } ``` ## Choosing a model | You want to charge... | Use | | - | - | | A fixed rate per call, token, or event | `unit` | | Less per unit as volume grows, billed in steps | `tiered` | | One rate for all units, based on total volume | `bulk` | | In blocks, such as per 1,000 messages | `package` | | A cut of another metric's value | `percentage` | | A price per seat | `license` | A price with no metric is a flat fee. Set `metricId` to `null` (or omit it) and use a `cadence` of `monthly`, `quarterly`, `annual`, or `one_time`. # Quickstart Source: https://docs.orvlin.com/quickstart Create a customer, define a price, and send your first usage event This walkthrough takes you from an empty workspace to a subscribed customer whose usage is being metered. You need an Orvlin workspace and a user who can create API keys. In the dashboard, create an API key. Choose the `developer` role if the key will send usage, or `viewer` if it only checks entitlements. See [API keys](/api-reference/api-keys#roles) for every role. The secret is shown once. Store it in your secret manager. ```bash theme={null} export ORVLIN_API_KEY="orv_xxxxxxxx_your_secret" export ORVLIN_API="https://api.orvlin.com" ``` Every request below sends the key as `Authorization: Bearer $ORVLIN_API_KEY`. A metric counts or sums matching events over a billing period. This one counts every `api_call` event. ```bash theme={null} curl -X POST "$ORVLIN_API/v1/metrics" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "api_calls_total", "description": "Total API calls", "aggregation": { "type": "count", "filter": {}, "groupBy": [] } }' ``` Note the `id` in the response. You need it for the price. A price charges for a metric. This one is a unit price of \$0.001 per call, billed monthly in arrears. ```bash theme={null} curl -X POST "$ORVLIN_API/v1/prices" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "api_call_price", "metricId": "", "config": { "model": "unit", "unitPrice": 0.001, "currency": "USD" }, "cadence": "monthly", "timing": "in_arrears" }' ``` See [Pricing models](/pricing-models) for the other ways to charge. A plan bundles prices that customers subscribe to. ```bash theme={null} curl -X POST "$ORVLIN_API/v1/plans" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "starter", "description": "Starter plan", "prices": [{ "priceId": "" }] }' ``` Note the plan `id`. ```bash theme={null} curl -X POST "$ORVLIN_API/v1/customers" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "cust_acme", "name": "Acme Corp", "email": "billing@acme.example", "currency": "USD", "metadata": { "region": "eu-west" } }' ``` If you omit `id`, Orvlin generates one (`cust_…`). Supplying your own ID lets you use the same identifier in your application. ```bash theme={null} curl -X POST "$ORVLIN_API/v1/subscriptions" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "customerId": "cust_acme", "planId": "" }' ``` Subscriptions take the plan's `planId`, not its name. ```bash theme={null} curl -X POST "$ORVLIN_API/v1/events" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "customerId": "cust_acme", "eventName": "api_call", "idempotencyKey": "req_8f2c1a", "properties": { "endpoint": "/v1/search", "method": "POST", "statusCode": 200, "latencyMs": 142 } }' ``` The response lists the accepted event IDs. Re-sending the same `idempotencyKey` does not count the call twice. ## Check what you've done Ask Orvlin what the customer is entitled to: ```bash theme={null} curl "$ORVLIN_API/v1/customers/cust_acme/entitlements" \ -H "Authorization: Bearer $ORVLIN_API_KEY" ``` The usage you sent shows up in the customer's consumption. In production, call the SDK instead of raw HTTP. It caches answers so your request path never waits on the network. See [Node SDK](/sdks/node). ## Next steps Batch events, choose properties, and design event names. Gate features in your application. Get notified when invoices are paid or subscriptions change. Keep your metrics, prices, and plans in a `pricing.yaml` file. # CLI Source: https://docs.orvlin.com/sdks/cli Validate, deploy, and simulate your pricing manifest, and generate typed SDK code The `orvlin` command-line tool works with your pricing manifest against your Orvlin workspace. Use it to generate typed SDK code, validate and deploy the catalog, and simulate what a subscription would be billed. ## Install ```bash theme={null} npm install -g @orvlin/cli orvlin --help ``` Sign in with your API key by setting `ORVLIN_API_KEY` in the environment. Every command accepts: | Option | Description | | - | - | | `-v, --verbose` | Detailed logs. | | `--config ` | A config file to read settings from. | ## Commands | Command | What it does | | - | - | | `orvlin generate` | Generates typed SDK code from your manifest. | | `orvlin deploy` | Validates the manifest and deploys it to a workspace environment. | | `orvlin simulate` | Simulates billing for a subscription or customer over a period. | ### `orvlin generate` ```bash theme={null} orvlin generate --manifest pricing.yaml --output ./generated-sdk ``` | Option | Default | Description | | - | - | - | | `-m, --manifest ` | `pricing.yaml` | The pricing manifest. | | `-o, --output ` | `./generated-sdk` | Output directory. | | `--package-name ` | `@myorg/orvlin-sdk` | The name of the generated package. | | `--watch` | | Regenerate when the manifest changes. | ### `orvlin deploy` ```bash theme={null} orvlin deploy --manifest pricing.yaml --environment sandbox --dry-run ``` | Option | Default | Description | | - | - | - | | `-m, --manifest ` | `pricing.yaml` | The pricing manifest. | | `-e, --environment ` | `development` | The workspace environment to deploy to, such as your sandbox or production workspace. | | `--dry-run` | | Show what would change without applying it. | | `--validate-only` | | Check the manifest and stop. | Deploys are safe to repeat. Resources that haven't changed are left alone. See [Pricing as code](/pricing-manifest). Run `--validate-only`, then `--dry-run`, and review the changes before you deploy to a production workspace. ### `orvlin simulate` ```bash theme={null} orvlin simulate \ --manifest pricing.yaml \ --customer cust_acme \ --period-start 2026-10-01 \ --period-end 2026-10-31 \ --output table ``` | Option | Description | | - | - | | `-m, --manifest ` | The pricing manifest. | | `--period-start ` / `--period-end ` | The period to simulate, as `YYYY-MM-DD`. | | `--subscription ` | Simulate an existing subscription. | | `--customer ` | Simulate a customer. | | `--plan ` | Compare against another plan. | | `--price-override ` | Try a different price. Repeat the option for more. | | `--output ` | `table` (default) or `json`. | Use simulation to check a new price against real usage before you publish it. # Node.js SDK Source: https://docs.orvlin.com/sdks/node Check entitlements, track usage, and manage credits from a Node.js server The `@orvlin/node` package is the server-side SDK for Node.js. Use it in the request path of your API to check entitlements and report usage. ## Install ```bash theme={null} npm install @orvlin/node ``` ## Create a client ```ts theme={null} import { Orvlin } from '@orvlin/node'; const orvlin = new Orvlin({ apiKey: process.env.ORVLIN_API_KEY!, baseUrl: 'https://api.orvlin.com', }); ``` Use the smallest API key role that works. A `viewer` key can check entitlements. Reporting usage needs a `developer` key. ### Options | Option | Default | Description | | - | - | - | | `apiKey` | required | Your API key. | | `baseUrl` | `ORVLIN_API_URL` | Your Orvlin host. | | `cacheTtlMs` | `30000` | How long a checked answer is fresh. | | `staleTtlMs` | 24 hours | How long a stale answer may be used when Orvlin is unreachable. | | `timeoutMs` | `2000` | Timeout for each request to Orvlin. | | `onUnavailable` | `'deny'` | Answer for a customer the cache has never seen while Orvlin is unreachable. | | `fetch` | global `fetch` | Inject a fetch implementation for tests or proxies. | | `logger` | none | Receives warnings, with `warn(message, meta)`. | ## Check an entitlement ```ts theme={null} const { hasAccess, remaining, source } = await orvlin.check('cust_acme', 'api_calls', { amount: 1 }); ``` `check` returns: | Field | Description | | - | - | | `hasAccess` | Whether the customer can use the feature. | | `allocated` | The allowance, or `null` for no cap. | | `consumed` | How much has been used this period. | | `remaining` | What is left, or `null` for no cap. | | `source` | `cache`, `stale`, `live`, or `fallback`. See [Entitlements](/guides/entitlements#how-answers-are-cached). | To read all of a customer's entitlements: ```ts theme={null} const entitlements = await orvlin.getEntitlements('cust_acme'); ``` To check synchronously from the cache only, use `checkCached`. It returns `null` when there is no cached answer yet. ## Cache management ```ts theme={null} await orvlin.prefetch(['cust_acme', 'cust_globex']); // warm the cache orvlin.invalidate('cust_acme'); // drop one customer orvlin.invalidate(); // drop everything const snapshot = orvlin.snapshot(); // save the cache orvlin.restore(snapshot); // load it into a new process ``` ## Track usage ```ts theme={null} await orvlin.track({ customerId: 'cust_acme', eventName: 'api_call', idempotencyKey: requestId, properties: { endpoint: '/v1/search', statusCode: 200, latencyMs: 142 }, }); ``` Pass an array to send several events in one call: ```ts theme={null} const result = await orvlin.track([ { customerId: 'cust_acme', eventName: 'api_call', idempotencyKey: 'req_1' }, { customerId: 'cust_acme', eventName: 'api_call', idempotencyKey: 'req_2' }, ]); // result: { accepted: 2, rejected: [] } ``` `track` returns the number accepted and the rejected events, with their index and the reason. Using the same `idempotencyKey` for retries counts the event once. ## Credits ```ts theme={null} const { allowed, balance } = await orvlin.checkCredits('cust_acme', { creditType: 'ai_images', quantity: 1, }); const wallets = await orvlin.getCreditBalances('cust_acme'); await orvlin.grantCredits('cust_acme', 'ai_images', { amount: 500, source: 'promotional', idempotencyKey: 'promo_launch_acme', }); await orvlin.purchaseCredits('cust_acme', 'ai_images', { credits: 1000, collection: 'charge', idempotencyKey: 'order_20261012_001', }); await orvlin.setAutoTopUp('cust_acme', 'ai_images', { threshold: 100, credits: 1000, maxPerMonth: 5, enabled: true, }); ``` See [Prepaid credits](/guides/prepaid-credits) for how these fit together. ## Errors Failed calls throw `OrvlinError`, which has a `status` property for the HTTP status code when there is one. ```ts theme={null} import { OrvlinError } from '@orvlin/node'; try { await orvlin.track(event); } catch (err) { if (err instanceof OrvlinError && err.status === 401) { // The API key is missing, revoked, or has the wrong role. } } ``` ## Next steps Design events so the numbers are right. What each status code means. # Python SDK Source: https://docs.orvlin.com/sdks/python Check entitlements, track usage, and manage credits from Python The `orvlin` package is the Python SDK for server-side code. It mirrors the [Node.js SDK](/sdks/node): the same entitlement cache, usage tracking, and credit operations, with Python naming. It supports Python 3.9 and later. An async client, `AsyncOrvlin`, is also included. ## Install ```bash theme={null} pip install orvlin ``` ## Create a client ```python theme={null} import os from orvlin import Orvlin orvlin = Orvlin( api_key=os.environ["ORVLIN_API_KEY"], base_url="https://api.orvlin.com", ) ``` If you omit `base_url`, the client reads `ORVLIN_API_URL`. ### Options | Option | Default | Description | | - | - | - | | `api_key` | required | Your API key. | | `base_url` | `ORVLIN_API_URL` | Your Orvlin host. | | `cache_ttl` | `30.0` | Seconds a checked answer is fresh. | | `stale_ttl` | 24 hours | Seconds a stale answer may be used when Orvlin is unreachable. | | `timeout` | `2.0` | Seconds to wait for each request. | | `on_unavailable` | `"deny"` | Answer for a customer the cache has never seen while Orvlin is unreachable. Or `"allow"`. | | `logger` | none | A `logging.Logger` for warnings. | ## Check an entitlement ```python theme={null} check = orvlin.check("cust_acme", "api_calls", amount=1) if not check.has_access: raise PermissionError("Upgrade your plan to continue.") ``` The result has `has_access`, `allocated`, `consumed`, `remaining`, and `source`. `remaining` is `None` for no cap. Other cache operations: ```python theme={null} orvlin.prefetch(["cust_acme", "cust_globex"]) orvlin.invalidate("cust_acme") # or orvlin.invalidate() for everyone state = orvlin.snapshot() # save the cache orvlin.restore(state) # load it into a new process ``` ## Track usage ```python theme={null} orvlin.track({ "customer_id": "cust_acme", "event_name": "api_call", "idempotency_key": request_id, "properties": {"endpoint": "/v1/search", "status_code": 200, "latency_ms": 142}, }) ``` Pass a list to send several events in one call. Each event's `idempotency_key` makes retries count once. `timestamp` accepts a `datetime` or an ISO 8601 string, and defaults to now. ## Credits ```python theme={null} check = orvlin.check_credits("cust_acme", credit_type="ai_images", quantity=1) orvlin.grant_credits("cust_acme", "ai_images", 500, source="promotional", idempotency_key="promo_launch_acme") orvlin.purchase_credits("cust_acme", "ai_images", 1000, collection="charge", idempotency_key="order_20261012_001") orvlin.set_auto_top_up("cust_acme", "ai_images", threshold=100, credits=1000, max_per_month=5, enabled=True) ``` ## Async Use `AsyncOrvlin` in async code. It takes the same arguments as `Orvlin`, and its methods are awaited: ```python theme={null} from orvlin import AsyncOrvlin orvlin = AsyncOrvlin(api_key=KEY, base_url="https://api.orvlin.com") async def handle(request): check = await orvlin.check("cust_acme", "api_calls", amount=1) ... ``` ## Errors Failed calls raise `OrvlinError`. Its `status` attribute holds the HTTP status code when there is one. ```python theme={null} from orvlin import OrvlinError try: orvlin.track(event) except OrvlinError as err: if err.status == 401: ... # the API key is missing, revoked, or has the wrong role ``` # React SDK Source: https://docs.orvlin.com/sdks/react Show features, balances, and plan options in a React app with the customer portal The `@orvlin/react` package provides React components and hooks for your customer-facing app. It reads a signed-in customer's entitlements and credit balances, so you can show or hide features in the UI. The React SDK does not use your API key. It authenticates with a **portal token** for one customer. Your backend creates that token with a [portal session](/guides/customer-portal) and passes it to the browser. Never put your API key in browser code. ## Install ```bash theme={null} npm install @orvlin/react ``` ## Provide the client Wrap your app in `OrvlinProvider`: ```tsx theme={null} import { OrvlinProvider } from '@orvlin/react'; export function App({ portalToken }: { portalToken: string }) { return ( ); } ``` | Prop | Description | | - | - | | `token` | The portal token for the signed-in customer. | | `getToken` | Instead of `token`: a function that returns one. It is called again when the token expires. | | `apiBase` | Your Orvlin API host. | | `refreshIntervalMs` | How often to refresh what is shown, in milliseconds. Default `30000`. `0` turns it off. | | `client` | An existing client to share with non-React code. | ## Show a feature The `` component renders its children only if the customer can use the feature: ```tsx theme={null} import { Feature } from '@orvlin/react'; } loading={}> ``` ## Hooks ```tsx theme={null} import { useEntitlement, useEntitlements, useCredits } from '@orvlin/react'; // One feature const { hasAccess, remaining, loading } = useEntitlement('api_calls', { amount: 1 }); // Everything the customer can use const { entitlements, loading, error, refresh } = useEntitlements(); // Credit balances; pass a credit type to focus on one const { wallet, balance, wallets, refresh } = useCredits('ai_images'); ``` Each hook returns `loading` so you can render a placeholder while data arrives. `useEntitlements` and `useCredits` also return `error`. ## Refresh after a change Entitlements and balances refresh on the interval you set. To refresh straight after the customer does something, call `refresh()` from `useEntitlements` or `useCredits`. ## Next steps Create the portal session your app needs. How checks and limits work. # Gate features with entitlements Source: https://docs.orvlin.com/tutorials/gate-features-with-entitlements Block requests for customers who are out of allowance, with a fast cached check **Time:** about 25 minutes · **Level:** intermediate · **You'll build:** an Express middleware that checks entitlements before each request and records usage only for allowed requests. ## Scenario Your plans include a monthly allowance of search requests. When a customer uses it up, the API should return `402 Payment Required` with a link to upgrade, without slowing down the requests that are allowed. This tutorial uses the setup from [Meter your first API](/tutorials/meter-your-first-api). Complete that first. ## Prerequisites * A `developer` API key for the sandbox workspace, and the `cust_tutorial_1` customer. * Node.js 20 or later. ```bash theme={null} mkdir gated-api && cd gated-api npm init -y npm install express @orvlin/node export ORVLIN_API="https://api.orvlin.com" export ORVLIN_API_KEY="orv_xxxxxxxx_your_secret" ``` Create one client and reuse it. The client keeps the entitlement cache, so each request doesn't open a new connection or wait for the network. ```js theme={null} // orvlin.js import { Orvlin } from '@orvlin/node'; export const orvlin = new Orvlin({ apiKey: process.env.ORVLIN_API_KEY, baseUrl: process.env.ORVLIN_API, cacheTtlMs: 30_000, // treat a checked answer as fresh for 30 seconds staleTtlMs: 86_400_000, // use a stale answer for up to 24 hours if Orvlin is unreachable timeoutMs: 2_000, // don't keep a user waiting on a slow check onUnavailable: 'deny', // unknown customers are refused if Orvlin is down }); ``` `onUnavailable: 'deny'` is the safe default. A customer you've seen recently still gets through on a stale answer. A customer the cache has never seen is refused until Orvlin responds. The middleware reads the customer from a request header, checks the feature, and either passes the request on or returns `402`. ```js theme={null} // gate.js import { orvlin } from './orvlin.js'; export function requireAllowance(feature) { return async (req, res, next) => { const customerId = req.header('x-customer-id'); if (!customerId) return res.status(401).json({ error: 'Missing customer' }); try { const check = await orvlin.check(customerId, feature, { amount: 1 }); if (!check.hasAccess) { return res.status(402).json({ error: 'Allowance used up', upgradeUrl: 'https://app.example.com/billing', }); } req.entitlement = check; next(); } catch (err) { // Only reached if the SDK itself fails, which is rare. next(err); } }; } ``` The `amount` is how much this request uses. Use `1` for one request, or the real cost for variable-cost requests. Record the event after the work succeeds. A failed request shouldn't count toward the allowance. ```js theme={null} // server.js import express from 'express'; import { orvlin } from './orvlin.js'; import { requireAllowance } from './gate.js'; const app = express(); app.get('/search', requireAllowance('search_requests'), async (req, res) => { const customerId = req.header('x-customer-id'); const requestId = req.header('x-request-id') ?? crypto.randomUUID(); const results = [{ title: 'Example result' }]; // your real search goes here // Count the request after it succeeds, keyed by the request ID. await orvlin.track({ customerId, eventName: 'search_request', idempotencyKey: requestId, properties: { statusCode: 200 }, }); res.json({ results, remaining: req.entitlement.remaining }); }); app.listen(3000, () => console.log('listening on :3000')); ``` `track` is awaited here so a failed usage write is visible. In production, you may prefer to queue it and not block the response. If you queue it, keep the same `idempotencyKey`. Start the server: ```bash theme={null} node server.js ``` In another terminal, call the endpoint: ```bash theme={null} curl -i http://localhost:3000/search -H "x-customer-id: cust_tutorial_1" ``` You should get `200 OK` and a `remaining` count. To test the `402` path, use a customer whose allowance is used up. Check the allowance for that customer and feature first with `GET /v1/customers/:id/entitlements/:featureName`, then send a request as that customer. The SDK caches answers for 30 seconds, so wait for the cache to expire or call `orvlin.invalidate(customerId)`. ```bash theme={null} curl -i http://localhost:3000/search -H "x-customer-id: cust_exhausted" ``` An exhausted customer gets `402 Payment Required`. Call the endpoint twice in a row. The second check should come from the cache, which you can see with the `source` field returned by `orvlin.check`: ```js theme={null} const first = await orvlin.check('cust_tutorial_1', 'search_requests'); const second = await orvlin.check('cust_tutorial_1', 'search_requests'); console.log(first.source, second.source); // "live" "cache" ``` Cached answers are what keep the check fast. See [How answers are cached](/guides/entitlements#how-answers-are-cached) for every source value. ## Production checklist * Create one `Orvlin` client per process, not one per request. * Pick `onUnavailable` deliberately. `'deny'` is safer for paid features. `'allow'` may suit low-risk features during an outage. * Use a `viewer` key for read-only services that only check entitlements. * Log the `source` field so you can see how often checks fall back. ## Next steps Let customers pay in advance for AI generations. Every cache setting and what it does. # Launch a customer portal Source: https://docs.orvlin.com/tutorials/launch-customer-portal Let customers see usage and invoices, and change plans, inside your own app **Time:** about 30 minutes · **Level:** intermediate · **You'll build:** a backend route that issues a scoped portal session, and a React page that shows the customer's plan and balance. ## Scenario Your customers want to check their usage and change plans without emailing you. You'll give each signed-in customer a short-lived portal session. The browser never sees your API key. ## Prerequisites * A sandbox workspace with a `developer` API key, and a customer such as `cust_tutorial_1`. * A backend that can keep a secret (Node.js is used here) and a React app. Your server checks that the user is signed in to your app, then creates a session for that user's customer only. ```js theme={null} // server/portal.js export async function createPortalSession(req, res) { const user = req.user; // from your own auth if (!user) return res.status(401).end(); const r = await fetch(`${process.env.ORVLIN_API}/v1/portal/sessions`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.ORVLIN_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ customerId: user.billingCustomerId, // the customer linked to this user expiresInSeconds: 900, // 15 minutes allowPlanChanges: true, allowCreditPurchases: false, }), }); if (!r.ok) return res.status(502).json({ error: 'Could not start billing portal' }); const session = await r.json(); res.json(session); // return only what the browser needs } ``` Never take the `customerId` from the browser. Read it from the signed-in user on your server, or any user could open another customer's billing. The React SDK uses the portal token, not your API key. Pass it to `OrvlinProvider`. Use `getToken` if your token expires, so the SDK can request a new one. ```tsx theme={null} // app/BillingPage.tsx import { OrvlinProvider, useEntitlements, useCredits } from '@orvlin/react'; export function BillingPage({ portalToken }: { portalToken: string }) { return ( ); } function Usage() { const { entitlements, loading, error } = useEntitlements(); const { balance } = useCredits('image_credits'); if (loading) return

Loading your plan…

; if (error) return

We couldn't load your billing details.

; return (

Your plan

    {entitlements.map((e) => (
  • {e.featureName}: {e.remaining ?? 'unlimited'} left
  • ))}

Image credits: {balance ?? 0}

); } ``` The hooks refresh automatically. Show `loading` and `error` states so the page never looks empty by mistake.
Before a customer changes plans, show what it will cost. The preview doesn't change anything. ```bash theme={null} curl -s "$ORVLIN_API/v1/portal/plan-change/preview?planId=$PRO_PLAN_ID" \ -H "Authorization: Bearer $PORTAL_TOKEN" | jq ``` Show the result on screen. When the customer confirms, call `POST /v1/portal/plan-change` with the same plan. ```bash theme={null} curl -s -X POST "$ORVLIN_API/v1/portal/plan-change" \ -H "Authorization: Bearer $PORTAL_TOKEN" \ -H "Content-Type: application/json" \ -d "{ \"planId\": \"$PRO_PLAN_ID\" }" ``` Confirm the plan change with a second click. Plan changes can change the next invoice. ```bash theme={null} curl -s "$ORVLIN_API/v1/portal/invoices" \ -H "Authorization: Bearer $PORTAL_TOKEN" | jq '.data[] | {id, status, total}' ``` Link each invoice to its hosted page from the [invoices API](/api-reference/invoices), so the customer can pay or download it. Sessions expire on their own. Keep `expiresInSeconds` short, and create a new session each time the customer opens the billing page. Tokens are scoped to one customer, so a leaked token can't read another customer's data.
## Production checklist * Create sessions only for users who are signed in to your app. * Derive the customer ID on the server, never from the request. * Keep sessions short-lived. * Enable `allowCreditPurchases` only if you have credit types configured. * Show the plan-change preview before every change. ## Next steps Every portal endpoint and option. Request and response details. # Meter your first API Source: https://docs.orvlin.com/tutorials/meter-your-first-api Build a complete billing setup for a metered API, from catalog to first invoice **Time:** about 20 minutes · **Level:** beginner · **You'll build:** a usage-billed API with one plan, one customer, and a verified usage record. ## Scenario You run a search API. Each request costs \$0.001, and customers pay monthly for what they use. By the end of this tutorial, every request your API serves will be recorded against the right customer, and you'll be able to see the usage on their account. ## Prerequisites * A sandbox workspace and a `developer` API key. See [Quickstart](/quickstart) if you need one. * Node.js 20 or later, to run the script in step 5. * `curl` and `jq` in your terminal. Set these variables once for the whole tutorial: ```bash theme={null} export ORVLIN_API="https://api.orvlin.com" export ORVLIN_API_KEY="orv_xxxxxxxx_your_secret" ``` A metric counts the events you care about. Start with a count of successful search requests. ```bash theme={null} METRIC_ID=$(curl -s -X POST "$ORVLIN_API/v1/metrics" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "search_requests", "description": "Successful search requests", "aggregation": { "type": "count", "filter": { "statusCode": 200 } } }' | jq -r '.id') echo "metric: $METRIC_ID" ``` The `filter` means failed requests (for example, `500`) are not billed. A unit price charges the same amount for each unit. This one is \$0.001 per request, billed monthly in arrears. ```bash theme={null} PRICE_ID=$(curl -s -X POST "$ORVLIN_API/v1/prices" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"search_price\", \"metricId\": \"$METRIC_ID\", \"config\": { \"model\": \"unit\", \"unitPrice\": 0.001, \"currency\": \"USD\" }, \"cadence\": \"monthly\", \"timing\": \"in_arrears\" }" | jq -r '.id') echo "price: $PRICE_ID" ``` Want volume discounts instead? Swap the `config` for a tiered model. See [Pricing models](/pricing-models). ```bash theme={null} PLAN_ID=$(curl -s -X POST "$ORVLIN_API/v1/plans" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"pay-as-you-go\", \"description\": \"Pay per search\", \"prices\": [{ \"priceId\": \"$PRICE_ID\" }] }" | jq -r '.id') echo "plan: $PLAN_ID" ``` Use your own customer ID so your application and Orvlin share one identifier. ```bash theme={null} curl -s -X POST "$ORVLIN_API/v1/customers" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "cust_tutorial_1", "name": "Tutorial Co", "currency": "USD" }' > /dev/null curl -s -X POST "$ORVLIN_API/v1/subscriptions" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"customerId\": \"cust_tutorial_1\", \"planId\": \"$PLAN_ID\" }" | jq '{id, status}' ``` You should see a subscription with `"status": "active"`, or `"trialing"` if the plan has a trial. Every billable request should produce one event, with an idempotency key tied to the request. Here is the pattern in Node.js. Save it as `meter.mjs`: ```js theme={null} // meter.mjs const API = process.env.ORVLIN_API; const KEY = process.env.ORVLIN_API_KEY; async function recordSearch(customerId, requestId, statusCode) { const res = await fetch(`${API}/v1/events`, { method: 'POST', headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ customerId, eventName: 'search_request', idempotencyKey: requestId, properties: { statusCode }, }), }); if (!res.ok) throw new Error(`Orvlin ${res.status}: ${await res.text()}`); return res.json(); } // Simulate three requests, one of which fails. await recordSearch('cust_tutorial_1', 'req_001', 200); await recordSearch('cust_tutorial_1', 'req_002', 200); await recordSearch('cust_tutorial_1', 'req_003', 500); console.log('recorded'); ``` Run it: ```bash theme={null} node meter.mjs ``` The `eventName` must match what your metric counts. Here it is `search_request`, and the metric counts events where `statusCode` is `200`. The `500` request is recorded but is not billed. Don't change the `idempotencyKey` between retries of the same request. Reusing a key stops a retry from being counted twice. List the events for the customer: ```bash theme={null} curl -s "$ORVLIN_API/v1/events?customer_id=cust_tutorial_1&event_name=search_request&limit=10" \ -H "Authorization: Bearer $ORVLIN_API_KEY" | jq '.data | length' ``` You should see `3`. Next, check what the customer can use: ```bash theme={null} curl -s "$ORVLIN_API/v1/customers/cust_tutorial_1/entitlements" \ -H "Authorization: Bearer $ORVLIN_API_KEY" | jq ``` ## What you built * A metric that counts successful requests. * A unit price of \$0.001 per request. * A plan the customer subscribes to. * A usage pipeline that is safe to retry. ## Next steps Stop requests when a customer runs out of allowance. Move time forward and see the first invoice. # Receive webhooks securely Source: https://docs.orvlin.com/tutorials/receive-webhooks-securely Verify signatures, process each event once, and recover from failed deliveries **Time:** about 25 minutes · **Level:** intermediate · **You'll build:** a webhook receiver that rejects forged requests, handles retries safely, and records what it processed. ## Scenario When an invoice is paid, your system should activate the customer's paid features. Webhooks deliver that event, but endpoints get retried, arrive out of order, and can be spoofed. This tutorial builds a receiver that handles all three. ## Prerequisites * A public HTTPS endpoint, or a tunnel such as `ngrok` for local testing. * A signed-in user session to register the webhook. Webhook management requires a user session, not an API key. * Node.js 20 or later. Subscribe to the two events this tutorial handles. Replace the URL with your endpoint. ```bash theme={null} curl -s -X POST "$ORVLIN_API/v1/webhooks" \ -H "Authorization: Bearer $USER_SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/orvlin", "events": ["invoice.paid", "subscription.plan_change"] }' | jq -r '.data.secret' ``` Save the secret that starts with `whsec_`. It's needed to verify every delivery, and it's shown in this response. Endpoints must use `https` in production. Store the secret in your secret manager, never in source control. Parsing the JSON before you verify changes the bytes, so verify first. Using Express: ```js theme={null} // webhooks.js import express from 'express'; import crypto from 'node:crypto'; const app = express(); const SECRET = process.env.ORVLIN_WEBHOOK_SECRET; // Keep the body as a raw Buffer for the route that verifies signatures. app.post('/webhooks/orvlin', express.raw({ type: 'application/json' }), (req, res) => { const timestamp = req.header('x-orvlin-timestamp'); const received = req.header('x-orvlin-signature'); if (!timestamp || !received) return res.status(400).end(); // Reject old deliveries so a captured request can't be replayed later. const ageMs = Math.abs(Date.now() - Date.parse(timestamp)); if (!(ageMs < 5 * 60_000)) return res.status(400).end(); const expected = crypto .createHmac('sha256', SECRET) .update(`${timestamp}.`) .update(req.body) .digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(received); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { return res.status(401).end(); } const event = JSON.parse(req.body.toString('utf8')); handle(event) .then(() => res.status(200).end()) .catch((err) => { console.error('handler failed', event.id, err); res.status(500).end(); // Orvlin will retry }); }); app.listen(3000); ``` Return `401` for a bad signature and `400` for missing headers. Don't return `200` for an unverified request. Orvlin retries a delivery until it gets a `2xx`, so the same event can arrive more than once. Record each event ID the first time you process it, and skip repeats. ```js theme={null} // handle.js const processed = new Set(); // use a database table in production export async function handle(event) { if (processed.has(event.id)) return; // duplicate delivery processed.add(event.id); switch (event.event) { case 'invoice.paid': await activateFeatures(event.data.customerId); break; case 'subscription.plan_change': await syncPlan(event.data.customerId, event.data); break; default: // Unknown events are acknowledged and ignored. break; } } async function activateFeatures(customerId) { console.log('activate features for', customerId); } async function syncPlan(customerId, data) { console.log('sync plan for', customerId, data); } ``` Mark the event as processed in the same database transaction as the side effect. If the side effect fails, the mark rolls back, and the retry runs it again. Send yourself a signed request with the same method Orvlin uses. The sample below signs a body with the secret from step 1: ```bash theme={null} TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z) BODY='{"id":"evt_test_1","event":"invoice.paid","timestamp":"'$TS'","data":{"customerId":"cust_tutorial_1"}}' SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$ORVLIN_WEBHOOK_SECRET" -hex | awk '{print $2}') curl -i -X POST http://localhost:3000/webhooks/orvlin \ -H "Content-Type: application/json" \ -H "X-Orvlin-Event: invoice.paid" \ -H "X-Orvlin-Timestamp: $TS" \ -H "X-Orvlin-Signature: $SIG" \ -d "$BODY" ``` You should get `200`. Send the exact same request again: it should also return `200`, and the handler should log nothing new. Change one character of the body and send it: you should get `401`. Orvlin records each delivery attempt. If your endpoint was down, look at the failures, fix the endpoint, and let Orvlin retry: ```bash theme={null} curl -s "$ORVLIN_API/v1/webhooks/$WEBHOOK_ID/deliveries?limit=20" \ -H "Authorization: Bearer $USER_SESSION_TOKEN" | jq '.data' ``` Orvlin retries failed deliveries with increasing delays for about a day. Use the `test` endpoint to check the endpoint after a fix: ```bash theme={null} curl -s -X POST "$ORVLIN_API/v1/webhooks/$WEBHOOK_ID/test" \ -H "Authorization: Bearer $USER_SESSION_TOKEN" ``` ## Production checklist * Verify every request before you parse it. * Compare signatures with a constant-time function. * Reject timestamps older than a few minutes. * Make handlers idempotent, keyed on the event `id`. * Respond quickly, and do slow work from a queue. * Rotate the secret if it leaks, then update the endpoint. ## Next steps Every event, header, and management endpoint. How Orvlin retries, what it guarantees, and what it doesn't. # Sell prepaid credits Source: https://docs.orvlin.com/tutorials/sell-prepaid-credits Sell credit packs, check balances before each AI generation, and top up automatically **Time:** about 30 minutes · **Level:** intermediate · **You'll build:** a credit-based image generator where customers buy credits, each generation draws them down, and balances top up automatically. ## Scenario An AI image app sells credits. A generation costs one credit. Customers buy packs, a starter grant covers their first generations, and customers who opt in are topped up automatically when they run low. ## Prerequisites * A sandbox workspace with a `developer` key and a customer, such as `cust_tutorial_1` from [Meter your first API](/tutorials/meter-your-first-api). * Node.js 20 or later with `@orvlin/node` installed. A credit type defines what the credits are for, and how the balance behaves at zero. Here `block` stops generations when the balance runs out, instead of billing overage. ```bash theme={null} curl -s -X POST "$ORVLIN_API/v1/credit-types" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "key": "image_credits", "name": "Image credits", "unitLabel": "credit", "overage": "block" }' ``` Check the response for the credit type's `key` and any field names your workspace returns. Use the `key` value in every later call. A grant gives credits for free. Use `idempotencyKey` so a retried signup doesn't grant twice. ```bash theme={null} curl -s -X POST "$ORVLIN_API/v1/customers/cust_tutorial_1/wallets/image_credits/grants" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 10, "source": "promotional", "description": "Welcome credits", "idempotencyKey": "welcome_cust_tutorial_1" }' | jq ``` Run it a second time. The response shows `created: false`, and the balance stays at 10. Selling a pack charges the card on file. Use the same idempotency approach, keyed to your order ID. ```bash theme={null} curl -s -X POST "$ORVLIN_API/v1/customers/cust_tutorial_1/wallets/image_credits/purchases" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "credits": 100, "collection": "charge", "description": "100-credit pack", "idempotencyKey": "order_2026_10_13_0001" }' | jq '{status: .block.status, invoice: .invoice.id}' ``` A purchase starts as `pending_payment` and becomes `active` once paid. Until then the credits aren't spendable. Use `"collection": "send_invoice"` for enterprise customers who pay by bank transfer. They get an emailed invoice instead of a card charge. Before you call the image model, check that the customer can pay for it. The check doesn't spend anything. ```js theme={null} // generate.js import { Orvlin } from '@orvlin/node'; const orvlin = new Orvlin({ apiKey: process.env.ORVLIN_API_KEY, baseUrl: process.env.ORVLIN_API, }); export async function generateImage(customerId, prompt) { const { allowed, balance } = await orvlin.checkCredits(customerId, { creditType: 'image_credits', quantity: 1, }); if (!allowed) { const err = new Error('Out of credits'); err.status = 402; err.balance = balance; throw err; } const image = await callImageModel(prompt); // your model call // Usage reported with track() is drawn from the wallet as it arrives. await orvlin.track({ customerId, eventName: 'image_generated', idempotencyKey: image.requestId, properties: { creditsUsed: 1 }, }); return image; } async function callImageModel(prompt) { return { requestId: crypto.randomUUID(), url: 'https://example.com/image.png', prompt }; } ``` The check and the track are separate on purpose. The check decides whether to start the work. The track records the spend after it happens. Usage reported with `track` is drawn from the customer's wallet as it arrives. Before you launch, generate a few test images in the sandbox and confirm the balance on the [ledger](#show-the-balance-and-history) goes down. Top up when the balance falls to 2 credits, buying 50 credits at a time, at most 5 times a month. ```bash theme={null} curl -s -X PUT "$ORVLIN_API/v1/customers/cust_tutorial_1/wallets/image_credits/auto-top-up" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "threshold": 2, "credits": 50, "maxPerMonth": 5, "enabled": true }' | jq ``` The monthly cap stops a runaway loop from charging a customer repeatedly. ```bash theme={null} curl -s "$ORVLIN_API/v1/customers/cust_tutorial_1/wallets" \ -H "Authorization: Bearer $ORVLIN_API_KEY" | jq curl -s "$ORVLIN_API/v1/customers/cust_tutorial_1/wallets/image_credits/ledger" \ -H "Authorization: Bearer $ORVLIN_API_KEY" | jq ``` The ledger lists every grant, purchase, and draw-down. It's the first place to look when a customer asks why their balance changed. ## Production checklist * Check credits before the expensive call, and track usage after it succeeds. * Use an `idempotencyKey` on every grant, purchase, and generation. * Set `overage: "block"` unless you want to bill customers for going over. * Surface `balance` in your UI, so customers know before they run out. ## Next steps Every field for grants, purchases, and auto top-up. Get notified when a pack's invoice is paid. # Test billing with test clocks Source: https://docs.orvlin.com/tutorials/test-billing-with-test-clocks Simulate a full billing cycle in minutes: usage, renewal, invoice, and payment retry **Time:** about 20 minutes · **Level:** beginner · **You'll build:** a simulated customer whose first billing period closes, producing an invoice you can inspect, without waiting a month. ## Scenario You've set up the plan in [Meter your first API](/tutorials/meter-your-first-api). Before you go live, you want to see the first invoice: the usage charges, the date it's due, and what happens when a payment fails. Waiting a real month is impractical, so you'll use a test clock. ## Prerequisites * A sandbox workspace. Test clocks only run in sandbox. * The `cust_tutorial_1` customer and their subscription from the first tutorial. * Some usage recorded on that customer. Freeze the clock at the subscription's start date, so the first period begins now. ```bash theme={null} CLOCK_ID=$(curl -s -X POST "$ORVLIN_API/v1/test-clocks" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "First invoice test", "frozenTime": "2026-10-01T00:00:00Z" }' | jq -r '.id') echo "clock: $CLOCK_ID" ``` `frozenTime` must include a timezone offset or `Z`. Use a date before the usage you want to bill. Attach one customer per request. Only attached customers move with the clock. ```bash theme={null} curl -s -X POST "$ORVLIN_API/v1/test-clocks/$CLOCK_ID/customers" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "customerId": "cust_tutorial_1" }' ``` The customer must be created in this sandbox. Customers in another workspace or a live workspace can't be attached. Usage has to be dated inside the period you're about to close. Send events with timestamps between the clock start and the end of the month: ```bash theme={null} curl -s -X POST "$ORVLIN_API/v1/events" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "events": [ { "customerId": "cust_tutorial_1", "eventName": "search_request", "timestamp": "2026-10-05T12:00:00Z", "idempotencyKey": "tc_req_1", "properties": { "statusCode": 200 } }, { "customerId": "cust_tutorial_1", "eventName": "search_request", "timestamp": "2026-10-20T09:30:00Z", "idempotencyKey": "tc_req_2", "properties": { "statusCode": 200 } } ] }' ``` With a unit price of $0.001, two requests bill $0.002 for this period. `to` is required and must be an ISO 8601 timestamp. Move it past the end of the first month: ```bash theme={null} curl -s -X POST "$ORVLIN_API/v1/test-clocks/$CLOCK_ID/advance" \ -H "Authorization: Bearer $ORVLIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "2026-11-02T00:00:00Z" }' | jq ``` Advancing the clock processes billing for the attached customer up to that time. That includes the renewal and any invoice due in the interval. ```bash theme={null} curl -s "$ORVLIN_API/v1/invoices?customer_id=cust_tutorial_1" \ -H "Authorization: Bearer $ORVLIN_API_KEY" | jq '.data[] | {id, status, total, dueDate}' ``` Check three things: the total matches your expectation (\$0.002 plus any plan fee), the status is `open` or `paid`, and the due date falls in the right place. Look at the line items on the invoice before you trust the total. Each usage price should appear as its own line. To check your retry and notification handling, a payment must fail. Use a test payment method that your payment provider documents as declining, attach it to the customer, then advance the clock again. The decline and any retries are recorded against the invoice: ```bash theme={null} curl -s "$ORVLIN_API/v1/invoices/$INVOICE_ID/payment-attempts" \ -H "Authorization: Bearer $ORVLIN_API_KEY" | jq ``` Watch the webhooks you configured in [Receive webhooks securely](/tutorials/receive-webhooks-securely) for `invoice.payment_failed`, then `invoice.payment_recovered` or `invoice.marked_uncollectible`. Use your payment provider's published test cards only, and only in sandbox. Real cards are never charged in a sandbox workspace. ## What to check before launch * Usage outside the period is excluded from the invoice. * A failed `500` request isn't billed, if your metric filters on `statusCode`. * The invoice total matches your pricing calculator. * Your webhook handler activates and deactivates features on the events above. ## Next steps Keep test and live data apart. The full invoice lifecycle.