Limbo QA — API Integration Guide#
This guide is for developers integrating with the Limbo QA API. It assumes you have already received your access credentials (API key, dashboard user/password, webhook signing secret) from the Limbo team. If you have not, contact us before continuing.
Do not share your API key, do not commit it to source control.
1. Overview#
1.1 What this API does#
The Limbo QA API performs automated quality-assurance analysis on a music release (album + tracks). You submit the album metadata and asset URLs; the API runs a series of checks (images, metadata, audio) and returns a structured report indicating whether the release is ready to ship, has warnings, or has blocking issues.
The pipeline is asynchronous. The submission endpoint returns immediately; the result is delivered either via webhook (recommended) or by polling.
1.2 Base URL#
| Resource | URL |
|---|---|
| API base | https://api.sandbox.qaagent.limbobeat.com/v1 |
| Dashboard | https://sandbox.qaagent.limbobeat.com |
1.3 Authentication#
Every API request must include your API key in the x-api-key header:
x-api-key: <YOUR_API_KEY>
The API key is shown only once when your access is provisioned. Store it securely on your side.
There is no OAuth, no Bearer token, no rotating credential flow. The API key is the single secret used to authenticate.
The dashboard uses a separate username/password. API integrations only ever use
x-api-key.
1.4 The dashboard#
The dashboard is a web UI where your operator can:
- See the history of every QA run you have submitted
- Open the full report for any run
- Edit your
qa_configdefaults (which checks are enabled, severity, thresholds) - Send a test payload to your webhook endpoint without consuming quota
You log in with the username/password you received when your access was provisioned.
2. API Reference#
The API exposes three endpoints. All require the x-api-key header.
| Method | Path | Purpose |
|---|---|---|
POST |
/qa/album |
Submit an album for QA analysis |
GET |
/qa/album/{albumId}/status |
Get the pipeline state and summary of a previous submission |
GET |
/qa/album/{albumId}/report |
Get the full report once analysis has completed |
2.1 POST /qa/album#
Submit one album with its tracks for QA analysis. The response is 202 Accepted with a qa_run_id you can use to retrieve the result.
POST /qa/album
Host: api.sandbox.qaagent.limbobeat.com
Headers#
| Name | Required | Value | Description |
|---|---|---|---|
x-api-key |
yes | <YOUR_API_KEY> |
Authentication |
Content-Type |
yes | application/json |
|
Idempotency-Key |
yes | any unique string ≤ 256 chars (UUID v4 is the standard choice) | Your identifier for one logical submission. See §5.2. |
Top-level body parameters#
| Name | Type | Required | Description |
|---|---|---|---|
album_id |
string | yes | Your stable, unique identifier for this album. Used to look the run up later. Treat as opaque — we do not parse it. |
callback_url |
string | no | Public HTTPS URL we POST the final report to (§3). If omitted, you must poll. |
album |
object | yes | See album object below. |
tracks |
array | yes | One or more tracks. See track object below. |
qa_config |
object | no | Per-request override. Deep-merged onto your saved config. Omit if you want your default. See §5.3. |
album object parameters#
| Name | Type | Required | Description |
|---|---|---|---|
title |
string | yes | Album/release title. |
main_artist |
array of strings | yes | At least one artist name. Use exact display form. |
genre_1 |
string | yes | Primary genre label (free text — your taxonomy is fine). |
cover_image_url |
string (URL) | yes | Public HTTPS URL of the cover image. Must be reachable from the public internet. |
label |
string | no | Label name. |
genre_2 |
string | no | Secondary genre. |
release_date |
string (YYYY-MM-DD) | no | |
upc |
string | no | 12- or 13-digit UPC/EAN. Validated for format + check digit. |
language |
string (ISO 639-1) | no | Defaults to "en". Used to scope spell-check and OCR. |
production_line |
string | no | Copyright/production line (e.g. 2026 My Label Records). |
version |
string | null | no | Version tag (Remix, Live, etc). |
contributor, producer, featuring, remixer, composer, author, lyricist, performer, arranger, director |
array of strings | null | no | Per-role credit lists, all optional. |
tracks[] object parameters#
Required:
| Name | Type | Description |
|---|---|---|
track_id |
string | Your stable identifier for the track. |
title |
string | |
main_artist |
array of strings | At least one. |
track_number |
integer | 1-based, position within the disc. |
audio_url |
string (URL) | Public HTTPS URL of the audio. Lossless (FLAC/WAV) is preferred and required for some sub-checks. |
genre_1 |
string |
Optional:
| Name | Type | Default | Description |
|---|---|---|---|
isrc |
string | null |
12-char ISRC. Format + duplicate-across-tracks checks apply. |
disc_number |
integer | 1 |
|
duration_seconds |
float | null |
Used by cross-checks; if omitted we read it from the audio file. |
version |
string | null |
Remix, Acoustic, Live, etc. |
language |
string | "en" |
|
explicit |
boolean | false |
If true, explicit text in OCR and lyrics is treated as expected. |
is_cover |
boolean | false |
Marks the track as a known cover. |
is_remix |
boolean | false |
Marks the track as a known remix. |
lyrics |
string | null |
Plain text. If present, spell-check + offensive-text run on it. |
genre_2 |
string | null |
|
production_line |
string | null |
Per-track override. |
featuring, contributor, remixer, composer, author, lyricist, performer, producer, publisher, arranger, director |
array of strings | null |
Per-role credit lists. |
Example request body#
{
"album_id": "MY-LABEL-2026-00042",
"callback_url": "https://my-app.example.com/webhooks/limbo-qa",
"album": {
"title": "Sample Album Title",
"main_artist": ["Example Artist"],
"label": "My Label Records",
"genre_1": "World Music",
"genre_2": "Folk",
"release_date": "2026-06-15",
"upc": "1234567890128",
"language": "en",
"production_line": "2026 My Label Records",
"version": null,
"cover_image_url": "https://my-cdn.example.com/covers/00042.jpg",
"producer": ["John Doe"],
"composer": ["Example Artist"],
"lyricist": ["Example Artist"]
},
"tracks": [
{
"track_id": "MY-LABEL-2026-00042-01",
"title": "Sample Album Title",
"main_artist": ["Example Artist"],
"isrc": "USXXX2600001",
"track_number": 1,
"disc_number": 1,
"duration_seconds": 245.3,
"language": "en",
"explicit": false,
"is_cover": false,
"is_remix": false,
"audio_url": "https://my-cdn.example.com/audio/00042-01.flac",
"lyrics": "Sample lyrics line one\nSample lyrics line two...",
"genre_1": "World Music",
"genre_2": "Folk",
"production_line": "2026 My Label Records",
"composer": ["Example Artist"],
"lyricist": ["Example Artist"],
"producer": ["John Doe"]
}
]
}
Response — 202 Accepted#
{
"album_id": "MY-LABEL-2026-00042",
"qa_run_id": "QA-2026-05-22-cde10f64",
"status": "QUEUED",
"status_url": "/qa/album/MY-LABEL-2026-00042/status?qa_run_id=QA-2026-05-22-cde10f64",
"report_url": "/qa/album/MY-LABEL-2026-00042/report?qa_run_id=QA-2026-05-22-cde10f64"
}
qa_run_id— unique handle for this submission. Store it.status— alwaysQUEUEDfor new submissions. See §5.1 for the full lifecycle.status_url/report_url— relative paths you can poll, both already qualified with the rightqa_run_id.
Response — 202 Accepted (idempotent replay)#
When you POST the same Idempotency-Key again and the previous request already finished, you receive the cached response with an extra flag:
{
"album_id": "...",
"qa_run_id": "...",
"status": "QUEUED",
"status_url": "...",
"report_url": "...",
"idempotent_replay": true
}
The submission is not processed again. Quota is not charged again.
Response — 400 Bad Request#
| Error code | When |
|---|---|
missing_idempotency_key |
The Idempotency-Key header is required and was not sent. |
generic { "error": "Invalid JSON body" } |
The body is not valid JSON. |
generic { "error": "Validation error: ..." } |
The body failed schema validation (missing required field, wrong type, etc). |
invalid_qa_config_override |
Your qa_config override merged into your saved config produces an invalid config. |
Response — 403 Forbidden#
| Error code | When |
|---|---|
tenant_unknown |
The API key is not recognized. Double-check the header. |
tenant_suspended |
Your access is not active. Contact the Limbo team. |
quota_exhausted |
See §5.4 for the shape and meaning. |
Response — 409 Conflict#
{ "error": "request_in_flight" }
Another POST with the same Idempotency-Key is still being processed. Poll the same key; do not retry until it returns.
Response — 500 Internal Server Error#
Transient failure. Quota is rolled back; retry with the same Idempotency-Key is safe.
2.2 GET /qa/album/{albumId}/status#
Lightweight poll. Returns the pipeline state and a summary of check counts. Use this when you only need to know whether a run finished — not the full report.
GET /qa/album/{albumId}/status?qa_run_id={qa_run_id}
Host: api.sandbox.qaagent.limbobeat.com
Path parameters#
| Name | Type | Required | Description |
|---|---|---|---|
albumId |
string | yes | Your album_id from the submission. |
Query parameters#
| Name | Type | Required | Description |
|---|---|---|---|
qa_run_id |
string | no | Specific run to look up. If omitted, returns the latest run owned by your account for the given albumId. |
Headers#
| Name | Required | Value |
|---|---|---|
x-api-key |
yes | <YOUR_API_KEY> |
Response — 200 OK#
{
"album_id": "MY-LABEL-2026-00042",
"qa_run_id": "QA-2026-05-22-cde10f64",
"status": "COMPLETED",
"overall_status": "WARNING",
"started_at": "2026-05-22T01:14:32.821707+00:00",
"completed_at": "2026-05-22T01:14:47.241891+00:00",
"fingerprint_completed_at": "2026-05-22T01:16:02.114553+00:00",
"track_count": "1",
"album_title": "Sample Album Title",
"main_artist": "Example Artist",
"label": "My Label Records",
"callback_url": "https://my-app.example.com/webhooks/limbo-qa",
"summary": {
"total_checks": "9",
"passed": "7",
"failed": "0",
"errors": "0",
"warnings": "2"
}
}
Field meanings:
status— pipeline state. See §5.1.overall_status— the verdict your business logic should branch on. See §5.5.summary.*— integer counts, returned as strings in this endpoint. Cast to int on your side.
Response — 404 Not Found#
{ "error": "Not Found", "message": "No QA run found for album_id=MY-LABEL-2026-00042" }
A 404 is returned both when the album does not exist AND when it exists but belongs to a different account — by design, the API never leaks the existence of records that are not yours.
2.3 GET /qa/album/{albumId}/report#
Returns the complete JSON report with every check result, per-track findings, the AI executive review, and your effective qa_config. Use this when you did not set a callback_url and need to pull the result, or when you need to fetch the report again after the webhook.
GET /qa/album/{albumId}/report?qa_run_id={qa_run_id}
Host: api.sandbox.qaagent.limbobeat.com
Path parameters#
| Name | Type | Required | Description |
|---|---|---|---|
albumId |
string | yes | Your album_id from the submission. |
Query parameters#
| Name | Type | Required | Description |
|---|---|---|---|
qa_run_id |
string | no | Specific run to look up. If omitted, returns the latest run owned by your account for the given albumId. |
Headers#
| Name | Required | Value |
|---|---|---|
x-api-key |
yes | <YOUR_API_KEY> |
Response — 200 OK#
Truncated for readability. See §5.5 for how to interpret overall_status, blocking_issues, and warnings.
{
"album_id": "MY-LABEL-2026-00042",
"qa_run_id": "QA-2026-05-22-cde10f64",
"upc": "1234567890128",
"timestamp": "2026-05-22T01:16:02.114553+00:00",
"overall_status": "WARNING",
"fingerprint_pending": false,
"summary": {
"total_checks": 9,
"passed": 7,
"warnings": 2,
"failed": 0,
"errors": 0
},
"blocking_issues": [],
"warnings": [
{
"check": "image_moderation",
"target": "https://my-cdn.example.com/covers/00042.jpg",
"category": "web_full_match",
"confidence": 1.0,
"severity": "WARNING",
"detail": "5 exact image match(es) found on web",
"urls": ["https://www.youtube.com/watch?v=...", "..."]
}
],
"album_checks": {
"image_moderation": { "check": "image_moderation", "status": "PASS", "images": [...] },
"image_ocr": { "check": "image_ocr", "status": "PASS", "images": [...] },
"spell_check": { "check": "spell_check", "status": "PASS", "issues": [], "fields_checked": {...} }
},
"track_checks": {
"MY-LABEL-2026-00042-01": {
"metadata_validation": { "check": "metadata_validation", "status": "PASS", "audio_specs": {...} },
"audio_silence": { "check": "audio_silence", "status": "PASS", "silences": [] },
"audio_spectrogram": { "check": "audio_spectrogram", "status": "PASS", "spectrogram_url": "..." },
"audio_speech_detection": { "check": "audio_speech_detection","status": "PASS", "classification": "MUSIC" },
"audio_fingerprint": {
"track_id": "MY-LABEL-2026-00042-01",
"status": "PASS",
"matches_found": 1,
"matches": [{
"title": "Sample Album Title",
"score": "100",
"artists": ["Example Artist"],
"release_date": "2025-06-06"
}],
"ai_detection": [{ "prediction": "human", "ai_probability": "9.0" }]
}
}
},
"track_info": { "MY-LABEL-2026-00042-01": { "title": "...", "track_number": 1, "isrc": "..." } },
"album_metadata": { "title": "...", "main_artist": ["..."], "label": "..." },
"ai_review": {
"executive_summary": "The release is consistent with the declared metadata...",
"issue_reviews": [
{ "check": "image_moderation", "verdict": "agree", "reasoning": "..." }
],
"missed_issues": []
},
"qa_config": { "checks_enabled": {...}, "thresholds": {...}, "sub_checks": {...} }
}
Response — 200 OK (still in progress)#
{ "status": "RUNNING", "message": "QA is still in progress" }
The run exists but the final report is not yet ready. Poll again shortly.
Response — 400 Bad Request#
{ "error": "Missing albumId path parameter" }
The URL was malformed.
Response — 403 Forbidden#
Same shapes as POST /qa/album (auth issues).
Response — 404 Not Found#
{ "error": "Not Found", "message": "QA run not found" }
No such run, or it belongs to a different account.
{ "error": "Not Found", "message": "Report not yet available" }
The run exists but the report has not been written yet and status is not RUNNING. Rare; retry once.
3. Webhook Callbacks#
Polling works, but the recommended way to receive the result is to set callback_url in your submission. We will POST the final report to that URL.
3.1 How it works#
- The webhook fires once per run, after the full analysis finishes.
- It waits until all checks (including the asynchronous audio fingerprint step) are complete — you receive one final webhook with all results merged.
- The body is the same JSON as
GET /qa/album/{albumId}/report(§2.3), with one extra field only present on webhooks:original_request, an echo of the body you originally submitted.
Headers we send#
Content-Type: application/json
X-QA-Signature: <hex-hmac-sha256>
X-QA-Timestamp: <epoch-seconds-as-string>
Recognizing a test fire#
When you trigger a webhook from the dashboard's Test webhook button (§3.3), the payload includes an extra top-level marker:
{ "test": true, ... }
This field is never present on real production callbacks. Treat it as a hard signal in your receiver: when the top-level test field is true, acknowledge the request (return 2xx) and stop processing. Do not write to your QA database, do not alert your team, do not advance the album's release state. Everything else in the payload — headers, signature scheme, body shape, field names, nesting — is identical to a real callback, so the rest of your code path (signature validation, JSON parsing) still gets exercised end-to-end.
3.2 Signature verification#
We sign every webhook with HMAC-SHA256 using your signing secret (delivered when your access was provisioned). You MUST verify the signature on every received webhook. Unverified callers can spoof requests against your endpoint otherwise.
The signing recipe is:
signed_content = X-QA-Timestamp + "." + raw_request_body
signature = HMAC-SHA256(signing_secret, signed_content).hex()
Compare the result against the X-QA-Signature header using a constant-time comparison.
Critical pitfalls#
- Use the RAW request body bytes, not a re-stringified version of the parsed JSON. Parsing + re-stringifying re-orders keys and changes whitespace; the HMAC will not match. Read the bytes BEFORE parsing.
- Constant-time compare. Use your language's constant-time comparison primitive (e.g.
hmac.compare_digestin Python,crypto.timingSafeEqualin Node). A plain==leaks the secret one byte at a time via timing. - Check the timestamp. Reject requests where
X-QA-Timestampis older than ~5 minutes — this prevents replay attacks where an attacker who once observed a valid request keeps sending it forever. - TLS only. Your
callback_urlmust be HTTPS. We will not connect to plain HTTP.
3.3 Testing your endpoint from the dashboard#
The dashboard has a Test webhook button that fires a synthetic payload with the same headers, signature scheme, and body shape as a real callback. Use it to validate your verification code before going live. The synthetic payload sets "test": true at the top level (see §3.1). Rate-limited at 10 seconds between tests and 20 tests per day.
3.4 Webhook response expectations#
We expect any 2xx response from your endpoint. We do not currently retry failed deliveries. If your endpoint is down at the moment of delivery, you can still pull the report via GET /qa/album/{albumId}/report — the data is persisted on our side for the run's retention period.
4. Tutorial — End-to-end workflow#
A complete integration is composed of a small handful of HTTP exchanges. This section walks through the typical flow.
4.1 Submit the album#
Generate one Idempotency-Key for this logical submission (a UUID v4 is the standard choice). Persist it on your side. If a retry fires, reuse the same key — never generate a new one for a retry (see §5.2).
POST /qa/album
Host: api.sandbox.qaagent.limbobeat.com
Content-Type: application/json
x-api-key: <YOUR_API_KEY>
Idempotency-Key: <UNIQUE_KEY_PER_SUBMISSION>
{ ...album payload as in §2.1... }
The response (§2.1) returns a qa_run_id. Store it — you will use it to retrieve the result.
If callback_url was included, you can stop here and wait for the webhook (§4.2 → option A). Otherwise you need to poll (§4.2 → option B).
4.2 Wait for the result#
Option A — Webhook (recommended). We will POST the report to your callback_url once the analysis is complete (§3). Make sure your endpoint verifies the HMAC signature (§3.2) before trusting the payload.
Option B — Polling. Call the status endpoint:
GET /qa/album/{albumId}/status?qa_run_id={qa_run_id}
Host: api.sandbox.qaagent.limbobeat.com
x-api-key: <YOUR_API_KEY>
Repeat until the response shows "status": "COMPLETED". See §5.1 for the full lifecycle of status.
4.3 Fetch the full report#
Once the run is complete, retrieve the report:
GET /qa/album/{albumId}/report?qa_run_id={qa_run_id}
Host: api.sandbox.qaagent.limbobeat.com
x-api-key: <YOUR_API_KEY>
The body is described in §2.3.
If you used a webhook, you already have the same body — this step is only needed if you missed the callback or want to fetch the report again.
4.4 Interpret the verdict#
Branch your business logic on overall_status. The four values map to four different actions; see §5.5 for the decision table.
The deduped lists of findings live in blocking_issues[] (FAIL-severity) and warnings[] (WARNING-severity).
5. Concepts#
5.1 Status lifecycle#
QUEUED ──► RUNNING ──► PENDING_FINGERPRINT ──► COMPLETED
- QUEUED — submission accepted, queued for processing.
- RUNNING — synchronous analysis (images, metadata, audio silence/speech/spectrogram) is in flight.
- PENDING_FINGERPRINT — synchronous analysis is done; we are waiting on the audio fingerprint match.
- COMPLETED — all checks done. The webhook fires now (if
callback_urlis set).
If you poll, GET /status is the cheap call; GET /report is the expensive one. Use status to know when the report is ready, then fetch the report once.
5.2 Idempotency#
POST /qa/album requires an Idempotency-Key header. We use it to deduplicate retries:
| Scenario | Behavior |
|---|---|
| Same key, completed prior call | Cached response returned. No rerun. No extra quota charged. |
| Same key, prior call still running | 409 request_in_flight. Wait, then poll the cached response. |
| Same key, prior call failed mid-flight | The slot is released; safe to retry with the same key. |
| New key | Fresh submission. Quota charged. |
Recommended practice: generate one UUID v4 per logical submission attempt on your side. Persist it next to the album record. If your retry logic fires, send the same UUID, never a new one.
If you omit the header you get a 400 missing_idempotency_key.
5.3 qa_config#
Your account has a saved qa_config (set via the dashboard or by your operator). Any submission can override individual fields by sending a partial qa_config in the body. The merge is deep — what you do not send is inherited from your saved default.
Top-level shape:
{
"qa_config": {
"checks_enabled": { ... },
"thresholds": { ... },
"sub_checks": { ... },
"exceptions": { ... }
}
}
checks_enabled#
Toggles whole checks on/off. Keys (boolean):
image_moderation— visual content moderation on the cover image.image_ocr— text extraction from images.audio_silence_detection— flags excessive silence in the audio.audio_speech_detection— flags tracks that are mostly speech rather than music.audio_spectrogram— analyzes the frequency profile of the audio.audio_fingerprint— matches the audio against a database for copyright and AI-generation checks.metadata_validation— validates ISRCs, UPC, track numbering, etc.spell_check— checks textual fields for spelling.cross_analysis— AI executive review across all checks.
thresholds#
Common knobs (full list visible in the dashboard):
| Key | Purpose |
|---|---|
silence_db_threshold |
Audio below this level (dBFS) counts as silence. More negative = stricter. |
track_min_duration_seconds |
Minimum track length. Shorter tracks fail. |
fingerprint_min_score |
Minimum fingerprint match score to flag a hit. |
cover_min_width |
Minimum cover width in pixels. |
cover_max_file_size_mb |
Maximum cover file size in MB. |
ai_detection_min_probability |
Threshold above which AI-generation detection flags a track. |
sub_checks#
Each check exposes a tree of {enabled, severity} toggles for individual sub-findings:
"sub_checks": {
"metadata_validation": {
"isrc_format": { "enabled": true, "severity": "FAIL" },
"duplicate_isrc":{ "enabled": true, "severity": "FAIL" },
"track_numbering":{"enabled": true, "severity": "FAIL" }
},
"audio_fingerprint": {
"copyright_match":{ "enabled": true, "severity": "FAIL" },
"ai_generated": { "enabled": true, "severity": "FAIL" },
"cover_version": { "enabled": true, "severity": "WARNING" }
}
}
severity is WARNING or FAIL. WARNING never blocks; FAIL marks the run as overall FAIL.
exceptions#
Per-check allowlists. Current key: spell_check. Words in this list are not flagged.
Per-request override example#
The values you send are deep-merged on top of your saved config. Use it to tweak a single threshold or disable a single sub-check for one submission without changing your default:
{
"qa_config": {
"thresholds": { "fingerprint_min_score": 85 },
"checks_enabled": { "audio_silence_detection": false }
}
}
The merged config is validated. A malformed override returns 400 invalid_qa_config_override.
5.4 Quota#
Your account has a lifetime cap configured at provisioning and visible in the dashboard. Each POST /qa/album consumes a number of units determined by your plan. The check is atomic: if a submission would exceed the cap, it is rejected and nothing is consumed.
The dashboard shows used / limit live. When you are out:
{ "error": "quota_exhausted", "used": 200, "limit": 200, "unit": "<plan-specific>", "requested": 14 }
unit— string identifying the counter your plan uses. Treat as opaque (informational only — do not hardcode values).requested— the amount this submission would have consumed. Lets you tell0 leftapart from a partial shortfall (e.g.you only had 13 left and asked for 14).
Contact the Limbo team to raise your cap.
5.5 Status semantics — overall_status#
overall_status is the verdict your business logic should branch on. The four values map to four different actions — they are NOT interchangeable:
| Value | Meaning | What to do |
|---|---|---|
PASS |
No issues found. | Ship the release. |
WARNING |
Non-blocking findings (soft validation, style, etc.). | A human should review, but you can still ship. |
ERROR |
One or more checks could not be executed (timeout, transient failure of an external dependency, malformed input). The QA did NOT reach a verdict on the release. | Retry the submission. Do NOT treat this as a verdict — the release has not been evaluated. |
FAIL |
The QA reached a verdict and found one or more blocking issues (e.g. copyright match, duplicate ISRC, missing required metadata). | Do not ship until the issues in blocking_issues[] are fixed. |
The key distinction is
ERRORvsFAIL. -ERRORmeans we could not finish the analysis — retry the submission. -FAILmeans we analyzed the release and it has problems you must fix — do not ship.Treating them the same will either block valid releases (treating
ERRORasFAIL) or ship broken ones (treatingFAILasERROR). Always branch on the exact value.
Related fields:
summary.{passed,warnings,failed,errors}— check-level rollups (how many individual checks landed in each bucket).blocking_issues[]— deduped list of FAIL-severity findings. Surface these whenoverall_status == "FAIL".warnings[]— deduped list of WARNING-severity findings.
6. Limits#
| Item | Value |
|---|---|
| API throttle (per account) | 10 req/s sustained, 20 burst |
| Webhook retry policy | none currently |
| Synthetic webhook test | 10 s cooldown, 20/day |
| Lifetime quota | per account, see your dashboard |
7. Troubleshooting#
| Symptom | Likely cause |
|---|---|
403 tenant_unknown |
Wrong API key, or header not sent. Confirm x-api-key exactly. |
403 tenant_suspended |
Your access is not active. Contact Limbo. |
400 missing_idempotency_key |
You forgot the header. Required on every POST. |
400 Validation error: ... |
Body does not match the schema. The error message points to the field. |
409 request_in_flight |
A concurrent POST with the same key is still running. Wait and poll. |
| HMAC verification fails | You parsed and re-stringified the body. Use the raw bytes (§3.2). |
| HMAC verification fails (raw bytes used) | Wrong signing secret, or the leading <timestamp>. was forgotten. |
| Webhook never fires | callback_url was not HTTPS, or your endpoint returned non-2xx. Pull GET /report to confirm the run finished. |
| Cover image / audio URL not accessible | URLs must be reachable from the public internet. Pre-signed URLs are fine if they have not expired. |
404 Not Found even though I just submitted |
The pipeline is still in RUNNING — poll GET /status first, or wait for the webhook. |
8. Support#
- Dashboard issues, lost credentials, quota bumps, account suspension: contact your Limbo account manager.
- API integration questions, bug reports: open a ticket with the Limbo team.
When reporting a bug, please include: qa_run_id, request Idempotency-Key, and the timestamp range of the incident. With those values we can pin down the run in seconds.