API reference
Waveline API webhooks
Webhooks send changes to your server or automation platform, so you can update a CRM or support workflow without continuously polling. A subscription belongs to one integration connection and respects its permissions and resource visibility.
Create a subscription
Open the integration's details in Settings → Integrations, then choose Add webhook. Choose a destination and the events you need. Save the signing secret when it is shown.
Destinations must use a public HTTPS origin enabled by Waveline. The setup dialog lists allowed origins. Contact Waveline support to enable your receiver's exact origin if needed. Localhost, private hosts, and redirects cannot be used as webhook destinations.
To create a subscription through the API, grant webhooks.write plus the corresponding resource read permissions. This Bash/cURL example requires messages.read and an approved destination:
curl --fail-with-body 'https://api.waveline.tel/v1/webhook-subscriptions' \
--header "Authorization: Bearer $WAVELINE_API_KEY" \
--header 'Content-Type: application/json' \
--header "Idempotency-Key: $WAVELINE_WEBHOOK_REQUEST_ID" \
--data '{"target_url":"https://hooks.example.com/waveline","event_types":["message.received","message.failed"]}'
Set WAVELINE_WEBHOOK_REQUEST_ID to a saved unique request ID before running this example. The response contains data.subscription and data.signing_secret. Store the secret in your receiver's secret configuration. It is distinct from your API key and must not be exposed to clients or logs.
Available events
| Event | Required read permission | Trigger |
|---|---|---|
call.completed | calls.read | The overall external call ends; the event includes only activity the connection may access. |
call.recording.ready | calls.read | An accessible recording becomes available. |
call.transcript.ready | calls.read | An accessible recording's saved transcript becomes available. |
call.summary.ready | calls.read | An accessible recording's summary becomes available. |
message.received | messages.read | An eligible inbound message is received. |
message.delivered | messages.read | An eligible outbound message is reported delivered. |
message.failed | messages.read | An eligible message is reported failed. |
contact.created | contacts.read | An accessible contact is created. |
contact.updated | contacts.read | An accessible contact is updated. |
Each event contains id, type, created_at, connection_id, and data. The data value uses the corresponding contact, call, message, or RecordingEvent schema. Contact events may also include origin identifying the writer; it can be absent on older deliveries and samples. Use it when preventing a CRM sync from writing its own changes back in a loop.
A completed call does not promise that its recording, transcript, or summary is ready. For the three readiness events, data.id identifies the recording and data.call_id identifies its call. These events also cover voicemail recordings when the corresponding content is available. They contain recording metadata, not a download URL or the transcript or summary text. Retrieve the content through the recording endpoints using credentials that still have access.
See event and resource models for all fields. There is no dedicated contact-deleted event; reconcile the records your application retains.
Verify signatures before processing
Read the exact request body bytes before JSON parsing. Waveline sends these headers:
| Header | Value |
|---|---|
webhook-id | Delivery ID, distinct from the logical event ID in the JSON body. |
webhook-timestamp | Unix timestamp in seconds for this delivery attempt. |
webhook-signature | One or more space-separated signatures, each starting with v1, followed by a base64 HMAC-SHA256 signature. During secret rotation, a delivery can include signatures for both secrets. |
The signed input is the delivery ID, a period, the timestamp, a period, and the original raw body. The HMAC key is the base64-decoded value after your signing secret's whsec_ prefix. Reject stale timestamps and use a constant-time signature comparison.
This Node.js helper accepts a Buffer containing the unmodified body and a Fetch-compatible Headers object. Configure your HTTP framework to preserve raw bytes; do not pass a reserialized JSON object.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyWavelineWebhook(rawBody, headers, secret, now = Date.now()) {
if (!Buffer.isBuffer(rawBody) || rawBody.length > 256 * 1024) return false;
const id = headers.get('webhook-id');
const timestamp = headers.get('webhook-timestamp');
const signature = headers.get('webhook-signature');
if (!id || !timestamp || !/^\d+$/.test(timestamp) || !signature) return false;
const seconds = Number(timestamp);
if (!Number.isSafeInteger(seconds) || !Number.isFinite(now)
|| Math.abs(now / 1000 - seconds) > 300) return false;
if (!/^whsec_[A-Za-z0-9+/]{43}=$/.test(secret ?? '')) return false;
const expected = createHmac('sha256', Buffer.from(secret.slice(6), 'base64'))
.update(`${id}.${timestamp}.`, 'utf8')
.update(rawBody)
.digest();
return signature.trim().split(/\s+/).some(candidate => {
if (!/^v1,[A-Za-z0-9+/]{43}=$/.test(candidate)) return false;
const received = Buffer.from(candidate.slice(3), 'base64');
return received.length === expected.length && timingSafeEqual(received, expected);
});
}
Only parse and process the event after verification succeeds. Enforce a request-size limit while reading the body as well as in the verifier. Keep your server clock synchronized. Valid signatures authenticate the request; they do not make repeated deliveries safe to process twice.
Acknowledge and deduplicate
Durably accept the verified event into your own queue or database, then return 2xx within 10 seconds. Process downstream CRM work asynchronously. If you cannot durably accept it, return a transient failure so Waveline can retry.
Delivery is at least once and ordering is not guaranteed. Within a connection, use the JSON envelope's id as the logical event deduplication key. Manual replay creates a new delivery ID while preserving the event ID. Make your destination change and processed-event record atomic, or pass the event ID to a destination that supports idempotency. Do not mark an event processed before its side effect is safely recorded.
For state synchronization, re-fetch a resource when needed instead of assuming the last event delivered represents its latest state. A sample event has an ID beginning with sample_; samples are setup data, not a promise of new activity.
Retries and delivery history
Connection failures, server errors, and transient 408, 409, 425, and 429 responses retry with backoff, up to 10 attempts. Waveline honors Retry-After up to 24 hours. Other 3xx and 4xx responses fail permanently; 410 disables the subscription. Redirects are not followed. Invalid destinations, unreadable secrets, and oversized events also fail without repeated delivery.
Inspect recent delivery status in the integration's details or with GET /v1/webhook-deliveries using webhooks.read. After correcting the cause, use the available retry action in settings for eligible failed deliveries. Every attempt rechecks access. Revoking the connection or disabling the subscription cancels outstanding work, but cannot recall an HTTP request already in flight.
Signing-secret rotation has a 24-hour overlap. During that period, deliveries carry signatures for the new secret and the immediately previous secret. Save the new secret and update your receiver before the overlap ends. If a connected platform retains the old secret, update or reconnect its trigger. Rotating again during the overlap retires the earlier secret immediately. See the webhook endpoint reference for subscription management and setup samples.