Skip to main content

Overview

Supply a callback_url on the original PUT and we’ll POST the final response to that URL when the operation reaches ready or failed. This eliminates polling latency for production integrations. Webhooks follow the Standard Webhooks specification, so any standards-compliant client library can verify them out of the box.
Polling is canonical. Webhooks are a best-effort latency optimisation; if you didn’t receive one within a reasonable window, fall back to polling the GET endpoint. Don’t rely on webhook receipt as proof of operation state.

Registering a callback

An active signing secret is required. Every webhook is signed, so we only deliver callbacks for accounts that have at least one active signing secret. With no secret there is nothing to sign with, and no callbacks are sent at all — even if you supply a callback_url. Create one in the dashboard before relying on webhooks.
Add callback_url to any PUT request body:
Your endpoint must:
  • Be publicly accessible over HTTPS with a valid TLS certificate (self-signed certs are rejected).
  • Return a 2xx status code within 10 seconds. Respond first and do the work afterwards (downloading audio inside the handler can take longer than that).
  • Handle duplicate deliveries idempotently — use the webhook-id header as your deduplication key.
We re-verify the URL against the same rules at delivery time, so DNS rebinding to a private IP between submission and delivery is rejected too.

Payload shape

The body is exactly what the matching GET returns: the same object, under the same key. There is no extra wrapper. A finished adapt.auto_duck request delivers:
The key is the one the endpoint’s own response uses — song for create.new_song, auto_cut for adapt.auto_cut_down, ducked_audio for adapt.auto_duck, and so on (see each endpoint’s guide, e.g. New song, or its poll page in the API Reference, e.g. create.new_song (poll)). Anything you can do with a polled response, you can do with a webhook body. The kind of event is sent in the webhook-type header, <route>.<status> — .ready or .failed — for fan-out routing on your side: for example create.new_song.ready or adapt.auto_duck.failed. The header is not covered by the signature (see below); the same information is in the signed body, in its key and its status. Signed download URLs in a ready body are issued when the delivery is sent, so each is valid for its full lifetime from the moment it reaches you; expires_at says until when. Correlate webhook deliveries with your own records using the echoed idempotency_key — you chose it on the PUT, so you already have it on your side.

Signature verification

Every delivery includes three headers used for verification (per the Standard Webhooks spec), plus webhook-type: To verify a webhook:
  1. Compute HMAC-SHA256(your_signing_secret, "<webhook-id>.<webhook-timestamp>.<raw-body>").
  2. Base64-encode it.
  3. Compare against the v1,… signature in webhook-signature using constant-time comparison.
  4. Reject if the signature doesn’t match, or if webhook-timestamp is more than 5 minutes from the current time.
Your webhook signing secret is a separate resource from your API key — you create them independently in the dashboard. The plaintext (whsec_…) is shown once on creation; we store only an encrypted copy. See authentication. There are official libraries that hide the HMAC math. From github.com/standard-webhooks/standard-webhooks: Both handlers verify the delivery, answer 200 straight away, skip a webhook-id they have already seen, and process the result afterwards.
Libraries for Go, Ruby, Kotlin, Rust, and PHP are available from the same repository.

Manual verification

If you’d rather not pull in a library, here’s the equivalent in plain Python:
Python
Always use a constant-time comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node, equivalent in your language). Direct == comparison leaks information about the secret via timing.

Retry behaviour

If your endpoint doesn’t return a 2xx, we retry with exponential backoff up to 6 attempts (total window ~9 hours): After the final retry attempt fails we give up and log the failure on our side. The operation result remains available via the GET endpoint indefinitely — fall back to polling if you suspect a delivery was missed.

Secret rotation

Signing secrets are an account-level resource, independent of your API keys — rotating one doesn’t touch the other. To rotate without dropping deliveries:
  1. Create a new signing secret in the dashboard. The plaintext is shown once.
  2. Roll out the new secret to your webhook handler so it can verify against either old or new.
  3. During the cutover, every outgoing webhook is signed with all of your non-revoked signing secrets (space-separated, e.g. v1,<new> v1,<old>). The Standard Webhooks libraries handle multi-signature verification automatically.
  4. Revoke the old secret when you’re confident every consumer has migrated.

Local testing

Use a tunnelling tool like ngrok to expose your local server during development:
Then pass the ngrok URL as callback_url when submitting test operations.