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:
- Read
GET /copilot/email-voice, and the contact's history. - Write the email in that voice.
- Save it with
POST /email-drafts. - 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.
| Field | What it carries |
|---|---|
locale | The language to write in |
style_guide | The workspace's own style guide, if it has one |
agent_instructions | The persona of the AI agent assigned to email: its tone, not its channel mechanics. Truncated at 6,000 characters |
sample_emails | Up to five real emails the team recently sent, deduplicated, each truncated at 1,200 characters |
signature | The caller's default signature |
contact | The 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.