VitrinaAPI

Write emails with an assistant

A connected assistant drafts the email; a person reviews it and sends it.

An email draft is an email someone leaves ready without sending it: recipient, subject, body and signature, saved for a person to review. It shows up in the inbox, under «Borradores».

A person can write one from Vitrina, or their own AI assistant can. That assistant connects through the Vitrina MCP, an API key or a personal token. Saving a draft never sends it: sending is a separate step, and it is always done by a person, or a credential acting as one.

Reading needs conversations:read. Saving, editing and discarding need conversations:write. Sending needs messages:send too, the same condition as sending a message.

The recommended path for an assistant connected over MCP:

  1. Read GET /copilot/email-voice, and the contact's history.
  2. Write the email in that voice.
  3. Save it with POST /email-drafts.
  4. Let a person review it and send it.

The workspace's voice

Before writing, an assistant can read GET /copilot/email-voice. It makes no model call: it gathers data the workspace already holds, so the draft sounds like the team and not like whoever is writing it.

curl "https://api.vitrinadev.com/api/v1/copilot/email-voice?contact_id=22222222-0000-4000-8000-000000000001" \
  -H "Authorization: Bearer $VITRINA_KEY"
{
  "data": {
    "locale": "es",
    "style_guide": "Cercano y claro. Tuteamos. Frases cortas. Nunca prometemos plazos que no controlamos.",
    "agent_instructions": "Eres el asistente del equipo. Respondes con calidez y precisión, tuteas y cierras siempre con un próximo paso concreto.",
    "agent": { "id": "33333333-0000-4000-8000-000000000001", "name": "Asistente" },
    "sample_emails": [
      {
        "subject": "Tu cotización",
        "body_text": "Hola Pedro,\n\nTe adjunto la cotización que conversamos. Cualquier duda me escribes por aquí o al WhatsApp.\n\nUn abrazo,",
        "sent_at": "2026-09-20T13:40:00.000Z"
      }
    ],
    "signature": {
      "id": "44444444-0000-4000-8000-000000000001",
      "name": "Firma principal",
      "text": "Persona del equipo\nEquipo comercial"
    },
    "contact": {
      "id": "22222222-0000-4000-8000-000000000001",
      "name": "María González",
      "email": "[email protected]"
    }
  }
}

Each field carries a different piece.

FieldWhat it carries
localeThe language to write in
style_guideThe workspace's own style guide, if it has one
agent_instructionsThe persona of the AI agent assigned to email: its tone, not its channel mechanics. Truncated at 6,000 characters
sample_emailsUp to five real emails the team recently sent, deduplicated, each truncated at 1,200 characters
signatureThe caller's default signature
contactThe contact, when contact_id is passed

The sample emails belong to the workspace, not to a person. Almost no human reply goes through Vitrina's own composer, so the only thing measurable is how the team writes.

Save a draft

POST /email-drafts saves an email without sending it.

curl -X POST https://api.vitrinadev.com/api/v1/email-drafts \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"contact_id":"22222222-0000-4000-8000-000000000001","subject":"Resumen de lo que conversamos hoy","body_text":"Hola María,\n\nTe dejo por escrito lo que revisamos hoy. Quedamos en coordinar la próxima reunión para el [fecha].\n\nSaludos,"}'
{
  "data": {
    "id": "5d5d5d5d-0000-4000-8000-000000000001",
    "contact": {
      "id": "22222222-0000-4000-8000-000000000001",
      "name": "María González",
      "email": "[email protected]"
    },
    "conversation_id": null,
    "to_address": "[email protected]",
    "cc": [],
    "subject": "Resumen de lo que conversamos hoy",
    "body_html": "<p>Hola María,</p><p>Te dejo por escrito lo que revisamos hoy. Quedamos en coordinar la próxima reunión para el [fecha].</p><p>Saludos,</p>",
    "body_text": "Hola María,\n\nTe dejo por escrito lo que revisamos hoy. Quedamos en coordinar la próxima reunión para el [fecha].\n\nSaludos,",
    "signature_id": null,
    "origin": "mcp",
    "created_by": {
      "id": "11111111-0000-4000-8000-000000000001",
      "name": "Persona del equipo"
    },
    "created_at": "2026-09-24T14:02:11.000Z",
    "updated_at": "2026-09-24T14:05:40.000Z"
  }
}

Every field is optional: a draft is work in progress. The recipient is contact_id, which wins when to_address is also set, or a bare address when the contact doesn't exist yet. body_text is the plain text, up to 8,000 characters; body_html is the rich version, optional. signature_id takes a saved signature's id, "none" for no signature, or is left out for the sender's default at send time. The signature never goes inside the body.

origin is set by the server, never by the request body. It is mcp for an API key, a personal token or a connected app, and human for a person with an open session in Vitrina.

Read, list and edit

GET /email-drafts lists drafts, newest first. mine=true, the default, returns only the caller's own. mine=false returns the whole workspace's. The exception is a role with narrowed record visibility: that role always sees only its own, regardless of mine. The list is keyset-paginated: next_cursor comes back once and is passed back as cursor.

{
  "data": [{ "id": "5d5d5d5d-0000-4000-8000-000000000001", "subject": "Resumen de lo que conversamos hoy" }],
  "next_cursor": null,
  "meta": { "pagination": { "nextCursor": null, "limit": 20 } }
}

GET /email-drafts/{id} returns one. PATCH /email-drafts/{id} changes only the fields sent, under the same rules as POST; conversation_id is never edited there. A draft the caller can't see answers 404, the same as one that no longer exists.

Send

POST /email-drafts/{id}/send sends the draft the same way POST /conversations/email does. It resolves the workspace's sending mailbox, opens the conversation under human control, assigns it to the sender and appends the sender's signature. It needs conversations:write and, on top of that, messages:send: this is the moment a message leaves Vitrina.

The draft is gone the instant the email leaves. If the send fails, it stays exactly as it was: a refusal never loses the text. Sending it twice creates nothing extra: the second request finds no draft and answers 404.

With no sending mailbox resolved, the answer is 422 EMAIL_SENDER_UNRESOLVED. A draft carrying conversation_id, meaning a reply inside a thread, can't be sent from here yet: it answers 422, naming the field in error.field_errors.

Trap

Sending needs a person behind it

Saving a draft works with a plain workspace API key, with nobody behind it. Sending doesn't: the credential has to resolve to a person, whether a session, a personal token or a connected app a person authorised. Without that, the answer is 400.

curl -X POST https://api.vitrinadev.com/api/v1/email-drafts/5d5d5d5d-0000-4000-8000-000000000001/send \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
{
  "data": {
    "conversation": {
      "id": "bbbbbbbb-0000-4000-8000-000000000001",
      "display_id": "C-132",
      "channel": "email",
      "status": "open",
      "handler": "human",
      "assignee_user_id": "11111111-0000-4000-8000-000000000001"
    },
    "message": {
      "id": "eeeeeeee-0000-4000-8000-000000000002",
      "content": "Hola María, te dejo por escrito lo que revisamos hoy.",
      "delivery_status": "sent"
    },
    "contact": {
      "id": "22222222-0000-4000-8000-000000000001",
      "created": false,
      "email": "[email protected]"
    }
  }
}

Idempotency-Key works like on any other published POST: retrying with the same key returns the same response instead of sending the email twice. It is described in Errors.

Discard

DELETE /email-drafts/{id} deletes it. There's no undo.

curl -X DELETE https://api.vitrinadev.com/api/v1/email-drafts/5d5d5d5d-0000-4000-8000-000000000001 \
  -H "Authorization: Bearer $VITRINA_KEY"

It answers 204, with no body.

On this page