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 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 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,
"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"
}
}Vitrina picks and manages each agent's AI model. It is not part of the API: it is neither read nor sent.
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. GET /ai-agents/{id}/draft reads the pending draft, and DELETE discards it without touching what's live. Careful with PUT /ai-agents/{id}/system-prompt: it does not go through the draft, it writes the live prompt immediately.
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.
Publishing never runs, waits on or depends on evaluations: they run only when someone asks for them.
Two editors at once
If more than one person or integration edits the same agent (several API keys, the app and a script), send the version you read with every write. Without it, whoever saves second overwrites the first without warning.
Every read carries its version in the body and in the X-Config-Version header (also ETag):
| Resource | Read with | Field | Send back as |
|---|---|---|---|
| Draft | GET /ai-agents/{id}/draft | draft_version | expected_version on PUT /ai-agents/{id}/draft, expected_draft_version on POST /ai-agents/{id}/publish |
| Live agent | GET /ai-agents/{id} or GET /ai-agents/{id}/system-prompt | config_version | expected_version on PUT /ai-agents/{id}/system-prompt, expected_config_version on POST /ai-agents/{id}/publish |
| Skill | GET /skills/{id} | version | expected_version on PUT /skills/{id} |
| Knowledge file | GET /kb-files | version | form field expected_version on PUT /kb-files/{id}/content |
Instead of the field you can send the header If-Match: "12".
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": "…", "expected_version": 10 }'If someone saved after your read, the answer is 409 and nothing is written:
{
"error": {
"code": "VERSION_CONFLICT",
"message": "The draft changed since you read it — someone else saved first. Nothing was written.",
"conflict": {
"resource": "ai_agent.draft",
"reason": "stale_version",
"expected_version": 10,
"current_version": 11,
"changed_fields": ["system_prompt"],
"read_path": "/api/v1/ai-agents/a3a3a3a3-0000-4000-8000-000000000001/draft",
"hint": "GET the draft, merge your edit onto it, and retry with the new draft_version."
}
}
}Read again, merge your change with theirs, and retry with the new version.
Publishing has a second guard. If the draft was started on a live configuration that changed afterwards (someone used PUT /ai-agents/{id}/system-prompt while the draft was waiting), publishing it would erase that change, so it answers 409 with reason: "draft_base_stale". To publish anyway after reviewing, send expected_config_version with the current live version, or discard the draft and stage it again.
Sending the version is optional for compatibility: a write without it works exactly as before and is logged. But without it there is no protection.
Every skill edit leaves an immutable row in GET /skills/{id}/versions with the full content, who wrote it (user or API key), through what (rest, mcp, coach…) and when. And every agent reply records which version produced it: GET /conversations/{id}/agent-runs carries ai_agent_version and the versions of the skills and files that turn used.
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. Send expected_version (see Two editors at once); every edit lands in the history at GET /skills/{id}/versions.
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.