Result callback specification
Update:
The service sends HTTP POST requests to your callbackUrl. Correlate the video task using the outer taskId; one live task can generate multiple media callbacks.
POSTcallbackUrl
Configuration and triggering
Configure callbacks in the console, or override project settings by submitting both a nonblank callbackUrl and callbackSecretKey. An incomplete pair falls back to project settings; there is no partial override. HTTP and HTTPS are supported; prefer HTTPS.
Scenario
Behavior
VOD
Sends results after asynchronous completion or failure.
Default live mode
Incrementally pushes newly detected items. URL streams may also apply an AFK-duration condition. Passing items need not produce callbacks.
Other live project modes
Projects can select accumulated detections or incremental callbacks for all checked media. This is a project setting, not a submission parameter; confirm it during integration.
Disconnection
Uses checkType=stream-closed with a different payload. Explicit stopping does not guarantee a stream-closed callback.
Headers and body
Field
Type
Description
Content-Type
Header
application/json
signature
Header
Lowercase MD5 signature computed as described below.
appId
String
Video moderation project ID.
taskId
String
Video task ID, not a unique callback ID.
checkType
String
video-check / stream-closed
result
String
Serialized inner JSON string, not an object. Verify the signature before parsing this string.
Callback signature
- Parse the outer JSON and use the received non-null field values; current video callbacks contain four string fields.
- Sort field names in ascending ASCII order and concatenate
key1 + value1 + key2 + value2 + ... without separators.
- Append the callback secret, encode as UTF-8, calculate MD5, and output lowercase hexadecimal.
- Compare with the
signature header. Use the original result string obtained by decoding the outer JSON; do not parse, sort, indent, or reserialize the inner JSON before verification.
import hashlib
import hmac
import json
def callback_signature(secret, payload):
values = {k: v for k, v in payload.items() if v is not None}
if not all(isinstance(v, str) for v in values.values()):
raise ValueError("Expected string callback fields")
text = "".join(k + values[k] for k in sorted(values)) + secret
return hashlib.md5(text.encode("utf-8")).hexdigest()
def verify_callback(raw_body, received_signature, secret):
payload = json.loads(raw_body)
expected = callback_signature(secret, payload)
if not hmac.compare_digest(expected, received_signature):
raise ValueError("Invalid callback signature")
return payload, json.loads(payload["result"])
Fixed signature fixture
Use the following fixture for offline signature validation only; the task ID and secret are not production values:
{
"appId": "1001",
"taskId": "cn_00000000-0000-0000-0000-000000000001_1791763200000",
"result": "{\"errorCode\":0,\"taskId\":\"cn_00000000-0000-0000-0000-000000000001_1791763200000\",\"code\":0,\"result\":0,\"videoSpams\":[],\"durationMillis\":10000}",
"checkType": "video-check"
}
secretKey = "example-secret"
signature = "2fe38e34dec78f263138004a93511821"
Live item callback example
{
"appId": "1001",
"taskId": "cn_00000000-0000-0000-0000-000000000001_1791763200000",
"result": "{\"errorCode\":0,\"taskId\":\"cn_00000000-0000-0000-0000-000000000001_1791763200000\",\"code\":2,\"result\":2,\"videoSpams\":[{\"taskId\":\"cn_00000000-0000-0000-0000-000000000001_1791763200000\",\"type\":1,\"result\":2,\"beginTime\":1791763205000,\"endTime\":1791763205000,\"checkDuration\":5000,\"duration\":0,\"url\":\"https://media.example.com/evidence/frame.jpg\",\"tags\":[{\"tag\":130,\"level\":2,\"confidence\":0.92,\"wordList\":[\"matched screen text\"]}],\"liveStreamId\":\"publisher-10001\",\"liveUserId\":\"publisher-10001\",\"ocr\":[\"matched screen text\"]},{\"taskId\":\"cn_00000000-0000-0000-0000-000000000001_1791763200000\",\"type\":2,\"result\":2,\"beginTime\":1791763207000,\"endTime\":1791763208000,\"duration\":5000,\"url\":\"https://media.example.com/evidence/audio.wav\",\"text\":\"matched content\",\"audioText\":\"Normal introduction, matched content, normal ending.\",\"speaker\":\"speaker-1\",\"tags\":[{\"tag\":130,\"level\":2,\"confidence\":0.92,\"wordList\":[\"matched content\"]}],\"liveStreamId\":\"publisher-10002\",\"liveUserId\":\"publisher-10002\",\"checkDuration\":7000}],\"liveProvider\":\"trtc\",\"liveRoomId\":\"room-10001\",\"extra\":{\"bizId\":\"rtc-10001\"},\"pullStreamSuccess\":true}",
"checkType": "video-check"
}
For VOD callback fields, see videoSpams in the VOD result API. For live callback fields, see videoSpams in the live result API. The two item structures are defined separately. Within each type, queries and callbacks share a field model, but item scope, optional fields, and code context differ. A live item failure with code=1 does not mean the whole live task ended.
Live ocr/audioText, tags[].confidence/wordList, and RTC speaker follow the live evidence contract. Images and audio can belong to different publishers; read liveUserId on each item and never use speaker as the publisher ID. Passing-segment transcripts are available only with the project’s incremental mode for all checked media; the default detections-only mode is unchanged.
Stream-closed callback
The fields below belong to the business object decoded from the outer result string. The outer envelope, signature, and acknowledgment are unchanged.
Field
Type
Description
streamUrl
String
Source URL for URL streams; can be a stream ID or room/channel ID for RTC, not necessarily a URL.
streamClosed
Boolean
true
checkDuration
Integer
Elapsed milliseconds from task submission to notification generation; not actual moderated audio duration or billable duration.
taskId
String
New. Moderation task ID, matching the outer field.
liveProvider
String
New, RTC only: zego/agora/trtc.
liveRoomId
String
New, RTC only: room or channel ID.
result
Integer
Optional. Highest cumulative risk level in saved results at summary time: 0 pass, 1 suspicious, 2 reject. Distinct from the outer result string.
spamCount
Integer
Optional. Number of saved detection items in the snapshot, not publishers or all sampled items.
pullStreamSuccess
Boolean
Optional. true when media receipt has been observed, independently of later disconnection; omission does not mean false.
zegoErrorReason
Object
Optional. Present only when a zego task fails; contains errorCode and errorMessage. Omitted on normal termination.
agoraErrorReason
Object
Optional. Present only when a agora task fails; contains errorCode and errorMessage. Omitted on normal termination.
trtcErrorReason
Object
Optional. Present only when a trtc task fails; contains errorCode and errorMessage. Omitted on normal termination.
summaryTime
Integer
New. Unix timestamp in milliseconds when this summary was generated.
summaryComplete
Boolean
New, currently always false. The notification does not wait for all in-flight moderation and does not confirm that every result has arrived.
endReason
String
New. Confirmed termination condition: retry_exhausted/recorder_stopped/unknown; see reason codes.
lastRecordingReason
String
Optional, URL streams only. Latest available recording diagnostic, possibly from an earlier retry; not necessarily the final termination cause. See diagnostic codes.
extra
Object
Submitted opaque data; may be omitted.
{
"appId": "1001",
"taskId": "cn_00000000-0000-0000-0000-000000000001_1791763200000",
"result": "{\"streamUrl\":\"room-10001\",\"streamClosed\":true,\"checkDuration\":60000,\"extra\":{\"bizId\":\"rtc-10001\"},\"taskId\":\"cn_00000000-0000-0000-0000-000000000001_1791763200000\",\"liveProvider\":\"trtc\",\"liveRoomId\":\"room-10001\",\"result\":2,\"spamCount\":2,\"pullStreamSuccess\":true,\"summaryTime\":1791763260000,\"summaryComplete\":false,\"endReason\":\"recorder_stopped\"}",
"checkType": "stream-closed"
}
URL-stream summary example
Only the business object decoded from the outer result string is shown below:
{
"streamUrl": "https://media.example.com/live/stream.m3u8",
"streamClosed": true,
"checkDuration": 60000,
"taskId": "cn_00000000-0000-0000-0000-000000000001_1791763200000",
"result": 2,
"spamCount": 2,
"pullStreamSuccess": true,
"summaryTime": 1791763260000,
"summaryComplete": false,
"endReason": "retry_exhausted",
"lastRecordingReason": "network_timeout"
}
- The summary reflects saved results at generation time. Tail segments and late results can still produce media callbacks and query results; this snapshot is not updated afterward.
- If the summary read fails, times out, or historical summary data is unavailable, the notification is still sent without unconfirmed
result/spamCount/pullStreamSuccess. Unknown results must not be treated as pass.
- Older callbacks may lack these additions. Explicit-stop responses and notification rules are unchanged; adding a summary does not promise an additional completion callback.
Acknowledgment, timeout, and retries
After signature verification and durable acceptance, promptly return HTTP 200 with the JSON below. Perform expensive business processing asynchronously after acceptance.
{
"code": 0
}
New receivers should consistently return code=0. Historical receivers may use compatible forms such as an empty HTTP 200 body; these are not the contract for new integrations. Do not use the internal relay response errorCode=0 as the customer acknowledgment format.
One delivery round consists of an initial attempt and at most 3 retries, with 10 seconds between failures and a 35-second overall timeout. A receiver call has a 5-second timeout, so fewer than 4 attempts may occur. Unacknowledged stream-closed notifications may be retried by later scans; there is no total four-delivery guarantee per event, exactly-once guarantee, or strict ordering guarantee.
Deduplicate by project, task, event type, and media identity/time. Do not deduplicate only by taskId, as that discards later items of the same live task; accumulated mode can repeat previous items. Queries recover saved results, not necessarily every passing-item callback.
RTC failure termination
Room-entry failures, authentication failures, fatal recording errors and unexpected process exits terminate the task as failed. Normal idle timeout, an explicit stop and normal audio segment rotation do not produce provider error objects.
liveProvider identifies the platform. Only its matching trtcErrorReason, agoraErrorReason or zegoErrorReason object is returned. Nested errorCode is the confirmed platform or recorder error code; errorMessage is a safe diagnostic explanation. This nested code differs from the top-level query business code 2107. When detailed provider diagnostics are unavailable, the confirmed SDK error or process exit reason is returned without guessing the cause.
For example, if the detailed cause of TRTC room-entry error -3301 is -100041, the object reports -100041 and explains that Linux access is not enabled. Treat a provider error object as a failure; code=1 or streamClosed=true alone does not indicate successful moderation.
A failed room entry with no media moderation result does not return result=0. Existing detections are retained if failure happens during recording, but do not establish successful moderation of the entire session. The error remains available to later queries and is not cleared by repeated stop requests. Missing error objects in older tasks do not prove either success or failure.
Failure notification uses the existing checkType=stream-closed callback channel. Provider error objects are inside the JSON decoded from the outer result string. Treat such notifications as failures rather than normal completion. The outer envelope, signature and acknowledgment are unchanged. Optional normal-disconnection summary fields are not guaranteed in failure notifications.
Failed callback request example:
{
"appId": "1001",
"taskId": "nx_00000000-0000-0000-0000-000000000001_1792639991214",
"checkType": "stream-closed",
"result": "{\"taskId\":\"nx_00000000-0000-0000-0000-000000000001_1792639991214\",\"streamUrl\":\"room-10001\",\"streamClosed\":true,\"checkDuration\":5000,\"liveProvider\":\"trtc\",\"liveRoomId\":\"room-10001\",\"trtcErrorReason\":{\"errorCode\":-100041,\"errorMessage\":\"TRTC Linux access is not enabled for this application; contact Tencent Cloud to enable it\"}}"
}
The service sends HTTP POST requests to your callbackUrl. Correlate the video task using the outer taskId; one live task can generate multiple media callbacks.
callbackUrlConfiguration and triggering
Configure callbacks in the console, or override project settings by submitting both a nonblank callbackUrl and callbackSecretKey. An incomplete pair falls back to project settings; there is no partial override. HTTP and HTTPS are supported; prefer HTTPS.
| Scenario | Behavior |
|---|---|
| VOD | Sends results after asynchronous completion or failure. |
| Default live mode | Incrementally pushes newly detected items. URL streams may also apply an AFK-duration condition. Passing items need not produce callbacks. |
| Other live project modes | Projects can select accumulated detections or incremental callbacks for all checked media. This is a project setting, not a submission parameter; confirm it during integration. |
| Disconnection | Uses checkType=stream-closed with a different payload. Explicit stopping does not guarantee a stream-closed callback. |
Headers and body
| Field | Type | Description |
|---|---|---|
Content-Type |
Header | application/json |
signature |
Header | Lowercase MD5 signature computed as described below. |
appId |
String | Video moderation project ID. |
taskId |
String | Video task ID, not a unique callback ID. |
checkType |
String | video-check / stream-closed |
result |
String | Serialized inner JSON string, not an object. Verify the signature before parsing this string. |
Callback signature
- Parse the outer JSON and use the received non-null field values; current video callbacks contain four string fields.
- Sort field names in ascending ASCII order and concatenate
key1 + value1 + key2 + value2 + ...without separators. - Append the callback secret, encode as UTF-8, calculate MD5, and output lowercase hexadecimal.
- Compare with the
signatureheader. Use the originalresultstring obtained by decoding the outer JSON; do not parse, sort, indent, or reserialize the inner JSON before verification.
import hashlib
import hmac
import json
def callback_signature(secret, payload):
values = {k: v for k, v in payload.items() if v is not None}
if not all(isinstance(v, str) for v in values.values()):
raise ValueError("Expected string callback fields")
text = "".join(k + values[k] for k in sorted(values)) + secret
return hashlib.md5(text.encode("utf-8")).hexdigest()
def verify_callback(raw_body, received_signature, secret):
payload = json.loads(raw_body)
expected = callback_signature(secret, payload)
if not hmac.compare_digest(expected, received_signature):
raise ValueError("Invalid callback signature")
return payload, json.loads(payload["result"])
Fixed signature fixture
Use the following fixture for offline signature validation only; the task ID and secret are not production values:
{
"appId": "1001",
"taskId": "cn_00000000-0000-0000-0000-000000000001_1791763200000",
"result": "{\"errorCode\":0,\"taskId\":\"cn_00000000-0000-0000-0000-000000000001_1791763200000\",\"code\":0,\"result\":0,\"videoSpams\":[],\"durationMillis\":10000}",
"checkType": "video-check"
}
secretKey = "example-secret"
signature = "2fe38e34dec78f263138004a93511821"
Live item callback example
{
"appId": "1001",
"taskId": "cn_00000000-0000-0000-0000-000000000001_1791763200000",
"result": "{\"errorCode\":0,\"taskId\":\"cn_00000000-0000-0000-0000-000000000001_1791763200000\",\"code\":2,\"result\":2,\"videoSpams\":[{\"taskId\":\"cn_00000000-0000-0000-0000-000000000001_1791763200000\",\"type\":1,\"result\":2,\"beginTime\":1791763205000,\"endTime\":1791763205000,\"checkDuration\":5000,\"duration\":0,\"url\":\"https://media.example.com/evidence/frame.jpg\",\"tags\":[{\"tag\":130,\"level\":2,\"confidence\":0.92,\"wordList\":[\"matched screen text\"]}],\"liveStreamId\":\"publisher-10001\",\"liveUserId\":\"publisher-10001\",\"ocr\":[\"matched screen text\"]},{\"taskId\":\"cn_00000000-0000-0000-0000-000000000001_1791763200000\",\"type\":2,\"result\":2,\"beginTime\":1791763207000,\"endTime\":1791763208000,\"duration\":5000,\"url\":\"https://media.example.com/evidence/audio.wav\",\"text\":\"matched content\",\"audioText\":\"Normal introduction, matched content, normal ending.\",\"speaker\":\"speaker-1\",\"tags\":[{\"tag\":130,\"level\":2,\"confidence\":0.92,\"wordList\":[\"matched content\"]}],\"liveStreamId\":\"publisher-10002\",\"liveUserId\":\"publisher-10002\",\"checkDuration\":7000}],\"liveProvider\":\"trtc\",\"liveRoomId\":\"room-10001\",\"extra\":{\"bizId\":\"rtc-10001\"},\"pullStreamSuccess\":true}",
"checkType": "video-check"
}
For VOD callback fields, see videoSpams in the VOD result API. For live callback fields, see videoSpams in the live result API. The two item structures are defined separately. Within each type, queries and callbacks share a field model, but item scope, optional fields, and code context differ. A live item failure with code=1 does not mean the whole live task ended.
Live ocr/audioText, tags[].confidence/wordList, and RTC speaker follow the live evidence contract. Images and audio can belong to different publishers; read liveUserId on each item and never use speaker as the publisher ID. Passing-segment transcripts are available only with the project’s incremental mode for all checked media; the default detections-only mode is unchanged.
Stream-closed callback
The fields below belong to the business object decoded from the outer result string. The outer envelope, signature, and acknowledgment are unchanged.
| Field | Type | Description |
|---|---|---|
streamUrl |
String | Source URL for URL streams; can be a stream ID or room/channel ID for RTC, not necessarily a URL. |
streamClosed |
Boolean | true |
checkDuration |
Integer | Elapsed milliseconds from task submission to notification generation; not actual moderated audio duration or billable duration. |
taskId |
String | New. Moderation task ID, matching the outer field. |
liveProvider |
String | New, RTC only: zego/agora/trtc. |
liveRoomId |
String | New, RTC only: room or channel ID. |
result |
Integer | Optional. Highest cumulative risk level in saved results at summary time: 0 pass, 1 suspicious, 2 reject. Distinct from the outer result string. |
spamCount |
Integer | Optional. Number of saved detection items in the snapshot, not publishers or all sampled items. |
pullStreamSuccess |
Boolean | Optional. true when media receipt has been observed, independently of later disconnection; omission does not mean false. |
zegoErrorReason |
Object | Optional. Present only when a zego task fails; contains errorCode and errorMessage. Omitted on normal termination. |
agoraErrorReason |
Object | Optional. Present only when a agora task fails; contains errorCode and errorMessage. Omitted on normal termination. |
trtcErrorReason |
Object | Optional. Present only when a trtc task fails; contains errorCode and errorMessage. Omitted on normal termination. |
summaryTime |
Integer | New. Unix timestamp in milliseconds when this summary was generated. |
summaryComplete |
Boolean | New, currently always false. The notification does not wait for all in-flight moderation and does not confirm that every result has arrived. |
endReason |
String | New. Confirmed termination condition: retry_exhausted/recorder_stopped/unknown; see reason codes. |
lastRecordingReason |
String | Optional, URL streams only. Latest available recording diagnostic, possibly from an earlier retry; not necessarily the final termination cause. See diagnostic codes. |
extra |
Object | Submitted opaque data; may be omitted. |
{
"appId": "1001",
"taskId": "cn_00000000-0000-0000-0000-000000000001_1791763200000",
"result": "{\"streamUrl\":\"room-10001\",\"streamClosed\":true,\"checkDuration\":60000,\"extra\":{\"bizId\":\"rtc-10001\"},\"taskId\":\"cn_00000000-0000-0000-0000-000000000001_1791763200000\",\"liveProvider\":\"trtc\",\"liveRoomId\":\"room-10001\",\"result\":2,\"spamCount\":2,\"pullStreamSuccess\":true,\"summaryTime\":1791763260000,\"summaryComplete\":false,\"endReason\":\"recorder_stopped\"}",
"checkType": "stream-closed"
}
URL-stream summary example
Only the business object decoded from the outer result string is shown below:
{
"streamUrl": "https://media.example.com/live/stream.m3u8",
"streamClosed": true,
"checkDuration": 60000,
"taskId": "cn_00000000-0000-0000-0000-000000000001_1791763200000",
"result": 2,
"spamCount": 2,
"pullStreamSuccess": true,
"summaryTime": 1791763260000,
"summaryComplete": false,
"endReason": "retry_exhausted",
"lastRecordingReason": "network_timeout"
}
- The summary reflects saved results at generation time. Tail segments and late results can still produce media callbacks and query results; this snapshot is not updated afterward.
- If the summary read fails, times out, or historical summary data is unavailable, the notification is still sent without unconfirmed
result/spamCount/pullStreamSuccess. Unknown results must not be treated as pass. - Older callbacks may lack these additions. Explicit-stop responses and notification rules are unchanged; adding a summary does not promise an additional completion callback.
Acknowledgment, timeout, and retries
After signature verification and durable acceptance, promptly return HTTP 200 with the JSON below. Perform expensive business processing asynchronously after acceptance.
{
"code": 0
}
New receivers should consistently return code=0. Historical receivers may use compatible forms such as an empty HTTP 200 body; these are not the contract for new integrations. Do not use the internal relay response errorCode=0 as the customer acknowledgment format.
One delivery round consists of an initial attempt and at most 3 retries, with 10 seconds between failures and a 35-second overall timeout. A receiver call has a 5-second timeout, so fewer than 4 attempts may occur. Unacknowledged stream-closed notifications may be retried by later scans; there is no total four-delivery guarantee per event, exactly-once guarantee, or strict ordering guarantee.
Deduplicate by project, task, event type, and media identity/time. Do not deduplicate only by taskId, as that discards later items of the same live task; accumulated mode can repeat previous items. Queries recover saved results, not necessarily every passing-item callback.
RTC failure termination
Room-entry failures, authentication failures, fatal recording errors and unexpected process exits terminate the task as failed. Normal idle timeout, an explicit stop and normal audio segment rotation do not produce provider error objects.
liveProvider identifies the platform. Only its matching trtcErrorReason, agoraErrorReason or zegoErrorReason object is returned. Nested errorCode is the confirmed platform or recorder error code; errorMessage is a safe diagnostic explanation. This nested code differs from the top-level query business code 2107. When detailed provider diagnostics are unavailable, the confirmed SDK error or process exit reason is returned without guessing the cause.
For example, if the detailed cause of TRTC room-entry error -3301 is -100041, the object reports -100041 and explains that Linux access is not enabled. Treat a provider error object as a failure; code=1 or streamClosed=true alone does not indicate successful moderation.
A failed room entry with no media moderation result does not return result=0. Existing detections are retained if failure happens during recording, but do not establish successful moderation of the entire session. The error remains available to later queries and is not cleared by repeated stop requests. Missing error objects in older tasks do not prove either success or failure.
Failure notification uses the existing checkType=stream-closed callback channel. Provider error objects are inside the JSON decoded from the outer result string. Treat such notifications as failures rather than normal completion. The outer envelope, signature and acknowledgment are unchanged. Optional normal-disconnection summary fields are not guaranteed in failure notifications.
Failed callback request example:
{
"appId": "1001",
"taskId": "nx_00000000-0000-0000-0000-000000000001_1792639991214",
"checkType": "stream-closed",
"result": "{\"taskId\":\"nx_00000000-0000-0000-0000-000000000001_1792639991214\",\"streamUrl\":\"room-10001\",\"streamClosed\":true,\"checkDuration\":5000,\"liveProvider\":\"trtc\",\"liveRoomId\":\"room-10001\",\"trtcErrorReason\":{\"errorCode\":-100041,\"errorMessage\":\"TRTC Linux access is not enabled for this application; contact Tencent Cloud to enable it\"}}"
}