Platform Open API
Platform Open API is designed for third-party server integrations. Use a platform key to manage users, projects, and balances. Use each project's token_key for model inference.
1. Overview
| Item | Value |
|---|---|
| API Base URL | https://api.letai.run/api/platform/open |
| Authentication | Authorization: Bearer <platform-key> |
| Request format | Content-Type: application/json |
| Time format | Unix timestamp in seconds |
| Business success | Response body has success === true |
The backend also accepts a raw platform key in Authorization, but the standard Bearer format is recommended. A request fails when the key, its credential, or its platform client is disabled, or when the key has expired.
A platform key is only valid for the management APIs on this page. It cannot call /v1/* inference APIs.
2. Common Responses
Success:
{
"success": true,
"message": "",
"data": {}
}
Failure:
{
"success": false,
"message": "error message"
}
The current implementation commonly returns HTTP 200 for both business success and business failure. Always inspect success; do not rely on the HTTP status alone.
Paginated Responses
Log and top-up lists return:
{
"success": true,
"message": "",
"data": {
"page": 1,
"page_size": 20,
"total": 1,
"items": []
}
}
| Query parameter | Required | Description |
|---|---|---|
p | No | Page number; start at 1 |
page_size | No | Items per page, up to 100; aliases ps and size are accepted |
3. Data Model And Quota
- A third-party platform maps to one platform client and one or more platform keys.
- A platform can create many platform users. Their API identifier is
platform_user_id. - A platform user can own many projects. Their API identifier is
platform_project_id. - Each project is bound to an isolated token. Only project upsert returns the complete
token_key. - Project tokens never expire, have unlimited token-level quota, and have no model allowlist by default.
- Inference is still limited by the owning user's balance, exposed as
user_quota_remaining.
Quota fields are integers. LetAI's current default conversion is:
1 USD = 500000 quota
Administrators can change QuotaPerUnit. The Open API does not derive quota from amount or currency; integrations must use the platform's current configuration and send raw quota explicitly.
4. Status And Log Types
Status Values
| Field | Value | Meaning |
|---|---|---|
User/project mapping status | 1 | Enabled |
User/project mapping status | 2 | Disabled |
user_status | 1 | Enabled |
user_status |
Disabling a platform user also disables the underlying local user, which blocks all project tokens owned by that user. Disabling a project only disables that project's token.
Log Types
type | Meaning |
|---|---|
0 or omitted | All |
1 | Top-up |
2 | Usage |
3 | Management |
4 | System |
5. Endpoint Summary
| Method | Path | Description |
|---|---|---|
POST | /users/upsert | Create or get a platform user |
POST | /users/:platform_user_id/disable | Disable a platform user |
POST | /users/:platform_user_id/enable | Enable a platform user |
All paths below are relative to /api/platform/open.
6. User APIs
6.1 Create Or Get A Platform User
POST /users/upsert
Authorization: Bearer <platform-key>
Content-Type: application/json
{
"external_user_id": "u_10001",
"external_user_name": "Alice",
"email": "alice@example.com",
"metadata": {
"channel": "partner-a"
}
}
| Body field | Type | Required | Description |
|---|---|---|---|
external_user_id | string | No | Third-party user ID; an idempotency key when non-empty |
external_user_name | string | No | Third-party display name |
email | string | No | Email address, up to 50 characters |
When the same non-empty external_user_id already exists under the platform, the API returns that user and updates a non-empty external_user_name, a non-empty email, and metadata when provided. Without external_user_id, every call creates a new user.
Key response fields:
{
"platform_user_id": 101,
"external_user_id": "u_10001",
"external_user_name": "Alice",
"local_user_id": 9001,
"group": "default",
"status": 1,
"user_status": 1,
"project_count": 0,
"quota_total": 0,
"quota_used": 0,
"quota_remaining": 0,
"request_count": 0,
"email": "alice@example.com",
"metadata": "{\"channel\":\"partner-a\"}"
}
metadata is a serialized JSON string in responses. It may be omitted when empty.
6.2 Disable Or Enable A Platform User
POST /users/:platform_user_id/disable
POST /users/:platform_user_id/enable
Authorization: Bearer <platform-key>
Both endpoints have no request body and return the latest user summary. Repeating the same state change is idempotent.
6.3 Get User Summary
GET /users/:platform_user_id/summary
Authorization: Bearer <platform-key>
The response uses the same schema as user upsert. Quota semantics:
| Field | Description |
|---|---|
quota_total | Current remaining quota plus cumulative used quota |
quota_used | Cumulative user usage |
quota_remaining | Current real user balance |
7. Project APIs
7.1 Create Or Get A Project And Token
POST /users/:platform_user_id/projects/upsert
Authorization: Bearer <platform-key>
Content-Type: application/json
{
"external_project_id": "p_10001",
"external_project_name": "Support Bot",
"metadata": {
"environment": "production"
}
}
| Body field | Type | Required | Description |
|---|---|---|---|
external_project_id | string | No | Third-party project ID; idempotent within one platform user |
external_project_name | string | No | Third-party project name, also used as the token name |
metadata | object | No | Custom metadata |
Without external_project_id, every call creates a new project. A project cannot be created while either the platform user or underlying local user is disabled.
Successful data response:
{
"platform_project_id": 201,
"platform_user_id": 101,
"external_project_id": "p_10001",
"external_project_name": "Support Bot",
"local_user_id": 9001,
"token_id": 3001,
"token_name": "Support Bot",
"token_key": "<project-token-key>",
"group": "default",
"status": 1,
"token_status": 1,
"quota_limited_by": "user",
"quota_used": 0,
"user_quota_remaining": 5000000,
"unlimited_quota": true,
"expires_at": -1,
"metadata": "{\"environment\":\"production\"}"
}
The complete token_key is only returned by project upsert. Store it securely on your server. Project summary, enable, and disable responses do not reveal it again.
7.2 Disable Or Enable A Project
POST /projects/:platform_project_id/disable
POST /projects/:platform_project_id/enable
Authorization: Bearer <platform-key>
Both endpoints have no request body and return the latest project summary without token_key. Enabling a project does not restore inference while its owning user remains disabled.
7.3 Get Project Summary
GET /projects/:platform_project_id/summary
Authorization: Bearer <platform-key>
The response is a project summary without token_key. Current project tokens are user-balance limited, so:
quota_limited_byisuserunlimited_quotaistruequota_usedis cumulative usage for this tokenuser_quota_remainingis the owning user's current real balanceexpires_atis-1, meaning the token never expires
8. Balance APIs
All balance APIs require both the platform user and underlying local user to be enabled. amount, refund_amount, and currency are audit fields and do not affect quota calculations.
8.1 Top Up A User
POST /users/:platform_user_id/topups
Authorization: Bearer <platform-key>
Content-Type: application/json
{
"external_order_id": "ord_20260727_0001",
"quota": 5000000,
"amount": "10.00",
"currency": "USD",
"remark": "plan top-up",
"metadata": {
"plan": "pro"
}
}
| Body field | Type | Required | Constraint |
|---|---|---|---|
external_order_id | string | Yes | Up to 128 characters; idempotent within the platform |
quota | integer | Yes | Must be greater than 0 |
amount | string | No | Up to 64 characters; audit only |
A repeated external_order_id returns the original record only when user, quota, amount, and currency match the original request. Otherwise, it returns an idempotency conflict error.
8.2 Refund Or Reverse A Top-up
POST /users/:platform_user_id/topups/refund
Authorization: Bearer <platform-key>
Content-Type: application/json
{
"external_refund_id": "refund_20260727_0001",
"external_order_id": "ord_20260727_0001",
"refund_quota": 1000000,
"refund_amount": "2.00",
"currency": "USD",
"remark": "partial refund"
}
| Body field | Type | Required | Constraint |
|---|---|---|---|
external_refund_id | string | Yes | Up to 128 characters; idempotent within the platform |
external_order_id | string | Yes | Up to 128 characters; must reference this user's top-up |
refund_quota | integer | Yes | Must be greater than 0 |
Cumulative refunds cannot exceed the original top-up quota, and the user's current balance must be at least the requested refund_quota. The response includes refunded_quota_total and refundable_quota_remaining.
8.3 Reset User Balance
POST /users/:platform_user_id/balance/reset
Authorization: Bearer <platform-key>
Content-Type: application/json
{
"external_reset_id": "reset_20260727_0001",
"target_quota": 2500000,
"remark": "synchronize subscription balance"
}
external_reset_id is required and can be up to 128 characters. target_quota is required and must be greater than or equal to 0. The response includes previous_quota, target_quota, and quota_delta.
8.4 Adjust User Balance
POST /users/:platform_user_id/balance/adjust
Authorization: Bearer <platform-key>
Content-Type: application/json
{
"external_adjust_id": "adjust_20260727_0001",
"delta_quota": -500000,
"remark": "manual deduction"
}
external_adjust_id is required and can be up to 128 characters. delta_quota cannot be 0. Positive values add balance; negative values deduct it. The resulting balance is clamped to 0 and never becomes negative.
requested_delta_quota is the requested change, while applied_delta_quota is the actual change. For example, a -500 request against a balance of 100 applies -100.
8.5 List Top-up Records
GET /users/:platform_user_id/topups?p=1&page_size=20
Authorization: Bearer <platform-key>
This endpoint supports common pagination only; it cannot filter by order ID. Each item contains the order, quota, audit amount, creation time, and a user quota summary calculated at query time.
9. Log Queries
GET /users/:platform_user_id/logs?p=1&page_size=20&type=2
GET /projects/:platform_project_id/logs?p=1&page_size=20&type=2
Authorization: Bearer <platform-key>
| Query parameter | Description |
|---|---|
type | Log type; 0 or omitted means all |
start_timestamp | Inclusive start Unix timestamp |
end_timestamp | Inclusive end Unix timestamp |
group | Exact group match |
model_name | Contains match for user logs; exact match for project logs |
Logs are ordered newest first. Returned id is a display sequence within the paginated result, not a stable database log ID. For security, channel_name is cleared and administrator details and rejection reasons are removed from other.
10. Call Inference With A Project Token
curl --request POST \
--url https://api.letai.run/v1/chat/completions \
--header 'Authorization: Bearer <project-token-key>' \
--header 'Content-Type: application/json' \
--data '{
"model": "gpt-5.4",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
Inference uses the project's token_key, not the platform key. Requests fail when the project or owning user is disabled, or when the user balance is insufficient.
11. Idempotency And Integration Guidance
| Operation | Idempotency key | Requirement |
|---|---|---|
| User upsert | external_user_id | Optional; recommended in production |
| Project upsert | external_project_id | Optional; recommended in production |
| Top-up | external_order_id | Required |
| Refund/reversal | external_refund_id | Required |
- Persist every
external_*ID and reuse the original request parameters when retrying. - Record
request_id, platform business order IDs, and responsemessagefor reconciliation and troubleshooting. - Store platform keys and project tokens separately with least privilege.
- After a balance request timeout, retry with the same idempotency key instead of generating a new order ID.
- Parse response
metadataas a JSON string. Empty fields may be omitted because ofomitempty.