AKStream.Next · 文档中心

Receive important AKStream.Next events with third-party Webhooks

Receive important AKStream.Next events with third-party Webhooks

Third-party Webhooks push important device, RTC, MediaServer, stream, and recording facts to external systems. One endpoint equals one delivery plan. Multiple plans have independent secrets, event scopes, timeouts, retries, tests, and delivery history.

Receiver contract first: expose an HTTPS URL reachable from the AKStream.Next node, for example https://events.example.com/akstream/webhook. Your application chooses the path and accepts POST with an UTF-8 JSON body. Verify the signature, dispatch on eventName, process data, then respond with any 2xx (typically 204 No Content). Put the complete URL and a shared secret into the AKStream.Next delivery plan. AKStream.Next calls your endpoint; the /api/v2/system/webhooks/... APIs are for managing delivery plans, not receiving notifications. Reverse proxies must preserve the body and X-AKStream-* headers. See the wire contract and event fields below.

Run a real test for every plan before production. Valid fields do not prove DNS, TLS, WAF, signature verification, or receiver idempotency.

Never put credentials in the URL. Endpoint URLs reject embedded credentials, query strings, and fragments. Existing shared secrets are encrypted at rest and are never returned to the page.

Webhook plans and aggregate delivery state

Quick start

  1. Open System Management → Third-party Webhook. The entry is located after RTC / SFU and before Runtime Environment.
  2. Select New delivery plan and enter a name, purpose, and one HTTPS endpoint.
  3. Generate a plan-specific HMAC secret and store it in the receiver’s secret manager.
  4. Select explained events from the Media, GB28181, RTC, and raw-hook groups.
  5. Save the plan, then send a test to that plan only.
  6. Confirm that its delivery row reaches Completed with the receiver’s real 2xx status.

One endpoint, one independently managed plan

Understand multiple delivery plans

flowchart LR
  device[GB28181 devices] --> outbox[(Business Outbox)]
  rtc[RTC rooms and participants] --> outbox
  media[MediaServer / streams / recordings] --> outbox
  outbox --> fanout[Match enabled plans]
  fanout --> deliveryA[(Delivery A)] --> planA[Alerting endpoint]
  fanout --> deliveryB[(Delivery B)] --> planB[Workflow endpoint]
  fanout --> deliveryC[(Delivery C)] --> planC[Data endpoint]

The same EventId is used across matching plans. Each plan signs with its own secret, so signatures differ. Plan A can complete while Plan B retries; neither result overwrites the other.

Plan fields

Field Required Rule Purpose
Name Yes Unique per cluster, 1–128 characters Identify the receiver and use case
Description No Up to 512 characters Owner, ticket, or integration boundary
Endpoint Yes Absolute HTTP(S), no credentials/query/fragment Exactly one address per plan
HMAC secret On create 16–256 characters Authenticate the raw request body
Enabled Yes Independent per plan Disabling one plan does not affect others
Events Yes At least one exact name or .* prefix Minimize disclosed event data
Allow HTTP No Off by default Controlled isolated networks only
Timeout Yes 2–30 seconds Bound one receiver call
Max attempts Yes 1–10 End automatic retries and require an operator

Leaving the secret field empty while editing preserves the existing secret. Entering a new value rotates only that plan. Archiving cancels unfinished deliveries and preserves historical evidence.

Choose events

Standard events are checkboxes with a trigger, use case, and volume indicator. Operators do not need to type internal names.

Media and recordings

UI label Event name Trigger Recommendation
MediaServer started media.server.started Lifecycle transitions from non-online to online Recommended
MediaServer stopped media.server.stopped Managed stop or lifecycle transition to offline Recommended
MediaServer crashed media.server.crashed The managed process disappears unexpectedly Recommended; alert
MediaServer start failed media.server.start_failed Automatic startup does not remain running Recommended; alert
MediaServer recovered media.server.recovered A previously offline server becomes online again Recommended; resolve alert
Stream online / offline media.stream.online / media.stream.offline Stream registration or deregistration Recommended
Recording file completed recording.file.completed An MP4 segment is finalized Recommended; file name only

MediaServer crash and start-failure events come from managed process and lifecycle state transitions, not from decorative health polling. The recording event exposes the file name but not the server’s absolute path.

GB28181 devices

UI label Event name Trigger
First registration device.gb28181.registered The first successful REGISTER creates the device record
Explicit unregister device.gb28181.unregistered Unregister completes channel and session convergence
Heartbeat timeout device.gb28181.offline Online state changes to offline after the heartbeat window
Device recovered device.gb28181.recovered An offline device successfully REGISTERs or sends Keepalive
Registration authentication failed device.gb28181.auth_failed Digest authentication fails and stale online state is governed

Repeated REGISTER refreshes and Keepalive requests do not generate recovery storms when the device state did not change.

RTC

UI label Event name Trigger
Room created / closed rtc.room.created / rtc.room.closed Room persistence succeeds; close and media release complete
Participant joined / left rtc.participant.joined / rtc.participant.left Secure admission completes; a participant explicitly leaves
Participant timed out rtc.participant.timed_out Heartbeat timeout governance completes
Publication started / stopped / failed rtc.publication.started / .stopped / .failed WHIP or RTMP publication state changes
Room recording failed rtc.recording.failed Room recording start or stop fails

Chat messages, whiteboard strokes, normal heartbeats, and page refreshes are not recommended external events.

Raw MediaServer hooks

zlm.webhook.* includes authentication, keepalive, play, publish, and flow-report hooks. Enable the wildcard only after the receiver has capacity limits and a reviewed data scope. Stable business events above are the normal integration surface.

Legacy snake_case rules such as zlm.webhook.on_stream_changed remain compatible and are normalized when the plan is saved.

Test and recover deliveries

The test action emits system.webhook.test to the selected plan only. This event is created only by an authorized operator, bypasses the normal event-scope check for that one plan, and exercises the real Outbox, signature, timeout, and retry path.

sequenceDiagram
  actor Admin
  participant Web as Webhook page
  participant API as AKStream.Next API
  participant DB as Outbox / Delivery
  participant Worker as Dispatcher
  participant Receiver
  Admin->>Web: Test selected plan
  Web->>API: POST /subscriptions/{id}/test
  API->>DB: Insert targeted test event
  Worker->>DB: Claim Delivery for this plan
  Worker->>Receiver: POST JSON + HMAC headers
  alt Receiver returns 2xx
    Receiver-->>Worker: 204
    Worker->>DB: Completed
  else Timeout or non-2xx
    Receiver-->>Worker: 500 / timeout
    Worker->>DB: Waiting for retry
  end

Per-plan delivery status, errors, and recovery

stateDiagram-v2
  [*] --> Pending
  Pending --> Delivering: conditional claim
  Delivering --> Completed: 2xx
  Delivering --> WaitingRetry: timeout / network / non-2xx
  WaitingRetry --> Delivering: due
  Delivering --> Failed: attempts exhausted
  Failed --> Pending: operator retry
  Pending --> Canceled: plan archived
  WaitingRetry --> Canceled: plan archived

The page never renders the raw Outbox payload. It shows the event name, EventId, attempts, HTTP status, timestamps, and a bounded error summary.

Implement a receiver

AKStream.Next sends POST to the complete URL configured for the plan, with Content-Type: application/json; charset=utf-8. Preserve the raw body, verify the timestamp and HMAC, and only then parse JSON. No GET endpoint or receiver-side polling is required. The field names below match the 1.0.0.158 producer code: data is not uniformly camel-cased.

Request body

{
  "schemaVersion": 1,
  "eventId": "e0b4e218-a6bb-487b-9af2-86e042a87c78",
  "eventName": "device.gb28181.registered",
  "occurredAtUtc": "2026-08-20T08:30:00Z",
  "source": "gb28181",
  "nodeId": "node-01",
  "data": {
    "DeviceId": "34020000001320000001",
    "Name": "East gate camera",
    "Direction": "Server",
    "Status": "Online",
    "previousStatus": null,
    "reason": "REGISTER accepted",
    "occurredAtUtc": "2026-08-20T08:30:00Z"
  }
}

The top-level envelope is generated by the dispatcher. data preserves the property names chosen by each event producer, including uppercase initials. Timestamps and IDs in examples illustrate format, not a fixed real delivery.

Envelope field Type Meaning
schemaVersion integer, currently 1 Parsing version; do not guess the shape of an unsupported version.
eventId UUID string Stable business-event identity across retries and plans; use for idempotency.
eventName string Selects the data schema below; log or ignore unknown names.
occurredAtUtc UTC ISO 8601 string Outbox creation time, not this HTTP attempt's time.
source string Producer module: gb28181, rtc, media, recording, zlm, or outbound-webhook-test.
nodeId string Originating AKStream.Next node; not a MediaServer ID.
data JSON value, object for current standard events Event-specific fields. Do not deserialize all events into one rigid type.

Event-specific data fields

These are the fields actually emitted by the current producers. Route by eventName, preserve exact capitalization, tolerate additional fields, and treat “nullable” as a present JSON property whose value may be null.

Events Fields, types, and meaning
device.gb28181.registered / unregistered / offline / recovered / auth_failed DeviceId string: GB28181 device ID; Name nullable string: device name; Direction: stored device direction; Status string: current status; previousStatus nullable string: previous status; reason string: reason for transition; occurredAtUtc UTC timestamp. Authentication-failure reasons may contain diagnostic text; do not publish them openly.
media.stream.online / offline MediaServerId nullable string: server ID; Vhost, App nullable strings: ZLM virtual host and app; streamId nullable string: stream ID; schema nullable string: protocol, such as rtsp; online boolean; occurredAtUtc UTC timestamp. A stream may generate separate notifications for different protocol tracks.
recording.file.completed MediaServerId, Vhost, App, streamId as above; fileName nullable string: finalized MP4 file name, not an absolute path or download URL; fileSize nullable string: upstream file_size; duration nullable string: upstream time_len; occurredAtUtc UTC timestamp. Use an authorized recording API for lookup/download.
media.server.started / stopped / recovered NodeId string: control node; ZlmNodeId nullable string: ZLM node; mediaServerId string: media server; previousStatus, status strings: before/after; hookName string: triggering Hook; occurredAtUtc UTC timestamp.
media.server.crashed NodeId, zlmNodeId: node IDs; previousPid integer: previous process ID; LastAction nullable string: last managed action; reason = managed-process-exited-unexpectedly; occurredAtUtc UTC timestamp.
media.server.start_failed NodeId, zlmNodeId, LastAction as above; reason = managed-start-failed; occurredAtUtc UTC timestamp. Full failure logs are not in this notification.
rtc.room.created RoomId string: meeting ID; Name, RoomType, MediaServerId, Status: room properties; ScheduledStartAtUtc, ScheduledEndAtUtc: nullable schedule timestamps. This event uses the room entity ID as stable eventId.
rtc.room.closed; rtc.participant.joined / left / timed_out; rtc.publication.started / stopped / failed; rtc.recording.failed Shared fields: RoomId string: meeting ID; Sequence integer: room-event sequence; EventType string: exact internal transition; ParticipantId string: related actor (possibly system); TargetParticipantId nullable string; occurredAtUtc UTC timestamp. These notifications do not contain device ID, participant name, chat/whiteboard content, media URL, or recording URL. Query authorized business APIs when those details are needed.
system.webhook.test actor string: test initiator; message string: test message; testedAtUtc UTC timestamp. Delivered only to the selected plan.
zlm.webhook.<HookName> HookName string: normalized ZLM Hook name; MediaServerId, App, Stream nullable strings; RawJson string containing the upstream Hook JSON, requiring a second JSON parse; ReceivedAt offset timestamp. This is not a stable business schema. The outer redaction pass does not parse or redact inside the RawJson string; subscribe cautiously, especially to authentication/play Hooks.

For example, a participant-joined notification carries this data object:

{"RoomId":"room-demo-001","Sequence":18,"EventType":"participant-joined","ParticipantId":"p-demo-001","TargetParticipantId":null,"occurredAtUtc":"2026-09-28T02:30:00Z"}

For a raw Hook, the wrapping is "data":{"HookName":"OnPlay","RawJson":"{\"app\":\"live\"}"}. Only consumers that subscribe to raw Hooks should parse RawJson a second time. Structured JSON properties named secret, password, passwd, token, accessToken, refreshToken, authorization, cookie, licenseCode, requestCode, apiKey, or params are replaced with [REDACTED] before sending; this does not guarantee redaction of an embedded RawJson string.

Headers and signature

Header Meaning
X-AKStream-Event Event name
X-AKStream-Event-Id Stable EventId across plans and retries
X-AKStream-Subscription-Id Current delivery-plan ID
X-AKStream-Timestamp Unix seconds used in the signature
X-AKStream-Signature v1=<lowercase HMAC-SHA256 hex>
Idempotency-Key Equal to EventId

Illustrative HTTP request (the signature is a placeholder; compute it from the actual body):

POST /akstream/webhook HTTP/1.1
Host: events.example.com
Content-Type: application/json; charset=utf-8
X-AKStream-Event: rtc.participant.joined
X-AKStream-Event-Id: 96f8f29e-a6e2-46c4-bb32-99a643043f2a
X-AKStream-Subscription-Id: 538fb28e-7739-4e44-b747-df375e9228d7
X-AKStream-Timestamp: 1790562600
X-AKStream-Signature: v1=<64-character lowercase hex digest>
Idempotency-Key: 96f8f29e-a6e2-46c4-bb32-99a643043f2a

{"schemaVersion":1,"eventId":"96f8f29e-a6e2-46c4-bb32-99a643043f2a","eventName":"rtc.participant.joined","occurredAtUtc":"2026-09-28T02:30:00Z","source":"rtc","nodeId":"node-01","data":{"RoomId":"room-demo-001","Sequence":18,"EventType":"participant-joined","ParticipantId":"p-demo-001","TargetParticipantId":null,"occurredAtUtc":"2026-09-28T02:30:00Z"}}

The signature input is:

<timestamp>.<raw HTTP request body>

The dot is a literal .; do not add angle brackets, whitespace, or a newline. Use the current plan's secret as the UTF-8 HMAC-SHA256 key, encode the digest as lowercase hex, and prefix it with v1=. Verify the received body before parsing/reserializing it and compare digests in constant time. Also check a bounded timestamp window (for example ±300 seconds), schemaVersion, and that event name/ID headers agree with the body. Every retry gets a fresh timestamp/signature but retains eventId.

Return any 2xx (200, 201, 202, or 204) only after durable business processing. The response body has no required schema and is not parsed for business data. Network errors, timeout, and non-2xx cause plan-specific retries (2–30 second per-attempt timeout, 1–10 attempts, backoff starting around 5 seconds and capped at 300 seconds). Never return 200 for a processing failure. A lost response can produce a duplicate after a successful commit: put the event-ID uniqueness record and business change in the same transaction. Use eventId for one plan or (subscriptionId, eventId) if several plans feed one receiver. Use 401/403 for failed verification, 405 for a wrong method, and 5xx for temporary failures.

Runnable Node.js example

Store the secret outside source code and shell history used for routine operations:

export WEBHOOK_SECRET='the plan secret copied from AKStream.Next'
node receiver.mjs
import crypto from 'node:crypto'
import http from 'node:http'

const secret = process.env.WEBHOOK_SECRET
if (!secret) throw new Error('WEBHOOK_SECRET is required')
const seen = new Set()

http.createServer((request, response) => {
  if (request.method !== 'POST' || request.url !== '/akstream/webhook') {
    response.writeHead(405).end()
    return
  }
  const chunks = []
  request.on('data', chunk => chunks.push(chunk))
  request.on('end', () => {
    try {
      const rawBody = Buffer.concat(chunks)
      const timestamp = String(request.headers['x-akstream-timestamp'] || '')
      const supplied = String(request.headers['x-akstream-signature'] || '').replace(/^v1=/, '')
      if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) throw new Error('stale timestamp')
      const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody.toString('utf8')}`, 'utf8').digest('hex')
      const left = Buffer.from(supplied, 'hex')
      const right = Buffer.from(expected, 'hex')
      if (left.length !== right.length || !crypto.timingSafeEqual(left, right)) throw new Error('invalid signature')
      const event = JSON.parse(rawBody.toString('utf8'))
      if (event.schemaVersion !== 1 || event.eventId !== request.headers['x-akstream-event-id']
          || event.eventName !== request.headers['x-akstream-event']) throw new Error('invalid event envelope')
      if (!seen.has(event.eventId)) { seen.add(event.eventId); console.log(event.eventName, event.data) }
      response.writeHead(204).end()
    } catch (error) {
      console.error(error.message)
      response.writeHead(401).end()
    }
  })
}).listen(9080, '127.0.0.1')

The 127.0.0.1 listener and in-memory Set are for same-machine testing only. On a separate host, configure the listener, reverse proxy, and plan URL so that the same HTTPS path is reachable. Limit body size in production and commit idempotency with the business transaction. The receiver does not need an AKStream.Next administrator token; its authentication credential is the plan's HMAC secret.

API reference

Method Path Permission Purpose
GET /api/v2/system/webhooks/status system.view Aggregate plan and delivery state
GET /api/v2/system/webhooks/catalog system.view Grouped event catalog
GET /api/v2/system/webhooks/subscriptions system.view List redacted plans
POST /api/v2/system/webhooks/subscriptions system.config.manage Create a plan
PUT /api/v2/system/webhooks/subscriptions/{id} system.config.manage Update a plan and optionally rotate its secret
POST /api/v2/system/webhooks/subscriptions/{id}/archive system.config.manage Archive while preserving history
POST /api/v2/system/webhooks/subscriptions/{id}/test system.config.manage Test only the selected plan
GET /api/v2/system/webhooks/subscriptions/{id}/deliveries system.view Read per-plan delivery history
POST /api/v2/system/webhooks/deliveries/{id}/retry system.config.manage Retry one failed delivery

Upgrade and migration

On the first start after upgrading, the legacy single endpoint under AkStream:OutboundWebhook is migrated to a plan named “Default delivery plan”. Its secret is immediately re-encrypted with the current installation's data-protection key, and migration logs never print the endpoint credential or secret.

Migration runs once. As soon as any active or archived plan exists, startup will not create another default plan. The legacy configuration remains only for the compatibility window and is no longer authoritative for multi-plan delivery. Back up the new tables before rolling back because older versions cannot represent multiple plans without data loss.

Troubleshooting

Symptom Check
Test button disabled Select an enabled plan with a configured secret
Waiting for retry HTTP status → DNS → TLS chain → WAF/proxy → receiver logs
Receiver returns 401 Raw-body verification → timestamp window → correct plan secret
Duplicate event Enforce receiver idempotency by EventId
Plan A succeeds, B fails Expected isolation; retry B only
No GB28181 event Confirm a real device state transition occurred
No RTC event Confirm secure join/leave or room lifecycle completed
No MediaServer crash event Check managed startup task, lifecycle hooks, Outbox, and selected events
Raw payload is not shown in the page This is intentional; use controlled server logs and business-entity APIs

Start with one test plan and recommended events. Add more plans only after each receiver passes its own real test.