VitrinaAPI

Sending mailboxes

The mailboxes the signed-in teammate may send a new email from: their own and the workspace principal, with which one is the default. Its id is the `from_account_id` of a send or a draft.

Beta
This may change at any time, with a changelog entry and a notice to recent callers — see versioning.

Download the full API pública projection: openapi.json.

MethodPathWhat it does
GET/messaging-accounts/{id}/comment-dm-public-replyRead the public comment reply for AI-judged comment→DMs
PUT/messaging-accounts/{id}/comment-dm-public-replySet or clear the public comment reply for AI-judged comment→DMs
GET/messaging-accounts/{id}/comment-triggersKeyword rules that auto-DM an Instagram commenter, with their fire counters
POST/messaging-accounts/{id}/comment-triggersCreate a keyword rule on an Instagram channel
DELETE/messaging-accounts/{id}/comment-triggers/{trigger_id}Delete a keyword rule
PATCH/messaging-accounts/{id}/comment-triggers/{trigger_id}Edit or disable a keyword rule
GET/messaging-accounts/{id}/email-signatureRead an email mailbox's business signature
PUT/messaging-accounts/{id}/email-signatureSet an email mailbox's business signature (auto | custom | none)
POST/messaging-accounts/emailConnect a mailbox over IMAP/SMTP
GET/messaging-accounts/email-sender/mineMailboxes you can send a new email from
POST/messaging-accounts/email/discoverDetect a mailbox's IMAP/SMTP settings from its address
POST/messaging-accounts/email/testTest IMAP/SMTP credentials

GET /messaging-accounts/{id}/comment-dm-public-reply

Read the public comment reply for AI-judged comment→DMs

The line posted PUBLICLY under an Instagram/Facebook comment after a DM that no keyword rule produced (the classifier judged the commenter interested and the DM was sent). null = no public reply (default). Keyword rules keep their own public_reply_template.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl https://api.vitrinadev.com/api/v1/messaging-accounts/<id>/comment-dm-public-reply \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "public_reply_template": "¡Te escribimos por DM! 📩"
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

PUT /messaging-accounts/{id}/comment-dm-public-reply

Set or clear the public comment reply for AI-judged comment→DMs

Up to 300 characters, posted verbatim under the comment only AFTER the DM went out, never on a comment judged not interested. Visible to everyone: keep it to «¡Te escribimos por DM!», never prices or health details. null or blank clears it. A failed public reply never undoes or retries the DM.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
public_reply_templatestring | nullyesmáx. 300
curl -X PUT https://api.vitrinadev.com/api/v1/messaging-accounts/<id>/comment-dm-public-reply \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "public_reply_template": "¡Te escribimos por DM! 📩"
  }'

Example response (200)

{
  "data": {
    "public_reply_template": "¡Te escribimos por DM! 📩"
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /messaging-accounts/{id}/comment-triggers

Keyword rules that auto-DM an Instagram commenter, with their fire counters

The deterministic tier of the comment→DM pipeline: which published word sends which DM, on which posts. Evaluated BEFORE any model call, which is what makes a post saying «comenta PRECIO» actually answer that word rather than probably answer it. trigger_count counts DMs actually SENT, not comments matched — a match dropped by the hourly cap or refused by the big-account gate sent nothing and appears instead in the comment→DM ledger under its own outcome. A null or empty media_ids means the rule applies to EVERY post on the account.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl https://api.vitrinadev.com/api/v1/messaging-accounts/<id>/comment-triggers \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "8c8c8c8c-0000-4000-8000-000000000001",
      "tenant_id": "6b6b6b6b-0000-4000-8000-000000000001",
      "messaging_account_id": "7a7a7a7a-0000-4000-8000-000000000001",
      "keyword": "info",
      "match_mode": "exact_word",
      "case_sensitive": false,
      "media_ids": null,
      "dm_template": "¡Hola! Gracias por comentar ✨ Te escribo por aquí para contarte más. ¿Qué te gustaría saber?",
      "public_reply_template": "¡Te escribimos por DM! ✨",
      "enabled": false,
      "trigger_count": 0,
      "last_triggered_at": null,
      "created_at": "2026-10-06T14:00:00.000Z",
      "updated_at": "2026-10-06T14:00:00.000Z"
    }
  ]
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /messaging-accounts/{id}/comment-triggers

Create a keyword rule on an Instagram channel

When a public comment on this account contains (or, with exact_word, IS) the keyword, send dm_template VERBATIM as a private reply — no model rewrites it. Creating a rule sends nothing by itself: every DM it later produces still passes the self-comment drop, Meta's one-private-reply-per-comment limit, the 7-day comment window, the 20/hour per-account cap, the muted-contact and big-account gate, and the suppression that keeps the bot off a thread a teammate is handling. Prefer exact_word for anything short: a contains rule on "hora" fires on "ahora". media_ids are META media ids and confine the rule to those posts; omit for every post (comments on ADS included). public_reply_template (optional, ≤300 chars) is posted PUBLICLY under the comment, verbatim, only after the DM went out — keep it to "te escribimos por DM"; a public-reply failure never undoes the DM. A commenter whose contact is blocked/spam or opted out of Instagram messages is skipped (skipped_opted_out); a marketing-only STOP does not count. Instagram accounts only.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
keywordstring—mín. 1, máx. 200
dm_templatestringyesmín. 1, máx. 900
public_reply_templatestring | null—máx. 300
match_modecontains \exact_word \post_keyword
case_sensitiveboolean—
media_idsstring[]—
enabledboolean—
curl -X POST https://api.vitrinadev.com/api/v1/messaging-accounts/<id>/comment-triggers \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "keyword": "info",
    "match_mode": "exact_word",
    "dm_template": "¡Hola! Gracias por comentar ✨ Te escribo por aquí para contarte más. ¿Qué te gustaría saber?",
    "public_reply_template": "¡Te escribimos por DM! ✨",
    "enabled": false
  }'

Example response (201)

{
  "data": {
    "id": "8c8c8c8c-0000-4000-8000-000000000001",
    "tenant_id": "6b6b6b6b-0000-4000-8000-000000000001",
    "messaging_account_id": "7a7a7a7a-0000-4000-8000-000000000001",
    "keyword": "info",
    "match_mode": "exact_word",
    "case_sensitive": false,
    "media_ids": null,
    "dm_template": "¡Hola! Gracias por comentar ✨ Te escribo por aquí para contarte más. ¿Qué te gustaría saber?",
    "public_reply_template": "¡Te escribimos por DM! ✨",
    "enabled": false,
    "trigger_count": 0,
    "last_triggered_at": null,
    "created_at": "2026-10-06T14:00:00.000Z",
    "updated_at": "2026-10-06T14:00:00.000Z"
  }
}

Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /messaging-accounts/{id}/comment-triggers/{trigger_id}

Delete a keyword rule

Permanent, counter included. To retire a finished campaign while keeping the record of how it performed, PATCH enabled: false instead.

ParameterInTypeRequiredConstraints
idpathuuidyes
trigger_idpathuuidyes
curl -X DELETE https://api.vitrinadev.com/api/v1/messaging-accounts/<id>/comment-triggers/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429

PATCH /messaging-accounts/{id}/comment-triggers/{trigger_id}

Edit or disable a keyword rule

Only the fields provided change. enabled: false retires a rule without deleting it — a finished campaign is worth keeping for the next one, and its trigger_count is the record of how the last one went. media_ids: [] clears the post allow-list back to "every post", which is the only way to undo a targeting mistake. A rule on another tenant's channel answers 404, identically to one that never existed.

ParameterInTypeRequiredConstraints
idpathuuidyes
trigger_idpathuuidyes

Body

FieldTypeRequiredConstraints
keywordstring—mín. 1, máx. 200
dm_templatestring—mín. 1, máx. 900
public_reply_templatestring | null—máx. 300
match_modecontains \exact_word \post_keyword
case_sensitiveboolean—
media_idsarray | null—
enabledboolean—
curl -X PATCH https://api.vitrinadev.com/api/v1/messaging-accounts/<id>/comment-triggers/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true
  }'

Example response (200)

{
  "data": {
    "id": "8c8c8c8c-0000-4000-8000-000000000001",
    "tenant_id": "6b6b6b6b-0000-4000-8000-000000000001",
    "messaging_account_id": "7a7a7a7a-0000-4000-8000-000000000001",
    "keyword": "info",
    "match_mode": "exact_word",
    "case_sensitive": false,
    "media_ids": null,
    "dm_template": "¡Hola! Gracias por comentar ✨ Te escribo por aquí para contarte más. ¿Qué te gustaría saber?",
    "public_reply_template": "¡Te escribimos por DM! ✨",
    "enabled": true,
    "trigger_count": 0,
    "last_triggered_at": null,
    "created_at": "2026-10-06T14:00:00.000Z",
    "updated_at": "2026-10-06T14:00:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /messaging-accounts/{id}/email-signature

Read an email mailbox's business signature

The signature this mailbox puts on customer email nobody on the team wrote — AI replies, First-touch, Seguimientos, Recontacto, reminders and API/MCP sends. A person replying from the inbox signs with their own signature and never gets this one.

mode: auto (default — built from the workspace branding: name, hosted logo, brand color, and the published address / phone / site), custom, or none. auto_preview is always the automatic signature (so a settings screen can preview it); effective is what customers get with the current mode (null = no signature). Email mailboxes only (422 otherwise).

ParameterInTypeRequiredConstraints
idpathuuidyes
curl https://api.vitrinadev.com/api/v1/messaging-accounts/<id>/email-signature \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "mode": "auto",
    "custom": null,
    "auto_preview": {
      "html": "<div class=\"vitrina-business-signature\" style=\"margin-top:24px;padding-top:16px;border-top:1px solid #e5e7eb;\"><table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"border-collapse:collapse;\"><tr><td style=\"padding:0 16px 0 0;vertical-align:middle;\"><img src=\"https://media.example.com/logo.png\" alt=\"Automotora Ejemplo\" height=\"40\" style=\"display:block;height:40px;width:auto;max-width:160px;border:0;\"></td><td style=\"vertical-align:middle;font-size:13px;line-height:1.5;color:#4b5563;\"><div style=\"font-size:14px;font-weight:600;color:#1d4ed8;\">Automotora Ejemplo</div><div>Av. Ejemplo 123, Santiago</div><div><a href=\"tel:+56900000000\" style=\"color:#4b5563;text-decoration:none;\">+56 9 0000 0000</a> &middot; <a href=\"https://ejemplo.cl\" style=\"color:#1d4ed8;text-decoration:none;\">ejemplo.cl</a></div></td></tr></table></div>",
      "text": "--\nAutomotora Ejemplo\nAv. Ejemplo 123, Santiago\n+56 9 0000 0000 · ejemplo.cl"
    },
    "effective": {
      "html": "<div class=\"vitrina-business-signature\" style=\"margin-top:24px;padding-top:16px;border-top:1px solid #e5e7eb;\"><table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"border-collapse:collapse;\"><tr><td style=\"padding:0 16px 0 0;vertical-align:middle;\"><img src=\"https://media.example.com/logo.png\" alt=\"Automotora Ejemplo\" height=\"40\" style=\"display:block;height:40px;width:auto;max-width:160px;border:0;\"></td><td style=\"vertical-align:middle;font-size:13px;line-height:1.5;color:#4b5563;\"><div style=\"font-size:14px;font-weight:600;color:#1d4ed8;\">Automotora Ejemplo</div><div>Av. Ejemplo 123, Santiago</div><div><a href=\"tel:+56900000000\" style=\"color:#4b5563;text-decoration:none;\">+56 9 0000 0000</a> &middot; <a href=\"https://ejemplo.cl\" style=\"color:#1d4ed8;text-decoration:none;\">ejemplo.cl</a></div></td></tr></table></div>",
      "text": "--\nAutomotora Ejemplo\nAv. Ejemplo 123, Santiago\n+56 9 0000 0000 · ejemplo.cl"
    }
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

PUT /messaging-accounts/{id}/email-signature

Set an email mailbox's business signature (auto | custom | none)

custom requires html or text; the html is sanitized with the same policy as composed email and its text twin is derived when omitted. auto and none discard a stored custom body. Applies to the next send (other worker processes pick it up within a minute). Returns the same shape as the GET.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
modeauto \custom \none
htmlstring | null—máx. 20000
textstring | null—máx. 5000
curl -X PUT https://api.vitrinadev.com/api/v1/messaging-accounts/<id>/email-signature \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "none"
  }'

Example response (200)

{
  "data": {
    "mode": "none",
    "custom": null,
    "auto_preview": {
      "html": "<div class=\"vitrina-business-signature\" style=\"margin-top:24px;padding-top:16px;border-top:1px solid #e5e7eb;\"><table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"border-collapse:collapse;\"><tr><td style=\"padding:0 16px 0 0;vertical-align:middle;\"><img src=\"https://media.example.com/logo.png\" alt=\"Automotora Ejemplo\" height=\"40\" style=\"display:block;height:40px;width:auto;max-width:160px;border:0;\"></td><td style=\"vertical-align:middle;font-size:13px;line-height:1.5;color:#4b5563;\"><div style=\"font-size:14px;font-weight:600;color:#1d4ed8;\">Automotora Ejemplo</div><div>Av. Ejemplo 123, Santiago</div><div><a href=\"tel:+56900000000\" style=\"color:#4b5563;text-decoration:none;\">+56 9 0000 0000</a> &middot; <a href=\"https://ejemplo.cl\" style=\"color:#1d4ed8;text-decoration:none;\">ejemplo.cl</a></div></td></tr></table></div>",
      "text": "--\nAutomotora Ejemplo\nAv. Ejemplo 123, Santiago\n+56 9 0000 0000 · ejemplo.cl"
    },
    "effective": null
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /messaging-accounts/email

Connect a mailbox over IMAP/SMTP

Creates the email_imap account and stores the credentials encrypted. Same body as /email/test plus naming. The connection is re-tested server-side before anything is created, so a failed connection answers 422 with the per-leg errors (imap / smtp, each with error_kind) and creates nothing.

Flow: POST /messaging-accounts/email/discover (finds the settings; with the password it also returns the verified candidate), optionally POST /messaging-accounts/email/test, then this route with the verified (or chosen) candidate’s imap / smtp and the password on both legs.

This is the raw-credential path. Prefer OAuth whenever discover returns oauth (Gmail, Microsoft 365): connect those through the OAuth sign-in in the app (Conectar Gmail / Microsoft 365), which needs no password.

The password is the mailbox owner’s real password. Sending it through an AI chat puts it in that chat’s transcript — only do it when the owner asks you to. It is stored encrypted, never returned (the account in the response carries no secrets, and later reads redact them) and never written to the audit log. Rate-limited per workspace (429).

Body

FieldTypeRequiredConstraints
emailstringyesemail, máx. 255
imapobjectyes
smtpobjectyes
namestring—mín. 1, máx. 120
from_namestring | null—máx. 120
assignee_ai_agent_idstring | null—
purposeconversations \bank_reconciliation—
curl -X POST https://api.vitrinadev.com/api/v1/messaging-accounts/email \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "name": "Correo de contacto",
    "imap": {
      "host": "mail.automotora-ejemplo.cl",
      "port": 993,
      "secure": true,
      "password": "contraseña-del-buzón"
    },
    "smtp": {
      "host": "mail.automotora-ejemplo.cl",
      "port": 465,
      "secure": true,
      "password": "contraseña-del-buzón"
    }
  }'

Example response (201)

{
  "data": {
    "id": "3f6d2c1e-8a4b-4d7e-9c10-5b2a7e9f0a11",
    "name": "Correo de contacto",
    "kind": "email_imap",
    "channels": [
      "email"
    ],
    "enabled": true,
    "config": {
      "connected_via": "imap_smtp",
      "email": "[email protected]",
      "from_name": null,
      "imap": {
        "host": "mail.automotora-ejemplo.cl",
        "port": 993,
        "secure": true,
        "user": "[email protected]"
      },
      "smtp": {
        "host": "mail.automotora-ejemplo.cl",
        "port": 465,
        "secure": true,
        "user": "[email protected]"
      }
    },
    "created_at": "2026-10-16T14:00:00.000Z"
  }
}

Answers: 201 · 400 · 401 · 403 · 404 · 409 · 422 · 429

GET /messaging-accounts/email-sender/mine

Mailboxes you can send a new email from

Lists the mailboxes the signed-in teammate may send a new email from: their own connected mailbox (the one whose address is their login email) and the workspace principal mailbox. default_id is the one used when a send or a draft does not set from_account_id — their own mailbox if they have one, otherwise the principal. can_connect_own is true when they have no mailbox of their own yet. Pass one of these ids as from_account_id to POST /conversations/email or to an email draft; any other mailbox is refused.

curl https://api.vitrinadev.com/api/v1/messaging-accounts/email-sender/mine \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "options": [
    {
      "id": "0b5f4c2e-7d1a-4e9b-9c3f-2a6d8e1f4b70",
      "name": "María",
      "address": "[email protected]",
      "role": "own"
    },
    {
      "id": "5e2a9d14-3c8b-4f60-a1e7-9b4c2d7f0a35",
      "name": "Ventas",
      "address": "[email protected]",
      "role": "principal"
    }
  ],
  "default_id": "0b5f4c2e-7d1a-4e9b-9c3f-2a6d8e1f4b70",
  "can_connect_own": false
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /messaging-accounts/email/discover

Detect a mailbox's IMAP/SMTP settings from its address

Finds the IMAP and SMTP host, port and TLS mode for an email address the way Thunderbird and Outlook do, trying in order: the MX (Google / Microsoft 365 → oauth hint), the domain's autoconfig, Thunderbird's ISPDB, Outlook autodiscover (what cPanel hosting answers), SRV records, and finally the usual hostnames that accept a connection. The first source with a hit wins; each candidate names its source. Nothing is persisted.

Without password: the ranked candidates. With password: the candidates are also logged in to, in order (at most 4, about 15 s in all), and verified is the first whose IMAP AND SMTP logins passed; otherwise attempts carries each leg's error and error_kind — auth means the server answered and refused the password, unreachable / timeout / tls mean the server could not be reached. When oauth is set (Gmail / Microsoft 365), connect through the OAuth flow instead; no login is tried.

Then connect with POST /messaging-accounts/email, sending verified (or the chosen candidate) with the password on both legs. Finding nothing is 200 with candidates: []. Rate-limited per workspace (429).

Body

FieldTypeRequiredConstraints
emailstringyesemail, máx. 255
passwordstring—mín. 1, máx. 1024
curl -X POST https://api.vitrinadev.com/api/v1/messaging-accounts/email/discover \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "contraseña-del-buzón"
  }'

Example response (200)

{
  "data": {
    "domain": "automotora-ejemplo.cl",
    "oauth": null,
    "candidates": [
      {
        "source": "autodiscover",
        "imap": {
          "host": "mail.automotora-ejemplo.cl",
          "port": 993,
          "secure": true,
          "user": "[email protected]"
        },
        "smtp": {
          "host": "mail.automotora-ejemplo.cl",
          "port": 465,
          "secure": true,
          "user": "[email protected]"
        }
      }
    ],
    "verified": null,
    "attempts": [
      {
        "candidate": {
          "source": "autodiscover",
          "imap": {
            "host": "mail.automotora-ejemplo.cl",
            "port": 993,
            "secure": true,
            "user": "[email protected]"
          },
          "smtp": {
            "host": "mail.automotora-ejemplo.cl",
            "port": 465,
            "secure": true,
            "user": "[email protected]"
          }
        },
        "ok": false,
        "imap": {
          "ok": false,
          "error": "Command failed: Authentication failed.",
          "error_kind": "auth"
        },
        "smtp": {
          "ok": false,
          "error": "Invalid login: 535 Incorrect authentication data",
          "error_kind": "auth"
        }
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /messaging-accounts/email/test

Test IMAP/SMTP credentials

Dials both legs with the supplied credentials and reports each independently — nothing is persisted, so this is the safe way to let someone iterate on a mailbox configuration.

Run POST /messaging-accounts/email/discover first to get the imap / smtp host, port and TLS mode instead of guessing them. If discover returns oauth (Gmail / Microsoft 365), do NOT use this route: have the owner connect it with the OAuth sign-in (Conectar Gmail / Microsoft 365 in the app), which needs no password.

The two legs fail independently: a mailbox can read (IMAP fine) and be unable to send (SMTP wrong), so read both results rather than treating the response as one boolean. error_kind says why a leg failed: auth = the server answered and refused the password, unreachable / timeout / tls = the server could not be reached with those settings. imap.user / smtp.user default to email when omitted, which is right for most providers.

The password is the mailbox owner’s real password. Sending it through an AI chat puts it in that chat’s transcript — only do it when the owner asks you to. It is used for this one test, is never stored by this route, and is never returned or written to the audit log. Rate-limited per workspace (429).

Body

FieldTypeRequiredConstraints
emailstringyesemail, máx. 255
imapobjectyes
smtpobjectyes
curl -X POST https://api.vitrinadev.com/api/v1/messaging-accounts/email/test \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "imap": {
      "host": "mail.automotora-ejemplo.cl",
      "port": 993,
      "secure": true,
      "password": "contraseña-del-buzón"
    },
    "smtp": {
      "host": "mail.automotora-ejemplo.cl",
      "port": 465,
      "secure": true,
      "password": "contraseña-del-buzón"
    }
  }'

Example response (200)

{
  "data": {
    "ok": false,
    "imap": {
      "ok": true,
      "error": null,
      "error_kind": null
    },
    "smtp": {
      "ok": false,
      "error": "Invalid login: 535 Incorrect authentication data",
      "error_kind": "auth"
    }
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

On this page