Developer Reference

NOVA API documentation

Everything you need to drive NOVA programmatically — dispatch qualification calls, manage billing, receive signed lifecycle webhooks, and pipe portal leads straight into the agent.

BASE URL https://api.novalabs.ae
On this page

Overview

The NOVA API is a JSON-over-HTTPS REST API. Authenticated endpoints expect a JWT in the Authorization header:

Authorization: Bearer <your-jwt>

You get a JWT from POST /auth/register or POST /auth/login. New accounts start a 7-day trial with 10 call minutes included — no card required.

Demo mode — read this first. NOVA currently runs in browser demo mode: submitting calls dispatches the full agent pipeline (context generation, agent room, 4-phase conversation), but the agent connects to a browser-based test room instead of dialing real phone numbers. You can talk to your agent end-to-end from the browser via the demo token endpoint. For production calling, contact ceo@novalabs.ae.

Browser requests are CORS-restricted to novalabs.ae origins; server-to-server requests are unrestricted. All request and response bodies are application/json unless noted otherwise.

Quickstart

Three requests from zero to a live agent conversation. You can do the same flow without code from the wizard at novalabs.ae — “Deploy Your First Agent”.

1. Create an account, get a token

curl -X POST https://api.novalabs.ae/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "you@example.com",
    "password": "a-strong-password"
  }'

The response includes your JWT and trial status:

{
  "access_token": "eyJhbGciOi...",
  "token_type": "bearer",
  "user": {
    "id": "8f3b2a4c-...",
    "email": "you@example.com",
    "trial_active": true,
    "trial_minutes_remaining": 10.0,
    "created_at": "2026-06-12T08:30:00Z"
  }
}

2. Dispatch your first agent call

curl -X POST https://api.novalabs.ae/calls \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $NOVA_TOKEN" \
  -d '{
    "owner_email": "you@example.com",
    "product": "CloudSync CRM",
    "website_url": "https://your-site.example",
    "context": "Mid-market CRM, AED 99/seat, main objection is migration effort",
    "goal": "qualify_interest",
    "consent": true,
    "leads": [
      { "phone": "+971501234567", "name": "Sarah", "company": "Acme Corp" }
    ]
  }'

NOVA builds a personalised call context per lead (scraping website_url if provided) and dispatches one agent per lead. The response gives you a call_id per lead:

{
  "calls": [
    {
      "call_id": "c1a2...",
      "context_id": "d4e5...",
      "phone": "+971501234567",
      "lead_name": "Sarah",
      "status": "in_progress",
      "expires_in_seconds": 600.0,
      "message": "Call dispatched successfully"
    }
  ],
  "total": 1,
  "dispatched": 1,
  "failed": 0
}

3. Poll call status

curl https://api.novalabs.ae/calls/$CALL_ID

Call jobs are ephemeral — they expire 10 minutes after creation, after which the status endpoint returns 404. Use webhooks for durable lifecycle events.

Authentication

Native JWT auth — register, log in, and pass the returned access_token as a Bearer header. Passwords must be at least 6 characters; emails are normalised to lowercase.

POST /auth/register no auth

Create an account and return a JWT. Starts a 7-day trial with 10 minutes. Optional fields: name, referral_code.

StatusMeaning
201Account created — body is { access_token, token_type, user }
409Email already registered
422Password shorter than 6 characters / invalid email
POST /auth/login no auth

Body { "email", "password" }. Returns the same { access_token, token_type, user } shape as register. 401 on bad credentials.

GET /auth/me bearer

Current user profile: id, email, email_verified, phone, trial_active, trial_minutes_remaining, created_at.

POST /auth/forgot-password no auth

Body { "email" }. Sends a 6-digit reset code valid for 10 minutes. Always returns 200 with a generic message (no account enumeration). Rate limit: 3 requests per email per hour.

POST /auth/reset-password no auth

Body { "email", "code", "new_password" }. Maximum 5 verification attempts per code (429 after that); 400 on invalid/expired code; 422 on a short password.

POST /auth/check-email no auth

Body { "email" }{ "exists": bool }. Used by the signup UI to route to log-in vs. register.

GET /me/features bearer

Per-user feature-flag payload — { "voice_cloning": bool, "byo_twilio": bool }. The dashboard reads this to decide whether to show the gated feature surfaces.

Calls

POST /calls bearer optional*

Submit a batch of 1–5 leads. NOVA generates a per-lead call context and dispatches one agent conversation per lead. *Anonymous submissions are currently accepted; when the FEATURE_CALLS_AUTH_REQUIRED server flag is on, a Bearer token is mandatory (401 without one). Authenticated calls are metered against your plan (402 when over limit).

Request body

FieldTypeNotes
owner_emailstring, requiredWhere call result briefs are sent
leadsarray, required1–5 objects: phone (E.164, required, no duplicates), name, company, email, title
productstring, requiredWhat you’re selling
website_urlstringScraped for product context when provided
contextstringFree-text product info, objections, pricing
goalenum, requiredbook_meeting · qualify_interest · collect_info · close_sale
languageenumen (default) or ar-AE
booking_linkstringRequired when goal=book_meeting
payment_linkstringRequired when goal=close_sale
pricing_summary / urgency_hook / goal_criteriastringOptional goal refinements
consentbool, requiredMust be true — you confirm permission to contact these numbers

Errors

StatusMeaning
400Missing consent · no leads · >5 leads · non-E.164 phone · duplicate phone · missing goal-specific link
401Auth required (when the server auth gate is on)
402Plan/trial minute limit would be exceeded
429Rate limit — max 5 submissions per IP per 60 seconds

Response is a BatchCallResponse — see the quickstart for the exact shape. Per-lead status is one of pending, in_progress, completed, failed.

GET /calls/{call_id} no auth

Status for one dispatched call: { call_id, status, phone, sms_sent, error, expires_in_seconds }. Returns 404 when the ID is unknown or the job passed its 10-minute TTL.

GET /contexts/{context_id} no auth

Debugging endpoint — returns the full generated context instance that was handed to the agent (opening line, qualification questions, objection handlers, fact sheet). Same 10-minute TTL and 404 semantics as call jobs.

Browser demo token

POST /token no auth

Generate a LiveKit participant token so you can join the agent’s room from a browser and have the full conversation over WebRTC — this is how “Join Call as Test Caller” works in demo mode.

curl -X POST https://api.novalabs.ae/token \
  -H "Content-Type: application/json" \
  -d '{ "room_name": "<room from your dispatch>", "participant_name": "Test Caller" }'

Response: { server_url, participant_token, room_name }. The token grants audio publish + subscribe and expires after 10 minutes. 500 if LiveKit is not configured on the server.

Billing & usage

Plans are usage-based on call minutes. Trial accounts get 10 minutes over 7 days. Card payments run through Stripe; UAE customers can pay in AED via Ziina.

GET /billing/plans no auth

Available plans with price, included minutes, and overage rate: Starter ($29/mo · 100 min), Growth ($99/mo · 500 min), Scale ($299/mo · 2000 min).

GET /billing/usage bearer

Current period usage: { plan, status, minutes_used, minutes_limit, minutes_remaining, period_start, period_end, trial_ends_at }.

GET /billing/subscription bearer

Active subscription details, or null when none exists.

POST /billing/subscribe bearer

Body { "plan": "starter" | "growth" | "scale", "payment_method_id" }. Creates a Stripe subscription and returns { subscription_id, status, client_secret } — confirm the payment client-side with the client_secret.

POST /billing/setup-intent bearer

Stripe SetupIntent for saving a payment method without charging — returns { client_secret, setup_intent_id }.

POST /billing/create-payment bearer

Ziina payment intent (AED) for a plan. Body { "plan_id" }; returns { redirect_url, payment_intent_id } — redirect the user to the hosted Ziina page. 503 when the payment service is not configured; 400 on an unknown plan.

POST /billing/cancel bearer

Cancel at period end. 404 when there is no active subscription.

POST /billing/webhook stripe only

Inbound Stripe webhook (subscription lifecycle + invoices). Requires a valid Stripe-Signature header — not for direct use.

Referrals

Every account gets a referral code. Pass it as referral_code on /auth/register and you earn a commission when the referred account converts to a paid plan. All endpoints require a Bearer token.

EndpointWhat it returns
GET /referrals/code Your code, commission rate, and shareable link (creates one if missing)
GET /referrals/stats Totals: referrals, pending signups, conversions, earnings, available balance (cents)
GET /referrals/list Individual referrals with status + earnings (limit/offset paging)
GET /referrals/earnings Earning records with commission breakdown
POST /referrals/payout Request a payout — body { amount_cents, method }; minimum $50; 400 if it exceeds your available balance
GET /referrals/payouts Your payout request history

Call quality scores

GET /api/calls/{call_id}/quality bearer optional

Per-call quality score produced asynchronously by an LLM-as-judge over a 5-dimension rubric. Returns { call_id, scored_at, score }; score is null while scoring is still pending. You also get the score pushed via the call.scored webhook event.

StatusMeaning
200Call exists (score may be null = pending)
403Authenticated but you don’t own this call
404Call not found

Outbound webhooks — call lifecycle

Register an HTTPS URL and NOVA POSTs a signed JSON payload on each subscribed event. All management endpoints require a Bearer token.

Events

  • call.started — outbound call dispatched
  • call.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 bearer
{
  "url": "https://hooks.example.com/nova",
  "events": ["call.started", "call.ended", "call.transcribed", "call.scored"]
}

The 201 response includes a secretsave it immediately. NOVA stores only its SHA-256 hash; the raw value is never retrievable again.

{
  "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-06-12T08:30:00Z"
}

Delivery payload

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-06-12T08:30:00Z",
  "data": {
    "call_id": "uuid",
    "customer_id": "uuid",
    "started_at": "2026-06-12T08:27:00Z",
    "ended_at": "2026-06-12T08:30:00Z",
    "duration_seconds": 187,
    "outcome": "qualified",
    "goal": "qualify_interest",
    "goal_completed": true,
    "to_phone_e164": "+15555550100"
  }
}

Verifying the signature

X-Nova-Signature is an HMAC-SHA256 over the raw request body. The HMAC key is the SHA-256 hex digest of your raw secret — hash your stored token first, then HMAC. Compare with a constant-time function.

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 backoff — waits of 1s, 5s, 30s between attempts, 4 attempts total, 10-second timeout per attempt. After the final failure the delivery is marked failed and not retried.

Management endpoints

EndpointBehaviour
GET /v1/webhooks List your webhooks (secrets never returned); ?include_inactive=true for soft-deleted ones
DELETE /v1/webhooks/{id} Soft-delete (is_active=false) — 204, idempotent
GET /v1/webhooks/{id}/deliveries Recent deliveries with status_code, attempt_count, error_message (limit 1–500, default 50)
POST /v1/webhooks/{id}/test Sends a fake call.started payload end-to-end; 400 on inactive webhooks

Constraints

  • HTTPS only — http:// URLs are rejected
  • Private / internal addresses are rejected (localhost, RFC1918, link-local)
  • Unknown event types are rejected with 400

Inbound portal webhooks — Property Finder & Bayut

NOVA can receive lead webhooks directly from UAE property portals and dispatch a qualification call within seconds of the inquiry. The receiver acknowledges in milliseconds and dispatches the call in the background.

Partnership-gated. Both receivers ship disabled and return 503 until the integration contract and shared HMAC secret are configured for your account. Contact ceo@novalabs.ae to enable portal ingestion.

POST /webhooks/property-finder hmac signed

Signature header: X-PropertyFinder-Signature with value sha256=<hex> — HMAC-SHA256 of the raw request body using the shared secret.

POST /webhooks/bayut hmac signed

Signature header: X-Bayut-Signature, same sha256=<hex> HMAC convention.

Verification & replay protection

  • Signatures are verified with a constant-time comparison; anything that doesn’t match — or doesn’t start with sha256= — is rejected with 401.
  • Replay protection: deliveries are deduplicated on the portal event ID. A replayed event ID gets an idempotent 200 acknowledgement without re-dispatching a call.
  • Malformed payloads return 400; a valid signed payload returns 200 with { accepted, lead_id | event_id, elapsed_ms }.

Response codes

StatusMeaning
200Lead accepted (or duplicate — idempotent ack)
400Payload failed schema validation
401Missing or invalid HMAC signature
503Receiver not enabled / secret not configured

Gated features — available on request

Two surfaces ship behind per-account feature flags and return 404 until enabled for your user. Check GET /me/features for your account’s flags, and contact ceo@novalabs.ae to request access.

Custom voice cloning — /voice/*

Clone your own voice for outbound agent calls. The flow is consent-first: a signed consent record is required before any audio is accepted, enrolment is limited to one attempt per 24 hours, and audio uploads are capped at 16 MB. Enrolment is not available from the EU/EEA/UK (451) pending data-protection compliance. Revoking a profile deletes the cloned voice at the provider.

EndpointPurpose
GET /voice/consentCurrent consent version + text
POST /voice/consentSign consent (typed name) — required before enrolment
POST /voice/enrollUpload enrolment audio (multipart) — returns 202 + profile ID
GET /voice/status/{id}Processing status: processing / ready / failed
POST /voice/preview/{id}Synthesize a short preview (≤300 chars) with the clone
POST /voice/activate/{id}Use this voice for your outbound calls
GET /voice/profilesList your voice profiles
DELETE /voice/{id}Revoke the profile + delete the provider-side clone

Bring-your-own Twilio — /twilio/*

Place calls from your own Twilio numbers. Credentials are stored encrypted (only the last 4 of the Account SID is ever returned), and number ownership must be proven via SMS OTP before a number can go active. OTP sends are rate-limited (3 per 10 minutes, 10 per 24 hours per number).

EndpointPurpose
POST /twilio/credentialsStore/replace your encrypted API key triplet
POST /twilio/credentials/validateLive-check the credentials against Twilio
GET /twilio/credentialsStatus envelope (status + SID last4 only)
DELETE /twilio/credentialsRevoke credentials + tear down imported numbers/trunks
GET /twilio/numbers/availableList numbers on your Twilio account
POST /twilio/numbers/importProvision a SIP trunk for one of your numbers
POST /twilio/numbers/{id}/send-otpSend the ownership-verification SMS code
POST /twilio/numbers/{id}/verifyVerify with the OTP — activates the number
POST /twilio/numbers/{id}/set-defaultUse this number as outbound caller ID
GET /twilio/numbersList your imported numbers
DELETE /twilio/numbers/{id}Revoke a number + its trunk

MCP — drive NOVA from AI agents

NOVA ships a Model Context Protocol server so AI assistants (Claude, Cursor, and any MCP-capable agent) can drive the platform directly — register accounts, dispatch qualification calls, check call status, and read results as tools instead of raw HTTP.

Setup, the tool catalogue, and configuration examples live in the repo: github.com/Kazemkhani/nova-mcp. The MCP server is a thin layer over the same public API documented on this page — everything it can do, you can do with plain HTTPS.

Errors

Errors follow the FastAPI convention — a JSON body with a detail field:

{ "detail": "Rate limit exceeded. Max 5 requests per 60 seconds." }
StatusWhen
400Validation failure (consent, phone format, missing goal fields, bad webhook URL…)
401Missing/invalid credentials or webhook signature
402Plan or trial minute limit exceeded
403Authenticated but not authorised for this resource
404Not found, expired (TTL), or feature not enabled for your account
409Conflict (duplicate email, already-imported number, stale consent version)
422Request body failed schema validation
429Rate limited — check Retry-After where present
451Feature unavailable in your region (voice cloning in EU/EEA/UK)
502Upstream provider error (payment, telephony, voice provider)
503Surface not configured / disabled (portal webhooks, payment service)

Operational endpoints

GET /health no auth

Shallow liveness: { status, timestamp, active_calls, ttl_seconds }.

GET /health/deep no auth

Dependency probes (database + voice infrastructure), each capped at 2 seconds. 200 when all probes pass, 503 with a per-check breakdown when any fail.

GET /finetune/status no auth

Read-only snapshot of the nightly fine-tune pipeline — latest run status, baseline vs. candidate score, and the currently promoted model alias.