# Documentación de Tinkay Generado desde https://tinkay.app/docs # Empezar --- URL: https://tinkay.app/docs/introduccion # Introducción Qué es Tinkay, cómo está compuesto y por dónde empezar según lo que necesites. Tinkay es una plataforma de atención al cliente: un Messenger que instalás en tu sitio, un Inbox donde tu equipo responde, una IA que resuelve sola lo que ya está documentado y un centro de ayuda público. Para integrarlo solo necesitás conocer cuatro piezas. ## Las cuatro piezas - **Messenger**: un script de dos líneas que inyecta un botón flotante y un iframe aislado. No toca el CSS de tu sitio. - **API REST v1**: crear y leer contactos, conversaciones, mensajes, tickets y artículos desde tu backend. - **Webhooks**: eventos firmados que Tinkay envía a tu servidor cuando algo pasa. - **Acciones de Tinkay AI**: endpoints tuyos que la IA puede llamar durante una conversación para responder con datos reales. ## Arquitectura El loader (`widget.js`) corre en el sitio del cliente y solo hace tres cosas: pide la configuración pública, dibuja el botón y monta un iframe con la app del Messenger. Toda la lógica y los datos viven dentro del iframe, en el dominio de Tinkay. Por eso el widget no puede romper tu sitio ni tu sitio puede leer las conversaciones: se comunican con `postMessage` sobre un contrato acotado. Flujo: ```text Tu sitio Tinkay -------- ---- widget.js ──── GET /api/v1/widget/config?token=pk_live_xxx ───▶ ◀─── { branding, modules, navigation } ──────────── iframe ──── app del Messenger (app.tinkay.app/widget) ──────▶ ◀─── postMessage: tinkay:ready / tinkay:unread ──────── ``` ## Por dónde seguir - Querés el widget en tu sitio: [Instalación rápida](/docs/instalacion-rapida) y después la guía de tu stack. - Querés sincronizar datos: [API REST v1](/docs/api). - Querés reaccionar a lo que pasa en Tinkay: [Webhooks](/docs/webhooks). - Querés que la IA consulte tu sistema: [Acciones de Tinkay AI](/docs/ai-acciones). --- URL: https://tinkay.app/docs/conceptos # Conceptos Workspace, token público, API key y dominios permitidos. ## Workspace Un workspace es una empresa dentro de Tinkay. Todo (conversaciones, contactos, artículos, equipo, facturación) pertenece a un workspace y nada se comparte entre workspaces. Su identificador es el slug que ves en la URL de la app: `app.tinkay.app/tuempresa`. Si manejás varias marcas, conviene un workspace por marca. ## Token público del Messenger Empieza con `pk_live_` y es la única credencial que va en el navegador. Solo sirve para cargar el Messenger y está limitada a los dominios que autorizaste. Es público por diseño: cualquiera puede verlo en el HTML de tu sitio. No da acceso a leer conversaciones ni datos de otros visitantes. Uso correcto: ```html ``` ## API key Empieza con `dk_live_` (o `dk_test_`) y es secreta. Va **solo en tu servidor**, nunca en el navegador ni en una app móvil. El workspace se deduce de la key en el servidor: nunca mandás un `workspace_id` desde el cliente. Cada key tiene scopes (`contacts:read`, `tickets:write`, etc.) y se revoca desde Configuración, Desarrolladores. ```bash curl https://api.tinkay.app/v1/contacts \ -H "Authorization: Bearer dk_live_xxx" ``` > Si una API key se filtró, revocala desde Configuración, Desarrolladores. La revocación es inmediata y no afecta al Messenger. ## Dominios permitidos El Messenger solo carga en los dominios que agregues en Configuración, Dominios. Es lo que impide que alguien copie tu token y muestre tu widget en otro sitio. Agregá cada variante que uses: `tuempresa.com`, `www.tuempresa.com`, `checkout.tuempresa.com` y el dominio de staging. > En desarrollo local, `localhost` está permitido siempre. ## Identidad del visitante Mientras nadie se identifica, el visitante es anónimo y su historial vive en el navegador. Cuando llamás a `Tinkay.identify()` con el email de tu usuario logueado, la conversación se asocia a un contacto real del workspace. Eso es lo que hace que tu equipo vea el nombre, el plan y los tickets previos de esa persona en el panel lateral del Inbox. --- URL: https://tinkay.app/docs/instalacion-rapida # Instalación rápida El Messenger andando en cinco minutos, en cualquier sitio. ## 1. Copiá el snippet Está en **Configuración, Instalación** de tu workspace, ya con tu token público. index.html: ```html ``` ## 2. Pegalo antes de Va en todas las páginas donde quieras el Messenger. Si tu sitio usa una plantilla o layout compartido, alcanza con ponerlo ahí una sola vez. El script es `async`: no bloquea el renderizado ni afecta tus Core Web Vitals. ## 3. Autorizá tu dominio En **Configuración, Dominios**, agregá el dominio del sitio. Sin esto el widget no carga. ## 4. Identificá a tus usuarios Si tu sitio tiene login, pasale a Tinkay quién es la persona para que tu equipo la reconozca. ```js window.Tinkay.identify({ email: "cliente@empresa.com", name: "Camila Rodríguez", plan: "Pro", created_at: "2026-01-15", }); ``` > Llamá a `identify` después del login y en cada carga de página mientras la sesión siga activa. ## Sin acceso al código Si no podés editar el HTML, usá [Google Tag Manager](/docs/google-tag-manager) o el bloque de código personalizado de tu plataforma ([Webflow](/docs/webflow), [Wix](/docs/wix), [Squarespace](/docs/squarespace)). # Instalar el Messenger --- URL: https://tinkay.app/docs/javascript # JavaScript El snippet universal para cualquier sitio HTML. ## Dónde va Justo antes de ``, en todas las páginas donde quieras el Messenger. index.html: ```html ``` ## Identificar al usuario Si tu sitio tiene sesión, pasá los datos de la persona apenas los tengas disponibles. ```js ``` > También podés llamar a `window.Tinkay.identify({ ... })` más tarde, por ejemplo después de un login por AJAX. ## Abrirlo desde tu propio botón Ocultá el lanzador y controlá la apertura desde cualquier elemento de tu página. ```js window.TinkaySettings = { token: "pk_live_xxx", hideLauncher: true }; document.querySelector("#soporte").addEventListener("click", function () { window.Tinkay.open(); }); ``` ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. --- URL: https://tinkay.app/docs/react # React Cargar el loader una sola vez y sincronizar la identidad con tu estado de sesión. ## Componente de carga Montalo una sola vez, cerca de la raíz. El loader ignora cargas repetidas, pero un componente dedicado deja claro dónde vive la integración. TinkayMessenger.tsx: ```tsx import { useEffect } from "react"; const TOKEN = "pk_live_xxx"; export function TinkayMessenger({ user }: { user?: { email: string; name: string; plan?: string } }) { useEffect(() => { if (document.getElementById("tinkay-loader")) return; window.TinkaySettings = { token: TOKEN }; const s = document.createElement("script"); s.id = "tinkay-loader"; s.src = "https://cdn.tinkay.app/widget.js"; s.async = true; document.body.appendChild(s); }, []); useEffect(() => { if (user) window.Tinkay?.identify(user); }, [user]); return null; } ``` ## Usarlo en la app App.tsx: ```tsx import { TinkayMessenger } from "./TinkayMessenger"; export default function App() { const { user } = useAuth(); return ( <> ); } ``` ## Tipos de TypeScript Declaralos una vez para que `window.Tinkay` y `window.TinkaySettings` tengan autocompletado. tinkay.d.ts: ```tsx interface TinkayVisitor { email?: string; name?: string; plan?: string; [key: string]: string | number | boolean | undefined; } interface TinkayApi { open(): void; close(): void; toggle(): void; identify(attrs: TinkayVisitor): void; isOpen(): boolean; } declare global { interface Window { Tinkay?: TinkayApi; TinkaySettings?: { token: string; hideLauncher?: boolean; position?: "left" | "right"; visitor?: TinkayVisitor }; } } export {}; ``` ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. - **Se carga dos veces**: verificá que el componente esté montado una sola vez y que el `id` del script sea único. --- URL: https://tinkay.app/docs/nextjs # Next.js App Router con next/script y identificación desde la sesión del servidor. ## App Router Usá `next/script` con `strategy="afterInteractive"` en el layout raíz. app/layout.tsx: ```tsx import Script from "next/script"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ``` > En Razor Pages el archivo es `Pages/Shared/_Layout.cshtml`. El resto es idéntico. ## 3. Identificá al usuario autenticado Leé los claims y pasalos en `visitor`. Tinkay asocia la conversación a un contacto real de tu workspace. Views/Shared/_Layout.cshtml: ```razor @using System.Security.Claims @inject IConfiguration Configuration ``` ## Alternativa: Tag Helper reutilizable Si preferís mantener la vista limpia, encapsulá todo en un Tag Helper y usá `` en el layout. TagHelpers/TinkayMessengerTagHelper.cs: ```csharp using System.Security.Claims; using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Razor.TagHelpers; namespace MyApp.TagHelpers; [HtmlTargetElement("tinkay-messenger", TagStructure = TagStructure.WithoutEndTag)] public class TinkayMessengerTagHelper : TagHelper { private readonly IConfiguration _config; private readonly IHttpContextAccessor _http; public TinkayMessengerTagHelper(IConfiguration config, IHttpContextAccessor http) { _config = config; _http = http; } public override void Process(TagHelperContext context, TagHelperOutput output) { var user = _http.HttpContext?.User; var settings = new Dictionary { ["token"] = _config["Tinkay:MessengerToken"] }; if (user?.Identity?.IsAuthenticated == true) { settings["visitor"] = new { email = user.FindFirstValue(ClaimTypes.Email), name = user.FindFirstValue(ClaimTypes.Name), plan = user.FindFirstValue("plan") ?? "Free" }; } var json = JsonSerializer.Serialize(settings); output.TagName = null; output.Content.SetHtmlContent( $"" + ""); } } ``` Registro y uso · _ViewImports.cshtml: ```razor @addTagHelper *, MyApp ``` Program.cs · Program.cs: ```csharp builder.Services.AddHttpContextAccessor(); ``` ## Content Security Policy Si tu app define un CSP (por middleware o `web.config`), habilitá los orígenes de Tinkay. Program.cs: ```csharp app.Use(async (context, next) => { context.Response.Headers.Append("Content-Security-Policy", "script-src 'self' 'unsafe-inline' https://cdn.tinkay.app; " + "frame-src https://app.tinkay.app; " + "connect-src 'self' https://app.tinkay.app"); await next(); }); ``` ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. - **Las comillas del email salen escapadas**: usá `@Html.Raw(...)` o serializá con `JsonSerializer` como en el Tag Helper. - **No aparece en producción con IIS**: confirmá que el dominio real (no el interno) esté en Configuración, Dominios. --- URL: https://tinkay.app/docs/blazor # Blazor Server y WebAssembly, con JS interop para identificar desde C#. ## 1. Agregá el script al host En **Blazor Server** el archivo es `Components/App.razor` (o `Pages/_Host.cshtml` en .NET 7). En **WebAssembly** es `wwwroot/index.html`. En ambos casos va antes de ``, después del script de Blazor. Blazor Server · Components/App.razor: ```razor ``` Blazor WebAssembly · wwwroot/index.html: ```html ``` ## 2. Servicio de interop Envolvé la API del widget en un servicio para llamarla desde cualquier componente. Services/TinkayInterop.cs: ```csharp using Microsoft.JSInterop; namespace MyApp.Services; public class TinkayInterop(IJSRuntime js) { public ValueTask OpenAsync() => js.InvokeVoidAsync("Tinkay.open"); public ValueTask CloseAsync() => js.InvokeVoidAsync("Tinkay.close"); public ValueTask IdentifyAsync(TinkayVisitor visitor) => js.InvokeVoidAsync("Tinkay.identify", visitor); } public record TinkayVisitor(string Email, string Name, string? Plan = null) { public string email => Email; public string name => Name; public string? plan => Plan; } ``` Registro · Program.cs: ```csharp builder.Services.AddScoped(); ``` ## 3. Identificar tras el render La interop de JavaScript solo está disponible después del primer render. Usá `OnAfterRenderAsync` con la guarda `firstRender`. Components/Layout/MainLayout.razor: ```razor @inherits LayoutComponentBase @inject TinkayInterop Tinkay @inject AuthenticationStateProvider AuthProvider @Body @code { protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; var state = await AuthProvider.GetAuthenticationStateAsync(); var user = state.User; if (user.Identity?.IsAuthenticated != true) return; await Tinkay.IdentifyAsync(new TinkayVisitor( Email: user.FindFirst(ClaimTypes.Email)?.Value ?? "", Name: user.Identity.Name ?? "", Plan: user.FindFirst("plan")?.Value)); } } ``` > En Blazor Server, `OnAfterRenderAsync` corre cuando el circuito está listo. Llamar a la interop antes lanza `InvalidOperationException`. ## Abrir el Messenger desde un botón Components/Pages/Ayuda.razor: ```razor @inject TinkayInterop Tinkay @code { private async Task AbrirSoporte() => await Tinkay.OpenAsync(); } ``` ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. - **`Tinkay is not defined`**: el loader todavía no terminó de cargar. Llamá a la interop en `OnAfterRenderAsync` y no en `OnInitializedAsync`. - **WebAssembly y prerender**: si usás prerendering, la identificación debe correr en el render interactivo, no en el prerender. --- URL: https://tinkay.app/docs/angular # Angular Servicio inyectable que carga el loader y sincroniza la identidad. ## Opción simple: index.html Si no necesitás identificar usuarios, alcanza con el snippet en `src/index.html`. src/index.html: ```html ``` ## Servicio inyectable tinkay.service.ts: ```tsx import { Injectable } from "@angular/core"; declare global { interface Window { Tinkay?: { open(): void; close(): void; identify(a: Record): void }; TinkaySettings?: { token: string }; } } @Injectable({ providedIn: "root" }) export class TinkayService { private loaded = false; load(token: string) { if (this.loaded) return; this.loaded = true; window.TinkaySettings = { token }; const s = document.createElement("script"); s.src = "https://cdn.tinkay.app/widget.js"; s.async = true; document.body.appendChild(s); } identify(attrs: Record) { window.Tinkay?.identify(attrs); } open() { window.Tinkay?.open(); } } ``` Uso · app.component.ts: ```tsx export class AppComponent implements OnInit { constructor(private tinkay: TinkayService, private auth: AuthService) {} ngOnInit() { this.tinkay.load(environment.tinkayToken); this.auth.user$.subscribe((u) => u && this.tinkay.identify({ email: u.email, name: u.name })); } } ``` ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. --- URL: https://tinkay.app/docs/vue-nuxt # Vue y Nuxt Plugin de Nuxt o composable de Vue 3. ## Nuxt: plugin de cliente plugins/tinkay.client.ts: ```js export default defineNuxtPlugin(() => { const token = useRuntimeConfig().public.tinkayToken; window.TinkaySettings = { token }; const s = document.createElement("script"); s.src = "https://cdn.tinkay.app/widget.js"; s.async = true; document.body.appendChild(s); return { provide: { tinkay: { identify: (a: Record) => window.Tinkay?.identify(a) } } }; }); ``` nuxt.config · nuxt.config.ts: ```js export default defineNuxtConfig({ runtimeConfig: { public: { tinkayToken: process.env.NUXT_PUBLIC_TINKAY_TOKEN }, }, }); ``` ## Vue 3: composable useTinkay.ts: ```js import { onMounted, watch, type Ref } from "vue"; export function useTinkay(token: string, user: Ref<{ email: string; name: string } | null>) { onMounted(() => { if (document.getElementById("tinkay-loader")) return; window.TinkaySettings = { token }; const s = document.createElement("script"); s.id = "tinkay-loader"; s.src = "https://cdn.tinkay.app/widget.js"; s.async = true; document.body.appendChild(s); }); watch(user, (u) => u && window.Tinkay?.identify(u), { immediate: true }); } ``` ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. --- URL: https://tinkay.app/docs/laravel # Laravel Blade layout con identificación desde el usuario autenticado. ## 1. Variable de entorno .env: ```bash TINKAY_MESSENGER_TOKEN=pk_live_xxx ``` config · config/services.php: ```php return [ // ... 'tinkay' => [ 'token' => env('TINKAY_MESSENGER_TOKEN'), ], ]; ``` ## 2. Componente Blade resources/views/components/tinkay.blade.php: ```php @php $settings = ['token' => config('services.tinkay.token')]; if (auth()->check()) { $settings['visitor'] = [ 'email' => auth()->user()->email, 'name' => auth()->user()->name, 'plan' => auth()->user()->plan ?? 'Free', 'created_at' => auth()->user()->created_at->toDateString(), ]; } @endphp ``` Uso en el layout · resources/views/layouts/app.blade.php: ```php ``` > `@json` escapa el contenido correctamente: no armes el objeto concatenando strings. ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. - **El token queda vacío**: corré `php artisan config:clear` después de editar el `.env`. --- URL: https://tinkay.app/docs/django # Django Template base con context processor para el token y el usuario. ## 1. Configuración settings.py: ```python import os TINKAY_MESSENGER_TOKEN = os.environ.get("TINKAY_MESSENGER_TOKEN", "") ``` Context processor · core/context_processors.py: ```python from django.conf import settings def tinkay(request): return {"tinkay_token": settings.TINKAY_MESSENGER_TOKEN} ``` Registro · settings.py: ```python TEMPLATES = [ { # ... "OPTIONS": { "context_processors": [ # ... "core.context_processors.tinkay", ], }, }, ] ``` ## 2. Template base templates/base.html: ```html {% load static %} ``` > Usá siempre el filtro `escapejs` en los valores del usuario para evitar romper el JavaScript. ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. --- URL: https://tinkay.app/docs/rails # Ruby on Rails Layout ERB con credenciales cifradas e identificación del current_user. ## 1. Credenciales ```bash bin/rails credentials:edit ``` credentials.yml.enc: ```yaml tinkay: messenger_token: pk_live_xxx ``` ## 2. Partial y layout app/views/shared/_tinkay.html.erb: ```ruby <% settings = { token: Rails.application.credentials.dig(:tinkay, :messenger_token) } %> <% if user_signed_in? %> <% settings[:visitor] = { email: current_user.email, name: current_user.name, plan: current_user.plan } %> <% end %> ``` Layout · app/views/layouts/application.html.erb: ```ruby <%= render "shared/tinkay" %> ``` ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. - **Turbo y navegación**: el loader se monta una sola vez y sobrevive a las visitas de Turbo. No lo reinyectes en `turbo:load`. --- URL: https://tinkay.app/docs/wordpress # WordPress Con el tema hijo o con un snippet en functions.php. ## Opción 1: functions.php Pegalo en el `functions.php` de tu **tema hijo** para que no se pierda al actualizar el tema. wp-content/themes/tu-tema-hijo/functions.php: ```php add_action( 'wp_footer', 'tinkay_messenger', 100 ); function tinkay_messenger() { $settings = array( 'token' => 'pk_live_xxx' ); if ( is_user_logged_in() ) { $user = wp_get_current_user(); $settings['visitor'] = array( 'email' => $user->user_email, 'name' => $user->display_name, ); } ?> El hook `wp_footer` garantiza que el script quede antes de `` en cualquier tema bien construido. ## Opción 2: sin tocar código 1. Instalá un plugin de scripts en el header y footer (por ejemplo WPCode). 2. Creá un snippet nuevo de tipo HTML. 3. Pegá el snippet de Tinkay y elegí la ubicación **Footer**. 4. Activá el snippet y guardá. ```html ``` ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. - **Un plugin de caché sirve HTML viejo**: purgá la caché después de guardar. --- URL: https://tinkay.app/docs/shopify # Shopify En theme.liquid, con los datos del cliente logueado. ## 1. Editar el tema 1. En el panel de Shopify, entrá a **Tienda online, Temas**. 2. En tu tema activo, abrí el menú y elegí **Editar código**. 3. Abrí `layout/theme.liquid`. 4. Pegá el snippet justo antes de `` y guardá. layout/theme.liquid: ```html ``` > El filtro `json` de Liquid escapa los valores: no uses comillas manuales alrededor. ## 2. Conectá la app de Shopify Además del widget, conectá Shopify desde **Apps** en tu workspace. Así tu equipo ve los pedidos del cliente en el panel lateral y Tinkay AI puede responder consultas de estado de pedido. ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. - **El tema usa secciones**: si `theme.liquid` no tiene `` visible, buscá el layout alternativo (`layout/checkout.liquid` no admite scripts en planes sin Shopify Plus). --- URL: https://tinkay.app/docs/woocommerce # WooCommerce Sobre WordPress, sumando datos de pedidos del cliente. ## Snippet con datos de compra functions.php: ```php add_action( 'wp_footer', 'tinkay_messenger_woo', 100 ); function tinkay_messenger_woo() { $settings = array( 'token' => 'pk_live_xxx' ); if ( is_user_logged_in() ) { $user = wp_get_current_user(); $count = wc_get_customer_order_count( $user->ID ); $total = wc_get_customer_total_spent( $user->ID ); $settings['visitor'] = array( 'email' => $user->user_email, 'name' => $user->display_name, 'orders_count' => $count, 'total_spent' => wc_format_decimal( $total, 2 ), ); } ?> `). 3. Guardá y tocá **Publish** para que se aplique al sitio en vivo. Footer code: ```html ``` > El código personalizado no corre dentro del Designer: solo se ve en el sitio publicado o en la vista previa del dominio. ## Solo en algunas páginas Si lo querés únicamente en ciertas páginas, usá **Page settings** de esa página en lugar de la configuración del sitio. ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. --- URL: https://tinkay.app/docs/wix # Wix Mediante el gestor de código personalizado de Wix. ## Pasos 1. En el panel del sitio, entrá a **Configuración, Código personalizado**. 2. Tocá **Agregar código** en la sección Body - final. 3. Pegá el snippet y elegí cargarlo en **Todas las páginas**. 4. Guardá y publicá el sitio. ```html ``` ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. - **El plan gratuito no permite código personalizado**: hay que pasar a un plan Premium. --- URL: https://tinkay.app/docs/squarespace # Squarespace Con la inyección de código del sitio. ## Pasos 1. Entrá a **Settings, Advanced, Code Injection**. 2. Pegá el snippet en el campo **Footer**. 3. Guardá: se aplica a todas las páginas del sitio. Footer: ```html ``` > La inyección de código requiere plan Business o superior. ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. --- URL: https://tinkay.app/docs/prestashop # PrestaShop En la plantilla del tema, con los datos del cliente. ## Plantilla del tema themes/tu-tema/templates/_partials/footer.tpl: ```html ``` ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. - **Caché de Smarty**: borrá la caché del tema desde Parámetros avanzados, Rendimiento. --- URL: https://tinkay.app/docs/google-tag-manager # Google Tag Manager Instalarlo sin tocar el código del sitio. ## Crear la etiqueta 1. En GTM, entrá a **Etiquetas** y tocá **Nueva**. 2. Elegí el tipo **HTML personalizado**. 3. Pegá el snippet completo, con las dos etiquetas ` ``` ## Identificar usando la capa de datos Si tu sitio ya empuja el usuario a `dataLayer`, leelo con una variable de GTM y pasalo al Messenger. ```html ``` > Nunca pongas una API key (`dk_live_`) en GTM: es código que corre en el navegador. ## Verificar la instalación Entrá a **Configuración, Instalación** en tu workspace, pegá la URL pública del sitio y tocá `Probar instalación`. Tinkay busca el script y valida que el dominio esté permitido. 1. Abrí tu sitio en una pestaña nueva y confirmá que aparece el botón flotante. 2. En la consola del navegador, escribí `window.Tinkay` y verificá que devuelve un objeto. 3. Escribí un mensaje de prueba y confirmá que llega al Inbox. ## Errores frecuentes - **El widget no aparece**: el dominio no está en Configuración, Dominios. Agregalo y recargá. - **Aparece en desarrollo pero no en producción**: agregá también el dominio de producción, incluida la variante con `www`. - **Content Security Policy**: permití `https://cdn.tinkay.app` en `script-src` y `https://app.tinkay.app` en `frame-src` y `connect-src`. - **La etiqueta no dispara**: revisá que el activador sea All Pages y que hayas publicado el contenedor, no solo guardado. --- URL: https://tinkay.app/docs/apps-moviles # Apps móviles Embeber el Messenger en una WebView de Flutter, React Native, Swift o Kotlin. Todavía no hay SDK nativo. Mientras tanto, el Messenger se embebe en una WebView apuntando a la URL del widget con el token y los datos del visitante en la query string. > Esta integración está en beta: el contrato de la URL puede cambiar. Avisamos con anticipación en el changelog. ## URL del widget **Parámetros** | Campo | Tipo | Descripción | | --- | --- | --- | | `token` (requerido) | string | Token público del Messenger. | | `host` (requerido) | string | Dominio autorizado del workspace. | | `email` | string | Identifica al visitante como un contacto existente. | | `name` | string | Nombre a mostrar en el Inbox. | ```text https://app.tinkay.app/widget?token=pk_live_xxx&host=tuempresa.com&email=cliente@empresa.com&name=Camila ``` ## Flutter lib/support_page.dart: ```text import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; class SupportPage extends StatefulWidget { const SupportPage({super.key, required this.email, required this.name}); final String email; final String name; @override State createState() => _SupportPageState(); } class _SupportPageState extends State { late final WebViewController _controller; @override void initState() { super.initState(); final uri = Uri.https('app.tinkay.app', '/widget', { 'token': 'pk_live_xxx', 'host': 'tuempresa.com', 'email': widget.email, 'name': widget.name, }); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..loadRequest(uri); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Soporte')), body: WebViewWidget(controller: _controller), ); } } ``` React Native · SupportScreen.tsx: ```tsx import { WebView } from "react-native-webview"; const TOKEN = "pk_live_xxx"; export function SupportScreen({ user }: { user: { email: string; name: string } }) { const url = `https://app.tinkay.app/widget?token=${TOKEN}&host=tuempresa.com` + `&email=${encodeURIComponent(user.email)}&name=${encodeURIComponent(user.name)}`; return ; } ``` Swift · SupportViewController.swift: ```text import WebKit final class SupportViewController: UIViewController { private let webView = WKWebView() override func viewDidLoad() { super.viewDidLoad() view.addSubview(webView) webView.frame = view.bounds webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] var components = URLComponents(string: "https://app.tinkay.app/widget")! components.queryItems = [ URLQueryItem(name: "token", value: "pk_live_xxx"), URLQueryItem(name: "host", value: "tuempresa.com"), URLQueryItem(name: "email", value: currentUser.email), URLQueryItem(name: "name", value: currentUser.name) ] webView.load(URLRequest(url: components.url!)) } } ``` ## Requisitos de la WebView - JavaScript habilitado y almacenamiento local activo (el historial del visitante se guarda ahí). - Permiso de red hacia `app.tinkay.app`. - En Android, habilitar `domStorageEnabled` para que persista la conversación. # Widget --- URL: https://tinkay.app/docs/widget-api # API de JavaScript Opciones de configuración, métodos, eventos y el contrato de postMessage. ## window.TinkaySettings Se define **antes** de cargar `widget.js`. Todos los campos menos `token` son opcionales. | Campo | Tipo | Descripción | | --- | --- | --- | | `token` (requerido) | string | Token público del Messenger (`pk_live_...`). | | `visitor` | object | Datos del usuario logueado. Equivale a llamar a `identify` al arrancar. | | `hideLauncher` | boolean | Oculta el botón flotante. Abrís el Messenger desde tu propio control. | | `position` | "left" | "right" | Lado de la pantalla. Por defecto usa lo configurado en el workspace. | | `color` | string | Color del lanzador en hexadecimal. Por defecto usa el del workspace. | | `sideSpacing` | number | Separación lateral en píxeles. Por defecto 20. | | `bottomSpacing` | number | Separación inferior en píxeles. Útil si tenés una barra fija abajo. | | `locale` | "es" | "en" | "pt" | Idioma de la interfaz del Messenger. | ```js window.TinkaySettings = { token: "pk_live_xxx", position: "right", bottomSpacing: 88, hideLauncher: false, visitor: { email: "cliente@empresa.com", name: "Camila Rodríguez" }, }; ``` ## Métodos Disponibles en `window.Tinkay` una vez que el loader terminó de ejecutarse. | Campo | Tipo | Descripción | | --- | --- | --- | | `Tinkay.open()` | void | Abre el panel del Messenger. | | `Tinkay.close()` | void | Cierra el panel. | | `Tinkay.toggle()` | void | Alterna abierto y cerrado. | | `Tinkay.identify(attrs)` | void | Asocia la conversación a un contacto. Guarda los atributos para las próximas visitas. | | `Tinkay.isOpen()` | boolean | Indica si el panel está abierto. | Uso típico: ```js // Abrir desde tu propio botón document.querySelector("#ayuda").addEventListener("click", () => window.Tinkay.open()); // Identificar al usuario después del login window.Tinkay.identify({ email: "cliente@empresa.com", name: "Camila Rodríguez", plan: "Pro", created_at: "2026-01-15", company: "Kipu Pagos", }); ``` > Si llamás a un método antes de que el loader termine, la llamada se descarta. Usá el evento `tinkay:ready` o comprobá `window.Tinkay?.open?.()`. ## Atributos del visitante `email` es el único campo que Tinkay usa para vincular con un contacto existente. El resto se muestra como atributos en el panel lateral del Inbox. | Campo | Tipo | Descripción | | --- | --- | --- | | `email` | string | Vincula con un contacto del workspace. Si no existe, se crea. | | `name` | string | Nombre a mostrar. | | `plan` | string | Plan o segmento del cliente. | | `company` | string | Empresa a la que pertenece. | | `created_at` | string (ISO 8601) | Fecha de alta del usuario en tu producto. | | `(cualquier otro)` | string | number | boolean | Se muestra como atributo personalizado en el panel del contacto. | ## Eventos El loader emite eventos del DOM sobre `window`. Sirven para sincronizar tu interfaz con el estado del Messenger. | Campo | Tipo | Descripción | | --- | --- | --- | | `tinkay:ready` | CustomEvent | El Messenger terminó de cargar y la API está disponible. | | `tinkay:open` | CustomEvent | Se abrió el panel. | | `tinkay:close` | CustomEvent | Se cerró el panel. | | `tinkay:unread` | CustomEvent<{ count: number }> | Cambió la cantidad de mensajes sin leer. | ```js window.addEventListener("tinkay:ready", () => { window.Tinkay.identify({ email: currentUser.email, name: currentUser.name }); }); window.addEventListener("tinkay:unread", (e) => { document.title = e.detail.count > 0 ? `(${e.detail.count}) Soporte` : "Soporte"; }); window.addEventListener("tinkay:open", () => analytics.track("support_opened")); ``` ## Contrato de postMessage El loader y el iframe se comunican con `postMessage`. Documentamos el contrato por si necesitás embeber el Messenger vos mismo (por ejemplo dentro de otra aplicación). Los mensajes del host llevan `source: "tinkay-host"`; los del iframe llevan `source: "tinkay-widget"`. Del iframe al host: ```js { source: "tinkay-widget", type: "tinkay:ready" } { source: "tinkay-widget", type: "tinkay:open" } { source: "tinkay-widget", type: "tinkay:close" } { source: "tinkay-widget", type: "tinkay:unread", count: 2 } { source: "tinkay-widget", type: "tinkay:resize", height: 640 } ``` Del host al iframe: ```js { source: "tinkay-host", type: "tinkay:identify", attrs: { email: "...", name: "..." } } { source: "tinkay-host", type: "tinkay:open" } { source: "tinkay-host", type: "tinkay:close" } ``` ## Apariencia El Messenger vive en un iframe: tu CSS no lo afecta y el suyo no afecta a tu sitio. Los colores, el saludo y los módulos se configuran en **Configuración, Messenger** del workspace. Lo único que podés ajustar desde el sitio es la posición y la separación de los bordes, útil cuando tenés una barra fija o un botón de cookies abajo. ```js window.TinkaySettings = { token: "pk_live_xxx", position: "left", sideSpacing: 24, bottomSpacing: 96, // deja lugar para tu barra fija }; ``` ## Cargarlo bajo demanda Si querés que el widget solo pese cuando alguien pide ayuda, inyectá el loader al hacer clic. ```js function abrirSoporte() { if (window.Tinkay) return window.Tinkay.open(); window.TinkaySettings = { token: "pk_live_xxx", hideLauncher: true }; const s = document.createElement("script"); s.src = "https://cdn.tinkay.app/widget.js"; s.async = true; s.onload = () => window.addEventListener("tinkay:ready", () => window.Tinkay.open(), { once: true }); document.body.appendChild(s); } ``` # API REST v1 --- URL: https://tinkay.app/docs/api # API REST v1 Autenticación, formato de respuestas, errores, paginación y límites. La API vive en `https://api.tinkay.app/v1`. Todas las respuestas son JSON y todos los endpoints requieren autenticación. El workspace se deduce de la API key en el servidor: nunca se envía un identificador de workspace desde el cliente. ## Autenticación Mandá la key en el header `Authorization`. Las keys se crean y revocan en [Configuración, Desarrolladores](/tinkay/settings/developer). curl: ```bash curl https://api.tinkay.app/v1/contacts \ -H "Authorization: Bearer dk_live_xxx" ``` Node: ```js const res = await fetch("https://api.tinkay.app/v1/contacts", { headers: { Authorization: `Bearer ${process.env.TINKAY_API_KEY}` }, }); const { data, next_cursor } = await res.json(); ``` Python: ```python import os, requests res = requests.get( "https://api.tinkay.app/v1/contacts", headers={"Authorization": f"Bearer {os.environ['TINKAY_API_KEY']}"}, timeout=10, ) payload = res.json() ``` C#: ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TINKAY_API_KEY")); var res = await http.GetFromJsonAsync("https://api.tinkay.app/v1/contacts"); ``` PHP: ```php $ch = curl_init("https://api.tinkay.app/v1/contacts"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("TINKAY_API_KEY")], ]); $payload = json_decode(curl_exec($ch), true); ``` > La API key es secreta: usala solo desde tu servidor. Para el navegador existe el token público del Messenger. ## Scopes Cada key tiene permisos acotados. Si le falta uno, la respuesta es 403 con el scope que hace falta. | Campo | Tipo | Descripción | | --- | --- | --- | | `contacts:read / contacts:write` | scope | Leer y crear contactos. | | `conversations:read / conversations:write` | scope | Leer conversaciones y enviar mensajes. | | `tickets:read / tickets:write` | scope | Leer y crear tickets. | | `articles:read` | scope | Leer artículos del centro de ayuda. | | `events:read` | scope | Leer el feed de eventos. | | `*` | scope | Acceso total, incluida la gestión de webhooks. | ## Formato de respuesta Las listas devuelven `data`, `next_cursor` y `has_more`. Los objetos individuales devuelven `data`. Lista: ```json { "data": [ { "id": "ct_8f2a", "name": "Camila Rodríguez", "email": "camila@kipu.app" } ], "next_cursor": "ct_8f2a", "has_more": true } ``` Objeto: ```json { "data": { "id": "ct_8f2a", "name": "Camila Rodríguez" } } ``` ## Errores Todos los errores devuelven el mismo shape, con un `code` estable para tu lógica y un `message` en español para tus logs. **Códigos de estado** | Campo | Tipo | Descripción | | --- | --- | --- | | `400` | invalid_json / missing_parameter | El cuerpo no es JSON válido o falta un parámetro obligatorio. | | `401` | unauthorized | Falta el header o la key no existe. | | `403` | forbidden | La key no tiene el scope necesario. | | `404` | not_found | El recurso no existe en este workspace. | | `409` | conflict | El recurso ya existe (por ejemplo, un contacto con ese email). | | `422` | validation_error | El cuerpo es JSON válido pero los campos no pasan la validación. | | `429` | rate_limited | Superaste el límite de requests. | ```json { "error": { "code": "validation_error", "message": "Hay campos inválidos.", "details": [{ "path": ["email"], "message": "Invalid email" }] } } ``` ## Paginación por cursor Pasá `limit` (1 a 100, por defecto 25) y `cursor`. El cursor es el `id` del último elemento de la página anterior, que viene en `next_cursor`. Cuando `has_more` es `false`, terminaste. Node: ```js async function* allContacts() { let cursor = null; do { const url = new URL("https://api.tinkay.app/v1/contacts"); url.searchParams.set("limit", "100"); if (cursor) url.searchParams.set("cursor", cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.TINKAY_API_KEY}` } }); const page = await res.json(); yield* page.data; cursor = page.next_cursor; } while (cursor); } ``` C#: ```csharp string? cursor = null; do { var url = $"https://api.tinkay.app/v1/contacts?limit=100" + (cursor is null ? "" : $"&cursor={cursor}"); var page = await http.GetFromJsonAsync>(url); foreach (var contact in page!.Data) Process(contact); cursor = page.NextCursor; } while (cursor is not null); ``` ## Límites 600 requests por minuto por API key. Cada respuesta trae los headers para que puedas frenar antes de recibir un 429. ```text X-RateLimit-Limit: 600 X-RateLimit-Remaining: 587 X-RateLimit-Reset: 1772668800 ``` > Si tenés que sincronizar seguido, usá [webhooks](/docs/webhooks) en lugar de consultar en bucle: es más rápido y no consume límite. --- URL: https://tinkay.app/docs/api-convenciones # Convenciones de la API Paginación, filtros, expansión, límites, errores, idempotencia y versionado. Todas las respuestas son JSON. Las listas comparten la misma forma y los objetos siempre traen un campo `object` para que puedas despachar por tipo sin adivinar. Los nombres de campo van en `snake_case`, en la respuesta y en el cuerpo que mandás. Las fechas son ISO 8601 en UTC y los importes vienen en pesos argentinos, sin decimales de centavos escondidos. Nunca mandes el id del workspace: lo deducimos de la API key. Un campo que no aplica llega como `null`, no ausente, así podés tipar la respuesta sin campos opcionales por todos lados. list response: ```json { "object": "list", "data": [ { "object": "contact", "id": "ct_juan", "name": "Juan Pérez" } ], "next_cursor": "ct_camila", "has_more": true } ``` ## Paginación por cursor Las listas devuelven hasta 25 elementos por defecto y 100 como máximo. Cuando `has_more` es `true`, pasá `next_cursor` en el parámetro `cursor` para pedir la página siguiente. No uses offsets: el cursor es estable aunque se creen registros mientras recorrés. | Campo | Tipo | Descripción | | --- | --- | --- | | `limit` | number | Elementos por página. Entre 1 y 100. Por defecto 25. | | `cursor` | string | `next_cursor` de la respuesta anterior. | Node: ```js async function* allContacts(key) { let cursor; do { const url = new URL("https://api.tinkay.app/v1/contacts"); url.searchParams.set("limit", "100"); if (cursor) url.searchParams.set("cursor", cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } }); const page = await res.json(); yield* page.data; cursor = page.has_more ? page.next_cursor : null; } while (cursor); } ``` Python: ```python import requests def all_contacts(key): cursor, url = None, "https://api.tinkay.app/v1/contacts" while True: params = {"limit": 100, **({"cursor": cursor} if cursor else {})} page = requests.get(url, params=params, headers={"Authorization": f"Bearer {key}"}).json() yield from page["data"] if not page["has_more"]: return cursor = page["next_cursor"] ``` ## Filtros y búsqueda Cada recurso documenta sus filtros. Los valores múltiples se separan con coma y se combinan con **o**; filtros distintos se combinan con **y**. Para buscar texto libre en varios recursos a la vez usá `GET /v1/search`. ```bash # Conversaciones abiertas o esperando, del equipo de soporte curl "https://api.tinkay.app/v1/conversations?status=open,waiting&team_id=team_soporte" \ -H "Authorization: Bearer dk_live_xxx" ``` ## Expandir relaciones Evitá pedidos en cascada con `expand`. Cada recurso lista qué relaciones acepta. ```bash curl "https://api.tinkay.app/v1/conversations/cv_1832?expand=contact,assignee,messages" \ -H "Authorization: Bearer dk_live_xxx" ``` > Cada relación expandida cuenta como un pedido para el límite de tasa. Expandí solo lo que vas a usar. ## Límites de tasa 600 pedidos por minuto por API key. Cada respuesta trae el estado del límite en los headers. Al llegar al límite devolvemos `429` con `Retry-After` en segundos. Esperá ese tiempo y reintentá con espera exponencial y jitter. | Campo | Tipo | Descripción | | --- | --- | --- | | `X-RateLimit-Limit` | number | Pedidos permitidos en la ventana. | | `X-RateLimit-Remaining` | number | Pedidos que te quedan. | | `X-RateLimit-Reset` | number | Epoch en segundos en que se reinicia la ventana. | | `Retry-After` | number | Solo en 429: segundos a esperar. | Reintento con espera: ```js async function request(url, init, attempt = 0) { const res = await fetch(url, init); if (res.status !== 429 || attempt >= 5) return res; const wait = Number(res.headers.get("Retry-After") ?? 1) * 1000; const jitter = Math.random() * 250; await new Promise((r) => setTimeout(r, wait * 2 ** attempt + jitter)); return request(url, init, attempt + 1); } ``` ## Errores Los errores traen un objeto `error` con un `code` estable para programar contra él y un `message` en español para mostrar o registrar. **Códigos frecuentes** | Campo | Tipo | Descripción | | --- | --- | --- | | `unauthorized` | 401 | Falta el header o la key no es válida. | | `forbidden` | 403 | La key no tiene el permiso necesario. | | `not_found` | 404 | El objeto no existe en este workspace. | | `validation_error` | 422 | El cuerpo no cumple el esquema. Mirá `details`. | | `rate_limited` | 429 | Superaste el límite de tasa. | ```json { "error": { "code": "validation_error", "message": "Hay campos inválidos.", "details": [ { "path": ["email"], "message": "Formato de email inválido" } ] } } ``` ## Idempotencia Todos los `POST` aceptan el header `Idempotency-Key`. Si repetís un pedido con la misma clave dentro de 24 horas devolvemos la respuesta original sin volver a crear nada. Usá un identificador único por operación de negocio, por ejemplo el id del pedido de tu sistema. ```bash curl -X POST https://api.tinkay.app/v1/tickets \ -H "Authorization: Bearer dk_live_xxx" \ -H "Idempotency-Key: order-88213-refund" \ -H "Content-Type: application/json" \ -d '{ "subject": "Reintegro pedido 88213", "contact_email": "juan@loomi.com.ar" }' ``` ## Versionado La versión vive en la URL (`/v1`) y el formato de los eventos en `api_version`. Agregar campos o recursos no rompe. Los cambios que rompen se publican como versión nueva, con seis meses de convivencia. ## Sandbox y producción Las keys con prefijo `dk_test_` operan sobre datos de prueba y no envían mensajes reales ni disparan webhooks a producción. Las keys `dk_live_` trabajan sobre datos reales. Nunca las uses en el navegador ni las subas al repositorio. > Si una key se filtró, revocala desde Configuración, Desarrolladores. La revocación es inmediata. --- URL: https://tinkay.app/docs/api-contacts # Contacts Crear, listar y leer las personas que escriben a tu equipo. ### `GET /v1/contacts` Lista los contactos del workspace, más recientes primero. Scope: `contacts:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `q` | string | Filtra por nombre o email. | | `limit` | number | 1 a 100. Por defecto 25. | | `cursor` | string | Id del último elemento de la página anterior. | ### `POST /v1/contacts` Crea un contacto. Devuelve 409 si ya existe uno con ese email. Scope: `contacts:write` | Campo | Tipo | Descripción | | --- | --- | --- | | `name` (requerido) | string | Nombre completo. | | `email` (requerido) | string | Email único dentro del workspace. | | `phone` | string | Teléfono en formato libre. | | `company` | string | Empresa a la que pertenece. | | `country` | string | País. Por defecto "Argentina". | | `tags` | string[] | Etiquetas para segmentar. | | `custom_fields` | object | Pares clave/valor con datos de tu sistema. | ### `GET /v1/contacts/{id}` Devuelve un contacto con sus conversaciones y tickets asociados. Scope: `contacts:read` ## Crear un contacto curl: ```bash curl -X POST https://api.tinkay.app/v1/contacts \ -H "Authorization: Bearer dk_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "name": "Camila Rodríguez", "email": "camila@kipu.app", "company": "Kipu Pagos", "tags": ["business"], "custom_fields": { "plan": "Business", "mrr": 399 } }' ``` Node: ```js const res = await fetch("https://api.tinkay.app/v1/contacts", { method: "POST", headers: { Authorization: `Bearer ${process.env.TINKAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: "Camila Rodríguez", email: "camila@kipu.app", company: "Kipu Pagos", custom_fields: { plan: "Business", mrr: 399 }, }), }); if (res.status === 409) { const { error } = await res.json(); console.log("Ya existía:", error.contact_id); } ``` Python: ```python import os, requests res = requests.post( "https://api.tinkay.app/v1/contacts", headers={"Authorization": f"Bearer {os.environ['TINKAY_API_KEY']}"}, json={ "name": "Camila Rodríguez", "email": "camila@kipu.app", "company": "Kipu Pagos", "custom_fields": {"plan": "Business", "mrr": 399}, }, timeout=10, ) res.raise_for_status() contact = res.json()["data"] ``` C#: ```csharp var payload = new { name = "Camila Rodríguez", email = "camila@kipu.app", company = "Kipu Pagos", custom_fields = new { plan = "Business", mrr = 399 } }; var res = await http.PostAsJsonAsync("https://api.tinkay.app/v1/contacts", payload); if (res.StatusCode == HttpStatusCode.Conflict) { var conflict = await res.Content.ReadFromJsonAsync(); logger.LogInformation("Contacto existente: {Id}", conflict!.Error.ContactId); } ``` PHP: ```php $payload = json_encode([ "name" => "Camila Rodríguez", "email" => "camila@kipu.app", "company" => "Kipu Pagos", "custom_fields" => ["plan" => "Business", "mrr" => 399], ]); $ch = curl_init("https://api.tinkay.app/v1/contacts"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("TINKAY_API_KEY"), "Content-Type: application/json", ], ]); $contact = json_decode(curl_exec($ch), true)["data"]; ``` ## Respuesta ```json { "data": { "id": "ct_8f2a91", "object": "contact", "name": "Camila Rodríguez", "email": "camila@kipu.app", "company": "Kipu Pagos", "country": "Argentina", "city": "Córdoba", "tags": ["experto"], "lifecycle": "customer", "vip": false, "source": "api", "created_at": "2026-09-02T14:21:08.412Z", "last_seen_at": "2026-09-02T14:21:08.412Z", "conversations_count": 0, "tickets_count": 0, "custom_fields": { "plan": "Experto", "mrr": 119900 } } } ``` > El email es la clave de deduplicación. Si ya existe, la respuesta 409 incluye `error.contact_id` para que actualices en lugar de duplicar. --- URL: https://tinkay.app/docs/api-conversations # Conversations Leer conversaciones del Inbox y sus mensajes. ### `GET /v1/conversations` Lista conversaciones ordenadas por último mensaje. Scope: `conversations:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `status` | "open" | "waiting" | "closed" | "snoozed" | Filtra por estado. | | `contact_id` | string | Solo las de un contacto. | | `limit` | number | 1 a 100. Por defecto 25. | | `cursor` | string | Paginación. | ### `GET /v1/conversations/{id}` Devuelve la conversación con sus mensajes públicos (sin notas internas). Scope: `conversations:read` ## Ejemplo curl: ```bash curl "https://api.tinkay.app/v1/conversations?status=open&limit=50" \ -H "Authorization: Bearer dk_live_xxx" ``` Node: ```js const url = new URL("https://api.tinkay.app/v1/conversations"); url.searchParams.set("status", "open"); url.searchParams.set("limit", "50"); const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } }); const { data } = await res.json(); ``` ## Objeto conversación ```json { "id": "cv_1832", "object": "conversation", "contact_id": "ct_juan", "subject": null, "channel": "messenger", "status": "open", "priority": "normal", "assignee_id": null, "team_id": "team_soporte", "ai_handled": true, "unread": 2, "tags": ["instalación"], "intent": "install_help", "csat": null, "ticket_id": null, "waiting_since": "2026-09-02T14:05:00.000Z", "last_message_at": "2026-09-02T14:05:00.000Z", "last_message_preview": "Ok, espero.", "last_message_author": "contact", "created_at": "2026-09-02T13:51:00.000Z" } ``` --- URL: https://tinkay.app/docs/api-messages # Messages Leer y enviar mensajes dentro de una conversación. ### `GET /v1/messages` Mensajes públicos de una conversación. Scope: `conversations:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `conversation_id` (requerido) | string | Id de la conversación. | ### `POST /v1/messages` Envía un mensaje como miembro del equipo. Scope: `conversations:write` | Campo | Tipo | Descripción | | --- | --- | --- | | `conversation_id` (requerido) | string | Conversación destino. | | `body` (requerido) | string | Texto del mensaje, hasta 5000 caracteres. | | `internal` | boolean | Si es `true`, queda como nota interna y el cliente no la ve. | ### `GET /v1/conversations/{id}/messages` Mismos mensajes, anidados bajo la conversación. Scope: `conversations:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `include_internal` | boolean | Incluye las notas internas del equipo. | ### `POST /v1/conversations/{id}/messages` Responde en la conversación sin repetir el id en el cuerpo. Scope: `conversations:write` | Campo | Tipo | Descripción | | --- | --- | --- | | `body` (requerido) | string | Texto del mensaje, hasta 5000 caracteres. | | `internal` | boolean | Si es `true`, queda como nota interna. | | `author_id` | string | Miembro que figura como autor. Por defecto, la integración. | ## Firmar el mensaje con una persona real Sin `author_id` el mensaje aparece como enviado por la integración. Pasando el id de un miembro, el cliente ve el nombre y la foto de esa persona, igual que si hubiera escrito desde el Inbox. Si el id no pertenece al workspace devolvemos 422 en vez de atribuir el mensaje a nadie. ```bash curl -X POST https://api.tinkay.app/v1/conversations/cv_1832/messages -H "Authorization: Bearer dk_live_xxx" -H "Content-Type: application/json" -d '{ "body": "Ya despachamos tu pedido.", "author_id": "mem_tomas" }' ``` ## Enviar un mensaje curl: ```bash curl -X POST https://api.tinkay.app/v1/messages \ -H "Authorization: Bearer dk_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "conversation_id": "cv_1832", "body": "Ya activamos tu cuenta. Cualquier cosa, escribinos." }' ``` C#: ```csharp var res = await http.PostAsJsonAsync("https://api.tinkay.app/v1/messages", new { conversation_id = "cv_1832", body = "Ya activamos tu cuenta. Cualquier cosa, escribinos." }); res.EnsureSuccessStatusCode(); ``` > Los mensajes creados por API aparecen en el Inbox como enviados por el equipo, con la marca de origen `api`. --- URL: https://tinkay.app/docs/api-tickets # Tickets Crear casos con SLA desde tu propio sistema. ### `GET /v1/tickets` Lista tickets ordenados por actualización. Scope: `tickets:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `status` | "open" | "in_progress" | "waiting" | "resolved" | Filtra por estado. | ### `POST /v1/tickets` Crea un ticket. El SLA se calcula según la prioridad. Scope: `tickets:write` | Campo | Tipo | Descripción | | --- | --- | --- | | `subject` (requerido) | string | De 3 a 200 caracteres. | | `description` | string | Hasta 5000 caracteres. | | `contact_id` (requerido) | string | Contacto del workspace. Devuelve 422 si no existe. | | `priority` | "low" | "normal" | "high" | "urgent" | Por defecto `normal`. | | `conversation_id` | string | Vincula el ticket a una conversación. | | `tags` | string[] | Etiquetas. | ### `GET /v1/tickets/{id}` Busca por id o por número (`#1042`). Scope: `tickets:read` ### `GET /v1/tickets/{id}/notes` Notas internas del ticket. Nunca las ve el cliente. Scope: `tickets:read` ### `POST /v1/tickets/{id}/notes` Agrega una nota interna, por ejemplo el resultado de un chequeo automático. Scope: `tickets:write` | Campo | Tipo | Descripción | | --- | --- | --- | | `body` (requerido) | string | Texto de la nota, hasta 5000 caracteres. | | `author_id` | string | Miembro que figura como autor. Por defecto, la integración. | ## Crear un ticket curl: ```bash curl -X POST https://api.tinkay.app/v1/tickets \ -H "Authorization: Bearer dk_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "subject": "Error 500 al exportar facturas", "description": "El cliente reporta un 500 en /export desde las 14:20.", "contact_id": "ct_8f2a91", "priority": "high", "tags": ["bug", "exportacion"] }' ``` Python: ```python res = requests.post( "https://api.tinkay.app/v1/tickets", headers={"Authorization": f"Bearer {os.environ['TINKAY_API_KEY']}"}, json={ "subject": "Error 500 al exportar facturas", "description": incident.summary, "contact_id": contact_id, "priority": "high", "tags": ["bug", "exportacion"], }, timeout=10, ) ticket = res.json()["data"] print(ticket["number"]) # 1043 ``` > El SLA se define por prioridad: urgente 2 horas, alta 8 horas, normal y baja 24 horas. --- URL: https://tinkay.app/docs/api-articles # Articles Leer los artículos del centro de ayuda. ### `GET /v1/articles` Lista artículos sin el cuerpo, con `content_length`. Scope: `articles:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `status` | "published" | "draft" | "needs_review" | "all" | Por defecto `published`. | | `collection_id` | string | Filtra por colección. | ### `GET /v1/articles/{id}` Devuelve el artículo completo con su HTML. Scope: `articles:read` ## Ejemplo ```bash curl "https://api.tinkay.app/v1/articles?status=published&limit=100" \ -H "Authorization: Bearer dk_live_xxx" ``` > El listado omite el HTML del artículo para que la respuesta sea liviana. Pedí el detalle solo de los que necesites. --- URL: https://tinkay.app/docs/api-events # Events Feed de lo que pasó en el workspace. ### `GET /v1/events` Eventos del workspace, más recientes primero. Incluye el catálogo de tipos en `event_types`. Scope: `events:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `name` | string | Filtra por tipo, por ejemplo `ai.handoff`. | | `limit` | number | 1 a 100. | | `cursor` | string | Paginación. | ## Respuesta ```json { "data": [ { "id": "evt_cc_cv_1832", "name": "conversation.created", "occurred_at": "2026-09-02T13:51:00.000Z", "workspace_id": "ws_tinkay", "payload": { "conversation_id": "cv_1832", "contact_id": "ct_juan", "channel": "messenger" } } ], "next_cursor": "evt_cc_cv_1832", "has_more": true, "event_types": [ { "name": "conversation.created", "description": "Se creó una conversación nueva" } ] } ``` > Para reaccionar en tiempo real conviene usar [webhooks](/docs/webhooks). Este endpoint sirve para auditoría y para recuperar eventos perdidos. --- URL: https://tinkay.app/docs/api-webhooks # Webhooks (API) Crear y listar suscripciones de webhook. ### `GET /v1/webhooks` Lista las suscripciones del workspace. Scope: `webhooks:read` ### `POST /v1/webhooks` Crea una suscripción. La respuesta incluye el `signing_secret` una sola vez. Scope: `webhooks:write` | Campo | Tipo | Descripción | | --- | --- | --- | | `url` (requerido) | string | Debe ser https. | | `events` (requerido) | string[] | Nombres de evento o `["*"]` para todos. | | `enabled` | boolean | Por defecto `true`. | ## Crear una suscripción ```bash curl -X POST https://api.tinkay.app/v1/webhooks \ -H "Authorization: Bearer dk_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.tuempresa.com/hooks/tinkay", "events": ["conversation.created", "ai.handoff", "ticket.created"] }' ``` Respuesta: ```json { "data": { "id": "wh_4c2a", "url": "https://api.tuempresa.com/hooks/tinkay", "events": ["conversation.created", "ai.handoff", "ticket.created"], "enabled": true, "failures": 0, "signing_secret": "whsec_9f2ab7c41d0e83ba5c19" } } ``` > Guardá el `signing_secret` al crearlo: no se vuelve a mostrar. Es el que usás para validar la firma, no la API key. --- URL: https://tinkay.app/docs/api-teams # Teams y Members Equipos, integrantes, roles y disponibilidad. ### `GET /v1/teams` Lista los equipos del workspace. Scope: `teams:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `limit` | number | Máximo 100. | | `cursor` | string | Paginación. | ### `GET /v1/teams/{id}` Devuelve un equipo con sus integrantes. Scope: `teams:read` ### `GET /v1/members` Lista los miembros del workspace. Scope: `teams:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `role` | string | owner, admin, manager o agent. | | `status` | string | active, away, offline o invited. | | `team_id` | string | Filtra por equipo. | ### `GET /v1/members/{id}` Devuelve un miembro. Scope: `teams:read` ## Ejemplo ```bash curl "https://api.tinkay.app/v1/members?status=active&team_id=team_soporte" \ -H "Authorization: Bearer dk_live_xxx" ``` Respuesta: ```json { "object": "list", "data": [ { "object": "member", "id": "mem_tomas", "name": "Tomás Aguirre", "email": "tomas@tinkay.app", "role": "agent", "status": "active", "team_ids": ["team_soporte"] } ], "next_cursor": null, "has_more": false } ``` --- URL: https://tinkay.app/docs/api-knowledge # Collections y Knowledge sources Colecciones del centro de ayuda y fuentes que alimentan a Tinkay AI. ### `GET /v1/collections` Lista las colecciones con su cantidad de artículos. Scope: `articles:read` ### `GET /v1/collections/{id}` Devuelve una colección. Scope: `articles:read` ### `GET /v1/knowledge-sources` Lista las fuentes conectadas y su estado de sincronización. Scope: `knowledge:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `status` | string | synced, processing o failed. | | `type` | string | website, pdf, notion, google_drive, url o api. | ### `GET /v1/knowledge-gaps` Preguntas que los clientes hicieron y la base todavía no responde, ordenadas por volumen. Scope: `knowledge:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `status` | string | open, ignored, assigned o generated. | | `kind` | string | missing u outdated. | ## Convertir huecos en artículos Cada hueco trae `asked_count`, la tendencia contra el período anterior y ejemplos reales de cómo lo preguntaron. Es la lista de trabajo del equipo de contenido: lo que está arriba es lo que más plata cuesta en derivaciones. ```bash curl "https://api.tinkay.app/v1/knowledge-gaps?status=open&limit=10" -H "Authorization: Bearer dk_live_xxx" ``` ## Detectar fuentes con problemas Combinado con el evento `knowledge_source.failed`, este endpoint sirve para alertar cuando una fuente deja de sincronizar. ```bash curl "https://api.tinkay.app/v1/knowledge-sources?status=failed" \ -H "Authorization: Bearer dk_live_xxx" ``` --- URL: https://tinkay.app/docs/api-learned-answers # Learned answers Lo que Tinkay AI ya aprendió y responde sin llamar al modelo. Una **respuesta aprendida** es una pregunta que Tinkay AI ya sabe contestar. Cuando una consulta coincide, se responde desde la memoria: sin tokens, sin costo de modelo y en menos de 200 ms. ### `GET /v1/learned-answers` Lista las respuestas aprendidas. Scope: `ai:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `status` | string | active, pending, paused u outdated. | | `intent` | string | Filtra por intención. | ### `GET /v1/learned-answers/{id}` Devuelve una respuesta aprendida con sus variantes y métricas. Scope: `ai:read` ## Respuesta ```json { "object": "learned_answer", "id": "kb_install", "question": "¿Dónde pego el script de Tinkay en mi sitio?", "variants": ["dónde va el script", "cómo instalo el widget en mi web"], "status": "active", "origin": "ai", "confidence": 0.96, "uses_30d": 148, "uses": 412, "saved_tokens": 688200, "saved_cost": 3128, "source_article_ids": ["art_instalar_script"] } ``` --- URL: https://tinkay.app/docs/api-automations # Automations e Integrations Flujos automáticos y apps conectadas. ### `GET /v1/automations` Lista las automatizaciones con su disparador y ejecuciones. Scope: `automations:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `enabled` | boolean | Filtra por estado. | ### `GET /v1/automations/{id}` Devuelve una automatización con sus nodos y conexiones. Scope: `automations:read` ### `GET /v1/integrations` Lista las apps y canales con su estado. Scope: `integrations:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `status` | string | connected, available o error. | ## Alertar canales caídos ```bash curl "https://api.tinkay.app/v1/integrations?status=error" \ -H "Authorization: Bearer dk_live_xxx" ``` --- URL: https://tinkay.app/docs/api-usage # Usage Consumo de IA, costo, ahorro por memoria y proyección del período. ### `GET /v1/usage` Resumen de uso del período, con serie diaria. Scope: `usage:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `days` | number | Ventana en días: 7, 30 o 90. Por defecto 30. | ## Respuesta ```json { "object": "usage", "period_start": "2026-08-04T00:00:00.000Z", "period_end": "2026-09-02T00:00:00.000Z", "resolutions": { "used": 924, "limit": 6000, "from_memory": 435, "remaining": 5076 }, "cost": 8134, "projected_cost": 12201, "budget": 120000, "saved": 3829, "memory_hit_rate": 47, "tokens_in": 1340500, "tokens_out": 203400, "cost_per_resolution": 8.8, "learned_answers": { "total": 24, "active": 19 }, "days": [ { "date": "2026-08-04T00:00:00.000Z", "memory": 9, "model": 38, "cost": 276, "saved": 69 } ] } ``` > Todos los importes vienen en pesos argentinos. `saved` es lo que la memoria evitó gastar: es la métrica que mejor muestra el retorno de mantener el conocimiento al día. --- URL: https://tinkay.app/docs/api-search # Search Búsqueda de texto libre en conversaciones, contactos, tickets y artículos. ### `GET /v1/search` Busca en todos los recursos o en los que indiques. Scope: `contacts:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `q` (requerido) | string | Texto a buscar. Mínimo 2 caracteres. | | `types` | string | Lista separada por coma: conversation, contact, ticket, article. | | `limit` | number | Resultados por tipo. Máximo 25. | ## Ejemplo ```bash curl "https://api.tinkay.app/v1/search?q=factura&types=ticket,article" \ -H "Authorization: Bearer dk_live_xxx" ``` Respuesta: ```json { "object": "list", "data": [ { "object": "ticket", "id": "tk_1039", "title": "Reemitir factura con datos fiscales", "subtitle": "CUIT 30-71456789-4" }, { "object": "article", "id": "art_medios_pago", "title": "Medios de pago y facturación", "subtitle": "Tarjeta, Mercado Pago, datos fiscales" } ], "has_more": false } ``` # Eventos --- URL: https://tinkay.app/docs/eventos # Referencia de eventos Los 126 eventos que emite Tinkay, con su payload y cuándo se disparan. Cada cambio relevante en un workspace emite un evento. Los mismos eventos alimentan los **webhooks**, las **automatizaciones**, el **registro de actividad** y el endpoint `GET /v1/events`. La versión actual del payload es `2026-09-01`. Los eventos nuevos y los campos nuevos no son cambios que rompan: tu integración debe ignorar lo que no conoce. > Los eventos marcados como disparador pueden iniciar una automatización sin escribir código. ## Estructura de un evento Todos los eventos llegan con la misma envoltura. Lo específico de cada uno vive en `data`. **Campos de la envoltura** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | Identificador único de la entrega. Usalo como clave de idempotencia. | | `object` | string | Siempre `"event"`. | | `name` | string | Nombre del evento, por ejemplo `conversation.created`. | | `api_version` | string | Versión del payload con la que se generó. | | `workspace_id` | string | Workspace que originó el evento. | | `occurred_at` | string | Fecha ISO 8601 del momento en que ocurrió. | | `data` | object | Payload propio del evento. Ver la tabla de cada uno. | event: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "contact_id": "ct_juan", "channel": "messenger" } } ``` ## Conversaciones Todo el ciclo de vida de una conversación: apertura, asignación, prioridad, etiquetas, SLA y cierre. [Ver el detalle de cada evento](/docs/eventos-conversation). | Campo | Tipo | Descripción | | --- | --- | --- | | `conversation.created` | disparador · alto | Alguien abrió una conversación nueva por cualquier canal. | | `conversation.assigned` | disparador · alto | La conversación pasó a una persona o a un equipo. | | `conversation.unassigned` | disparador · medio | La conversación quedó sin responsable. | | `conversation.claimed` | disparador · medio | Un agente tomó una conversación que estaba esperando. | | `conversation.status_changed` | disparador · alto | Cambió el estado: abierta, esperando, pospuesta o cerrada. | | `conversation.closed` | disparador · alto | Se cerró la conversación. | | `conversation.reopened` | disparador · medio | El cliente volvió a escribir en una conversación cerrada. | | `conversation.snoozed` | disparador · medio | Se pospuso hasta una fecha. | | `conversation.priority_changed` | disparador · medio | Cambió la prioridad. | | `conversation.tagged` | disparador · alto | Se agregó una etiqueta. | | `conversation.untagged` | medio | Se quitó una etiqueta. | | `conversation.rated` | disparador · medio | El cliente calificó la atención (CSAT). | | `conversation.merged` | bajo | Dos conversaciones del mismo contacto se unieron. | | `conversation.sla_breached` | disparador · pro · medio | Se venció el tiempo de primera respuesta o de resolución. | | `conversation.participant_added` | bajo | Se sumó otro agente a la conversación. | ## Mensajes Cada mensaje que entra o sale, incluidas notas internas, entregas y lecturas. [Ver el detalle de cada evento](/docs/eventos-message). | Campo | Tipo | Descripción | | --- | --- | --- | | `message.created` | disparador · alto | Se envió o se recibió un mensaje. | | `message.received` | disparador · alto | Mensaje entrante de un cliente. | | `message.sent` | alto | Mensaje saliente de tu equipo o de la IA. | | `message.delivered` | alto | El canal confirmó la entrega. | | `message.read` | alto | El cliente leyó el mensaje. | | `message.failed` | disparador · bajo | El canal rechazó el envío. | | `message.note_created` | disparador · medio | Se agregó una nota interna, invisible para el cliente. | | `message.reaction_added` | medio | Alguien reaccionó a un mensaje. | ## Tinkay AI Lo que hace Tinkay AI: cuándo responde, con qué fuente, cuándo deriva, qué aprende y cuánto consume. [Ver el detalle de cada evento](/docs/eventos-ai). | Campo | Tipo | Descripción | | --- | --- | --- | | `ai.replied` | disparador · alto | Tinkay AI respondió en una conversación. | | `ai.resolved` | disparador · alto | Tinkay AI resolvió la consulta sin intervención humana. | | `ai.handoff` | disparador · alto | Tinkay AI derivó la conversación a una persona. | | `ai.closed_conversation` | disparador · medio | Tinkay AI cerró la conversación después de resolverla. | | `ai.action_called` | disparador · medio | Tinkay AI llamó a una acción de tu sistema. | | `ai.action_failed` | disparador · bajo | Una acción devolvió error o venció el tiempo de espera. | | `ai.answer_learned` | disparador · bajo | Se aprobó una respuesta aprendida: desde ahora se responde sin llamar al modelo. | | `ai.answer_served` | alto | Se respondió con una respuesta aprendida, sin costo de modelo. | | `ai.answer_outdated` | disparador · bajo | Cambió el artículo de origen y la respuesta aprendida quedó marcada para revisar. | | `ai.training_requested` | disparador · pro · bajo | Alguien pidió aprobación para entrenar a la IA con una respuesta. | | `ai.budget_threshold` | disparador · bajo | El consumo del modelo llegó al umbral configurado. | | `ai.budget_exceeded` | disparador · bajo | Se agotó el presupuesto o el cupo de resoluciones del plan. | | `ai.provider_changed` | bajo | Se cambió el proveedor del modelo, por ejemplo a una API propia. | | `ai.provider_error` | disparador · bajo | El proveedor de IA devolvió error y se aplicó el plan alternativo. | ## Tickets Casos con seguimiento: creación, estado, responsable, SLA y resolución. [Ver el detalle de cada evento](/docs/eventos-ticket). | Campo | Tipo | Descripción | | --- | --- | --- | | `ticket.created` | disparador · medio | Se creó un ticket. | | `ticket.updated` | disparador · medio | Cambió algún campo del ticket. | | `ticket.status_changed` | disparador · medio | El ticket cambió de estado. | | `ticket.assigned` | disparador · medio | El ticket pasó a una persona o equipo. | | `ticket.resolved` | disparador · medio | Se resolvió el ticket. | | `ticket.reopened` | disparador · bajo | Se reabrió un ticket resuelto. | | `ticket.sla_breached` | disparador · pro · medio | Venció el SLA del ticket. | | `ticket.note_added` | medio | Se agregó una nota interna al ticket. | | `ticket.deleted` | bajo | Se eliminó un ticket. | ## Contactos Personas que escriben: alta, identificación, atributos, segmentos y bajas. [Ver el detalle de cada evento](/docs/eventos-contact). | Campo | Tipo | Descripción | | --- | --- | --- | | `contact.created` | disparador · alto | Se creó un contacto. | | `contact.updated` | disparador · alto | Cambiaron datos o atributos del contacto. | | `contact.identified` | disparador · alto | El sitio identificó al visitante con Tinkay.identify. | | `contact.merged` | bajo | Dos contactos duplicados se unieron. | | `contact.tagged` | disparador · medio | Se etiquetó a un contacto. | | `contact.deleted` | bajo | Se eliminó un contacto, por ejemplo por un pedido de privacidad. | | `contact.segment_entered` | disparador · pro · medio | El contacto empezó a cumplir las condiciones de un segmento. | | `contact.segment_left` | disparador · pro · medio | El contacto dejó de cumplir un segmento. | ## Empresas Empresas agrupadas a partir de sus contactos. [Ver el detalle de cada evento](/docs/eventos-company). | Campo | Tipo | Descripción | | --- | --- | --- | | `company.created` | pro · bajo | Se creó una empresa a partir de sus contactos. | | `company.updated` | pro · bajo | Cambiaron datos de la empresa. | ## Conocimiento Artículos, colecciones, fuentes de conocimiento y brechas detectadas. [Ver el detalle de cada evento](/docs/eventos-knowledge). | Campo | Tipo | Descripción | | --- | --- | --- | | `article.created` | bajo | Se creó un artículo, aunque sea borrador. | | `article.published` | disparador · bajo | Se publicó un artículo en el centro de ayuda. | | `article.updated` | disparador · medio | Se editó un artículo publicado. | | `article.unpublished` | bajo | Un artículo volvió a borrador. | | `article.rated` | medio | Alguien votó si el artículo fue útil. | | `collection.created` | bajo | Se creó una colección. | | `knowledge_source.synced` | disparador · bajo | Terminó de sincronizarse una fuente de conocimiento. | | `knowledge_source.failed` | disparador · bajo | Falló la sincronización de una fuente. | | `knowledge_gap.detected` | disparador · bajo | Se detectó una pregunta frecuente sin respuesta documentada. | | `knowledge_gap.resolved` | bajo | Se cubrió una brecha con un artículo o una respuesta aprendida. | ## Datos de tu empresa Los cuatro niveles con los que Tinkay lee datos de tu negocio: atributos firmados del Messenger, conectores HTTP en vivo, sincronización de clientes y acceso directo por réplica de solo lectura. [Ver el detalle de cada evento](/docs/eventos-data). | Campo | Tipo | Descripción | | --- | --- | --- | | `attribute.detected` | disparador · medio | El Messenger mandó un atributo que Tinkay no conocía y quedó disponible para la IA. | | `attribute.updated` | bajo | Cambió la configuración de un atributo: etiqueta, tipo, visibilidad para la IA o marca de sensible. | | `attribute.payload_rejected` | disparador · bajo | Llegó un payload de atributos con firma inválida o campos mal formados. | | `connector.called` | alto | Tinkay consultó en vivo un conector HTTP de tu sistema durante una conversación. | | `connector.failed` | disparador · bajo | Un conector devolvió error, venció el tiempo de espera o no pudo mapear la respuesta. | | `connector.updated` | bajo | Se creó, editó o pausó un conector HTTP. | | `write_action.proposed` | disparador · medio | Tinkay propuso escribir algo en tu sistema y quedó esperando confirmación. | | `write_action.confirmed` | disparador · medio | Una persona autorizó la escritura: recién ahí sale el pedido a tu sistema. | | `write_action.rejected` | disparador · bajo | Alguien rechazó la escritura y nunca salió ningún pedido. | | `write_action.executed` | disparador · medio | Se escribió en tu sistema: se reservó un turno, se cambió un pedido, se emitió una nota de crédito. | | `write_action.failed` | disparador · bajo | Una escritura devolvió error o venció el tiempo de espera, y la conversación se derivó a una persona. | | `write_action.blocked` | disparador · bajo | Una protección frenó la escritura antes de que saliera: límites, acción apagada o datos faltantes. | | `write_action.reverted` | disparador · bajo | Se deshizo una escritura ejecutando la acción inversa declarada. | | `write_action.updated` | bajo | Se creó, editó o pausó una acción de escritura, o cambió su nivel de confirmación. | | `sync.completed` | disparador · medio | Terminó una sincronización de tu base de clientes hacia Contactos. | | `sync.failed` | disparador · bajo | Falló una sincronización de clientes y no se aplicó ningún cambio. | | `database.connected` | business · bajo | Se habilitó el acceso directo por réplica de solo lectura o agente de túnel. | | `database.query_blocked` | disparador · business · bajo | Se bloqueó una consulta contra tu base por salir del alcance permitido. | ## Equipo Miembros, equipos, roles, disponibilidad y permisos. [Ver el detalle de cada evento](/docs/eventos-team). | Campo | Tipo | Descripción | | --- | --- | --- | | `member.invited` | disparador · bajo | Se invitó a alguien al workspace. | | `member.joined` | disparador · bajo | La persona aceptó la invitación. | | `member.role_changed` | disparador · bajo | Cambió el rol de un miembro. | | `member.removed` | bajo | Se quitó a alguien del workspace. | | `member.availability_changed` | disparador · alto | Un agente cambió su disponibilidad. | | `team.created` | bajo | Se creó un equipo. | | `team.member_added` | bajo | Se sumó una persona a un equipo. | | `team.member_removed` | bajo | Se quitó una persona de un equipo. | | `team.permissions_changed` | disparador · pro · bajo | El líder cambió los permisos del equipo, por ejemplo quién puede entrenar la IA. | ## Automatizaciones Ejecuciones de tus flujos, con sus pasos y errores. [Ver el detalle de cada evento](/docs/eventos-automation). | Campo | Tipo | Descripción | | --- | --- | --- | | `automation.enabled` | bajo | Se activó una automatización. | | `automation.disabled` | bajo | Se pausó una automatización. | | `automation.run_started` | alto | Empezó una ejecución. | | `automation.run_completed` | alto | Terminó una ejecución con éxito. | | `automation.run_failed` | disparador · bajo | Falló un paso de la automatización. | | `automation.goal_reached` | pro · medio | Se cumplió el objetivo definido para la automatización. | ## Canales Canales y apps conectadas: altas, bajas y fallas de token. [Ver el detalle de cada evento](/docs/eventos-channel). | Campo | Tipo | Descripción | | --- | --- | --- | | `channel.connected` | disparador · bajo | Se conectó un canal o una app. | | `channel.disconnected` | bajo | Se desconectó un canal. | | `channel.error` | disparador · bajo | Un canal dejó de funcionar, por ejemplo por un token vencido. | ## Messenger Comportamiento del Messenger en el sitio del cliente. [Ver el detalle de cada evento](/docs/eventos-widget). | Campo | Tipo | Descripción | | --- | --- | --- | | `widget.opened` | disparador · alto | Un visitante abrió el Messenger. | | `widget.closed` | alto | El visitante cerró el Messenger. | | `widget.article_viewed` | alto | El visitante leyó un artículo dentro del Messenger. | | `widget.search_performed` | disparador · alto | El visitante buscó en el centro de ayuda desde el Messenger. | | `widget.search_empty` | disparador · medio | Una búsqueda no devolvió resultados: candidata a artículo nuevo. | ## Facturación Suscripción, facturas y límites de consumo. [Ver el detalle de cada evento](/docs/eventos-billing). | Campo | Tipo | Descripción | | --- | --- | --- | | `subscription.created` | bajo | Se activó una suscripción. | | `subscription.updated` | disparador · bajo | Cambió el plan o la cantidad de asientos. | | `subscription.canceled` | disparador · bajo | Se canceló la suscripción. | | `invoice.paid` | bajo | Se cobró una factura. | | `invoice.payment_failed` | disparador · bajo | Falló el cobro. | | `usage.limit_approaching` | disparador · bajo | El consumo llegó al 80% del límite del plan. | | `usage.limit_reached` | disparador · bajo | Se alcanzó un límite del plan. | ## Seguridad API keys, webhooks, accesos y sesiones. [Ver el detalle de cada evento](/docs/eventos-security). | Campo | Tipo | Descripción | | --- | --- | --- | | `api_key.created` | bajo | Se creó una API key. | | `api_key.revoked` | disparador · bajo | Se revocó una API key. | | `webhook.created` | bajo | Se registró un webhook. | | `webhook.delivery_failed` | disparador · bajo | Una entrega de webhook falló después de todos los reintentos. | | `webhook.disabled` | disparador · bajo | Se desactivó un webhook por fallas repetidas. | | `login.succeeded` | medio | Alguien inició sesión. | | `login.failed` | disparador · bajo | Intento de acceso fallido. | | `session.revoked` | bajo | Se cerró una sesión activa. | ## Workspace Configuración general, dominios y publicaciones. [Ver el detalle de cada evento](/docs/eventos-workspace). | Campo | Tipo | Descripción | | --- | --- | --- | | `domain.verified` | bajo | Se verificó un dominio permitido o el del centro de ayuda. | | `workspace.updated` | bajo | Cambió la configuración general del workspace. | | `messenger.published` | disparador · bajo | Se publicó una versión del Messenger. | | `help_center.published` | bajo | Se publicó el centro de ayuda. | --- URL: https://tinkay.app/docs/eventos-conversation # Eventos de conversaciones Todo el ciclo de vida de una conversación: apertura, asignación, prioridad, etiquetas, SLA y cierre. Todo el ciclo de vida de una conversación: apertura, asignación, prioridad, etiquetas, SLA y cierre. Son 15 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## conversation.created Alguien abrió una conversación nueva por cualquier canal. Disponible desde 2026-01-15. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `workspace_id` | string | ID del workspace | | `contact_id` | string | ID del contacto | | `channel` | string | messenger | email | whatsapp | instagram | api | | `subject` | string | Asunto, si el canal lo trae | | `created_at` | string | Fecha ISO 8601 | conversation.created: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "workspace_id": "ws_tinkay", "contact_id": "ct_juan", "channel": "messenger", "subject": "Widget no carga en Shopify", "created_at": "2026-09-02T14:03:11.000Z" } } ``` ## conversation.assigned La conversación pasó a una persona o a un equipo. Disponible desde 2026-01-15. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `assignee_id` | string | ID del agente o null | | `team_id` | string | ID del equipo o null | | `assigned_by` | string | ID de quien asignó, o 'automation' | | `previous_assignee_id` | string | null | Responsable anterior | conversation.assigned: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.assigned", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "assignee_id": "mem_tomas", "team_id": "team_soporte", "assigned_by": "mem_camila", "previous_assignee_id": null } } ``` ## conversation.unassigned La conversación quedó sin responsable. Disponible desde 2026-03-02. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `previous_assignee_id` | string | null | Responsable anterior | | `unassigned_by` | string | ID de quien la liberó | conversation.unassigned: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.unassigned", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "previous_assignee_id": null, "unassigned_by": "mem_camila" } } ``` ## conversation.claimed Un agente tomó una conversación que estaba esperando. Disponible desde 2026-09-02. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `member_id` | string | ID de quien la tomó | | `waiting_seconds` | number | Cuánto esperó el cliente | conversation.claimed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.claimed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "member_id": "mem_tomas", "waiting_seconds": 312 } } ``` ## conversation.status_changed Cambió el estado: abierta, esperando, pospuesta o cerrada. Disponible desde 2026-01-15. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `status` | string | open | waiting | snoozed | closed | | `previous_status` | string | Estado anterior | | `changed_by` | string | ID del agente, 'ai' o 'automation' | conversation.status_changed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.status_changed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "status": "open", "previous_status": "waiting", "changed_by": "mem_camila" } } ``` ## conversation.closed Se cerró la conversación. Disponible desde 2026-01-15. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `closed_by` | string | ID del agente, 'ai' o 'automation' | | `resolution` | string | resolved | no_response | duplicate | | `duration_seconds` | number | Desde la apertura | conversation.closed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.closed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "closed_by": "mem_camila", "resolution": "resolved", "duration_seconds": 1840 } } ``` ## conversation.reopened El cliente volvió a escribir en una conversación cerrada. Disponible desde 2026-01-15. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `reopened_by` | string | ID del contacto o del agente | | `closed_for_seconds` | number | Cuánto estuvo cerrada | conversation.reopened: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.reopened", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "reopened_by": "mem_camila", "closed_for_seconds": 86400 } } ``` ## conversation.snoozed Se pospuso hasta una fecha. Disponible desde 2026-04-10. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `snoozed_until` | string | Fecha ISO 8601 | | `snoozed_by` | string | ID del agente | conversation.snoozed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.snoozed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "snoozed_until": "2026-09-02T14:03:11.000Z", "snoozed_by": "mem_camila" } } ``` ## conversation.priority_changed Cambió la prioridad. Disponible desde 2026-02-01. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `priority` | string | low | normal | high | urgent | | `previous_priority` | string | Prioridad anterior | conversation.priority_changed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.priority_changed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "priority": "urgent", "previous_priority": "normal" } } ``` ## conversation.tagged Se agregó una etiqueta. Disponible desde 2026-01-15. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `tag` | string | Etiqueta agregada | | `tags` | array | Lista completa después del cambio | | `tagged_by` | string | ID del agente, 'ai' o 'automation' | conversation.tagged: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.tagged", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "tag": "instalación", "tags": [ "instalación", "shopify" ], "tagged_by": "mem_camila" } } ``` ## conversation.untagged Se quitó una etiqueta. Disponible desde 2026-01-15. Volumen medio. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `tag` | string | Etiqueta quitada | | `tags` | array | Lista completa después del cambio | conversation.untagged: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.untagged", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "tag": "instalación", "tags": [ "instalación", "shopify" ] } } ``` ## conversation.rated El cliente calificó la atención (CSAT). Disponible desde 2026-02-20. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `score` | number | 1 a 5 | | `comment` | string | Comentario libre o null | | `rated_by` | string | ID del contacto | | `agent_id` | string | Quién atendió | conversation.rated: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.rated", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "score": 5, "comment": "Resolvieron todo en minutos.", "rated_by": "mem_camila", "agent_id": "obj_8f1c2a" } } ``` ## conversation.merged Dos conversaciones del mismo contacto se unieron. Disponible desde 2026-05-06. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | Conversación resultante | | `merged_conversation_id` | string | La que se absorbió | | `merged_by` | string | ID del agente | conversation.merged: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.merged", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "merged_conversation_id": "obj_8f1c2a", "merged_by": "mem_camila" } } ``` ## conversation.sla_breached Se venció el tiempo de primera respuesta o de resolución. Disponible desde 2026-03-18. Volumen medio. Se puede usar como disparador de automatizaciones. Requiere plan Avanzado. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `kind` | string | first_response | resolution | | `target_seconds` | number | Objetivo del SLA | | `elapsed_seconds` | number | Tiempo transcurrido | | `assignee_id` | string | Responsable | conversation.sla_breached: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.sla_breached", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "kind": "first_response", "target_seconds": 3600, "elapsed_seconds": 5120, "assignee_id": "mem_tomas" } } ``` ## conversation.participant_added Se sumó otro agente a la conversación. Disponible desde 2026-06-01. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `member_id` | string | Agente sumado | | `added_by` | string | Quién lo sumó | conversation.participant_added: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.participant_added", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "member_id": "mem_tomas", "added_by": "mem_camila" } } ``` --- URL: https://tinkay.app/docs/eventos-message # Eventos de mensajes Cada mensaje que entra o sale, incluidas notas internas, entregas y lecturas. Cada mensaje que entra o sale, incluidas notas internas, entregas y lecturas. Son 8 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## message.created Se envió o se recibió un mensaje. Disponible desde 2026-01-15. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `conversation_id` | string | ID de la conversación | | `author_type` | string | contact | member | ai | system | | `author_id` | string | ID del autor o null | | `body` | string | Texto del mensaje | | `attachments` | array | Lista de adjuntos | | `created_at` | string | Fecha ISO 8601 | message.created: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "message.created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "conversation_id": "cv_1832", "author_type": "contact", "author_id": "obj_8f1c2a", "body": "Hola, puse el script pero el widget no aparece.", "attachments": [ { "id": "att_1", "name": "captura.png", "size": 184320, "type": "image" } ], "created_at": "2026-09-02T14:03:11.000Z" } } ``` ## message.received Mensaje entrante de un cliente. Disponible desde 2026-01-15. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `conversation_id` | string | ID de la conversación | | `contact_id` | string | Quién escribió | | `body` | string | Texto del mensaje | | `channel` | string | Canal de origen | message.received: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "message.received", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "conversation_id": "cv_1832", "contact_id": "ct_juan", "body": "Hola, puse el script pero el widget no aparece.", "channel": "messenger" } } ``` ## message.sent Mensaje saliente de tu equipo o de la IA. Disponible desde 2026-01-15. Volumen alto. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `conversation_id` | string | ID de la conversación | | `author_type` | string | member | ai | | `author_id` | string | ID del autor | message.sent: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "message.sent", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "conversation_id": "cv_1832", "author_type": "member", "author_id": "obj_8f1c2a" } } ``` ## message.delivered El canal confirmó la entrega. Disponible desde 2026-04-02. Volumen alto. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `channel` | string | Canal | | `delivered_at` | string | Fecha ISO 8601 | message.delivered: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "message.delivered", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "channel": "messenger", "delivered_at": "2026-09-02T14:03:11.000Z" } } ``` ## message.read El cliente leyó el mensaje. Disponible desde 2026-04-02. Volumen alto. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `read_at` | string | Fecha ISO 8601 | message.read: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "message.read", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "read_at": "2026-09-02T14:03:11.000Z" } } ``` ## message.failed El canal rechazó el envío. Disponible desde 2026-04-02. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `channel` | string | Canal | | `error_code` | string | Código del proveedor | | `error_message` | string | Detalle | message.failed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "message.failed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "channel": "messenger", "error_code": "131047", "error_message": "Message failed to send because more than 24 hours have passed." } } ``` ## message.note_created Se agregó una nota interna, invisible para el cliente. Disponible desde 2026-01-15. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `conversation_id` | string | ID de la conversación | | `author_id` | string | Quién la escribió | | `mentions` | array | IDs mencionados | message.note_created: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "message.note_created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "conversation_id": "cv_1832", "author_id": "obj_8f1c2a", "mentions": [ "mem_nico" ] } } ``` ## message.reaction_added Alguien reaccionó a un mensaje. Disponible desde 2026-05-20. Volumen medio. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `emoji` | string | Emoji | | `member_id` | string | Quién reaccionó | message.reaction_added: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "message.reaction_added", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "emoji": "👍", "member_id": "mem_tomas" } } ``` --- URL: https://tinkay.app/docs/eventos-ai # Eventos de tinkay ai Lo que hace Tinkay AI: cuándo responde, con qué fuente, cuándo deriva, qué aprende y cuánto consume. Lo que hace Tinkay AI: cuándo responde, con qué fuente, cuándo deriva, qué aprende y cuánto consume. Son 14 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## ai.replied Tinkay AI respondió en una conversación. Disponible desde 2026-01-15. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `conversation_id` | string | ID de la conversación | | `message_id` | string | Mensaje generado | | `source` | string | memory | model | | `confidence` | number | 0 a 1 | | `intent` | string | Intención detectada | | `sources` | array | Artículos usados | | `tokens` | object | Tokens de entrada y salida | | `cost_ars` | number | Costo de la llamada en pesos | ai.replied: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.replied", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "conversation_id": "cv_1832", "message_id": "msg_8f1c2a", "source": "memory", "confidence": 0.93, "intent": "install_help", "sources": [ "art_instalar_script" ], "tokens": { "input": 1450, "output": 220 }, "cost_ars": 7.94 } } ``` ## ai.resolved Tinkay AI resolvió la consulta sin intervención humana. Disponible desde 2026-01-15. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `conversation_id` | string | ID de la conversación | | `intent` | string | Intención | | `turns` | number | Mensajes que tomó | | `csat` | string | Calificación si la hubo | ai.resolved: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.resolved", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "conversation_id": "cv_1832", "intent": "install_help", "turns": 3, "csat": "Calificación si la hubo" } } ``` ## ai.handoff Tinkay AI derivó la conversación a una persona. Disponible desde 2026-01-15. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `conversation_id` | string | ID de la conversación | | `reason` | string | low_confidence | sensitive_topic | vip | customer_request | out_of_hours | | `confidence` | number | Confianza del último intento | | `team_id` | string | Equipo sugerido | ai.handoff: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.handoff", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "conversation_id": "cv_1832", "reason": "low_confidence", "confidence": 0.93, "team_id": "team_soporte" } } ``` ## ai.closed_conversation Tinkay AI cerró la conversación después de resolverla. Disponible desde 2026-09-02. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `conversation_id` | string | ID de la conversación | | `reason` | string | resolved | no_response | | `asked_csat` | boolean | Si pidió calificación | ai.closed_conversation: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.closed_conversation", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "conversation_id": "cv_1832", "reason": "low_confidence", "asked_csat": true } } ``` ## ai.action_called Tinkay AI llamó a una acción de tu sistema. Disponible desde 2026-02-12. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `conversation_id` | string | ID de la conversación | | `action_id` | string | ID de la acción | | `request` | object | Parámetros enviados | | `response_status` | number | Código HTTP | | `duration_ms` | number | Tiempo de respuesta | ai.action_called: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.action_called", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "conversation_id": "cv_1832", "action_id": "act_subscription", "request": { "workspace": "tinkay" }, "response_status": 200, "duration_ms": 212 } } ``` ## ai.action_failed Una acción devolvió error o venció el tiempo de espera. Disponible desde 2026-02-12. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `conversation_id` | string | ID de la conversación | | `action_id` | string | ID de la acción | | `error` | string | Detalle del error | | `timed_out` | boolean | true si venció | ai.action_failed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.action_failed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "conversation_id": "cv_1832", "action_id": "act_subscription", "error": "connect ETIMEDOUT", "timed_out": false } } ``` ## ai.answer_learned Se aprobó una respuesta aprendida: desde ahora se responde sin llamar al modelo. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `learned_answer_id` | string | ID del objeto | | `question` | string | Pregunta canónica | | `approved_by` | string | Quién la aprobó | | `origin` | string | ai | manual | gap | ai.answer_learned: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.answer_learned", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "learned_answer_id": "kb_install", "question": "¿Dónde pego el script de Tinkay en mi sitio?", "approved_by": "mem_camila", "origin": "ai" } } ``` ## ai.answer_served Se respondió con una respuesta aprendida, sin costo de modelo. Disponible desde 2026-09-02. Volumen alto. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `learned_answer_id` | string | ID del objeto | | `conversation_id` | string | ID de la conversación | | `match_score` | number | Similitud con la pregunta | | `saved_cost_ars` | number | Ahorro estimado en pesos | ai.answer_served: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.answer_served", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "learned_answer_id": "kb_install", "conversation_id": "cv_1832", "match_score": 0.91, "saved_cost_ars": 7.59 } } ``` ## ai.answer_outdated Cambió el artículo de origen y la respuesta aprendida quedó marcada para revisar. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `learned_answer_id` | string | ID del objeto | | `article_id` | string | Artículo que cambió | | `reason` | string | Motivo | ai.answer_outdated: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.answer_outdated", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "learned_answer_id": "kb_install", "article_id": "art_instalar_script", "reason": "low_confidence" } } ``` ## ai.training_requested Alguien pidió aprobación para entrenar a la IA con una respuesta. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. Requiere plan Avanzado. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `candidate_id` | string | ID del objeto | | `requested_by` | string | Quién lo pidió | | `approver_ids` | array | Quiénes pueden aprobarlo | ai.training_requested: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.training_requested", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "candidate_id": "cand_dotnet", "requested_by": "mem_tomas", "approver_ids": [ "mem_camila", "mem_nico" ] } } ``` ## ai.budget_threshold El consumo del modelo llegó al umbral configurado. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `percent` | number | Porcentaje alcanzado | | `spent_ars` | number | Gastado en el período | | `budget_ars` | number | Presupuesto | ai.budget_threshold: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.budget_threshold", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "percent": 80, "spent_ars": 366160, "budget_ars": 460000 } } ``` ## ai.budget_exceeded Se agotó el presupuesto o el cupo de resoluciones del plan. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `spent_ars` | number | Gastado | | `budget_ars` | number | Presupuesto | | `fallback` | string | memory_only | handoff | byo_key | | `byo_provider` | string | Proveedor propio si está configurado | ai.budget_exceeded: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.budget_exceeded", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "spent_ars": 366160, "budget_ars": 460000, "fallback": "memory_only", "byo_provider": "openai" } } ``` ## ai.provider_changed Se cambió el proveedor del modelo, por ejemplo a una API propia. Disponible desde 2026-09-02. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `provider` | string | tinkay | openai | anthropic | azure | google | custom | | `model` | string | Modelo elegido | | `changed_by` | string | Quién lo cambió | ai.provider_changed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.provider_changed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "provider": "openai", "model": "gpt-4o-mini", "changed_by": "mem_camila" } } ``` ## ai.provider_error El proveedor de IA devolvió error y se aplicó el plan alternativo. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `provider` | string | Proveedor | | `status` | string | Código HTTP | | `fallback_used` | string | memory | handoff | ai.provider_error: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ai.provider_error", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "provider": "openai", "status": "open", "fallback_used": "memory" } } ``` --- URL: https://tinkay.app/docs/eventos-ticket # Eventos de tickets Casos con seguimiento: creación, estado, responsable, SLA y resolución. Casos con seguimiento: creación, estado, responsable, SLA y resolución. Son 9 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## ticket.created Se creó un ticket. Disponible desde 2026-01-15. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `number` | number | Número visible | | `subject` | string | Asunto | | `contact_id` | string | Contacto | | `conversation_id` | string | Conversación vinculada o null | | `priority` | string | Prioridad | | `created_by` | string | Agente, 'ai' o 'api' | ticket.created: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ticket.created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "number": 1041, "subject": "Widget no carga en Shopify", "contact_id": "ct_juan", "conversation_id": "cv_1832", "priority": "urgent", "created_by": "mem_camila" } } ``` ## ticket.updated Cambió algún campo del ticket. Disponible desde 2026-01-15. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `changes` | object | Campos modificados con valor anterior y nuevo | | `updated_by` | string | Quién lo cambió | ticket.updated: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ticket.updated", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "changes": { "status": "in_progress" }, "updated_by": "mem_camila" } } ``` ## ticket.status_changed El ticket cambió de estado. Disponible desde 2026-01-15. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `status` | string | open | in_progress | waiting | resolved | | `previous_status` | string | Estado anterior | ticket.status_changed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ticket.status_changed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "status": "open", "previous_status": "waiting" } } ``` ## ticket.assigned El ticket pasó a una persona o equipo. Disponible desde 2026-01-15. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `assignee_id` | string | Responsable | | `team_id` | string | Equipo | ticket.assigned: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ticket.assigned", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "assignee_id": "mem_tomas", "team_id": "team_soporte" } } ``` ## ticket.resolved Se resolvió el ticket. Disponible desde 2026-01-15. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `resolved_by` | string | Quién lo resolvió | | `resolution_seconds` | number | Tiempo total | ticket.resolved: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ticket.resolved", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "resolved_by": "mem_camila", "resolution_seconds": 4320 } } ``` ## ticket.reopened Se reabrió un ticket resuelto. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `reopened_by` | string | Quién lo reabrió | ticket.reopened: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ticket.reopened", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "reopened_by": "mem_camila" } } ``` ## ticket.sla_breached Venció el SLA del ticket. Disponible desde 2026-03-18. Volumen medio. Se puede usar como disparador de automatizaciones. Requiere plan Avanzado. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `due_at` | string | Fecha ISO 8601 | | `overdue_seconds` | number | Cuánto se pasó | | `assignee_id` | string | Responsable | ticket.sla_breached: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ticket.sla_breached", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "due_at": "2026-09-02T14:03:11.000Z", "overdue_seconds": 7200, "assignee_id": "mem_tomas" } } ``` ## ticket.note_added Se agregó una nota interna al ticket. Disponible desde 2026-01-15. Volumen medio. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `note_id` | string | ID de la nota | | `author_id` | string | Autor | ticket.note_added: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ticket.note_added", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "note_id": "obj_8f1c2a", "author_id": "obj_8f1c2a" } } ``` ## ticket.deleted Se eliminó un ticket. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `deleted_by` | string | Quién lo eliminó | ticket.deleted: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "ticket.deleted", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "deleted_by": "mem_camila" } } ``` --- URL: https://tinkay.app/docs/eventos-contact # Eventos de contactos Personas que escriben: alta, identificación, atributos, segmentos y bajas. Personas que escriben: alta, identificación, atributos, segmentos y bajas. Son 8 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## contact.created Se creó un contacto. Disponible desde 2026-01-15. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `email` | string | Email | | `name` | string | Nombre | | `source` | string | messenger | import | api | manual | contact.created: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "contact.created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "email": "juan@loomi.com.ar", "name": "Juan Pérez", "source": "memory" } } ``` ## contact.updated Cambiaron datos o atributos del contacto. Disponible desde 2026-01-15. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `changes` | object | Campos modificados | | `updated_by` | string | Quién lo cambió | contact.updated: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "contact.updated", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "changes": { "status": "in_progress" }, "updated_by": "mem_camila" } } ``` ## contact.identified El sitio identificó al visitante con Tinkay.identify. Disponible desde 2026-02-05. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `external_id` | string | ID en tu sistema | | `attributes` | object | Atributos enviados | contact.identified: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "contact.identified", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "external_id": "usr_84213", "attributes": { "plan": "pro", "seats": 15 } } } ``` ## contact.merged Dos contactos duplicados se unieron. Disponible desde 2026-05-06. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | Contacto resultante | | `merged_contact_id` | string | El que se absorbió | contact.merged: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "contact.merged", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "merged_contact_id": "obj_8f1c2a" } } ``` ## contact.tagged Se etiquetó a un contacto. Disponible desde 2026-01-15. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `tag` | string | Etiqueta | | `tags` | array | Lista completa | contact.tagged: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "contact.tagged", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "tag": "instalación", "tags": [ "instalación", "shopify" ] } } ``` ## contact.deleted Se eliminó un contacto, por ejemplo por un pedido de privacidad. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `reason` | string | manual | gdpr_request | api | contact.deleted: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "contact.deleted", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "reason": "low_confidence" } } ``` ## contact.segment_entered El contacto empezó a cumplir las condiciones de un segmento. Disponible desde 2026-06-11. Volumen medio. Se puede usar como disparador de automatizaciones. Requiere plan Avanzado. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `segment_id` | string | ID del segmento | | `segment_name` | string | Nombre | contact.segment_entered: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "contact.segment_entered", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "segment_id": "seg_pro", "segment_name": "Cuentas Pro" } } ``` ## contact.segment_left El contacto dejó de cumplir un segmento. Disponible desde 2026-06-11. Volumen medio. Se puede usar como disparador de automatizaciones. Requiere plan Avanzado. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `segment_id` | string | ID del segmento | contact.segment_left: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "contact.segment_left", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "segment_id": "seg_pro" } } ``` --- URL: https://tinkay.app/docs/eventos-company # Eventos de empresas Empresas agrupadas a partir de sus contactos. Empresas agrupadas a partir de sus contactos. Son 2 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## company.created Se creó una empresa a partir de sus contactos. Disponible desde 2026-06-11. Volumen bajo. Requiere plan Avanzado. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `name` | string | Razón social | | `domain` | string | Dominio | company.created: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "company.created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "name": "Juan Pérez", "domain": "tinkay.app" } } ``` ## company.updated Cambiaron datos de la empresa. Disponible desde 2026-06-11. Volumen bajo. Requiere plan Avanzado. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `changes` | object | Campos modificados | company.updated: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "company.updated", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "changes": { "status": "in_progress" } } } ``` --- URL: https://tinkay.app/docs/eventos-knowledge # Eventos de conocimiento Artículos, colecciones, fuentes de conocimiento y brechas detectadas. Artículos, colecciones, fuentes de conocimiento y brechas detectadas. Son 10 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## article.created Se creó un artículo, aunque sea borrador. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `title` | string | Título | | `collection_id` | string | Colección | | `author_id` | string | Autor | article.created: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "article.created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "title": "Instalar el script en cualquier sitio", "collection_id": "col_messenger", "author_id": "obj_8f1c2a" } } ``` ## article.published Se publicó un artículo en el centro de ayuda. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `title` | string | Título | | `slug` | string | URL pública | | `published_by` | string | Quién publicó | article.published: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "article.published", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "title": "Instalar el script en cualquier sitio", "slug": "instalar-script", "published_by": "mem_camila" } } ``` ## article.updated Se editó un artículo publicado. Disponible desde 2026-01-15. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `changes` | object | Campos modificados | | `updated_by` | string | Quién lo editó | article.updated: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "article.updated", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "changes": { "status": "in_progress" }, "updated_by": "mem_camila" } } ``` ## article.unpublished Un artículo volvió a borrador. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `unpublished_by` | string | Quién lo despublicó | article.unpublished: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "article.unpublished", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "unpublished_by": "mem_camila" } } ``` ## article.rated Alguien votó si el artículo fue útil. Disponible desde 2026-02-20. Volumen medio. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `helpful` | string | yes | partial | no | | `comment` | string | Comentario o null | article.rated: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "article.rated", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "helpful": "yes", "comment": "Resolvieron todo en minutos." } } ``` ## collection.created Se creó una colección. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `name` | string | Nombre | collection.created: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "collection.created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "name": "Juan Pérez" } } ``` ## knowledge_source.synced Terminó de sincronizarse una fuente de conocimiento. Disponible desde 2026-03-01. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `type` | string | website | pdf | notion | google_drive | url | api | | `pages` | number | Páginas indexadas | | `duration_ms` | number | Duración | knowledge_source.synced: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "knowledge_source.synced", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "type": "website", "pages": 142, "duration_ms": 212 } } ``` ## knowledge_source.failed Falló la sincronización de una fuente. Disponible desde 2026-03-01. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `type` | string | Tipo de fuente | | `error` | string | Detalle del error | knowledge_source.failed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "knowledge_source.failed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "type": "Tipo de fuente", "error": "connect ETIMEDOUT" } } ``` ## knowledge_gap.detected Se detectó una pregunta frecuente sin respuesta documentada. Disponible desde 2026-04-22. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `question` | string | Pregunta | | `occurrences` | number | Veces preguntada | | `kind` | string | missing | outdated | knowledge_gap.detected: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "knowledge_gap.detected", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "question": "¿Dónde pego el script de Tinkay en mi sitio?", "occurrences": 64, "kind": "first_response" } } ``` ## knowledge_gap.resolved Se cubrió una brecha con un artículo o una respuesta aprendida. Disponible desde 2026-04-22. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `resolved_with` | string | article | learned_answer | | `resource_id` | string | ID del recurso | knowledge_gap.resolved: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "knowledge_gap.resolved", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "resolved_with": "article", "resource_id": "obj_8f1c2a" } } ``` --- URL: https://tinkay.app/docs/eventos-data # Eventos de datos de tu empresa Los cuatro niveles con los que Tinkay lee datos de tu negocio: atributos firmados del Messenger, conectores HTTP en vivo, sincronización de clientes y acceso directo por réplica de solo lectura. Los cuatro niveles con los que Tinkay lee datos de tu negocio: atributos firmados del Messenger, conectores HTTP en vivo, sincronización de clientes y acceso directo por réplica de solo lectura. Son 18 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## attribute.detected El Messenger mandó un atributo que Tinkay no conocía y quedó disponible para la IA. Disponible desde 2026-09-02. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `key` | string | Clave del atributo tal como llega en el payload | | `type` | string | text | number | boolean | date | currency | email | url | | `origin` | string | messenger | api | sync | connector | manual | | `contact_id` | string | Contacto donde se vio primero | | `sensitive` | boolean | true si quedó marcado como dato sensible | attribute.detected: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "attribute.detected", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "key": "saldo_ars", "type": "text", "origin": "ai", "contact_id": "ct_juan", "sensitive": false } } ``` ## attribute.updated Cambió la configuración de un atributo: etiqueta, tipo, visibilidad para la IA o marca de sensible. Disponible desde 2026-09-02. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `key` | string | Clave del atributo | | `changes` | object | Campos modificados con valor anterior y nuevo | | `ai_visible` | boolean | Si Tinkay AI puede leerlo | | `updated_by` | string | Quién lo cambió | attribute.updated: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "attribute.updated", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "key": "saldo_ars", "changes": { "status": "in_progress" }, "ai_visible": true, "updated_by": "mem_camila" } } ``` ## attribute.payload_rejected Llegó un payload de atributos con firma inválida o campos mal formados. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `reason` | string | invalid_signature | malformed_json | unknown_field | type_mismatch | | `field` | string | Campo que falló o null | | `host` | string | Dominio que lo envió | attribute.payload_rejected: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "attribute.payload_rejected", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "reason": "low_confidence", "field": "saldo_ars", "host": "tienda.ejemplo.com" } } ``` ## connector.called Tinkay consultó en vivo un conector HTTP de tu sistema durante una conversación. Disponible desde 2026-09-02. Volumen alto. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `connector_id` | string | ID del objeto | | `conversation_id` | string | Conversación que lo pidió | | `trigger` | string | ai | agent | automation | test | | `status` | string | ok | error | timeout | | `response_status` | number | Código HTTP | | `duration_ms` | number | Tiempo de respuesta | connector.called: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "connector.called", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "connector_id": "conn_saldo", "conversation_id": "cv_1832", "trigger": "ai", "status": "open", "response_status": 200, "duration_ms": 212 } } ``` ## connector.failed Un conector devolvió error, venció el tiempo de espera o no pudo mapear la respuesta. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `connector_id` | string | ID del objeto | | `conversation_id` | string | Conversación afectada o null | | `status` | string | error | timeout | | `response_status` | number | Código HTTP o null | | `error` | string | Detalle del error | | `attempts` | number | Intentos hechos | connector.failed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "connector.failed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "connector_id": "conn_saldo", "conversation_id": "cv_1832", "status": "open", "response_status": 200, "error": "connect ETIMEDOUT", "attempts": 5 } } ``` ## connector.updated Se creó, editó o pausó un conector HTTP. Disponible desde 2026-09-02. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `connector_id` | string | ID del objeto | | `name` | string | Nombre del conector | | `action` | string | created | updated | paused | resumed | deleted | | `updated_by` | string | Quién lo hizo | connector.updated: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "connector.updated", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "connector_id": "conn_saldo", "name": "Juan Pérez", "action": "created", "updated_by": "mem_camila" } } ``` ## write_action.proposed Tinkay propuso escribir algo en tu sistema y quedó esperando confirmación. Disponible desde 2026-09-02. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `run_id` | string | ID del objeto | | `action_id` | string | ID de la acción | | `conversation_id` | string | Conversación que la originó | | `confirmation` | string | customer | agent | | `params` | string | Datos con los que se ejecutaría | | `idempotency_key` | string | Clave que evita duplicados | | `preview` | string | Texto que vio la persona antes de confirmar | write_action.proposed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "write_action.proposed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "run_id": "obj_8f1c2a", "action_id": "act_subscription", "conversation_id": "cv_1832", "confirmation": "customer", "params": "Datos con los que se ejecutaría", "idempotency_key": "Clave que evita duplicados", "preview": "Texto que vio la persona antes de confirmar" } } ``` ## write_action.confirmed Una persona autorizó la escritura: recién ahí sale el pedido a tu sistema. Disponible desde 2026-09-02. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `run_id` | string | ID del objeto | | `action_id` | string | ID de la acción | | `approved_by` | string | ID del agente, o 'customer' si la confirmó el cliente en el chat | | `approval_kind` | string | customer | agent | | `waited_seconds` | number | Cuánto estuvo esperando | write_action.confirmed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "write_action.confirmed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "run_id": "obj_8f1c2a", "action_id": "act_subscription", "approved_by": "mem_camila", "approval_kind": "customer", "waited_seconds": 120 } } ``` ## write_action.rejected Alguien rechazó la escritura y nunca salió ningún pedido. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `run_id` | string | ID del objeto | | `action_id` | string | ID de la acción | | `rejected_by` | string | ID del agente, o 'customer' | | `reason` | string | Motivo escrito por quien la rechazó | | `conversation_id` | string | Conversación afectada | write_action.rejected: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "write_action.rejected", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "run_id": "obj_8f1c2a", "action_id": "act_subscription", "rejected_by": "mem_camila", "reason": "low_confidence", "conversation_id": "cv_1832" } } ``` ## write_action.executed Se escribió en tu sistema: se reservó un turno, se cambió un pedido, se emitió una nota de crédito. Disponible desde 2026-09-02. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `run_id` | string | ID del objeto | | `action_id` | string | ID de la acción | | `conversation_id` | string | Conversación que la originó | | `trigger` | string | ai | agent | automation | test | | `response_status` | number | Código HTTP | | `duration_ms` | number | Tiempo de respuesta | | `idempotency_key` | string | Clave enviada | | `deduped` | string | true si tu sistema reconoció la clave y no volvió a escribir | | `result` | string | Campos mapeados de la respuesta | write_action.executed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "write_action.executed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "run_id": "obj_8f1c2a", "action_id": "act_subscription", "conversation_id": "cv_1832", "trigger": "ai", "response_status": 200, "duration_ms": 212, "idempotency_key": "Clave enviada", "deduped": "true si tu sistema reconoció la clave y no volvió a escribir", "result": "Campos mapeados de la respuesta" } } ``` ## write_action.failed Una escritura devolvió error o venció el tiempo de espera, y la conversación se derivó a una persona. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `run_id` | string | ID del objeto | | `action_id` | string | ID de la acción | | `status` | string | error | timeout | | `response_status` | number | Código HTTP o null | | `error` | string | Detalle del error | | `retriable` | string | true si se puede reintentar con la misma clave | | `conversation_id` | string | Conversación afectada | write_action.failed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "write_action.failed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "run_id": "obj_8f1c2a", "action_id": "act_subscription", "status": "open", "response_status": 200, "error": "connect ETIMEDOUT", "retriable": "true si se puede reintentar con la misma clave", "conversation_id": "cv_1832" } } ``` ## write_action.blocked Una protección frenó la escritura antes de que saliera: límites, acción apagada o datos faltantes. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `run_id` | string | ID del objeto | | `action_id` | string | ID de la acción | | `reason` | string | rate_limit_conversation | rate_limit_day | workspace_cap | action_disabled | writes_disabled | missing_params | | `detail` | string | Explicación en texto | | `conversation_id` | string | Conversación afectada | write_action.blocked: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "write_action.blocked", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "run_id": "obj_8f1c2a", "action_id": "act_subscription", "reason": "low_confidence", "detail": "Explicación en texto", "conversation_id": "cv_1832" } } ``` ## write_action.reverted Se deshizo una escritura ejecutando la acción inversa declarada. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `run_id` | string | Ejecución que se revirtió | | `undo_run_id` | string | Ejecución que la deshizo | | `action_id` | string | Acción original | | `undo_action_id` | string | Acción inversa | | `reverted_by` | string | Quién la deshizo | write_action.reverted: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "write_action.reverted", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "run_id": "obj_8f1c2a", "undo_run_id": "obj_8f1c2a", "action_id": "act_subscription", "undo_action_id": "obj_8f1c2a", "reverted_by": "mem_camila" } } ``` ## write_action.updated Se creó, editó o pausó una acción de escritura, o cambió su nivel de confirmación. Disponible desde 2026-09-02. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `action_id` | string | ID del objeto | | `name` | string | Nombre de la acción | | `action` | string | created | updated | paused | resumed | deleted | | `confirmation` | string | auto | customer | agent | | `updated_by` | string | Quién lo hizo | write_action.updated: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "write_action.updated", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "action_id": "act_subscription", "name": "Juan Pérez", "action": "created", "confirmation": "auto", "updated_by": "mem_camila" } } ``` ## sync.completed Terminó una sincronización de tu base de clientes hacia Contactos. Disponible desde 2026-09-02. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `job_id` | string | ID del objeto | | `source` | string | Origen sincronizado | | `inserted` | number | Contactos creados | | `updated` | number | Contactos actualizados | | `skipped` | number | Filas salteadas | | `duration_ms` | number | Duración total | sync.completed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "sync.completed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "job_id": "job_clientes", "source": "memory", "inserted": 412, "updated": 1268, "skipped": 9, "duration_ms": 212 } } ``` ## sync.failed Falló una sincronización de clientes y no se aplicó ningún cambio. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `job_id` | string | ID del objeto | | `source` | string | Origen | | `error` | string | Detalle del error | | `rows_read` | number | Filas leídas antes de cortar | | `retry_at` | string | Fecha ISO 8601 | sync.failed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "sync.failed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "job_id": "job_clientes", "source": "memory", "error": "connect ETIMEDOUT", "rows_read": 1689, "retry_at": "2026-09-02T14:03:11.000Z" } } ``` ## database.connected Se habilitó el acceso directo por réplica de solo lectura o agente de túnel. Disponible desde 2026-09-02. Volumen bajo. Requiere plan Experto. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `tunnel_id` | string | ID del objeto | | `mode` | string | read_replica | tunnel_agent | | `engine` | string | postgres | mysql | sqlserver | mongodb | | `tables` | array | Tablas habilitadas | | `connected_by` | string | Quién lo habilitó | database.connected: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "database.connected", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "tunnel_id": "tun_replica", "mode": "read_replica", "engine": "postgres", "tables": [ "clientes", "facturas", "pedidos" ], "connected_by": "mem_camila" } } ``` ## database.query_blocked Se bloqueó una consulta contra tu base por salir del alcance permitido. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. Requiere plan Experto. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `tunnel_id` | string | ID del objeto | | `reason` | string | write_attempt | table_not_allowed | row_limit | timeout | | `table` | string | Tabla involucrada | | `conversation_id` | string | Conversación que la originó o null | database.query_blocked: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "database.query_blocked", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "tunnel_id": "tun_replica", "reason": "low_confidence", "table": "clientes", "conversation_id": "cv_1832" } } ``` --- URL: https://tinkay.app/docs/eventos-team # Eventos de equipo Miembros, equipos, roles, disponibilidad y permisos. Miembros, equipos, roles, disponibilidad y permisos. Son 9 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## member.invited Se invitó a alguien al workspace. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `email` | string | Email invitado | | `role` | string | Rol asignado | | `invited_by` | string | Quién invitó | member.invited: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "member.invited", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "email": "juan@loomi.com.ar", "role": "agent", "invited_by": "mem_camila" } } ``` ## member.joined La persona aceptó la invitación. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `email` | string | Email | | `role` | string | Rol | member.joined: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "member.joined", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "email": "juan@loomi.com.ar", "role": "agent" } } ``` ## member.role_changed Cambió el rol de un miembro. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `role` | string | Rol nuevo | | `previous_role` | string | Rol anterior | | `changed_by` | string | Quién lo cambió | member.role_changed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "member.role_changed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "role": "agent", "previous_role": "manager", "changed_by": "mem_camila" } } ``` ## member.removed Se quitó a alguien del workspace. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `removed_by` | string | Quién lo quitó | member.removed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "member.removed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "removed_by": "mem_camila" } } ``` ## member.availability_changed Un agente cambió su disponibilidad. Disponible desde 2026-05-14. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `status` | string | active | away | offline | | `changed_at` | string | Fecha ISO 8601 | member.availability_changed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "member.availability_changed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "status": "open", "changed_at": "2026-09-02T14:03:11.000Z" } } ``` ## team.created Se creó un equipo. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `name` | string | Nombre del equipo | team.created: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "team.created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "name": "Juan Pérez" } } ``` ## team.member_added Se sumó una persona a un equipo. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del equipo | | `member_id` | string | ID del miembro | | `team_role` | string | lead | trainer | agent | team.member_added: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "team.member_added", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "member_id": "mem_tomas", "team_role": "trainer" } } ``` ## team.member_removed Se quitó una persona de un equipo. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del equipo | | `member_id` | string | ID del miembro | team.member_removed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "team.member_removed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "member_id": "mem_tomas" } } ``` ## team.permissions_changed El líder cambió los permisos del equipo, por ejemplo quién puede entrenar la IA. Disponible desde 2026-09-02. Volumen bajo. Se puede usar como disparador de automatizaciones. Requiere plan Avanzado. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del equipo | | `permissions` | object | Permisos resultantes | | `changed_by` | string | Quién los cambió | team.permissions_changed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "team.permissions_changed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "permissions": { "canTrainAi": [ "mem_camila" ], "canPublishArticles": [ "mem_camila", "mem_tomas" ] }, "changed_by": "mem_camila" } } ``` --- URL: https://tinkay.app/docs/eventos-automation # Eventos de automatizaciones Ejecuciones de tus flujos, con sus pasos y errores. Ejecuciones de tus flujos, con sus pasos y errores. Son 6 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## automation.enabled Se activó una automatización. Disponible desde 2026-02-01. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `name` | string | Nombre | | `enabled_by` | string | Quién la activó | automation.enabled: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "automation.enabled", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "name": "Juan Pérez", "enabled_by": "mem_camila" } } ``` ## automation.disabled Se pausó una automatización. Disponible desde 2026-02-01. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `name` | string | Nombre | | `disabled_by` | string | Quién la pausó | automation.disabled: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "automation.disabled", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "name": "Juan Pérez", "disabled_by": "mem_camila" } } ``` ## automation.run_started Empezó una ejecución. Disponible desde 2026-02-01. Volumen alto. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID de la ejecución | | `automation_id` | string | ID de la automatización | | `trigger_event` | string | Evento que la disparó | | `context` | object | Objeto que la originó | automation.run_started: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "automation.run_started", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "automation_id": "auto_devoluciones", "trigger_event": "message.received", "context": { "conversation_id": "cv_1832" } } } ``` ## automation.run_completed Terminó una ejecución con éxito. Disponible desde 2026-02-01. Volumen alto. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID de la ejecución | | `automation_id` | string | ID de la automatización | | `steps` | number | Pasos ejecutados | | `duration_ms` | number | Duración | automation.run_completed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "automation.run_completed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "automation_id": "auto_devoluciones", "steps": 4, "duration_ms": 212 } } ``` ## automation.run_failed Falló un paso de la automatización. Disponible desde 2026-02-01. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID de la ejecución | | `automation_id` | string | ID de la automatización | | `step_id` | string | Paso que falló | | `error` | string | Detalle | automation.run_failed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "automation.run_failed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "automation_id": "auto_devoluciones", "step_id": "n3", "error": "connect ETIMEDOUT" } } ``` ## automation.goal_reached Se cumplió el objetivo definido para la automatización. Disponible desde 2026-09-02. Volumen medio. Requiere plan Avanzado. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `automation_id` | string | ID de la automatización | | `goal` | string | Objetivo | | `conversation_id` | string | Conversación asociada | automation.goal_reached: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "automation.goal_reached", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "automation_id": "auto_devoluciones", "goal": "conversation_resolved", "conversation_id": "cv_1832" } } ``` --- URL: https://tinkay.app/docs/eventos-channel # Eventos de canales Canales y apps conectadas: altas, bajas y fallas de token. Canales y apps conectadas: altas, bajas y fallas de token. Son 3 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## channel.connected Se conectó un canal o una app. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `provider` | string | whatsapp | instagram | gmail | outlook | shopify | tiendanube | slack | stripe | | `connected_by` | string | Quién lo conectó | channel.connected: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "channel.connected", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "provider": "openai", "connected_by": "mem_camila" } } ``` ## channel.disconnected Se desconectó un canal. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `provider` | string | Proveedor | | `reason` | string | manual | token_expired | revoked | channel.disconnected: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "channel.disconnected", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "provider": "openai", "reason": "low_confidence" } } ``` ## channel.error Un canal dejó de funcionar, por ejemplo por un token vencido. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `provider` | string | Proveedor | | `error` | string | Detalle | | `since` | string | Fecha ISO 8601 | channel.error: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "channel.error", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "provider": "openai", "error": "connect ETIMEDOUT", "since": "2026-09-02T14:03:11.000Z" } } ``` --- URL: https://tinkay.app/docs/eventos-widget # Eventos de messenger Comportamiento del Messenger en el sitio del cliente. Comportamiento del Messenger en el sitio del cliente. Son 5 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## widget.opened Un visitante abrió el Messenger. Disponible desde 2026-02-05. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `visitor_id` | string | ID anónimo o del contacto | | `url` | string | Página donde lo abrió | | `referrer` | string | Referente | widget.opened: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "widget.opened", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "visitor_id": "obj_8f1c2a", "url": "https://tinkay.app/precios", "referrer": "Referente" } } ``` ## widget.closed El visitante cerró el Messenger. Disponible desde 2026-02-05. Volumen alto. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `visitor_id` | string | ID del visitante | | `session_seconds` | number | Duración de la sesión | widget.closed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "widget.closed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "visitor_id": "obj_8f1c2a", "session_seconds": 96 } } ``` ## widget.article_viewed El visitante leyó un artículo dentro del Messenger. Disponible desde 2026-02-05. Volumen alto. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `visitor_id` | string | ID del visitante | | `article_id` | string | Artículo | | `seconds` | number | Tiempo de lectura | widget.article_viewed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "widget.article_viewed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "visitor_id": "obj_8f1c2a", "article_id": "art_instalar_script", "seconds": 48 } } ``` ## widget.search_performed El visitante buscó en el centro de ayuda desde el Messenger. Disponible desde 2026-02-05. Volumen alto. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `visitor_id` | string | ID del visitante | | `query` | string | Texto buscado | | `results` | number | Cantidad de resultados | widget.search_performed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "widget.search_performed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "visitor_id": "obj_8f1c2a", "query": "instalar en dotnet", "results": 0 } } ``` ## widget.search_empty Una búsqueda no devolvió resultados: candidata a artículo nuevo. Disponible desde 2026-04-22. Volumen medio. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `visitor_id` | string | ID del visitante | | `query` | string | Texto buscado | widget.search_empty: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "widget.search_empty", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "visitor_id": "obj_8f1c2a", "query": "instalar en dotnet" } } ``` --- URL: https://tinkay.app/docs/eventos-billing # Eventos de facturación Suscripción, facturas y límites de consumo. Suscripción, facturas y límites de consumo. Son 7 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## subscription.created Se activó una suscripción. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `plan` | string | free | starter | pro | business, que en la interfaz se llaman Free, Esencial, Avanzado y Experto | | `seats` | number | Asientos, se factura por plaza | | `interval` | string | monthly | yearly | subscription.created: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "subscription.created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "plan": "pro", "seats": 15, "interval": "monthly" } } ``` ## subscription.updated Cambió el plan o la cantidad de asientos. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `plan` | string | Plan nuevo: free | starter | pro | business | | `previous_plan` | string | Plan anterior | | `seats` | number | Asientos | | `prorated_amount_ars` | number | Prorrateo en pesos argentinos | subscription.updated: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "subscription.updated", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "plan": "pro", "previous_plan": "starter", "seats": 15, "prorated_amount_ars": 33458 } } ``` ## subscription.canceled Se canceló la suscripción. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `ends_at` | string | Fecha ISO 8601 | | `reason` | string | Motivo declarado o null | subscription.canceled: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "subscription.canceled", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "ends_at": "2026-09-02T14:03:11.000Z", "reason": "low_confidence" } } ``` ## invoice.paid Se cobró una factura. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `number` | number | Número de factura | | `amount_ars` | number | Importe en pesos | | `period_start` | string | Fecha ISO 8601 | | `period_end` | string | Fecha ISO 8601 | invoice.paid: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "invoice.paid", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "number": 1041, "amount_ars": 79900, "period_start": "2026-09-02T14:03:11.000Z", "period_end": "2026-09-02T14:03:11.000Z" } } ``` ## invoice.payment_failed Falló el cobro. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `number` | number | Número de factura | | `amount_ars` | number | Importe en pesos | | `failure_reason` | string | Motivo del rechazo | | `retry_at` | string | Fecha ISO 8601 | invoice.payment_failed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "invoice.payment_failed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "number": 1041, "amount_ars": 79900, "failure_reason": "card_declined", "retry_at": "2026-09-02T14:03:11.000Z" } } ``` ## usage.limit_approaching El consumo llegó al 80% del límite del plan. Disponible desde 2026-03-05. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `metric` | string | ai_resolutions | contacts | storage | seats | | `used` | number | Consumido | | `limit` | number | Límite del plan | usage.limit_approaching: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "usage.limit_approaching", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "metric": "ai_resolutions", "used": 12400, "limit": 15000 } } ``` ## usage.limit_reached Se alcanzó un límite del plan. Disponible desde 2026-03-05. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `metric` | string | Métrica | | `used` | number | Consumido | | `limit` | number | Límite | | `overage_policy` | string | bill | memory_only | byo_key | stop | usage.limit_reached: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "usage.limit_reached", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "metric": "ai_resolutions", "used": 12400, "limit": 15000, "overage_policy": "bill" } } ``` --- URL: https://tinkay.app/docs/eventos-security # Eventos de seguridad API keys, webhooks, accesos y sesiones. API keys, webhooks, accesos y sesiones. Son 8 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## api_key.created Se creó una API key. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `name` | string | Nombre | | `scopes` | array | Permisos | | `created_by` | string | Quién la creó | api_key.created: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "api_key.created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "name": "Juan Pérez", "scopes": [ "contacts:read", "tickets:write" ], "created_by": "mem_camila" } } ``` ## api_key.revoked Se revocó una API key. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `revoked_by` | string | Quién la revocó | api_key.revoked: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "api_key.revoked", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "revoked_by": "mem_camila" } } ``` ## webhook.created Se registró un webhook. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `url` | string | Endpoint | | `events` | array | Eventos suscritos | webhook.created: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "webhook.created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "url": "https://tinkay.app/precios", "events": [ "conversation.created", "ai.handoff" ] } } ``` ## webhook.delivery_failed Una entrega de webhook falló después de todos los reintentos. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID de la entrega | | `webhook_id` | string | ID del webhook | | `event` | string | Evento | | `status` | string | Código HTTP | | `attempts` | number | Intentos realizados | webhook.delivery_failed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "webhook.delivery_failed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "webhook_id": "wh_1", "event": "Evento", "status": "open", "attempts": 5 } } ``` ## webhook.disabled Se desactivó un webhook por fallas repetidas. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del objeto | | `url` | string | Endpoint | | `consecutive_failures` | number | Fallas seguidas | webhook.disabled: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "webhook.disabled", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "url": "https://tinkay.app/precios", "consecutive_failures": 12 } } ``` ## login.succeeded Alguien inició sesión. Disponible desde 2026-01-15. Volumen medio. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `member_id` | string | ID del miembro | | `ip` | string | Dirección IP | | `user_agent` | string | Navegador | | `method` | string | password | google | sso | login.succeeded: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "login.succeeded", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "member_id": "mem_tomas", "ip": "181.46.12.8", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "method": "password" } } ``` ## login.failed Intento de acceso fallido. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `email` | string | Email usado | | `ip` | string | Dirección IP | | `reason` | string | bad_password | unknown_user | mfa_failed | login.failed: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "login.failed", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "email": "juan@loomi.com.ar", "ip": "181.46.12.8", "reason": "low_confidence" } } ``` ## session.revoked Se cerró una sesión activa. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `session_id` | string | ID del objeto | | `member_id` | string | ID del miembro | | `revoked_by` | string | Quién la cerró | session.revoked: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "session.revoked", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "session_id": "ses_2", "member_id": "mem_tomas", "revoked_by": "mem_camila" } } ``` --- URL: https://tinkay.app/docs/eventos-workspace # Eventos de workspace Configuración general, dominios y publicaciones. Configuración general, dominios y publicaciones. Son 4 eventos. Todos llegan con la [envoltura estándar](/docs/eventos) y su contenido propio en `data`. ## domain.verified Se verificó un dominio permitido o el del centro de ayuda. Disponible desde 2026-02-05. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `domain` | string | Dominio | | `kind` | string | widget | help_center | domain.verified: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "domain.verified", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "domain": "tinkay.app", "kind": "first_response" } } ``` ## workspace.updated Cambió la configuración general del workspace. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `id` | string | ID del workspace | | `changes` | object | Campos modificados | | `updated_by` | string | Quién lo cambió | workspace.updated: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "workspace.updated", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "changes": { "status": "in_progress" }, "updated_by": "mem_camila" } } ``` ## messenger.published Se publicó una versión del Messenger. Disponible desde 2026-01-15. Volumen bajo. Se puede usar como disparador de automatizaciones. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `published_by` | string | Quién publicó | | `version` | number | Número de versión | messenger.published: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "messenger.published", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "published_by": "mem_camila", "version": 12 } } ``` ## help_center.published Se publicó el centro de ayuda. Disponible desde 2026-01-15. Volumen bajo. **Campos de data** | Campo | Tipo | Descripción | | --- | --- | --- | | `published_by` | string | Quién publicó | | `domain` | string | Dominio público | help_center.published: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "help_center.published", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "published_by": "mem_camila", "domain": "tinkay.app" } } ``` # Webhooks --- URL: https://tinkay.app/docs/webhooks # Webhooks Recibir eventos firmados en tu servidor, con reintentos y verificación HMAC. Tinkay envía un `POST` con `Content-Type: application/json` cada vez que ocurre un evento al que te suscribiste. Hay **126 eventos** disponibles: mirá la [referencia completa](/docs/eventos). Tu endpoint tiene que responder un 2xx dentro de 10 segundos. Todo lo que tarde más se considera una falla y entra en la cola de reintentos. > Respondé 200 apenas recibís el evento y hacé el trabajo pesado en una cola. Es la diferencia entre una integración estable y una que se cae en los picos. ## Crear el webhook 1. Entrá a **Configuración, Desarrolladores, Webhooks** y tocá `Nuevo webhook`. 2. Pegá la URL HTTPS de tu endpoint. 3. Elegí los eventos. Podés seleccionar un grupo entero o suscribirte a todos con `*`. 4. Guardá y copiá el **signing secret** (`whsec_...`). Se muestra una sola vez. 5. Tocá `Enviar prueba` para confirmar que tu endpoint responde. ## Formato de la entrega Cada entrega trae los headers de identificación y firma, y el evento completo en el cuerpo. **Headers de la entrega** | Campo | Tipo | Descripción | | --- | --- | --- | | `X-Tinkay-Event` | string | Nombre del evento. | | `X-Tinkay-Delivery` | string | Identificador de la entrega. Cambia en cada reintento. | | `X-Tinkay-Attempt` | number | Número de intento, empezando en 1. | | `X-Tinkay-Timestamp` | number | Epoch en segundos usado para firmar. | | `X-Tinkay-Signature` | string | `sha256=` seguido del HMAC en hexadecimal. | Headers: ```text POST /hooks/tinkay HTTP/1.1 Content-Type: application/json User-Agent: Tinkay-Webhooks/1.0 X-Tinkay-Event: conversation.created X-Tinkay-Delivery: dlv_7c1a93f0 X-Tinkay-Attempt: 1 X-Tinkay-Timestamp: 1772668800 X-Tinkay-Signature: sha256=9f2ab7c41d0e83ba5c1904e6f8b2d7a3c5e1f0d9b8a7c6e5d4f3a2b1c0d9e8f7 ``` Cuerpo: ```json { "id": "evt_8f1c2a9d4b", "object": "event", "name": "conversation.created", "api_version": "2026-09-01", "workspace_id": "ws_tinkay", "occurred_at": "2026-09-02T14:03:11.000Z", "data": { "id": "cv_1832", "contact_id": "ct_juan", "channel": "messenger" } } ``` ## Un endpoint mínimo Este ejemplo valida la firma, responde rápido y encola el trabajo real. Node (Express) · webhooks.js: ```js import express from "express"; import crypto from "node:crypto"; const app = express(); const SECRET = process.env.TINKAY_WEBHOOK_SECRET; // El cuerpo crudo es obligatorio: JSON.stringify cambia bytes y rompe la firma. app.post("/hooks/tinkay", express.raw({ type: "application/json" }), (req, res) => { if (!verify(req)) return res.status(400).send("invalid signature"); const event = JSON.parse(req.body.toString("utf8")); res.sendStatus(200); // responder primero queue.push(event); // procesar después }); function verify(req) { const timestamp = req.get("X-Tinkay-Timestamp"); const signature = (req.get("X-Tinkay-Signature") || "").replace("sha256=", ""); if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const expected = crypto .createHmac("sha256", SECRET) .update(`${timestamp}.${req.body.toString("utf8")}`) .digest("hex"); const a = Buffer.from(expected, "hex"); const b = Buffer.from(signature, "hex"); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` ## Qué eventos elegir De los 126 eventos, 80 sirven además como disparadores de automatizaciones dentro de Tinkay. Si lo que querés hacer es asignar, etiquetar o responder, conviene una automatización antes que un webhook. Suscribite solo a lo que vas a procesar: cada evento extra es tráfico y latencia en tu servidor. - **Sincronizar un CRM**: `contact.created`, `contact.updated`, `contact.merged`, `contact.deleted`. - **Alertas internas**: `ai.handoff`, `conversation.sla_breached`, `channel.error`, `invoice.payment_failed`. - **Data warehouse**: `conversation.closed`, `conversation.rated`, `ai.replied`, `ticket.resolved`. - **Cumplimiento**: `login.failed`, `api_key.revoked`, `member.role_changed`, `team.permissions_changed`. ## Probar en desarrollo Exponé tu servidor local con un túnel y usá `Enviar prueba` en Configuración, Desarrolladores. Podés elegir cualquier evento del catálogo y recibirlo con datos realistas. ```bash ngrok http 3000 # usá la URL https que te da como destino del webhook ``` --- URL: https://tinkay.app/docs/webhooks-firma # Verificar la firma HMAC SHA-256 con el signing secret, en Node, C#, Python, PHP y Go. La firma es un **HMAC SHA-256** de `{timestamp}.{cuerpo crudo}` usando el signing secret del webhook (`whsec_...`), que no es la API key. Verificá siempre sobre el cuerpo **crudo**. Si tu framework parsea el JSON y lo vuelve a serializar, los bytes cambian y la firma no coincide. > Compará con una función de tiempo constante y rechazá timestamps de más de cinco minutos para evitar reenvíos. ## Implementaciones Node: ```js import crypto from "node:crypto"; export function verifyTinkay(rawBody, headers, secret) { const timestamp = headers["x-tinkay-timestamp"]; const received = String(headers["x-tinkay-signature"] || "").replace("sha256=", ""); if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex"); const a = Buffer.from(expected, "hex"); const b = Buffer.from(received, "hex"); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` C# · TinkayWebhooks.cs: ```csharp using System.Security.Cryptography; using System.Text; public static class TinkayWebhooks { public static bool Verify(string rawBody, string timestamp, string signature, string secret) { var unixNow = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (Math.Abs(unixNow - long.Parse(timestamp)) > 300) return false; using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}")); var expected = Convert.ToHexString(hash).ToLowerInvariant(); var received = signature.Replace("sha256=", ""); return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(received)); } } ``` Python: ```python import hashlib, hmac, time def verify_tinkay(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool: if abs(time.time() - int(timestamp)) > 300: return False expected = hmac.new( secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, signature.replace("sha256=", "")) ``` PHP: ```php 300) { return false; } $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret); return hash_equals($expected, str_replace('sha256=', '', $signature)); } ``` Go · tinkay.go: ```text package tinkay import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "math" "strconv" "strings" "time" ) func Verify(rawBody []byte, timestamp, signature, secret string) bool { ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil || math.Abs(float64(time.Now().Unix()-ts)) > 300 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp + ".")) mac.Write(rawBody) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(strings.TrimPrefix(signature, "sha256="))) } ``` ## Rotar el secreto Desde el detalle del webhook podés rotar el signing secret. Durante 24 horas aceptamos firmas con el secreto anterior, así podés desplegar sin ventana de error. Guardá el secreto en tu gestor de variables de entorno, nunca en el repositorio. --- URL: https://tinkay.app/docs/webhooks-fiabilidad # Reintentos, orden y versionado Cómo entregamos, qué garantizamos y cómo escribir un consumidor a prueba de fallas. ## Reintentos Si tu endpoint no responde 2xx en 10 segundos, reintentamos con espera exponencial durante 24 horas: 30 segundos, 2 minutos, 10 minutos, 1 hora, 6 horas y 24 horas. Después de 20 entregas fallidas seguidas, o 5 días de fallas continuas, pausamos el webhook y te avisamos por email y con el evento `webhook.disabled`. Reactivarlo desde Configuración, Desarrolladores no pierde el historial. **Códigos de respuesta** | Campo | Tipo | Descripción | | --- | --- | --- | | `2xx` | éxito | La entrega se marca como completada. | | `410` | baja definitiva | Damos de baja el webhook sin reintentar. | | `429` | reintento | Respetamos el header `Retry-After` si viene. | | `otros 4xx y 5xx` | reintento | Entra en la cola de reintentos. | ## Idempotencia Un mismo evento puede llegar más de una vez: garantizamos entrega **al menos una vez**, no exactamente una vez. Guardá el `id` del evento y descartá los repetidos. Es la forma más simple de volverte inmune a los reintentos. Node: ```js async function handle(event) { // Insert que falla si el id ya existe: el evento repetido se descarta solo. const inserted = await db.processedEvents.insertIfAbsent(event.id); if (!inserted) return; await process(event); } ``` Python: ```python def handle(event): if not processed_events.insert_if_absent(event["id"]): return # ya procesado process(event) ``` ## Orden de entrega No garantizamos el orden. Un `conversation.closed` puede llegar antes que un `message.created` de la misma conversación. Usá `occurred_at` para ordenar y, si tu lógica depende del estado final, leé el objeto por API antes de escribir. > Nunca reconstruyas el estado sumando eventos en orden de llegada. Reconciliá contra la API cuando el orden importa. ## Versionado Cada entrega incluye `api_version`. La actual es `2026-09-01`. Agregar un evento nuevo o un campo nuevo dentro de `data` no se considera un cambio que rompa. Tu consumidor debe ignorar lo que no conoce. Los cambios que rompen se publican como una versión nueva, con seis meses de convivencia y aviso previo. ## Direcciones de salida Si tu firewall filtra por IP, permití estas direcciones. Avisamos con 30 días de anticipación antes de cambiarlas. ```text 52.14.108.0/24 35.171.44.0/24 18.229.201.0/24 ``` ## Historial de entregas Guardamos cada intento durante 30 días, con el cuerpo enviado, la respuesta de tu servidor, el código, la duración y el número de intento. Está en Configuración, Desarrolladores, Webhooks, y también por API. Sirve para dos cosas: entender por qué falló algo sin pedirnos logs, y recuperar eventos que tu sistema perdió durante una caída sin tener que reprocesar toda la base. ### `GET /v1/webhooks/{id}` Devuelve el endpoint con su tasa de éxito, latencia p95 y fallos consecutivos. Scope: `webhooks:read` ### `GET /v1/webhooks/{id}/deliveries` Lista los intentos de entrega, del más nuevo al más viejo. Scope: `webhooks:read` | Campo | Tipo | Descripción | | --- | --- | --- | | `status` | string | success, failed o pending. | | `event` | string | Filtra por nombre de evento. | | `limit` | number | Máximo 100. | ### `PATCH /v1/webhooks/{id}` Cambia la URL, los eventos o pausa el endpoint. Scope: `webhooks:write` ### `DELETE /v1/webhooks/{id}` Da de baja el endpoint y su historial. Scope: `webhooks:write` Entregas fallidas de las últimas horas: ```bash curl "https://api.tinkay.app/v1/webhooks/wh_1/deliveries?status=failed&limit=50" -H "Authorization: Bearer dk_live_xxx" ``` Reprocesar sin pedir reenvío · recuperar.js: ```js // Cada entrega trae el cuerpo original: podés reprocesarlo vos mismo, // sin esperar a que reintentemos ni pedirnos un replay. const res = await fetch("https://api.tinkay.app/v1/webhooks/wh_1/deliveries?status=failed&limit=100", { headers: { Authorization: `Bearer ${process.env.TINKAY_API_KEY}` }, }); const { data } = await res.json(); for (const delivery of data) { const event = JSON.parse(delivery.request_body); // El id del evento no cambia entre reintentos: úsalo como clave de idempotencia. await procesar(event.id, event.name, event.data); } ``` ## Buenas prácticas - Respondé 200 antes de procesar. Encolá y trabajá asincrónicamente. - Verificá la firma en todas las entregas, también en producción. - Guardá el cuerpo crudo unos días: sirve para reprocesar sin pedir reenvíos. - Monitoreá el registro de entregas en Configuración, Desarrolladores: ahí ves código, duración e intento de cada una. - Usá `Reenviar` para reprocesar una entrega puntual después de arreglar un bug. # Tinkay AI --- URL: https://tinkay.app/docs/ai-acciones # Acciones de Tinkay AI Endpoints tuyos que la IA llama durante la conversación para responder con datos reales. Una acción es un endpoint tuyo que Tinkay AI puede llamar mientras conversa. Sirve para responder cosas que no están en la base de conocimiento porque viven en tu base de datos: el estado de una suscripción, un envío, un turno. La IA decide cuándo llamarla a partir del nombre y la descripción que le des, así que escribilas pensando en cuándo querés que se use. ## Configurarla 1. Entrá a **Tinkay AI, Acciones** y tocá `Nueva acción`. 2. Poné un nombre claro (`Consultar suscripción`) y una descripción que explique cuándo usarla. 3. Elegí el método y la URL. Podés usar `{parametro}` en la ruta. 4. Definí los parámetros con nombre, descripción y si son obligatorios. 5. Activá la acción y probala en el **Test Lab**. ## Contrato de la llamada Tinkay llama a tu endpoint con los parámetros que la IA extrajo de la conversación. Request: ```text GET https://api.tuempresa.com/tinkay/subscription/kipu Authorization: Bearer X-Tinkay-Workspace: ws_tinkay X-Tinkay-Conversation: cv_1790 ``` Response esperada: ```json { "plan": "Business", "seats": 12, "next_invoice_at": "2026-10-01", "amount_usd": 399, "status": "active" } ``` > Devolvé JSON plano con claves descriptivas. La IA redacta la respuesta a partir de esos campos, así que nombres claros dan mejores respuestas. ## Implementar el endpoint C# (.NET 8): ```csharp app.MapGet("/tinkay/subscription/{workspace}", (string workspace, HttpRequest req, IConfiguration cfg) => { var token = req.Headers.Authorization.ToString().Replace("Bearer ", ""); if (token != cfg["Tinkay:ActionSecret"]) return Results.Unauthorized(); var sub = db.Subscriptions.FirstOrDefault(s => s.WorkspaceSlug == workspace); if (sub is null) return Results.NotFound(new { message = "No encontramos esa cuenta." }); return Results.Ok(new { plan = sub.Plan, seats = sub.Seats, next_invoice_at = sub.NextInvoiceAt.ToString("yyyy-MM-dd"), amount_usd = sub.AmountUsd, status = sub.Status }); }); ``` Node (Express): ```js app.get("/tinkay/subscription/:workspace", async (req, res) => { if (req.get("Authorization") !== `Bearer ${process.env.TINKAY_ACTION_SECRET}`) { return res.sendStatus(401); } const sub = await db.subscriptions.findByWorkspace(req.params.workspace); if (!sub) return res.status(404).json({ message: "No encontramos esa cuenta." }); res.json({ plan: sub.plan, seats: sub.seats, next_invoice_at: sub.nextInvoiceAt, amount_usd: sub.amountUsd, status: sub.status, }); }); ``` Python (FastAPI): ```python @app.get("/tinkay/subscription/{workspace}") def subscription(workspace: str, authorization: str = Header(default="")): if authorization != f"Bearer {os.environ['TINKAY_ACTION_SECRET']}": raise HTTPException(status_code=401) sub = db.subscription_for(workspace) if sub is None: raise HTTPException(status_code=404, detail="No encontramos esa cuenta.") return { "plan": sub.plan, "seats": sub.seats, "next_invoice_at": sub.next_invoice_at.isoformat(), "amount_usd": sub.amount_usd, "status": sub.status, } ``` ## Qué pasa si falla - **Timeout de 5 segundos**: si tardás más, Tinkay AI responde con lo que sabe y deriva a una persona. - **Error 4xx o 5xx**: la IA no inventa el dato; avisa que no pudo consultarlo y deriva. - **404 con `message`**: la IA usa ese mensaje para responder con naturalidad. > No devuelvas datos sensibles que no quieras que la IA le muestre a quien está del otro lado de la conversación. --- URL: https://tinkay.app/docs/ai-fuentes # Fuentes de conocimiento Qué usa Tinkay AI para responder y cómo mantenerlo actualizado. Tinkay AI responde solo con lo que tiene indexado. Nunca completa con conocimiento general del modelo: si algo no está, lo dice y deriva. ## Tipos de fuente - **Sitio web**: rastreamos tus páginas públicas y las reindexamos periódicamente. - **Centro de ayuda**: se sincroniza automáticamente al publicar un artículo. - **Archivos**: PDF, DOCX y TXT que subís desde el panel. - **Notion y Google Drive**: elegís las páginas o carpetas a importar. - **API**: podés crear artículos por API y quedan indexados como cualquier otro. ## Crear conocimiento por API Si tu documentación vive en tu repositorio, publicala en Tinkay en tu pipeline de CI. ```bash curl "https://api.tinkay.app/v1/articles?status=published" \ -H "Authorization: Bearer dk_live_xxx" ``` > La creación de artículos por API está en vista previa. Escribinos si la necesitás y te habilitamos el scope `articles:write`. ## Buenas prácticas - Un tema por artículo: la IA cita mejor documentos cortos y específicos. - Poné la respuesta en el primer párrafo y después el detalle. - Revisá **Brechas de conocimiento** en el panel: ahí aparece lo que la gente pregunta y la IA no puede responder. --- URL: https://tinkay.app/docs/ai-escalacion # Escalación Cuándo la IA deja de responder y pasa la conversación a una persona. ## Reglas de derivación - **Umbral de confianza**: si la confianza de la respuesta queda por debajo del umbral (70% por defecto), deriva. - **Temas sensibles**: una lista de temas donde la IA nunca responde sola (cobros, datos personales, reclamos legales). - **Contactos VIP**: opcionalmente, los contactos marcados como VIP van siempre a una persona. - **Fuera de horario**: podés elegir que la IA responda igual, que solo tome el mensaje o que derive. ## Reaccionar a una derivación El evento `ai.handoff` te permite avisar a tu equipo por Slack, crear una tarea o disparar una llamada. ```js // En tu endpoint de webhooks if (event.name === "ai.handoff") { await slack.chat.postMessage({ channel: "#soporte", text: `Tinkay AI derivó una conversación (${event.payload.reason}). Ver: https://app.tinkay.app/tinkay/inbox/${event.payload.conversation_id}`, }); } ``` ## Medir la calidad En **Analytics, IA** ves la tasa de resolución, la tasa de derivación y los motivos. Si sube la derivación por confianza baja, casi siempre falta documentación: revisá las brechas de conocimiento. # SDKs --- URL: https://tinkay.app/docs/sdk-csharp # SDK de C# Cliente tipado para .NET 8 sobre HttpClient. El paquete está en vista previa. Mientras tanto, este cliente cubre lo esencial y se registra con `IHttpClientFactory`. ```bash dotnet add package Tinkay.Sdk --prerelease ``` ## Registro Program.cs: ```csharp builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri("https://api.tinkay.app/v1/"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", builder.Configuration["Tinkay:ApiKey"]); client.Timeout = TimeSpan.FromSeconds(15); }); ``` ## Cliente TinkayClient.cs: ```csharp using System.Net.Http.Json; public sealed class TinkayClient(HttpClient http) { public async Task CreateContactAsync(CreateContact input, CancellationToken ct = default) { var res = await http.PostAsJsonAsync("contacts", input, ct); res.EnsureSuccessStatusCode(); var body = await res.Content.ReadFromJsonAsync>(cancellationToken: ct); return body!.Data; } public async IAsyncEnumerable ListContactsAsync([EnumeratorCancellation] CancellationToken ct = default) { string? cursor = null; do { var url = cursor is null ? "contacts?limit=100" : $"contacts?limit=100&cursor={cursor}"; var page = await http.GetFromJsonAsync>(url, ct); foreach (var item in page!.Data) yield return item; cursor = page.NextCursor; } while (cursor is not null && !ct.IsCancellationRequested); } public async Task CreateTicketAsync(CreateTicket input, CancellationToken ct = default) { var res = await http.PostAsJsonAsync("tickets", input, ct); res.EnsureSuccessStatusCode(); var body = await res.Content.ReadFromJsonAsync>(cancellationToken: ct); return body!.Data; } } public record Envelope(T Data); public record Paged(IReadOnlyList Data, [property: JsonPropertyName("next_cursor")] string? NextCursor, [property: JsonPropertyName("has_more")] bool HasMore); public record CreateContact(string Name, string Email, string? Company = null); public record CreateTicket(string Subject, string Description, [property: JsonPropertyName("contact_id")] string ContactId, string Priority = "normal"); ``` ## Uso ```csharp public class SupportService(TinkayClient tinkay) { public async Task ReportIncidentAsync(User user, Incident incident) { var contact = await tinkay.CreateContactAsync(new(user.FullName, user.Email, user.Company)); await tinkay.CreateTicketAsync(new( Subject: $"Error {incident.Code} en {incident.Module}", Description: incident.Summary, ContactId: contact.Id, Priority: incident.IsBlocking ? "urgent" : "normal")); } } ``` --- URL: https://tinkay.app/docs/sdk-node # SDK de Node Cliente para Node 20 y TypeScript. ```bash npm install @tinkay/sdk ``` ## Uso ```tsx import { Tinkay } from "@tinkay/sdk"; const tinkay = new Tinkay({ apiKey: process.env.TINKAY_API_KEY! }); const contact = await tinkay.contacts.create({ name: "Camila Rodríguez", email: "camila@kipu.app", company: "Kipu Pagos", }); await tinkay.tickets.create({ subject: "Error 500 al exportar", description: "Reportado desde el panel interno.", contact_id: contact.id, priority: "high", }); for await (const c of tinkay.contacts.list()) { console.log(c.email); } ``` ## Sin SDK Mientras el paquete está en vista previa, un wrapper de veinte líneas alcanza. tinkay.ts: ```tsx const BASE = "https://api.tinkay.app/v1"; async function request(path: string, init: RequestInit = {}): Promise { const res = await fetch(BASE + path, { ...init, headers: { Authorization: `Bearer ${process.env.TINKAY_API_KEY}`, "Content-Type": "application/json", ...init.headers, }, }); if (!res.ok) { const { error } = await res.json().catch(() => ({ error: { message: res.statusText } })); throw new Error(`Tinkay ${res.status}: ${error.message}`); } return res.json() as Promise; } export const tinkay = { createContact: (body: { name: string; email: string; company?: string }) => request<{ data: { id: string } }>("/contacts", { method: "POST", body: JSON.stringify(body) }), createTicket: (body: { subject: string; description: string; contact_id: string; priority?: string }) => request<{ data: { id: string; number: number } }>("/tickets", { method: "POST", body: JSON.stringify(body) }), }; ``` --- URL: https://tinkay.app/docs/sdk-python # SDK de Python Cliente para Python 3.11 con requests. ```bash pip install tinkay ``` ## Uso ```python import os from tinkay import Tinkay tinkay = Tinkay(api_key=os.environ["TINKAY_API_KEY"]) contact = tinkay.contacts.create( name="Camila Rodríguez", email="camila@kipu.app", company="Kipu Pagos", ) tinkay.tickets.create( subject="Error 500 al exportar", description="Reportado desde el panel interno.", contact_id=contact["id"], priority="high", ) for c in tinkay.contacts.list(): print(c["email"]) ``` ## Sin SDK tinkay_client.py: ```python import os import requests BASE = "https://api.tinkay.app/v1" class TinkayError(Exception): pass class Tinkay: def __init__(self, api_key: str | None = None): self.session = requests.Session() self.session.headers["Authorization"] = f"Bearer {api_key or os.environ['TINKAY_API_KEY']}" def _request(self, method: str, path: str, **kwargs): res = self.session.request(method, BASE + path, timeout=15, **kwargs) if not res.ok: detail = res.json().get("error", {}).get("message", res.reason) raise TinkayError(f"Tinkay {res.status_code}: {detail}") return res.json() def create_contact(self, **fields): return self._request("POST", "/contacts", json=fields)["data"] def list_contacts(self): cursor = None while True: params = {"limit": 100, **({"cursor": cursor} if cursor else {})} page = self._request("GET", "/contacts", params=params) yield from page["data"] cursor = page["next_cursor"] if not cursor: break ``` --- URL: https://tinkay.app/docs/sdk-php # SDK de PHP Cliente para PHP 8.2 con Guzzle o curl. ```bash composer require tinkay/tinkay-php ``` ## Uso ```php contacts()->create([ "name" => "Camila Rodríguez", "email" => "camila@kipu.app", "company" => "Kipu Pagos", ]); $tinkay->tickets()->create([ "subject" => "Error 500 al exportar", "description" => "Reportado desde el panel interno.", "contact_id" => $contact["id"], "priority" => "high", ]); ``` ## Sin SDK TinkayClient.php: ```php $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$this->apiKey}", "Content-Type: application/json", ], ...($body ? [CURLOPT_POSTFIELDS => json_encode($body)] : []), ]); $raw = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $decoded = json_decode($raw, true) ?? []; if ($status >= 400) { throw new RuntimeException("Tinkay {$status}: " . ($decoded["error"]["message"] ?? "error")); } return $decoded; } public function createContact(array $fields): array { return $this->request("POST", "/contacts", $fields)["data"]; } } ``` # Guías --- URL: https://tinkay.app/docs/migrar-desde-intercom # Migrar desde Intercom Conversaciones, contactos, artículos y el dominio del centro de ayuda. ## 1. Exportar desde Intercom 1. En Intercom, entrá a **Settings, Data, Export** y pedí la exportación de contactos y conversaciones. 2. Descargá los artículos del Help Center desde **Articles, Export**. 3. Anotá el dominio del centro de ayuda actual y la lista de URLs publicadas. ## 2. Importar contactos Podés subir el CSV desde **Contactos, Importar CSV** o hacerlo por API si necesitás mapear atributos propios. ```js import { parse } from "csv-parse/sync"; import { readFileSync } from "node:fs"; const rows = parse(readFileSync("intercom-contacts.csv"), { columns: true }); for (const row of rows) { const res = await fetch("https://api.tinkay.app/v1/contacts", { method: "POST", headers: { Authorization: `Bearer ${process.env.TINKAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: row.name, email: row.email, company: row.company, tags: row.tags ? row.tags.split(",") : [], custom_fields: { intercom_id: row.id, signed_up_at: row.signed_up_at }, }), }); if (res.status === 409) continue; // ya existía if (!res.ok) console.error(row.email, await res.text()); } ``` > Guardá el id de Intercom en `custom_fields`: sirve para reconciliar si tenés que repetir la importación. ## 3. Migrar el centro de ayuda Creá las colecciones equivalentes en **Conocimiento, Colecciones** y publicá los artículos manteniendo el mismo slug. Así las URLs viejas siguen funcionando. Después apuntá tu dominio (por ejemplo `help.tuempresa.com`) a Tinkay con un CNAME hacia `tinkay.help`. Ver [dominio propio](/docs/dominio-propio). ## 4. Cambiar el widget Sacá el snippet de Intercom y poné el de Tinkay en el mismo lugar. Podés convivir unos días, pero no conviene: los clientes ven dos burbujas. ```html ``` ## 5. Ventana de migración - Hacelo un viernes a la tarde: el volumen baja y tenés el fin de semana de colchón. - Dejá las conversaciones abiertas de Intercom hasta cerrarlas ahí; las nuevas entran por Tinkay. - Avisale al equipo el día anterior y dejá el Inbox de Tinkay abierto en paralelo. --- URL: https://tinkay.app/docs/sincronizar-contactos # Sincronizar contactos Mantener tus usuarios y sus atributos alineados con Tinkay. ## Estrategia recomendada No sincronices en bloque cada noche. Hacé dos cosas: **identificá en el navegador** a quien usa tu producto y **empujá cambios desde el backend** cuando pasa algo relevante (upgrade, baja, cambio de plan). ## En el navegador ```js window.Tinkay.identify({ email: user.email, name: user.fullName, plan: user.plan, company: user.company, created_at: user.createdAt, seats: user.seats, }); ``` > Esto crea el contacto si no existe y actualiza los atributos si ya existía. Para la mayoría de los productos alcanza con esto. ## Desde el backend Cuando cambia algo importante fuera de la sesión (un pago, una baja), empujalo por API. C#: ```csharp public async Task SyncContactAsync(User user) { var payload = new { name = user.FullName, email = user.Email, company = user.Company, tags = user.IsChurnRisk ? new[] { "riesgo" } : Array.Empty(), custom_fields = new { plan = user.Plan, mrr = user.Mrr, seats = user.Seats } }; var res = await http.PostAsJsonAsync("https://api.tinkay.app/v1/contacts", payload); // 409 significa que ya existe: no es un error para una sincronización. if (res.StatusCode is not (HttpStatusCode.Created or HttpStatusCode.Conflict)) res.EnsureSuccessStatusCode(); } ``` Python: ```python def sync_contact(user): res = requests.post( "https://api.tinkay.app/v1/contacts", headers={"Authorization": f"Bearer {os.environ['TINKAY_API_KEY']}"}, json={ "name": user.full_name, "email": user.email, "company": user.company, "custom_fields": {"plan": user.plan, "mrr": user.mrr, "seats": user.seats}, }, timeout=10, ) if res.status_code not in (201, 409): res.raise_for_status() ``` ## Carga inicial Para la primera importación, usá el CSV de **Contactos, Importar CSV**: valida, muestra una vista previa y detecta duplicados por email. --- URL: https://tinkay.app/docs/crear-tickets # Crear tickets desde tu sistema Abrir casos en Tinkay cuando algo falla en tu producto. ## Flujo típico 1. Tu sistema detecta un problema que afecta a un cliente. 2. Buscás o creás el contacto en Tinkay por email. 3. Creás el ticket con la prioridad adecuada y las etiquetas que uses para reportar. 4. Guardás el `id` del ticket en tu base para poder enlazarlo después. ## Implementación Node: ```js async function abrirTicket({ user, incident }) { const headers = { Authorization: `Bearer ${process.env.TINKAY_API_KEY}`, "Content-Type": "application/json", }; // 1. Contacto (409 significa que ya existe) const contactRes = await fetch("https://api.tinkay.app/v1/contacts", { method: "POST", headers, body: JSON.stringify({ name: user.name, email: user.email, company: user.company }), }); const contactBody = await contactRes.json(); const contactId = contactRes.status === 409 ? contactBody.error.contact_id : contactBody.data.id; // 2. Ticket const ticketRes = await fetch("https://api.tinkay.app/v1/tickets", { method: "POST", headers, body: JSON.stringify({ subject: `${incident.code}: ${incident.title}`, description: incident.summary, contact_id: contactId, priority: incident.blocking ? "urgent" : "high", tags: ["incidente", incident.module], }), }); const { data } = await ticketRes.json(); return data; // { id, number, slaDueAt, ... } } ``` > Si el incidente nace de una conversación existente, pasá `conversation_id` y el ticket queda vinculado en el Inbox. ## Cerrar el círculo Suscribite a `ticket.updated` por [webhook](/docs/webhooks) para reflejar el estado en tu propio panel y avisarle a quien reportó cuando se resuelve. --- URL: https://tinkay.app/docs/datos-propios # Mostrar tus datos en el panel Que tu equipo vea plan, uso y estado del cliente junto a la conversación. ## Atributos del contacto Todo lo que mandes en `custom_fields` (por API) o en atributos extra de `identify` (en el navegador) aparece en el panel del contacto, dentro del Inbox. Usá nombres legibles: se muestran tal cual al equipo. ```js window.Tinkay.identify({ email: user.email, name: user.name, plan: "Business", "Asientos usados": 12, "Facturas impagas": 0, "Cliente desde": "2024-03-11", }); ``` ## Datos en vivo con acciones Los atributos son una foto del momento. Si necesitás el dato actualizado al segundo (saldo, estado de un envío), es mejor una [acción de Tinkay AI](/docs/ai-acciones): la IA lo consulta cuando hace falta y lo usa para responder. ## Buenas prácticas - No mandes datos sensibles que tu equipo no necesita ver. - Diez atributos bien elegidos son mejores que cuarenta. - Actualizá en cada carga de página: `identify` es idempotente. --- URL: https://tinkay.app/docs/dominio-propio # Dominio propio para el centro de ayuda Publicar la ayuda en ayuda.tuempresa.com o el subdominio que elijas. ## 1. Elegí el subdominio Puede ser el que quieras: `ayuda.tuempresa.com`, `help.tuempresa.com`, `soporte.tuempresa.com`. Cargalo en **Conocimiento, Centro de ayuda, Dominio**. ## 2. Creá el CNAME ```text Tipo Nombre Valor TTL CNAME ayuda tinkay.help 3600 ``` > No uses un registro A: la IP puede cambiar. Si tu proveedor no admite CNAME en la raíz, usá siempre un subdominio. ## 3. Verificá Tocá `Verificar DNS` en el panel. Cuando el registro propaga, emitimos el certificado automáticamente (suele tardar menos de cinco minutos). ```bash dig CNAME ayuda.tuempresa.com +short # debe devolver tinkay.help. ``` ## SEO - El centro de ayuda genera `sitemap.xml` y `robots.txt` automáticamente. - Mantené los slugs si venís de otra herramienta: evitás perder posiciones. - Pedí la indexación manual en Search Console para acelerar el primer rastreo. --- URL: https://tinkay.app/docs/seguridad # Seguridad Credenciales, dominios permitidos, CSP y datos sensibles. ## Credenciales - El token del Messenger (`pk_live_`) es público y va en el navegador. Está limitado por dominio. - La API key (`dk_live_`) es secreta: solo en tu servidor, nunca en el front, en GTM ni en una app móvil. - El signing secret del webhook (`whsec_`) es distinto de la API key y solo sirve para validar firmas. - Rotá las keys si sospechás una filtración: la revocación es inmediata. ## Dominios permitidos El Messenger solo carga en los dominios autorizados. Es la defensa contra que alguien copie tu token y lo use en otro sitio. Agregá cada variante que uses, incluida la de staging. ## Content Security Policy ```text script-src 'self' https://cdn.tinkay.app; frame-src https://app.tinkay.app; connect-src 'self' https://app.tinkay.app; ``` ## Datos sensibles - No mandes números de tarjeta, contraseñas ni documentos completos en `identify` ni en `custom_fields`. - Si tu producto maneja datos de salud o financieros, definí temas sensibles en la escalación de Tinkay AI para que nunca responda sola sobre eso. - Los adjuntos se guardan cifrados y se sirven con URLs firmadas de corta duración. ## Acceso del equipo - Asigná el rol mínimo necesario: Agente para quien solo responde. - En el plan Experto podés exigir SSO y 2FA para todo el workspace. - El registro de actividad guarda quién cambió qué, con IP y fecha. Revocar una key: ```bash # Configuración, Desarrolladores, API keys, Revocar # o rotá la variable de entorno y creá una nueva key curl https://api.tinkay.app/v1/contacts -H "Authorization: Bearer dk_live_xxx" -i | head -1 ``` --- URL: https://tinkay.app/docs/eventos-en-tiempo-real # Recibir eventos en tiempo real De cero a un consumidor de eventos en producción, con cola y reintentos. Tinkay emite 126 eventos distintos. Un consumidor bien hecho tiene tres partes: **verificar**, **encolar** y **procesar**. Separarlas es lo que evita perder eventos cuando tu servicio tiene un pico o se cae. ## 1. Elegí los eventos Suscribite solo a lo que vas a usar. Podés empezar con un grupo entero y afinar después. 1. En **Configuración, Desarrolladores, Webhooks**, creá el webhook con tu URL. 2. Elegí los eventos con el buscador o seleccionando un grupo completo. 3. Copiá el signing secret y guardalo como variable de entorno. ## 2. Verificá y encolá El endpoint HTTP hace lo mínimo: valida la firma, encola y responde 200. Nada de lógica de negocio acá. server.js: ```js import express from "express"; import crypto from "node:crypto"; import { queue } from "./queue.js"; const app = express(); const SECRET = process.env.TINKAY_WEBHOOK_SECRET; app.post("/hooks/tinkay", express.raw({ type: "application/json" }), async (req, res) => { const raw = req.body.toString("utf8"); if (!verify(raw, req.get("X-Tinkay-Timestamp"), req.get("X-Tinkay-Signature"))) { return res.status(400).send("invalid signature"); } const event = JSON.parse(raw); await queue.add("tinkay-event", event, { jobId: event.id }); // jobId evita duplicados res.sendStatus(200); }); function verify(raw, timestamp, signature) { if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const expected = crypto.createHmac("sha256", SECRET).update(`${timestamp}.${raw}`).digest("hex"); const a = Buffer.from(expected, "hex"); const b = Buffer.from(String(signature).replace("sha256=", ""), "hex"); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` > Usar el `id` del evento como `jobId` de la cola resuelve la idempotencia sin escribir una línea extra. ## 3. Procesá por tipo El worker despacha por nombre de evento. Lo que no conocés se ignora sin romper nada. worker.js: ```js const handlers = { "conversation.created": onConversationCreated, "conversation.closed": onConversationClosed, "ai.handoff": onHandoff, "contact.updated": onContactUpdated, }; export async function process(event) { const handler = handlers[event.name]; if (!handler) return; // evento nuevo o no suscrito: se ignora await handler(event.data, event); } ``` ## 4. Monitoreá - El **registro de entregas** del webhook muestra código, duración e intento de cada entrega. - Alertá sobre el evento `webhook.delivery_failed`: significa que agotamos los reintentos. - Si acumulás fallas, el webhook se desactiva solo y recibís `webhook.disabled`. --- URL: https://tinkay.app/docs/sincronizar-crm # Sincronizar tu CRM con webhooks Mantener contactos y empresas al día en los dos sentidos, sin polling. ## De Tinkay a tu CRM Suscribite a los eventos de contacto y mapeá el payload a tu modelo. **Eventos a suscribir** | Campo | Tipo | Descripción | | --- | --- | --- | | `contact.created` | alta | Crear el registro en el CRM. | | `contact.updated` | cambio | Actualizar solo los campos de `changes`. | | `contact.identified` | enriquecimiento | Vincular con tu `external_id`. | | `contact.merged` | unificación | Fusionar los dos registros del CRM. | | `contact.deleted` | baja | Borrar o anonimizar por pedido de privacidad. | ```js async function onContactEvent(event) { const { id, email, name } = event.data; // Buscar por el id de Tinkay guardado como campo externo evita duplicados. const existing = await crm.contacts.findByExternalId(id); if (existing) return crm.contacts.update(existing.id, { email, name }); return crm.contacts.create({ email, name, externalId: id, source: "Tinkay" }); } ``` ## De tu CRM a Tinkay Cuando algo cambia en el CRM, escribí el contacto en Tinkay. Es un `POST` idempotente por email. ```bash curl -X POST https://api.tinkay.app/v1/contacts \ -H "Authorization: Bearer dk_live_xxx" \ -H "Idempotency-Key: crm-sync-84213" \ -H "Content-Type: application/json" \ -d '{ "email": "juan@loomi.com.ar", "name": "Juan Pérez", "company": "Loomi", "custom_fields": { "Plan": "Pro", "MRR": "149" } }' ``` > Marcá los cambios que vienen del CRM para no reenviarlos de vuelta cuando Tinkay emita `contact.updated`. Sin eso entrás en un lazo infinito. ## Carga inicial Para el primer volcado usá la paginación por cursor en vez de esperar eventos. Después dejá los webhooks al día. ```bash curl "https://api.tinkay.app/v1/contacts?limit=100" \ -H "Authorization: Bearer dk_live_xxx" ``` --- URL: https://tinkay.app/docs/migrar-de-polling # Migrar de polling a webhooks Dejar de consultar la API cada minuto sin perder eventos en la transición. Consultar la API cada minuto gasta límite de tasa, agrega latencia y se pierde los cambios intermedios. Los webhooks resuelven las tres cosas, pero la migración conviene hacerla en capas. ## 1. Escribí el consumidor en paralelo Creá el webhook y procesá los eventos escribiendo en una tabla aparte. No toques todavía tu tabla de producción. ## 2. Compará durante unos días Con el job de polling todavía activo, compará ambos resultados. Deberían coincidir salvo por los cambios intermedios, que solo ve el webhook. Comparación: ```sql -- Filas que el polling no vio SELECT w.object_id, w.updated_at FROM webhook_state w LEFT JOIN polling_state p ON p.object_id = w.object_id WHERE p.object_id IS NULL OR p.updated_at < w.updated_at; ``` ## 3. Cambiá la fuente de verdad Cuando la comparación no muestre diferencias, apuntá tu lógica a la tabla del webhook y bajá la frecuencia del polling a una vez por día. ## 4. Dejá una red de seguridad Guardá un job diario que reconcilie contra `GET /v1/...` con `updated_after`. Cubre el caso raro de una caída larga de tu endpoint. Si tu servicio estuvo caído más de 24 horas, los reintentos ya vencieron: la reconciliación es la única forma de recuperar esos cambios. > Webhooks para lo inmediato, reconciliación diaria para dormir tranquilo. Esa combinación es la que usan los equipos grandes. --- URL: https://tinkay.app/docs/auditoria-cumplimiento # Auditoría y cumplimiento Llevar el registro de actividad a tu SIEM y responder pedidos de privacidad. ## Registro de actividad Todo cambio administrativo queda registrado con actor, objetivo, fecha e IP. Lo ves en **Configuración, Seguridad** y lo exportás por API. Para un SIEM, suscribite a los eventos de seguridad y mandalos a tu colector. **Eventos de seguridad** | Campo | Tipo | Descripción | | --- | --- | --- | | `login.succeeded` | acceso | Inicio de sesión con método, IP y navegador. | | `login.failed` | acceso | Intento fallido con el motivo. | | `session.revoked` | acceso | Cierre de sesión forzado. | | `api_key.created` | credenciales | Alta de API key con sus permisos. | | `api_key.revoked` | credenciales | Baja de API key. | | `member.role_changed` | permisos | Cambio de rol de un miembro. | | `team.permissions_changed` | permisos | Cambio de permisos de equipo. | ## Pedidos de acceso y borrado Ante un pedido de datos personales, exportá todo lo asociado al contacto y luego eliminalo. El borrado emite `contact.deleted` con `reason: gdpr_request` para que tu sistema haga lo mismo. ```bash # 1. Encontrar el contacto curl "https://api.tinkay.app/v1/search?q=juan@loomi.com.ar&types=contact" \ -H "Authorization: Bearer dk_live_xxx" # 2. Exportar sus conversaciones curl "https://api.tinkay.app/v1/conversations?contact_id=ct_juan&limit=100" \ -H "Authorization: Bearer dk_live_xxx" # 3. Eliminar curl -X DELETE https://api.tinkay.app/v1/contacts/ct_juan \ -H "Authorization: Bearer dk_live_xxx" ``` ## Retención - Las conversaciones y los tickets se conservan mientras el workspace esté activo. - El registro de auditoría se conserva 24 meses en el plan Experto. - Las entregas de webhook se conservan 30 días con su cuerpo completo, para reenviar o depurar. --- URL: https://tinkay.app/docs/accion-tinkay-ai # Construir una acción para Tinkay AI Exponer un endpoint propio para que la IA consulte tu sistema durante la conversación. Una **acción** es un endpoint tuyo que Tinkay AI puede llamar cuando la conversación lo necesita. Es lo que separa una IA que repite documentación de una que responde con el estado real de la cuenta. ## 1. Escribí el endpoint Recibe los parámetros que definas y devuelve JSON plano. Cuanto más simple, mejor lo usa la IA. C# · SubscriptionsController.cs: ```csharp [ApiController] [Route("internal/billing")] public class SubscriptionsController : ControllerBase { [HttpGet("subscriptions/{workspace}")] public async Task Get(string workspace) { if (!Request.Headers.TryGetValue("X-Tinkay-Signature", out var signature)) return Unauthorized(); var sub = await _billing.FindAsync(workspace); if (sub is null) return NotFound(new { error = "workspace_not_found" }); return Ok(new { plan = sub.Plan, seats = sub.Seats, next_invoice_at = sub.NextInvoiceAt, status = sub.Status }); } } ``` Node: ```js app.get("/internal/billing/subscriptions/:workspace", async (req, res) => { const sub = await billing.find(req.params.workspace); if (!sub) return res.status(404).json({ error: "workspace_not_found" }); res.json({ plan: sub.plan, seats: sub.seats, next_invoice_at: sub.nextInvoiceAt, status: sub.status, }); }); ``` ## 2. Registrala en Tinkay 1. Entrá a **Tinkay AI, Acciones** y tocá `Nueva acción`. 2. Poné un nombre claro: la IA lo usa para decidir cuándo llamarla. `Consultar suscripción` funciona mejor que `getSub`. 3. Describí qué devuelve, en una línea. 4. Definí método, URL y parámetros, marcando cuáles son obligatorios. 5. Probala desde el **AI Test Lab** con una consulta real. ## 3. Contrato y errores **Qué esperamos de tu endpoint** | Campo | Tipo | Descripción | | --- | --- | --- | | `Tiempo de respuesta` | 5 s | Pasado ese tiempo se corta y la IA responde con lo que sabe. | | `Códigos` | 2xx / 4xx / 5xx | Un 4xx se interpreta como dato no encontrado; un 5xx como falla temporal. | | `Cuerpo` | JSON plano | Objetos anidados o listas largas confunden a la IA. Devolvé lo justo. | | `Autenticación` | header firmado | Enviamos `X-Tinkay-Signature` para que valides el origen. | > No devuelvas datos sensibles que no quieras que la IA repita: todo lo que responde el endpoint puede terminar en el chat. ## 4. Observá su uso Los eventos `ai.action_called` y `ai.action_failed` te dan visibilidad de cada llamada, con duración y estado. En **Tinkay AI, Acciones** ves las ejecuciones de los últimos 30 días por acción. --- URL: https://tinkay.app/docs/proveedor-ia-propio # Usar tu propio proveedor de IA Conectar tu API de OpenAI, Anthropic, Azure o un modelo compatible. Tinkay incluye un cupo de resoluciones con modelo en cada plan. Si te quedás sin cupo, o si tu empresa exige que la inferencia pase por su propia cuenta, podés conectar tu proveedor y todo sigue funcionando igual. Las **respuestas aprendidas** no consumen cupo ni llaman a ningún proveedor: se sirven desde la memoria de tu workspace. ## Conectar el proveedor 1. Entrá a **Tinkay AI, Configuración, Proveedor de IA**. 2. Elegí el proveedor: Tinkay, OpenAI, Anthropic, Azure OpenAI, Google o compatible con OpenAI. 3. Pegá tu API key. Se guarda cifrada y no se vuelve a mostrar. 4. Elegí el modelo y, si corresponde, la URL base. 5. Tocá `Probar conexión` y guardá. > La key se guarda cifrada y solo se usa desde nuestros servidores. Nunca viaja al navegador ni al Messenger. ## Cadena de respaldo Definís el orden en el que Tinkay intenta responder. Si un paso falla o se agota, pasa al siguiente sin cortar la conversación. | Campo | Tipo | Descripción | | --- | --- | --- | | `1. Memoria` | sin costo | Respuestas aprendidas. No consumen cupo ni llaman a ningún modelo. | | `2. Cupo del plan` | incluido | Modelo administrado por Tinkay. | | `3. Tu proveedor` | tu cuenta | Se factura en tu cuenta del proveedor, no en Tinkay. | | `4. Derivar` | humano | Si nada responde, pasa a una persona con el contexto completo. | ## Qué se registra El panel de **Tinkay AI, Uso** separa el consumo del cupo incluido del de tu proveedor, con su costo estimado. Los eventos `ai.provider_changed` y `ai.provider_error` te avisan de cambios y fallas. ai.provider_error: ```json { "id": "evt_3f9a1c", "name": "ai.provider_error", "data": { "provider": "openai", "status": 429, "fallback_used": "memory" } } ```