Developers — API & webhooks
Setting up webhooks
Receive real-time events like completed calls at your own endpoint, and verify their signatures securely.
Webhooks push events from SunoVoice AI to your own systems the moment they happen — so you can log calls in your CRM, trigger a Slack message, or kick off an automation. Instead of polling the API, you give us a URL and we POST to it when something occurs.
Add a webhook endpoint
- Open the Webhooks tab.
- Click to add an endpoint and enter the URL that should receive events (it must be publicly reachable over HTTPS).
- Choose which events to subscribe to.
- Save. We return a signing secret (it starts with
whsec_) once — copy and store it now. You'll use it to verify that incoming requests really came from us.
You can send a test event from the dashboard to confirm your endpoint receives and responds correctly.
Events
| Event | When it fires |
|---|---|
call.completed | A call finishes — includes its details and summary |
We add new event types over time; subscribe only to the ones you need.
The request we send
We send an HTTP POST with a JSON body shaped like this:
{
"event": "call.completed",
"timestamp": "2026-06-12T09:30:00Z",
"data": { }
}
Headers on every delivery:
| Header | Value |
|---|---|
X-SunoVoice-Event | The event type, e.g. call.completed |
X-SunoVoice-Signature | Base64 HMAC-SHA256 signature of the request |
Your endpoint should respond with a 2x status quickly (within a few seconds). Do any heavy processing asynchronously.
Verifying the signature
Always verify the signature before trusting a webhook, so attackers can't forge events. The signature is:
base64( HMAC_SHA256( secret, timestamp + "." + raw_request_body ) )
where timestamp is the value from the JSON body and raw_request_body is the exact bytes you received (don't re-serialize the JSON — use the raw body). Compare your computed value to the X-SunoVoice-Signature header.
Python
import hmac, hashlib, base64, json
def verify(secret: str, raw_body: bytes, signature_header: str) -> bool:
timestamp = json.loads(raw_body)["timestamp"]
payload = timestamp.encode() + b"." + raw_body
expected = base64.b64encode(
hmac.new(secret.encode(), payload, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, signature_header)
Node.js
const crypto = require("crypto");
function verify(secret, rawBody, signatureHeader) {
const timestamp = JSON.parse(rawBody).timestamp;
const payload = Buffer.concat([Buffer.from(timestamp + "."), Buffer.from(rawBody)]);
const expected = crypto.createHmac("sha256", secret).update(payload).digest("base64");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
Reject any request whose signature doesn't match.
Retries & reliability
If your endpoint is down or returns a non-2xx status, we retry with backoff so a brief outage doesn't lose events. Every delivery attempt is logged on the Webhooks tab, where you can see whether each one succeeded and its response status.
Because retries can happen, design your handler to be idempotent — processing the same event twice should be safe.
Best practices
- Verify every signature before acting on a payload.
- Respond fast (
2xx), then process asynchronously. - Be idempotent to handle retries safely.
- Keep your signing secret private, just like an API key.
- Use the delivery log to debug failures.
Next step
Manage your plan and payments: Plans & billing.
Still stuck? Contact our team and we'll help you out.