Errors and conventions
Error shape
Every error carries a human-readable detail:
{ "detail": "not found" }
Some carry an additional machine-readable flag, so a client can act on the condition rather than matching on prose:
{ "detail": "organization setup is required before using the app", "needs_org_setup": true }
| Flag | Meaning |
|---|---|
needs_org_setup | The account has no organization yet |
needs_waitlist_approval | The organization is pending or rejected |
must_change_password | An invited user has not set their own password |
Status codes
| Status | Meaning |
|---|---|
400 Bad Request | Malformed body, or a field that failed validation |
401 Unauthorized | Missing, invalid, expired, or revoked credential |
403 Forbidden | Valid credential, insufficient role / scope / org state |
404 Not Found | No such resource in the caller's organization |
409 Conflict | State conflict — duplicate email, campaign already running |
429 Too Many Requests | Too many failed OTP attempts |
500 Internal Server Error | Unhandled failure; details are logged, not returned |
Cross-tenant requests return 404
Asking for a resource that belongs to another organization returns 404, the
same as one that does not exist. The two are deliberately indistinguishable —
returning 403 would let a caller probe for another tenant's UUIDs by watching
which ones answered differently.
Internal errors reveal nothing
A 500 always returns the same body:
{ "detail": "internal error" }
The underlying error is logged server-side. Database messages carry column names and query fragments, so they are never sent to a client.
Identifiers
Resources are addressed by UUID in public-facing routes, never by the integer primary key. UUIDs are unguessable, which is what lets a recording URL or a widget key be handed out without becoming an enumeration vector.
Two binaries differ in their console routes:
| Binary | Identifier in paths |
|---|---|
nextneural_converse | :uuid throughout |
nextneural_coach | :uuid throughout |
nextneural_intel | :id — integer primary key |
nextneural_assist | :id for customers and agents, :uuid for calls |
Pagination
List endpoints that paginate take limit and offset:
| Param | Default |
|---|---|
limit | 50 |
offset | 0 |
They return the page alongside the unpaginated total, so a client can render "page 2 of 18" without a second request:
{ "calls": [ "..." ], "total": 891 }
Dates and times
Timestamps are RFC 3339. Values read back from the database carry the server's
UTC offset and sub-second precision rather than a Z suffix:
2026-09-22T09:36:21.936466+05:30
Parse with a full RFC 3339 parser rather than matching on Z. Values the
application generates itself — webhook timestamp, transcript ts — are
emitted as UTC:
2026-09-22T09:00:04Z
Date filters (date_from, date_to) accept either a full RFC 3339 timestamp
or a bare YYYY-MM-DD. A bare date_to covers the whole of that day.
nextneural_converse campaign calling windows are evaluated against the server's local
time, not a per-organization timezone. On a UTC server, a window of 09:00–18:00
means 14:30–23:30 IST. Set TZ on the service accordingly.
Rate limits
These binaries do not apply per-key HTTP rate limiting. The pacing that exists is domain-specific rather than a middleware:
nextneural_conversecampaigns dial atcalls_per_minute(default 10)- Signup OTP verification locks out after repeated wrong codes (
429) - Voice sample generation refuses a second concurrent run with
409
If you need request-rate limiting, terminate it at your reverse proxy.
Content types
Request bodies are JSON unless the endpoint is documented as multipart. File
uploads — avatars, call recordings, knowledge base documents — are
multipart/form-data.