Appearance
panel-qualifier
82 endpoints.
GET /api/v1/panel/qualifier/agents
List Agents
200 — Successful Response
json
{}POST /api/v1/panel/qualifier/agents
Create Agent
Request body
json
{
"properties": {
"name": {
"type": "string",
"title": "Name"
},
"slug": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Slug"
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Description"
},
"agent_type": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Agent Type"
},
"template": {
"type": "string",
"title": "Template",
"default": "BANT"
}
},
"type": "object",
"required": [
"name"
],
"title": "AgentCreate"
}201 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/agents/{slug}
Get Agent
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}PATCH /api/v1/panel/qualifier/agents/{slug}
Update Agent
Edit an agent's display name / description. The slug (URL) stays stable so links and references don't break.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"properties": {
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Name"
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Description"
}
},
"type": "object",
"title": "AgentUpdate",
"description": "Edit an agent's display fields (the slug/URL stays stable)."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}DELETE /api/v1/panel/qualifier/agents/{slug}
Delete Agent
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/agents/{slug}/versions
List Versions
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/agents/{slug}/versions
Save Version
Compile + persist a new immutable version (spec §3.3). Operator prompt edits persist with the version.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"properties": {
"definition": {
"additionalProperties": true,
"type": "object",
"title": "Definition"
},
"compiled_system_prompt": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Compiled System Prompt"
},
"notes": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Notes"
},
"activate": {
"type": "boolean",
"title": "Activate",
"default": true
}
},
"type": "object",
"required": [
"definition"
],
"title": "SaveVersionRequest"
}201 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/trainer-rules
Get Trainer Rules
200 — Successful Response
json
{}PUT /api/v1/panel/qualifier/trainer-rules
Set Trainer Rules
Set tenant-wide Trainer rules applied to EVERY agent's interview. Takes effect on new Trainer chats (the CLI keeps a session's original system prompt on resume).
Request body
json
{
"properties": {
"rules": {
"type": "string",
"title": "Rules",
"default": ""
}
},
"type": "object",
"title": "TrainerRulesIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}PUT /api/v1/panel/qualifier/agents/{slug}/persona
Set Agent Persona
Save the Agent Persona AND fold it into THIS agent's definition. The persona text is stored workspace-wide (seeds the Trainer + overrides every call, as before); on top of that a focused reconcile pass maps it onto the agent's draft — persona fields (name/tone/language), any greeting/disclosure it states, and standing rules as instruction items — so saving the persona visibly updates the Agent Definition.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"properties": {
"rules": {
"type": "string",
"title": "Rules",
"default": ""
}
},
"type": "object",
"title": "TrainerRulesIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/agents/{slug}/persona/apply
Apply Agent Persona
Apply the CURRENT workspace Agent Persona to this agent's definition if it isn't already applied (idempotent via the persona hash — a no-op once applied). The Trainer calls this on load so the persona is applied automatically by default, no manual Save.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/agents/{slug}/trainer
Trainer Chat
One Trainer interview turn. Resumes the agent's persistent Trainer session; persists the evolving draft definition so re-entry resumes (spec §3.1).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"properties": {
"message": {
"type": "string",
"title": "Message"
},
"image_paths": {
"items": {
"type": "string"
},
"type": "array",
"title": "Image Paths"
}
},
"type": "object",
"required": [
"message"
],
"title": "TrainerMessageIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/agents/{slug}/trainer/fork
Trainer Fork Chat
Fork the interview at an operator bubble with an edited prompt. Seeds a fresh Claude session with the transcript up to the fork point (CLI sessions can't rewind), then continues. The fork becomes the agent's active Trainer session.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"properties": {
"message": {
"type": "string",
"title": "Message"
},
"history": {
"items": {
"properties": {
"role": {
"type": "string",
"title": "Role"
},
"text": {
"type": "string",
"title": "Text"
}
},
"type": "object",
"required": [
"role",
"text"
],
"title": "TrainerTurnIn",
"description": "One chat bubble in the operator/trainer transcript (for forking)."
},
"type": "array",
"title": "History"
},
"image_paths": {
"items": {
"type": "string"
},
"type": "array",
"title": "Image Paths"
}
},
"type": "object",
"required": [
"message"
],
"title": "TrainerForkIn",
"description": "Fork the interview at an operator bubble: replay the transcript up to (but\nnot including) that bubble into a fresh session, then send the edited message."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/agents/{slug}/trainer/reset
Trainer Reset
Start a NEW chat: drop the persistent Trainer session + transcript so the next message begins a fresh conversation. The built-up DRAFT DEFINITION is PRESERVED — a new chat must never discard the agent's knowledge/persona/criteria (never lose data).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/agents/{slug}/trainer/upload
Trainer Upload
Upload a file for the Trainer — an image (logo/brochure/screenshot, spec §3.2 brand cues) OR a document (price list / PDF / CSV). The file is extracted into the Knowledge base RIGHT HERE (one focused call) and persisted, so a price list lands as entries without a slow two-call chat turn. Returns {path, filename, added, definition}: if added>0 the frontend shows the new entries; if added==0 (e.g. a logo) it attaches the path to the next chat message for brand-cue analysis instead.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}PATCH /api/v1/panel/qualifier/agents/{slug}/knowledge
Update Knowledge
Replace the draft's merged Knowledge base grid (product/company knowledge + training instructions as a list of rows) — a way to train the agent independent of the Trainer chat (spec §3.2 items 12–13). Persists to the draft so re-entry resumes it; these compile into the runtime system prompt on Save version.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"properties": {
"entries": {
"items": {
"properties": {
"type": {
"type": "string",
"title": "Type",
"default": "knowledge"
},
"topic": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Topic",
"default": ""
},
"content": {
"type": "string",
"title": "Content",
"default": ""
},
"source": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Source",
"default": ""
},
"enabled": {
"type": "boolean",
"title": "Enabled",
"default": true
}
},
"type": "object",
"title": "KnowledgeEntryIn",
"description": "One row of the merged Knowledge base grid."
},
"type": "array",
"title": "Entries"
}
},
"type": "object",
"title": "KnowledgeUpdate",
"description": "Replace the draft's merged knowledge/instructions grid (Knowledge base sidebar),\nindependent of the Trainer chat. Blank-content rows are dropped server-side."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/knowledge-library
List Knowledge Library
The tenant's shared company knowledge — profiles, price lists, websites.
200 — Successful Response
json
{}GET /api/v1/panel/qualifier/knowledge-library/{source_id}
Get Knowledge Source
One source WITH its extracted entries (the library preview).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
source_id | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}PATCH /api/v1/panel/qualifier/knowledge-library/{source_id}
Update Knowledge Source
Rename a source, note what it is for, or hand-edit its extracted rows.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
source_id | path | yes | string |
Request body
json
{
"properties": {
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Name"
},
"notes": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Notes"
},
"entries": {
"anyOf": [
{
"items": {
"properties": {
"type": {
"type": "string",
"title": "Type",
"default": "knowledge"
},
"topic": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Topic",
"default": ""
},
"content": {
"type": "string",
"title": "Content",
"default": ""
},
"source": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Source",
"default": ""
},
"enabled": {
"type": "boolean",
"title": "Enabled",
"default": true
}
},
"type": "object",
"title": "KnowledgeEntryIn",
"description": "One row of the merged Knowledge base grid."
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Entries"
}
},
"type": "object",
"title": "KnowledgeSourceUpdate",
"description": "Rename a library source, note what it is for, or hand-edit its extracted rows."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}DELETE /api/v1/panel/qualifier/knowledge-library/{source_id}
Delete Knowledge Source
Remove a source from the library. Agents keep working — a source they no longer resolve is simply dropped from their next compile (nothing was copied into them).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
source_id | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/knowledge-library/upload
Upload Knowledge Source
Upload a company profile / price list / other document into the shared library.
The file LANDS FIRST — name, type and filename are in the library the moment the bytes are stored — and is read into Knowledge-base rows by a background job the library polls (status "extracting"). Reading a full price list is one focused model call that runs for minutes; doing it inside this request left the operator staring at a spinner with nothing listed, which reads as an upload that failed. Extraction is best-effort: a source that yields nothing is still there, and can be re-read or written up by hand.
201 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/knowledge-library/website
Add Website Source
Add a website to the shared library. A real browser crawls the site — who the company is, how to reach it and the FAQs it publishes, plus (only when include_products is asked for) the item groups and every item with its price — which takes far longer than a request may, so the source is created immediately as "crawling" and filled in by a background task the library polls (see _run_instance_crawl).
Request body
json
{
"properties": {
"url": {
"type": "string",
"title": "Url"
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Name"
},
"include_products": {
"type": "boolean",
"title": "Include Products",
"default": false
}
},
"type": "object",
"required": [
"url"
],
"title": "WebsiteSourceIn",
"description": "Add a website to the shared library — it is read and extracted server-side."
}201 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/knowledge-library/{source_id}/refresh
Refresh Knowledge Source
Re-read the source (the stored file, or the website again) and REPLACE its entries. Every agent using it picks the new rows up the next time its prompt is compiled.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
source_id | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}PUT /api/v1/panel/qualifier/agents/{slug}/knowledge-sources
Set Agent Knowledge Sources
Choose which company-knowledge sources this agent uses. Only the ids that really belong to this tenant are stored, and nothing is copied into the draft: the rows are merged in when the prompt is compiled, so switching one off removes it again.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"properties": {
"source_ids": {
"items": {
"type": "string"
},
"type": "array",
"title": "Source Ids"
}
},
"type": "object",
"title": "AgentSourcesIn",
"description": "Which library sources an agent uses (ids from /knowledge-library). Ids that are\nnot this tenant's are ignored rather than rejected."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/agent-skills
List Agent Skills
The Agent Skills catalogue — the behavioural modules that can be switched on per agent (see app/apps/portal/services/qualifier_agent_skills.py).
Two sources, deliberately unequal. The five BUILT-IN skills are code: their wording ships with the product and the compiler resolves them from code, so a row can never change what a live agent says. CUSTOM skills, added on the Agent Skills grid since 2026-08-31, do come from the database — safe for the same reason the original code-only ruling was really about, since a version's compiled prompt is frozen at publish and an edited skill only reaches an agent on its next save, as a new version with a number to point at.
Refreshed here (not merely read from cache) so the Trainer's picker never offers a skill that was just deleted, or misses one just added.
200 — Successful Response
json
{}PUT /api/v1/panel/qualifier/agents/{slug}/skills
Set Agent Skills
Choose which Agent Skills this agent runs with. Unknown slugs are dropped rather than rejected (the catalogue can move under a saved definition), and the stored order is catalogue order so the compiled prompt is deterministic.
Saves to the DRAFT only — like knowledge sources, nothing reaches a live call until a new version is published, which is what keeps a behaviour change auditable to a version rather than appearing mid-call.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"properties": {
"skills": {
"items": {
"type": "string"
},
"type": "array",
"title": "Skills",
"default": []
}
},
"type": "object",
"title": "AgentSkillsIn",
"description": "The full set of Agent Skill slugs this agent should run with — REPLACES\nthe current selection, same contract as AgentSourcesIn above."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/agents/{slug}/compile
Compile Agent
Deterministically compile a definition + validate + preview the opening (spec §3.1).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"properties": {
"definition": {
"additionalProperties": true,
"type": "object",
"title": "Definition"
}
},
"type": "object",
"required": [
"definition"
],
"title": "CompileRequest"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}PATCH /api/v1/panel/qualifier/agents/{slug}/persona-fields
Update Persona Fields
Set the agent's persona fields (name/tone/language/voice/call direction) DIRECTLY on the draft — no LLM, no reconcile, purely the values given. Non-destructive: only the fields you pass change; the rest of the definition (knowledge, criteria, …) is untouched. This is the 'Edit persona' control in the Agent Definition.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"properties": {
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Name"
},
"tone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Tone"
},
"language": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Language"
},
"tts_voice": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Tts Voice"
},
"call_direction": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Call Direction"
},
"agent_type": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Agent Type"
},
"purpose": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Purpose"
}
},
"type": "object",
"title": "PersonaFieldsIn",
"description": "Direct edit of the agent's persona fields from the Agent Definition panel. Only\nprovided (non-None) fields are changed; everything else is left exactly as-is."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}PATCH /api/v1/panel/qualifier/agents/{slug}/fields
Update Fields
Set multiple definition fields at once by dot-path — a batch of {path: value} (e.g. {"opening.greeting": "...", "qualification.threshold.min": 0.7}). No LLM; NON-DESTRUCTIVE: only the given paths change, everything else is left exactly as-is. Backs the per-section Edit dialogs in the Agent Definition. Type-safe via _apply_path (won't corrupt structure).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"type": "object",
"additionalProperties": true,
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/agents/{slug}/apply-edit
Apply Edit
Apply a single suggested edit (field_path + proposed) to the draft definition — backs the Eval Report's one-click "Apply to Trainer" (spec §4.3).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"type": "object",
"additionalProperties": true,
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/scenarios
List Scenarios
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
agent_id | query | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/scenarios
Create Scenario
Request body
json
{
"properties": {
"name": {
"type": "string",
"title": "Name"
},
"slug": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Slug"
},
"agent_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Agent Id"
},
"persona": {
"additionalProperties": true,
"type": "object",
"title": "Persona"
},
"ground_truth": {
"additionalProperties": true,
"type": "object",
"title": "Ground Truth"
},
"edge_cases": {
"items": {
"type": "string"
},
"type": "array",
"title": "Edge Cases"
},
"seed": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Seed"
},
"channel": {
"type": "string",
"title": "Channel",
"default": "inbound"
}
},
"type": "object",
"required": [
"name"
],
"title": "ScenarioCreate"
}201 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}PATCH /api/v1/panel/qualifier/scenarios/{slug}
Update Scenario
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"properties": {
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Name"
},
"persona": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"title": "Persona"
},
"ground_truth": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"title": "Ground Truth"
},
"edge_cases": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Edge Cases"
},
"seed": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Seed"
},
"enabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Enabled"
},
"channel": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Channel"
}
},
"type": "object",
"title": "ScenarioUpdate"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}DELETE /api/v1/panel/qualifier/scenarios/{slug}
Delete Scenario
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/scenarios/generate
Generate Scenario
Studio-generated caller persona + ground truth from a free-text brief (spec §4.2).
Request body
json
{
"properties": {
"agent_id": {
"type": "string",
"title": "Agent Id"
},
"brief": {
"type": "string",
"title": "Brief"
}
},
"type": "object",
"required": [
"agent_id",
"brief"
],
"title": "GenerateScenarioRequest"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/run
Run Test Call
Run a single test call against the agent's current Trainer config (draft) — so the test uses the latest Trainer data + the org Agent Identity — evaluate it, persist, and return the transcript + Eval Report (spec §4).
Request body
json
{
"properties": {
"agent_id": {
"type": "string",
"title": "Agent Id"
},
"scenario_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Scenario Id"
},
"scenario": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"title": "Scenario"
},
"mode": {
"type": "string",
"title": "Mode",
"default": "sim"
},
"max_turns": {
"type": "integer",
"title": "Max Turns",
"default": 30
}
},
"type": "object",
"required": [
"agent_id"
],
"title": "RunSimRequest"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/call-out
Call Out
Place a REAL outbound test call — the manual "Call now" trigger from Test Studio/the agent editor. See :func:place_outbound_call for the shared logic.
Request body
json
{
"properties": {
"agent_id": {
"type": "string",
"title": "Agent Id"
},
"to_number": {
"type": "string",
"title": "To Number"
},
"lead_name": {
"type": "string",
"title": "Lead Name",
"default": ""
}
},
"type": "object",
"required": [
"agent_id",
"to_number"
],
"title": "CallOutRequest"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/telephony/test-register
Telephony Test Register
Ask the call center AI worker to attempt a SIP REGISTER with the org's SIP account and report whether it succeeded — the AI Call Center → Telephony Settings "Register" button.
Request body
json
{
"properties": {
"sip_username": {
"type": "string",
"title": "Sip Username",
"default": ""
},
"sip_password": {
"type": "string",
"title": "Sip Password",
"default": ""
},
"sip_domain": {
"type": "string",
"title": "Sip Domain",
"default": ""
},
"sip_server": {
"type": "string",
"title": "Sip Server",
"default": ""
}
},
"type": "object",
"title": "TelephonyTestIn",
"description": "AI Call Center → Telephony Settings \"Register\" button. Tests the supplied values (so the\noperator can verify before saving); blanks fall back to the saved settings."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/inbound-numbers
List Inbound Numbers
The numbers an agent can claim, each with the agent whose claim currently wins here.
Empty means no DID is wired to the AI extension yet — the UI says so rather than offering numbers that would ring nowhere. The holder shown is the most recent claim on THIS site (see the module note): another site's newer claim wins the actual call, and only the worker can see across sites.
200 — Successful Response
json
{}PUT /api/v1/panel/qualifier/agents/{slug}/inbound-number
Set Agent Inbound Number
Claim one of the AI-routed numbers for THIS agent (or drop the claim it holds).
The pool is shared, so this takes the number from nobody: other agents keep their claims, and this one — being the newest — is what the number answers as from now on (module note above). Re-picking a number somebody else has since claimed hands it back, which is exactly how two people take turns testing on one DID.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string |
Request body
json
{
"properties": {
"number": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Number"
}
},
"type": "object",
"title": "InboundNumberIn",
"description": "Allocate a number to this agent, or release it (number = null / \"\")."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/inbound-call
Inbound Call
Resolve a dialled DID to the agent that answers it, and hand the worker a token.
"The agent that answers it" = the one that claimed the number most recently on this site; the worker compares that claim time against the other sites' before it answers.
This is the ONE qualifier endpoint with no user session: an inbound call has no operator behind it. It is authenticated by the shared worker secret (the worker and this API run on the same box, loopback only) and it mints nothing more than a normal access token for the agent's own owner, so every downstream call — /voice-turn/open, the plan gates, billing — behaves exactly as it does for an operator-placed call.
Request body
json
{
"properties": {
"to_number": {
"type": "string",
"title": "To Number"
},
"from_number": {
"type": "string",
"title": "From Number",
"default": ""
}
},
"type": "object",
"required": [
"to_number"
],
"title": "InboundCallIn",
"description": "The AI worker announcing an inbound call it just answered."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/run-suite
Run Suite
Run every enabled scenario for an agent — regression pass/fail dashboard (spec §4.3).
Request body
json
{
"properties": {
"agent_id": {
"type": "string",
"title": "Agent Id"
}
},
"type": "object",
"required": [
"agent_id"
],
"title": "RunSuiteRequest"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/plans
List Plans
The package ladder — what an upgrade prompt offers. Unpublished tiers are included so a tenant sitting on a retired plan still sees their own.
200 — Successful Response
json
{}GET /api/v1/panel/qualifier/plan
Get Plan
This tenant's package, its effective limits, and this period's usage — the Plan & Usage page in one request.
features is the full registry with a granted flag rather than only the granted keys, so the page can render the whole ladder with ticks and crosses (and name what an upgrade would add) without a second round trip.
200 — Successful Response
json
{}PUT /api/v1/panel/qualifier/plan
Assign Plan
Put this tenant on a package (or change/suspend the one they're on).
Owner-gated rather than admin: this is the row that decides what the tenant is billed AND whether they can see the app at all, so it isn't an ordinary admin edit. Selling through the shop writes the same row (see services/qualifier_billing.py); this is the manual path.
Request body
json
{
"properties": {
"plan_code": {
"type": "string",
"title": "Plan Code"
},
"status": {
"type": "string",
"title": "Status",
"default": "active"
},
"billing_day": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Billing Day"
},
"included_minutes_override": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Included Minutes Override"
},
"max_agents_override": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Max Agents Override"
},
"max_concurrent_override": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Max Concurrent Override"
},
"extra_features": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Extra Features"
},
"extra_languages": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Extra Languages"
},
"notes": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Notes"
},
"skip_trial": {
"type": "boolean",
"title": "Skip Trial",
"default": false
}
},
"type": "object",
"required": [
"plan_code"
],
"title": "PlanAssignIn",
"description": "Put a tenant on a package. ``plan_code`` is one of ``QualPlan.code``."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/onboarding
Get Onboarding
What this org still has to do before its AI Call Center does real work.
Added 2026-09-12 when the product opened to customer orgs: every step below already had its own page, but nothing told a new customer which of them were still missing — and each one fails in a way that looks like a broken product (an agent with no number never rings, a campaign with no agent never dials). Read-only and cheap: counts, no PBX writes, no worker calls. numbers reads the PBX (via _claimable_numbers) and degrades to 0 if it is down.
200 — Successful Response
json
{}GET /api/v1/panel/qualifier/usage/history
Usage History
Closed billing periods for this tenant — the metered history behind past invoices (see QualUsagePeriod on why only closed periods are stored).
200 — Successful Response
json
{}POST /api/v1/panel/qualifier/plan/convert
Convert Trial
End this tenant's AI Call Center trial and move onto the paid plan.
This is the moment the setup fee (waived during the trial, snapshotted into setup_fee_due when the trial started) becomes payable. Nothing in this codebase auto-invoices AccItem.setup_fee today — every setup fee is quoted and billed by hand (see AccItem.setup_fee's own docstring) — so this endpoint only flips the state; an operator still raises the actual invoice and records it on setup_fee_invoiced_on/setup_fee_invoice_ref from the qualifier-subscriptions grid, same as QualUsagePeriod's own invoiced_at/invoice_ref bookkeeping.
200 — Successful Response
json
{}POST /api/v1/panel/qualifier/payg/signup
Payg Signup
Buy AICC-PAYG (R0/month) — the standard once-off-purchase pipeline, same as a top-up, just free. A R0 order needs no Paystack round-trip: submit the invoice directly (nothing is owed) so it still raises a normal Pending Activation for staff to process — free doesn't mean unaudited, and that Activation is what actually creates this tenant's QualTenantPlan row (see provisioning.call_provider).
200 — Successful Response
json
{}POST /api/v1/panel/qualifier/balance/topup
Topup Balance
Buy a fixed AI Call Center balance top-up. Same pipeline as any other once-off portal purchase — reusing source="storefront" means the EXISTING website_order webhook branch (app/api/paystack.py) and book_website_order handle payment -> invoice -> submit -> Activation with zero new webhook code. The balance itself credits when staff process that Activation (provisioning.call_provider's ai_call_center_payg_topup branch) — not instant on payment; see this feature's plan for why (consistency with every other product's standard activation gate, decided over speed on 2026-08-05).
Request body
json
{
"properties": {
"denomination": {
"type": "string",
"title": "Denomination"
}
},
"type": "object",
"required": [
"denomination"
],
"title": "BalanceTopupIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/calls/{call_id}/recording
Get Recording
Stream a call's recording for the Call Logs player.
Served through the API rather than statically by nginx on purpose: these are recorded customer conversations, so every read goes through the same tenant-scoped auth as the transcript it belongs to.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
call_id | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/calls/{call_id}/recording
Upload Recording
Store the call center AI worker's mixed recording of a finished call.
Posted by the worker at teardown (call-center-ai-worker/app/brain.py upload_recording) with the token it already holds for this call, so no new auth path exists. answered_at is the worker's first-audio timestamp — the authoritative answer moment, more accurate than the backend's "when /voice-turn/open ran", so re-metering off it here corrects any ring time that slipped into the duration.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
call_id | path | yes | string | |
answered_at | query | no | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/retention/purge
Retention Purge
POPIA retention (spec §2): delete the transcript turns + eval reports and scrub captured lead fields for calls older than each agent's data_capture.retention_days. Runs daily via the in-process scheduler; this triggers it on demand for the caller's tenant. Returns purge counts.
200 — Successful Response
json
{}POST /api/v1/panel/qualifier/voice-turn/open
Voice Open
Start a live call: create the session and return the opening line.
The opening is DETERMINISTIC — the scripted greeting (+ disclosure) straight from the definition — so we DON'T spend an LLM round-trip on it. That round-trip (a cold Groq call) can take 10-15s, which the caller hears as DEAD AIR the moment they answer, and they hang up before the agent ever speaks. The greeting is fixed script text anyway (the LLM was only echoing it verbatim). The Groq brain is stateless and replays the transcript each turn, so persisting this opening as turn 0 gives the next (real) turn full continuity — there's no live session to warm here.
Request body
json
{
"properties": {
"agent_slug": {
"type": "string",
"title": "Agent Slug"
},
"mode": {
"type": "string",
"title": "Mode",
"default": "voice"
},
"to_number": {
"type": "string",
"title": "To Number",
"default": ""
},
"lead_name": {
"type": "string",
"title": "Lead Name",
"default": ""
},
"balance": {
"type": "string",
"title": "Balance",
"default": ""
}
},
"type": "object",
"required": [
"agent_slug"
],
"title": "VoiceOpenRequest"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/voice-turn/{call_id}
Voice Turn
Run one agent turn for an in-progress call (rehydrated from the call row).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
call_id | path | yes | string |
Request body
json
{
"properties": {
"caller_text": {
"type": "string",
"title": "Caller Text"
},
"detected_language": {
"type": "string",
"title": "Detected Language",
"default": ""
}
},
"type": "object",
"required": [
"caller_text"
],
"title": "VoiceTurnRequest"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/voice-turn/{call_id}/end
Voice Turn End
Finalize a live call when the CALLER hangs up (SIP BYE) — the worker calls this so the backend doesn't leave the session 'running' until the reaper sweeps it ~2 min later (the "agent still active" symptom). Closes the row AND — the important part — never loses an agreed callback: if the call is callback-mode, the caller showed interest, a callback day/time can be extracted from the transcript, and nothing was booked yet, it books it NOW (mid-call booking can miss when the caller drops the line before the model emits a wrap-up action). Idempotent.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
call_id | path | yes | string |
Request body
json
{
"properties": {
"reason": {
"type": "string",
"title": "Reason",
"default": "caller_hangup"
}
},
"type": "object",
"title": "VoiceEndRequest"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/voice-turn/{call_id}/stream
Voice Turn Stream
Streaming variant of voice-turn: emits NDJSON — {"t":"d","x":<delta>} lines as the agent speaks, then a final {"t":"done", ...} with the enforced decision. Lets the worker TTS sentence-by-sentence (talk on the first sentence). Persists the turn at the end; get_db commits after the streamed body completes.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
call_id | path | yes | string |
Request body
json
{
"properties": {
"caller_text": {
"type": "string",
"title": "Caller Text"
},
"detected_language": {
"type": "string",
"title": "Detected Language",
"default": ""
}
},
"type": "object",
"required": [
"caller_text"
],
"title": "VoiceTurnRequest"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/calls
List Calls
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
agent_id | query | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/calls/{call_id}
Get Call
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
call_id | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/calls/{call_id}/stop
Stop Call
Force-end a live (running) chat/call. Marks the session closed so the voice worker / sim loop stops driving it and it drops out of the active-chats list.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
call_id | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/calls/{call_id}/review
Review Call
Scan one logged call and offer concrete improvements — the Call Logs row action.
Reviewed against the agent VERSION the call actually ran on, not today's draft: otherwise it flags problems already fixed and proposes edits already made. The result is cached on the call (metrics["review"], no schema migration — same place the consent/disclosure flags ride) so re-opening a reviewed call is free; ?force=1 re-scans. Suggested edits carry the agent slug so each one can be pushed straight into the Trainer draft via /agents/{slug}/apply-edit.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
call_id | path | yes | string | |
force | query | no | boolean |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/calls/{call_id}/review/applied
Mark Review Edit Applied
Tick a review's suggested edit off once it has been pushed into the Trainer draft. Persisted on the stored review so re-opening the report shows it as applied rather than offering it again — a free-text edit applied twice appends a DUPLICATE knowledge entry (see apply_edit's else branch).
field_path locates the edit and is always the one the REVIEW proposed. The operator may reword the value or re-point the path before applying, so applied_field_path/applied_proposed record what actually went into the draft — otherwise re-opening the report would show a tick against a suggestion that isn't what was applied.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
call_id | path | yes | string |
Request body
json
{
"type": "object",
"additionalProperties": true,
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/calls/clear
Clear Calls
Clear the call log: soft-delete every ended call for the tenant (optionally scoped to one agent). Live ("running") calls are left untouched so in-progress chats aren't broken. Soft delete keeps booked callbacks intact (they SET NULL).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
agent_id | query | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/callbacks
List Callbacks
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
frm | query | no | ||
to | query | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/callbacks
Create Callback
Request body
json
{
"properties": {
"callback_at": {
"type": "string",
"format": "date-time",
"title": "Callback At"
},
"call_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Call Id"
},
"lead_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Lead Name"
},
"lead_phone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Lead Phone"
},
"lead_email": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Lead Email"
},
"notes": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Notes"
},
"lead_data": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"title": "Lead Data"
}
},
"type": "object",
"required": [
"callback_at"
],
"title": "CallbackCreate"
}201 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}PATCH /api/v1/panel/qualifier/callbacks/{callback_id}
Update Callback
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
callback_id | path | yes | string |
Request body
json
{
"properties": {
"callback_at": {
"anyOf": [
{
"type": "string",
"format": "date-time"
},
{
"type": "null"
}
],
"title": "Callback At"
},
"status": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Status"
},
"lead_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Lead Name"
},
"lead_phone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Lead Phone"
},
"lead_email": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Lead Email"
},
"notes": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Notes"
}
},
"type": "object",
"title": "CallbackUpdate"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}DELETE /api/v1/panel/qualifier/callbacks/{callback_id}
Delete Callback
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
callback_id | path | yes | string |
204 — Successful Response
422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/commitments/send-test-warning
Send Test Commitment Warning
Send the daily payment-deadline warning template right now, to a chosen email/phone, so an admin can check wording/delivery before the real job fires (see scheduler_engine._handler_debtor_commitment_warnings, which runs this same content for every commitment due today). Not tied to a real DebtorCommitment row — 'today' and the supplied name/amount (or sensible test defaults) stand in for a real one.
Request body
json
{
"properties": {
"to_email": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "To Email"
},
"to_phone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "To Phone"
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Name"
},
"amount": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Amount"
}
},
"type": "object",
"title": "CommitmentTestWarningIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/commitments
List Commitments
The commitment calendar's feed, optionally narrowed to one debtor.
customer_id/status back the Create Commitment modal's "this customer already promised to pay on …" panel: it asks for this AccCustomer's scheduled rows so the operator sees the open promise BEFORE booking a second one, instead of only finding out via the same-day 409.
The unfiltered calendar feed stays tenant-scoped, but a customer_id lookup deliberately is NOT — exactly like the shield the AR Ageing grid's Commitment Date column reads (:func:customer_account_status.open_commitments_by_customer, which skips tenant_id because AccCustomer is per-site/cross-tenant). Scoping this one by tenant would let the grid show a commitment date that the modal then claims doesn't exist.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
frm | query | no | ||
to | query | no | ||
customer_id | query | no | ||
status | query | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/commitments
Create Commitment
Request body
json
{
"properties": {
"promised_at": {
"type": "string",
"format": "date-time",
"title": "Promised At"
},
"promised_amount": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Promised Amount"
},
"call_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Call Id"
},
"deal_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Deal Id"
},
"customer_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Customer Id"
},
"lead_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Lead Name"
},
"lead_phone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Lead Phone"
},
"lead_email": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Lead Email"
},
"notes": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Notes"
},
"lead_data": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"title": "Lead Data"
},
"allow_duplicate": {
"type": "boolean",
"title": "Allow Duplicate",
"default": false
},
"proof_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Proof Id"
}
},
"type": "object",
"required": [
"promised_at"
],
"title": "CommitmentCreate"
}201 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}PATCH /api/v1/panel/qualifier/commitments/{commitment_id}
Update Commitment
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
commitment_id | path | yes | string |
Request body
json
{
"properties": {
"promised_at": {
"anyOf": [
{
"type": "string",
"format": "date-time"
},
{
"type": "null"
}
],
"title": "Promised At"
},
"promised_amount": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Promised Amount"
},
"status": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Status"
},
"lead_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Lead Name"
},
"lead_phone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Lead Phone"
},
"lead_email": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Lead Email"
},
"notes": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Notes"
},
"proof_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Proof Id"
}
},
"type": "object",
"title": "CommitmentUpdate"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}DELETE /api/v1/panel/qualifier/commitments/{commitment_id}
Delete Commitment
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
commitment_id | path | yes | string |
204 — Successful Response
422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/commitments/proofs
Upload Commitment Proof
Verify a bank proof of payment and stage it for a commitment.
Slow by design — the model reads the document (a cold CLI start plus a multi-page scan runs into tens of seconds), and the answer is worth waiting for. A rejected file never becomes a row, so a 422 here means the document itself was refused; the message says why.
201 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}DELETE /api/v1/panel/qualifier/commitments/proofs/{proof_id}
Discard Commitment Proof
Throw away a proof that was staged and not used — the collector re-picked the file, or closed the modal. Only ever removes an UNBOUND proof: one that bought a commitment date is evidence and stays.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
proof_id | path | yes | string |
204 — Successful Response
422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/commitments/{commitment_id}/proof
List Commitment Proofs
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
commitment_id | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/commitments/proofs/{proof_id}/content
Get Commitment Proof Content
Serve the raw file inline, for preview/download by an authenticated client (an <img src> can't send the Authorization header).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
proof_id | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/sms/status
Sms Status
Whether SMS sending is configured (so the UI can warn before composing).
200 — Successful Response
json
{}GET /api/v1/panel/qualifier/sms/balance
Sms Balance
Carrier (Panacea) SMS credit balance — ported from the portal's SyncSmsBalance.
200 — Successful Response
json
{}GET /api/v1/panel/qualifier/sms/inbox
Sms Inbox
Received SMS since last_id (Panacea messages_get) — ported from FetchSmsInbox.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
last_id | query | no | integer |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/panel/qualifier/sms/message-status/{message_id}
Sms Message Status
Delivery status of a previously sent message (Panacea message_status).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
message_id | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/sms/send
Send Single Sms
Text ONE person: a typed number, or the number captured on a call/callback.
This used to be a bulk blast (sendBulkSms, "Bulk SMS" in the UI). Owner, 2026-08-11: "limit the ai bulk_sms to single sms". The cap is enforced HERE, at the route, rather than by only changing the composer — the endpoint is reachable with a session cookie and a curl, so a UI-only limit is a suggestion, not a rule.
Numbers are de-duped BEFORE the count, so naming the same person twice (a pasted number that is also the one captured on the call) is one send, not a 400. Two DIFFERENT people is the error case.
Still returns the per-recipient shape {enabled, sent, failed, results[]} for the one send, so callers, logs and the composer's result panel didn't have to learn a second response format for a one-element list.
Request body
json
{
"properties": {
"message": {
"type": "string",
"title": "Message"
},
"recipients": {
"items": {
"type": "string"
},
"type": "array",
"title": "Recipients"
},
"callback_ids": {
"items": {
"type": "string"
},
"type": "array",
"title": "Callback Ids"
},
"call_ids": {
"items": {
"type": "string"
},
"type": "array",
"title": "Call Ids"
}
},
"type": "object",
"required": [
"message"
],
"title": "SmsSendRequest",
"description": "SINGLE SMS send. ``recipients`` is a pasted/typed number; ``callback_ids``\nand ``call_ids`` pull the captured lead phone number from that callback/call.\nSources are merged, normalised and de-duped, and the result must resolve to\nexactly ONE number — the route 400s on more (owner, 2026-08-11: \"limit the ai\nbulk_sms to single sms\").\n\nThe list-shaped fields are kept rather than collapsed to scalars: they are\nstill the natural way to say \"this pasted number OR the one captured on that\ncall\", the de-dupe is what makes two sources resolving to the same person a\nsingle send rather than an error, and narrowing the wire format would break\nevery stored client for no gain. The CAP is the product rule; the shape is\njust how recipients are addressed."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/panel/qualifier/email/send
Send Custom Email
Email ONE lead something you typed (owner, 2026-08-11: "add send custom email functionality").
The sibling of /sms/send above, and deliberately shaped like it: same recipient resolution, same one-at-a-time rule, same 402-on-missing-feature. Unlike the callback confirmation (_send_callback_confirmation) there is no template — subject and body are whatever the sender wrote — so this is the one path where tenant-authored text goes out over the site's own SMTP identity. Two guards follow from that:
- the body is escaped and rendered to HTML here, never accepted as HTML (see EmailSendRequest); and
- it goes through
mailer.send_emailwith akind, so every send lands in the email log next to the automated ones and is attributable.
mailer.send_email returns False (never raises) when SMTP is off or the send fails, which is reported as {"ok": false, "enabled": ...} rather than a 500 — same best-effort contract the callback mailer has.
Request body
json
{
"properties": {
"subject": {
"type": "string",
"title": "Subject"
},
"body": {
"type": "string",
"title": "Body"
},
"recipients": {
"items": {
"type": "string"
},
"type": "array",
"title": "Recipients"
},
"callback_ids": {
"items": {
"type": "string"
},
"type": "array",
"title": "Callback Ids"
},
"call_ids": {
"items": {
"type": "string"
},
"type": "array",
"title": "Call Ids"
}
},
"type": "object",
"required": [
"subject",
"body"
],
"title": "EmailSendRequest",
"description": "Custom email to one lead (owner, 2026-08-11: \"add send custom email\nfunctionality\"). Same recipient-resolution shape as SmsSendRequest — a typed\naddress, or the address captured on a call/callback — and the same\nexactly-one rule.\n\n``body`` is plain text the sender typed. It is rendered to HTML by the route\n(paragraph-per-blank-line, everything escaped) rather than accepted as HTML:\nthis composer is reachable by any tenant admin, and taking raw HTML from it\nwould put an unreviewed authoring surface into mail we send under the site's\nown domain and SPF/DKIM."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}