VitrinaAPI

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:

StatusMeaning
receivedWhat the customer said was logged
groundedWhich conversation it happened in was confirmed
reproducedA scenario reproduced the complaint against the version it was about
proposedA fix was proposed
appliedThe fix was applied
verifiedThe fix was confirmed to work
ya_cumpleThe agent already did it right, and the complaint was about an older version
harnessThe 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

EventWhen
ai_agent.publishA draft was published as the live config
skill.created / .updated / .deletedThe skill library changed
custom_tool.created / .updated / .deletedThe 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.

On this page