Call Logs API
Pull per-call records with the same sk-gpushare-* key — status, failure reason, async task outcome, credits actually charged, and an incremental-sync recipe
The per-call ledger behind logs.dflop.top is now readable with the same API key you already use for inference. Typical uses:
- Sync the call stream into your own system for reconciliation, reporting or alerting
- Find out why one call failed (error code, redacted error text, how many times the gateway re-routed internally)
- Look up the outcome of an async job (video / music / speech / image): paste the
idyou got at submit time intoref
Read-only, and it never costs credits. A key with a zero balance can still read its logs — which is exactly when you most need to.
| Endpoint | Purpose |
|---|---|
GET /v1/logs | Per-call rows, filtered and paginated |
GET /v1/logs/{id} | Technical view of one row (request params / upstream echo / gateway) |
GET /v1/logs/summary | Aggregates over a window (per day / per model / four terminal states) |
Base URL https://api.dflop.top. Auth is the same as the chat endpoints (Authorization: Bearer sk-gpushare-… or x-api-key) — see Authentication.
Visibility (read this first)#
By default a key only sees the calls it made itself. That is deliberate: a key you handed out should not be able to enumerate the traffic of sibling keys on the same account.
# Default: only this key's records
curl "https://api.dflop.top/v1/logs?limit=5" \
-H "Authorization: Bearer $PLATFORM_API_KEY"
To pull every key on the account in one request, the account owner has to turn it on per key in the console:
model.dflop.top → API Keys → open a key → tick “Let this key read call logs for the whole account”
Then:
curl "https://api.dflop.top/v1/logs?scope=account&limit=5" \
-H "Authorization: Bearer $PLATFORM_API_KEY"
# Within account scope you can still narrow to one key
curl "https://api.dflop.top/v1/logs?scope=account&key_id=<uuid>" \
-H "Authorization: Bearer $PLATFORM_API_KEY"
Passing scope=account without the toggle returns 403 permission_denied.
| Key | scope=key (default) | scope=account |
|---|---|---|
| Key you created in the console | ✅ | needs the toggle |
| Enterprise-issued employee key | ✅ | ❌ never |
| Compute-package / desktop credential | ✅ | ❌ never |
Agent key (scoped to /v1/agent/*) | ❌ 403 | ❌ 403 |
GET /v1/logs#
Query parameters#
| Parameter | Default | Notes |
|---|---|---|
scope | key | key = this key only; account = whole account (needs the toggle) |
key_id | — | scope=account only: narrow to one key |
period | 30d | today / 7d / 30d / this_month / all. Ignored when from+to are given |
from / to | — | Must be given together. Either YYYY-MM-DD or an RFC3339 timestamp; the two forms may be mixed |
model | — | Exact model id, comma-separated for multi-select: model=gpt-5.5,glm-4.7 |
status | — | Comma-separated: success / error / interrupted / rejected |
unit_type | — | Comma-separated: image / video / music / audio / avatar / voice / search / transcript, plus chat (token-billed conversation rows) |
error_code | — | Exact error code, comma-separated. Codes are listed in Errors |
ref | — | Exact lookup by task ID or request ID (both columns are tried — you don't have to know which kind you're holding) |
order | desc | desc newest first; asc oldest first (for incremental pulls) |
limit | 50 | 1–200 |
cursor | — | The next_cursor from the previous page, passed back verbatim |
include | attempts | Comma-separated: attempts (re-route detail) / in_flight (unfinished async jobs) / none |
The upper bound's meaning depends on its form — this one matters:
to=2026-01-03(date form) → includes all of 3 Januaryto=2026-01-03T12:00:00Z(timestamp form) → excludes that instant (created_at < to)Use the timestamp form for incremental sync: pass the previous
toas the nextfromand you get neither gaps nor duplicates.
An empty filter value means “don't filter”, not “match nothing”.
?status=behaves exactly like omittingstatus. A misspelled status (e.g.?status=failed) returns 400 rather than silently returning an empty page.
Response#
{
"object": "list",
"currency": "points", // unit of every money field: credits
"scope": "key", // the visibility actually applied
"period_from": "2026-10-08T00:00:00Z",
"period_to": "2026-11-07T03:21:00Z",
"data": [
{
"object": "log",
"id": 90210,
"created_at": "2026-11-07T03:18:42Z", // terminal instant (async: settlement)
"submitted_at": "2026-11-07T03:17:05Z", // when the request reached the gateway
"key_id": "…", "key_name": "prod", "key_prefix": "sk-gpushare-a1b2",
"model": "doubao-seedance-2.0",
"status": "success", // success|error|interrupted|rejected
"error_code": null,
"error_message": null,
"input_tokens": 0, "output_tokens": 0, "cached_tokens": 0,
"unit_count": 5, "unit_type": "video", // per-unit billed row; null = token-billed
"cost": "182.40", // credits charged, a JSON **string**
"latency_ms": 97210,
"request_id": "3f9c…", // = the x-gateway-trace response header
"task_id": "9b1e…", // the id returned by the async submit call
"result_urls": ["https://…"],
"result_expires_at": "2026-11-08T03:18:42Z",
"attempts_count": 1, // null = not queried this round (see below)
"attempts": [ // present only with include=attempts
{ "seq": 1, "created_at": "…", "latency_ms": 4100,
"error_code": "upstream_error", "error_message": "upstream temporarily unavailable" }
]
}
],
"has_more": true,
"next_cursor": "d:1762..._90210_1759..._1762..."
}
Money is a JSON string ("182.40"), not a number — deliberately, so it never passes through IEEE-754. Parse it as a decimal.
One row = the terminal state of one external call#
- Failed attempts produced by the gateway automatically re-routing between upstream lines are not separate rows, are not billed, and do not count as requests. They are folded into the terminal row's
attempts[](attempts_countis how many). ⚠️attempts_countisnullwhen it could not be determined this round — either you didn't ask forattempts, or that sub-query itself failed. Not 0. Returning 0 would report “we didn't look” as “it definitely never re-routed”, and telling those apart is the whole point of the field. - Four states:
success/error/interrupted(client disconnected or the gateway timed out) /rejected— rejected at submit time (bad parameters, model unavailable, insufficient balance, content policy). Rejected rows always cost 0 but are still recorded, so you can reconcile “sent but never ran”. - An async job only becomes a ledger row once it settles. Jobs still running are returned separately via
include=in_flight(off by default — that query is markedly more expensive than the list itself). ⚠️ It is returned only on the first page (the request without acursor); one copy per pagination run is enough. On later pages the field is absent entirely, which stays distinguishable on the wire from “asked for, and nothing is in flight” (an empty array).
Pagination#
# First page
curl "https://api.dflop.top/v1/logs?period=7d&limit=100" -H "Authorization: Bearer $KEY"
# Follow next_cursor until has_more is false
curl "https://api.dflop.top/v1/logs?cursor=<next_cursor>" -H "Authorization: Bearer $KEY"
The cursor is opaque — pass it back verbatim. It freezes the window resolved on page 1, so a relative period=7d doesn't slide forward mid-run and quietly drop the oldest sliver.
⚠️ The cursor records the sort direction: handing a order=desc cursor to an order=asc request returns 400 rather than silently giving you a misaligned slice.
Incremental sync recipe#
To keep pulling the stream into your own store:
# Watermark = the instant you last synced through (RFC3339)
WATERMARK="2026-11-07T03:00:00Z"
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
curl -sS "https://api.dflop.top/v1/logs?from=${WATERMARK}&to=${NOW}&order=asc&limit=200" \
-H "Authorization: Bearer $PLATFORM_API_KEY"
# After exhausting the pages, advance WATERMARK to NOW
Two things to get right:
- Deduplicate on
id. Under concurrency a row's insertion order can differ very slightly from itscreated_at(normal database-sequence behaviour), so this is not a strict change feed. Rewind the watermark by ~5 minutes to overlap and dedupe onid.idis globally unique and never reused. - Async jobs are booked at their terminal instant (
created_at), not at submit time. A video job that ran for 20 minutes appears in the window covering its settlement. To align on submit time, use the row'ssubmitted_at.
GET /v1/logs/{id}#
The technical view of one row — what resolution, what duration, what the upstream reported back. {id} is the id from the list.
curl "https://api.dflop.top/v1/logs/90210" -H "Authorization: Bearer $KEY"
{
"id": 90210,
"kind": "video", // video|image|music|audio|null (chat / retrieval)
"request": [{ "k": "resolution", "v": "1080p" }, { "k": "duration", "v": "5" }],
"upstream": [{ "k": "framespersecond", "v": "24" }, { "k": "billable_tokens", "v": "…" }],
"gateway": [{ "k": "client_protocol", "v": "openai_chat" }, { "k": "request_bytes", "v": "…" }],
"request_note": null // why params are empty (e.g. the snapshot was cleared at settlement)
}
- Field names are deliberately untranslated — they are the names in the API reference, so you can match them line by line.
- All three groups may be empty arrays: chat turns have no task row, older tasks have no upstream snapshot. Empty ≠ error.
- Visibility matches the list: a key without the account toggle asking for another key's row gets 404.
GET /v1/logs/summary#
Window aggregates, for reconciliation.
curl "https://api.dflop.top/v1/logs/summary?period=this_month" -H "Authorization: Bearer $KEY"
{
"object": "logs.summary",
"currency": "points",
"scope": "key",
"period_from": "…", "period_to": "…",
"total": { "requests": 1840, "cost": "9123.55",
"input_tokens": 8812340, "output_tokens": 412990, "cached_tokens": 6610220 },
"status_counts": { "success": 1802, "error": 21, "interrupted": 4, "rejected": 13 },
"by_day": [{ "date": "2026-11-01T00:00:00Z", "requests": 61, "cost": "302.10", "input_tokens": 0, "output_tokens": 0, "cached_tokens": 0 }],
"by_model": [{ "model": "gpt-5.5", "requests": 1204, "cost": "5120.00", "total_tokens": 7712330 }]
}
Three differences from the list endpoint:
- No row-level filtering. Passing
model/status/unit_type/error_code/refreturns 400 — this aggregates the whole window, and silently ignoring those would hand you the account's totals under a?model=Xquery and let you file them under X. For a per-model breakdown readby_model[]in the response; for row-level filtering use/v1/logs. - The window is capped at 365 days and
period=allis rejected — leaving it uncapped would starve shared resources. Pull longer histories in segments. - Results are cached for 60 seconds (minute-aligned buckets). A just-settled call may take up to a minute to show up here; for real-time reconciliation use
/v1/logs, which has no such cache.
status_counts excludes internal re-route attempts. A success rate is normally success / (success + error + interrupted) — rejected calls never reached an upstream at all.
Rate limits#
| Default | 60 requests per minute per key (shared across all three endpoints) |
| Relationship to inference quota | Completely separate. Hammering the logs API will not make your production calls hit 429 |
| Response headers | x-ratelimit-limit / x-ratelimit-remaining / x-ratelimit-reset (seconds) |
| Over the limit | 429 logs_rate_limited plus Retry-After |
Reconciliation doesn't need high frequency anyway: one call a minute at 200 rows a page keeps up with any realistic volume.
Errors#
| Status | code | Meaning |
|---|---|---|
| 400 | invalid_request | Bad parameters (misspelled status, half-specified range, cursor/order mismatch, unknown include token, row-level filters passed to /summary) |
| 401 | invalid_api_key | Key missing, revoked, expired, or the account is not active |
| 403 | permission_denied | scope=account without the toggle, or an agent key calling this API |
| 404 | log_not_found | No such id, or it is outside this key's visibility |
| 429 | logs_rate_limited | Over the per-minute limit; retry after Retry-After |
Error bodies use the OpenAI shape: {"error": {"message", "type", "code"}}. Full reference in Errors.
Mapping to the web UI / CSV export#
The CSV export on logs.dflop.top is the same data. Only two names differ:
| CSV / web | This API | Why |
|---|---|---|
cost_usd | cost | Both are credits. The CSV column name predates the platform-wide move off dollars; customer scripts read it by name, so it can't be renamed. The new API doesn't inherit it |
logs[] | data[] | Switched to the OpenAI list shape (object + data + has_more) |
Everything else is named and means the same. CSV's attempts_count is this API's field of the same name; result_urls is |-separated in CSV and an array here.
About redaction#
error_message and error_code are redacted before they leave the server: URLs, key material and upstream line names are masked. This does not truncate the diagnosis — the classification (error_code) and the human-readable cause are preserved; only our upstream topology is withheld. When reporting a problem, include request_id as well.