Webhooks
Get notified on your server when sessions start and end, and when companions are created, updated, or deleted
A webhook sends an HTTP POST to your server whenever something happens in your project — a session starts or ends, or a companion is created, updated, or deleted. Use webhooks to react in real time instead of polling the API: record usage as sessions close, or update your own records when a companion finishes generating.
Creating a webhook
Set up an endpoint in the dashboard
Events
The five events you can subscribe to
Payload format
The envelope every event shares
Session events
session.started and session.closed
Companion events
companion.created, updated, and deleted
Verifying requests
Confirm a request came from Napster
Delivery history
Inspect every delivery and its response
Creating a webhook
You create webhooks in the developer dashboard, per project.
Open the Webhooks page
In the dashboard, select your project and go to Webhooks, then click New webhook.
Configure the webhook
| Field | Description |
|---|---|
| Endpoint URL | The URL that receives the POST requests. Must be an https:// URL that's reachable from the internet. |
| Events | The events that trigger this webhook. See Events. |
| Status | When enabled, the webhook receives events. Disable it to pause deliveries without deleting it. |
Save the signing secret
Click Create webhook. The confirmation page shows the webhook's ID and its signing secret — a base64 string such as q3Zr8WmN1xTbV0yLc7sKd2Pf9uHaE4jRgO6nYwB5tQA=. Copy the secret and store it securely, for example in an environment variable on your server. You use it to verify that requests come from Napster.
The signing secret is shown only once. If you lose it, delete the webhook and create a new one.
A project can have one enabled webhook at a time. You need the organization Admin role to create, edit, or delete webhooks.
Events
| Event | When it's sent |
|---|---|
session.started | A session connected and started. |
session.closed | A session that started has ended. Sessions that fail before starting don't send this event. |
companion.created | A companion was created. Its status is pending while the avatar is generated. |
companion.updated | A companion changed, including each status change while its avatar is generated. |
companion.deleted | A companion was deleted. |
Payload format
Every event shares the same envelope. The event itself is in data, under session or companion:
{
"eventId": "67d67303ec4f8672fefb704952206d48",
"eventType": "companion.created",
"timestamp": 1790685304,
"organizationId": "org_abc123",
"projectId": "proj_abc123",
"data": {
"companion": { }
}
}| Field | Description |
|---|---|
eventId | A unique ID for the event. Use it to detect duplicate deliveries. |
eventType | The event name, for example session.closed. |
timestamp | When the event happened, as a Unix timestamp in seconds. |
organizationId | The organization the event belongs to. |
projectId | The project the event belongs to. |
data | The event payload — data.session for session events, data.companion for companion events. |
Session events
session.started and session.closed describe the session in data.session:
{
"eventId": "9f1c2e7a-4b3d-4f6e-8a1b-2c3d4e5f6a7b",
"eventType": "session.closed",
"timestamp": 1790690142,
"organizationId": "org_abc123",
"projectId": "proj_abc123",
"data": {
"session": {
"id": "sess_xyz789",
"type": "webrtc",
"companionId": "comp_abc123",
"externalClientId": "user_42",
"createdAt": 1790689801,
"startedAt": 1790689803,
"closedAt": 1790690142,
"reason": "connection_closed"
}
}
}| Field | Description |
|---|---|
id | The session ID. Use it with GET /public/sessions/{sessionId} to fetch the full session, including cost and transcript. |
type | The channel: webrtc, websocket, voip, or sip. Kiosk sessions report websocket. |
companionId | The companion the session used. |
externalClientId | The externalClientId you passed when opening the session, or null. |
createdAt / startedAt | When the session was created and when it started, as Unix timestamps in seconds. |
closedAt | When the session ended. session.closed only. |
reason | Why the session ended. session.closed only. See Close reasons. |
session.closed doesn't include the session's cost. Billing is finalized as the session closes — fetch the session with its id to read cost.
Companion events
companion.created, companion.updated, and companion.deleted describe the companion in data.companion. For companion.deleted, it's the companion as it was before deletion. Companion events cover the companions your project owns, not the Napster stock catalog.
{
"eventId": "e10b49a2b70d4657d728873dd473874c",
"eventType": "companion.updated",
"timestamp": 1790685858,
"organizationId": "org_abc123",
"projectId": "proj_abc123",
"data": {
"companion": {
"id": "comp_abc123",
"firstName": "Maya",
"lastName": "Bennett",
"previewUrl": "https://cdn.napster.com/companions/comp_abc123/preview.jpg",
"videoLoopUrl": "https://cdn.napster.com/companions/comp_abc123/video.mp4",
"ethnicity": "multiracial",
"gender": "female",
"headline": "Helping people find clarity, momentum, and practical next steps.",
"tags": {},
"status": "completed",
"createdAt": 1790685277,
"externalClientId": null,
"versions": ["v1"]
}
}
}| Field | Description |
|---|---|
id | The companion ID. |
firstName / lastName | The companion's name. |
previewUrl | URL of the companion's preview image. |
videoLoopUrl | URL of the companion's idle video loop, or null until generation finishes. |
ethnicity / gender | The companion's appearance attributes. |
headline | The companion's short description. |
tags | Key–value tags on the companion. |
status | Generation status: pending, generationCompleted, readyToUse, or completed. |
createdAt | When the companion was created, as a Unix timestamp in seconds. |
externalClientId | The external client ID associated with the companion, or null. |
versions | The avatar model versions available for the companion, for example ["v1"]. Empty until generation finishes. |
A new companion sends companion.created with status: "pending", then one companion.updated for each status change as it generates: generationCompleted, readyToUse, and finally completed. You can attach it to an agent from readyToUse; edit it once it's completed.
Verifying requests
Every delivery is signed with your webhook's signing secret. Verify the signature before trusting a request, so you only act on events that come from Napster.
Each request carries an X-Webhook-Signature header: the HMAC-SHA256 of the raw request body, keyed with your signing secret and hex-encoded.
POST /webhooks/napster HTTP/1.1
Content-Type: application/json
X-Webhook-Signature: 077820582aecb562633e656670fa842e81a391a0daebaa85f3bf71d39e933b9bTo verify a request, compute the same HMAC over the body you received and compare it to the header:
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
// express.raw keeps the body as the exact bytes Napster signed
app.post("/webhooks/napster", express.raw({ type: "application/json" }), (req, res) => {
const expected = createHmac("sha256", process.env.NAPSTER_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const received = req.get("X-Webhook-Signature") ?? "";
if (
received.length !== expected.length ||
!timingSafeEqual(Buffer.from(received), Buffer.from(expected))
) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
res.sendStatus(200);
// Process event.eventType after responding
});import hashlib
import hmac
import os
from flask import Flask, abort, request
app = Flask(__name__)
@app.post("/webhooks/napster")
def napster_webhook():
expected = hmac.new(
os.environ["NAPSTER_WEBHOOK_SECRET"].encode(),
request.get_data(),
hashlib.sha256,
).hexdigest()
received = request.headers.get("X-Webhook-Signature", "")
if not hmac.compare_digest(expected, received):
abort(401)
event = request.get_json()
# Process event["eventType"] after responding
return "", 200Sign the raw body exactly as received. Parsing the JSON and serializing it again changes the bytes, and the signature won't match. Use the signing secret exactly as shown in the dashboard: the base64 string itself is the key, so don't decode it.
Handling deliveries
A delivery succeeds when your endpoint responds with any 2xx status within 10 seconds. Any other status, a network error, or a timeout counts as a failed attempt.
A failed attempt is retried up to 3 more times with exponential backoff: about 1, 2, and 4 seconds between attempts. If every attempt fails, the delivery is marked failed and isn't retried again. You can still inspect it in the delivery history.
- Respond quickly with a
2xxstatus. Do any slow work after you respond, so the delivery isn't treated as failed. - Expect duplicates. If your endpoint handles a request but responds too late, the retry delivers the same event again. Use
eventIdto skip events you've already processed. - Don't rely on order. Use
timestampto order events for the same session or companion.
Delivery history
The dashboard keeps a history of every delivery for each webhook. Open a webhook to see each delivery's event, status, and timestamp. Open a delivery to see the request that was sent — its body and headers — and each attempt, with the response status, body, and latency your endpoint returned. Response bodies are stored up to 4 KB.
Use it to debug your endpoint: if deliveries fail, the response your endpoint returned is recorded on each attempt.