API Overview
The PortaAIM API is a RESTful JSON API that provides full programmatic access to all platform features.
Base URL
https://api.portaaim.ai/api/v1
Authentication
Most API requests are authenticated using an API key passed in the X-API-Key header:
X-API-Key: your_api_key_here
An API key carries one or both roles — charging (submit usage) and admin (manage customers, products, and billing configuration) — and only works on the endpoints its role covers. API keys are scoped to a specific tenant and environment (live or sandbox), so no additional environment header is needed. Create and manage API keys from Developers → API Keys in the dashboard.
A few endpoints refuse an API key of any role and require an admin login (email/password → JWT): creating and revoking a key on the API Keys endpoint, GET /admins/me, and the session endpoints under /auth/*. See Endpoints That Require an Admin Login for the reason behind each one.
See Authentication for details.
Request Format
- Content-Type:
application/json - All request bodies are JSON
- Dates use ISO 8601 format:
2026-03-23T10:30:00Z
Response Format
Endpoints return the resource directly as JSON — there is no wrapping envelope. For example, GET /customers/{id} returns a Customer object at the top level; list endpoints return a list-response object (PaymentListResponse, CustomerListResponse, etc.) shaped per the API ref for that resource.
Error Responses
A failed request answers with one envelope:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "An item with ID 'GPT-4-TURBO' already exists. Please use a different Item ID.",
"details": {}
}
}
message is written for a person and details carries whatever else is useful for that
particular refusal. code is symbolic, but do not branch on it yet: one status can still arrive
under two codes depending on which layer produced it (a 502 as either BILLING_BACKEND_ERROR
or BAD_GATEWAY, for instance). Branch on the status.
A rejected request body fills details.errors with one entry per field, so you can tell the
caller exactly what to change:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Input data validation error",
"details": {
"errors": [
{"loc": ["body", "amount"], "msg": "Input should be greater than 0", "type": "greater_than"}
],
"path": "/api/v1/customers/42/adjust-balance"
}
}
}
The charging endpoints answer a refusal in their own shape: see Charging.
HTTP status codes:
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | The request is malformed, or breaks a rule this API checks itself |
| 401 | Not signed in, or the session expired |
| 403 | The credential is not accepted on this endpoint |
| 404 | No such resource |
| 409 | A genuine conflict: the request is valid but cannot be applied as it stands |
| 422 | Your input is not acceptable. Fix the request and send it again |
| 429 | Too many requests, retry later |
| 500 | A fault on our side |
| 502 | The billing backend could not serve the request. Safe to retry |
The numeric id of a plan, service, or invoice in the path answers 422 when it cannot be an id at
all (not digits, zero, a leading zero, or more than 18 digits), and the message names the parameter
and the value you sent. A well-formed id with no such record answers 404. A customer segment also
accepts your external_id, so only a number no customer can have answers 422 there.
Some endpoints still answer 400 for a rule they check themselves, so treat 400 and 422
alike: both mean the request needs changing, and read the message for what to change.
Pagination
List endpoints support pagination:
GET /api/v1/customers?limit=20&offset=0
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Items per page |
offset | integer | 0 | Number of items to skip |
Every list response uses the same envelope: the rows under an entity-named key (e.g. customers, invoices), plus total (rows matching the query), limit/offset (the page you requested), and has_more (whether more rows exist beyond this page).
Money & Currency
All monetary amounts are plain decimal numbers in major units (e.g. dollars, not cents) and are expressed in your account's base currency. PortaAIM uses a single currency per account, so amounts do not carry a per-field currency code — retrieve your base currency from GET /api/v1/tenant (currency) or GET /api/v1/currencies (default_currency).
Dates & Times
Timestamps are ISO-8601 in UTC (e.g. 2026-03-23T10:30:00Z) and use the date-time format. Calendar dates (e.g. invoice issue_date/due_date, trend points) are YYYY-MM-DD with the date format.
API Endpoints Summary
| Resource | Endpoint | Description |
|---|---|---|
| Customers | /customers/* | Customer CRUD and billing |
| Services | /services/* | Service management |
| Items | /items/* | Item management |
| Products | /products/* | Plan management |
| Charging | /charging/* | Usage event submission |
| Events | /events/* | Event queries |
| Invoices | /invoices/* | Invoice management |
| Invoice Template | /templates/invoice* | The template invoices are rendered with |
| Payments | /payments/* | Payment queries |
| API Keys | /api-keys/* | Key management — creating and revoking need an admin login (JWT), not an API key |
| Tenants | /tenants/* | Tenant configuration |
| Analytics | /analytics/* | Business metrics |
| Reference Data | /reference/*, /profile/timezones* | Countries, regions and timezones a record can name |