Export Usage Details
LMU AI Usage export guide: log in for a JWT, then call /api/v1/usage with pagination, date range and model filtering, with examples in three languages.
Log in with your account and password to get a JWT, then use that JWT to call /api/v1/usage and pull the full token usage details table, identical to the console /usage page.
Ideal for long-running scripts that automate export, reconciliation, and loading into enterprise BI — no need to manually download a CSV each time.
Why can't you pull it directly with an sk-... API key? The key is only for gateway business calls (/v1/messages, etc.) and does not expose account-level detail queries — this is a deliberate security design: if a key leaks, you don't want your entire bill dumped. Account-level data must use account authentication (JWT).
1. API overview
| Item | Value |
|---|---|
| Base URL | https://api.lmuai.ai |
| Authentication | Authorization: Bearer <access_token> (the JWT from account login) |
| Access token validity | 86400 seconds (24 hours), renewable with refresh_token |
| Response format | JSON; code: 0 means success, on failure code != 0 with a message field |
| Recommended use | Automated billing export, loading into enterprise BI, reconciliation scripts |
2. Step 1 — Log in to get a JWT
POST /api/v1/auth/login
POST /api/v1/auth/login HTTP/1.1
Content-Type: application/json
{
"email": "[email protected]",
"password": "your-password"
}Response (excerpt):
{
"code": 0,
"message": "success",
"data": {
"access_token": "eyJhbGc...",
"refresh_token": "rt_756a220ffa...",
"expires_in": 86400,
"token_type": "Bearer",
"user": { "id": 3, "email": "...", "balance": 1575.13 }
}
}All subsequent requests just include Authorization: Bearer <access_token>.
3. Step 2 — Pull the usage details
GET /api/v1/usage
GET /api/v1/usage?page=1&page_size=200&start_date=2026-06-01&end_date=2026-06-23&timezone=Asia/Shanghai
Authorization: Bearer <access_token>Query parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page | int | No | 1 | Page number, starting from 1 |
page_size | int | No | 20 | Rows per page, 200 recommended (oversized values are truncated) |
start_date | string | No | — | Start date YYYY-MM-DD, parsed by timezone |
end_date | string | No | — | End date (inclusive of that day) |
timezone | string | No | UTC | Timezone for parsing the date parameters, e.g. Asia/Shanghai; strongly recommended, otherwise the boundaries won't match your local time |
api_key_id | int | No | — | View only one key's details (must belong to the current user) |
model | string | No | — | Filter by model ID (e.g. claude-sonnet-5) |
stream | bool | No | — | true for streaming only, false for non-streaming only |
billing_type | int | No | — | Billing type enum (see below) |
sort_by | string | No | created_at | Sort field |
sort_order | string | No | desc | asc or desc |
Response structure
{
"code": 0,
"message": "success",
"data": {
"items": [ { /* UsageLog */ }, ... ],
"total": 30423,
"page": 1,
"page_size": 200,
"pages": 153
}
}The pagination fields are at the top level of data, not in the outer envelope. The details array is at data.items — data is not itself an array.
Field mapping for a single record
These correspond exactly to each column of the console /usage page:
| Console column | JSON field | Type | Description |
|---|---|---|---|
| Model | model | string | The model ID actually billed |
| Reasoning effort | reasoning_effort | string | null | low / medium / high / xhigh / max |
| Inbound endpoint | inbound_endpoint | string | null | The path the client called, e.g. /v1/messages |
| Upstream endpoint | upstream_endpoint | string | null | The normalized upstream path |
| Request type | request_type | string | chat / stream / responses / messages, etc. |
| Streaming | stream | bool | Whether it was streaming |
| Billing mode | billing_mode | string | token / per_request / image / subscription |
| Billing type | billing_type | int | Billing type enum |
| Multiplier | rate_multiplier | float | The channel multiplier applied to this request |
| Input tokens | input_tokens | int | |
| Output tokens | output_tokens | int | |
| Cache creation | cache_creation_tokens | int | All cache writes |
| Cache creation 5m | cache_creation_5m_tokens | int | Anthropic ephemeral 5-minute tier |
| Cache creation 1h | cache_creation_1h_tokens | int | Anthropic 1-hour tier |
| Cache read | cache_read_tokens | int | |
| Input cost | input_cost | float | USD |
| Output cost | output_cost | float | USD |
| Cache creation cost | cache_creation_cost | float | USD |
| Cache read cost | cache_read_cost | float | USD |
| List-price total | total_cost | float | USD, before the multiplier |
| Actual charge | actual_cost | float | = total_cost × rate_multiplier, what is actually deducted from your balance |
| First-token latency | first_token_ms | int | null | TTFT, valid for streaming |
| Total duration | duration_ms | int | null | |
| Time | created_at | ISO8601 | Server-side write time |
| Request ID | request_id | string | For troubleshooting and tracing |
| Image generation | image_count / image_size / image_output_tokens / image_output_cost | — | Populated only for multimodal requests |
| API Key | api_key_id + nested api_key object | int + obj | |
| Group | group_id + nested group object | int + obj |
Each record carries nested user / api_key / group objects (about +1KB per row). The api_key object contains the full key text — be sure to redact it in script logs / screenshots / CSVs; for bulk export, if you don't need the nested objects, use jq or field mapping on the client to keep only the fields you need.
Billing type enum (billing_type)
| Value | Meaning |
|---|---|
0 | Billed by token (the vast majority of requests) |
1 | Deducted from a subscription's quota |
The billing_mode field is the string version, describing the specific pricing model (token / per_request / image / subscription, etc.); it is more readable and recommended for categorization.
4. Token renewal
The access token expires after 24 hours. There are two ways to renew it:
Renew with refresh_token (recommended for scripts)
POST /api/v1/auth/refresh
Content-Type: application/json
{
"refresh_token": "rt_756a220ffa..."
}Returns a new access_token (and usually rotates the refresh_token too). The refresh token is valid for 30 days.
Log in again
Crude but simple, good for one-off scripts: call /auth/login again before each run.
5. Code examples
Bash + curl + jq
#!/usr/bin/env bash
set -e
EMAIL="${EMAIL:?set EMAIL env}"
PASSWORD="${PASSWORD:?set PASSWORD env}"
BASE="https://api.lmuai.ai"
JWT=$(curl -s -X POST "$BASE/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" \
| jq -r '.data.access_token')
[ -n "$JWT" ] && [ "$JWT" != "null" ] || { echo "login failed" >&2; exit 1; }
START="2026-06-01"; END="2026-06-30"; TZ="Asia/Shanghai"; PAGE_SIZE=200
# Fetch page 1 to get total
META=$(curl -s "$BASE/api/v1/usage?page=1&page_size=$PAGE_SIZE&start_date=$START&end_date=$END&timezone=$TZ" \
-H "Authorization: Bearer $JWT")
PAGES=$(echo "$META" | jq -r '.data.pages')
# CSV header
echo "created_at,model,request_type,input_tokens,output_tokens,cache_read_tokens,total_cost,actual_cost,duration_ms" > usage.csv
# Write page 1
echo "$META" | jq -r '.data.items[] | [.created_at, .model, .request_type, .input_tokens, .output_tokens, .cache_read_tokens, .total_cost, .actual_cost, .duration_ms] | @csv' >> usage.csv
# Fetch remaining pages
for ((p=2; p<=PAGES; p++)); do
curl -s "$BASE/api/v1/usage?page=$p&page_size=$PAGE_SIZE&start_date=$START&end_date=$END&timezone=$TZ" \
-H "Authorization: Bearer $JWT" \
| jq -r '.data.items[] | [.created_at, .model, .request_type, .input_tokens, .output_tokens, .cache_read_tokens, .total_cost, .actual_cost, .duration_ms] | @csv' >> usage.csv
done
echo "saved $(wc -l < usage.csv) lines to usage.csv"Python
import os, csv, requests
BASE = "https://api.lmuai.ai"
EMAIL = os.environ["EMAIL"]
PASSWORD = os.environ["PASSWORD"]
def login() -> str:
r = requests.post(f"{BASE}/api/v1/auth/login",
json={"email": EMAIL, "password": PASSWORD},
timeout=15)
r.raise_for_status()
data = r.json()
if data["code"] != 0:
raise RuntimeError(data.get("message", "login failed"))
return data["data"]["access_token"]
def fetch_usage(jwt: str, **params):
"""Yield each row from paginated /usage endpoint."""
page = 1
while True:
r = requests.get(f"{BASE}/api/v1/usage",
headers={"Authorization": f"Bearer {jwt}"},
params={**params, "page": page, "page_size": 200},
timeout=30)
r.raise_for_status()
d = r.json()["data"]
for row in d.get("items", []):
yield row
if page >= d.get("pages", 1):
return
page += 1
if __name__ == "__main__":
jwt = login()
cols = ["created_at","model","request_type","input_tokens","output_tokens",
"cache_read_tokens","total_cost","actual_cost","duration_ms"]
with open("usage.csv", "w", newline="") as f:
w = csv.writer(f); w.writerow(cols)
for row in fetch_usage(jwt, start_date="2026-06-01", end_date="2026-06-30",
timezone="Asia/Shanghai"):
w.writerow([row.get(c) for c in cols])
print("done")Node.js
import fs from 'node:fs'
const BASE = 'https://api.lmuai.ai'
const { EMAIL, PASSWORD } = process.env
async function login() {
const r = await fetch(`${BASE}/api/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
})
const j = await r.json()
if (j.code !== 0) throw new Error(j.message || 'login failed')
return j.data.access_token
}
async function* fetchUsage(jwt, params) {
for (let page = 1; ; page++) {
const q = new URLSearchParams({ ...params, page, page_size: '200' })
const r = await fetch(`${BASE}/api/v1/usage?${q}`, {
headers: { Authorization: `Bearer ${jwt}` },
})
const j = await r.json()
for (const row of j.data.items) yield row
if (page >= j.data.pages) return
}
}
const jwt = await login()
const cols = ['created_at','model','request_type','input_tokens','output_tokens',
'cache_read_tokens','total_cost','actual_cost','duration_ms']
const out = fs.createWriteStream('usage.csv')
out.write(cols.join(',') + '\n')
for await (const row of fetchUsage(jwt, {
start_date: '2026-06-01', end_date: '2026-06-30', timezone: 'Asia/Shanghai',
})) {
out.write(cols.map(c => JSON.stringify(row[c] ?? '')).join(',') + '\n')
}
console.log('done')6. Practical notes
- Add a delay between bulk page turns — when paging through N pages in a row,
sleep(0.1~0.3s)is recommended to avoid triggering rate limits. - Don't pull too long a time range at once — spanning several months and millions of rows is slow. Slicing by month is more stable.
page_sizeof 200 recommended — the server truncates oversized values.- Save bandwidth — the nested
user/api_key/groupobjects are large; if you only want the detail rows, filter withjqon the client. - Common errors:
401JWT expired → renew withrefresh_tokenor log in again403Unauthorized access (e.g. querying someone else's key viaapi_key_id) → check that the key belongs to the current user400Bad parameter (e.g.start_dateformat) → check thatYYYY-MM-DD+timezoneare valid
7. Security recommendations (must read)
Your account password + JWT are equivalent to full account access. Before scripting an export, make sure:
| Item | Recommendation |
|---|---|
| Credential management | Never commit password / access_token / refresh_token to git or CI logs; use environment variables, a secrets manager, or .env (with .gitignore) |
| Account security | Strongly recommend enabling TOTP two-factor authentication (enable it in account settings). This API supports TOTP verification during login |
| Regular rotation | Change your account password regularly; rotate the refresh_token immediately after it expires |
| Anomaly monitoring | Watch the account's "last login time / IP"; change your password immediately if you spot an unfamiliar device |
| Field redaction | The nested api_key.key (full sk-... text), user.email, and refresh_token in the response are all sensitive fields — redact them before output / logs / CSV |
| Least privilege | If you can separate them, use a dedicated sub-account for the export script (if your organization has a multi-account structure) to avoid long-term exposure of the main account |
| Error retries | Don't retry indefinitely on login failure — it easily triggers risk controls; exponential backoff is recommended |
8. Field quick reference
Handy for CSV column mapping:
| Label | Field | Unit |
|---|---|---|
| Model | model | — |
| Reasoning effort | reasoning_effort | — |
| Inbound endpoint | inbound_endpoint | path |
| Upstream endpoint | upstream_endpoint | path |
| Request type | request_type | enum |
| Streaming | stream | bool |
| Billing mode | billing_mode | enum |
| Billing type | billing_type | int |
| Multiplier | rate_multiplier | float |
| Input tokens | input_tokens | int |
| Output tokens | output_tokens | int |
| Cache read tokens | cache_read_tokens | int |
| Cache write tokens | cache_creation_tokens | int |
| Cache write 5m | cache_creation_5m_tokens | int |
| Cache write 1h | cache_creation_1h_tokens | int |
| Input cost | input_cost | USD |
| Output cost | output_cost | USD |
| Cache creation cost | cache_creation_cost | USD |
| Cache read cost | cache_read_cost | USD |
| List-price total | total_cost | USD |
| Actual charge | actual_cost | USD |
| First token | first_token_ms | ms |
| Total duration | duration_ms | ms |
| Time | created_at | ISO8601 |
| Request ID | request_id | string |
| Group | group_id / group.name | int / string |
| API Key | api_key_id / api_key.name | int / string |
Last updated:
Still have questions? Ask in the LMU AI Community (in Chinese) and sign in with your console account.
Get an LMU AI key and start using Claude, Codex and more
Free sign-up, flexible plans, one key across Claude Code, Codex CLI, Cursor, VS Code, OpenCode, Cherry Studio and other AI tools.
Sign up