Skip to content

panel-qualifier ​

← All modules

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
source_idpathyesstring

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

NameInRequiredTypeDescription
source_idpathyesstring

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

NameInRequiredTypeDescription
source_idpathyesstring

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

NameInRequiredTypeDescription
source_idpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
agent_idqueryno

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
slugpathyesstring

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

NameInRequiredTypeDescription
call_idpathyesstring

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

NameInRequiredTypeDescription
call_idpathyesstring
answered_atquerynostring

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

NameInRequiredTypeDescription
call_idpathyesstring

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

NameInRequiredTypeDescription
call_idpathyesstring

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

NameInRequiredTypeDescription
call_idpathyesstring

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

NameInRequiredTypeDescription
agent_idqueryno

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

NameInRequiredTypeDescription
call_idpathyesstring

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

NameInRequiredTypeDescription
call_idpathyesstring

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

NameInRequiredTypeDescription
call_idpathyesstring
forcequerynoboolean

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

NameInRequiredTypeDescription
call_idpathyesstring

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

NameInRequiredTypeDescription
agent_idqueryno

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

NameInRequiredTypeDescription
frmqueryno
toqueryno

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

NameInRequiredTypeDescription
callback_idpathyesstring

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

NameInRequiredTypeDescription
callback_idpathyesstring

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

NameInRequiredTypeDescription
frmqueryno
toqueryno
customer_idqueryno
statusqueryno

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

NameInRequiredTypeDescription
commitment_idpathyesstring

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

NameInRequiredTypeDescription
commitment_idpathyesstring

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

NameInRequiredTypeDescription
proof_idpathyesstring

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

NameInRequiredTypeDescription
commitment_idpathyesstring

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

NameInRequiredTypeDescription
proof_idpathyesstring

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

NameInRequiredTypeDescription
last_idquerynointeger

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

NameInRequiredTypeDescription
message_idpathyesstring

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_email with a kind, 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"
}

Lubb One Documentation