Guides
How to Build an AI Music API Usage Dashboard in Node.js
- Written by
- Sonilo Team
- Published

Build an AI music API usage dashboard by reading account limits first, then a dated usage window, validating both responses, and reconciling the summary against its daily rows. Keep the API key server-side, retry only safe GET reads with a bound, and treat aggregate usage as retrospective account evidence—not a per-job invoice.
Build an AI music API usage dashboard by reading account limits first, then a dated usage window, validating both responses, and reconciling the summary against its daily rows. Keep the API key server-side, retry only safe GET reads with a bound, and treat aggregate usage as retrospective account evidence—not a per-job invoice.
By Sonilo Team · Facts verified August 21, 2026
Disclosure: Sonilo publishes this guide and operates the API used as the concrete example. The `account-usage-snapshot-v1` module was tested with deterministic fixtures. It did not read a customer account, generate media, inspect an invoice, or measure production availability.
Use two account reads for two different questions
Call `GET /v1/account/services` first to learn what the authenticated account can use now. Then call `GET /v1/account/usage?days=30` to retrieve what the account used during a bounded historical window.
The current List Services reference documents enabled services, requests per minute, concurrent generations, the account discount factor, and the upload cap. The current Get Usage reference documents summary totals plus daily rows for requests, billed duration, and cost.
| Read | Question it answers | Do not infer |
|---|---|---|
| `GET /v1/account/services` | Which services and live account limits are visible now? | Historical spend or future availability |
| `GET /v1/account/usage?days=30` | What aggregate requests, billed seconds, and cost appear in this UTC window? | A per-task invoice, current balance, or refund ledger |
The Sonilo OpenAPI contract sets `days` to an integer from 1 through 365, with 30 as the default. Pass the window explicitly so the dashboard label and API request cannot silently disagree.
This is a post-spend observation workflow. For a planned job or campaign, use the separate pre-submit AI music cost estimator before opening a billable generation request.
Keep the API key behind a server boundary
Both account reads require `Authorization: Bearer <SONILO_API_KEY>`. The Sonilo API introduction says to keep keys server-side. A browser should request a narrow view from your own authenticated application route, not receive the provider key or call the provider directly.
The data path should remain explicit:
- An authenticated user requests an account-usage view from your application.
- Your server verifies the user and resolves the correct workspace or tenant.
- Server code reads `SONILO_API_KEY` from its environment or secret store.
- The server calls the two canonical account endpoints.
- It validates and normalizes the provider responses.
- It returns only the fields that this dashboard is allowed to expose.
Do not put the bearer value in a URL, application response, rendered page, analytics property, error string, or cache key. The broader Next.js server-side API boundary covers the browser/server/provider split; the safe AI API logging guide defines a smaller operational-event allowlist.
The sample returns enabled service names and numeric account fields. It does not return the original payload wholesale, because a future response can gain fields that the dashboard was never reviewed to display.
Validate the documented usage shape
The current Get Usage response has two levels:
- `summary.total_requests`, `summary.total_duration_seconds`, and `summary.total_cost`.
- `summary.period_start` as an inclusive UTC boundary and `summary.period_end` as an exclusive UTC boundary.
- `daily[].date`, `daily[].requests`, `daily[].duration_seconds`, and `daily[].cost`.
Validate types before formatting numbers or drawing bars. If `daily[0].cost` arrives as a string when the contract says number, fail the snapshot instead of letting JavaScript concatenate values or plot an unreliable chart.
Use the response's period boundaries as the source of truth for the visible label. “Last 30 days” can be ambiguous at timezone boundaries; an exact UTC start and exclusive end can be audited later.
The module normalizes snake-case provider fields into an application-owned shape. That creates one boundary where an interface change can fail clearly instead of leaking through multiple charts.
Reconcile the summary against daily rows
Do not assume an aggregate and its components match merely because they came in one response. Sum each daily measure, compare it with the corresponding summary total, and surface the delta.
`account-usage-snapshot-v1` calculates three checks:
- `summary.total_requests - sum(daily[].requests)`
- `summary.total_duration_seconds - sum(daily[].duration_seconds)`
- `summary.total_cost - sum(daily[].cost)`
| Check result | Dashboard behavior | Investigation |
|---|---|---|
| All three deltas are zero | Mark this payload internally aligned | Keep the source timestamps and window |
| Request delta only | Warn; do not invent missing rows | Check window boundaries and provider contract |
| Duration or cost delta | Warn and preserve both values | Check rounding, late-arriving rows, or schema change |
| Invalid type or missing field | Reject the snapshot | Do not coerce an unknown contract silently |
The reconciliation is an application invariant, not a claim that every future response must have zero deltas. Its purpose is to make a difference visible. If a mismatch appears, preserve the raw provider response in an access-controlled diagnostic store according to your data policy; do not expose the bearer header with it.
Two derived values are useful when the payload is aligned: total cost divided by total requests, and total billed seconds divided by total requests. Label both as window averages. They do not describe the price or duration of any particular job.
The module also selects the daily row with the highest reported cost. That is a triage pointer, not proof of an incident or waste.
Build a dashboard-safe snapshot
The complete local module uses only Node.js built-ins. Its public result has four parts: a version, the requested window, normalized services and usage, and reconciliation output.
The orchestration is intentionally small:
- `const servicesPayload = await getJson('/account/services', {`
- ` apiKey,`
- ` ...requestOptions,`
- `});`
- `const usagePayload = await getJson(`
- ` `/account/usage?days=${days}`,`
- ` { apiKey, ...requestOptions },`
- `);`
- `const services = normalizeServices(servicesPayload);`
- `const usage = normalizeUsage(usagePayload);`
- ``
- `return {`
- ` version: 'account-usage-snapshot-v1',`
- ` requestedDays: days,`
- ` services,`
- ` usage,`
- ` reconciliation: reconcileUsage(usage),`
- `};`
Read services before usage even when the current screen emphasizes cost. The service response supplies capability context for the same account: a dashboard can distinguish “historical usage is zero” from “the expected service is not enabled.”
Do not turn `discount_factor` into a historical discount claim. It describes the current authenticated account response. Yesterday's usage rows are already reported costs; multiplying them by today's factor again would rewrite history.
Bound timeouts and retries
Account reads are HTTP GET requests. RFC 9110 defines GET as idempotent and defines `Retry-After` as either a delay in seconds or an HTTP date. That permits a narrow retry policy for failed reads, but it does not justify an endless loop.
The tested contract makes at most three attempts for 429, 502, 503, or 504. It respects `Retry-After` when present, otherwise applies a bounded delay. Each fetch also uses a 15-second timeout through the current Node.js v22 fetch and AbortSignal APIs.
- `const retryAfter = parseRetryAfter(`
- ` response.headers.get('retry-after'),`
- ` now(),`
- `);`
- `const delayMs = retryAfter ?? Math.min(`
- ` 1_000 * (2 ** (attempt - 1)),`
- ` 8_000,`
- `);`
- `await sleep(delayMs);`
Do not retry 401. The key is missing, invalid, or revoked, and sleeping cannot repair it. Do not retry 403 as though it were congestion; the key was understood but lacks access. Treat 404 as a host or path check first, because the runtime base is `https://api.sonilo.com/v1`, not the documentation site or your local route.
The current Sonilo errors guide and official account-dashboard example distinguish these failures and direct 429 clients to honor `Retry-After`.
A dashboard poller also needs a schedule outside this request function. Cache a successful snapshot briefly for the authorized tenant and prevent every browser tab from starting an independent provider polling loop. Cache scope and access policy matter more than sub-second freshness for aggregate historical data.
Separate usage from live admission control
Historical usage does not tell a worker whether it may submit now. Before a generation POST, use the account's current RPM and concurrency fields through the generation admission queue.
The sequence for a paid worker is:
- Estimate and approve the planned cost.
- Validate the input and source duration.
- Check current service availability and capacity.
- Claim a durable request identity.
- Submit one generation request.
- Record the accepted task or stream outcome.
- Observe account usage later as a separate accounting signal.
The duplicate-job prevention contract remains necessary because a daily request count cannot identify which ambiguous POST was accepted. Aggregate usage can reveal that total activity changed, but it cannot safely decide which individual job to replay.
If an async request returned a task ID, use the bounded polling workflow for that task. The current API does not send webhooks or callbacks, so a usage dashboard is not a substitute completion channel.
Keep per-job attribution in your own ledger
A useful internal job record should retain the fields that aggregate account usage cannot reconstruct: application request ID, tenant, provider task ID when present, service, intended duration, variants, submission time, terminal state, estimate, output verification, and the accounting window in which you expect usage to appear.
Do not manufacture a per-job cost by dividing a daily total when jobs have different services, durations, variants, or account terms. The result may be a useful aggregate average, but it is not task attribution.
Keep ambiguous submissions visible. A network timeout after POST can leave the application uncertain about provider acceptance. The circuit-breaker guide separates dependency health from replay safety, while the graceful worker shutdown workflow preserves accepted task ownership during replacement.
After a task succeeds, verify and store the media through the temporary-output durability workflow. A usage row does not prove that the expected output file exists, is non-empty, or was copied safely.
The thirteen-test contract
The module ran with the stable Node.js v22 test runner and no external packages. Thirteen deterministic tests passed on Node.js v22.23.2 on August 21, 2026:
- Call services before usage and produce a normalized snapshot.
- Send the bearer value only in request headers and omit it from output.
- Reject a missing key before fetch.
- Reject usage windows outside 1 through 365 days.
- Classify 401, 403, 404, and 429 separately.
- Honor a numeric `Retry-After` before retrying a GET.
- Parse an HTTP-date `Retry-After` value.
- Stop after three retryable failures.
- Avoid retrying a 401.
- Reject a malformed daily cost instead of coercing it.
- Surface summary-versus-daily deltas.
- Select peak daily cost and calculate window ratios.
- Return null ratios and no peak day for an empty window.
These tests prove the local request order, normalization, retry boundary, secret containment, reconciliation math, and failure classifications for controlled fixtures. They do not prove production API availability, customer usage, billing accuracy, a balance, a refund, invoice settlement, or correct tenant authorization in a real application.
When not to use this dashboard
Do not use aggregate account usage as a billing invoice. The response has summary and daily totals, not invoice line items, payment status, taxes, or contract adjustments.
Do not use it as a current balance. The documented usage fields do not include a spendable-balance field.
Do not use it for per-task refund or charge disputes. The public response does not provide a task-level charge or refund ledger.
Do not use it as a generation completion signal. Track the NDJSON stream or the accepted task ID according to the endpoint contract.
Do not use it as your only abuse control. A historical window cannot replace tenant authentication, quotas, current queue admission, budget gates, and alerts.
Do not send the raw snapshot to every signed-in user. Map provider account data to the correct tenant, role, and least-privilege view first.
Ship the smallest trustworthy dashboard
Start with four visible values: the exact UTC window, total requests, billed duration, and reported cost. Add a reconciliation status and current service limits beside them. Keep the provider key server-side and preserve failures as explicit states rather than zeros.
Use the Video-to-Music API guide for the broader integration, the cost estimator before submission, and the current account usage reference as the field authority when you implement this snapshot.


