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.com |
| 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": "your@email.com",
"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.com"
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.com"
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.com'
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:
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 upGemini Batch Image API
LMU AI async batch image API: submit many Gemini image tasks at once, poll status and details, download images or ZIP, with idempotency and cost estimates.
Enterprise
LMU AI enterprise Claude / GPT / Gemini API plans: 6 SKUs (Enterprise Standard + Flagship) on Anthropic and OpenAI, with contracts, invoices, and support.