Configure the agent that talks
Publish the draft, wire up its tools and take change requests.
GET /ai-agents lists the agents that answer for the workspace when nobody on the team writes first. Each one carries a system_prompt, a model and the tools it may call. It also carries the skills it knows how to apply and the knowledge base it may quote from.
Reading needs ai_agents:read; writing, ai_agents:write.
Draft and live version
Every agent has two faces. The system_prompt, the model and the tools already wired are what runs right now. Anything starting with draft_ is a change in progress that nobody else sees yet.
curl https://api.vitrinadev.com/api/v1/ai-agents/a3a3a3a3-0000-4000-8000-000000000001 \
-H "Authorization: Bearer $VITRINA_KEY"{
"data": {
"id": "a3a3a3a3-0000-4000-8000-000000000001",
"name": "Sales assistant",
"status": "active",
"is_default": true,
"model": "deepseek/deepseek-v4.1-flash",
"system_prompt": "You are a sales assistant. Answer briefly and warmly.",
"published_version_id": "a3a3a3a3-1000-4000-8000-000000000007",
"draft_system_prompt": null,
"draft_updated_at": null,
"created_at": "2026-08-04T19:07:05.226Z",
"updated_at": "2026-09-17T14:49:02.893Z"
}
}model is almost never sent. The platform picks and manages each agent's model, and the UI never asks for it. Sending model is an escape hatch and not an open catalogue.
Editing the draft
curl -X PUT https://api.vitrinadev.com/api/v1/ai-agents/a3a3a3a3-0000-4000-8000-000000000001/draft \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"system_prompt": "You are a sales assistant. You now also offer financing.",
"knowledge_tags": ["hours", "warranty", "financing"]
}'None of this reaches the runtime yet. PUT /ai-agents/{id}/system-prompt is a shortcut over the same thing: it edits only the prompt, staged the same way. GET /ai-agents/{id}/draft reads the pending draft, and DELETE discards it without touching what's live.
Publishing
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/a3a3a3a3-0000-4000-8000-000000000001/publish \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{}'Publishing copies the draft onto what runs, snapshots a version (GET /ai-agents/{id}/versions) and fires ai_agent.publish.
There's a brake. If the agent has an enabled golden Agent Evals suite whose last verdict is blocked or stale, the call answers 409 instead of publishing. Check the brake before you automate a publish:
curl https://api.vitrinadev.com/api/v1/ai-agents/a3a3a3a3-0000-4000-8000-000000000001/publish-gate \
-H "Authorization: Bearer $VITRINA_KEY"{
"data": {
"gate": "clear",
"suite": {
"id": "b6b6b6b6-0000-4000-8000-000000000001",
"name": "Golden",
"kind": "golden"
},
"last_run": {
"pass_rate": 91.2,
"hard_fails": 0
}
}
}With force: true the publish skips the brake, and it lands in the audit log with who did it.
POST /ai-agents/{id}/versions/{version}/restore loads a past version into the draft, to review before publishing. POST /ai-agents/{id}/versions/{version}/rollback does both at once: it loads and publishes, putting that version live immediately.
Skills
A skill is a reusable playbook attached to one or more agents: "how to offer time slots", "how to ask for a reference number before confirming". It lives in a workspace library and not inside one agent.
curl -X PUT https://api.vitrinadev.com/api/v1/skills/a4a4a4a4-0000-4000-8000-000000000001 \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "When the customer wants to book, offer ONLY slots from the nearest branch and confirm before reserving."
}'Editing a skill is content only and it applies instantly. There's no draft or publish step for a skill: the change reaches every agent it's attached to on their next turn.
POST /ai-agents/{id}/skills attaches one that already exists, and DELETE /ai-agents/{id}/skills/{skillId} detaches it without deleting it. DELETE /skills/{id} does delete it, as a soft delete. It leaves the library and every agent instantly.
Tools
GET /ai-agents/tools-catalog lists what this workspace may wire today, filtered by vertical and connected integrations. PUT /ai-agents/{id}/tools replaces the whole set with tool_names, and applies live, not to the draft: the agent's next turn already sees the change.
Custom tools
When the API the agent needs to call is the workspace's own, there's no code to write. The Tool Store teaches the call as a template.
curl -X POST https://api.vitrinadev.com/api/v1/custom-tools \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "get_weather",
"description": "Look up the current weather for a city",
"parameters": [
{ "name": "city", "type": "string", "description": "City name", "required": true }
],
"request_template": {
"method": "GET",
"url": "https://api.example.com/weather?city=${param.city}"
}
}'Parameter values are interpolated into the URL, the headers or the body as ${param.NAME}. A credential is referenced as ${secret.NAME} and never written into the template. POST /custom-tools/{id}/test makes the real call, since there's no sandbox mode, and stores every invocation redacted for audit.
When the target API needs authentication, the credential lives on its own, under /tool-credentials:
curl -X POST https://api.vitrinadev.com/api/v1/tool-credentials \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "weather_api_key", "kind": "api_key", "value": "wapi_live_abc123wx91" }'{
"data": {
"id": "a7a7a7a7-0000-4000-8000-000000000002",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"name": "weather_api_key",
"kind": "api_key",
"value_preview": "••••wx91",
"metadata": {},
"created_by": "11111111-0000-4000-8000-000000000001",
"created_at": "2026-09-12T10:00:00.000Z",
"updated_at": "2026-09-12T10:00:00.000Z",
"last_used_at": "2026-09-21T18:04:11.000Z",
"rotation_grace_until": null
}
}The plaintext value is never shown again. Every read answers only value_preview, a few masked characters, the same as an API key. Rotating is a PATCH with a new value. The old value keeps working for another 24 hours, so a call already in flight doesn't break mid-rotation.
Knowledge base
An agent quotes from what it has attached and not from the whole workspace library:
curl https://api.vitrinadev.com/api/v1/ai-agents/a3a3a3a3-0000-4000-8000-000000000001/knowledge \
-H "Authorization: Bearer $VITRINA_KEY"POST /ai-agents/{id}/knowledge either uploads a new file straight onto the agent, as multipart, or attaches one already in the library with { "kb_file_id": "…" }. DELETE /ai-agents/{id}/knowledge/{fileId} detaches it, and the file keeps existing and keeps backing any other agent. How a file is uploaded, generated and replaced lives in Knowledge base.
Voice channel
curl https://api.vitrinadev.com/api/v1/ai-agents/a3a3a3a3-0000-4000-8000-000000000001/voice-channel \
-H "Authorization: Bearer $VITRINA_KEY"It returns the line's state, the greeting, the voice picked from a curated catalogue and the handoff number to a human. Only the brand segment of the greeting is editable. The AI disclosure and the recording notice are composed server-side, and they can't be removed. voice_id is a key from that catalogue and not a workspace-owned id.
PUT saves the change and pushes it to the speech layer. If the push fails it's left pending, and POST /ai-agents/{id}/voice-channel/propagate retries it.
Change requests: when a customer asks for something different
A change request (agent_change_request) is a requirement somebody stated in their own words: "don't use em dashes", "if something isn't available, tell them we'll find it". It's distinct from a finding the platform noticed on its own.
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/a3a3a3a3-0000-4000-8000-000000000001/change-requests \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "verbatim": "Don'"'"'t use em dashes, it sounds robotic.", "reporter_kind": "client_via_member" }'It can also be filed as audio (multipart/form-data, field file). It gets transcribed, and the transcript is the verbatim, so nothing is stored that couldn't be turned into text.
The lifecycle:
| Status | Meaning |
|---|---|
received | What the customer said was logged |
grounded | Which conversation it happened in was confirmed |
reproduced | A scenario reproduced the complaint against the version it was about |
proposed | A fix was proposed |
applied | The fix was applied |
verified | The fix was confirmed to work |
ya_cumple | The agent already did it right, and the complaint was about an older version |
harness | The fix belongs to the platform and not to this agent's prompt |
POST .../change-requests/{crId}/ground searches which conversations it happened in, touching nothing. POST .../ground/confirm is the human's answer to that search. POST .../scenario authors a test scenario from the request and runs it against the version complained about. POST .../propose investigates and stores a proposed fix, once the scenario has reproduced the problem red.
Events
| Event | When |
|---|---|
ai_agent.publish | A draft was published as the live config |
skill.created / .updated / .deleted | The skill library changed |
custom_tool.created / .updated / .deleted | The Tool Store changed |
ai_agent.publish carries ai_agent_id, name and updated_at, just enough to know which agent changed and when. The publish's content is read with GET /ai-agents/{id}/versions.