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 id you got at submit time into ref

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.

EndpointPurpose
GET /v1/logsPer-call rows, filtered and paginated
GET /v1/logs/{id}Technical view of one row (request params / upstream echo / gateway)
GET /v1/logs/summaryAggregates over a window (per day / per model / four terminal states)

Base URL https://qianyi.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://qianyi.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:

qianduan.dflop.top → API Keys → open a key → tick “Let this key read call logs for the whole account”

Then:

curl "https://qianyi.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://qianyi.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.

Keyscope=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#

ParameterDefaultNotes
scopekeykey = this key only; account = whole account (needs the toggle)
key_id—scope=account only: narrow to one key
period30dtoday / 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)
orderdescdesc newest first; asc oldest first (for incremental pulls)
limit501–200
cursor—The next_cursor from the previous page, passed back verbatim
includeattemptsComma-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 January
  • to=2026-01-03T12:00:00Z (timestamp form) → excludes that instant (created_at < to)

Use the timestamp form for incremental sync: pass the previous to as the next from and you get neither gaps nor duplicates.

An empty filter value means “don't filter”, not “match nothing”. ?status= behaves exactly like omitting status. 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_count is how many). ⚠️ attempts_count is null when it could not be determined this round — either you didn't ask for attempts, 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 a cursor); 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://qianyi.dflop.top/v1/logs?period=7d&limit=100" -H "Authorization: Bearer $KEY"
# Follow next_cursor until has_more is false
curl "https://qianyi.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://qianyi.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:

  1. Deduplicate on id. Under concurrency a row's insertion order can differ very slightly from its created_at (normal database-sequence behaviour), so this is not a strict change feed. Rewind the watermark by ~5 minutes to overlap and dedupe on id. id is globally unique and never reused.
  2. 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's submitted_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://qianyi.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://qianyi.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 / ref returns 400 — this aggregates the whole window, and silently ignoring those would hand you the account's totals under a ?model=X query and let you file them under X. For a per-model breakdown read by_model[] in the response; for row-level filtering use /v1/logs.
  • The window is capped at 365 days and period=all is 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#

Default60 requests per minute per key (shared across all three endpoints)
Relationship to inference quotaCompletely separate. Hammering the logs API will not make your production calls hit 429
Response headersx-ratelimit-limit / x-ratelimit-remaining / x-ratelimit-reset (seconds)
Over the limit429 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#

StatuscodeMeaning
400invalid_requestBad parameters (misspelled status, half-specified range, cursor/order mismatch, unknown include token, row-level filters passed to /summary)
401invalid_api_keyKey missing, revoked, expired, or the account is not active
403permission_deniedscope=account without the toggle, or an agent key calling this API
404log_not_foundNo such id, or it is outside this key's visibility
429logs_rate_limitedOver 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 / webThis APIWhy
cost_usdcostBoth 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.