WAVV Public API v3

Read your team's call data over REST, and receive call events in real time over signed webhooks. The two share one canonical call shape, so what you poll and what you're pushed are identical.

Version
3.0.0
Format
JSON over HTTPS
Base URL
https://api.wavv.com/v3

Getting Started

Overview

The v3 Public API gives you programmatic access to your team's call activity in two ways:

  • RESTGET /calls, plus per-call recording and transcript endpoints, for backfills, reconciliation, and on-demand lookups.
  • Webhooks — four call.* events pushed to an HTTPS endpoint you register, for real-time work.

Both surfaces serialize a call through the same code path, so a call fetched from GET /calls/{id} and the data object inside a call.ended webhook are field-for-field identical. Build your model once.

All requests and responses are JSON. Timestamps are ISO 8601 in UTC (e.g. 2026-05-21T14:30:00Z). Phone numbers are unformatted 10-digit NANP strings (e.g. 5125551234).

Getting Started

Authentication

Every REST endpoint requires an API key, passed as a Bearer token in the Authorization header:

Authorization: Bearer <your_api_key>

A missing, malformed, or revoked key returns 401 with error code INVALID_API_KEY.

Keys are team-scoped: a key can only ever read calls belonging to the team it was issued for. There is no cross-team or agency-wide call access in this version, and no team id is accepted as a parameter — the key determines the team. Manage keys in the WAVV Integrations panel.

Webhooks are not authenticated with an API key. Each webhook has its own signing secret and is verified by signature — see Verifying signatures.

Getting Started

Pagination and filtering

GET /calls returns calls newest-first and pages with a keyset cursor.

Cursor
Each response carries a nextCursor. Pass it back as the cursor query parameter to get the next page. nextCursor is null on the last page. The value is the startedAt of the last returned call — treat it as opaque; the format may change.
Page size
limit defaults to 50 and is capped at 200. Values outside that range, and values that aren't positive integers, resolve to the default.
Time range
startedAfter and startedBefore bound startedAt inclusively, and each may be used on its own. Send both as ISO 8601 — a value that can't be parsed as a date is treated as absent, so validate before sending to be sure you get the window you intend.
Stable paging
Paging is keyset rather than offset, so calls created while you page through will not shift or duplicate the rows you have already read. The cursor has one-second resolution; deduplicate on id if your integration requires exactness across page boundaries.

Webhooks

Verifying signatures

Every delivery carries an X-WAVV-Signature header:

X-WAVV-Signature: t=1779720600,v1=6f2a…c91b
  • t — Unix timestamp in seconds, stamped fresh at send time (so a retry carries a new t).
  • v1 — hex HMAC-SHA256 of the timestamp, a literal dot, and the raw body concatenated together, keyed with the webhook's signing secret.

To verify, in order:

  • Read the raw request body as bytes, before any JSON parsing. Re-serializing a parsed body changes the bytes and the signatures will not match.
  • Recompute the HMAC over t + "." + rawBody with your signing secret.
  • Compare against v1 in constant time (crypto.timingSafeEqual, not ===).
  • Reject the delivery if |now - t| exceeds your tolerance. 300 seconds is a common choice; this check is what prevents a captured delivery from being replayed later.

The signing secret is prefixed whsec_ and is available in the Integrations panel. Because verification requires the secret itself, it remains retrievable there — handle it with the same care as a password. Deleting a webhook also deletes its signing secret.

const crypto = require('node:crypto');

function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
  const t = Number(parts.t);
  if (!Number.isFinite(t)) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(parts.v1 ?? '', 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Webhooks

Delivery and retries

Register endpoints in the WAVV Integrations panel: give the webhook a name, an HTTPS URL, and the set of events it should receive. Each webhook can subscribe to any combination of the four events, and a team can have several.

Request
POST with Content-Type: application/json and the X-WAVV-Signature header. Your endpoint has 10 seconds to respond before the attempt times out.
Success
Any 2xx acknowledges the delivery. Any other status, including a redirect, is treated as a failure and retried. Acknowledge first and process the payload afterwards.
Retries
Up to 5 attempts with exponential backoff starting at 1 second. Each webhook retries independently, so a failing endpoint does not delay or duplicate delivery to your other endpoints. A delivery that fails all five attempts is not retried further.
Rate
Deliveries are grouped per webhook and paced at roughly 10 per second. A slow endpoint throttles only its own queue.
Make handlers idempotent
Delivery is at-least-once, so the same event may arrive more than once, and because each event is queued independently, events for one call may arrive out of order. Deduplicate on (event, data.id) and design each handler to be safely replayable.
URL requirements
The URL must be an absolute http or https URL on a publicly routable host. localhost and addresses in loopback, RFC-1918 private, link-local, and CGNAT ranges are not accepted. This is validated at registration and confirmed again before each delivery.

Calls

List calls

GET/calls

Returns the team's calls, newest first. All parameters are optional; with none supplied you get the most recent page of every call on the team.

transcript is omitted from this response to keep pages small. Fetch it per-call from GET /calls/{id}/transcript.

Filters combine with AND.

The direction filter selects by call type: inbound returns received and forwarded calls, and outbound returns dialer-placed calls. Specialized call types such as whispers and transfers carry direction: "outbound" on the call object but fall outside the direction=outbound filter. To retrieve every call regardless of type, omit the filter and read the direction field from each result.

Query parameters
ParameterTypeDescription
directionstringoptRestrict to one call direction. One of: inbound, outbound.
campaignIdstring (uuid)optRestrict to a single dialing campaign.
startedAfterstring (date-time)optOnly calls with startedAt at or after this instant. Unparseable values are ignored.
startedBeforestring (date-time)optOnly calls with startedAt at or before this instant. Unparseable values are ignored.
cursorstringoptThe nextCursor from a previous response. Omit for the first page.
limitintegeroptPage size, 1–200. Defaults to 50. Invalid values fall back to the default.
Responses
200OK — a CallListEnvelope.
401API key missing, malformed, or revoked — INVALID_API_KEY.
500Failed to list calls — SERVER_ERROR.
Response shape
{
  "data": [
    {
      "id": "3c9a17b4-2f5e-4d81-b0a7-6e2c9d4f1a03",
      "teamId": "8f3b2c10-4e7a-4b1c-9d2e-1a2b3c4d5e6f",
      "campaignId": "5d1e8a72-9c34-4f60-8b2a-7e0d3c5f9b18",
      "direction": "outbound",
      "phone": "5125551234",
      "callerId": "5125559876",
      "contactId": "ghl-99213",
      "contactName": "Dana Reyes",
      "startedAt": "2026-05-21T14:28:12Z",
      "answeredAt": "2026-05-21T14:28:19Z",
      "endedAt": "2026-05-21T14:30:00Z",
      "seconds": 101,
      "outcome": "HUNG_UP",
      "disposition": "Interested",
      "human": true,
      "note": "Sending pricing.",
      "summary": "Prospect asked for pricing on the multi-line plan.",
      "recorded": true
    }
  ],
  "nextCursor": "2026-05-21T14:28:12Z"
}

Calls

Get a call

GET/calls/{id}

Returns one call. As with the list endpoint, transcript is excluded.

A call that exists but belongs to another team returns 404, not 403 — the API does not confirm the existence of calls outside your team.

Path parameters
ParameterTypeDescription
idstring (uuid)reqThe call's unique identifier.
Responses
200OK — a Call.
401API key missing, malformed, or revoked — INVALID_API_KEY.
404Call not found, or not on this team — NOT_FOUND.
500Failed to fetch call — SERVER_ERROR.
Response shape
{
  "id": "3c9a17b4-2f5e-4d81-b0a7-6e2c9d4f1a03",
  "teamId": "8f3b2c10-4e7a-4b1c-9d2e-1a2b3c4d5e6f",
  "campaignId": "8f3b2c10-4e7a-4b1c-9d2e-1a2b3c4d5e6f",
  "direction": "inbound",
  "phone": "5125551234",
  "callerId": "5125559876",
  "contactId": "ghl-99213",
  "contactName": "Dana Reyes",
  "startedAt": "2026-05-21T14:30:00Z",
  "answeredAt": "2026-05-21T14:30:00Z",
  "endedAt": "2026-05-21T14:30:00Z",
  "seconds": 101,
  "outcome": "BUSY",
  "disposition": "Interested",
  "human": true,
  "note": "string",
  "summary": "string",
  "recorded": true
}

Calls

Get a recording URL

GET/calls/{id}/recording

Returns a freshly signed URL for the call's audio recording.

The URL is generated per request and is valid for the window given in expiresAt, currently 72 hours. Store the call id rather than the URL, and request a new one whenever you need access.

A 404 covers both a call that has no recording and a call id that isn't available to your key. Read recorded on the call object beforehand when you need to tell the two apart.

Path parameters
ParameterTypeDescription
idstring (uuid)reqThe call's unique identifier.
Responses
200OK — a RecordingUrl.
401API key missing, malformed, or revoked — INVALID_API_KEY.
404Call not found, or the call has no recording — NOT_FOUND.
500Failed to fetch recording — SERVER_ERROR.
Response shape
{
  "url": "https://example.com",
  "expiresAt": "2026-05-21T14:30:00Z"
}

Calls

Get a transcript

GET/calls/{id}/transcript

Returns the call's transcript and AI-generated summary.

Both fields are populated asynchronously once the call ends, so a 200 carrying transcript: null indicates the transcript is not yet available for that call. Waiting for the call.recorded event before fetching is more efficient than polling.

summary also appears on the call object; it is repeated here so one request returns both halves of the post-call content.

Path parameters
ParameterTypeDescription
idstring (uuid)reqThe call's unique identifier.
Responses
200OK — a Transcript.
401API key missing, malformed, or revoked — INVALID_API_KEY.
404Call not found, or not on this team — NOT_FOUND.
500Failed to fetch transcript — SERVER_ERROR.
Response shape
{
  "transcript": "Agent: Hi Dana, this is…",
  "summary": "string"
}

Four events cover the life of a call. Every payload uses the same envelope — event, sentAt, and a data object holding the Call — so one handler can serve all four and branch on event.

Call events

Outbound call started

EVENTcall.started

Fires the moment an outbound call is created — when the dialer places it, before the far end has answered or rejected.

Because the call has only just begun, endedAt, seconds, answeredAt, disposition, human, note, and summary are null, and outcome is UNKNOWN. Use this event to open a record that call.ended will complete; call results are not available until then.

Payload — CallEvent
FieldTypeDescription
eventstringreqWhich event fired. Branch your handler on this. One of: call.started, call.incoming, call.ended, call.recorded.
sentAtstring (date-time)reqWhen this payload was rendered. It is set once when the event is dispatched, so retries of the same delivery carry the original value. Use the t parameter in X-WAVV-Signature for freshness and replay checks.
dataCallreqThe canonical call shape. Identical in REST responses and webhook payloads. transcript is never included; fetch it from GET /calls/{id}/transcript.
Your endpoint should return
2XXAny 2xx acknowledges the delivery. Anything else is retried.
Example delivery
{
  "event": "call.started",
  "sentAt": "2026-05-21T14:28:12Z",
  "data": {
    "id": "3c9a17b4-2f5e-4d81-b0a7-6e2c9d4f1a03",
    "teamId": "8f3b2c10-4e7a-4b1c-9d2e-1a2b3c4d5e6f",
    "campaignId": "5d1e8a72-9c34-4f60-8b2a-7e0d3c5f9b18",
    "direction": "outbound",
    "phone": "5125551234",
    "callerId": "5125559876",
    "contactId": "ghl-99213",
    "contactName": "Dana Reyes",
    "startedAt": "2026-05-21T14:28:12Z",
    "answeredAt": null,
    "endedAt": null,
    "seconds": null,
    "outcome": "UNKNOWN",
    "disposition": null,
    "human": null,
    "note": null,
    "summary": null,
    "recorded": false
  }
}

Call events

Inbound call received

EVENTcall.incoming

Fires when an inbound call arrives on one of the team's numbers, at the point the call record is created — before it is answered, routed, or forwarded.

The counterpart to call.started for the inbound direction, with the same fields still unpopulated. phone and callerId follow the call rather than the account, so here phone is your WAVV number that was dialed and callerId is the caller's number. campaignId and contactId are null unless the caller matches a known CRM contact.

Payload — CallEvent
FieldTypeDescription
eventstringreqWhich event fired. Branch your handler on this. One of: call.started, call.incoming, call.ended, call.recorded.
sentAtstring (date-time)reqWhen this payload was rendered. It is set once when the event is dispatched, so retries of the same delivery carry the original value. Use the t parameter in X-WAVV-Signature for freshness and replay checks.
dataCallreqThe canonical call shape. Identical in REST responses and webhook payloads. transcript is never included; fetch it from GET /calls/{id}/transcript.
Your endpoint should return
2XXAny 2xx acknowledges the delivery. Anything else is retried.
Example delivery
{
  "event": "call.incoming",
  "sentAt": "2026-05-21T16:02:44Z",
  "data": {
    "id": "b7e4d290-13af-4c65-9a08-fd2e6b510c77",
    "teamId": "8f3b2c10-4e7a-4b1c-9d2e-1a2b3c4d5e6f",
    "campaignId": null,
    "direction": "inbound",
    "phone": "5125559876",
    "callerId": "5125551234",
    "contactId": null,
    "contactName": null,
    "startedAt": "2026-05-21T16:02:44Z",
    "answeredAt": null,
    "endedAt": null,
    "seconds": null,
    "outcome": "UNKNOWN",
    "disposition": null,
    "human": null,
    "note": null,
    "summary": null,
    "recorded": false
  }
}

Call events

Call ended and dispositioned

EVENTcall.ended

Fires when the call is over and its outcome has been recorded. This is the complete call: endedAt, seconds, answeredAt, outcome, human, and any disposition or note the agent entered are all populated.

For most integrations this is the only event you need. It fires for both directions.

Two fields may still be null here and arrive later: summary (AI-generated) and any recording — see call.recorded.

Payload — CallEvent
FieldTypeDescription
eventstringreqWhich event fired. Branch your handler on this. One of: call.started, call.incoming, call.ended, call.recorded.
sentAtstring (date-time)reqWhen this payload was rendered. It is set once when the event is dispatched, so retries of the same delivery carry the original value. Use the t parameter in X-WAVV-Signature for freshness and replay checks.
dataCallreqThe canonical call shape. Identical in REST responses and webhook payloads. transcript is never included; fetch it from GET /calls/{id}/transcript.
Your endpoint should return
2XXAny 2xx acknowledges the delivery. Anything else is retried.
Example delivery
{
  "event": "call.ended",
  "sentAt": "2026-05-21T14:30:01Z",
  "data": {
    "id": "3c9a17b4-2f5e-4d81-b0a7-6e2c9d4f1a03",
    "teamId": "8f3b2c10-4e7a-4b1c-9d2e-1a2b3c4d5e6f",
    "campaignId": "5d1e8a72-9c34-4f60-8b2a-7e0d3c5f9b18",
    "direction": "outbound",
    "phone": "5125551234",
    "callerId": "5125559876",
    "contactId": "ghl-99213",
    "contactName": "Dana Reyes",
    "startedAt": "2026-05-21T14:28:12Z",
    "answeredAt": "2026-05-21T14:28:19Z",
    "endedAt": "2026-05-21T14:30:00Z",
    "seconds": 101,
    "outcome": "HUNG_UP",
    "disposition": "Interested",
    "human": true,
    "note": "Sending pricing.",
    "summary": null,
    "recorded": true
  }
}

Call events

Recording available

EVENTcall.recorded

Fires when a call's audio recording has finished processing and is available.

This is the only event whose data carries an extra field: recordingUrl, a signed URL for the audio. The URL is generated at send time and is time-limited, so download the audio when you receive it, or request a fresh URL from GET /calls/{id}/recording later. Store the call id rather than the URL.

This event typically follows call.ended within seconds. As with all events, order is not guaranteed — see Delivery and retries.

Payload — CallRecordedEvent
FieldTypeDescription
eventstringreqAlways call.recorded.
sentAtstring (date-time)reqWhen this payload was rendered.
dataCallWithRecordingreqA Call plus the signed recording URL. Used only as the data of a call.recorded event.
Your endpoint should return
2XXAny 2xx acknowledges the delivery. Anything else is retried.
Example delivery
{
  "event": "call.recorded",
  "sentAt": "2026-05-21T14:30:14Z",
  "data": {
    "id": "3c9a17b4-2f5e-4d81-b0a7-6e2c9d4f1a03",
    "teamId": "8f3b2c10-4e7a-4b1c-9d2e-1a2b3c4d5e6f",
    "campaignId": "5d1e8a72-9c34-4f60-8b2a-7e0d3c5f9b18",
    "direction": "outbound",
    "phone": "5125551234",
    "callerId": "5125559876",
    "contactId": "ghl-99213",
    "contactName": "Dana Reyes",
    "startedAt": "2026-05-21T14:28:12Z",
    "answeredAt": "2026-05-21T14:28:19Z",
    "endedAt": "2026-05-21T14:30:00Z",
    "seconds": 101,
    "outcome": "HUNG_UP",
    "disposition": "Interested",
    "human": true,
    "note": "Sending pricing.",
    "summary": "Prospect asked for pricing on the multi-line plan.",
    "recorded": true,
    "recordingUrl": "https://recordings.wavv.com/3c9a17b4…?X-Amz-Expires=259200&X-Amz-Signature=…"
  }
}

Reference

Object reference

The shape of every object the API returns or accepts. A field marked req is always present in a response (or required in a request body). Types ending in | null may come back as null.

Call

The canonical call shape. Identical in REST responses and webhook payloads. transcript is never included; fetch it from GET /calls/{id}/transcript.

FieldTypeDescription
idstring (uuid)reqThe call's unique identifier. Stable; safe to store and use as your dedupe key.
teamIdstring (uuid)reqThe team the call belongs to. Always the team your API key is scoped to.
campaignIdstring (uuid) | nullreqThe dialing campaign this call was placed under. null for inbound and ad-hoc calls.
directionstringreqinbound covers calls received on a team number, including forwarded ones. Everything else — dialer-placed calls, and internal types such as whispers and transfers — reports as outbound. One of: inbound, outbound.
phonestringreqThe number that was called. On outbound that is the contact you dialed; on inbound it is your own WAVV number that the caller reached. Unformatted 10 digits.
callerIdstring | nullreqThe number the call came from. On outbound that is the WAVV caller ID you presented; on inbound it is the external caller's number. Unformatted 10 digits. Because phone and callerId follow the call rather than the account, read direction first to determine which of the two identifies your contact.
contactIdstring | nullreqThe contact's id in the connected CRM, when the call was placed against a known contact. null for manual dials and unmatched inbound calls.
contactNamestring | nullreqThe contact's display name at the time of the call. null when unknown.
startedAtstring (date-time)reqWhen the call was created — dialed, or received. Also the sort key and pagination cursor.
answeredAtstring (date-time) | nullreqWhen the call connected, derived as endedAt - seconds. null until both of those are known, and for calls that were never answered.
endedAtstring (date-time) | nullreqWhen the call finished. null while the call is still in progress.
secondsinteger | nullreqTalk time in whole seconds. null while the call is in progress or if it never connected.
outcomestring | nullreqHow the call resolved, as determined by WAVV. UNKNOWN until the call ends. Treat this list as open — new members may be added. One of: BUSY, DISCONNECTED, NO_ANSWER, HUNG_UP, USER_HUNG_UP, UNKNOWN, VOICEMAIL, CALLBACK, NO_VOICEMAIL, NO_CALLBACK, TRANSFERRED, VOICEMAIL_RETRY.
dispositionstring | nullreqThe free-text disposition the agent selected, drawn from your team's own configured list. null if none was set. Values are team-defined, not a WAVV enum.
humanboolean | nullreqWhether a human answered, according to WAVV's answering-machine detection. false means a machine was detected; null means no determination was made, which is a distinct result from false.
notestring | nullreqFree-text note the agent attached to the call. null if none.
summarystring | nullreqAI-generated call summary. Produced asynchronously after the call ends, so it is typically null on call.ended and populated shortly after.
recordedbooleanreqWhether audio was recorded. When true, fetch the audio from GET /calls/{id}/recording — the URL is never embedded in this object.
CallWithRecording — extends Call

A Call plus the signed recording URL. Used only as the data of a call.recorded event.

FieldTypeDescription
recordingUrlstring (uri)reqSigned, time-limited URL to the call audio. Generated at send time and valid for 72 hours. Download promptly; do not store the URL.
CallListEnvelope

One page of calls.

FieldTypeDescription
dataCall[]reqThe page of calls, newest first.
nextCursorstring | nullreqPass back as cursor for the next page. null on the last page.
RecordingUrl

A freshly signed link to a call's audio.

FieldTypeDescription
urlstring (uri)reqSigned URL to the audio file.
expiresAtstring (date-time)reqWhen the signature stops working — 72 hours from the request.
Transcript

Post-call text content. Both fields are populated asynchronously.

FieldTypeDescription
transcriptstring | nullreqFull call transcript. null if not transcribed, or not ready yet.
summarystring | nullreqAI-generated summary. Same value as summary on the call object.
CallEvent

The envelope every webhook delivery uses.

FieldTypeDescription
eventstringreqWhich event fired. Branch your handler on this. One of: call.started, call.incoming, call.ended, call.recorded.
sentAtstring (date-time)reqWhen this payload was rendered. It is set once when the event is dispatched, so retries of the same delivery carry the original value. Use the t parameter in X-WAVV-Signature for freshness and replay checks.
dataCallreqThe canonical call shape. Identical in REST responses and webhook payloads. transcript is never included; fetch it from GET /calls/{id}/transcript.
CallRecordedEvent — the call.recorded envelope

Identical to CallEvent, except data also carries recordingUrl.

FieldTypeDescription
eventstringreqAlways call.recorded.
sentAtstring (date-time)reqWhen this payload was rendered.
dataCallWithRecordingreqA Call plus the signed recording URL. Used only as the data of a call.recorded event.
Error

Every non-2xx REST response uses this shape.

FieldTypeDescription
errorstringreqHuman-readable message intended for logs and diagnostics. Branch on code rather than this field, as the wording may change.
codestringreqMachine-readable code — see Error codes.

Reference

Error codes

Every error response carries a machine-readable code. The known codes:

CodeMeaning
INVALID_API_KEYMissing, malformed, or revoked API key.
KEY_SCOPE_INSUFFICIENTThe key cannot access the requested resource.
NOT_FOUNDThe call does not exist, or belongs to another team. Also returned when a call exists but has no recording.
INVALID_REQUESTThe request was malformed.
RATE_LIMITEDToo many requests. Back off and retry.
SERVER_ERRORSomething failed on our side. Safe to retry.
Error response shape
{
  "error": "Call not found",
  "code": "NOT_FOUND"
}