Developer API
Two API-key surfaces: one for submitting recordings to be analysed, and one
used by nextneural_coach to have a simulated call scored.
Authorization: Bearer nn_k_your_key_here
Submit analyses
POST /api/v1/sales/analyses · scope sales:analyses:write
Submits up to ten recordings by URL. The audio is fetched server-side, so whatever hosts it must be reachable from the binary.
{
"items": [
{
"audio_url": "https://recordings.example/call-1042.mp3",
"reference_id": "crm-9981",
"customer_name": "Priya Kumar",
"customer_phone": "+918888888888",
"customer_segment": "winback",
"direction": "outbound"
}
]
}
| Field | Required | Limit |
|---|---|---|
audio_url | ✓ | 1–500 characters |
reference_id | — | ≤ 200 — your identifier, for idempotency |
customer_name | — | ≤ 255 |
customer_phone | — | ≤ 40 |
customer_segment | — | ≤ 100 |
direction | — | inbound or outbound |
items must contain 1 to 10 entries.
Response
{
"accepted": 2,
"rejected": 1,
"results": [
{ "index": 0, "reference_id": "crm-9981", "call_id": 1042, "call_uuid": "sc1a2b3c-…" },
{ "index": 1, "reference_id": "crm-9982", "call_id": 1043, "call_uuid": "sc4d5e6f-…", "warning": "customer segment created" },
{ "index": 2, "reference_id": "crm-9983", "status": "rejected", "error": "audio_url could not be fetched" }
]
}
Partial success: index maps each result back to your items array, and
accepted + rejected sum to its length. A rejected item can be resubmitted
on its own without resending the batch.
Use reference_id — resubmitting the same one is recognised rather than
creating a duplicate analysis, which is what makes a retry after a network
failure safe.
Calls land in the active project, in status pending. Analysis is
asynchronous; poll the next endpoint.
| Status | Cause |
|---|---|
400 | Validation failed — the message names the field |
403 | The key lacks sales:analyses:write |
500 | {"detail": "failed to provision data source"} |
Get an analysis
GET /api/v1/sales/analyses/{call_id} · scope sales:analyses:read
call_id is the integer returned at submission.
{
"call_id": 1042,
"call_uuid": "84c31f06-533d-445d-826a-01945b42eb6a",
"reference_id": "crm-9981",
"status": "completed",
"error": null,
"analysis": {
"transcript": [
{ "speaker": "agent", "text": "Hi, is that Priya?" }
],
"summary": "Customer asked about delivery timing.",
"sentiment": "positive",
"outcome": "follow_up_scheduled",
"key_details": [
{ "label": "Promised date", "value": "2026-09-27" }
],
"custom_fields": { "budget_confirmed": true },
"scores": { "opening": 3 },
"goal_status": "achieved",
"goal_reasoning": "Follow-up booked.",
"is_flagged": false,
"duration_seconds": 113,
"direction": "outbound",
"customer": {
"id": 1,
"uuid": "e086be62-4c5f-4627-be9a-893a1043a966",
"name": "Priya Kumar",
"phone": "+919000000001",
"segment": "winback"
}
},
"created_at": "2026-09-22T09:41:02.118+05:30",
"updated_at": "2026-09-22T09:41:02.118+05:30"
}
The envelope carries the identifiers and processing state; everything the
analysis produced sits under analysis, which is null until status is
completed. error explains a failed status.
Inside analysis the extracted fields are custom_fields — not
custom_field_values as the console
call object names them. key_details is an
array of {label, value} objects here.
Poll until status is completed or failed. See
statuses.
| Status | Cause |
|---|---|
404 | {"detail": "Analysis not found"} — wrong id, or another organization's |
Field labels
GET /api/v1/sales/extraction-field-labels · scope sales:analyses:read
The names and types behind the keys in custom_field_values, so an integration
can label them without hardcoding your configuration:
{
"fields": [
{ "uuid": "ef1a2b3c-…", "name": "budget_confirmed", "field_type": "boolean" }
]
}
Simulation judging
POST /api/nn-intel/simulations/judge · scope sales:simulations:judge
Scores a transcript that never happened on a phone — this is how
nextneural_coach has a practice call judged by the same
extraction a real call receives, so a trainee's score is comparable to live
performance.
{
"transcript": [
{ "speaker": "agent", "text": "Hi, is that Priya?", "start": 0.0 },
{ "speaker": "customer", "text": "Yes, who's calling?", "start": 2.4 }
],
"call_type": "outbound",
"scenario_objective": "Book a product demo with a price-sensitive SMB owner.",
"hidden_state": { "budget": 40000, "mood": "sceptical" },
"objections": ["too expensive", "already have a supplier"],
"knowledge_context": "Standing Desk Pro is ₹52,000 with a 3-year warranty.",
"rubric_fields": [
{
"id": "objection_handling",
"name": "Objection handling",
"field_type": "number",
"description": "Did the rep address the objection with evidence?",
"min_value": 0,
"max_value": 3
}
],
"include_org_signals": true
}
| Field | Required | Notes |
|---|---|---|
transcript | ✓ | At least one segment |
call_type | — | inbound or outbound |
scenario_objective | — | What the trainee was supposed to achieve |
hidden_state | — | What the simulated customer knew but did not volunteer |
objections | — | Objections the scenario was meant to raise |
knowledge_context | — | Facts the answer should be grounded in |
rubric_fields | — | Criteria to score against; id is required on each |
include_org_signals | — | Also apply the org's own extraction fields. Absent means true |
No call record is created — this scores and returns.
| Status | Cause |
|---|---|
400 | Validation failed |
403 | The key lacks sales:simulations:judge |
503 | {"detail": "analysis pipeline is not running"} |
sales:simulations:judge is deliberately separate from
sales:analyses:write, so the key issued to the coach can score simulations
without also being able to create call records in your analysis history.
Scopes
| Endpoint | Scope |
|---|---|
POST /api/v1/sales/analyses | sales:analyses:write |
GET /api/v1/sales/analyses/{call_id} | sales:analyses:read |
GET /api/v1/sales/extraction-field-labels | sales:analyses:read |
POST /api/nn-intel/simulations/judge | sales:simulations:judge |