Guides
How to Log AI Music API Jobs Without Leaking Secrets
- Written by
- Sonilo Team
- Published

Use a fail-closed field allowlist. Keep correlation IDs, operation, route, status, timing, task state, and media shape; replace credentials and customer content with booleans, counts, sizes, and safe identifiers.
Use a fail-closed field allowlist. Keep correlation IDs, operation, route, status, timing, task state, and media shape; replace credentials and customer content with booleans, counts, sizes, and safe identifiers. Never serialize an entire request, response, form, header collection, or provider error into a log.
By Sonilo Team · Facts verified August 16, 2026
Disclosure: Sonilo publishes this guide and operates the API used as the concrete example. The `safe-ai-api-log-v1` sanitizer was tested with synthetic fixtures; it was not tested against production customer traffic and is not a compliance certification.
Start with what the log must answer
A useful AI media job log should let an operator answer five questions without reconstructing the customer's content:
- Which application request and internal job does this event belong to?
- Which provider operation and route ran?
- Did the provider accept, reject, process, succeed, or fail the job?
- How long did the observed step take?
- What safe media shape came back: count, content type, and byte size?
The OWASP Logging Cheat Sheet frames useful event data as `when`, `where`, `who`, and `what`. It also recommends an interaction identifier that joins events from one user action or long-running workflow. For an async music job, keep your own request ID and application job ID from submission through task polling and asset storage.
That correlation contract is more useful than a raw request dump. A dump is noisy, changes whenever the SDK changes, and can preserve credentials or customer material long after the API call ends.
Treat four values as capabilities or customer content
The current Sonilo API reference requires an `Authorization: Bearer ...` header on every request. RFC 6750 defines a bearer token by possession: a party holding the token can use it without proving possession of separate key material. The Sonilo key is not being described here as an OAuth-issued token; the relevant property is its use in the HTTP Bearer scheme. Store only `auth_present: true`, never the value.
Signed media URLs are also capabilities. Amazon S3's Signature Version 4 query-authentication documentation shows authentication fields such as `X-Amz-Credential` and `X-Amz-Signature` inside the URL query. Google Cloud Storage's signed-URL documentation states that anyone possessing an active signed URL can use its granted access. Do not log a signed URL and then try to redact only familiar parameter names. Keep at most a separately parsed scheme and host; drop its path, query, fragment, and user information.
Prompts and uploaded filenames are not credentials, but they can contain client names, unreleased campaign details, personal information, or internal project codes. Record `prompt_present` and `prompt_chars`, not prompt text. Record an upload's content type and size, not its original filename.
Task responses require the same care. The current Get Task response can include temporary result URLs plus content type and file size. Keep the media type and byte size. Do not copy result URLs into logs, error reports, traces, analytics, or support screenshots.
| Input or result | Do not log | Keep instead |
|---|---|---|
| Provider authentication | Bearer header or API key | `auth_present` boolean |
| Remote source | Full signed URL, object path, or query | `source.kind`, scheme, and host |
| Uploaded file | Bytes or original filename | Content type and byte size |
| Prompt | Raw text | Presence and character count |
| Async result | Temporary output URL | Output count, content type, and byte size |
| Error | Raw body, stack, or reflected request | Stable error code and bounded sanitized message |
Use an allowlist instead of recursive redaction
Recursive redaction starts from all available data and tries to remove known dangerous fields. It fails open when a new SDK adds a credential under a new name, an error reflects a URL, a provider nests media objects differently, or a multipart field contains sensitive text.
An allowlist starts from an empty object and copies only fields that have an operational reason to exist. Unknown fields disappear automatically. The difference is important for long-lived logs, backups, exports, alert payloads, and third-party observability systems.
`safe-ai-api-log-v1` emits this fixed contract:
| Group | Allowed fields |
|---|---|
| Identity | Schema version, timestamp, event, request ID, application job ID |
| Request route | Operation, method, endpoint scheme, host, and path without query |
| Request shape | Source kind, source host or file type/size, mode, format, variant count, prompt presence/length |
| Outcome | HTTP status, elapsed milliseconds, provider task ID/status, output count/types/sizes, sanitized error |
Task IDs are identifiers, not credentials in the current API because task reads still require bearer authentication. They can still be sensitive operational data. Keep logs access-controlled and apply a retention policy; hash or replace provider task IDs if your organization's threat model does not require direct lookup.
A tested Node.js sanitizer
The implementation uses only Node.js built-ins. It parses URLs with the Node.js v22 WHATWG URL API, constructs a new object from an allowlist, and serializes exactly one JSON event.
Save the core policy as `safe-ai-api-log.mjs`:
- `function safeUrlParts(value) {`
- ` try {`
- ` const url = new URL(String(value));`
- ` if (!["http:", "https:"].includes(url.protocol)) {`
- ` return {scheme: "unsupported"};`
- ` }`
- ` return {`
- ` scheme: url.protocol.slice(0, -1),`
- ` host: url.hostname,`
- ` route: url.pathname.replace(/\/{2,}/g, "/"),`
- ` query_present: Boolean(url.search),`
- ` };`
- ` } catch { return {scheme: "invalid"}; }`
- `}`
- `function cleanText(value, max = 240) {`
- ` return String(value ?? "")`
- ` .replace(/[\r\n\t]+/g, " ")`
- ` .replace(/\s{2,}/g, " ")`
- ` .slice(0, max);`
- `}`
- `export function buildSafeApiLog(input) {`
- ` const endpoint = safeUrlParts(input.endpoint_url);`
- ` const form = input.form && typeof input.form === "object" ? input.form : {};`
- ` const source = form.video_url`
- ` ? {kind: "url", scheme: safeUrlParts(form.video_url).scheme, host: safeUrlParts(form.video_url).host ?? "invalid"}`
- ` : form.video && typeof form.video === "object"`
- ` ? {kind: "file", content_type: cleanText(form.video.type || "unknown", 100), size_bytes: Number.isSafeInteger(form.video.size) ? form.video.size : null}`
- ` : {kind: "none"};`
- ` return {`
- ` schema: "safe-ai-api-log-v1",`
- ` timestamp: new Date(input.timestamp).toISOString(),`
- ` event: cleanText(input.event, 80),`
- ` request_id: cleanText(input.request_id, 128),`
- ` app_job_id: cleanText(input.app_job_id, 128),`
- ` operation: cleanText(input.operation, 80),`
- ` method: cleanText(input.method, 12).toUpperCase(),`
- ` endpoint: {scheme: endpoint.scheme, host: endpoint.host ?? "invalid", route: endpoint.route ?? "invalid"},`
- ` auth_present: Boolean(input.headers?.authorization || input.headers?.Authorization),`
- ` request: {`
- ` source,`
- ` mode: cleanText(form.mode || "default", 40),`
- ` output_format: cleanText(form.output_format || "default", 40),`
- ` variants_num: Number.isSafeInteger(form.variants_num) ? form.variants_num : null,`
- ` prompt_present: typeof form.prompt === "string" && form.prompt.length > 0,`
- ` prompt_chars: typeof form.prompt === "string" ? form.prompt.length : 0,`
- ` },`
- ` outcome: {`
- ` http_status: Number.isInteger(input.http_status) ? input.http_status : null,`
- ` elapsed_ms: Number.isFinite(input.elapsed_ms) ? Math.max(0, Math.round(input.elapsed_ms)) : null,`
- ` },`
- ` };`
- `}`
The complete tested version additionally validates identifiers, summarizes nested output media, strips URLs and bearer-shaped values from errors, truncates untrusted text, and ignores unknown top-level and body fields. Keep that policy in one reviewed module rather than duplicating partial redaction across route handlers and workers.
Write one structured event per lifecycle boundary
The current Video to Music endpoint accepts multipart input and can return an async task identifier. Its OpenAPI 1.0.0 document provides a machine-readable request and response contract. Log the boundaries around that workflow, not every loop iteration.
- `provider.request.started`: application request ID, job ID, operation, safe request shape, and start time.
- `provider.request.finished`: HTTP status, elapsed time, and task ID when accepted.
- `provider.task.observed`: provider task status only when it changes, plus the poll attempt count.
- `provider.asset.accepted`: output count, content types, byte sizes, and your durable asset ID after verification.
- `provider.operation.failed`: stable code, bounded sanitized message, and the lifecycle stage.
Do not log the same unchanged processing response every few seconds. Keep polling cadence and retry control in the separate async polling workflow, and emit task events only on state changes or at a deliberately sampled interval.
Use the same application request key across a safe retry or queue redelivery. The duplicate-job prevention guide defines the corresponding request-state contract. Correlation helps investigate duplicates; it does not make a non-idempotent POST safe to replay.
Sanitize errors and defend the log sink
Provider and proxy errors are untrusted input. They can reflect request values, contain a signed URL, include a bearer-shaped token, or carry carriage returns and line feeds. OWASP explicitly recommends sanitizing CR, LF, and delimiter characters to prevent log injection.
Use stable local error codes whenever possible. If a short provider message is operationally necessary, flatten control characters, redact token patterns and URL queries, truncate it, then serialize through your structured logger. Never attach the raw response body automatically.
JSON serialization keeps embedded newlines escaped inside one JSON string, but the sink still matters. Configure the collector to parse one event per record, restrict log access, encrypt transport and storage, test logger failures, and define retention based on your legal and operational needs. The OWASP Secrets Management Cheat Sheet also recommends a process for removing exposed secrets from logs while preserving required integrity.
If a real key reaches a log, treat it as exposure. Revoke and replace the credential using the provider's supported process; deleting one visible event is not enough when the value may also exist in indexes, alert payloads, exports, or backups. The Next.js server-boundary guide covers keeping the key out of browser code in the first place.
The eleven-test contract
`safe-ai-api-log-v1` ran on Node.js v22.23.2 with no external packages. Eleven deterministic tests passed on August 16, 2026:
- Keep the allowlisted request, route, correlation, task, and timing fields.
- Replace a bearer value with an `auth_present` boolean.
- Remove endpoint query strings.
- Remove a signed source object's path, credential, and signature.
- Record prompt presence and length without prompt text.
- Summarize nested result media without output URLs.
- Omit uploaded filenames while retaining content type and size.
- Flatten CR, LF, and tab characters in untrusted errors.
- Redact bearer-shaped values and URL queries inside errors.
- Fail closed for invalid identifiers and URLs.
- Ignore unknown top-level and request-body fields.
The fixtures are synthetic. They prove the sanitizer's observed output for the tested cases, not that every logger, SDK, framework, transport, storage system, or downstream export is safe. Add integration tests at the actual log sink and canary secrets that must never appear in collected output.
When this pattern is not enough
Do not use application logs as an audit ledger for billing, license evidence, or media custody. Those records need their own integrity, access, and retention controls. Do not put full prompts or media URLs into a “secure debug” stream by default; a second store still creates a disclosure surface.
A boolean and count-based log may also be too sparse for incident response in a regulated environment. Add fields only after data classification, threat modeling, access-control design, and retention review. If your organization must correlate a sensitive identifier, consider a scoped keyed hash rather than the raw value, then rotate and govern that key separately.
Finally, URL sanitization is not URL validation. The separate presigned source-URL guide covers lifetime and handoff checks. The temporary-output durability guide covers copying and verifying result media before temporary access expires.
Ship the log contract before production traffic
Write one shared sanitizer, test it with recognizable canary secrets, and route every provider lifecycle event through it. Review the emitted schema, not the in-memory input object. Then confirm the collector, alerts, dashboards, exports, and backups contain the same safe contract.
Use the broader Sonilo Video-to-Music API guide for request flow, then implement against the current API reference. The practical rule is simple: log the shape of the work, not the credential or customer content that made the work possible.


