MatchTune Audit API
Send links to YouTube, TikTok, Instagram or Facebook posts. For each one the pipeline downloads the audio, works out what kind of sound it contains, identifies the music and checks whether the track is in a MatchTune target library. You get one result document per post.
Quickstart
Processing is asynchronous: you submit posts, then come back for the results. A post usually takes a few minutes. It can take up to about 30 minutes when the target-library lookups are busy.
Submit the posts
curl -X POST https://audit-api.matchtune.com/v1/requests \
-H "X-API-Key: ak_your_key" \
-H "Content-Type: application/json" \
-d '{"items": [{"url": "https://www.youtube.com/watch?v=XXXXXXXXXXX"},
{"url": "https://www.tiktok.com/@example_brand/video/1234567890123456789"}]}'
Each item can carry the columns that only you know: tracked_account is the account the post is audited for, external_id is your own id for the post, and metadata is any JSON object of your own attributes (a campaign, a market, your internal ids; up to 8 KB). Both are stored with the item and returned untouched on the item, next to its result in /results, and in every webhook for it. On the request, target_libraries names the libraries that count as yours, by their MatchTune names. Simpler still, pass your project_code (for example BEN-2607161): the project's scoped libraries and rightsholder flags are then taken from the MatchTune dashboard.
-d '{"project_code": "ABC-2607161", "external_request_id": "batch-2026-09-22-001",
"items": [{"url": "https://www.instagram.com/p/XXXXXXXXXXX/",
"tracked_account": "example_brand", "external_id": "post-48213",
"metadata": {"campaign": "spring-2026", "market": "FR"}}]}'
201 Created{ "request_id": "3f2b8c1e-5a7d-4e9b-8c21-6d0f4a9b7e10", "external_request_id": "batch-2026-09-22-001",
"total_items": 1, "trace_id": "5e1c9a7b3d2f48e6a0b4c8d7e2f1a9b3", "reused": false,
"items": [ { "job_id": "b41d7e92-8c0a-4f63-9e15-2a7c6f3d8b04", "position": 0, "url": "https://www.instagram.com/p/XXXXXXXXXXX/", "external_id": "post-48213" } ] }
Poll until the request is finished
Every 30 to 60 seconds, check status and progress_percent. The request is done when status is COMPLETED.
curl https://audit-api.matchtune.com/v1/requests/3f2b8c1e-5a7d-4e9b-8c21-6d0f4a9b7e10 \
-H "X-API-Key: ak_your_key"
Fetch the results
curl https://audit-api.matchtune.com/v1/requests/3f2b8c1e-5a7d-4e9b-8c21-6d0f4a9b7e10/results \
-H "X-API-Key: ak_your_key"
If you'd rather not poll, pass a callback_url and we will POST each result to you as soon as it's ready, signed so you can check it came from us.
Authentication
Every call needs your API key. Send it in the X-API-Key header, or as Authorization: Bearer ak_… (both are declared as security schemes in the OpenAPI document). Keys start with ak_ and are issued by MatchTune. We store only a hash of your key, so we can't show it to you again. If you lose it, ask us to rotate it: the old key stops working immediately.
Each key belongs to one customer account, and everything is scoped to that account:
- Requests you create are filed under your account. Any
customer_idyou put in the body is ignored. - Lists only contain your requests.
- Requests and items of other accounts answer
404 Not Found, exactly as if they didn't exist.
A missing, unknown or disabled key gets 401 Unauthorized with error code UNAUTHORIZED.
Requests, items and steps
A request is one submission: a batch of up to 100,000 URLs. Each URL becomes an item with its own job_id. Items are processed independently and in parallel, so one broken link never holds up the others.
A request can end up with more items than you submitted: a carousel that holds several videos is audited one item per video. Read total_items from the request rather than counting what you sent.
Each item moves through four steps, in this order. An item's current_stage is always one of them.
- DOWNLOAD
- PROCESSING
- ENRICHMENT
- COMPLETE
| Step | What happens |
|---|---|
DOWNLOAD | Fetches the post's audio and its public metadata: caption, date, likes, account. |
PROCESSING | Analyses the audio: how much is music and how much is speech, which track is playing, and whether it is in a MatchTune target library. This is usually the longest step. |
ENRICHMENT | Completes the track details, such as label and ISRC. |
COMPLETE | Writes the result document and sends the item.completed webhook to your callback_url, if you gave one. |
Carousels
An Instagram carousel can hold several videos, each with its own music. We audit every video slide as its own item: the item you submitted keeps the slide its url points at (or the first video when the url names the carousel itself), and each further video slide is added to the request as a new item while the request runs. Image slides produce nothing.
- A slide item has
parent_job_id(the item it was split from) and inherits itsexternal_id,metadataandtracked_account. It appears in/items,/resultsand webhooks like any other item, and the request'stotal_itemsgrows accordingly. - In its result,
POST URLis the slide's own link (https://www.instagram.com/p/<slide id>/),PARENT POST URLis the carousel, andPOST TYPEisCarousel. Date, caption, likes and comments are the carousel's. - A carousel with no video at all is one item with
AUDIO STATUSNo Audio. - Slides are added during the
DOWNLOADstep, so poll the request (or list its items) rather than assuming the list is final at submission time.
Submitting one carousel that holds two videos gives two records:
GET /v1/requests/{id}/results{
"results": [
{ "job_id": "5d1b0fb3-…", "parent_job_id": null, "position": 0,
"result": { "POST TYPE": "Carousel",
"POST URL": "https://www.instagram.com/p/SLIDE1/",
"PARENT POST URL": "https://www.instagram.com/p/CAROUSEL/", … } },
{ "job_id": "e6edb49c-…", "parent_job_id": "5d1b0fb3-…", "position": 2,
"result": { "POST TYPE": "Carousel",
"POST URL": "https://www.instagram.com/p/SLIDE2/",
"PARENT POST URL": "https://www.instagram.com/p/CAROUSEL/", … } }
]
}
Statuses
Items, requests and the individual steps of an item share one set of statuses. An item's current_stage tells you which step it is in.
| Status | Meaning |
|---|---|
| QUEUED | Accepted, waiting to start. |
| PROCESSING | Being worked on. |
| RETRYING | A step failed temporarily (for example, the platform rate-limited the download) and is being retried automatically. |
| COMPLETED | Finished. The item has a result document. |
| FAILED | Could not be processed. last_error says why, for example a private, deleted or unsupported post. |
| CANCELLED | Stopped before it finished. |
| SKIPPED | Only on steps: the step never ran because an earlier step failed. |
A request's status becomes COMPLETED once every item has finished, whether the item completed or failed. Always check each item's status, not only the request's.
The result document
Each completed item has one result document. The keys are in upper case so they map directly onto audit spreadsheet columns, and an empty value is written -, as in the spreadsheet. The document contains those columns and nothing else; the item's job_id is on the item and results endpoints around it.
Audio category
AUDIO CATEGORY takes one of these values:
Only the first three contain music we identify. For Dialogue Only, Sound Effects Only, Silent Track and Mixed Sources (several short, overlapping sound sources, where a match is far more often wrong than right) no track is reported and AUDIO STATUS is Audio Not Recognized.
Re-uploads and covers
Social platforms are full of copies of commercial songs published under an uploader's own name, sometimes with the real performer hidden in the title ("Song (feat. Real Artist)" by an anonymous account). Recognition services index those copies as tracks in their own right. When we can establish beyond doubt which recording a copy reproduces, the track columns carry the original — its title, artist, label and ISRC — so the licensing question is answered against the real rightsholder. A genuine collaboration or a remix is never rewritten.
Match confidence
Every recognised track comes with MATCH CONFIDENCE. We report a track as High only when it is established: two independent recognition services name the same recording, one service matches strongly and nothing contradicts it, or a fingerprint check against the catalogue's own recording confirms it. When several candidates compete, or the only one is too weak to stand on its own (for example a single service matching a copy with no ISRC), nothing is established: the post is reported as Audio Not Recognized, with no track. A wrong identification costs more than a miss.
Fields
| Field | Type | Description |
|---|---|---|
AUDIO STATUS | string | Audio Recognized, Audio Not Recognized, or No Audio when the post carries no audio track at all. |
AUDIO CATEGORY | string | What the soundtrack is made of (see above). |
TRACK NAME | string | Identified track title, as registered for its ISRC when the ISRC is known. When the audio is a re-upload of a commercial song published under someone else's name, the columns name the original recording (see below). |
TRACK ARTIST | string | Identified artist, as registered for its ISRC when the ISRC is known. |
TRACK RECORD LABEL | string | Record label that owns the ISRC, looked up in a music catalogue by ISRC; otherwise the label the recognition services reported. |
TRACK ISRC | string | International Standard Recording Code. |
MUSIC SOURCE | string | The music source the track's record label resolves to in the MatchTune library dictionary (for example Epidemic Sound, Meta Sound Collection, Universal Music Group), or Not Found in Target Libraries. - unless the audio was recognised. |
IN TARGET LIBRARY | string | Yes when the music source is one of the project's scoped libraries (from project_code or target_libraries), No otherwise; answered only for a recognised track, - for the rest. |
LICENSE END DATE | string | License end date of the library track, when known. |
REVIEW FLAG | string | Review Required when a person should check the answer (for example a lookup that could not finish); how sure the identification is lives in MATCH CONFIDENCE. Cross-Platform Use when a platform library's track appears on another platform. - otherwise. |
MATCH CONFIDENCE | string | High when a track is reported: the identification is established (see Match confidence). - when no track is reported. |
RISK STATUS | string | At Risk, Compliant or Unverified: the MatchTune risk rules applied to this post. |
MUSIC DURATION (SEC) | number | Seconds of music detected. |
MUSIC PRESENCE SCORE | number | 0–100, how clearly music is present. |
PLATFORM | string | Platform the post comes from: YouTube, TikTok, Instagram or Facebook. |
POST URL | string | Canonical URL of the post. |
POST ORIGIN | string | The account that published the post, as a handle (example_brand), not a display name. |
TRACKED ACCOUNT | string | The account this post is audited for, as sent with the url. Defaults to POST ORIGIN when you don't send one. |
POST DATE | string | Publication date and time, YYYY-MM-DD HH:MM:SS (date only when the platform gives no time). |
POST TYPE | string | Media type of the post: Video, Carousel for a slide of a multi-media post, Image. |
POST CAPTION | string | Caption or description. |
POST LIKES | integer | Like count at download time. |
POST COMMENTS | integer | Comment count at download time. |
VIDEO DURATION (SEC) | number | Length of the post. |
ARTIST POPULARITY | string | Reach of the identified artist, from MatchTune's audience data: Tier 1: Mega Artists, Tier 2: Mainstream Artists, Tier 3: Established Artists, Tier 4: Moderate Reach Artists or Tier 5: Long-Tail Artists. - when the track was not recognised or the artist has no audience data. |
ACCOUNT TYPE | string | Owned when the post was published by the tracked account itself, Collaboration when someone else published it for them. Needs tracked_account. |
PARENT POST URL | string | For a carousel slide, the carousel it belongs to (see Carousels). - otherwise. |
Example
A YouTube post, recognised{
"PLATFORM": "YouTube",
"POST URL": "https://www.youtube.com/watch?v=XXXXXXXXXXX",
"POST ORIGIN": "example_brand",
"TRACKED ACCOUNT": "example_brand",
"ACCOUNT TYPE": "Owned",
"POST DATE": "2024-05-01 09:12:44",
"AUDIO STATUS": "Audio Recognized",
"AUDIO CATEGORY": "Music Only",
"IN TARGET LIBRARY": "No",
"MUSIC SOURCE": "Not Found in Target Libraries",
"REVIEW FLAG": "-",
"MATCH CONFIDENCE": "High",
"RISK STATUS": "At Risk",
"TRACK NAME": "Example Track",
"TRACK ARTIST": "Example Artist",
"TRACK RECORD LABEL": "Example Records",
"TRACK ISRC": "ZZX012600001",
"VIDEO DURATION (SEC)": 13,
"MUSIC DURATION (SEC)": 13.009,
"MUSIC PRESENCE SCORE": 94.8,
"POST LIKES": 312,
"POST COMMENTS": 7,
… plus the remaining fields listed above
}
Callbacks
When a request includes callback_url, we POST a JSON event to that URL for each of these moments:
| Event | When | Carries |
|---|---|---|
item.completed | An item finished and has a result. | job_id, external_id, metadata, parent_job_id (carousel slides), the result document. |
item.failed | An item could not be processed. | job_id, external_id, metadata, an error like the item's last_error. |
request.completed | Every item of the request has finished, one way or the other. | request_id, external_request_id. |
POST {your callback_url}{
"event": "item.completed",
"request_id": "3f2b8c1e-5a7d-4e9b-8c21-6d0f4a9b7e10",
"external_request_id": "batch-2026-09-22-001",
"job_id": "b41d7e92-8c0a-4f63-9e15-2a7c6f3d8b04",
"external_id": "post-48213",
"metadata": { "campaign": "spring-2026", "market": "FR" },
"parent_job_id": null,
"status": "COMPLETED",
"result": { … the result document … },
"error": null,
"timestamp": "2026-09-22T14:06:24.976633+00:00"
}
Every delivery carries three headers: X-MatchTune-Event (the event name), X-MatchTune-Delivery (an id that stays the same across retries of one event, so you can drop duplicates) and X-MatchTune-Signature.
Verifying the signature
Your account has a webhook secret (MatchTune gives it to you with your key). The signature header is t=<unix seconds>,v1=<hex> where v1 is HMAC-SHA256(secret, "<t>.<raw request body>"). Compute it over the body bytes exactly as received, compare in constant time, and reject timestamps older than five minutes:
# Python
import hmac, hashlib, time
def verify(secret, body: bytes, header: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300: return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
Delivery and retries
- Answer with any
2xxwithin 10 seconds; do the work afterwards. - Anything else, or a timeout, is retried after 1 s, 5 s and 25 s, then given up. Use
/resultsto catch up on anything you missed: it is always the source of truth. - Deliveries are sent from
51.91.64.130; allowlist that address if your endpoint filters by source. - Deliveries can arrive out of order, and rarely twice. Key on
job_id, not on arrival order and not onexternal_idalone: a carousel's slides share theexternal_idof the item they came from.
Errors and limits
Every error, whatever its status, has the same shape: a machine-readable code, a message for humans, a trace_id to quote to support (also sent in the X-Trace-Id response header) and, when useful, details:
429 Too Many Requests{
"error": {
"code": "RATE_LIMITED",
"message": "daily limit of 500 items reached (480 used in the last 24 h, 30 requested)",
"trace_id": "5e1c9a7b3d2f48e6a0b4c8d7e2f1a9b3",
"details": { "limit": 500, "used_24h": 480, "requested": 30 }
}
}
| Status | Code | When |
|---|---|---|
401 | UNAUTHORIZED | Missing, unknown or disabled API key. |
404 | NOT_FOUND | The request or item doesn't exist, or belongs to another account. |
409 | NOT_READY | You asked for an item's result before the item completed. details carries its current status and stage. |
413 | PAYLOAD_TOO_LARGE | More than 100,000 items in one request. |
422 | VALIDATION_ERROR | The body or a parameter isn't valid; details lists each field and what is wrong with it. |
422 | UNKNOWN_PROJECT, UNKNOWN_LIBRARY | A project_code or a name in target_libraries that MatchTune doesn't know. |
422 | INVALID_CURSOR | A cursor that this API didn't issue. |
429 | RATE_LIMITED | Your account's daily quota would be exceeded. The quota counts items submitted over a rolling 24 hours. |
500 | INTERNAL_ERROR | Something went wrong on our side. Retry later and quote the trace_id if it persists. |
503 | LIBRARIES_NOT_READY | The library dictionary isn't loaded yet; retry in a minute or omit target_libraries. |
Item errors
When an item fails, its last_error has a class, the stage where it stopped, a readable message and, for downloads, the platform's own reason.
| class | Meaning |
|---|---|
gone | The platform confirms the post no longer exists: deleted, or its account removed or made private. Resubmitting will not help. |
permanent | The post cannot be processed as submitted: geo-restricted, unsupported url, malformed link. |
transient | A temporary problem; the item was retried automatically before failing. |
timeout | The download took too long. |
too_long | The post is longer than your account's maximum post duration (3 minutes 30 seconds unless MatchTune set another limit for you) and was not processed. reason gives the post's length and the limit. |
Pagination
List endpoints use a cursor. Pass the next_cursor you received as cursor on the next call, until next_cursor is null. For /items and /results it is the item position; for /requests it is an opaque string. Never build a cursor yourself.
Safe retries
Send an Idempotency-Key header with POST /v1/requests (any unique string, such as your own batch id). If the call is repeated with the same key, you get back the original request with "reused": true instead of a duplicate.
OpenAPI document
The machine-readable contract is at /openapi.json (OpenAPI 3.1): both security schemes, every request and response schema including the result document and the webhook event, and the error envelope on each operation. Point your generator or API client at it.
Create a request
Submits posts for processing. Returns right away with 201 Created; the work happens in the background.
| Body field | Type | Description |
|---|---|---|
items required | object[] | 1 to 100,000 posts. Each one becomes an item, in this order. An item has url (required), tracked_account, external_id (your id for the post, up to 128 characters) and metadata (a JSON object of your own attributes, up to 8 KB, returned untouched on the item, in /results and in webhooks). |
external_request_id | string | Your id for this batch. Echoed on the request, in lists and in webhooks; filterable on GET /v1/requests. |
project_code | string | Your MatchTune project. Its scoped libraries and rightsholder flags are applied. |
target_libraries | string[] | Without a project: the MatchTune library names that count as yours. |
callback_url | url | Where to POST signed events as items complete. See Callbacks. |
urls deprecated | string[] | The previous name of items; still accepted, with plain URL strings. Send one or the other. |
| Header | Description |
|---|---|
Idempotency-Key | Optional. Makes retries safe (see Safe retries). |
Request{
"external_request_id": "batch-2026-09-22-001",
"project_code": "ABC-2607161",
"callback_url": "https://example.com/hooks/audit",
"items": [
{ "url": "https://www.instagram.com/p/XXXXXXXXXXX/", "tracked_account": "example_brand", "external_id": "post-48213" }
]
}
201 Created{
"request_id": "7c9e2a44-1b3f-4d8e-a6c5-0e8f2d1b9a37",
"external_request_id": "batch-2026-09-22-001",
"total_items": 1,
"trace_id": "5e1c9a7b3d2f48e6a0b4c8d7e2f1a9b3",
"reused": false,
"items": [
{ "job_id": "b41d7e92-8c0a-4f63-9e15-2a7c6f3d8b04", "position": 0, "url": "https://www.instagram.com/p/XXXXXXXXXXX/", "external_id": "post-48213" }
]
}
items in the response lists every item's job_id in submission order, so you can track them right away without listing the request. A carousel can add further items later, so treat this list as the starting point, not the final one. Keep the request_id; the other endpoints take it. Quote the trace_id when you contact support about a request.
List your requests
Returns your requests, newest first, with item counts per status. Paginated with an opaque cursor.
| Query | Type | Description |
|---|---|---|
status | string | Only requests in this status, for example PROCESSING. |
external_request_id | string | Only the request(s) you submitted with this id. |
project_code | string | Only requests for this project. |
created_after, created_before | datetime | ISO 8601 bounds on created_at. |
cursor | string | The next_cursor of the previous page. |
limit | integer | Default 50, max 500. |
200 OK{
"requests": [
{
"request_id": "7c9e2a44-1b3f-4d8e-a6c5-0e8f2d1b9a37",
"external_request_id": "batch-2026-09-22-001",
"customer_id": "acme",
"project_code": "ABC-2607161",
"created_at": "2026-09-18T06:04:21.551319Z",
"status": "PROCESSING",
"total_items": 3,
"counts": { "QUEUED": 0, "PROCESSING": 1, "RETRYING": 0, "COMPLETED": 2, "FAILED": 0, … },
"progress_percent": 66.67,
"rate_per_min": 0.4
}
],
"next_cursor": "MjAyNi0wOS0xOFQwNjowNDoyMS41NTEzMTkrMDA6MDB8N2M5ZTJhNDQ…"
}
Get request progress
Everything from the list entry, plus an estimated time to completion and per-stage detail. This is the endpoint to poll.
| Field | Type | Description |
|---|---|---|
status | string | PROCESSING until every item has finished, then COMPLETED. |
total_items | integer | Items in the request. It can grow while the request runs when a carousel is split into one item per video. |
counts | object | Number of items in each status. |
progress_percent | number | Share of items that have finished (completed, failed or cancelled). |
rate_per_min | number | Items completed per minute recently. |
eta_minutes | number | null | Estimated minutes left, once enough items have completed to estimate. |
stages | object | For each step (DOWNLOAD, PROCESSING, ENRICHMENT, COMPLETE), how many items are in it, per status. |
stage_durations | object | For each step: samples, avg_ms, p50_ms, p95_ms, p99_ms. |
item_durations | object | The same statistics for whole completed items. |
external_request_id, project_code | string | null | As submitted. |
target_libraries | string[] | null | The libraries in scope for this request, resolved to MatchTune's names (from the project or from what you sent). |
callback_url, trace_id | string | As submitted, and the id to quote to support. |
Get all results of a request
Every item in submission order, with its result document. result stays null until the item completes; for failed items, last_error explains why. You can call this at any time, so it also works for collecting results progressively.
| Query | Type | Description |
|---|---|---|
status | string | Only items in this status, for example COMPLETED. |
cursor | integer | Position to start from. Default 0. |
limit | integer | Default 100, max 500. |
200 OK{
"results": [
{
"job_id": "b41d7e92-8c0a-4f63-9e15-2a7c6f3d8b04",
"external_id": "post-48213",
"metadata": { "campaign": "spring-2026", "market": "FR" },
"parent_job_id": null,
"position": 0,
"url": "https://www.youtube.com/watch?v=XXXXXXXXXXX",
"status": "COMPLETED",
"current_stage": "COMPLETE",
"last_error": null,
"result": { "AUDIO STATUS": "Audio Recognized", "TRACK NAME": "Example Track", … }
},
{
"job_id": "e8a3f610-4d2b-4c7e-b951-7f0e3a2c6d18",
"external_id": "post-48214",
"position": 1,
"url": "https://www.youtube.com/watch?v=YYYYYYYYYYY",
"status": "FAILED",
"current_stage": "DOWNLOAD",
"last_error": {
"class": "permanent",
"stage": "DOWNLOAD",
"message": "The post could not be downloaded: it is private, deleted, geo-restricted or not a supported post.",
"reason": "Video unavailable"
},
"result": null
}
],
"next_cursor": null
}
List items
A lighter view of the items without result documents. You can filter by status or stage, for example to find the failures.
| Query | Type | Description |
|---|---|---|
status | string | Only items in this status, such as FAILED. |
stage | string | Only items currently in this step: DOWNLOAD, PROCESSING, ENRICHMENT or COMPLETE. |
external_id | string | Only the item(s) you submitted with this id. |
cursor | integer | Position to start from. Default 0. |
limit | integer | Default 100, max 1000. |
Each entry has job_id, external_id, metadata, parent_job_id (carousel slides only), position, url, status, current_stage, created_at, updated_at and last_error. The page ends with next_cursor.
Get one item's result
Returns the item's result document on its own. Before the item completes, you get 409 with its current state:
409 Conflict{ "error": { "code": "NOT_READY", "message": "item has no result yet", "trace_id": "5e1c9a7b…",
"details": { "status": "PROCESSING", "current_stage": "PROCESSING", "last_error": null } } }
Follow one item
The item's status plus its four steps, each with its own status, start and end times, and duration. total_ms is the processing time summed over the steps. A step that is still running reports the time elapsed so far in duration_ms.
200 OK{
"job_id": "b41d7e92-8c0a-4f63-9e15-2a7c6f3d8b04",
"external_id": "post-48213",
"request_id": "3f2b8c1e-5a7d-4e9b-8c21-6d0f4a9b7e10",
"position": 0,
"url": "https://www.youtube.com/watch?v=XXXXXXXXXXX",
"status": "PROCESSING",
"current_stage": "PROCESSING",
"last_error": null,
"total_ms": 538,
"steps": [
{ "stage": "DOWNLOAD", "status": "COMPLETED", "started_at": "2026-09-16T14:06:10.137Z", "completed_at": "2026-09-16T14:06:10.675Z", "duration_ms": 538 },
{ "stage": "PROCESSING", "status": "PROCESSING", "started_at": "2026-09-16T14:06:10.737Z", "completed_at": null, "duration_ms": 9120 },
{ "stage": "ENRICHMENT", "status": "QUEUED", "started_at": null, "completed_at": null, "duration_ms": null },
{ "stage": "COMPLETE", "status": "QUEUED", "started_at": null, "completed_at": null, "duration_ms": null }
]
}