NOVA Developer Docs
Reference for the public NOVA API. Authenticated endpoints expect
Authorization: Bearer <jwt>.
Webhooks
Receive real-time notifications when call lifecycle events occur. Register an HTTPS URL and we POST a signed JSON payload on each subscribed event.
Events
call.started— outbound call dispatchedcall.ended— call hung up (any reason)call.transcribed— full transcript persisted-
call.scored— LLM-as-judge quality score persisted
Register a webhook
POST /v1/webhooks
Authorization: Bearer <your-jwt>
Content-Type: application/json
{
"url": "https://hooks.example.com/nova",
"events": ["call.started", "call.ended", "call.transcribed", "call.scored"]
}
The response includes a secret — save it immediately. We
store only its SHA-256 hash; the raw value is never retrievable after
this response.
{
"id": "8f3b2a4c-1234-...",
"url": "https://hooks.example.com/nova",
"events": ["call.started", "call.ended", "call.transcribed", "call.scored"],
"secret": "raw-token-shown-once-save-it-now",
"is_active": true,
"created_at": "2026-05-10T08:30:00Z"
}
Payload shape
POST <your-url>
Content-Type: application/json
X-Nova-Event: call.ended
X-Nova-Signature: sha256=<hex_hmac>
User-Agent: NOVA-Webhooks/1.0
{
"event": "call.ended",
"delivered_at": "2026-05-10T08:30:00Z",
"data": {
"call_id": "uuid",
"customer_id": "uuid",
"started_at": "2026-05-10T08:27:00Z",
"ended_at": "2026-05-10T08:30:00Z",
"duration_seconds": 187,
"outcome": "qualified",
"goal": "qualify_interest",
"goal_completed": true,
"to_phone_e164": "+15555550100"
}
}
Verifying the signature
The X-Nova-Signature header is an HMAC-SHA256 computed
over the raw request body. The HMAC key is the
SHA-256 hash of your raw secret — hash your stored
token first, then HMAC.
Python
import hashlib
import hmac
def verify(raw_secret: str, body: bytes, header: str) -> bool:
key = hashlib.sha256(raw_secret.encode()).hexdigest()
expected = "sha256=" + hmac.new(
key.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
Node
const crypto = require("crypto");
function verify(rawSecret, body, header) {
const key = crypto.createHash("sha256")
.update(rawSecret).digest("hex");
const expected = "sha256=" + crypto
.createHmac("sha256", key)
.update(body).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(header)
);
}
Retries
Non-2xx responses and network errors are retried with exponential backoff: 1s, 5s, 30s — up to 4 attempts total. Each attempt has a 10-second timeout. After the final failure the delivery is marked failed and not retried.
Debugging deliveries
GET /v1/webhooks/{id}/deliveries?limit=50
Authorization: Bearer <your-jwt>
Returns the most recent delivery rows with status_code,
attempt_count, delivered_at, and
error_message. Use
POST /v1/webhooks/{id}/test to send a fake
call.started payload end-to-end.
Other endpoints
-
GET /v1/webhooks— list your webhooks (no secrets) -
DELETE /v1/webhooks/{id}— soft-delete (setsis_active=false) -
POST /v1/webhooks/{id}/test— send a testcall.startedpayload
Constraints
- HTTPS only (
http://URLs are rejected) - Private / internal IPs are rejected (localhost, RFC1918, link-local)
- Maximum 4 delivery attempts per event