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 and the tools it may call. It also carries the skills it knows how to apply and the knowledge base it may quote from.

Video · in Spanish · 1:19Nobody goes unanswered

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):

ResourceRead withFieldSend back as
DraftGET /ai-agents/{id}/draftdraft_versionexpected_version on PUT /ai-agents/{id}/draft, expected_draft_version on POST /ai-agents/{id}/publish
Live agentGET /ai-agents/{id} or GET /ai-agents/{id}/system-promptconfig_versionexpected_version on PUT /ai-agents/{id}/system-prompt, expected_config_version on POST /ai-agents/{id}/publish
SkillGET /skills/{id}versionexpected_version on PUT /skills/{id}
Knowledge fileGET /kb-filesversionform 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:

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