Set up webhook notifications, verify their signatures, and test your integration.
A webhook is an HTTP request that notifies your server when an event occurs. Chunkify sends a JSON payload to your webhook URL for each event you subscribe to.
For example, the asset.playback_ready event tells your application when the HLS stream becomes playable.
The video asset may still be preparing, with its poster and timeline previews not yet available.
asset.ready means playback, the poster, and timeline previews are all ready. Subscribe to it as well if your application needs those additional URLs.
Set up a webhook in the dashboard
In your Project, open Developers → Webhooks → Setup webhook.
Enter your server's Webhook URL, choose an event group such as Assets, and select the events your application needs. Then select Create webhook.
Copy the Webhook secret from the Webhooks page. Your server uses it to verify incoming notifications.
The webhook is enabled by default. Select asset.playback_ready to receive a notification as soon as the HLS stream is playable, or asset.ready when you also need the poster and timeline previews.
Set up a webhook with the SDK
Set events to the types your application needs. This example subscribes to asset.playback_ready so your server is notified when the HLS stream becomes playable.
importChunkifyfrom'@chunkify/chunkify';constclient=newChunkify({projectAccessToken:'My Project Access Token',});// Create a webhook
constwebhook=awaitclient.webhooks.create({url:'https://example.com/webhook',events:['asset.playback_ready'],});
fromchunkifyimportChunkifyclient=Chunkify(project_access_token="My Project Access Token",)# Create a webhookwebhook=client.webhooks.create(url="https://example.com/webhook",events=["asset.playback_ready"],)
useChunkify\Client;$client=newClient(projectAccessToken:'My Project Access Token',);// Create a webhook
$webhook=$client->webhooks->create(url:'https://example.com/webhook',events:['asset.playback_ready'],);
If you omit events, Chunkify subscribes the webhook to all supported events except asset.playback_ready. Subscribe to it explicitly to receive playback notifications.
Event history
Chunkify records every supported event, even when no webhook is configured.
For each subscribed webhook, Chunkify creates a notification and records its delivery attempts. Creating a webhook does not send past events.
Verify the signature before processing a notification. Use your Webhook secret, the raw request body, and the webhook-id, webhook-timestamp, and webhook-signature headers.
The raw request body is the JSON exactly as received, before your framework parses it. Parsing and re-encoding it can change the bytes and make signature verification fail.
These examples verify the signature and handle asset.playback_ready. Replace the example secret with your Project's Webhook secret.
// This example uses Express for simplicity
importexpressfrom'express';importtype{RequestHandler}from'express';importChunkifyfrom'@chunkify/chunkify';constapp=express();constPORT=8787;// Use raw body for signature verification
app.use(express.raw({type:'application/json'}));constwebhookHandler: RequestHandler=(req,res)=>{constheaders: Record<string,string>={'webhook-signature':req.header('webhook-signature')||'','webhook-id':req.header('webhook-id')||'','webhook-timestamp':req.header('webhook-timestamp')||'',};constclient=newChunkify({projectAccessToken:'My Project Access Token',webhookKey:'your secret key',});// Unwrap the webhook will also verify the signature using the secret key
letnotification: Chunkify.UnwrapWebhookEvent;try{notification=client.webhooks.unwrap(req.body.toString(),{headers});}catch(error){console.error('Error unwrapping webhook:',error);res.status(400).send('Error unwrapping webhook');return;}constevent=notification.event;console.log('Webhook event received:',event);switch(event){case'asset.playback_ready':constpayload=notification.dataasChunkify.UnwrapWebhookEvent.NotificationPayloadAsset;console.log(`Video asset ${payload.asset.id} can play`);break;// Complete with other cases for other event type as needed.
default:console.log(`Unexpected event type: ${event}`);}res.status(200).send('OK');};app.post('/webhook',webhookHandler);app.listen(PORT,()=>{console.log(`Test server listening on http://localhost:${PORT}`);});
"""Webhook server using Python's built-in http.server."""fromhttp.serverimportHTTPServer,BaseHTTPRequestHandlerimportjsonfromchunkifyimportChunkifyfromchunkify.typesimportUnwrapWebhookEventPORT=8787# Initialize client with webhook key for signature verificationclient=Chunkify(webhook_key="your secret key",)classWebhookHandler(BaseHTTPRequestHandler):"""Handle webhook requests."""defdo_POST(self):# noqa: N802"""Handle POST requests to /webhook."""ifself.path!="/webhook":self.send_response(404)self.end_headers()return# Read raw bodycontent_length=int(self.headers.get("Content-Length",0))payload=self.rfile.read(content_length).decode("utf-8")# Extract webhook headersheaders={"webhook-signature":self.headers.get("webhook-signature",""),"webhook-id":self.headers.get("webhook-id",""),"webhook-timestamp":self.headers.get("webhook-timestamp",""),}# Unwrap the webhook (also verifies the signature)try:notification:UnwrapWebhookEvent=client.webhooks.unwrap(payload,headers=headers)exceptExceptionaserror:print(f"Error unwrapping webhook: {error}")self.send_response(400)self.send_header("Content-Type","application/json")self.end_headers()self.wfile.write(json.dumps({"error":"Error unwrapping webhook"}).encode())return# Get the event typeevent=notification.eventprint(f"Webhook event received: {event}")# Handle different event typesifevent=="asset.playback_ready":print(f"Video asset {notification.data.asset.id} can play")else:print(f"Unexpected event type: {event}")# Send success responseself.send_response(200)self.send_header("Content-Type","application/json")self.end_headers()self.wfile.write(json.dumps({"status":"OK"}).encode())deflog_message(self,format,*args):# noqa: N802"""Suppress default logging."""passif__name__=="__main__":server=HTTPServer(("localhost",PORT),WebhookHandler)print(f"Test server listening on http://localhost:{PORT}")try:server.serve_forever()exceptKeyboardInterrupt:print("\nShutting down server...")server.shutdown()
useChunkify\Client;useChunkify\Core\Exceptions\WebhookException;$client=newClient(webhookKey:'your secret key',);$body=file_get_contents('php://input')?:'';$headers=array_change_key_case(getallheaders()?:[],CASE_LOWER);try{// Unwrapping the webhook also verifies its signature
$notification=$client->webhooks->unwrap($body,['webhook-signature'=>$headers['webhook-signature']??'','webhook-id'=>$headers['webhook-id']??'','webhook-timestamp'=>$headers['webhook-timestamp']??'',],);switch($notification->event){case'asset.playback_ready':printf("Video asset %s can play\n",$notification->data->asset->id);break;default:error_log("Unexpected event type: {$notification->event}");}http_response_code(200);echo'OK';}catch(WebhookException$error){error_log('Error unwrapping webhook: '.$error->getMessage());http_response_code(400);echo'Error unwrapping webhook';}
packagemainimport("fmt""io""net/http""github.com/chunkifydev/chunkify-go""github.com/chunkifydev/chunkify-go/option")funchandleWebhook(whttp.ResponseWriter,r*http.Request){client:=chunkify.NewClient(option.WithWebhookKey("your secret key"),)// Read the request body
body,err:=io.ReadAll(r.Body)iferr!=nil{http.Error(w,"Error reading request body",http.StatusBadRequest)return}// Unwrap the notification payload
notification,err:=client.Webhooks.Unwrap(body,r.Header)iferr!=nil{http.Error(w,"Error unwrapping webhook",http.StatusBadRequest)return}switchnotification.Event{casechunkify.UnwrapWebhookEventEventAssetPlaybackReady:data:=notification.Data.AsUnwrapWebhookEventDataNotificationPayloadAsset()fmt.Printf("Video asset %s can play\n",data.Asset.ID)default:fmt.Printf("unexpected event type: %s\n",notification.Event)}}funcmain(){// Register the webhook handler
http.HandleFunc("/webhook",handleWebhook)// Start the server
port:=":8787"fmt.Printf("Webhook server listening on http://localhost%s/webhook\n",port)iferr:=http.ListenAndServe(port,nil);err!=nil{fmt.Printf("Server failed to start: %v\n",err)}}
Return a 2xx response after accepting a notification. Chunkify retries failed deliveries, so make your handler safe to run more than once for the same event.
You can send a notification to a specific webhook to retry a failed delivery or test your handler. When a matching event is retained, Chunkify uses its saved payload.
Provide webhook_id, event, and the object_id of the resource that produced the event. For asset.playback_ready, use the video asset ID.
Notification attempts are limited to five for the same event and webhook. See Create a notification for the full request and response.
# Overview
> Set up webhook notifications, verify their signatures, and test your integration.
A webhook is an HTTP request that notifies your server when an event occurs. Chunkify sends a JSON payload to your webhook URL for each event you subscribe to.
For example, the `asset.playback_ready` event tells your application when the HLS stream becomes playable.
The video asset may still be `preparing`, with its poster and timeline previews not yet available.
`asset.ready` means playback, the poster, and timeline previews are all ready. Subscribe to it as well if your application needs those additional URLs.
## Set up a webhook in the dashboard
1. In your Project, open **Developers → Webhooks → Setup webhook**.
2. Enter your server's **Webhook URL**, choose an event group such as **Assets**, and select the events your application needs. Then select **Create webhook**.
3. Copy the **Webhook secret** from the Webhooks page. Your server uses it to verify incoming notifications.
<a href="/docs/assets/images/webhooks/setup-webhook.png"><img src="/docs/assets/images/webhooks/setup-webhook.png" alt="Webhook setup dialog with Asset events selected and delivery enabled" width="600"/></a>
The webhook is enabled by default. Select `asset.playback_ready` to receive a notification as soon as the HLS stream is playable, or `asset.ready` when you also need the poster and timeline previews.
## Set up a webhook with the SDK
Set `events` to the types your application needs. This example subscribes to `asset.playback_ready` so your server is notified when the HLS stream becomes playable.
<codegroup>
<codeblock lang="Typescript">
```typescript
import Chunkify from '@chunkify/chunkify';
const client = new Chunkify({
projectAccessToken: 'My Project Access Token',
});
// Create a webhook
const webhook = await client.webhooks.create({
url: 'https://example.com/webhook',
events: ['asset.playback_ready'],
});
```
</codeblock>
<codeblock lang="Python">
```python
from chunkify import Chunkify
client = Chunkify(
project_access_token="My Project Access Token",
)
# Create a webhook
webhook = client.webhooks.create(
url="https://example.com/webhook",
events=["asset.playback_ready"],
)
```
</codeblock>
<codeblock lang="PHP">
```php
use Chunkify\Client;
$client = new Client(
projectAccessToken: 'My Project Access Token',
);
// Create a webhook
$webhook = $client->webhooks->create(
url: 'https://example.com/webhook',
events: ['asset.playback_ready'],
);
```
</codeblock>
<codeblock lang="Go">
```go Go
package main
import (
"context"
"fmt"
"github.com/chunkifydev/chunkify-go"
"github.com/chunkifydev/chunkify-go/option"
)
func main() {
client := chunkify.NewClient(
option.WithProjectAccessToken("My Project Access Token"),
)
webhook, err := client.Webhooks.New(
context.Background(),
chunkify.WebhookNewParams{
URL: "https://example.com/webhook",
Events: []string{"asset.playback_ready"},
},
)
if err != nil {
panic(err)
}
fmt.Println(webhook.ID)
}
```
</codeblock>
</codegroup>
<note>
If you omit `events`, Chunkify subscribes the webhook to all supported events except `asset.playback_ready`. Subscribe to it explicitly to receive playback notifications.
</note>
## Event history
Chunkify records every [supported event](/docs/integration/events#supported-events), even when no webhook is configured.
For each subscribed webhook, Chunkify creates a notification and records its delivery attempts. Creating a webhook does not send past events.
## Verify webhook signature
Chunkify signs notifications according to the [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md).
Verify the signature before processing a notification. Use your Webhook secret, the raw request body, and the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers.
The raw request body is the JSON exactly as received, before your framework parses it. Parsing and re-encoding it can change the bytes and make signature verification fail.
These examples verify the signature and handle `asset.playback_ready`. Replace the example secret with your Project's Webhook secret.
<codegroup>
<codeblock lang="Typescript">
```typescript Typescript
// This example uses Express for simplicity
import express from 'express';
import type { RequestHandler } from 'express';
import Chunkify from '@chunkify/chunkify';
const app = express();
const PORT = 8787;
// Use raw body for signature verification
app.use(express.raw({ type: 'application/json' }));
const webhookHandler: RequestHandler = (req, res) => {
const headers: Record<string, string> = {
'webhook-signature': req.header('webhook-signature') || '',
'webhook-id': req.header('webhook-id') || '',
'webhook-timestamp': req.header('webhook-timestamp') || '',
};
const client = new Chunkify({
projectAccessToken: 'My Project Access Token',
webhookKey: 'your secret key',
});
// Unwrap the webhook will also verify the signature using the secret key
let notification: Chunkify.UnwrapWebhookEvent;
try {
notification = client.webhooks.unwrap(req.body.toString(), { headers });
} catch (error) {
console.error('Error unwrapping webhook:', error);
res.status(400).send('Error unwrapping webhook');
return;
}
const event = notification.event;
console.log('Webhook event received:', event);
switch (event) {
case 'asset.playback_ready':
const payload = notification.data as Chunkify.UnwrapWebhookEvent.NotificationPayloadAsset;
console.log(`Video asset ${payload.asset.id} can play`);
break;
// Complete with other cases for other event type as needed.
default:
console.log(`Unexpected event type: ${event}`);
}
res.status(200).send('OK');
};
app.post('/webhook', webhookHandler);
app.listen(PORT, () => {
console.log(`Test server listening on http://localhost:${PORT}`);
});
```
</codeblock>
<codeblock lang="Python">
```python Python
"""Webhook server using Python's built-in http.server."""
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
from chunkify import Chunkify
from chunkify.types import UnwrapWebhookEvent
PORT = 8787
# Initialize client with webhook key for signature verification
client = Chunkify(
webhook_key="your secret key",
)
class WebhookHandler(BaseHTTPRequestHandler):
"""Handle webhook requests."""
def do_POST(self): # noqa: N802
"""Handle POST requests to /webhook."""
if self.path != "/webhook":
self.send_response(404)
self.end_headers()
return
# Read raw body
content_length = int(self.headers.get("Content-Length", 0))
payload = self.rfile.read(content_length).decode("utf-8")
# Extract webhook headers
headers = {
"webhook-signature": self.headers.get("webhook-signature", ""),
"webhook-id": self.headers.get("webhook-id", ""),
"webhook-timestamp": self.headers.get("webhook-timestamp", ""),
}
# Unwrap the webhook (also verifies the signature)
try:
notification: UnwrapWebhookEvent = client.webhooks.unwrap(payload, headers=headers)
except Exception as error:
print(f"Error unwrapping webhook: {error}")
self.send_response(400)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"error": "Error unwrapping webhook"}).encode())
return
# Get the event type
event = notification.event
print(f"Webhook event received: {event}")
# Handle different event types
if event == "asset.playback_ready":
print(f"Video asset {notification.data.asset.id} can play")
else:
print(f"Unexpected event type: {event}")
# Send success response
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"status": "OK"}).encode())
def log_message(self, format, *args): # noqa: N802
"""Suppress default logging."""
pass
if __name__ == "__main__":
server = HTTPServer(("localhost", PORT), WebhookHandler)
print(f"Test server listening on http://localhost:{PORT}")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nShutting down server...")
server.shutdown()
```
</codeblock>
<codeblock lang="PHP">
```php PHP
use Chunkify\Client;
use Chunkify\Core\Exceptions\WebhookException;
$client = new Client(
webhookKey: 'your secret key',
);
$body = file_get_contents('php://input') ?: '';
$headers = array_change_key_case(getallheaders() ?: [], CASE_LOWER);
try {
// Unwrapping the webhook also verifies its signature
$notification = $client->webhooks->unwrap(
$body,
[
'webhook-signature' => $headers['webhook-signature'] ?? '',
'webhook-id' => $headers['webhook-id'] ?? '',
'webhook-timestamp' => $headers['webhook-timestamp'] ?? '',
],
);
switch ($notification->event) {
case 'asset.playback_ready':
printf("Video asset %s can play\n", $notification->data->asset->id);
break;
default:
error_log("Unexpected event type: {$notification->event}");
}
http_response_code(200);
echo 'OK';
} catch (WebhookException $error) {
error_log('Error unwrapping webhook: ' . $error->getMessage());
http_response_code(400);
echo 'Error unwrapping webhook';
}
```
</codeblock>
<codeblock lang="Go">
```go Go
package main
import (
"fmt"
"io"
"net/http"
"github.com/chunkifydev/chunkify-go"
"github.com/chunkifydev/chunkify-go/option"
)
func handleWebhook(w http.ResponseWriter, r *http.Request) {
client := chunkify.NewClient(
option.WithWebhookKey("your secret key"),
)
// Read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusBadRequest)
return
}
// Unwrap the notification payload
notification, err := client.Webhooks.Unwrap(body, r.Header)
if err != nil {
http.Error(w, "Error unwrapping webhook", http.StatusBadRequest)
return
}
switch notification.Event {
case chunkify.UnwrapWebhookEventEventAssetPlaybackReady:
data := notification.Data.AsUnwrapWebhookEventDataNotificationPayloadAsset()
fmt.Printf("Video asset %s can play\n", data.Asset.ID)
default:
fmt.Printf("unexpected event type: %s\n", notification.Event)
}
}
func main() {
// Register the webhook handler
http.HandleFunc("/webhook", handleWebhook)
// Start the server
port := ":8787"
fmt.Printf("Webhook server listening on http://localhost%s/webhook\n", port)
if err := http.ListenAndServe(port, nil); err != nil {
fmt.Printf("Server failed to start: %v\n", err)
}
}
```
</codeblock>
</codegroup>
Return a `2xx` response after accepting a notification. Chunkify retries failed deliveries, so make your handler safe to run more than once for the same event.
## Testing your webhook integration locally
Use the Chunkify CLI to forward notifications to your local server. See [Receive webhook notifications locally](/docs/cli/integration#receiving-webhooks-notifications-locally) for the setup steps.
## Manually send a notification
You can send a notification to a specific webhook to retry a failed delivery or test your handler. When a matching event is retained, Chunkify uses its saved payload.
Provide `webhook_id`, `event`, and the `object_id` of the resource that produced the event. For `asset.playback_ready`, use the video asset ID.
Notification attempts are limited to five for the same event and webhook. See [Create a notification](/docs/api-reference/notifications/create-a-notification) for the full request and response.
<codegroup>
<codeblock lang="Typescript">
```typescript
import Chunkify from '@chunkify/chunkify';
const client = new Chunkify({
projectAccessToken: 'My Project Access Token',
});
const notification = await client.notifications.create({
event: 'asset.playback_ready',
object_id: 'ast_2G6MJiNz71bHQGNzGwKx5cJwPFS',
webhook_id: 'wh_2G6MJiNz71bHQGNzGwKx5cJwPFS',
});
```
</codeblock>
<codeblock lang="Python">
```python
from chunkify import Chunkify
client = Chunkify(
project_access_token="My Project Access Token",
)
notification = client.notifications.create(
event="asset.playback_ready",
object_id="ast_2G6MJiNz71bHQGNzGwKx5cJwPFS",
webhook_id="wh_2G6MJiNz71bHQGNzGwKx5cJwPFS",
)
```
</codeblock>
<codeblock lang="PHP">
```php
<?php
use Chunkify\Client;
$client = new Client(
projectAccessToken: 'My Project Access Token',
);
$notification = $client->notifications->create(
event: 'asset.playback_ready',
objectID: 'ast_2G6MJiNz71bHQGNzGwKx5cJwPFS',
webhookID: 'wh_2G6MJiNz71bHQGNzGwKx5cJwPFS',
);
```
</codeblock>
<codeblock lang="Go">
```go
package main
import (
"context"
"fmt"
"log"
"github.com/chunkifydev/chunkify-go"
"github.com/chunkifydev/chunkify-go/option"
)
func main() {
client := chunkify.NewClient(
option.WithProjectAccessToken("My Project Access Token"),
)
notification, err := client.Notifications.New(
context.TODO(),
chunkify.NotificationNewParams{
Event: chunkify.NotificationNewParamsEventAssetPlaybackReady,
ObjectID: "ast_2G6MJiNz71bHQGNzGwKx5cJwPFS",
WebhookID: "wh_2G6MJiNz71bHQGNzGwKx5cJwPFS",
},
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", notification)
}
```
</codeblock>
</codegroup>