# Contatos Source: https://docs.connectvets.com.br/flow/api-reference/contacts Criar, atualizar, consultar e apagar contatos via API externa # Contatos Identifique o contato com **`external_id`**, **`document`** ou **`phone`** (pelo menos um). Ordem de busca: `external_id` → `document` → `phone` (telefone normalizado BR). Na criação, `name` e `phone` são obrigatórios. Não envie `peti9_id` numérico (**400**). Use `external_id` como texto opaco do seu ERP. Veja também a [política de sincronização](/flow/api-reference/sync-policy) (`flow` vs `erp`). ## `PUT …/contacts` — upsert Scope: `contacts:write` ```http theme={null} PUT https://api.connectvets.com.br/flow/v1/external/workspaces/{workspace_id}/contacts Authorization: Bearer flow_live_ Content-Type: application/json { "external_id": "erp-cust-8821", "document": "12345678901", "phone": "+5511999999999", "name": "Maria", "email": "maria@example.com", "address": "Rua Exemplo, 100", "source": "ERP" } ``` ### Campos | Campo | Tipo | Criar | Atualizar | Limpar (`null`) | | - | - | - | - | - | | `external_id` | string | Opcional | Define/atualiza | Limpa coluna anulável | | `document` | string | Opcional | Define/atualiza | Limpa | | `phone` | string | Obrigatório na criação | Normalizado BR; **409** se duplicado | — | | `name` | string | Obrigatório na criação | Conforme política flow/erp | — | | `email`, `address`, `source`, `profile_pic_url` | string | Opcional | Conforme política | Limpa | ### Códigos HTTP | Código | Significado | | - | - | | 200 | Contato atualizado | | 201 | Contato criado (ou restaurado) | | 400 | Dados inválidos ou campo legado rejeitado | | 401 | Token ausente ou inválido | | 403 | Sem permissão, workspace ou billing | | 409 | Telefone ou `external_id` duplicado | ### Warnings Resposta **2xx** significa que o contato foi gravado. Se houver `warnings`, nome ou telefone do payload podem não ter sido aplicados (política Flow): * `name_not_applied_user_edited_in_flow` * `phone_not_applied_user_edited_in_flow` ### cURL completo ```bash theme={null} export FLOW_BASE_URL="https://api.connectvets.com.br/flow/v1" export TOKEN="flow_live_" export WORKSPACE_ID="$(curl -sS "$FLOW_BASE_URL/external/me" -H "Authorization: Bearer $TOKEN" | jq -r .workspace_id)" curl -sS -X PUT "$FLOW_BASE_URL/external/workspaces/$WORKSPACE_ID/contacts" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"external_id":"erp-1","document":"12345678901","name":"João","phone":"+5511987654321"}' ``` ## Consultar e apagar ### `GET …/contacts/:contact_id` Scope: `contacts:read` — devolve uma projeção limitada do contato (sem pets, `peti9_id`, etc.). ```http theme={null} GET https://api.connectvets.com.br/flow/v1/external/workspaces/{workspace_id}/contacts/{contact_id} Authorization: Bearer flow_live_ ``` ### `DELETE …/contacts/:contact_id` Scope: `contacts:delete` — soft-delete; responde **204** mesmo se o contato já estiver apagado (idempotente). ```http theme={null} DELETE https://api.connectvets.com.br/flow/v1/external/workspaces/{workspace_id}/contacts/{contact_id} Authorization: Bearer flow_live_ ``` ## Auditoria Cada PUT/DELETE externo bem-sucedido gera auditoria (`contact_external_api_audit_logs`, retenção **90 dias**). Admins veem o histórico no modal do contato no CRM. # Erros e limites Source: https://docs.connectvets.com.br/flow/api-reference/errors Códigos HTTP comuns da API externa do Flow # Erros e limites Use a tabela abaixo. Muitas falhas de login no mesmo IP podem gerar **429**. Billing inativo também bloqueia com **403**. | Código | Significado | | - | - | | 200 | Sucesso (também usado quando `triggered: false` na pesquisa) | | 201 | Contato criado (ou restaurado) | | 204 | Apagado com sucesso (idempotente) | | 400 | Dados inválidos ou campo legado rejeitado | | 401 | Token ausente ou inválido | | 403 | Sem permissão, workspace ou billing | | 404 | Recurso não encontrado | | 409 | Telefone ou `external_id` duplicado | | 429 | Muitas falhas de autenticação do mesmo IP | Workspace com billing inativo bloqueia todas as rotas externas, incluindo MCP. # Pesquisa de satisfação Source: https://docs.connectvets.com.br/flow/api-reference/satisfaction-surveys Disparar pesquisa de satisfação a partir de qualquer ERP # Disparar pesquisa de satisfação Endpoint genérico para qualquer ERP (preferível a webhooks específicos de parceiro em novas integrações). Exige scope `satisfaction:write` e pesquisa habilitada no CRM. ## Pré-requisitos 1. Configuração de pesquisa habilitada no CRM, com etapa de pesquisa definida. 2. Contato já sincronizado (use [`PUT` de contatos](/flow/api-reference/contacts) antes, se preciso). 3. Contato com lead ativo (não finalizado) e chat WhatsApp associado. ## `POST …/satisfaction-surveys` Identifique o contato com `external_id`, `document` ou `phone` (mesma ordem da API de contatos). O campo `reason` é opcional (padrão: `visit_completed_external`). ```http theme={null} POST https://api.connectvets.com.br/flow/v1/external/workspaces/{workspace_id}/satisfaction-surveys Authorization: Bearer flow_live_ Content-Type: application/json { "external_id": "erp-cust-8821", "reason": "visit_completed" } ``` ### Resposta 200 ```json theme={null} { "triggered": true, "lead_id": "uuid", "contact_id": "uuid", "reason": "visit_completed" } ``` | Campo / caso | Significado | | - | - | | `triggered: true` | Pesquisa enviada / lead movido para a etapa de pesquisa | | `triggered: false` | Config desabilitada, pesquisa já enviada ou condições não atendidas (**ainda assim HTTP 200**) | | 400 | Identificador ausente / body inválido | | 403 | Scope / workspace / billing | | 404 | Contato ou lead ativo não encontrado | ### cURL ```bash theme={null} export FLOW_BASE_URL="https://api.connectvets.com.br/flow/v1" export TOKEN="flow_live_" export WORKSPACE_ID="$(curl -sS "$FLOW_BASE_URL/external/me" -H "Authorization: Bearer $TOKEN" | jq -r .workspace_id)" curl -sS -X POST "$FLOW_BASE_URL/external/workspaces/$WORKSPACE_ID/satisfaction-surveys" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"external_id":"erp-cust-8821","reason":"visit_completed"}' ``` # Política de sync Source: https://docs.connectvets.com.br/flow/api-reference/sync-policy Quem vence quando ERP e equipe editam o mesmo contato # Política de sincronização Define quem manda quando o ERP e a equipe editam o mesmo contato. Configure em **Integrações → Acesso à API**. | Valor | Significado | | - | - | | `flow` (padrão) | Flow preserva edições manuais — se a equipe alterou nome/telefone no CRM, o ERP não sobrescreve esses campos. | | `erp` | ERP sobrescreve — o que o ERP enviar de nome/telefone passa a valer no Flow. | Quando a política `flow` bloqueia um campo, o upsert ainda retorna **2xx** e pode incluir `warnings`: * `name_not_applied_user_edited_in_flow` * `phone_not_applied_user_edited_in_flow` Veja [Contatos](/flow/api-reference/contacts). # Workspaces Source: https://docs.connectvets.com.br/flow/api-reference/workspaces Descobrir o workspace_id a partir da API key # Workspaces — identificar workspace A chave de API já está ligada a um único workspace. Use `GET /external/me` para descobrir o `workspace_id` sem depender da UI — útil no onboarding do ERP. ## `GET /external/me` Sem scope específico — basta uma chave válida e não revogada. Workspace vem da chave (sem `:workspace_id` no path). ```http theme={null} GET https://api.connectvets.com.br/flow/v1/external/me Authorization: Bearer flow_live_ ``` ### Resposta 200 ```json theme={null} { "workspace_id": "uuid", "workspace_name": "Clínica Exemplo", "key": { "name": "ERP produção", "prefix": "flow_live_abcd1234", "scopes": ["contacts:write", "satisfaction:write"] } } ``` ### cURL ```bash theme={null} export FLOW_BASE_URL="https://api.connectvets.com.br/flow/v1" export TOKEN="flow_live_" curl -sS "$FLOW_BASE_URL/external/me" \ -H "Authorization: Bearer $TOKEN" ``` Use `workspace_id` nas rotas `/external/workspaces/{workspace_id}/…`. Não há listagem de vários workspaces: cada chave resolve exatamente um tenant. # Autenticação Source: https://docs.connectvets.com.br/flow/authentication Como autenticar na API externa do Flow com Bearer flow_live_ # Autenticação Crie a chave em **Integrações → Gerenciar chaves**. Em cada pedido HTTP envie: ```http theme={null} Authorization: Bearer flow_live_ ``` * O nome do scheme `Bearer` é case-insensitive (RFC 7235). * O servidor armazena apenas o hash SHA-256 do token. * O token só aparece **uma vez** na criação — se perder, revogue e crie outra. * A chave é amarrada a **um workspace**; rotas com `:workspace_id` exigem que o path bata com a chave (senão **403**). ## Emitir chave (session) Admins autenticados no CRM: ```http theme={null} POST /api/workspaces/{workspace_id}/api-keys Cookie: session=… Content-Type: application/json { "name": "ERP produção", "scopes": ["contacts:write", "contacts:read", "satisfaction:write"] } ``` Resposta **201** inclui `token` (uma vez) e `workspace_id`. ## Descobrir o workspace pela chave ```bash theme={null} curl -sS "https://api.connectvets.com.br/flow/v1/external/me" \ -H "Authorization: Bearer flow_live_" ``` Veja [Workspaces](/flow/api-reference/workspaces) e [Permissões (scopes)](/flow/scopes). Nunca exponha o token em front-end ou repositório público. Muitas falhas de autenticação do mesmo IP podem gerar **429**. # Sync da IA e auditoria de conhecimento Source: https://docs.connectvets.com.br/flow/connectia/sync-and-knowledge-audit Como saber se o treinamento está sincronizado, se o cliente preencheu e se o agente lê o Estilo de Comunicação # Sync da IA e auditoria de conhecimento Guia interno (CS / suporte) para separar três perguntas que costumam se misturar: 1. O cliente **salvou** no Flow? 2. A IA **já processou** (sincronizou)? 3. O agente **está usando** aquele conteúdo no prompt? ## Duas telas, duas perguntas | Pergunta | Onde olhar | O que significa “ok” | | - | - | - | | O cliente salvou e a IA já processou? | Flow → **Sincronização da IA** (`/ai-sync`) | Status **sincronizado** (`ready`) + mensagem “Salvo e disponível para o agente” | | O que pode entrar no contexto do modelo? | Shirechat → agente → **Auditoria de conhecimento** | Item com decisão **Sempre** ou **Pode ser roteado**, origem **Treinamento do agente**, sem motivo de falha/processamento | A auditoria avisa: estar na lista **não prova** uso em uma mensagem específica — use o **chat de teste** para telemetria por resposta. ## Como saber se está sincronizado 1. Abra **Sincronização da IA** com o **mesmo agente** do funil. 2. Na card **Saúde da sincronização**, veja o badge agregado e o mapa por categoria. 3. Para **Estilo de comunicação**, a categoria no backend é `communication_identity`. 4. Interpretação rápida: * **`ready`**: treinamento disponível para o agente. * **`saved` / `queued` / `syncing` / `processing`**: salvo no Flow, pipeline ainda rodando — use *Atualizar agora* ou aguarde. * **`failed` / `retained` / `stalled`**: salvo no Flow, **agente não tem a versão nova**. * **`no_agent` / `skipped_shared`**: dados ok no workspace, mas este agente não recebeu o update. No **catálogo**, falhas são por item; o grupo pode mostrar “10 falha · 20 sincronizado”. ## Como saber se o cliente preencheu 1. Fonte de verdade do formulário: ConnectIA → **Estilo de Comunicação** / **Linguagem da equipe** (`/ai-tone`). 2. Campo persistido: `ai_communication_identity` no workspace. 3. Prova de que virou treinamento: após salvar, em **Sincronização da IA** a linha **Estilo de comunicação** fica **`ready`**. **Não use** o item `ps:estilo-comunicacao` na auditoria como prova de preenchimento — é o **template gerador da plataforma**, não os dados do cliente. ## Estilo de Comunicação: o agente lê? ### Por que aparece “Não entra” em `ps:estilo-comunicacao` `estilo-comunicacao` e `linguagem-equipe` entram **via embeddings do ConnectIA**, não pelo texto do prompt stack. Na auditoria isso aparece como decisão **Não entra** + motivo **embedding\_synced**. É o comportamento esperado. O texto tipo “GERADOR DE TREINAMENTO — ESTILO DE COMUNICAÇÃO” é a **receita** usada no save do ConnectIA; **não** é o prompt injetado a cada mensagem. ### O que o agente realmente usa * Documentos gerados no save, título **Estilo de Comunicação** / **Linguagem da Equipe** * Origem **Treinamento do agente** (`a:…`) * Modo **always** → decisão **Sempre no contexto** ### Checklist prático 1. Auditoria → atalho **Estilo do cliente (embeddings)** → item com decisão **Sempre** → **Ver** (conteúdo com valores do cliente). 2. Sincronização da IA → **Estilo de comunicação** = **`ready`**. 3. Chat de teste: a lista “Embeddings usados nesta resposta” mostra só RAG **roteado**. Treinamentos **Sempre no contexto** (estilo) **não** aparecem aí — confie na auditoria + sync. ### Prompt stack “retired” Uma release marcada como `retired` no cabeçalho da auditoria **não** invalida embeddings já sincronizados. Afeta sobretudo treinamentos que entram **pelo texto do stack**. ### Admin alterou o template global de estilo O texto de fábrica novo só república embeddings no **próximo salvamento** de Estilo / Linguagem no ConnectIA. ## Checklist rápido 1. **Sincronizado?** → `/ai-sync`, categoria desejada = `ready`. 2. **Cliente preencheu?** → `/ai-tone` (campos preenchidos) + sync `ready`. 3. **Agente com treinamento X ou Y?** → Auditoria: **Sempre** = injetado sempre; **Pode ser roteado** = só se o roteador escolher; **Não entra** = ler o **Motivo**. 4. **Estilo?** → Ignorar `ps:estilo-comunicacao`; validar `a:…` **Estilo de Comunicação**, decisão **Sempre**; sync `communication_identity` = `ready`. # Checklist de homologação do ERP Source: https://docs.connectvets.com.br/flow/integration/erp-checklist Lista técnica para validar uma integração com o Flow antes de usar em produção # Checklist de homologação do ERP Use esta lista antes de ativar a integração em produção. ## Acesso * [ ] A API key foi criada pelo administrador do workspace correto. * [ ] A chave tem somente os scopes necessários. * [ ] O ERP chama `GET /external/me` e usa o `workspace_id` retornado. * [ ] A chave está em um cofre de segredos, nunca no front-end ou no repositório. ## Contatos * [ ] Cada cliente do ERP possui um `external_id` estável. * [ ] O mesmo cliente sempre é enviado com o mesmo `external_id`. * [ ] O primeiro envio inclui `name` e `phone`. * [ ] Telefones brasileiros usam o formato internacional, como `+5511999999999`. * [ ] O ERP trata `200` como atualização e `201` como criação. * [ ] O ERP trata `409` sem repetir indefinidamente a mesma chamada. * [ ] O ERP lê `warnings` de respostas 2xx quando nome ou telefone podem ter sido preservados pelo Flow. ## Sincronização * [ ] A clínica definiu se o Flow ou o ERP controla nome e telefone. * [ ] O ERP não envia `peti9_id`; use somente `external_id`. * [ ] Reenvios são seguros: a mesma operação não cria contatos duplicados. * [ ] Exclusões só são habilitadas quando o ERP realmente precisa apagá-las. ## Webhooks * [ ] A URL do ERP usa HTTPS público. * [ ] O ERP valida a assinatura `X-Flow-Signature`. * [ ] O ERP aceita tentativas repetidas do mesmo evento sem duplicar efeitos. * [ ] O ERP responde rapidamente com HTTP 2xx e processa trabalho demorado de forma assíncrona. * [ ] A equipe configurou uma forma de acompanhar falhas de entrega. ## Pesquisa de satisfação * [ ] O scope `satisfaction:write` foi concedido apenas se necessário. * [ ] O ERP envia a pesquisa somente após um atendimento concluído. * [ ] O ERP mantém um identificador próprio do atendimento para evitar disparos duplicados. ## Liberação * [ ] Um contato de teste foi criado e encontrado na tela **Contatos** do Flow. * [ ] Uma atualização do mesmo contato não criou duplicata. * [ ] O webhook de teste foi recebido e validado, se usado. * [ ] A integração foi testada com dados fictícios antes dos dados reais. * [ ] A clínica sabe quem revoga a chave em caso de incidente. Veja também o [passo a passo para a clínica](/flow/integration/erp-getting-started) e a [referência de contatos](/flow/api-reference/contacts). # Integrar seu ERP ao Flow Source: https://docs.connectvets.com.br/flow/integration/erp-getting-started Passo a passo simples para conectar seu sistema ao Flow # Integrar seu ERP ao Flow Este guia é para quem usa um ERP na clínica e quer manter os contatos organizados também no Flow. Você não precisa ser desenvolvedor para seguir os passos de preparação. Para configurar o seu sistema, envie este guia para a pessoa ou empresa responsável pelo ERP. ## Escolha o seu caminho Se sua clínica usa Peti9, abra **Integrações → Peti9 ERP** no Flow e clique em **Configurar**. Peça ao responsável técnico pelo seu ERP para usar a API externa do Flow. Os passos abaixo mostram como liberar o acesso. ## Antes de começar Você precisa ser administrador do workspace da clínica no Flow e ter: * acesso ao menu **Integrações**; * o nome do ERP que será conectado; * alguém responsável pelo ERP, caso ele não seja Peti9; * um ambiente de testes do ERP, se ele tiver um. Um **workspace** é o espaço da sua clínica dentro do Flow. Cada clínica possui suas próprias chaves e dados. ## Passo 1: abra as integrações 1. Entre no Flow. 2. No menu lateral, clique em **Integrações**. 3. Abra a aba **Acesso à API**. Você verá uma explicação curta do processo e o botão para gerenciar as chaves da API. ## Passo 2: crie uma chave de acesso 1. Clique em **Gerenciar chaves**. 2. Clique em criar uma nova chave. 3. Dê um nome fácil de reconhecer, por exemplo `ERP Clínica Centro - produção`. 4. Marque as permissões: * `contacts:write` para criar e atualizar contatos; * `contacts:read` se o ERP também precisa consultar contatos; * `contacts:delete` somente se o ERP deve apagar contatos; * `satisfaction:write` se o ERP avisará quando um atendimento terminar. 5. Copie a chave exibida na tela e guarde-a em um cofre de senhas. A chave aparece uma única vez. Não envie a chave por WhatsApp, e-mail, print, planilha ou repositório de código. Se ela vazar, revogue-a e crie outra. ## Passo 3: envie estas informações ao responsável pelo ERP Envie: * a URL da API: `https://api.connectvets.com.br/flow/v1`; * a chave criada no passo anterior; * o nome da clínica/workspace; * o link desta página; * o [checklist de homologação](/flow/integration/erp-checklist). O responsável técnico deve primeiro chamar `GET /external/me` para descobrir o `workspace_id` correto. Depois, ele poderá criar ou atualizar um contato. ## Passo 4: faça um teste simples Peça para o responsável pelo ERP enviar um contato de teste, por exemplo: ```json theme={null} { "external_id": "cliente-teste-001", "name": "Cliente de teste", "phone": "+5511999999999", "document": "12345678901" } ``` Depois, abra **Contatos** no Flow e procure por “Cliente de teste”. Se o contato aparecer, a primeira parte da integração está pronta. ## O que acontece depois * Quando o ERP cria ou altera um contato, o Flow mantém o cadastro da clínica atualizado. * O campo `external_id` é o código do cliente dentro do seu ERP. Ele permite atualizar a mesma pessoa sem criar duplicatas. * A clínica pode escolher se o nome e telefone são controlados pelo Flow ou pelo ERP em **Integrações → Acesso à API**. * O Flow pode avisar o ERP quando contatos ou etapas de lead mudarem usando [webhooks](/flow/webhooks). ## Se algo não funcionar | Situação | O que conferir | | - | - | | A chave não funciona | Verifique se ela foi copiada inteira e se não foi revogada. | | Aparece erro 403 | Confirme que a chave pertence ao workspace da clínica e possui a permissão necessária. | | O contato não aparece | Confirme nome, telefone e `external_id`; o telefone deve incluir `+55`. | | O contato duplicou | Garanta que o ERP sempre envie o mesmo `external_id` para a mesma pessoa. | | O responsável técnico não sabe por onde começar | Envie o [checklist de homologação](/flow/integration/erp-checklist) e a [referência de contatos](/flow/api-reference/contacts). | ## Recursos para o responsável técnico * [Início rápido técnico](/flow/quickstart) * [Autenticação](/flow/authentication) * [Contatos](/flow/api-reference/contacts) * [Política de sincronização](/flow/api-reference/sync-policy) * [Pesquisa de satisfação](/flow/api-reference/satisfaction-surveys) * [Webhooks](/flow/webhooks) # Flow API Source: https://docs.connectvets.com.br/flow/introduction API externa do ConnectVets Flow para ERP, webhooks e agentes de IA # ConnectVets Flow API Documentação para quem vai integrar um sistema externo (ERP, middleware ou agente de IA) com o Flow. Os pedidos vão para o **backend (API gateway)**, não para a URL do CRM no navegador. Toda chamada exige workspace com billing ativo. ## O que você pode fazer Ideal para ERP: contatos, descoberta de workspace e disparo de pesquisa com contrato HTTP estável. O Flow avisa o seu sistema quando um contato muda ou um lead troca de etapa. Para agentes de IA (Claude, Cursor, etc.): cole a URL e a chave — sem escrever código. ## Base URL Não use a URL do front-end (CRM) nas integrações. | Ambiente | Base URL | | - | - | | Produção | `https://api.connectvets.com.br/flow/v1` | | Sandbox | `https://api-sandbox.connectvets.com.br/flow/v1` | | Local (flow na porta 8090) | `http://localhost:8090/api` | Exemplo de caminho completo: ``` https://api.connectvets.com.br/flow/v1/external/workspaces/{workspace_id}/contacts ``` No gateway o prefixo é `/flow/v1`; no servidor isso vira `/api/…`. ## Autenticação ```http theme={null} Authorization: Bearer flow_live_ ``` Crie a chave em **Integrações → Gerenciar chaves**. Cada chave pertence a **um único workspace**. Use [`GET /external/me`](/flow/api-reference/workspaces) para obter o `workspace_id`. Próximo passo: [Início rápido](/flow/quickstart). # MCP (agentes de IA) Source: https://docs.connectvets.com.br/flow/mcp Conectar Claude, Cursor e outros clientes MCP ao Flow # MCP — integração com agentes de IA O [Model Context Protocol](https://modelcontextprotocol.io) permite que um agente de IA consulte os dados do workspace no Flow. O cliente só cola a URL e a chave — o agente descobre sozinho as ferramentas disponíveis. ## Quando usar MCP Use quando o cliente quer perguntar em linguagem natural (ex.: “quais leads estão na etapa X?”) via Claude, Cursor, ChatGPT ou outro app compatível com MCP. ## MCP vs API REST ERP e sistemas que **gravam** dados devem usar a [API REST](/flow/api-reference/contacts). MCP é para **consulta via IA** (somente leitura na v1). ## Como configurar 1. Crie uma chave em **Integrações → Gerenciar chaves** com o scope `mcp:read` (pode combinar com outros scopes). 2. No app de IA, adicione um servidor MCP HTTP usando uma das URLs abaixo. 3. Envie o header `Authorization: Bearer flow_live_…` — o workspace é detectado pela chave (não precisa passar `workspace_id` na URL). ## URLs do servidor MCP | Ambiente | URL | | - | - | | Produção | `https://api.connectvets.com.br/flow/v1/mcp` | | Sandbox | `https://api-sandbox.connectvets.com.br/flow/v1/mcp` | | Local | `http://localhost:8090/api/mcp` | Transport: Streamable HTTP (stateless). ## Exemplo no Cursor Em **Settings → MCP**, adicione um servidor com configuração semelhante a esta (ajuste a URL do ambiente): ```json theme={null} { "mcpServers": { "flow": { "url": "https://api.connectvets.com.br/flow/v1/mcp", "headers": { "Authorization": "Bearer flow_live_" } } } } ``` ## Exemplo no Claude / outros clientes HTTP Clientes MCP com transporte HTTP (Streamable) usam um JSON parecido com: ```json theme={null} { "mcpServers": { "flow": { "type": "http", "url": "https://api.connectvets.com.br/flow/v1/mcp", "headers": { "Authorization": "Bearer flow_live_" } } } } ``` ## Ferramentas disponíveis (somente leitura) Depois de conectar, o agente lista e chama estas tools automaticamente, sempre limitadas ao workspace da chave: | Tool | O que faz | | - | - | | `list_leads` | Lista leads ativos — filtros opcionais: `search`, `funnel_id`, `stage_id`, `limit`, `offset` | | `get_lead` | Detalhe de um lead por `lead_id` (contato + etapa) | | `list_pipelines` | Lista funis (pipelines) do workspace | | `list_pipeline_stages` | Lista etapas de um funil (exige `funnel_id`) | | `search_contacts` | Busca contatos por nome, telefone, e-mail ou documento | | `list_calendar_events` | Lista eventos de calendário — filtros: `from`/`to` (RFC3339), `status`, `limit`, `offset` | | `list_agendas` | Lista agendas (calendários) do workspace | ## Exemplos de perguntas para o agente * “Liste os leads ativos do funil principal.” * “Busque o contato com telefone 11999998888.” * “Quais consultas estão agendadas esta semana?” * “Mostre as etapas do funil X.” ## Limites importantes * A v1 do MCP é **somente leitura** — não cria nem altera leads/contatos/agenda. * A chave define o workspace: o cliente nunca escolhe outro tenant. * Workspace com billing inativo também bloqueia o MCP (igual à API REST). # Início rápido Source: https://docs.connectvets.com.br/flow/quickstart Primeira integração com a API externa do Flow em quatro passos # Início rápido — Flow Quatro passos para a primeira integração funcionar. Se você é responsável pela clínica e não programa, comece pelo [passo a passo de integração com ERP](/flow/integration/erp-getting-started). Esta página é destinada a quem vai configurar o sistema. ## 1. Crie uma API key No Flow CRM: **Integrações → Acesso à API → Gerenciar chaves**. Escolha os scopes necessários, por exemplo: * `contacts:write` * `satisfaction:write` * `mcp:read` Copie o token `flow_live_…` na hora — ele só aparece uma vez. Guarde em um cofre de segredos. ## 2. Descubra o workspace ```bash theme={null} export FLOW_BASE_URL="https://api-sandbox.connectvets.com.br/flow/v1" export TOKEN="flow_live_" curl -sS "$FLOW_BASE_URL/external/me" \ -H "Authorization: Bearer $TOKEN" ``` A resposta inclui `workspace_id`. Guarde-o para as demais rotas (ou resolva dinamicamente com o mesmo `GET`). ## 3. Upsert de contato ```bash theme={null} export WORKSPACE_ID="" curl -sS -X PUT "$FLOW_BASE_URL/external/workspaces/$WORKSPACE_ID/contacts" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"external_id":"erp-1","document":"12345678901","name":"João","phone":"+5511987654321"}' ``` ## 4. Pesquisa, webhooks ou MCP * Pesquisa: [`POST …/satisfaction-surveys`](/flow/api-reference/satisfaction-surveys) * Webhooks: configure em Integrações → Webhooks ([guia](/flow/webhooks)) * Agentes de IA: [MCP](/flow/mcp) Importante: não use a URL do front-end (CRM) nas integrações. Use o gateway (`/flow/v1`). Workspace com billing inativo bloqueia a API (403). # Permissões (scopes) Source: https://docs.connectvets.com.br/flow/scopes Scopes das API keys do Flow e rotas correspondentes # Permissões (scopes) A chave só consegue o que os scopes permitem. Sem o scope certo a API responde **403**. | Scope | Rota | | - | - | | *(qualquer chave válida)* | `GET /external/me` | | `contacts:write` | `PUT /external/workspaces/:workspace_id/contacts` | | `contacts:read` | `GET /external/workspaces/:workspace_id/contacts/:contact_id` | | `contacts:delete` | `DELETE /external/workspaces/:workspace_id/contacts/:contact_id` | | `satisfaction:write` | `POST /external/workspaces/:workspace_id/satisfaction-surveys` | | `mcp:read` | `ANY /mcp` (servidor MCP somente leitura) | Todas as rotas externas também exigem **billing ativo** no workspace. # Webhooks Source: https://docs.connectvets.com.br/flow/webhooks Eventos de saída assinados do Flow para o seu sistema # Webhooks (saída) Configure em **Integrações → Acesso à API → Webhooks** (session, `workspaces:manage`). O Flow envia **POST JSON**. Valide a assinatura `X-Flow-Signature` (HMAC-SHA256 do corpo bruto). ## Requisitos da URL | Ambiente | Esquema | Destino | | - | - | - | | **Produção** | Somente `https://` | Host público com certificado TLS válido para o hostname | | **Sandbox** | `https://` ou `http://` | Host público na internet (sem dados reais) | Em **qualquer** ambiente o Flow rejeita `localhost`, `127.0.0.1`, redes privadas e metadata cloud. Redirects HTTP durante a entrega não são seguidos. HTTPS nunca ignora erros de certificado. ### Exemplo (Sandbox, HTTP público) ```text theme={null} http://fw1.example.com:53183/Api/ConnectVets/OnEventAsync ``` ### Testar na sua máquina local O Flow **não** consegue alcançar `http://localhost:...` nem IPs da sua LAN — esses endereços só existem na sua rede. Para desenvolver localmente, exponha o endpoint com um túnel HTTPS público, por exemplo: * [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/) * [ngrok](https://ngrok.com/) * [Svix Play / CLI](https://docs.svix.com/play) * [Hookdeck CLI](https://hookdeck.com/docs/use-cases/test-debug-localhost) Registre a URL pública gerada pelo túnel (em produção ela deve ser HTTPS). ## Eventos | Evento | Quando | | - | - | | `contact.created` | Quando um contato é criado | | `contact.updated` | Quando um contato é atualizado | | `contact.deleted` | Quando um contato é apagado (soft-delete) | | `lead.stage_changed` | Quando um lead muda de etapa no funil | | `message.received` | Quando uma mensagem recebida é persistida no CRM e a linha WhatsApp é identificada | | `message.sent` | Quando uma mensagem enviada é persistida no CRM e a linha WhatsApp é identificada | ## Envelope ```json theme={null} { "event": "contact.updated", "workspace_id": "uuid", "occurred_at": "2026-05-16T12:34:56Z", "data": { "contact_id": "uuid" } } ``` ```http theme={null} X-Flow-Signature: sha256= ``` **Verificação:** `hex = HMAC-SHA256(secret, raw_body)` — use os bytes brutos do JSON recebido, sem re-serializar. Falhas retentam com backoff; após falhas sustentadas o webhook pode ser desativado (`active=false`). As entregas têm semântica **at-least-once**. Use `data.id` ou `data.provider_message_id` para deduplicar no sistema consumidor. ## Diagnóstico de falhas Em **Integrações → Acesso à API → Webhooks → Últimas entregas**, o Flow mostra cada tentativa, o status HTTP e uma recomendação. Quando o diagnóstico confirma que a falha está no endpoint configurado, a interface indica o ajuste necessário. Em causas desconhecidas, o diagnóstico permanece neutro para investigação conjunta. | Diagnóstico | Origem provável | Ajuste recomendado | | - | - | - | | Certificado TLS para outro domínio | Endpoint do cliente | Instale um certificado válido para o hostname exato da URL, com a cadeia completa. | | Certificado expirado, não confiável ou incompleto | Endpoint do cliente | Renove ou corrija o certificado TLS e a cadeia emitida pela autoridade certificadora. | | Domínio não encontrado | Endpoint do cliente | Corrija o DNS público para que o hostname resolva para o servidor correto. | | Conexão recusada | Endpoint do cliente | Verifique o processo, a porta, firewall e proxy reverso. | | Tempo limite excedido | Endpoint do cliente | Garanta disponibilidade e responda rapidamente com HTTP 2xx. | | Resposta HTTP 4xx | Endpoint do cliente | Revise autenticação, validação e formato esperado pelo endpoint. | | Resposta HTTP 5xx | Endpoint do cliente | Investigue os logs e a disponibilidade do serviço receptor. | O Flow mantém a validação TLS em HTTPS. Não é seguro ignorar erros de certificado: isso poderia enviar dados a um servidor que não comprova ser o domínio configurado. No Sandbox, prefira HTTP público explícito se ainda não tiver certificado válido. ## Mensagens WhatsApp Os eventos de mensagem incluem o número conectado do workspace e o número do contato. `from_phone` e `to_phone` sempre representam o sentido real da mensagem. Para preservar esse contrato, mensagens sem uma conexão WhatsApp resolvida ou sem o número comercial da conexão não geram evento; o Flow registra a omissão em log para investigação. ```json theme={null} { "event": "message.received", "workspace_id": "uuid", "occurred_at": "2026-05-16T12:34:56Z", "data": { "id": "uuid", "provider_message_id": "wamid.example", "chat_id": "uuid", "connection_id": "uuid", "direction": "inbound", "business_phone": "5511999990000", "contact_phone": "5511988880000", "from_phone": "5511988880000", "to_phone": "5511999990000", "message_type": "textMessage", "content": "Olá, preciso de atendimento", "timestamp": 1778934896 } } ``` Em `message.sent`, `direction` é `outbound` e `from_phone` é o `business_phone`. Mensagens de mídia podem incluir `caption`, `media_url` e `mimetype`. `media_url` só é emitida quando o provedor informa uma URL HTTP(S); URLs do provedor podem expirar e não devem ser tratadas como armazenamento durável. # ConnectVets Docs Source: https://docs.connectvets.com.br/index Documentação das APIs públicas ConnectVets — Notes e Flow # Documentação ConnectVets APIs para integrar sistemas externos (ERP, middleware e agentes de IA) com os produtos ConnectVets. Transcrição e análise de consultas veterinárias. API REST com API Key (`X-API-KEY`). CRM e integrações externas: contatos, pesquisa de satisfação, webhooks e MCP. ## Ambientes | Produto | Produção | Sandbox | | - | - | - | | Notes | `https://api.connectvets.com.br/notes/v1` | `https://api-sandbox.connectvets.com.br/notes/v1` | | Flow | `https://api.connectvets.com.br/flow/v1` | `https://api-sandbox.connectvets.com.br/flow/v1` | Escolha a aba **Notes** ou **Flow** no topo para ver a documentação completa de cada produto. # Create Source: https://docs.connectvets.com.br/notes/api-reference/notes/create # Criar Notes > Envie áudio para transcrição e criação de nota veterinária ## Descrição Envia um arquivo de áudio para transcrição automática e criação de uma nova nota veterinária. O áudio será processado por IA e dividido em seções estruturadas. ## Requisitos de Áudio **Regras de ouro para obter a melhor performance e acurácia:** 1. Taxa de amostragem **16 kHz (16.000 Hz)** 2. **Mono** (1 canal) 3. **PCM linear sem compressão** — se precisar compactar, utilize **FLAC** (lossless) **Formato recomendado: "WAV – 16 kHz – 16-bit – mono"** ### Formatos Aceitos | Formato | Status | Qualidade | Observações | | - | - | - | - | | **WAV** | ✅ Recomendado | Excelente | PCM linear 16-bit, 16kHz, mono | | **FLAC** | ✅ Recomendado | Excelente | Compressão sem perda | | **MP3** | ⚠️ Aceito | Boa | Formato amplamente compatível | | **OGG** | ⚠️ Aceito | Boa | Compressão eficiente | | **M4A** | ⚠️ Aceito | Boa | Formato Apple/AAC | | **WebM** | ⚠️ Aceito | Boa | Formato web moderno | ## Parâmetros da Requisição ### Headers | Header | Obrigatório | Valor | | - | - | - | | `X-API-KEY` | ✅ Sim | Sua chave de API | | `Content-Type` | ✅ Sim | `multipart/form-data` | ### Body (multipart/form-data) | Campo | Tipo | Obrigatório | Descrição | | - | - | - | - | | `audio` | File | ✅ Sim | Arquivo de áudio (WAV/FLAC recomendado) | | `name` | String | ✅ Sim | Nome do paciente | | `external_id` | String | ❌ Não | ID do seu sistema para referência | | `gender` | Enum | ❌ Não | Sexo: `male`, `female`, `unidentified` (padrão) | | `metadata` | String (JSON) | ❌ Não | Metadados adicionais em formato JSON | ### Exemplo de Metadados ```json theme={null} theme={null} [ {"key": "procedimento", "value": "vacina"}, {"key": "peso", "value": "12.5kg"}, {"key": "idade", "value": "3 anos"}, {"key": "veterinario", "value": "Dr. João Silva"} ] ``` ```bash cURL theme={null} theme={null} curl -X POST https://api.connectvets.com.br/notes \ -H "X-API-KEY: cvn_live_abc123def456..." \ -F "audio=@consulta_rex_20240214.wav" \ -F "name=Rex" \ -F "external_id=CLIENTE_123" \ -F "gender=male" \ -F 'metadata=[{"key":"procedimento","value":"vacina"}]' ``` ```javascript theme={null} theme={null} const formData = new FormData(); formData.append('audio', audioFile); formData.append('name', 'Rex'); formData.append('external_id', 'CLIENTE_123'); formData.append('gender', 'male'); formData.append('metadata', JSON.stringify([ {"key": "procedimento", "value": "vacina"} ])); const response = await fetch('https://api.connectvets.com.br/notes', { method: 'POST', headers: { 'X-API-KEY': 'cvn_live_abc123def456...' }, body: formData }); const note = await response.json(); ``` ```python theme={null} theme={null} import requests url = "https://api.connectvets.com.br/notes" headers = { "X-API-KEY": "cvn_live_abc123def456..." } files = { 'audio': ('consulta_rex.wav', open('consulta_rex.wav', 'rb'), 'audio/wav') } data = { 'name': 'Rex', 'external_id': 'CLIENTE_123', 'gender': 'male', 'metadata': '[{"key":"procedimento","value":"vacina"}]' } response = requests.post(url, headers=headers, files=files, data=data) note = response.json() ``` ```json Response theme={null} theme={null} { "id": "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3", "name": "Rex", "gender": "male", "audio_name": "consulta_rex_20240214.wav", "audio_url": "https://cdn.connectvets.com.br/audio/6a4fe1de.wav", "transcription_status": "pending", "transcription_url": null, "external_id": "CLIENTE_123", "metadata": [ {"key": "procedimento", "value": "vacina"} ], "note_sections": [], "created_at": "2024-02-14T18:25:43Z", "updated_at": "2024-02-14T18:25:43Z" } ``` ### Status da Transcrição A nota é criada imediatamente com status `pending`. O processamento acontece de forma assíncrona: | Status | Descrição | Tempo Estimado | | - | - | - | | `pending` | Aguardando processamento | Imediato | | `processing` | IA transcrevendo o áudio | 2-5 minutos | | `completed` | Transcrição finalizada | - | | `failed` | Erro no processamento | - | ### Erros Comuns #### 400 Bad Request ```json theme={null} theme={null} { "error": "bad_request", "message": "Audio file is required", "code": "MISSING_AUDIO_FILE" } ``` **Causas possíveis:** * Arquivo de áudio não enviado * Nome do paciente não informado * Formato de metadata inválido #### 401 Unauthorized ```json theme={null} theme={null} { "error": "unauthorized", "message": "API key is required", "code": "MISSING_API_KEY" } ``` #### 402 Payment Required ```json theme={null} theme={null} { "error": "payment_required", "message": "Subscription inactive or limit exceeded", "code": "SUBSCRIPTION_INACTIVE" } ``` #### 413 Payload Too Large ```json theme={null} theme={null} { "error": "payload_too_large", "message": "Audio file exceeds maximum size of 100MB", "code": "FILE_TOO_LARGE" } ``` #### 422 Unprocessable Entity ```json theme={null} theme={null} { "error": "unprocessable_entity", "message": "Invalid audio format", "code": "INVALID_AUDIO_FORMAT", "details": { "expected": "audio/wav, audio/flac", "received": "audio/mp3" } } ``` ## Monitoramento do Processamento ### Webhook (Recomendado) Configure webhooks para receber notificações automáticas: ```json theme={null} theme={null} { "event": "note.completed", "data": { "id": "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3", "name": "Rex", "transcription_status": "completed", "external_id": "CLIENTE_123" }, "timestamp": "2024-02-14T18:30:23Z" } ``` ### Polling Consulte periodicamente o status da nota: ```javascript theme={null} theme={null} async function waitForCompletion(noteId) { let status = 'pending'; while (status === 'pending' || status === 'processing') { await new Promise(resolve => setTimeout(resolve, 30000)); // 30s const response = await fetch(`/notes/${noteId}`, { headers: { 'X-API-KEY': apiKey } }); const note = await response.json(); status = note.transcription_status; if (status === 'completed') { console.log('Transcrição finalizada!'); console.log('Seções:', note.note_sections.length); break; } else if (status === 'failed') { console.error('Erro no processamento'); break; } } } ``` ## Limites e Restrições ### Limites de Upload | Limite | Valor | Observação | | - | - | - | | **Tamanho máximo** | 100 MB | Por arquivo | | **Duração máxima** | 2 horas | Áudio muito longo pode falhar | | **Formato** | WAV, FLAC, MP3, OGG, M4A, WebM | WAV/FLAC recomendados | | **Taxa de upload** | 10 por minuto | Por API Key | ### Limites por Plano | Plano | Notas/mês | Tamanho | Retenção | | - | - | - | - | | **Gratuito** | 10 | 50 MB | 30 dias | | **Profissional** | 500 | 100 MB | 1 ano | | **Enterprise** | Ilimitado | 200 MB | Customizável | ## Melhores Práticas ### 🎙️ Qualidade do Áudio * **Ambiente silencioso**: Minimize ruído de fundo * **Distância adequada**: 30-50cm do microfone * **Volume consistente**: Evite variações extremas * **Fala clara**: Pronuncie bem as palavras técnicas ### 🔧 Integração * **Use external\_id**: Para vincular com seu sistema * **Configure webhooks**: Para notificações em tempo real * **Trate erros**: Implemente retry com backoff exponencial * **Monitore limites**: Acompanhe uso da API ### 📊 Metadados Úteis ```json theme={null} theme={null} [ {"key": "procedimento", "value": "consulta de rotina"}, {"key": "veterinario", "value": "Dr. João Silva"}, {"key": "peso", "value": "12.5kg"}, {"key": "temperatura", "value": "38.5°C"}, {"key": "pressao", "value": "normal"}, {"key": "observacoes", "value": "animal cooperativo"} ] ``` ## Próximos Passos Como buscar e filtrar suas notas Acesse detalhes e seções da nota Receba notificações automáticas Exemplos de busca e filtros ## OpenAPI ```yaml POST /notes theme={null} openapi: 3.0.3 info: title: ConnectVets Notes API description: API para transcrição e análise de consultas veterinárias version: 1.0.0 contact: name: ConnectVets Support email: suporte@connectvets.com.br url: https://connectvets.com.br license: name: Proprietary url: https://connectvets.com.br/terms servers: - url: https://api-sandbox.connectvets.com.br/notes/v1 description: Homologação security: - ApiKeyAuth: [] paths: /notes: post: tags: - Notes summary: Criar nova nota description: > Envie áudio para transcrição e criação de nota veterinária. **Rate Limiting**: Este endpoint está sujeito aos limites da sua API Key (padrão: 100 req/min, 1000 req/hora). operationId: createNote requestBody: required: true content: multipart/form-data: schema: type: object required: - audio - name properties: audio: type: string format: binary description: Arquivo de áudio (WAV/FLAC recomendado) example: consulta_rex_20240214.wav name: type: string description: Nome do paciente example: Rex external_id: type: string description: ID do seu sistema para referência example: CLIENTE_123 gender: type: string enum: - male - female - unidentified description: Sexo do paciente example: male metadata: type: string description: Metadados adicionais em formato JSON example: '[{"key":"procedimento","value":"vacina"}]' responses: '201': description: Nota criada com sucesso content: application/json: schema: $ref: '#/components/schemas/Note' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '422': $ref: '#/components/responses/UnprocessableEntity' components: schemas: Note: type: object properties: id: type: string format: uuid description: ID único da nota example: 6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3 name: type: string description: Nome do paciente example: Rex gender: type: string enum: - male - female - unidentified description: Sexo do paciente example: male audio_name: type: string description: Nome do arquivo de áudio example: consulta_rex_20240214.wav audio_url: type: string format: uri description: URL do arquivo de áudio example: https://cdn.connectvets.com/audio/6a4fe1de.wav transcription_status: type: string enum: - pending - processing - completed - failed description: Status da transcrição example: completed transcription_url: type: string format: uri nullable: true description: URL da transcrição example: https://cdn.connectvets.com/transcriptions/6a4fe1de.txt external_id: type: string nullable: true description: ID externo do seu sistema example: CLIENTE_123 metadata: type: array items: $ref: '#/components/schemas/Metadata' description: Metadados adicionais created_at: type: string format: date-time description: Data de criação example: '2024-02-14T18:25:43Z' updated_at: type: string format: date-time description: Data de atualização example: '2024-02-14T18:30:23Z' Metadata: type: object properties: key: type: string description: Chave do metadado example: procedimento value: type: string description: Valor do metadado example: vacina Error: type: object properties: error: type: string description: Código do erro example: bad_request message: type: string description: Mensagem de erro example: Invalid audio format code: type: string description: Código específico example: INVALID_AUDIO_FORMAT details: type: object description: Detalhes adicionais do erro responses: BadRequest: description: Requisição inválida content: application/json: schema: $ref: '#/components/schemas/Error' example: error: bad_request message: Invalid audio format code: INVALID_AUDIO_FORMAT details: expected: audio/wav received: audio/mp3 Unauthorized: description: Não autorizado content: application/json: schema: $ref: '#/components/schemas/Error' example: error: unauthorized message: API Key missing or invalid code: INVALID_API_KEY UnprocessableEntity: description: Entidade não processável content: application/json: schema: $ref: '#/components/schemas/Error' example: error: unprocessable_entity message: Audio file is too large code: AUDIO_TOO_LARGE securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-KEY description: API Key para autenticação ``` # Export Source: https://docs.connectvets.com.br/notes/api-reference/notes/export # Exportar Nota > Exporta o prontuário completo em diferentes formatos para download direto. **Requisito**: O export só está disponível para notas com `transcription_status: "completed"`. **Rate Limiting**: Este endpoint está sujeito aos limites da sua API Key (padrão: 100 req/min, 1000 req/hora). ## Descrição Exporta o prontuário completo de uma nota em diferentes formatos para download direto. O export inclui todas as seções estruturadas da nota formatadas apropriadamente. **Requisito**: O export só está disponível para notas com `transcription_status: "completed"` ## Parâmetros da Requisição ### Path Parameters | Parâmetro | Tipo | Obrigatório | Descrição | | - | - | - | - | | `noteId` | UUID | ✅ Sim | ID único da nota | ### Headers | Header | Obrigatório | Valor | | - | - | - | | `X-API-KEY` | ✅ Sim | Sua chave de API | ### Query Parameters | Parâmetro | Tipo | Obrigatório | Descrição | Padrão | | - | - | - | - | - | | `format` | String | ❌ Não | Formato de export: `rtf` ou `txt` | `txt` | ## Formatos Disponíveis ### TXT (Texto Simples) * **Content-Type**: `text/plain; charset=utf-8` * **Estrutura**: Texto simples e estruturado com hierarquia * **Codificação**: UTF-8 * **Uso**: Compatibilidade máxima, fácil integração ### RTF (Rich Text Format) * **Content-Type**: `application/rtf` * **Estrutura**: Formatação rica com cores e fontes * **Características**: * Títulos em azul e negrito * Seções organizadas com cores diferentes * Fontes apropriadas (Arial para títulos, Times New Roman para conteúdo) * **Uso**: Documentos profissionais, compatível com Word ## Exemplos de Requisição ### Exportar em formato TXT (padrão) ```bash theme={null} theme={null} curl -H "X-API-KEY: cvn_live_abc123def456..." \ -O -J \ https://api.connectvets.com/notes/6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3/export ``` ### Exportar em formato RTF ```bash theme={null} theme={null} curl -H "X-API-KEY: cvn_live_abc123def456..." \ -O -J \ "https://api.connectvets.com/notes/6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3/export?format=rtf" ``` ### JavaScript/TypeScript ```javascript theme={null} theme={null} async function exportNote(noteId, format = 'txt') { const response = await fetch(`https://api.connectvets.com/notes/${noteId}/export?format=${format}`, { headers: { 'X-API-KEY': 'cvn_live_abc123def456...' } }); if (!response.ok) { throw new Error(`Erro no export: ${response.status}`); } // Obter nome do arquivo do header const contentDisposition = response.headers.get('Content-Disposition'); const filename = contentDisposition?.match(/filename="(.+)"/)?.[1] || `nota_${noteId}.${format}`; // Criar download const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } // Uso await exportNote('6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3', 'rtf'); ``` ### Python ```python theme={null} theme={null} import requests def export_note(api_key, note_id, format='txt'): url = f"https://api.connectvets.com/notes/{note_id}/export" headers = {"X-API-KEY": api_key} params = {"format": format} response = requests.get(url, headers=headers, params=params) if response.status_code == 200: # Obter nome do arquivo content_disposition = response.headers.get('Content-Disposition', '') filename = content_disposition.split('filename="')[1].split('"')[0] if 'filename=' in content_disposition else f'nota_{note_id}.{format}' # Salvar arquivo with open(filename, 'wb') as f: f.write(response.content) print(f"Nota exportada: {filename}") return filename else: print(f"Erro no export: {response.status_code}") return None # Uso api_key = "cvn_live_abc123def456..." note_id = "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3" # Export em TXT txt_file = export_note(api_key, note_id, 'txt') # Export em RTF rtf_file = export_note(api_key, note_id, 'rtf') ``` ## Resposta de Sucesso (200 OK) ### Headers de Resposta ```http theme={null} theme={null} Content-Type: text/plain; charset=utf-8 Content-Disposition: attachment; filename="Rex_prontuario.txt" Content-Length: 2048 ``` ```http theme={null} theme={null} Content-Type: application/rtf Content-Disposition: attachment; filename="Rex_prontuario.rtf" Content-Length: 4096 ``` ### Exemplo de Conteúdo TXT ```text theme={null} theme={null} ============================================== PRONTUÁRIO VETERINÁRIO - Rex ============================================== Data da Consulta: 2024-02-14T18:25:43Z Paciente: Rex Sexo: Macho ID Externo: CLIENTE_123 ============================================== ANAMNESE ============================================== Paciente apresenta histórico de vacinação em dia... ============================================== EXAME FÍSICO ============================================== Peso: 12kg Frequência Cardíaca: 90 bpm Temperatura: 38.5°C... ============================================== DIAGNÓSTICO ============================================== Diagnóstico principal: Consulta de rotina... ============================================== TRATAMENTO ============================================== Recomendações: - Manter vacinação em dia - Retorno em 6 meses... ``` ## Tratamento de Erros ### 400 Bad Request ```json theme={null} theme={null} { "error": "bad_request", "message": "Invalid format parameter", "code": "INVALID_FORMAT", "details": { "allowed_formats": ["txt", "rtf"], "received": "pdf" } } ``` ### 404 Not Found ```json theme={null} theme={null} { "error": "not_found", "message": "Note not found", "code": "NOTE_NOT_FOUND" } ``` ### 422 Unprocessable Entity ```json theme={null} theme={null} { "error": "unprocessable_entity", "message": "Note transcription not completed", "code": "TRANSCRIPTION_NOT_COMPLETED", "details": { "current_status": "processing", "required_status": "completed" } } ``` ## Casos de Uso ### Export Automático após Processamento ```javascript theme={null} theme={null} async function processAndExport(audioFile, metadata) { // 1. Criar nota const note = await createNote(audioFile, metadata); // 2. Aguardar conclusão await waitForCompletion(note.id); // 3. Exportar automaticamente await exportNote(note.id, 'rtf'); console.log('Processo completo: nota criada e exportada'); } ``` ### Export em Lote ```javascript theme={null} theme={null} async function bulkExport(noteIds, format = 'txt') { const results = []; for (const noteId of noteIds) { try { await exportNote(noteId, format); results.push({ noteId, status: 'success' }); } catch (error) { results.push({ noteId, status: 'error', error: error.message }); } } return results; } ``` ### Integração com Sistema Local ```javascript theme={null} theme={null} async function syncAndExport(noteId) { // Verificar se nota está completa const status = await getNoteStatus(noteId); if (status.status !== 'completed') { throw new Error('Nota ainda não foi processada'); } // Exportar ambos os formatos const [txtFile, rtfFile] = await Promise.all([ exportNote(noteId, 'txt'), exportNote(noteId, 'rtf') ]); // Salvar referências no sistema local await saveExportReferences(noteId, { txtFile, rtfFile }); return { txtFile, rtfFile }; } ``` ## Boas Práticas ### 📄 Escolha do Formato * **TXT**: Use para integrações automatizadas, processamento de texto, compatibilidade máxima * **RTF**: Use para documentos profissionais, apresentações, quando formatação é importante ### 🔄 Cache e Performance * Cache exports localmente quando possível * Use o export apenas quando necessário (após `transcription_status: completed`) * Para múltiplos exports, considere fazer em lote com controle de rate ### 📁 Organização de Arquivos ```javascript theme={null} theme={null} // Estrutura de pastas recomendada const exportPath = `exports/${date}/${patientName}_${noteId}.${format}`; // Nomenclatura consistente const filename = `${patientName}_${consultationDate}_${noteId}.${format}`; ``` ### ⚠️ Tratamento de Erros ```javascript theme={null} theme={null} async function safeExport(noteId, format) { try { // Verificar status primeiro const status = await getNoteStatus(noteId); if (status.status !== 'completed') { return { error: 'Nota não processada', status: status.status }; } // Fazer export const filename = await exportNote(noteId, format); return { success: true, filename }; } catch (error) { return { error: error.message }; } } ``` ## OpenAPI ```yaml GET /notes/{noteId}/export theme={null} openapi: 3.0.3 info: title: ConnectVets Notes API description: API para transcrição e análise de consultas veterinárias version: 1.0.0 contact: name: ConnectVets Support email: suporte@connectvets.com.br url: https://connectvets.com.br license: name: Proprietary url: https://connectvets.com.br/terms servers: - url: https://api-sandbox.connectvets.com.br/notes/v1 description: Homologação security: - ApiKeyAuth: [] paths: /notes/{noteId}/export: get: tags: - Notes summary: Exportar nota description: > Exporta o prontuário completo em diferentes formatos para download direto. **Requisito**: O export só está disponível para notas com `transcription_status: "completed"`. **Rate Limiting**: Este endpoint está sujeito aos limites da sua API Key (padrão: 100 req/min, 1000 req/hora). operationId: exportNote parameters: - name: noteId in: path required: true schema: type: string format: uuid description: ID único da nota example: 550e8400-e29b-41d4-a716-446655440000 - name: format in: query required: false schema: type: string enum: - txt - rtf default: txt description: Formato de export example: rtf responses: '200': description: Arquivo exportado com sucesso content: text/plain: schema: type: string format: binary description: Arquivo TXT com prontuário example: | PRONTUÁRIO MÉDICO ================ Paciente: Rex Anamnese -------- Paciente apresenta histórico de... application/rtf: schema: type: string format: binary description: Arquivo RTF com prontuário formatado headers: Content-Disposition: description: Nome do arquivo para download schema: type: string example: attachment; filename="Rex_prontuario.txt" Content-Type: description: Tipo de conteúdo baseado no formato schema: type: string examples: txt: value: text/plain; charset=utf-8 rtf: value: application/rtf '400': description: Parâmetros inválidos content: application/json: schema: $ref: '#/components/schemas/Error' examples: invalid_format: summary: Formato inválido value: error: bad_request message: Invalid format parameter code: INVALID_FORMAT details: allowed_formats: - txt - rtf received: pdf invalid_note_id: summary: ID inválido value: error: bad_request message: Invalid note id code: INVALID_NOTE_ID '401': $ref: '#/components/responses/Unauthorized' '403': description: Permissões insuficientes content: application/json: schema: $ref: '#/components/schemas/Error' example: error: forbidden message: Note does not belong to tenant code: NOTE_ACCESS_DENIED '404': $ref: '#/components/responses/NotFound' '422': description: Nota não processada content: application/json: schema: $ref: '#/components/schemas/Error' example: error: unprocessable_entity message: Note transcription not completed code: TRANSCRIPTION_NOT_COMPLETED details: current_status: processing required_status: completed components: schemas: Error: type: object properties: error: type: string description: Código do erro example: bad_request message: type: string description: Mensagem de erro example: Invalid audio format code: type: string description: Código específico example: INVALID_AUDIO_FORMAT details: type: object description: Detalhes adicionais do erro responses: Unauthorized: description: Não autorizado content: application/json: schema: $ref: '#/components/schemas/Error' example: error: unauthorized message: API Key missing or invalid code: INVALID_API_KEY NotFound: description: Recurso não encontrado content: application/json: schema: $ref: '#/components/schemas/Error' example: error: not_found message: Note not found code: NOTE_NOT_FOUND securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-KEY description: API Key para autenticação ``` # Get Source: https://docs.connectvets.com.br/notes/api-reference/notes/get # Listar Note Especifico > Acesse detalhes completos de uma nota, incluindo seções estruturadas ## Descrição Retorna os detalhes completos de uma nota específica, incluindo: * **Transcrição completa** (quando disponível) * **Seções estruturadas** (anamnese, exame físico, diagnóstico, etc.) * **Metadados** e informações do áudio * **URLs de download** do áudio e transcrição As **seções estruturadas** (`note_sections`) estão disponíveis **apenas** neste endpoint. Elas não são retornadas na listagem `GET /notes` ## Parâmetros da Requisição ### Path Parameters | Parâmetro | Tipo | Obrigatório | Descrição | | - | - | - | - | | `id` | UUID | ✅ Sim | ID único da nota | ### Headers | Header | Obrigatório | Valor | | - | - | - | | `X-API-KEY` | ✅ Sim | Sua chave de API | ## Exemplos de Requisição ### cURL ```bash theme={null} theme={null} curl -H "X-API-KEY: cvn_live_abc123def456..." \ https://api.connectvets.com/notes/6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3 ``` ### JavaScript/TypeScript ```javascript theme={null} theme={null} async function getNoteById(noteId) { const response = await fetch(`https://api.connectvets.com/notes/${noteId}`, { headers: { 'X-API-KEY': 'cvn_live_abc123def456...' } }); if (!response.ok) { throw new Error(`Nota não encontrada: ${response.status}`); } return response.json(); } // Uso const note = await getNoteById('6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3'); console.log(`Nota: ${note.name}`); console.log(`Status: ${note.transcription_status}`); console.log(`Seções: ${note.note_sections.length}`); ``` ### Python ```python theme={null} theme={null} import requests def get_note_by_id(api_key, note_id): url = f"https://api.connectvets.com/notes/{note_id}" headers = {"X-API-KEY": api_key} response = requests.get(url, headers=headers) if response.status_code == 404: raise ValueError(f"Nota {note_id} não encontrada") response.raise_for_status() return response.json() # Uso api_key = "cvn_live_abc123def456..." note_id = "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3" note = get_note_by_id(api_key, note_id) print(f"Paciente: {note['name']}") print(f"Status: {note['transcription_status']}") if note['note_sections']: print("Seções encontradas:") for section in note['note_sections']: print(f"- {section['title']}: {len(section['content'])} caracteres") ``` ### Go ```go theme={null} theme={null} package main import ( "encoding/json" "fmt" "net/http" ) func getNoteByID(apiKey, noteID string) (*Note, error) { url := fmt.Sprintf("https://api.connectvets.com/notes/%s", noteID) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("X-API-KEY", apiKey) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode == 404 { return nil, fmt.Errorf("nota %s não encontrada", noteID) } if resp.StatusCode != 200 { return nil, fmt.Errorf("erro %d: %s", resp.StatusCode, resp.Status) } var note Note err = json.NewDecoder(resp.Body).Decode(¬e) return ¬e, err } // Uso note, err := getNoteByID("cvn_live_abc123def456...", "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3") if err != nil { log.Fatal(err) } fmt.Printf("Paciente: %s\n", note.Name) fmt.Printf("Seções: %d\n", len(note.NoteSections)) ``` ## Resposta de Sucesso (200 OK) ```json theme={null} theme={null} { "id": "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3", "name": "Rex", "gender": "male", "audio_name": "rex_visit_20240214.wav", "audio_url": "https://cdn.connectvets.com/audio/6a4fe1de.wav", "transcription_status": "completed", "transcription_url": "https://cdn.connectvets.com/transcripts/6a4fe1de.txt", "external_id": "CLIENTE_123", "metadata": [ {"key": "procedimento", "value": "vacina"}, {"key": "veterinario", "value": "Dr. João Silva"}, {"key": "peso", "value": "12.5kg"} ], "note_sections": [ { "id": "9b7d8b6a-12e3-45fa-9c1c-7e12f5c4a1b2", "title": "Anamnese", "label": "anamnesis", "content": "Paciente Rex, cão macho de 3 anos, da raça Golden Retriever. Proprietário relata que o animal tem apresentado diminuição do apetite nos últimos 3 dias. Não houve vômitos ou diarréia. Animal ativo, mas menos que o habitual.", "order": "1" }, { "id": "0c0d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f", "title": "Exame Físico", "label": "physical_exam", "content": "Peso: 28kg. Temperatura: 38.8°C. Frequência cardíaca: 90 bpm. Frequência respiratória: 24 mpm. Mucosas rosadas e úmidas. Linfonodos normais. Abdômen sem alterações à palpação.", "order": "2" }, { "id": "1d1e2f3g-4b5c-6d7e-8f9g-0a1b2c3d4e5f", "title": "Diagnóstico", "label": "diagnosis", "content": "Suspeita de gastrite leve. Possivelmente relacionada à mudança de alimentação recente. Recomendado dieta leve e observação.", "order": "3" }, { "id": "2e2f3g4h-5c6d-7e8f-9g0h-1b2c3d4e5f6g", "title": "Tratamento", "label": "treatment", "content": "Prescrito omeprazol 20mg, 1 comprimido pela manhã por 7 dias. Dieta com ração gastro intestinal. Retorno em 1 semana ou se piorar dos sintomas.", "order": "4" } ], "created_at": "2024-02-14T18:25:43Z", "updated_at": "2024-02-14T18:30:23Z" } ``` ## Seções Estruturadas ### Tipos de Seções Comuns | Label | Título | Descrição | | - | - | - | | `anamnesis` | Anamnese | Histórico e relato do proprietário | | `physical_exam` | Exame Físico | Avaliação física e sinais vitais | | `diagnosis` | Diagnóstico | Conclusões e diagnósticos | | `treatment` | Tratamento | Prescrições e recomendações | | `observations` | Observações | Notas adicionais | | `follow_up` | Seguimento | Instruções de retorno | ### Estrutura das Seções | Campo | Tipo | Descrição | | - | - | - | | `id` | UUID | Identificador único da seção | | `title` | String | Nome exibido da seção | | `label` | String | Identificador interno (para programação) | | `content` | String | Conteúdo transcrito da seção | | `order` | String | Ordem de exibição (1, 2, 3...) | ## Estados da Nota ### Nota Pendente/Processando ```json theme={null} theme={null} { "id": "7b5fe2ef-63d5-5c3c-b41f-5c4fa8e8d9c4", "name": "Luna", "transcription_status": "processing", "transcription_url": null, "note_sections": [], "created_at": "2024-02-15T09:15:22Z", "updated_at": "2024-02-15T09:16:10Z" } ``` Quando o status é `pending` ou `processing`, os campos `transcription_url` e `note_sections` estarão vazios ou nulos ### Nota com Erro ```json theme={null} theme={null} { "id": "8c6fe3f0-74e6-6d4d-c52f-6d5fa9f9e0d5", "name": "Milo", "transcription_status": "failed", "transcription_url": null, "note_sections": [], "created_at": "2024-02-16T14:20:15Z", "updated_at": "2024-02-16T14:22:30Z" } ``` ## Casos de Uso Práticos ### Exibir Detalhes no Frontend ```javascript theme={null} theme={null} // Componente React para exibir nota function NoteDetails({ noteId }) { const [note, setNote] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { async function loadNote() { try { const noteData = await getNoteById(noteId); setNote(noteData); } catch (error) { console.error('Erro ao carregar nota:', error); } finally { setLoading(false); } } loadNote(); }, [noteId]); if (loading) return
Carregando...
; if (!note) return
Nota não encontrada
; return (

{note.name}

Status: {note.transcription_status}

{note.note_sections.map(section => (

{section.title}

{section.content}

))}
); } ``` ### Baixar Transcrição ```javascript theme={null} theme={null} // Baixar arquivo de transcrição async function downloadTranscription(note) { if (note.transcription_status !== 'completed' || !note.transcription_url) { throw new Error('Transcrição não disponível'); } const response = await fetch(note.transcription_url); const text = await response.text(); // Criar download const blob = new Blob([text], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${note.name}_transcricao.txt`; a.click(); URL.revokeObjectURL(url); } ``` ### Buscar Seção Específica ```javascript theme={null} theme={null} // Encontrar seção por label function findSection(note, label) { return note.note_sections.find(section => section.label === label); } // Extrair informações específicas function extractKeyInfo(note) { const anamnesis = findSection(note, 'anamnesis'); const diagnosis = findSection(note, 'diagnosis'); const treatment = findSection(note, 'treatment'); return { paciente: note.name, problema: anamnesis?.content.substring(0, 100) + '...', diagnostico: diagnosis?.content || 'Não informado', tratamento: treatment?.content || 'Não informado' }; } ``` ### Gerar Relatório ```javascript theme={null} theme={null} // Gerar relatório estruturado function generateReport(note) { if (note.transcription_status !== 'completed') { return 'Nota ainda não processada'; } let report = `RELATÓRIO VETERINÁRIO\n\n`; report += `Paciente: ${note.name}\n`; report += `Data: ${new Date(note.created_at).toLocaleDateString('pt-BR')}\n`; // Metadados if (note.metadata.length > 0) { report += `\nInformações Adicionais:\n`; note.metadata.forEach(meta => { report += `- ${meta.key}: ${meta.value}\n`; }); } report += `\n${'='.repeat(50)}\n\n`; // Seções ordenadas const sections = note.note_sections.sort((a, b) => parseInt(a.order) - parseInt(b.order) ); sections.forEach(section => { report += `${section.title.toUpperCase()}\n`; report += `${'-'.repeat(section.title.length)}\n`; report += `${section.content}\n\n`; }); return report; } ``` ### Sincronização com Sistema Local ```javascript theme={null} theme={null} // Sincronizar nota com banco local async function syncNoteToLocal(noteId) { try { const remoteNote = await getNoteById(noteId); // Verificar se há atualizações const localNote = await getLocalNote(noteId); if (localNote && localNote.updated_at >= remoteNote.updated_at) { return { updated: false, note: localNote }; } // Salvar/atualizar no banco local await saveLocalNote(remoteNote); return { updated: true, note: remoteNote }; } catch (error) { console.error(`Erro ao sincronizar nota ${noteId}:`, error); throw error; } } ``` ## Tratamento de Erros ### 404 Not Found ```json theme={null} theme={null} { "error": "not_found", "message": "Note not found", "code": "NOTE_NOT_FOUND" } ``` **Causas:** * ID da nota inexistente * Nota pertence a outro tenant/clínica * Nota foi deletada ### 401 Unauthorized ```json theme={null} theme={null} { "error": "unauthorized", "message": "API key is required", "code": "MISSING_API_KEY" } ``` ### Exemplo de Tratamento ```javascript theme={null} theme={null} async function safeGetNote(noteId) { try { const response = await fetch(`/notes/${noteId}`, { headers: { 'X-API-KEY': apiKey } }); if (response.status === 404) { return { error: 'Nota não encontrada', note: null }; } if (response.status === 401) { return { error: 'Acesso negado', note: null }; } if (!response.ok) { return { error: `Erro ${response.status}`, note: null }; } const note = await response.json(); return { error: null, note }; } catch (error) { return { error: error.message, note: null }; } } // Uso com tratamento const { error, note } = await safeGetNote(noteId); if (error) { console.error(error); } else { console.log('Nota carregada:', note.name); } ``` ## Downloads e Arquivos ### Baixar Áudio Original ```javascript theme={null} theme={null} async function downloadAudio(note) { const response = await fetch(note.audio_url); const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = note.audio_name; a.click(); URL.revokeObjectURL(url); } ``` ### Verificar Disponibilidade ```javascript theme={null} theme={null} function checkAvailability(note) { return { audio: !!note.audio_url, transcription: note.transcription_status === 'completed' && !!note.transcription_url, sections: note.note_sections.length > 0, completed: note.transcription_status === 'completed' }; } const availability = checkAvailability(note); if (availability.transcription) { console.log('Transcrição disponível para download'); } ``` ## Performance e Cache ### Cache Inteligente ```javascript theme={null} theme={null} // Cache com TTL para notas completadas const noteCache = new Map(); async function getCachedNote(noteId) { const cached = noteCache.get(noteId); // Cache hit para notas completadas (não mudam) if (cached && cached.transcription_status === 'completed') { return cached; } // Cache hit com TTL para notas em processamento if (cached && Date.now() - cached._cached_at < 30000) { // 30s return cached; } // Cache miss - buscar nova versão const note = await getNoteById(noteId); note._cached_at = Date.now(); noteCache.set(noteId, note); return note; } ``` ## Próximos Passos Busque e filtre suas notas Envie áudio para transcrição Edite conteúdo das seções Entenda como funcionam as notas ## OpenAPI ```yaml GET /notes/{noteId} theme={null} openapi: 3.0.3 info: title: ConnectVets Notes API description: API para transcrição e análise de consultas veterinárias version: 1.0.0 contact: name: ConnectVets Support email: suporte@connectvets.com.br url: https://connectvets.com.br license: name: Proprietary url: https://connectvets.com.br/terms servers: - url: https://api-sandbox.connectvets.com.br/notes/v1 description: Homologação security: - ApiKeyAuth: [] paths: /notes/{noteId}: get: tags: - Notes summary: Obter nota específica description: > Recuperar uma nota pelo ID. **Rate Limiting**: Este endpoint está sujeito aos limites da sua API Key (padrão: 100 req/min, 1000 req/hora). operationId: getNote parameters: - name: noteId in: path required: true schema: type: string format: uuid description: ID da nota example: 550e8400-e29b-41d4-a716-446655440000 responses: '200': description: Nota encontrada content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/NoteWithSections' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' components: schemas: NoteWithSections: allOf: - $ref: '#/components/schemas/Note' - type: object properties: note_sections: type: array items: $ref: '#/components/schemas/NoteSection' description: Seções estruturadas da nota Note: type: object properties: id: type: string format: uuid description: ID único da nota example: 6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3 name: type: string description: Nome do paciente example: Rex gender: type: string enum: - male - female - unidentified description: Sexo do paciente example: male audio_name: type: string description: Nome do arquivo de áudio example: consulta_rex_20240214.wav audio_url: type: string format: uri description: URL do arquivo de áudio example: https://cdn.connectvets.com/audio/6a4fe1de.wav transcription_status: type: string enum: - pending - processing - completed - failed description: Status da transcrição example: completed transcription_url: type: string format: uri nullable: true description: URL da transcrição example: https://cdn.connectvets.com/transcriptions/6a4fe1de.txt external_id: type: string nullable: true description: ID externo do seu sistema example: CLIENTE_123 metadata: type: array items: $ref: '#/components/schemas/Metadata' description: Metadados adicionais created_at: type: string format: date-time description: Data de criação example: '2024-02-14T18:25:43Z' updated_at: type: string format: date-time description: Data de atualização example: '2024-02-14T18:30:23Z' NoteSection: type: object properties: id: type: string format: uuid description: ID da seção example: abc123-def456-ghi789 title: type: string description: Título da seção example: Anamnese label: type: string description: Label da seção example: anamnese content: type: string description: Conteúdo da seção example: Paciente apresenta sintomas de... order: type: string description: Ordem da seção example: '1' Error: type: object properties: error: type: string description: Código do erro example: bad_request message: type: string description: Mensagem de erro example: Invalid audio format code: type: string description: Código específico example: INVALID_AUDIO_FORMAT details: type: object description: Detalhes adicionais do erro Metadata: type: object properties: key: type: string description: Chave do metadado example: procedimento value: type: string description: Valor do metadado example: vacina responses: Unauthorized: description: Não autorizado content: application/json: schema: $ref: '#/components/schemas/Error' example: error: unauthorized message: API Key missing or invalid code: INVALID_API_KEY NotFound: description: Recurso não encontrado content: application/json: schema: $ref: '#/components/schemas/Error' example: error: not_found message: Note not found code: NOTE_NOT_FOUND securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-KEY description: API Key para autenticação ``` # List Source: https://docs.connectvets.com.br/notes/api-reference/notes/list # Listar Notes > Obter lista paginada de notas. **Rate Limiting**: Este endpoint está sujeito aos limites da sua API Key (padrão: 100 req/min, 1000 req/hora). ## Descrição Retorna uma lista paginada de notas do tenant/clínica associado à sua API Key. Suporta filtros avançados para buscar notas específicas. ## Parâmetros da Requisição ### Headers | Header | Obrigatório | Valor | | - | - | - | | `X-API-KEY` | ✅ Sim | Sua chave de API | ### Query Parameters | Parâmetro | Tipo | Obrigatório | Descrição | Exemplo | | - | - | - | - | - | | `external_id` | String | ❌ Não | Filtrar por ID externo | `CLIENTE_123` | | `transcription_status` | Enum | ❌ Não | Status da transcrição | `completed` | | `valid` | Boolean | ❌ Não | Filtrar notas válidas/inválidas | `true` | | `name` | String | ❌ Não | Nome do paciente (busca parcial) | `rex` | | `from` | Date | ❌ Não | Data inicial (YYYY-MM-DD) | `2024-01-01` | | `to` | Date | ❌ Não | Data final (YYYY-MM-DD) | `2024-01-31` | | `page` | Integer | ❌ Não | Número da página (padrão: 1) | `2` | | `per_page` | Integer | ❌ Não | Itens por página (padrão: 20, máx: 100) | `50` | ### Valores de transcription\_status | Status | Descrição | | - | - | | `pending` | Aguardando processamento | | `processing` | Em processamento pela IA | | `completed` | Transcrição finalizada | | `failed` | Erro no processamento | ## Exemplos de Requisição ### Listar todas as notas (básico) ```bash theme={null} theme={null} curl -H "X-API-KEY: cvn_live_abc123def456..." \ https://api.connectvets.com/notes ``` ### Filtrar por status completado ```bash theme={null} theme={null} curl -H "X-API-KEY: cvn_live_abc123def456..." \ "https://api.connectvets.com/notes?transcription_status=completed" ``` ### Buscar por nome do paciente ```bash theme={null} theme={null} curl -H "X-API-KEY: cvn_live_abc123def456..." \ "https://api.connectvets.com/notes?name=rex" ``` ### Filtrar por período ```bash theme={null} theme={null} curl -H "X-API-KEY: cvn_live_abc123def456..." \ "https://api.connectvets.com/notes?from=2024-01-01&to=2024-01-31" ``` ### Filtros combinados com paginação ```bash theme={null} theme={null} curl -H "X-API-KEY: cvn_live_abc123def456..." \ "https://api.connectvets.com/notes?transcription_status=completed&from=2024-01-01&page=2&per_page=50" ``` ### JavaScript/TypeScript ```javascript theme={null} theme={null} // Função básica de listagem async function listNotes(filters = {}) { const params = new URLSearchParams(); // Adicionar filtros Object.entries(filters).forEach(([key, value]) => { if (value !== undefined && value !== null) { params.append(key, value); } }); const url = `https://api.connectvets.com/notes${params.toString() ? '?' + params.toString() : ''}`; const response = await fetch(url, { headers: { 'X-API-KEY': 'cvn_live_abc123def456...' } }); return response.json(); } // Exemplos de uso const allNotes = await listNotes(); const completedNotes = await listNotes({ transcription_status: 'completed' }); const monthlyNotes = await listNotes({ from: '2024-01-01', to: '2024-01-31', page: 1, per_page: 50 }); const patientNotes = await listNotes({ name: 'rex', transcription_status: 'completed' }); ``` ### Python ```python theme={null} theme={null} import requests from datetime import datetime, timedelta def list_notes(api_key, **filters): url = "https://api.connectvets.com/notes" headers = {"X-API-KEY": api_key} # Remover valores None params = {k: v for k, v in filters.items() if v is not None} response = requests.get(url, headers=headers, params=params) return response.json() # Exemplos de uso api_key = "cvn_live_abc123def456..." # Todas as notas all_notes = list_notes(api_key) # Notas completadas completed = list_notes( api_key, transcription_status="completed" ) # Notas do último mês last_month = datetime.now() - timedelta(days=30) recent_notes = list_notes( api_key, from_date=last_month.strftime("%Y-%m-%d"), to_date=datetime.now().strftime("%Y-%m-%d") ) # Busca específica search_results = list_notes( api_key, name="rex", external_id="CLIENTE_123" ) ``` ### Go ```go theme={null} theme={null} package main import ( "encoding/json" "fmt" "net/http" "net/url" ) type NotesFilter struct { ExternalID string `json:"external_id,omitempty"` TranscriptionStatus string `json:"transcription_status,omitempty"` Valid *bool `json:"valid,omitempty"` Name string `json:"name,omitempty"` From string `json:"from,omitempty"` To string `json:"to,omitempty"` Page int `json:"page,omitempty"` PerPage int `json:"per_page,omitempty"` } func listNotes(apiKey string, filters NotesFilter) (*NotesListResponse, error) { baseURL := "https://api.connectvets.com/notes" // Construir query parameters params := url.Values{} if filters.ExternalID != "" { params.Add("external_id", filters.ExternalID) } if filters.TranscriptionStatus != "" { params.Add("transcription_status", filters.TranscriptionStatus) } if filters.Name != "" { params.Add("name", filters.Name) } if filters.From != "" { params.Add("from", filters.From) } if filters.To != "" { params.Add("to", filters.To) } if filters.Page > 0 { params.Add("page", fmt.Sprintf("%d", filters.Page)) } if filters.PerPage > 0 { params.Add("per_page", fmt.Sprintf("%d", filters.PerPage)) } url := baseURL if len(params) > 0 { url += "?" + params.Encode() } req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("X-API-KEY", apiKey) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var result NotesListResponse err = json.NewDecoder(resp.Body).Decode(&result) return &result, err } ``` ## Resposta de Sucesso (200 OK) ```json theme={null} theme={null} { "pagination": { "current_page": 1, "total_pages": 10, "total_items": 200 }, "notes": [ { "id": "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3", "name": "Rex", "gender": "male", "audio_name": "rex_visit_20240214.wav", "transcription_status": "completed", "external_id": "CLIENTE_123", "metadata": [ {"key": "procedimento", "value": "vacina"}, {"key": "veterinario", "value": "Dr. João Silva"} ], "created_at": "2024-02-14T18:25:43Z", "updated_at": "2024-02-14T18:30:23Z" }, { "id": "7b5fe2ef-63d5-5c3c-b41f-5c4fa8e8d9c4", "name": "Luna", "gender": "female", "audio_name": "luna_consulta_20240215.wav", "transcription_status": "processing", "external_id": "CLIENTE_456", "metadata": [ {"key": "procedimento", "value": "consulta de rotina"} ], "created_at": "2024-02-15T09:15:22Z", "updated_at": "2024-02-15T09:16:10Z" } ] } ``` ### Estrutura da Resposta | Campo | Tipo | Descrição | | - | - | - | | `pagination` | Object | Informações de paginação | | `pagination.current_page` | Integer | Página atual | | `pagination.total_pages` | Integer | Total de páginas | | `pagination.total_items` | Integer | Total de itens | | `notes` | Array | Lista de notas | **Importante**: O campo `note_sections` **não é incluído** na listagem. Para obter as seções, use o endpoint `GET /notes/{id}` ## Paginação ### Navegação entre Páginas ```javascript theme={null} theme={null} // Função para navegar pelas páginas async function getAllNotes(filters = {}) { let allNotes = []; let currentPage = 1; let totalPages = 1; do { const response = await listNotes({ ...filters, page: currentPage, per_page: 100 // máximo por página }); allNotes = allNotes.concat(response.notes); currentPage++; totalPages = response.pagination.total_pages; } while (currentPage <= totalPages); return allNotes; } ``` ### Controle de Paginação ```javascript theme={null} theme={null} // Hook React para paginação function useNotesPagination(filters = {}) { const [notes, setNotes] = useState([]); const [pagination, setPagination] = useState({}); const [loading, setLoading] = useState(false); const loadPage = async (page = 1) => { setLoading(true); try { const response = await listNotes({ ...filters, page, per_page: 20 }); setNotes(response.notes); setPagination(response.pagination); } catch (error) { console.error('Erro ao carregar notas:', error); } finally { setLoading(false); } }; return { notes, pagination, loading, loadPage }; } ``` ## Filtros Avançados ### Busca por Texto ```javascript theme={null} theme={null} // Busca inteligente no nome do paciente async function searchPatients(query) { return listNotes({ name: query.toLowerCase(), transcription_status: 'completed' }); } // Busca por ID externo async function findByExternalId(externalId) { const response = await listNotes({ external_id: externalId }); return response.notes.length > 0 ? response.notes[0] : null; } ``` ### Filtros por Data ```javascript theme={null} theme={null} // Notas de hoje async function getTodayNotes() { const today = new Date().toISOString().split('T')[0]; return listNotes({ from: today, to: today }); } // Notas da última semana async function getWeeklyNotes() { const today = new Date(); const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); return listNotes({ from: weekAgo.toISOString().split('T')[0], to: today.toISOString().split('T')[0] }); } // Notas por período customizado async function getNotesInPeriod(startDate, endDate) { return listNotes({ from: startDate, to: endDate, transcription_status: 'completed' }); } ``` ### Filtros por Status ```javascript theme={null} theme={null} // Notas pendentes de processamento async function getPendingNotes() { return listNotes({ transcription_status: 'pending' }); } // Notas com erro async function getFailedNotes() { return listNotes({ transcription_status: 'failed' }); } // Notas em processamento async function getProcessingNotes() { return listNotes({ transcription_status: 'processing' }); } ``` ## Casos de Uso Comuns ### Dashboard/Relatórios ```javascript theme={null} theme={null} // Estatísticas do painel async function getDashboardStats() { const [total, completed, pending, failed] = await Promise.all([ listNotes({ per_page: 1 }), // para pegar total_items listNotes({ transcription_status: 'completed', per_page: 1 }), listNotes({ transcription_status: 'pending', per_page: 1 }), listNotes({ transcription_status: 'failed', per_page: 1 }) ]); return { total: total.pagination.total_items, completed: completed.pagination.total_items, pending: pending.pagination.total_items, failed: failed.pagination.total_items }; } ``` ### Sincronização ```javascript theme={null} theme={null} // Sincronizar notas modificadas desde última consulta async function syncNotesSince(lastSyncDate) { const response = await listNotes({ from: lastSyncDate, per_page: 100 }); // Processar notas atualizadas for (const note of response.notes) { await updateLocalNote(note); } return response.notes; } ``` ### Busca de Paciente ```javascript theme={null} theme={null} // Buscar histórico de um paciente async function getPatientHistory(patientName) { const response = await listNotes({ name: patientName, transcription_status: 'completed' }); // Ordenar por data mais recente return response.notes.sort((a, b) => new Date(b.created_at) - new Date(a.created_at) ); } ``` ## Tratamento de Erros ### Códigos de Status | Status | Descrição | Ação | | - | - | - | | `200` | Sucesso | Processar resultados | | `400` | Parâmetros inválidos | Verificar filtros | | `401` | API Key inválida | Verificar autenticação | | `429` | Rate limit excedido | Aguardar e tentar novamente | ### Exemplo de Tratamento ```javascript theme={null} theme={null} async function safeListNotes(filters = {}) { try { const response = await fetch(url, { headers }); if (!response.ok) { switch (response.status) { case 400: throw new Error('Filtros inválidos'); case 401: throw new Error('API Key inválida'); case 429: throw new Error('Muitas requisições. Tente em alguns minutos.'); default: throw new Error(`Erro ${response.status}: ${response.statusText}`); } } return await response.json(); } catch (error) { console.error('Erro ao listar notas:', error); throw error; } } ``` ## Performance e Melhores Práticas ### 🚀 Otimização * **Use paginação**: Não carregue todas as notas de uma vez * **Filtros específicos**: Reduza o volume de dados * **Cache inteligente**: Cache resultados frequentes * **Polling eficiente**: Use webhooks ao invés de polling constante ### 📊 Monitoramento ```javascript theme={null} theme={null} // Monitorar status das notas async function monitorNoteStatus() { const processing = await listNotes({ transcription_status: 'processing' }); console.log(`${processing.pagination.total_items} notas em processamento`); const failed = await listNotes({ transcription_status: 'failed' }); if (failed.pagination.total_items > 0) { console.warn(`${failed.pagination.total_items} notas falharam`); } } ``` ## Próximos Passos Acesse detalhes completos e seções da nota Envie áudio para transcrição Exemplos complexos de busca Entenda como funcionam as notas ## OpenAPI ```yaml GET /notes theme={null} openapi: 3.0.3 info: title: ConnectVets Notes API description: API para transcrição e análise de consultas veterinárias version: 1.0.0 contact: name: ConnectVets Support email: suporte@connectvets.com.br url: https://connectvets.com.br license: name: Proprietary url: https://connectvets.com.br/terms servers: - url: https://api-sandbox.connectvets.com.br/notes/v1 description: Homologação security: - ApiKeyAuth: [] paths: /notes: get: tags: - Notes summary: Listar notas description: > Obter lista paginada de notas. **Rate Limiting**: Este endpoint está sujeito aos limites da sua API Key (padrão: 100 req/min, 1000 req/hora). operationId: listNotes parameters: - name: page in: query schema: type: integer default: 1 description: Número da página example: 1 - name: per_page in: query schema: type: integer default: 20 maximum: 100 description: Itens por página example: 20 - name: external_id in: query schema: type: string description: Filtrar por ID externo example: CLIENTE_123 - name: transcription_status in: query schema: type: string enum: - pending - processing - completed - failed description: Filtrar por status de transcrição example: completed - name: name in: query schema: type: string description: Busca parcial por nome do paciente example: Rex - name: from in: query schema: type: string format: date description: Data inicial (YYYY-MM-DD) example: '2024-01-01' - name: to in: query schema: type: string format: date description: Data final (YYYY-MM-DD) example: '2024-12-31' responses: '200': description: Lista de notas content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/Note' pagination: $ref: '#/components/schemas/Pagination' '401': $ref: '#/components/responses/Unauthorized' components: schemas: Note: type: object properties: id: type: string format: uuid description: ID único da nota example: 6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3 name: type: string description: Nome do paciente example: Rex gender: type: string enum: - male - female - unidentified description: Sexo do paciente example: male audio_name: type: string description: Nome do arquivo de áudio example: consulta_rex_20240214.wav audio_url: type: string format: uri description: URL do arquivo de áudio example: https://cdn.connectvets.com/audio/6a4fe1de.wav transcription_status: type: string enum: - pending - processing - completed - failed description: Status da transcrição example: completed transcription_url: type: string format: uri nullable: true description: URL da transcrição example: https://cdn.connectvets.com/transcriptions/6a4fe1de.txt external_id: type: string nullable: true description: ID externo do seu sistema example: CLIENTE_123 metadata: type: array items: $ref: '#/components/schemas/Metadata' description: Metadados adicionais created_at: type: string format: date-time description: Data de criação example: '2024-02-14T18:25:43Z' updated_at: type: string format: date-time description: Data de atualização example: '2024-02-14T18:30:23Z' Pagination: type: object properties: current_page: type: integer description: Página atual example: 1 total_pages: type: integer description: Total de páginas example: 10 total_items: type: integer description: Total de itens example: 200 per_page: type: integer description: Itens por página example: 20 Metadata: type: object properties: key: type: string description: Chave do metadado example: procedimento value: type: string description: Valor do metadado example: vacina Error: type: object properties: error: type: string description: Código do erro example: bad_request message: type: string description: Mensagem de erro example: Invalid audio format code: type: string description: Código específico example: INVALID_AUDIO_FORMAT details: type: object description: Detalhes adicionais do erro responses: Unauthorized: description: Não autorizado content: application/json: schema: $ref: '#/components/schemas/Error' example: error: unauthorized message: API Key missing or invalid code: INVALID_API_KEY securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-KEY description: API Key para autenticação ``` # Retry Source: https://docs.connectvets.com.br/notes/api-reference/notes/retry # Retry Transcrição > Reprocessa uma nota que falhou na transcrição ou que você deseja transcrever novamente ## Descrição Inicia uma nova tentativa de processamento para uma nota que falhou na transcrição ou que você deseja reprocessar. Útil para casos onde: * A transcrição falhou devido a problemas temporários * Você deseja reprocessar uma nota com melhorias na IA * Houve problemas na qualidade da primeira transcrição **Compatibilidade**: Este endpoint permite retry para notas com status `failed`, `completed` ou `pending`. Não é possível fazer retry de uma nota que já está com status `processing`. ## Parâmetros da Requisição ### Headers | Header | Obrigatório | Valor | | - | - | - | | `X-API-KEY` | ✅ Sim | Sua chave de API | | `Content-Type` | ✅ Sim | `application/json` | ### Body (JSON) | Campo | Tipo | Obrigatório | Descrição | | - | - | - | - | | `note_id` | UUID | ✅ Sim | ID da nota que será reprocessada | ## Exemplos de Requisição ### cURL ```bash theme={null} theme={null} curl -X POST https://api.connectvets.com/transcripts/retry \ -H "X-API-KEY: cvn_live_abc123def456..." \ -H "Content-Type: application/json" \ -d '{ "note_id": "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3" }' ``` ### JavaScript/TypeScript ```javascript theme={null} theme={null} async function retryTranscription(noteId) { const response = await fetch('https://api.connectvets.com/transcripts/retry', { method: 'POST', headers: { 'X-API-KEY': 'cvn_live_abc123def456...', 'Content-Type': 'application/json' }, body: JSON.stringify({ note_id: noteId }) }); if (!response.ok) { throw new Error(`Erro no retry: ${response.status}`); } const result = await response.json(); return result; } // Uso try { const result = await retryTranscription('6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3'); console.log('Retry iniciado:', result.status); console.log('URL da transcrição:', result.transcription); } catch (error) { console.error('Erro:', error.message); } ``` ### Python ```python theme={null} theme={null} import requests import json def retry_transcription(api_key, note_id): url = "https://api.connectvets.com/transcripts/retry" headers = { "X-API-KEY": api_key, "Content-Type": "application/json" } data = { "note_id": note_id } response = requests.post(url, headers=headers, json=data) if response.status_code == 200: result = response.json() print(f"Retry iniciado para nota {note_id}") print(f"Status: {result['status']}") print(f"Transcrição: {result['transcription']}") return result else: print(f"Erro: {response.status_code}") print(response.text) return None # Uso api_key = "cvn_live_abc123def456..." note_id = "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3" result = retry_transcription(api_key, note_id) ``` ### Go ```go theme={null} theme={null} package main import ( "bytes" "encoding/json" "fmt" "net/http" ) type RetryRequest struct { NoteID string `json:"note_id"` } type RetryResponse struct { NoteID string `json:"note_id"` Status string `json:"status"` Message string `json:"message"` Transcription string `json:"transcription"` } func retryTranscription(apiKey, noteID string) (*RetryResponse, error) { url := "https://api.connectvets.com/transcripts/retry" requestBody := RetryRequest{ NoteID: noteID, } jsonData, err := json.Marshal(requestBody) if err != nil { return nil, err } req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, err } req.Header.Set("X-API-KEY", apiKey) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("erro %d: %s", resp.StatusCode, resp.Status) } var result RetryResponse err = json.NewDecoder(resp.Body).Decode(&result) return &result, err } // Uso result, err := retryTranscription("cvn_live_abc123def456...", "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3") if err != nil { log.Fatal(err) } fmt.Printf("Status: %s\n", result.Status) fmt.Printf("Mensagem: %s\n", result.Message) ``` ## Resposta de Sucesso (200 OK) ```json theme={null} theme={null} { "note_id": "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3", "status": "processing", "message": "Transcription retry initiated successfully", "transcription": "https://cdn.connectvets.com/notes/6a4fe1de.txt" } ``` | Campo | Tipo | Descrição | | - | - | - | | `note_id` | UUID | ID da nota que está sendo reprocessada | | `status` | String | Novo status da nota (sempre "processing") | | `message` | String | Mensagem de confirmação | | `transcription` | String | URL onde a nova transcrição ficará disponível | ## Tratamento de Erros ### 400 Bad Request ```json theme={null} theme={null} { "error": "bad_request", "message": "Invalid note_id format", "code": "INVALID_NOTE_ID" } ``` **Causas:** * `note_id` ausente no corpo da requisição * `note_id` com formato inválido (não é UUID) * Corpo da requisição malformado ### 400 Bad Request - Já em Processamento ```json theme={null} theme={null} { "error": "bad_request", "message": "transcription is already being processed", "code": "TRANSCRIPTION_IN_PROGRESS" } ``` **Causa:** A nota já está com status `processing` e não pode ser reprocessada novamente até a conclusão. ### 403 Forbidden ```json theme={null} theme={null} { "error": "forbidden", "message": "Note does not belong to current tenant", "code": "NOTE_ACCESS_DENIED" } ``` **Causa:** A nota não pertence ao tenant/clínica associado à API Key utilizada. ### 404 Not Found ```json theme={null} theme={null} { "error": "not_found", "message": "Note not found", "code": "NOTE_NOT_FOUND" } ``` **Causa:** Nota com o ID fornecido não existe. ## Casos de Uso ### Retry Automático para Notas Falhadas ```javascript theme={null} theme={null} async function retryFailedNotes() { // Buscar notas com status failed const failedNotes = await listNotes({ transcription_status: 'failed' }); console.log(`Encontradas ${failedNotes.notes.length} notas falhadas`); const retryResults = []; for (const note of failedNotes.notes) { try { const result = await retryTranscription(note.id); retryResults.push({ noteId: note.id, status: 'success', newStatus: result.status }); console.log(`✅ Retry iniciado para nota ${note.id}`); // Aguardar um pouco entre retries await new Promise(resolve => setTimeout(resolve, 1000)); } catch (error) { retryResults.push({ noteId: note.id, status: 'error', error: error.message }); console.error(`❌ Erro no retry da nota ${note.id}:`, error.message); } } return retryResults; } ``` ### Retry com Monitoramento ```javascript theme={null} theme={null} async function retryAndMonitor(noteId) { try { // Iniciar retry const retryResult = await retryTranscription(noteId); console.log(`🔄 Retry iniciado para nota ${noteId}`); // Monitorar progresso let status = 'processing'; let attempts = 0; const maxAttempts = 60; // 5 minutos máximo while (status === 'processing' && attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 5000)); // 5s const statusResult = await getNoteStatus(noteId); status = statusResult.status; attempts++; console.log(`📊 Tentativa ${attempts}: Status ${status}`); if (status === 'completed') { console.log('✅ Retry concluído com sucesso!'); return { success: true, status }; } else if (status === 'failed') { console.log('❌ Retry falhou novamente'); return { success: false, status }; } } if (attempts >= maxAttempts) { console.log('⏰ Timeout: Retry demorou muito'); return { success: false, status: 'timeout' }; } } catch (error) { console.error('❌ Erro no retry:', error.message); return { success: false, error: error.message }; } } ``` ### Retry Inteligente com Backoff ```javascript theme={null} theme={null} async function intelligentRetry(noteId, maxRetries = 3) { let attempt = 0; while (attempt < maxRetries) { try { // Verificar status atual const status = await getNoteStatus(noteId); if (status.status === 'processing') { console.log('⏳ Nota ainda em processamento, aguardando...'); await new Promise(resolve => setTimeout(resolve, 30000)); continue; } if (status.status === 'completed') { console.log('✅ Nota já processada com sucesso'); return { success: true, alreadyCompleted: true }; } // Iniciar retry await retryTranscription(noteId); console.log(`🔄 Retry ${attempt + 1}/${maxRetries} iniciado`); // Aguardar processamento const result = await waitForCompletion(noteId); if (result.status === 'completed') { console.log('✅ Retry bem-sucedido!'); return { success: true, attempt: attempt + 1 }; } attempt++; if (attempt < maxRetries) { // Backoff exponencial: 2^attempt minutos const delayMinutes = Math.pow(2, attempt); console.log(`⏰ Aguardando ${delayMinutes} minutos antes do próximo retry...`); await new Promise(resolve => setTimeout(resolve, delayMinutes * 60 * 1000)); } } catch (error) { console.error(`❌ Erro no retry ${attempt + 1}:`, error.message); attempt++; if (attempt < maxRetries) { await new Promise(resolve => setTimeout(resolve, 60000)); // 1 minuto } } } console.log(`❌ Falha após ${maxRetries} tentativas`); return { success: false, attempts: maxRetries }; } ``` ## Monitoramento após Retry ### Verificação de Status Após iniciar um retry, use o endpoint [`/notes/{id}/status`](/notes/api-reference/notes/status) para monitorar o progresso: ```javascript theme={null} theme={null} async function monitorRetry(noteId) { console.log('📊 Monitorando retry...'); let status = 'processing'; let checkCount = 0; while (status === 'processing') { await new Promise(resolve => setTimeout(resolve, 10000)); // 10s const result = await getNoteStatus(noteId); status = result.status; checkCount++; console.log(`Verificação ${checkCount}: ${status}`); if (status === 'completed') { console.log('🎉 Retry concluído!'); break; } else if (status === 'failed') { console.log('💥 Retry falhou'); break; } // Timeout após 10 minutos if (checkCount > 60) { console.log('⏰ Timeout do monitoramento'); break; } } return status; } ``` ## Boas Práticas ### 🔄 Gestão de Retries * **Limite tentativas**: Não faça retry infinitamente * **Use backoff**: Aguarde entre tentativas com delay crescente * **Monitore status**: Verifique se a nota já está em processamento * **Log adequadamente**: Registre tentativas e resultados ### ⏰ Timing * **Aguarde falhas**: Não faça retry imediatamente após falha * **Respeite processamento**: Não tente retry em notas `processing` * **Use polling inteligente**: Interval de 5-10s para verificar status ### 🚨 Tratamento de Erros ```javascript theme={null} theme={null} function shouldRetry(error) { // Não retry em casos irrecuperáveis const nonRetryableErrors = [ 'NOTE_NOT_FOUND', 'NOTE_ACCESS_DENIED', 'INVALID_NOTE_ID' ]; return !nonRetryableErrors.includes(error.code); } async function safeRetry(noteId) { try { return await retryTranscription(noteId); } catch (error) { if (shouldRetry(error)) { console.log('Erro temporário, pode tentar novamente'); } else { console.log('Erro permanente, não tentar novamente'); } throw error; } } ``` ### 📈 Métricas Monitore métricas importantes: * Taxa de sucesso dos retries * Tempo médio de processamento * Notas que falharam múltiplas vezes * Patterns de falhas para feedback à equipe ## OpenAPI ```yaml POST /transcripts/retry theme={null} openapi: 3.0.3 info: title: ConnectVets Notes API description: API para transcrição e análise de consultas veterinárias version: 1.0.0 contact: name: ConnectVets Support email: suporte@connectvets.com.br url: https://connectvets.com.br license: name: Proprietary url: https://connectvets.com.br/terms servers: - url: https://api-sandbox.connectvets.com.br/notes/v1 description: Homologação security: - ApiKeyAuth: [] paths: /transcripts/retry: post: tags: - Notes summary: Reprocessar nota falhada description: > Reinicia o processamento de transcrição para uma nota que falhou. **Requisito**: O retry só está disponível para notas com `transcription_status: "failed"`. **Rate Limiting**: Este endpoint está sujeito aos limites da sua API Key (padrão: 100 req/min, 1000 req/hora). operationId: retryNote parameters: - name: noteId in: path required: true schema: type: string format: uuid description: ID único da nota para reprocessar example: 550e8400-e29b-41d4-a716-446655440000 requestBody: required: false content: application/json: schema: type: object description: Body vazio para reprocessamento example: {} responses: '200': description: Reprocessamento iniciado com sucesso content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/Note' message: type: string description: Mensagem de confirmação example: Note reprocessing started successfully example: data: id: 550e8400-e29b-41d4-a716-446655440000 name: Rex transcription_status: pending created_at: '2024-02-14T18:25:43Z' updated_at: '2024-02-14T19:15:30Z' message: Note reprocessing started successfully '400': description: Parâmetros inválidos content: application/json: schema: $ref: '#/components/schemas/Error' example: error: bad_request message: Invalid note id format code: INVALID_NOTE_ID '401': $ref: '#/components/responses/Unauthorized' '403': description: Permissões insuficientes content: application/json: schema: $ref: '#/components/schemas/Error' example: error: forbidden message: Note does not belong to tenant code: NOTE_ACCESS_DENIED '404': $ref: '#/components/responses/NotFound' '422': description: Nota não pode ser reprocessada content: application/json: schema: $ref: '#/components/schemas/Error' examples: not_failed: summary: Status não é 'failed' value: error: unprocessable_entity message: Note can only be retried when status is 'failed' code: INVALID_RETRY_STATUS details: current_status: completed required_status: failed already_processing: summary: Já está sendo processado value: error: unprocessable_entity message: Note is already being processed code: NOTE_ALREADY_PROCESSING details: current_status: processing components: schemas: Note: type: object properties: id: type: string format: uuid description: ID único da nota example: 6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3 name: type: string description: Nome do paciente example: Rex gender: type: string enum: - male - female - unidentified description: Sexo do paciente example: male audio_name: type: string description: Nome do arquivo de áudio example: consulta_rex_20240214.wav audio_url: type: string format: uri description: URL do arquivo de áudio example: https://cdn.connectvets.com/audio/6a4fe1de.wav transcription_status: type: string enum: - pending - processing - completed - failed description: Status da transcrição example: completed transcription_url: type: string format: uri nullable: true description: URL da transcrição example: https://cdn.connectvets.com/transcriptions/6a4fe1de.txt external_id: type: string nullable: true description: ID externo do seu sistema example: CLIENTE_123 metadata: type: array items: $ref: '#/components/schemas/Metadata' description: Metadados adicionais created_at: type: string format: date-time description: Data de criação example: '2024-02-14T18:25:43Z' updated_at: type: string format: date-time description: Data de atualização example: '2024-02-14T18:30:23Z' Error: type: object properties: error: type: string description: Código do erro example: bad_request message: type: string description: Mensagem de erro example: Invalid audio format code: type: string description: Código específico example: INVALID_AUDIO_FORMAT details: type: object description: Detalhes adicionais do erro Metadata: type: object properties: key: type: string description: Chave do metadado example: procedimento value: type: string description: Valor do metadado example: vacina responses: Unauthorized: description: Não autorizado content: application/json: schema: $ref: '#/components/schemas/Error' example: error: unauthorized message: API Key missing or invalid code: INVALID_API_KEY NotFound: description: Recurso não encontrado content: application/json: schema: $ref: '#/components/schemas/Error' example: error: not_found message: Note not found code: NOTE_NOT_FOUND securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-KEY description: API Key para autenticação ``` # Status Source: https://docs.connectvets.com.br/notes/api-reference/notes/status # Obter Status da Nota > Retorna o status atual de transcrição de uma nota específica. **Rate Limiting**: ⚡ Este endpoint **NÃO possui rate limiting**, mas recomendamos fazer chamadas com intervalo de pelo menos 5 segundos para evitar sobrecarga desnecessária. ## Casos de Uso Este endpoint é ideal para: * **Polling de status**: ⚡ Verificar periodicamente se uma transcrição foi concluída (sem rate limiting!) * **Integração assíncrona**: Aguardar conclusão antes de buscar dados completos * **Monitoramento**: Acompanhar o progresso de processamento em lote * **UI responsiva**: Atualizar interface com status em tempo real ### Exemplo de Polling Eficiente ```javascript theme={null} theme={null} async function pollNoteStatus(noteId, maxAttempts = 60) { for (let attempt = 0; attempt < maxAttempts; attempt++) { const response = await fetch(`/notes/${noteId}/status`, { headers: { 'X-API-KEY': 'sua_api_key' } }); const { data } = await response.json(); if (data.status === 'completed' || data.status === 'failed') { return data; } // Aguardar 5 segundos antes da próxima verificação await new Promise(resolve => setTimeout(resolve, 5000)); } throw new Error('Timeout: Status polling exceeded maximum attempts'); } ``` ## Notas Importantes **Rate Limiting**: ⚡ Este endpoint **NÃO possui rate limiting**, permitindo verificações frequentes quando necessário. **Recomendação**: Mesmo sem rate limiting, implemente um intervalo de pelo menos **5 segundos** entre verificações de status da mesma nota para evitar sobrecarga desnecessária do servidor. Notas com status `completed` podem ainda ter `valid: false` se falharam na validação. Use o endpoint `/notes/{noteId}` para obter detalhes sobre possíveis erros. ## OpenAPI ```yaml GET /notes/{noteId}/status theme={null} openapi: 3.0.3 info: title: ConnectVets Notes API description: API para transcrição e análise de consultas veterinárias version: 1.0.0 contact: name: ConnectVets Support email: suporte@connectvets.com.br url: https://connectvets.com.br license: name: Proprietary url: https://connectvets.com.br/terms servers: - url: https://api-sandbox.connectvets.com.br/notes/v1 description: Homologação security: - ApiKeyAuth: [] paths: /notes/{noteId}/status: get: tags: - Notes summary: Obter status da nota description: > Retorna o status atual de transcrição de uma nota específica. **Rate Limiting**: ⚡ Este endpoint **NÃO possui rate limiting**, mas recomendamos fazer chamadas com intervalo de pelo menos 5 segundos para evitar sobrecarga desnecessária. operationId: getNoteStatus parameters: - name: noteId in: path required: true schema: type: string format: uuid description: ID da nota para verificar o status example: 550e8400-e29b-41d4-a716-446655440000 responses: '200': description: Status retornado com sucesso content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/StatusResult' examples: completed: summary: Transcrição concluída value: data: status: completed valid: true processing: summary: Em processamento value: data: status: processing valid: false failed: summary: Falha no processamento value: data: status: failed valid: false '400': description: ID da nota inválido content: application/json: schema: $ref: '#/components/schemas/Error' example: error: bad_request message: Invalid note id code: INVALID_NOTE_ID '401': $ref: '#/components/responses/Unauthorized' '403': description: Permissões insuficientes content: application/json: schema: $ref: '#/components/schemas/Error' example: error: forbidden message: Note does not belong to tenant code: NOTE_ACCESS_DENIED '404': $ref: '#/components/responses/NotFound' '429': description: Rate limit excedido content: application/json: schema: $ref: '#/components/schemas/Error' example: error: too_many_requests message: Rate limit exceeded code: RATE_LIMIT_EXCEEDED components: schemas: StatusResult: type: object required: - status - valid properties: status: type: string enum: - pending - processing - completed - failed description: Status atual da transcrição example: completed valid: type: boolean description: Indica se a nota foi validada após o processamento example: true example: status: completed valid: true Error: type: object properties: error: type: string description: Código do erro example: bad_request message: type: string description: Mensagem de erro example: Invalid audio format code: type: string description: Código específico example: INVALID_AUDIO_FORMAT details: type: object description: Detalhes adicionais do erro responses: Unauthorized: description: Não autorizado content: application/json: schema: $ref: '#/components/schemas/Error' example: error: unauthorized message: API Key missing or invalid code: INVALID_API_KEY NotFound: description: Recurso não encontrado content: application/json: schema: $ref: '#/components/schemas/Error' example: error: not_found message: Note not found code: NOTE_NOT_FOUND securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-KEY description: API Key para autenticação ``` # Update section Source: https://docs.connectvets.com.br/notes/api-reference/notes/update-section # Atualizar Seção da Nota > Atualiza uma seção específica de uma nota. **Rate Limiting**: Este endpoint está sujeito aos limites da sua API Key (padrão: 100 req/min, 1000 req/hora). ## Casos de Uso Este endpoint permite atualizar seções específicas de uma nota após sua criação: * **Correção de dados**: Corrigir informações em seções específicas * **Enriquecimento**: Adicionar informações complementares a uma seção * **Reorganização**: Alterar ordem de exibição das seções * **Categorização**: Atualizar labels para melhor organização **Permissões**: Este endpoint requer uma API Key com permissões de **escrita** (`write`) ou **administrador** (`admin`). **Rate Limiting**: Este endpoint está sujeito aos limites da sua API Key (padrão: 100 req/min, 1000 req/hora). ## Parâmetros ### Query Parameters | Parâmetro | Tipo | Obrigatório | Descrição | | - | - | - | - | | `section_id` | string (UUID) | ✅ | ID da seção que será atualizada | ### Request Body | Campo | Tipo | Obrigatório | Descrição | | - | - | - | - | | `title` | string | ✅ | Título da seção | | `content` | string | ✅ | Conteúdo da seção | | `order` | string | ✅ | Ordem de exibição (ex: "1", "2", "3") | | `label` | string | ❌ | Rótulo interno para classificação | ## Exemplos de Uso ### Atualizar Conteúdo de uma Seção ```javascript theme={null} theme={null} const response = await fetch('/note_sections?section_id=123e4567-e89b-12d3-a456-426614174000', { method: 'PATCH', headers: { 'X-API-KEY': 'sua_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'Exame Físico', content: 'Exame físico atualizado: Animal apresenta melhora significativa...', order: '2', label: 'exame_fisico' }) }); const updatedSection = await response.json(); ``` ### Reorganizar Ordem das Seções ```javascript theme={null} theme={null} // Exemplo: Mover seção para primeira posição const response = await fetch('/note_sections?section_id=123e4567-e89b-12d3-a456-426614174000', { method: 'PATCH', headers: { 'X-API-KEY': 'sua_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'Anamnese', content: 'História clínica detalhada...', order: '1' // Movendo para primeira posição }) }); ``` ## Resposta ### Sucesso (200) ```json theme={null} theme={null} { "id": "123e4567-e89b-12d3-a456-426614174000", "note_id": "550e8400-e29b-41d4-a716-446655440000", "title": "Exame Físico", "label": "exame_fisico", "content": "Exame físico atualizado: Animal apresenta melhora significativa...", "order": "2", "created_at": "2024-02-14T10:30:00Z", "updated_at": "2024-02-14T15:45:00Z" } ``` ## Códigos de Erro | Código | Descrição | Solução | | - | - | - | | `400` | Parâmetros inválidos ou malformados | Verifique se o `section_id` é um UUID válido | | `401` | API Key inválida ou não fornecida | Verifique o header `X-API-KEY` | | `403` | Permissões insuficientes | Use uma API Key com permissões de escrita | | `404` | Seção não encontrada | Verifique se o `section_id` existe | | `422` | Dados de entrada inválidos | Verifique se todos os campos obrigatórios estão presentes | ## Notas Importantes **Versionamento**: As atualizações criam uma nova versão da seção, mantendo o histórico através do campo `updated_at`. **Validação**: O campo `order` deve ser uma string numérica válida. Recomenda-se usar valores sequenciais como "1", "2", "3" para facilitar a ordenação. **Workflow Recomendado**: 1. Use `GET /notes/{id}` para obter as seções existentes 2. Identifique o `section_id` da seção que deseja atualizar 3. Faça a atualização com `PATCH /note_sections` 4. Verifique o resultado com outro `GET /notes/{id}` ## OpenAPI ```yaml PATCH /note_sections theme={null} openapi: 3.0.3 info: title: ConnectVets Notes API description: API para transcrição e análise de consultas veterinárias version: 1.0.0 contact: name: ConnectVets Support email: suporte@connectvets.com.br url: https://connectvets.com.br license: name: Proprietary url: https://connectvets.com.br/terms servers: - url: https://api-sandbox.connectvets.com.br/notes/v1 description: Homologação security: - ApiKeyAuth: [] paths: /note_sections: patch: tags: - Notes summary: Atualizar seção da nota description: > Atualiza uma seção específica de uma nota. **Rate Limiting**: Este endpoint está sujeito aos limites da sua API Key (padrão: 100 req/min, 1000 req/hora). operationId: updateNoteSection parameters: - name: section_id in: query required: true schema: type: string format: uuid description: ID da seção a ser atualizada example: 123e4567-e89b-12d3-a456-426614174000 requestBody: required: true content: application/json: schema: type: object required: - title - content - order properties: title: type: string description: Título da seção example: Exame Físico label: type: string description: Rótulo interno para classificação example: exame_fisico content: type: string description: Conteúdo da seção example: Animal apresenta bom estado geral... order: type: string description: Ordem de exibição (ex. "1", "2", "3") example: '2' responses: '200': description: Seção atualizada com sucesso content: application/json: schema: $ref: '#/components/schemas/NoteSection' '400': description: Parâmetros inválidos content: application/json: schema: $ref: '#/components/schemas/Error' example: error: bad_request message: Invalid section_id format code: INVALID_SECTION_ID '401': $ref: '#/components/responses/Unauthorized' '403': description: Permissões insuficientes content: application/json: schema: $ref: '#/components/schemas/Error' example: error: forbidden message: Insufficient permissions to update note sections code: INSUFFICIENT_PERMISSIONS '404': description: Seção não encontrada content: application/json: schema: $ref: '#/components/schemas/Error' example: error: not_found message: Note section not found code: SECTION_NOT_FOUND '422': $ref: '#/components/responses/UnprocessableEntity' components: schemas: NoteSection: type: object properties: id: type: string format: uuid description: ID da seção example: abc123-def456-ghi789 title: type: string description: Título da seção example: Anamnese label: type: string description: Label da seção example: anamnese content: type: string description: Conteúdo da seção example: Paciente apresenta sintomas de... order: type: string description: Ordem da seção example: '1' Error: type: object properties: error: type: string description: Código do erro example: bad_request message: type: string description: Mensagem de erro example: Invalid audio format code: type: string description: Código específico example: INVALID_AUDIO_FORMAT details: type: object description: Detalhes adicionais do erro responses: Unauthorized: description: Não autorizado content: application/json: schema: $ref: '#/components/schemas/Error' example: error: unauthorized message: API Key missing or invalid code: INVALID_API_KEY UnprocessableEntity: description: Entidade não processável content: application/json: schema: $ref: '#/components/schemas/Error' example: error: unprocessable_entity message: Audio file is too large code: AUDIO_TOO_LARGE securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-KEY description: API Key para autenticação ``` # Overview Source: https://docs.connectvets.com.br/notes/api-reference/overview # Visão Geral da API > Documentação completa da API REST do ConnectVets Notes ## Base URL ### Produção ``` https://api.connectvets.com.br/notes/v1 ``` ### Homologação ``` https://api-sandbox.connectvets.com.br/notes/v1 ``` **Frontend da aplicação**: * Produção: [https://notes.connectvets.com.br](https://notes.connectvets.com.br) * Homologação: [https://notes-sandbox.connectvets.com.br](https://notes-sandbox.connectvets.com.br) ## Autenticação Para todas as requisições utilize o header: ```http theme={null} theme={null} X-API-KEY: ``` Sua API Key é secreta! Nunca a compartilhe em código público ou logs. ## Obtendo sua API Key Para obter acesso à API: 1. **Contate nossa equipe** via [Discord ConnectVets](https://discord.gg/uN4NxUGk) 2. **Forneça informações** sobre seu projeto e casos de uso 3. **Receba suas credenciais** com instruções de configuração 4. **Comece a integrar** seguindo nossa documentação Cada API Key possui limites específicos de uso. Entre em contato para discutir suas necessidades. ## Convenções de Campos * **UUIDs** são strings no formato RFC 4122 * **Datas** utilizam ISO 8601 em UTC (`YYYY-MM-DDThh:mm:ssZ`) * **Status** são strings em lowercase com underscore ## Rate Limiting A API implementa rate limiting baseado na API Key para a maioria dos endpoints: ### Endpoints com Rate Limiting Os seguintes endpoints possuem limitações (padrão: **100 req/min**, **1000 req/hora**): * `POST /notes` - Criação de notas * `GET /notes` - Listagem de notas * `GET /notes/{id}` - Detalhes da nota ### Endpoints SEM Rate Limiting * `GET /notes/{id}/status` - **Sem limitação**, mas recomendamos intervalo de 5s entre chamadas ### Headers de Rate Limiting Quando aplicável, os headers de resposta incluem informações sobre os limites: ```http theme={null} theme={null} X-RateLimit-Limit-Minute: 100 X-RateLimit-Remaining-Minute: 87 X-RateLimit-Reset-Minute: 1642694400 ``` ## Formato de Resposta ### Sucesso Todas as respostas de sucesso seguem um formato JSON consistente: ```json theme={null} theme={null} { "data": {}, // ou array para listas "meta": {} // informações adicionais como paginação } ``` ### Erro Respostas de erro incluem informações detalhadas: ```json theme={null} theme={null} { "error": "bad_request", "message": "Invalid audio format", "code": "INVALID_AUDIO_FORMAT", "details": { "expected": "audio/wav", "received": "audio/mp3" } } ``` ## Códigos de Status HTTP | Código | Significado | Descrição | | - | - | - | | `200` | OK | Requisição bem-sucedida | | `201` | Created | Recurso criado com sucesso | | `400` | Bad Request | Dados inválidos na requisição | | `401` | Unauthorized | API Key ausente ou inválida | | `403` | Forbidden | Permissões insuficientes | | `404` | Not Found | Recurso não encontrado | | `413` | Payload Too Large | Arquivo muito grande | | `422` | Unprocessable Entity | Dados válidos mas processamento falhou | | `429` | Too Many Requests | Rate limit excedido | | `500` | Internal Server Error | Erro interno do servidor | ## Endpoints Disponíveis A API ConnectVets Notes oferece endpoints para gerenciar **notas de consultas veterinárias**: ### Notes | Endpoint | Método | Descrição | Rate Limiting | | - | - | - | - | | `/notes` | POST | Criar nova nota com upload de áudio | ✅ 100/min, 1000/h | | `/notes` | GET | Listar notas com filtros | ✅ 100/min, 1000/h | | `/notes/{id}` | GET | Obter detalhes de uma nota específica | ✅ 100/min, 1000/h | | `/notes/{id}/status` | GET | Obter status de transcrição de uma nota | ❌ Sem limitação\* | | `/notes/{id}/export` | GET | Exportar nota em formatos RTF/TXT | ✅ 100/min, 1000/h | | `/notes/{noteId}/retry` | POST | Reprocessar nota falhada | ✅ 100/min, 1000/h | | `/note_sections` | PATCH | Atualizar seção específica de uma nota | ✅ 100/min, 1000/h | ### Transcripts | Endpoint | Método | Descrição | Rate Limiting | | - | - | - | - | | `/transcripts/retry` | POST | Reprocessar uma nota que falhou | ✅ 100/min, 1000/h | \* Recomendamos intervalo de 5s entre chamadas ## Tipos de Dados ### Note ```json theme={null} theme={null} { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Rex", "gender": "male", "transcription_status": "completed", "external_id": "CLIENTE_123", "metadata": [{"key": "veterinario", "value": "Dr. Silva"}], "created_at": "2024-02-14T18:25:43Z", "updated_at": "2024-02-14T18:30:23Z" } ``` ### NoteSection ```json theme={null} theme={null} { "id": "9b7d8b6a-12e3-45fa-9c1c-7e12f5c4a1b2", "title": "Anamnese", "label": "anamnesis", "content": "Paciente apresenta histórico de...", "order": "1" } ``` ### StatusResult ```json theme={null} theme={null} { "status": "completed", "valid": true } ``` ### MetadataItem ```json theme={null} theme={null} { "key": "veterinario", "value": "Dr. Silva" } ``` ## Paginação Endpoints que retornam listas implementam paginação: ### Parâmetros de Query * `page`: Número da página (padrão: 1) * `limit`: Itens por página (padrão: 10, máximo: 100) ### Resposta ```json theme={null} theme={null} { "data": [...], "meta": { "total": 200, "page": 1, "limit": 10, "total_pages": 20 } } ``` ## Filtros para Notes O endpoint `/notes` suporta diversos filtros via query parameters: * `transcription_status`: Filtrar por status (`pending`, `processing`, `completed`, `failed`) * `name`: Busca parcial por nome do paciente * `from` / `to`: Filtrar por data (YYYY-MM-DD) * `days`: Últimos X dias * `external_id`: Filtrar por ID externo ### Exemplos ```bash theme={null} theme={null} # Notas processadas GET /notes?transcription_status=completed # Notas de um período GET /notes?from=2024-01-01&to=2024-01-31 # Notas de um paciente GET /notes?name=Rex # Últimos 7 dias GET /notes?days=7 # Combinando filtros GET /notes?transcription_status=completed&days=30&limit=50 ``` **Dica**: Para verificar apenas o status de uma nota específica sem buscar todos os dados, use o endpoint [`/notes/{noteId}/status`](/notes/api-reference/notes/status) - sem rate limiting para polling eficiente. ### Boas Práticas para Rate Limiting 1. **Use `/notes/{id}/status` para polling**: Sem limitações, ideal para verificações frequentes 2. **Implemente backoff exponencial**: Para retries em caso de erro 429 3. **Cache resultados quando possível**: Evite chamadas desnecessárias 4. **Monitore headers de rate limit**: Para ajustar a frequência das requisições ## Upload de Arquivos O endpoint `POST /notes` aceita upload de arquivos de áudio usando `multipart/form-data`: ```bash theme={null} theme={null} curl -X POST "https://api-sandbox.connectvets.com.br/notes/v1/notes" \ -H "X-API-KEY: sua_api_key_aqui" \ -F "audio=@caminho/para/audio.mp3" \ -F "metadata={\"patient_name\":\"Rex\",\"owner_name\":\"João Silva\"}" ``` ## Tratamento de Erros ### Rate Limit Exceeded (429) Quando o rate limit é excedido, você receberá: ```json theme={null} theme={null} { "error": "too_many_requests", "message": "Rate limit exceeded", "code": "RATE_LIMIT_EXCEEDED" } ``` **Importante**: O endpoint `/notes/{id}/status` não possui esta limitação. ### Retry com Backoff Para erros temporários (429, 500), implemente retry com backoff exponencial: ```javascript theme={null} theme={null} async function apiCall(url, options, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { try { const response = await fetch(url, options); if (response.status === 429) { const retryAfter = response.headers.get('Retry-After') || 30; await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); continue; } if (response.ok) return await response.json(); throw new Error(`HTTP ${response.status}`); } catch (error) { if (attempt === maxRetries - 1) throw error; await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000)); } } } ``` *** **Próximo passo**: Explore os endpoints de Notes na seção [API Endpoints](/notes/api-reference/notes/create) ou veja [exemplos práticos](/notes/examples/basic-integration). # Schemas Source: https://docs.connectvets.com.br/notes/api-reference/schemas # Schemas e Tipos de Dados > Definições completas dos tipos de dados da API ConnectVets Notes ## Visão Geral Esta página documenta todos os tipos de dados (schemas) utilizados pela API ConnectVets Notes. Estes schemas seguem as convenções JSON e são consistentes em toda a API. ## Schemas Principais ### Note Representa uma nota veterinária completa com áudio, transcrição e seções. ```typescript theme={null} theme={null} interface Note { id: string; // UUID da nota name: string; // Nome do paciente gender: "male" | "female" | "unidentified"; // Sexo do paciente transcription_status: "pending" | "processing" | "completed" | "failed"; // Status da transcrição external_id?: string; // ID do sistema externo (opcional) metadata: MetadataItem[]; // Metadados adicionais note_sections: NoteSection[]; // Seções estruturadas (apenas no GET /notes/{id}) created_at: string; // Data de criação (ISO 8601) updated_at: string; // Data da última atualização (ISO 8601) } ``` #### Exemplo ```json theme={null} theme={null} { "id": "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3", "name": "Rex", "gender": "male", "audio_name": "rex_visit_20240214.wav", "transcription_status": "completed", "external_id": "CLIENTE_123", "metadata": [ {"key": "procedimento", "value": "vacina"}, {"key": "veterinario", "value": "Dr. João Silva"} ], "note_sections": [ { "id": "9b7d8b6a-12e3-45fa-9c1c-7e12f5c4a1b2", "title": "Anamnese", "label": "anamnesis", "content": "Paciente apresenta diminuição do apetite nos últimos 3 dias...", "order": "1" } ], "created_at": "2024-02-14T18:25:43Z", "updated_at": "2024-02-14T18:30:23Z" } ``` ### NoteSection Representa uma seção estruturada de uma nota (anamnese, exame físico, etc.). ```typescript theme={null} theme={null} interface NoteSection { id: string; // UUID da seção title: string; // Título exibido da seção label: string; // Identificador interno (para programação) content: string; // Conteúdo transcrito da seção order: string; // Ordem de exibição ("1", "2", "3"...) } ``` #### Labels Comuns | Label | Título | Descrição | | - | - | - | | `anamnesis` | Anamnese | Histórico e relato do proprietário | | `physical_exam` | Exame Físico | Avaliação física e sinais vitais | | `diagnosis` | Diagnóstico | Conclusões e diagnósticos | | `treatment` | Tratamento | Prescrições e recomendações | | `observations` | Observações | Notas adicionais | | `follow_up` | Seguimento | Instruções de retorno | #### Exemplo ```json theme={null} theme={null} { "id": "9b7d8b6a-12e3-45fa-9c1c-7e12f5c4a1b2", "title": "Exame Físico", "label": "physical_exam", "content": "Peso: 28kg. Temperatura: 38.8°C. FC: 90 bpm. FR: 24 mpm. Mucosas rosadas e úmidas.", "order": "2" } ``` ### MetadataItem Par chave-valor para armazenar informações adicionais sobre a nota. ```typescript theme={null} theme={null} interface MetadataItem { key: string; // Chave do metadado value: string; // Valor do metadado } ``` #### Exemplos Comuns ```json theme={null} theme={null} [ {"key": "procedimento", "value": "consulta de rotina"}, {"key": "veterinario", "value": "Dr. João Silva"}, {"key": "peso", "value": "12.5kg"}, {"key": "temperatura", "value": "38.5°C"}, {"key": "observacoes", "value": "animal cooperativo"} ] ``` ### Pagination Informações de paginação para endpoints que retornam listas. ```typescript theme={null} theme={null} interface Pagination { current_page: number; // Página atual total_pages: number; // Total de páginas total_items: number; // Total de itens } ``` #### Exemplo ```json theme={null} theme={null} { "current_page": 1, "total_pages": 10, "total_items": 200 } ``` ### NoteListResponse Resposta para o endpoint `GET /notes` (listagem). ```typescript theme={null} theme={null} interface NoteListResponse { pagination: Pagination; // Informações de paginação notes: Note[]; // Array de notas (sem note_sections) } ``` **Importante**: Na listagem, as notas **não incluem** o campo `note_sections`. Para obter as seções, use `GET /notes/{id}`. #### Exemplo ```json theme={null} theme={null} { "pagination": { "current_page": 1, "total_pages": 5, "total_items": 100 }, "notes": [ { "id": "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3", "name": "Rex", "gender": "male", "transcription_status": "completed", "external_id": "CLIENTE_123", "metadata": [ {"key": "procedimento", "value": "vacina"} ], "created_at": "2024-02-14T18:25:43Z", "updated_at": "2024-02-14T18:30:23Z" } ] } ``` ## Enums ### TranscriptionStatus Status do processamento da transcrição: ```typescript theme={null} theme={null} type TranscriptionStatus = "pending" | "processing" | "completed" | "failed"; ``` | Valor | Descrição | Comportamento | | - | - | - | | `pending` | Aguardando processamento | Nota criada, mas ainda não iniciou o processamento | | `processing` | IA processando o áudio | Transcrição em andamento | | `completed` | Transcrição finalizada | Seções disponíveis, arquivo de transcrição pronto | | `failed` | Erro no processamento | Problema durante a transcrição | ### Gender Sexo do paciente: ```typescript theme={null} theme={null} type Gender = "male" | "female" | "unidentified"; ``` | Valor | Descrição | | - | - | | `male` | Macho | | `female` | Fêmea | | `unidentified` | Não identificado (padrão) | ## Formatos de Data Todas as datas utilizam o formato **ISO 8601** em **UTC**: ``` YYYY-MM-DDThh:mm:ssZ ``` ### Exemplos ```json theme={null} theme={null} { "created_at": "2024-02-14T18:25:43Z", "updated_at": "2024-02-14T18:30:23Z" } ``` ### Parsing em Diferentes Linguagens #### JavaScript ```javascript theme={null} theme={null} const createdAt = new Date("2024-02-14T18:25:43Z"); console.log(createdAt.toLocaleDateString('pt-BR')); // Output: 14/02/2024 ``` #### Python ```python theme={null} theme={null} from datetime import datetime created_at = datetime.fromisoformat("2024-02-14T18:25:43Z".replace('Z', '+00:00')) print(created_at.strftime("%d/%m/%Y %H:%M")) # Output: 14/02/2024 18:25 ``` #### Go ```go theme={null} theme={null} package main import ( "fmt" "time" ) func main() { t, _ := time.Parse(time.RFC3339, "2024-02-14T18:25:43Z") fmt.Println(t.Format("02/01/2006 15:04")) // Output: 14/02/2024 18:25 } ``` ## Identificadores UUID Todos os IDs utilizam o formato **UUID v4** (RFC 4122): ``` 6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3 ``` ### Características * **Comprimento**: 36 caracteres * **Formato**: 8-4-4-4-12 (hexadecimal com hífens) * **Geração**: Aleatório/pseudo-aleatório * **Unicidade**: Globalmente único ### Validação #### JavaScript ```javascript theme={null} theme={null} function isValidUUID(uuid) { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(uuid); } ``` #### Python ```python theme={null} theme={null} import uuid def is_valid_uuid(uuid_string): try: uuid.UUID(uuid_string, version=4) return True except ValueError: return False ``` ## Tratamento de Nulos ### Campos Opcionais Campos que podem ser `null` ou ausentes: * `transcription_url` - Disponível apenas quando `transcription_status = "completed"` * `external_id` - Sempre opcional * `note_sections` - Vazio para status `pending`/`processing`/`failed` ### Exemplo com Valores Nulos ```json theme={null} theme={null} { "id": "7b5fe2ef-63d5-5c3c-b41f-5c4fa8e8d9c4", "name": "Luna", "transcription_status": "processing", "transcription_url": null, "external_id": null, "note_sections": [] } ``` ## Validação de Dados ### Regras de Validação #### Note (criação) * `name`: Obrigatório, 1-100 caracteres * `gender`: Opcional, enum válido * `external_id`: Opcional, máx 100 caracteres * `metadata`: Opcional, array de objetos válidos #### MetadataItem * `key`: Obrigatório, 1-50 caracteres * `value`: Obrigatório, 1-500 caracteres ### Exemplo de Validação JavaScript ```javascript theme={null} theme={null} function validateNote(note) { const errors = []; if (!note.name || note.name.length === 0) { errors.push("Nome do paciente é obrigatório"); } if (note.name && note.name.length > 100) { errors.push("Nome do paciente deve ter no máximo 100 caracteres"); } if (note.gender && !["male", "female", "unidentified"].includes(note.gender)) { errors.push("Sexo deve ser male, female ou unidentified"); } if (note.metadata) { note.metadata.forEach((item, index) => { if (!item.key) { errors.push(`Metadata[${index}]: chave é obrigatória`); } if (!item.value) { errors.push(`Metadata[${index}]: valor é obrigatório`); } }); } return errors; } ``` ## Próximos Passos Como criar uma nova nota com áudio Como buscar e filtrar notas Como acessar detalhes de uma nota Entenda os conceitos das notas # Authentication Source: https://docs.connectvets.com.br/notes/authentication # Autenticação > Como autenticar suas requisições com API Keys do ConnectVets Notes # Autenticação com API Keys 🔐 O ConnectVets Notes usa **API Keys** para autenticar todas as requisições. É simples, seguro e permite controle granular de acesso. **Todas as requisições** para a API ConnectVets Notes devem incluir uma API Key válida no header `X-API-KEY`. ## Como funciona Crie uma API Key no dashboard do ConnectVets Notes Adicione o header `X-API-KEY` em todas as requisições Envie sua requisição - a autenticação é automática ## Criando uma API Key ### Pelo Dashboard Web Faça login em [notes.connectvets.com.br](https://notes.connectvets.com.br) Vá para **Configurações** → **API Keys** no menu lateral Clique em **"Nova API Key"** e preencha: * **Nome**: Identifique o uso (ex: "Integração Sistema X") * **Tipo**: Selecione as permissões necessárias * **Expiração**: Defina quando expira (opcional) ⚠️ **Importante**: Copie a chave imediatamente - ela só aparece uma vez! ### Via API (para automação) ```bash cURL theme={null} theme={null} curl -X POST "https://api-sandbox.connectvets.com.br/notes/v1/api-keys" \ -H "X-API-KEY: your_admin_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Integração Sistema X", "permissions": ["notes:read", "notes:write"], "expires_at": "2024-12-31T23:59:59Z" }' ``` ```javascript JavaScript theme={null} theme={null} const response = await fetch('https://api-sandbox.connectvets.com.br/notes/v1/api-keys', { method: 'POST', headers: { 'X-API-KEY': 'your_admin_key', 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Integração Sistema X', permissions: ['notes:read', 'notes:write'], expires_at: '2024-12-31T23:59:59Z' }) }); const apiKey = await response.json(); console.log('Nova API Key:', apiKey.data.key); ``` ```python Python theme={null} theme={null} import requests data = { 'name': 'Integração Sistema X', 'permissions': ['notes:read', 'notes:write'], 'expires_at': '2024-12-31T23:59:59Z' } response = requests.post( 'https://api-sandbox.connectvets.com.br/notes/v1/api-keys', headers={'X-API-KEY': 'your_admin_key'}, json=data ) api_key = response.json() print(f"Nova API Key: {api_key['data']['key']}") ``` ## Usando API Keys Inclua sua API Key no header `X-API-KEY` de todas as requisições: ```bash cURL theme={null} theme={null} curl -X GET "https://api-sandbox.connectvets.com.br/notes/v1/notes" \ -H "X-API-KEY: cvn_1234567890abcdef" \ -H "Content-Type: application/json" ``` ```javascript JavaScript theme={null} theme={null} // Fetch API const response = await fetch('https://api-sandbox.connectvets.com.br/notes/v1/notes', { headers: { 'X-API-KEY': 'cvn_1234567890abcdef', 'Content-Type': 'application/json' } }); // Axios import axios from 'axios'; const api = axios.create({ baseURL: 'https://api-sandbox.connectvets.com.br/notes/v1', headers: { 'X-API-KEY': 'cvn_1234567890abcdef' } }); ``` ```python Python theme={null} theme={null} import requests # Session com header padrão session = requests.Session() session.headers.update({ 'X-API-KEY': 'cvn_1234567890abcdef', 'Content-Type': 'application/json' }) response = session.get('https://api-sandbox.connectvets.com.br/notes/v1/notes') ``` ```go Go theme={null} theme={null} package main import ( "net/http" ) func main() { client := &http.Client{} req, _ := http.NewRequest("GET", "https://api-sandbox.connectvets.com.br/notes/v1/notes", nil) req.Header.Add("X-API-KEY", "cvn_1234567890abcdef") req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ## Tipos de API Keys **Permissões**: Todas * Criar/deletar API Keys * Gerenciar usuários * Acesso completo às notas **Permissões**: Leitura e escrita * Criar notas * Atualizar notas existentes * Visualizar todas as notas **Permissões**: Somente leitura * Visualizar notas * Buscar e filtrar * Sem permissões de escrita ## Anatomia de uma API Key As API Keys do ConnectVets seguem um formato específico para facilitar identificação: ``` cvn_1234567890abcdef1234567890abcdef │ │ │ └── Chave secreta (32 caracteres hex) └── Prefixo identificador ConnectVets Notes ``` * **Prefixo**: `cvn_` identifica chaves do ConnectVets Notes * **Comprimento**: 36 caracteres total (4 do prefixo + 32 da chave) * **Formato**: Hexadecimal (0-9, a-f) * **Geração**: Cryptographically secure random * **Encoding**: UTF-8 ## Segurança e Boas Práticas * Armazene em variáveis de ambiente * Use HTTPS em produção * Monitore uso das chaves * Rotacione chaves regularmente * Use chaves com permissões mínimas * Commitar chaves no código * Logar chaves em arquivos * Compartilhar chaves por email/chat * Usar chaves de admin para tudo * Deixar chaves sem expiração ### Armazenamento Seguro ```bash Variáveis de Ambiente theme={null} theme={null} # .env CONNECTVETS_API_KEY=cvn_1234567890abcdef # .bashrc ou .zshrc export CONNECTVETS_API_KEY=cvn_1234567890abcdef ``` ```javascript Node.js theme={null} theme={null} // Usando dotenv require('dotenv').config(); const apiKey = process.env.CONNECTVETS_API_KEY; // Ou usando config const config = { apiKey: process.env.CONNECTVETS_API_KEY || 'fallback_for_dev' }; ``` ```python Python theme={null} theme={null} import os from dotenv import load_dotenv load_dotenv() API_KEY = os.getenv('CONNECTVETS_API_KEY') # Ou usando python-decouple from decouple import config API_KEY = config('CONNECTVETS_API_KEY') ``` ```go Go theme={null} theme={null} package main import ( "os" "log" ) func main() { apiKey := os.Getenv("CONNECTVETS_API_KEY") if apiKey == "" { log.Fatal("CONNECTVETS_API_KEY não encontrada") } } ``` ## Rate Limiting API Keys têm limites de taxa baseados no tipo: **1000 req/min** Limite mais alto para operações administrativas **500 req/min** Limite moderado para operações normais **200 req/min** Limite conservador para consultas ### Respondendo ao Rate Limiting Quando você excede o limite, a API retorna `429 Too Many Requests`: ```json Resposta 429 theme={null} theme={null} { "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Rate limit exceeded. Try again in 60 seconds.", "details": { "limit": 500, "remaining": 0, "reset_at": "2024-01-15T10:31:00Z" } } } ``` ```javascript Tratamento em JS theme={null} theme={null} async function apiCall() { try { const response = await fetch(url, { headers }); if (response.status === 429) { const resetTime = response.headers.get('X-RateLimit-Reset'); const waitTime = new Date(resetTime) - new Date(); console.log(`Rate limit hit. Waiting ${waitTime}ms...`); await new Promise(resolve => setTimeout(resolve, waitTime)); return apiCall(); // Retry } return response.json(); } catch (error) { console.error('API Error:', error); } } ``` ```python Tratamento em Python theme={null} theme={null} import time import requests def api_call_with_retry(): while True: response = requests.get(url, headers=headers) if response.status_code == 429: reset_time = response.headers.get('X-RateLimit-Reset') wait_time = int(reset_time) - int(time.time()) print(f"Rate limit hit. Waiting {wait_time}s...") time.sleep(wait_time) continue return response.json() ``` ## Monitoramento e Logs ### Verificar uso de uma API Key ```bash cURL theme={null} theme={null} curl -X GET "https://api-sandbox.connectvets.com.br/notes/v1/api-keys/cvn_1234567890abcdef/usage" \ -H "X-API-KEY: your_admin_key" ``` ```javascript JavaScript theme={null} theme={null} const usage = await fetch(`https://api-sandbox.connectvets.com.br/notes/v1/api-keys/${keyId}/usage`, { headers: { 'X-API-KEY': 'your_admin_key' } }); const stats = await usage.json(); console.log('Uso da API Key:', stats.data); ``` ### Listar todas as API Keys ```bash cURL theme={null} theme={null} curl -X GET "https://api-sandbox.connectvets.com.br/notes/v1/api-keys" \ -H "X-API-KEY: your_admin_key" ``` ```javascript JavaScript theme={null} theme={null} const keys = await fetch('https://api-sandbox.connectvets.com.br/notes/v1/api-keys', { headers: { 'X-API-KEY': 'your_admin_key' } }); const apiKeys = await keys.json(); console.log('API Keys ativas:', apiKeys.data); ``` ## Rotação de Chaves Para manter a segurança, rotacione suas API Keys regularmente: Gere uma nova chave com as mesmas permissões Substitua a chave antiga pela nova em todos os sistemas Verifique se tudo funciona com a nova chave Delete a chave antiga do dashboard **Cuidado**: Sempre teste a nova chave antes de revogar a antiga para evitar interrupções no serviço. ## Resolução de Problemas **Possíveis causas:** * API Key inválida ou expirada * Header `X-API-KEY` ausente * Typo na chave **Soluções:** * Verifique se a chave está correta * Confirme que o header está sendo enviado * Regenere a chave se necessário **Possíveis causas:** * API Key sem permissões suficientes * Tentativa de acessar recursos de outro tenant * Chave desabilitada **Soluções:** * Use uma chave com permissões adequadas * Verifique se está acessando o tenant correto * Confirme se a chave está ativa **Possíveis causas:** * Limite de rate excedido * Muitas requisições simultâneas **Soluções:** * Implemente retry com backoff * Distribua requisições ao longo do tempo * Use cache para reduzir chamadas ## Próximos passos Use sua API Key para processar o primeiro áudio Veja todos os endpoints disponíveis Receba notificações automáticas de eventos Veja implementações práticas # Api keys Source: https://docs.connectvets.com.br/notes/concepts/api-keys # API Keys > Entenda como funcionam as chaves de API para autenticação segura As **API Keys** são credenciais que autenticam e autorizam acesso à API ConnectVets Notes. **Importante**: O acesso à API ConnectVets Notes é **exclusivo do plano Enterprise**. Entre em contato conosco via [Discord](https://discord.gg/uN4NxUGk) para verificar sua elegibilidade e configurar seu acesso. ## Como Funcionam Confirme que você é cliente do plano Enterprise - apenas clientes Enterprise têm acesso à API Contate nossa equipe via [Discord](https://discord.gg/uN4NxUGk) para solicitar acesso Nossa equipe criará suas credenciais com limites de rate limiting personalizados para seu uso Você receberá sua API Key, informações sobre seus limites específicos e instruções de uso Use a chave no header `X-API-KEY` em todas as requisições ## Tipos de API Keys ### Produção (Enterprise) * **Ambiente**: `https://api-sandbox.connectvets.com.br/notes/v1` * **Uso**: Aplicações em produção (clientes Enterprise) * **Rate Limiting**: Limites personalizados conforme necessidade do cliente * **Suporte**: Prioritário e dedicado ### Homologação (Enterprise) * **Ambiente**: `https://api-sandbox.connectvets.com.br/notes/v1` * **Uso**: Testes e homologação (clientes Enterprise) * **Rate Limiting**: Limitado para ambiente de testes * **Suporte**: Via Discord ## Configuração ### Headers Obrigatórios ```http theme={null} theme={null} X-API-KEY: sua_api_key_aqui Content-Type: application/json ``` ### Exemplo de Uso ```javascript theme={null} theme={null} const apiKey = process.env.CONNECTVETS_API_KEY; const response = await fetch('https://api-sandbox.connectvets.com.br/notes/v1/notes', { method: 'GET', headers: { 'X-API-KEY': apiKey, 'Content-Type': 'application/json' } }); ``` ## Limites e Cotas **Plano Enterprise Obrigatório**: O consumo da API ConnectVets Notes é exclusivo para clientes do plano Enterprise. Os limites de rate limiting são configurados individualmente para cada cliente Enterprise baseado em: * Volume de uso esperado * Tipo de integração * Criticidade da aplicação * Horários de pico **Para conhecer seus limites específicos**, entre em contato com nossa equipe via [Discord](https://discord.gg/uN4NxUGk). ## Monitoramento ### Headers de Resposta A API retorna informações sobre uso nos headers: ```http theme={null} theme={null} X-RateLimit-Limit-Minute: 100 X-RateLimit-Remaining-Minute: 87 X-RateLimit-Reset-Minute: 1642694400 X-RateLimit-Limit-Hour: 1000 X-RateLimit-Remaining-Hour: 756 ``` ### Dashboard (Em Breve) Estamos desenvolvendo um dashboard para acompanhamento em tempo real: * Uso de cotas em tempo real * Histórico de requisições * Alertas de limite * Estatísticas de performance ## Segurança **Nunca exponha sua API Key** em código público, logs ou URLs. Use variáveis de ambiente. ### Boas Práticas 1. **Variáveis de Ambiente** ```bash theme={null} theme={null} export CONNECTVETS_API_KEY="sua_api_key_aqui" ``` 2. **Rotação Regular** * Contate nossa equipe via [Discord](https://discord.gg/uN4NxUGk) para rotacionar chaves periodicamente * Especialmente importante após vazamentos ou mudanças de equipe 3. **Monitoramento** * Acompanhe logs de acesso e uso * Configure alertas para uso anômalo 4. **Segregação** * Use chaves diferentes para desenvolvimento e produção * Limite permissões por ambiente ## Troubleshooting ### Erro 401: Unauthorized ```json theme={null} theme={null} { "error": "unauthorized", "message": "Invalid or missing API key" } ``` **Soluções:** * Verifique se o header `X-API-KEY` está presente * Confirme que a chave está correta * Verifique se a chave não expirou ### Erro 403: Forbidden ```json theme={null} theme={null} { "error": "forbidden", "message": "Insufficient permissions for this endpoint" } ``` **Soluções:** * Verifique se sua chave tem permissões para o endpoint * Contate nossa equipe via [Discord](https://discord.gg/uN4NxUGk) para upgrade de plano ### Erro 429: Rate Limit ```json theme={null} theme={null} { "error": "too_many_requests", "message": "Rate limit exceeded", "retry_after": 60 } ``` **Soluções:** * Implemente retry com backoff exponencial * Considere upgrade para plano com limite maior * Otimize frequência de requisições ## Gerenciamento **Importante**: API Keys não são gerenciadas via API. Todas as operações são feitas através de contato direto com nossa equipe via [Discord](https://discord.gg/uN4NxUGk). ### Para Solicitar Nova Chave **Pré-requisito**: Você deve ser cliente do plano Enterprise para ter acesso à API. 1. **Contate via Discord**: [https://discord.gg/uN4NxUGk](https://discord.gg/uN4NxUGk) 2. **Informe**: * Nome da empresa/projeto (cliente Enterprise) * Casos de uso esperados * Volume estimado de requisições * Ambiente (produção/desenvolvimento) * Necessidades específicas de rate limiting ### Para Modificar Limites Como cliente Enterprise, você pode solicitar ajustes nos seus limites de rate limiting. Entre em contato via [Discord](https://discord.gg/uN4NxUGk) informando: * Justificativa técnica para o ajuste * Métricas de uso atual da API * Projeções de crescimento de tráfego * Impacto nos negócios dos limites atuais ### Para Renovar ou Revogar Nossa equipe pode ajudar com: * Renovação de chaves expiradas * Revogação por segurança * Migração entre ambientes *** **Próximo passo**: Explore os [endpoints de Notes](/notes/api-reference/notes/create) ou veja [exemplos de autenticação](/notes/examples/basic-integration). # Notes Source: https://docs.connectvets.com.br/notes/concepts/notes # Como funcionam as Notas > Entenda o fluxo de processamento de notas de consulta veterinária ## O que são Notas? As **Notas** são transcrições estruturadas de consultas veterinárias. Você envia um arquivo de áudio via API e recebe de volta texto organizado em seções úteis como anamnese, diagnóstico e tratamento. ## Fluxo de Processamento Envie seu arquivo de áudio via POST para `/v1/notes` Nossa IA processa e transcreve o áudio (1-5 minutos) O texto é automaticamente organizado em seções médicas Os dados ficam disponíveis via GET `/v1/notes/{id}` ## Estados de Processamento Uma nota pode ter os seguintes status: **Aguardando processamento** Áudio foi recebido e está na fila **Em processamento** IA está transcrevendo o áudio **Concluído** Transcrição finalizada e seções disponíveis **Falhou** Erro no processamento (verifique qualidade do áudio) ## Estrutura de uma Nota ### Dados Básicos ```json theme={null} theme={null} { "id": "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3", "name": "Rex", "gender": "male", "external_id": "CLIENTE_123", "transcription_status": "completed", "created_at": "2024-02-14T18:25:43Z" } ``` | Campo | Tipo | Descrição | | - | - | - | | `id` | UUID | Identificador único da nota | | `name` | String | Nome do paciente | | `gender` | String | Sexo: `male`, `female`, `unidentified` | | `external_id` | String | ID do seu sistema (opcional) | | `transcription_status` | String | Status atual do processamento | ### Seções Estruturadas Quando processada, a nota é dividida automaticamente em seções: ```json theme={null} theme={null} { "note_sections": [ { "id": "9b7d8b6a-12e3-45fa-9c1c-7e12f5c4a1b2", "title": "Anamnese", "label": "anamnesis", "content": "Paciente apresenta histórico de...", "order": "1" }, { "title": "Diagnóstico", "label": "diagnosis", "content": "Após exame físico, diagnosticado...", "order": "2" } ] } ``` ### Seções Padrão **Histórico do paciente** * Sintomas relatados pelo tutor * Histórico médico anterior * Observações de comportamento **Avaliação física** * Peso, temperatura, sinais vitais * Observações do exame clínico * Alterações encontradas **Conclusões médicas** * Diagnóstico principal * Diagnósticos diferenciais * Suspeitas clínicas **Plano terapêutico** * Medicações prescritas * Dosagens e frequência * Instruções para o tutor ## Metadados Personalizados Você pode incluir informações adicionais ao criar uma nota: ```json theme={null} theme={null} { "metadata": [ {"key": "procedimento", "value": "vacina"}, {"key": "peso", "value": "12.5kg"}, {"key": "idade", "value": "3 anos"}, {"key": "veterinario", "value": "Dr. Silva"} ] } ``` **Casos de uso para metadados:** * Identificação do veterinário * Tipo de procedimento * Informações do paciente * Referências do seu sistema ## Requisitos de Áudio ### Formato Recomendado Para melhores resultados, use **WAV, 16kHz, mono, 16-bit** **Especificações ideais:** * **Formato**: WAV ou FLAC * **Taxa de amostragem**: 16.000 Hz * **Canais**: Mono (1 canal) * **Bit depth**: 16-bit ### Formatos Aceitos | Formato | Suporte | Qualidade | | - | - | - | | WAV | ✅ Ideal | Excelente | | FLAC | ✅ Bom | Boa | | MP3 | ⚠️ Aceito | Regular | | M4A | ⚠️ Aceito | Regular | ### Dicas para Melhor Qualidade Minimize ruído de fundo durante a gravação Mantenha 30-50cm de distância do microfone Evite falar muito rápido ou com volume muito baixo Entre 1 minuto e 1 hora de duração ## Limitações **Limitações importantes:** * **Tamanho máximo**: 100MB por arquivo * **Duração máxima**: 60 minutos * **Idioma**: Português brasileiro (primário) * **Qualidade**: Áudio com muito ruído pode falhar ## Monitoramento via Webhook Ao invés de fazer polling, configure webhooks para receber notificações: ```json theme={null} theme={null} { "event": "note.completed", "data": { "note_id": "6a4fe1de-52c4-4b2b-a30f-4b3fa9d7d8b3", "status": "completed" } } ``` Veja mais em [Conceitos de Webhooks](/notes/concepts/webhooks). *** **Próximo passo**: Experimente criar sua primeira nota seguindo o [Guia de Início Rápido](/notes/quickstart). # Webhooks Source: https://docs.connectvets.com.br/notes/concepts/webhooks # Webhooks > Receba notificações automáticas sobre eventos na sua conta Os **Webhooks** permitem que você receba notificações automáticas quando eventos importantes acontecem no ConnectVets Notes, como quando uma transcrição é concluída ou falha. ## Como Funcionam Nossa equipe configura os webhooks no seu sistema conforme suas necessidades Quando um evento ocorre (ex: transcrição concluída), enviamos uma requisição POST Sua aplicação recebe a notificação e pode processar o evento imediatamente Responda com status 200 para confirmar o recebimento ## Eventos Disponíveis ### note.transcription.completed Disparado quando uma transcrição é finalizada com sucesso: ```json theme={null} theme={null} { "event": "note.transcription.completed", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Rex", "transcription_status": "completed", "external_id": "CLIENTE_123", "created_at": "2024-02-14T18:25:43Z", "completed_at": "2024-02-14T18:30:23Z" }, "timestamp": "2024-02-14T18:30:24Z", "webhook_id": "wh_abc123def456" } ``` ### note.transcription.failed Disparado quando uma transcrição falha: ```json theme={null} theme={null} { "event": "note.transcription.failed", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Rex", "transcription_status": "failed", "error": { "code": "AUDIO_QUALITY_LOW", "message": "Áudio com qualidade insuficiente para transcrição" }, "external_id": "CLIENTE_123", "created_at": "2024-02-14T18:25:43Z", "failed_at": "2024-02-14T18:28:15Z" }, "timestamp": "2024-02-14T18:28:16Z", "webhook_id": "wh_abc123def456" } ``` ## Implementação ### Endpoint do Webhook Sua aplicação deve ter um endpoint que aceite requisições POST: ```javascript Node.js/Express theme={null} theme={null} app.post('/webhooks/connectvets', (req, res) => { const { event, data, timestamp } = req.body; console.log(`Evento recebido: ${event} em ${timestamp}`); switch (event) { case 'note.transcription.completed': handleTranscriptionCompleted(data); break; case 'note.transcription.failed': handleTranscriptionFailed(data); break; default: console.log(`Evento não tratado: ${event}`); } // Resposta de confirmação res.status(200).json({ received: true }); }); function handleTranscriptionCompleted(noteData) { // Lógica para processar transcrição concluída console.log(`Nota ${noteData.id} foi transcrita com sucesso`); // Exemplo: notificar o usuário notifyUser(noteData.external_id, 'Sua consulta foi transcrita!'); // Exemplo: atualizar banco local updateLocalNote(noteData.id, { status: 'completed' }); } function handleTranscriptionFailed(noteData) { // Lógica para processar falha na transcrição console.log(`Falha na transcrição da nota ${noteData.id}: ${noteData.error.message}`); // Exemplo: notificar erro notifyUser(noteData.external_id, 'Houve um problema na transcrição. Tente novamente.'); } ``` ```python Flask theme={null} theme={null} from flask import Flask, request, jsonify import logging app = Flask(__name__) @app.route('/webhooks/connectvets', methods=['POST']) def handle_webhook(): data = request.get_json() event = data.get('event') event_data = data.get('data') timestamp = data.get('timestamp') logging.info(f"Evento recebido: {event} em {timestamp}") if event == 'note.transcription.completed': handle_transcription_completed(event_data) elif event == 'note.transcription.failed': handle_transcription_failed(event_data) else: logging.warning(f"Evento não tratado: {event}") return jsonify({"received": True}), 200 def handle_transcription_completed(note_data): """Processa transcrição concluída""" note_id = note_data['id'] logging.info(f"Nota {note_id} foi transcrita com sucesso") # Exemplo: notificar o usuário notify_user(note_data['external_id'], 'Sua consulta foi transcrita!') # Exemplo: atualizar banco local update_local_note(note_id, {'status': 'completed'}) def handle_transcription_failed(note_data): """Processa falha na transcrição""" note_id = note_data['id'] error_msg = note_data['error']['message'] logging.error(f"Falha na transcrição da nota {note_id}: {error_msg}") # Exemplo: notificar erro notify_user(note_data['external_id'], 'Houve um problema na transcrição.') ``` ```php PHP theme={null} theme={null} 'Invalid JSON']); exit; } $event = $data['event'] ?? ''; $eventData = $data['data'] ?? []; $timestamp = $data['timestamp'] ?? ''; error_log("Evento recebido: $event em $timestamp"); switch ($event) { case 'note.transcription.completed': handleTranscriptionCompleted($eventData); break; case 'note.transcription.failed': handleTranscriptionFailed($eventData); break; default: error_log("Evento não tratado: $event"); } // Resposta de confirmação http_response_code(200); echo json_encode(['received' => true]); function handleTranscriptionCompleted($noteData) { $noteId = $noteData['id']; error_log("Nota $noteId foi transcrita com sucesso"); // Exemplo: notificar o usuário notifyUser($noteData['external_id'], 'Sua consulta foi transcrita!'); // Exemplo: atualizar banco local updateLocalNote($noteId, ['status' => 'completed']); } function handleTranscriptionFailed($noteData) { $noteId = $noteData['id']; $errorMsg = $noteData['error']['message']; error_log("Falha na transcrição da nota $noteId: $errorMsg"); // Exemplo: notificar erro notifyUser($noteData['external_id'], 'Houve um problema na transcrição.'); } ?> ``` ## Segurança ### Validação de Origem Para garantir que o webhook vem do ConnectVets, valide o header de assinatura: ```javascript theme={null} theme={null} const crypto = require('crypto'); function validateWebhookSignature(payload, signature, secret) { const computedSignature = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); const expectedSignature = `sha256=${computedSignature}`; return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) ); } app.post('/webhooks/connectvets', (req, res) => { const signature = req.headers['x-connectvets-signature']; const payload = JSON.stringify(req.body); if (!validateWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } // Processar webhook... }); ``` ### HTTPS Obrigatório * Todos os webhooks **devem** usar HTTPS * Certificados SSL válidos são obrigatórios * URLs localhost não são aceitas em produção ## Retry Logic ### Como Funciona Se seu endpoint não responder com status 2xx, reenviamos automaticamente: | Tentativa | Delay | | - | - | | 1ª | Imediato | | 2ª | 1 minuto | | 3ª | 5 minutos | | 4ª | 15 minutos | | 5ª | 1 hora | ### Idempotência Implemente verificação de duplicatas usando o `webhook_id`: ```javascript theme={null} theme={null} const processedWebhooks = new Set(); app.post('/webhooks/connectvets', (req, res) => { const webhookId = req.body.webhook_id; if (processedWebhooks.has(webhookId)) { // Já processado, apenas confirme return res.status(200).json({ received: true, duplicate: true }); } processedWebhooks.add(webhookId); // Processar webhook... }); ``` ## Monitoramento ### Logs Importantes Monitore estes aspectos: * **Latência de resposta**: Tempo para processar webhook * **Taxa de erro**: Percentual de falhas no processamento * **Timeouts**: Requisições que demoram muito * **Duplicatas**: Webhooks reprocessados ### Exemplo de Logging ```javascript theme={null} theme={null} const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.File({ filename: 'webhooks.log' }) ] }); app.post('/webhooks/connectvets', (req, res) => { const startTime = Date.now(); const { event, webhook_id } = req.body; try { // Processar webhook... const duration = Date.now() - startTime; logger.info('webhook_processed', { event, webhook_id, duration_ms: duration, status: 'success' }); res.status(200).json({ received: true }); } catch (error) { const duration = Date.now() - startTime; logger.error('webhook_failed', { event, webhook_id, duration_ms: duration, error: error.message, status: 'error' }); res.status(500).json({ error: 'Processing failed' }); } }); ``` ## Configuração **Importante**: Webhooks não são gerenciados via API. A configuração é feita através de contato direto com nossa equipe. ### Para Configurar Webhooks 1. **Contate nossa equipe** via [WhatsApp (+55 31 8883-5141)](https://wa.me/553185127147) 2. **Forneça informações**: * URL do seu endpoint (deve ser HTTPS) * Eventos que deseja receber * Método de autenticação preferido * Ambiente (produção/desenvolvimento) ### Informações Necessárias * **URL do Endpoint**: `https://seuapp.com/webhooks/connectvets` * **Eventos**: `["note.transcription.completed", "note.transcription.failed"]` * **Headers Customizados** (opcional): Para autenticação adicional * **Secret para Validação**: Para verificar autenticidade ### Teste em Desenvolvimento Nossa equipe pode configurar webhooks para ambiente de desenvolvimento usando: * **ngrok** para túneis locais * **Webhook.site** para testes rápidos * **RequestBin** para debug ## Troubleshooting ### Webhook Não Recebido **Possíveis causas:** * Endpoint inacessível (timeout/erro) * Certificado SSL inválido * Firewall bloqueando nossa IP **Soluções:** * Verificar se endpoint responde a GET/POST * Validar certificado SSL * Liberar IPs do ConnectVets no firewall ### Erro 401/403 **Possíveis causas:** * Autenticação adicional no endpoint * Headers de validação incorretos **Soluções:** * Revisar lógica de autenticação * Verificar headers `x-connectvets-signature` ### Webhooks Duplicados **Possíveis causas:** * Endpoint demorou para responder (timeout) * Resposta não 2xx **Soluções:** * Implementar verificação de `webhook_id` * Otimizar tempo de processamento * Responder rapidamente (mover processamento pesado para background) ## Exemplos de Uso ### Integração com CRM ```javascript theme={null} theme={null} async function handleTranscriptionCompleted(noteData) { // Buscar informações da consulta no sistema local const appointment = await getAppointmentByExternalId(noteData.external_id); if (appointment) { // Atualizar status no CRM await updateCRMRecord(appointment.crm_id, { transcription_status: 'completed', transcription_url: `https://notes.connectvets.com.br/notes/${noteData.id}`, last_updated: new Date() }); // Notificar veterinário await sendNotification(appointment.veterinarian_id, { type: 'transcription_ready', patient: noteData.name, note_id: noteData.id }); } } ``` ### Workflow de Aprovação ```javascript theme={null} theme={null} async function handleTranscriptionCompleted(noteData) { // Verificar se nota precisa de aprovação const needsApproval = await checkApprovalRules(noteData); if (needsApproval) { // Enviar para fila de aprovação await addToApprovalQueue(noteData.id, { priority: noteData.urgent ? 'high' : 'normal', assigned_to: await getReviewerForNote(noteData) }); // Notificar revisor await notifyReviewer(noteData); } else { // Aprovação automática await autoApproveNote(noteData.id); } } ``` *** **Próximo passo**: Veja [exemplos práticos de webhooks](/notes/examples/webhook-handling) ou explore os [endpoints de Notes](/notes/api-reference/notes/create). # Advanced filtering Source: https://docs.connectvets.com.br/notes/examples/advanced-filtering # Filtros e Busca > Como usar filtros para buscar notas específicas na API # Filtros e Busca de Notas Este exemplo mostra como usar os filtros disponíveis na API para buscar notas específicas. ## Filtros Básicos ### Buscar por Status ```bash theme={null} theme={null} # Buscar apenas notas processadas curl -X GET "https://api-sandbox.connectvets.com.br/notes/v1/notes?transcription_status=completed" \ -H "X-API-KEY: sua_api_key_aqui" # Buscar notas com erro curl -X GET "https://api-sandbox.connectvets.com.br/notes/v1/notes?transcription_status=failed" \ -H "X-API-KEY: sua_api_key_aqui" ``` ### Filtrar por Data ```bash theme={null} theme={null} # Notas de um período específico curl -X GET "https://api-sandbox.connectvets.com.br/notes/v1/notes?from=2024-01-01&to=2024-01-31" \ -H "X-API-KEY: sua_api_key_aqui" # Notas dos últimos 7 dias curl -X GET "https://api-sandbox.connectvets.com.br/notes/v1/notes?days=7" \ -H "X-API-KEY: sua_api_key_aqui" ``` ### Buscar por Nome do Paciente ```bash theme={null} theme={null} # Buscar paciente específico curl -X GET "https://api-sandbox.connectvets.com.br/notes/v1/notes?name=Rex" \ -H "X-API-KEY: sua_api_key_aqui" ``` ## Implementações por Linguagem ### JavaScript/Node.js ```javascript theme={null} theme={null} class NotesFilter { constructor(apiKey) { this.apiKey = apiKey; this.baseUrl = 'https://api-sandbox.connectvets.com.br/notes/v1'; } async searchNotes(filters = {}) { const params = new URLSearchParams(); // Adicionar filtros à query Object.entries(filters).forEach(([key, value]) => { if (value !== null && value !== undefined && value !== '') { params.append(key, value); } }); const response = await fetch(`${this.baseUrl}/notes?${params}`, { headers: { 'X-API-KEY': this.apiKey, 'Content-Type': 'application/json' } }); return await response.json(); } // Filtros específicos async getCompletedNotes(page = 1, limit = 10) { return this.searchNotes({ transcription_status: 'completed', page, limit }); } async getNotesByDateRange(from, to) { return this.searchNotes({ from: from, // YYYY-MM-DD to: to, // YYYY-MM-DD transcription_status: 'completed' }); } async getPatientNotes(patientName) { return this.searchNotes({ name: patientName, transcription_status: 'completed' }); } async getRecentNotes(days = 7) { const to = new Date().toISOString().split('T')[0]; const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000) .toISOString().split('T')[0]; return this.getNotesByDateRange(from, to); } } // Exemplos de uso async function examples() { const filter = new NotesFilter(process.env.CONNECTVETS_API_KEY); try { // Buscar notas processadas hoje const today = new Date().toISOString().split('T')[0]; const todayNotes = await filter.getNotesByDateRange(today, today); console.log('Notas de hoje:', todayNotes.data.length); // Buscar notas de um paciente específico const rexNotes = await filter.getPatientNotes('Rex'); console.log('Notas do Rex:', rexNotes.data.length); // Buscar notas dos últimos 30 dias const recentNotes = await filter.getRecentNotes(30); console.log('Notas recentes:', recentNotes.data.length); // Busca personalizada const customSearch = await filter.searchNotes({ transcription_status: 'completed', from: '2024-01-01', limit: 50 }); console.log('Busca personalizada:', customSearch.data.length); } catch (error) { console.error('Erro na busca:', error); } } examples(); ``` ### Python ```python theme={null} theme={null} import requests from datetime import datetime, timedelta from typing import Dict, List, Optional class NotesFilter: def __init__(self, api_key: str): self.api_key = api_key self.base_url = "https://api-sandbox.connectvets.com.br/notes/v1" self.session = requests.Session() self.session.headers.update({"X-API-KEY": api_key}) def search_notes(self, filters: Dict = None) -> Dict: """Buscar notas com filtros""" params = {} if filters: # Remover valores vazios params = {k: v for k, v in filters.items() if v is not None and v != ''} response = self.session.get(f"{self.base_url}/notes", params=params) response.raise_for_status() return response.json() def get_completed_notes(self, page: int = 1, limit: int = 10) -> Dict: """Buscar apenas notas processadas""" return self.search_notes({ 'transcription_status': 'completed', 'page': page, 'limit': limit }) def get_notes_by_date_range(self, from_date: str, to_date: str) -> Dict: """Buscar notas por período (YYYY-MM-DD)""" return self.search_notes({ 'from': from_date, 'to': to_date, 'transcription_status': 'completed' }) def get_patient_notes(self, patient_name: str) -> Dict: """Buscar notas de um paciente específico""" return self.search_notes({ 'name': patient_name, 'transcription_status': 'completed' }) def get_recent_notes(self, days: int = 7) -> Dict: """Buscar notas dos últimos X dias""" to_date = datetime.now().strftime('%Y-%m-%d') from_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') return self.get_notes_by_date_range(from_date, to_date) # Exemplos de uso def main(): filter_client = NotesFilter(os.getenv('CONNECTVETS_API_KEY')) try: # Buscar notas de hoje today = datetime.now().strftime('%Y-%m-%d') today_notes = filter_client.get_notes_by_date_range(today, today) print(f"Notas de hoje: {len(today_notes['data'])}") # Buscar notas de um paciente rex_notes = filter_client.get_patient_notes('Rex') print(f"Notas do Rex: {len(rex_notes['data'])}") # Buscar notas dos últimos 30 dias recent_notes = filter_client.get_recent_notes(30) print(f"Notas recentes: {len(recent_notes['data'])}") # Busca personalizada custom_search = filter_client.search_notes({ 'transcription_status': 'completed', 'from': '2024-01-01', 'limit': 50 }) print(f"Busca personalizada: {len(custom_search['data'])}") except Exception as e: print(f"Erro: {e}") if __name__ == "__main__": main() ``` ### PHP ```php theme={null} theme={null} apiKey = $apiKey; } public function searchNotes($filters = []) { // Remover valores vazios $params = array_filter($filters, function($value) { return $value !== null && $value !== ''; }); $url = $this->baseUrl . '/notes'; if (!empty($params)) { $url .= '?' . http_build_query($params); } $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey, 'Content-Type: application/json' ] ]); $response = curl_exec($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); if ($httpCode !== 200) { throw new Exception('Erro na busca: ' . $response); } return json_decode($response, true); } public function getCompletedNotes($page = 1, $limit = 10) { return $this->searchNotes([ 'transcription_status' => 'completed', 'page' => $page, 'limit' => $limit ]); } public function getNotesByDateRange($fromDate, $toDate) { return $this->searchNotes([ 'from' => $fromDate, 'to' => $toDate, 'transcription_status' => 'completed' ]); } public function getPatientNotes($patientName) { return $this->searchNotes([ 'name' => $patientName, 'transcription_status' => 'completed' ]); } public function getRecentNotes($days = 7) { $toDate = date('Y-m-d'); $fromDate = date('Y-m-d', strtotime("-{$days} days")); return $this->getNotesByDateRange($fromDate, $toDate); } } // Exemplos de uso try { $filter = new NotesFilter($_ENV['CONNECTVETS_API_KEY']); // Buscar notas de hoje $today = date('Y-m-d'); $todayNotes = $filter->getNotesByDateRange($today, $today); echo "Notas de hoje: " . count($todayNotes['data']) . "\n"; // Buscar notas de um paciente $rexNotes = $filter->getPatientNotes('Rex'); echo "Notas do Rex: " . count($rexNotes['data']) . "\n"; // Buscar notas dos últimos 30 dias $recentNotes = $filter->getRecentNotes(30); echo "Notas recentes: " . count($recentNotes['data']) . "\n"; } catch (Exception $e) { echo "Erro: " . $e->getMessage() . "\n"; } ?> ``` ## Parâmetros de Filtro Disponíveis | Parâmetro | Tipo | Descrição | Exemplo | | - | - | - | - | | `transcription_status` | String | Status da transcrição | `completed`, `processing`, `failed` | | `name` | String | Nome do paciente (busca parcial) | `Rex`, `Mia` | | `from` | Date | Data inicial (YYYY-MM-DD) | `2024-01-01` | | `to` | Date | Data final (YYYY-MM-DD) | `2024-01-31` | | `days` | Integer | Últimos X dias | `7`, `30` | | `page` | Integer | Página (paginação) | `1`, `2`, `3` | | `limit` | Integer | Limite por página | `10`, `50`, `100` | | `external_id` | String | ID do seu sistema | `CLIENTE_123` | ## Combinando Filtros ### Busca Complexa ```javascript theme={null} theme={null} // Buscar notas processadas do último mês de pacientes específicos const complexSearch = await filter.searchNotes({ transcription_status: 'completed', from: '2024-01-01', to: '2024-01-31', limit: 100 }); // Filtrar ainda mais nos resultados (lado cliente) const dogNotes = complexSearch.data.filter(note => note.name.toLowerCase().includes('rex') || note.name.toLowerCase().includes('bob') ); ``` ### Paginação ```javascript theme={null} theme={null} async function getAllNotes(filters = {}) { const allNotes = []; let page = 1; const limit = 50; while (true) { const response = await filter.searchNotes({ ...filters, page, limit }); if (response.data.length === 0) break; allNotes.push(...response.data); page++; // Limite de segurança if (page > 100) break; } return allNotes; } ``` ## Casos de Uso Práticos ### 1. Relatório Diário ```javascript theme={null} theme={null} async function dailyReport(date) { const notes = await filter.getNotesByDateRange(date, date); const stats = { total: notes.data.length, completed: notes.data.filter(n => n.transcription_status === 'completed').length, failed: notes.data.filter(n => n.transcription_status === 'failed').length }; console.log(`Relatório de ${date}:`); console.log(`Total: ${stats.total}`); console.log(`Processadas: ${stats.completed}`); console.log(`Com erro: ${stats.failed}`); return stats; } ``` ### 2. Buscar Histórico do Paciente ```javascript theme={null} theme={null} async function getPatientHistory(patientName) { const notes = await filter.getPatientNotes(patientName); // Ordenar por data (mais recente primeiro) const sortedNotes = notes.data.sort((a, b) => new Date(b.created_at) - new Date(a.created_at) ); return sortedNotes.map(note => ({ id: note.id, date: note.created_at.split('T')[0], status: note.transcription_status, sections: note.note_sections?.length || 0 })); } ``` ### 3. Monitorar Falhas ```javascript theme={null} theme={null} async function checkFailures() { const failedNotes = await filter.searchNotes({ transcription_status: 'failed', days: 1 // Último dia }); if (failedNotes.data.length > 0) { console.log(`⚠️ ${failedNotes.data.length} notas falharam nas últimas 24h`); failedNotes.data.forEach(note => { console.log(`- ${note.id}: ${note.name} (${note.created_at})`); }); } return failedNotes.data; } ``` ## Otimização de Performance ### Cache de Resultados ```javascript theme={null} theme={null} class CachedNotesFilter extends NotesFilter { constructor(apiKey) { super(apiKey); this.cache = new Map(); this.cacheTimeout = 5 * 60 * 1000; // 5 minutos } async searchNotes(filters = {}) { const cacheKey = JSON.stringify(filters); const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { console.log('📋 Resultado do cache'); return cached.data; } const result = await super.searchNotes(filters); this.cache.set(cacheKey, { data: result, timestamp: Date.now() }); return result; } } ``` *** Voltar aos exemplos básicos Ver documentação completa **Dica**: Use paginação para grandes volumes de dados e implemente cache para melhorar a performance. # Basic integration Source: https://docs.connectvets.com.br/notes/examples/basic-integration # Integração Básica > Exemplo prático de como integrar a ConnectVets Notes API ao seu sistema # Integração Básica com a API Este exemplo mostra como integrar a API em diferentes linguagens de programação para processar consultas veterinárias. ## Configuração Inicial ### Variáveis de Ambiente ```bash theme={null} theme={null} # .env CONNECTVETS_API_KEY=sua_api_key_aqui CONNECTVETS_BASE_URL=https://api-sandbox.connectvets.com.br/notes/v1 ``` ## Implementações por Linguagem ### JavaScript/Node.js ```javascript theme={null} theme={null} const fetch = require('node-fetch'); const FormData = require('form-data'); const fs = require('fs'); class ConnectVetsClient { constructor(apiKey) { this.apiKey = apiKey; this.baseUrl = 'https://api-sandbox.connectvets.com.br/notes/v1'; } async createNote(audioFilePath, metadata = {}) { const formData = new FormData(); formData.append('audio', fs.createReadStream(audioFilePath)); formData.append('metadata', JSON.stringify(metadata)); const response = await fetch(`${this.baseUrl}/notes`, { method: 'POST', headers: { 'X-API-KEY': this.apiKey, ...formData.getHeaders() }, body: formData }); return await response.json(); } async getNote(noteId) { const response = await fetch(`${this.baseUrl}/notes/${noteId}`, { headers: { 'X-API-KEY': this.apiKey, 'Content-Type': 'application/json' } }); return await response.json(); } async waitForCompletion(noteId, maxWaitTime = 300000) { const startTime = Date.now(); while (Date.now() - startTime < maxWaitTime) { const note = await this.getNote(noteId); if (note.data.transcription_status === 'completed') { return note.data; } if (note.data.transcription_status === 'failed') { throw new Error('Processamento falhou'); } await new Promise(resolve => setTimeout(resolve, 10000)); // Aguarda 10s } throw new Error('Timeout: Processamento demorou muito'); } } // Exemplo de uso async function processConsultation() { const client = new ConnectVetsClient(process.env.CONNECTVETS_API_KEY); try { // Criar nota const result = await client.createNote('./audio_consulta.mp3', { patient_name: 'Rex', owner_name: 'João Silva', veterinarian: 'Dr. Santos' }); console.log('Nota criada:', result.data.id); // Aguardar processamento const completedNote = await client.waitForCompletion(result.data.id); // Exibir resultados completedNote.note_sections.forEach(section => { console.log(`${section.title}: ${section.content}`); }); } catch (error) { console.error('Erro:', error.message); } } processConsultation(); ``` ### Python ```python theme={null} theme={null} import requests import time import os from typing import Dict, Any, Optional class ConnectVetsClient: def __init__(self, api_key: str): self.api_key = api_key self.base_url = "https://api-sandbox.connectvets.com.br/notes/v1" self.session = requests.Session() self.session.headers.update({"X-API-KEY": api_key}) def create_note(self, audio_file_path: str, metadata: Dict[str, Any] = None) -> Dict: """Criar uma nova nota com arquivo de áudio""" files = { 'audio': ('audio.mp3', open(audio_file_path, 'rb'), 'audio/mpeg') } data = { 'metadata': str(metadata or {}) } response = self.session.post( f"{self.base_url}/notes", files=files, data=data ) response.raise_for_status() return response.json() def get_note(self, note_id: str) -> Dict: """Obter detalhes de uma nota""" response = self.session.get(f"{self.base_url}/notes/{note_id}") response.raise_for_status() return response.json() def wait_for_completion(self, note_id: str, max_wait_time: int = 300) -> Dict: """Aguardar processamento da nota""" start_time = time.time() while time.time() - start_time < max_wait_time: note = self.get_note(note_id) status = note['data']['transcription_status'] if status == 'completed': return note['data'] elif status == 'failed': raise Exception('Processamento falhou') time.sleep(10) # Aguarda 10 segundos raise Exception('Timeout: Processamento demorou muito') # Exemplo de uso def main(): client = ConnectVetsClient(os.getenv('CONNECTVETS_API_KEY')) try: # Criar nota result = client.create_note('./audio_consulta.mp3', { 'patient_name': 'Rex', 'owner_name': 'João Silva', 'veterinarian': 'Dr. Santos' }) print(f"Nota criada: {result['data']['id']}") # Aguardar processamento completed_note = client.wait_for_completion(result['data']['id']) # Exibir resultados for section in completed_note['note_sections']: print(f"{section['title']}: {section['content']}") except Exception as e: print(f"Erro: {e}") if __name__ == "__main__": main() ``` ### PHP ```php theme={null} theme={null} apiKey = $apiKey; } public function createNote($audioFilePath, $metadata = []) { $curl = curl_init(); $postFields = [ 'audio' => new CURLFile($audioFilePath), 'metadata' => json_encode($metadata) ]; curl_setopt_array($curl, [ CURLOPT_URL => $this->baseUrl . '/notes', CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postFields, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey ] ]); $response = curl_exec($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); if ($httpCode !== 200) { throw new Exception('Erro na criação da nota: ' . $response); } return json_decode($response, true); } public function getNote($noteId) { $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => $this->baseUrl . '/notes/' . $noteId, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey, 'Content-Type: application/json' ] ]); $response = curl_exec($curl); curl_close($curl); return json_decode($response, true); } public function waitForCompletion($noteId, $maxWaitTime = 300) { $startTime = time(); while (time() - $startTime < $maxWaitTime) { $note = $this->getNote($noteId); $status = $note['data']['transcription_status']; if ($status === 'completed') { return $note['data']; } if ($status === 'failed') { throw new Exception('Processamento falhou'); } sleep(10); // Aguarda 10 segundos } throw new Exception('Timeout: Processamento demorou muito'); } } // Exemplo de uso try { $client = new ConnectVetsClient($_ENV['CONNECTVETS_API_KEY']); // Criar nota $result = $client->createNote('./audio_consulta.mp3', [ 'patient_name' => 'Rex', 'owner_name' => 'João Silva', 'veterinarian' => 'Dr. Santos' ]); echo "Nota criada: " . $result['data']['id'] . "\n"; // Aguardar processamento $completedNote = $client->waitForCompletion($result['data']['id']); // Exibir resultados foreach ($completedNote['note_sections'] as $section) { echo $section['title'] . ": " . $section['content'] . "\n"; } } catch (Exception $e) { echo "Erro: " . $e->getMessage() . "\n"; } ?> ``` ## Tratamento de Erros ### Códigos de Status HTTP | Código | Significado | Como tratar | | - | - | - | | 200 | Sucesso | Processar resposta normalmente | | 400 | Dados inválidos | Verificar parâmetros enviados | | 401 | API Key inválida | Verificar autenticação | | 413 | Arquivo muito grande | Reduzir tamanho do áudio | | 429 | Muitas requisições | Implementar retry com backoff | | 500 | Erro interno | Tentar novamente mais tarde | ### Exemplo de Tratamento ```javascript theme={null} theme={null} async function makeRequest(url, options) { const maxRetries = 3; let attempt = 0; while (attempt < maxRetries) { try { const response = await fetch(url, options); if (response.status === 429) { // Rate limit - aguardar antes de tentar novamente const retryAfter = response.headers.get('Retry-After') || '30'; await new Promise(resolve => setTimeout(resolve, parseInt(retryAfter) * 1000)); attempt++; continue; } if (!response.ok) { throw new Error(`HTTP ${response.status}: ${await response.text()}`); } return await response.json(); } catch (error) { attempt++; if (attempt >= maxRetries) { throw error; } // Backoff exponencial await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000)); } } } ``` ## Próximos Passos Receba notificações automáticas Veja todos os endpoints disponíveis *** **Dica**: Use webhooks ao invés de polling para uma integração mais eficiente e em tempo real. # Webhook handling Source: https://docs.connectvets.com.br/notes/examples/webhook-handling # Implementação de Webhooks > Como receber notificações automáticas da ConnectVets Notes API # Webhooks para Notificações Automáticas Os webhooks eliminam a necessidade de fazer polling constante, notificando sua aplicação quando eventos importantes acontecem. ## Eventos Disponíveis | Evento | Quando ocorre | | - | - | | `note.completed` | Quando o processamento é concluído | | `note.failed` | Quando há erro no processamento | | `note.processing` | Quando o processamento inicia | ## Estrutura do Payload ```json theme={null} theme={null} { "event": "note.completed", "timestamp": "2024-02-14T18:25:43Z", "data": { "note_id": "550e8400-e29b-41d4-a716-446655440000", "status": "completed", "external_id": "CLIENTE_123" } } ``` ## Implementações ### Node.js + Express ```javascript theme={null} theme={null} const express = require('express'); const crypto = require('crypto'); const app = express(); // Middleware para webhook (raw body) app.use('/webhook', express.raw({ type: 'application/json' })); // Sua chave secreta do webhook (fornecida pela equipe ConnectVets) const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; function verifySignature(payload, signature, secret) { const expectedSignature = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature.replace('sha256=', ''), 'hex'), Buffer.from(expectedSignature, 'hex') ); } app.post('/webhook', (req, res) => { const signature = req.headers['x-connectvets-signature']; const payload = req.body; // Verificar assinatura if (!verifySignature(payload, signature, WEBHOOK_SECRET)) { return res.status(401).send('Signature verification failed'); } const event = JSON.parse(payload); // Processar evento switch (event.event) { case 'note.completed': handleNoteCompleted(event.data); break; case 'note.failed': handleNoteFailed(event.data); break; case 'note.processing': handleNoteProcessing(event.data); break; } res.status(200).send('OK'); }); async function handleNoteCompleted(data) { console.log(`Nota ${data.note_id} processada com sucesso`); // Buscar detalhes completos da nota const noteDetails = await fetchNoteDetails(data.note_id); // Atualizar seu sistema await updateLocalDatabase(noteDetails); // Notificar usuário await notifyUser(data.external_id, 'completed'); } async function handleNoteFailed(data) { console.log(`Erro no processamento da nota ${data.note_id}`); // Registrar erro await logError(data); // Notificar sobre o erro await notifyUser(data.external_id, 'failed'); } async function handleNoteProcessing(data) { console.log(`Processamento iniciado para nota ${data.note_id}`); // Atualizar status na interface await updateStatus(data.note_id, 'processing'); } app.listen(3000, () => { console.log('Webhook listener rodando na porta 3000'); }); ``` ### Python + Flask ```python theme={null} theme={null} from flask import Flask, request, abort import hashlib import hmac import json import os app = Flask(__name__) WEBHOOK_SECRET = os.getenv('WEBHOOK_SECRET') def verify_signature(payload, signature, secret): expected_signature = hmac.new( secret.encode('utf-8'), payload, hashlib.sha256 ).hexdigest() received_signature = signature.replace('sha256=', '') return hmac.compare_digest(expected_signature, received_signature) @app.route('/webhook', methods=['POST']) def webhook(): signature = request.headers.get('X-ConnectVets-Signature') payload = request.get_data() # Verificar assinatura if not verify_signature(payload, signature, WEBHOOK_SECRET): abort(401) event = json.loads(payload) # Processar evento if event['event'] == 'note.completed': handle_note_completed(event['data']) elif event['event'] == 'note.failed': handle_note_failed(event['data']) elif event['event'] == 'note.processing': handle_note_processing(event['data']) return 'OK', 200 def handle_note_completed(data): print(f"Nota {data['note_id']} processada com sucesso") # Buscar detalhes da nota note_details = fetch_note_details(data['note_id']) # Atualizar banco de dados local update_local_database(note_details) # Enviar notificação notify_user(data.get('external_id'), 'completed') def handle_note_failed(data): print(f"Erro no processamento da nota {data['note_id']}") # Registrar erro log_error(data) # Notificar sobre erro notify_user(data.get('external_id'), 'failed') def handle_note_processing(data): print(f"Processamento iniciado para nota {data['note_id']}") # Atualizar status update_status(data['note_id'], 'processing') if __name__ == '__main__': app.run(host='0.0.0.0', port=3000) ``` ### PHP ```php theme={null} theme={null} ``` ## Configuração do Webhook ### Solicitar Configuração Para configurar webhooks, entre em contato com nossa equipe via [WhatsApp (+55 31 8512-7147)](https://wa.me/553185127147) informando: * **URL do endpoint**: `https://seudominio.com/webhook` * **Eventos desejados**: `["note.completed", "note.failed", "note.processing"]` * **Ambiente**: produção ou desenvolvimento Nossa equipe configurará os webhooks e fornecerá a chave secreta para validação. ### Validar Webhook ```bash theme={null} theme={null} # Testar se seu endpoint está funcionando curl -X POST "https://seudominio.com/webhook" \ -H "Content-Type: application/json" \ -H "X-ConnectVets-Signature: sha256=test" \ -d '{"event":"test","data":{}}' ``` ## Segurança e Boas Práticas ### 1. Sempre Verificar Assinatura ```javascript theme={null} theme={null} // ❌ NUNCA faça isso app.post('/webhook', (req, res) => { const event = req.body; processEvent(event); // Sem verificação! }); // ✅ SEMPRE faça isso app.post('/webhook', (req, res) => { if (!verifySignature(req.body, req.headers['x-connectvets-signature'], SECRET)) { return res.status(401).send('Unauthorized'); } processEvent(req.body); }); ``` ### 2. Ser Idempotente ```javascript theme={null} theme={null} const processedEvents = new Set(); app.post('/webhook', (req, res) => { const event = JSON.parse(req.body); // Evitar processar o mesmo evento duas vezes if (processedEvents.has(event.id)) { return res.status(200).send('Already processed'); } processEvent(event); processedEvents.add(event.id); res.status(200).send('OK'); }); ``` ### 3. Responder Rapidamente ```javascript theme={null} theme={null} app.post('/webhook', (req, res) => { // Responder imediatamente res.status(200).send('OK'); // Processar assincronamente setImmediate(() => { processEvent(JSON.parse(req.body)); }); }); ``` ## Testando Webhooks ### Usando ngrok para Desenvolvimento ```bash theme={null} theme={null} # Instalar ngrok npm install -g ngrok # Expor porta local ngrok http 3000 # Usar URL do ngrok no webhook # https://abc123.ngrok.io/webhook ``` ### Logs de Debug ```javascript theme={null} theme={null} app.post('/webhook', (req, res) => { console.log('Headers:', req.headers); console.log('Body:', req.body.toString()); // ... resto do código }); ``` *** Entenda como funcionam os webhooks Voltar ao exemplo de integração **Importante**: Sempre use HTTPS em produção e mantenha sua chave secreta segura. # Audio requirements Source: https://docs.connectvets.com.br/notes/integration/audio-requirements # Requisitos de Áudio > Especificações técnicas e melhores práticas para arquivos de áudio no ConnectVets Notes ## Visão Geral O **ConnectVets Notes** processa arquivos de áudio de consultas veterinárias para gerar transcrições e análises estruturadas. A qualidade do áudio impacta diretamente na precisão dos resultados. **Recomendação**: Áudios de **boa qualidade** resultam em transcrições **95%+ precisas** e análises mais detalhadas. ## Formatos Suportados ### Formatos Aceitos | Formato | Extensão | Recomendado | Qualidade | Tamanho | | - | - | - | - | - | | **MP3** | `.mp3` | ✅ **SIM** | Boa | Médio | | **WAV** | `.wav` | ⭐ **IDEAL** | Excelente | Grande | | **M4A** | `.m4a` | ✅ **SIM** | Boa | Médio | | **AAC** | `.aac` | ✅ **SIM** | Boa | Pequeno | | **OGG** | `.ogg` | ⚠️ Limitado | Boa | Médio | | **WEBM** | `.webm` | ⚠️ Limitado | Boa | Médio | ### Formato Recomendado: WAV ```bash theme={null} theme={null} # Especificações ideais para WAV Formato: WAV (PCM) Sample Rate: 44.1 kHz ou 48 kHz Bit Depth: 16-bit ou 24-bit Canais: Mono ou Estéreo Duração máxima: 60 minutos Tamanho máximo: 25 MB ``` ## Especificações Técnicas ### Qualidade de Áudio * **Sample Rate**: 16 kHz+ * **Bit Depth**: 16-bit+ * **Duração**: 10 segundos - 60 minutos * **Tamanho**: Máximo 100 MB * **SNR**: 15 dB+ (Signal-to-Noise Ratio) * **Sample Rate**: 44.1 kHz ou 48 kHz * **Bit Depth**: 24-bit * **Duração**: 5-45 minutos * **Tamanho**: 20-80 MB * **SNR**: 25 dB+ ### Limitações de Tamanho ```javascript theme={null} theme={null} // Validação de arquivo no frontend function validateAudioFile(file) { const maxSize = 25 * 1024 * 1024; // 100 MB const minSize = 1 * 1024; // 1 KB const supportedFormats = ['mp3', 'wav', 'm4a', 'aac']; // Verificar tamanho if (file.size > maxSize) { return { valid: false, error: 'Arquivo muito grande. Máximo: 100 MB' }; } if (file.size < minSize) { return { valid: false, error: 'Arquivo muito pequeno. Mínimo: 1 KB' }; } // Verificar formato const extension = file.name.toLowerCase().split('.').pop(); if (!supportedFormats.includes(extension)) { return { valid: false, error: `Formato não suportado. Use: ${supportedFormats.join(', ')}` }; } return { valid: true }; } // Uso da validação const fileInput = document.getElementById('audio-file'); fileInput.addEventListener('change', (event) => { const file = event.target.files[0]; const validation = validateAudioFile(file); if (!validation.valid) { alert(validation.error); return; } // Arquivo válido, prosseguir com upload uploadAudio(file); }); ``` ## Configuração de Gravação ### Configurações do Microfone **Ideal para clínicas:** * Microfone condensador ou dinâmico * Distância: 15-30 cm da boca * Padrão polar: Cardioide ou supercardioide * Redução de ruído ativa **Exemplos:** * Audio-Technica ATR2100x-USB * Blue Yeti ou Blue Yeti Nano * Shure SM58 (com interface) * Rode PodMic **Para gravação móvel:** * iPhone: Usar app de gravação nativo * Android: Usar Google Recorder ou similar * Configurar qualidade máxima * Segurar device a 20-30 cm **Configurações iOS:** ``` Configurações > Gravação de Voz Qualidade: Sem Compressão Formato: WAV (se disponível) ``` **Configurações Android:** ``` Configurações do App de Gravação Qualidade: Alta (48kHz/24-bit) Formato: WAV ou FLAC ``` **Para uso ocasional:** * Headset com microfone integrado * Posicionar mic próximo à boca * Evitar microfones muito baratos **Modelos recomendados:** * SteelSeries Arctis 7 * HyperX Cloud II * Logitech G Pro X ### Ambiente de Gravação ```mermaid theme={null} theme={null} graph TD A[Ambiente Ideal] --> B[Sala Fechada] A --> C[Pouco Ruído] A --> D[Sem Eco] B --> E[Porta fechada] B --> F[Janelas fechadas] C --> G[Ar condicionado baixo] C --> H[Equipamentos silenciados] C --> I[Sem conversas paralelas] D --> J[Sala com móveis] D --> K[Cortinas ou carpete] D --> L[Evitar salas vazias] ``` ### Configuração de Software ```javascript WebRTC (Browser) theme={null} theme={null} // Configuração para gravação no navegador async function configureAudioRecording() { const constraints = { audio: { sampleRate: 48000, // Sample rate alto channelCount: 1, // Mono (suficiente para voz) echoCancellation: true, // Cancelamento de eco noiseSuppression: true, // Supressão de ruído autoGainControl: true, // Controle automático de ganho sampleSize: 16 // 16-bit depth } }; try { const stream = await navigator.mediaDevices.getUserMedia(constraints); const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus', audioBitsPerSecond: 128000 // 128 kbps }); return mediaRecorder; } catch (error) { console.error('Erro ao configurar gravação:', error); } } ``` ```javascript React Native theme={null} theme={null} // Configuração para React Native import { AudioRecorder, AudioUtils } from 'react-native-audio'; const audioPath = AudioUtils.DocumentDirectoryPath + '/consultation.wav'; const audioSettings = { SampleRate: 48000, Channels: 1, AudioQuality: 'High', AudioEncoding: 'wav', OutputFormat: 'wav', MeteringEnabled: true, IncludeBase64: false, AudioEncodingBitRate: 256000 }; // Iniciar gravação AudioRecorder.prepareRecordingAtPath(audioPath, audioSettings); AudioRecorder.startRecording(); ``` ```python Python (pyaudio) theme={null} theme={null} import pyaudio import wave # Configuração para gravação desktop def configure_audio_recording(): chunk = 1024 format = pyaudio.paInt16 # 16-bit channels = 1 # Mono rate = 48000 # 48kHz sample rate audio = pyaudio.PyAudio() stream = audio.open( format=format, channels=channels, rate=rate, input=True, frames_per_buffer=chunk ) return audio, stream, (chunk, format, channels, rate) # Gravar áudio def record_audio(duration_seconds): audio, stream, config = configure_audio_recording() chunk, format, channels, rate = config frames = [] for _ in range(0, int(rate / chunk * duration_seconds)): data = stream.read(chunk) frames.append(data) # Salvar arquivo with wave.open('consultation.wav', 'wb') as wf: wf.setnchannels(channels) wf.setsampwidth(audio.get_sample_size(format)) wf.setframerate(rate) wf.writeframes(b''.join(frames)) stream.close() audio.terminate() ``` ## Otimização de Áudio ### Pré-processamento ```javascript theme={null} theme={null} // Normalizar volume do áudio antes de enviar async function normalizeAudio(audioBlob) { const audioContext = new AudioContext(); const arrayBuffer = await audioBlob.arrayBuffer(); const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); // Encontrar pico máximo const channelData = audioBuffer.getChannelData(0); let maxPeak = 0; for (let i = 0; i < channelData.length; i++) { const sample = Math.abs(channelData[i]); if (sample > maxPeak) { maxPeak = sample; } } // Normalizar se necessário if (maxPeak > 0 && maxPeak < 0.95) { const normalizeRatio = 0.95 / maxPeak; for (let i = 0; i < channelData.length; i++) { channelData[i] *= normalizeRatio; } } return audioBuffer; } ``` ### Compressão Inteligente ```bash theme={null} theme={null} # Usando FFmpeg para otimizar áudio # Converter para formato ideal mantendo qualidade # MP3 de alta qualidade ffmpeg -i input.wav -c:a libmp3lame -b:a 192k -ar 48000 output.mp3 # WAV otimizado ffmpeg -i input.wav -c:a pcm_s16le -ar 48000 -ac 1 output.wav # M4A de alta qualidade ffmpeg -i input.wav -c:a aac -b:a 256k -ar 48000 output.m4a # Reduzir ruído de fundo ffmpeg -i input.wav -af "afftdn=nf=-40" output_clean.wav ``` ## Casos de Uso Específicos ### Consulta Individual ```javascript theme={null} theme={null} // Configuração para consulta 1:1 (veterinário + dono) const consultationConfig = { environment: 'quiet_room', participants: 2, microphoneDistance: '20-30cm', recordingDuration: '10-30 minutes', recommendedFormat: 'WAV 48kHz/16-bit', settings: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } }; ``` ### Cirurgia ```javascript theme={null} theme={null} // Configuração para ambiente cirúrgico const surgeryConfig = { environment: 'operating_room', participants: '3-5', challenges: ['equipment_noise', 'air_conditioning', 'multiple_speakers'], microphoneDistance: '15-25cm', recordingDuration: '30-120 minutes', recommendedFormat: 'WAV 48kHz/24-bit', settings: { echoCancellation: false, // Pode interferir com múltiplas vozes noiseSuppression: true, autoGainControl: false, // Manter ganho fixo manualGain: 0.7 } }; ``` ### Telemedicina ```javascript theme={null} theme={null} // Configuração para consultas remotas const telemedicineConfig = { environment: 'home_office', participants: 2, challenges: ['internet_latency', 'compression', 'echo'], recordingSource: 'video_call_recording', recommendedFormat: 'MP3 192kbps+', preprocessing: { noiseReduction: true, volumeNormalization: true, echoRemoval: true } }; ``` ## Resolução de Problemas ### Problemas Comuns **Sintomas:** * Transcrição incompleta * Erro: "Audio volume too low" **Soluções:** ```javascript theme={null} theme={null} // Verificar nível antes de enviar function checkAudioLevel(audioBuffer) { const channelData = audioBuffer.getChannelData(0); let rms = 0; for (let i = 0; i < channelData.length; i++) { rms += channelData[i] * channelData[i]; } rms = Math.sqrt(rms / channelData.length); if (rms < 0.01) { return { status: 'too_low', message: 'Aumentar volume do microfone' }; } return { status: 'ok' }; } ``` **Ajustes:** * Aproximar microfone * Aumentar ganho do sistema * Falar mais alto/claro **Sintomas:** * Clipping/distorção * Erro: "Audio clipping detected" **Soluções:** ```javascript theme={null} theme={null} function detectClipping(audioBuffer) { const channelData = audioBuffer.getChannelData(0); let clippedSamples = 0; for (let i = 0; i < channelData.length; i++) { if (Math.abs(channelData[i]) >= 0.99) { clippedSamples++; } } const clippingRatio = clippedSamples / channelData.length; if (clippingRatio > 0.001) { // 0.1% de clipping return { status: 'clipping', message: 'Reduzir ganho do microfone' }; } return { status: 'ok' }; } ``` **Ajustes:** * Reduzir ganho * Afastar microfone * Usar limitador/compressor **Sintomas:** * Transcrição confusa * Erro: "Multiple speakers overlapping" **Soluções:** * Orientar sobre fala alternada * Usar microfone direcional * Posicionar mic estrategicamente ```javascript theme={null} theme={null} // Detectar múltiplas vozes function detectMultipleSpeakers(audioBuffer) { // Análise de frequência para detectar sobreposição const fftSize = 2048; const analyser = new AnalyserNode(audioContext, { fftSize }); // Implementar detecção de sobreposição // Retornar aviso se detectado } ``` **Sintomas:** * Transcrição com ruídos * Erro: "Background noise too high" **Soluções:** * Fechar portas/janelas * Desligar equipamentos * Usar noise gate ```bash theme={null} theme={null} # Reduzir ruído com FFmpeg ffmpeg -i noisy_audio.wav -af "afftdn=nf=-40:nt=w" clean_audio.wav ``` ### Ferramentas de Diagnóstico ```javascript theme={null} theme={null} // Analisador completo de qualidade de áudio class AudioQualityAnalyzer { static async analyze(audioFile) { const audioContext = new AudioContext(); const arrayBuffer = await audioFile.arrayBuffer(); const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); const analysis = { duration: audioBuffer.duration, sampleRate: audioBuffer.sampleRate, channels: audioBuffer.numberOfChannels, quality: this.assessQuality(audioBuffer), issues: this.detectIssues(audioBuffer) }; return analysis; } static assessQuality(audioBuffer) { const channelData = audioBuffer.getChannelData(0); // Calcular RMS (volume médio) let rms = 0; for (let i = 0; i < channelData.length; i++) { rms += channelData[i] * channelData[i]; } rms = Math.sqrt(rms / channelData.length); // Calcular SNR estimado const snr = this.estimateSNR(channelData); // Detectar clipping const clipping = this.detectClipping(channelData); let quality = 'good'; if (rms < 0.01 || snr < 15 || clipping > 0.01) { quality = 'poor'; } else if (rms < 0.05 || snr < 20 || clipping > 0.001) { quality = 'fair'; } return { score: quality, rms: rms, snr: snr, clipping: clipping }; } static detectIssues(audioBuffer) { const issues = []; const channelData = audioBuffer.getChannelData(0); // Volume muito baixo const rms = this.calculateRMS(channelData); if (rms < 0.01) { issues.push({ type: 'low_volume', severity: 'high', message: 'Volume muito baixo - aproximar microfone ou aumentar ganho' }); } // Clipping const clipping = this.detectClipping(channelData); if (clipping > 0.001) { issues.push({ type: 'clipping', severity: 'high', message: 'Distorção detectada - reduzir ganho do microfone' }); } // Duração muito curta if (audioBuffer.duration < 10) { issues.push({ type: 'short_duration', severity: 'medium', message: 'Áudio muito curto - mínimo recomendado: 10 segundos' }); } return issues; } } // Uso do analisador audioInput.addEventListener('change', async (event) => { const file = event.target.files[0]; const analysis = await AudioQualityAnalyzer.analyze(file); console.log('Análise de qualidade:', analysis); if (analysis.issues.length > 0) { console.warn('Problemas detectados:', analysis.issues); // Mostrar alertas para o usuário } }); ``` ## Melhores Práticas ### Antes da Gravação * ✅ Testar equipamento de áudio * ✅ Verificar níveis de ruído * ✅ Posicionar microfone corretamente * ✅ Orientar participantes sobre fala clara * ✅ Configurar software de gravação * ✅ Sample rate: 48kHz+ * ✅ Bit depth: 16-bit+ * ✅ Formato: WAV ou MP3 192kbps+ * ✅ Espaço em disco suficiente * ✅ Bateria carregada (dispositivos móveis) ### Durante a Consulta * 🎯 **Falar claramente** e em ritmo normal * 🎯 **Evitar interrupções** simultâneas * 🎯 **Monitorar níveis** de áudio se possível * 🎯 **Manter distância** consistente do microfone * 🎯 **Minimizar ruídos** de papel, objetos, etc. ### Após a Gravação ```javascript theme={null} theme={null} // Verificação pós-gravação async function postRecordingCheck(audioFile) { const checks = { fileSize: audioFile.size > 1024 && audioFile.size < 100 * 1024 * 1024, duration: await getAudioDuration(audioFile), format: ['mp3', 'wav', 'm4a', 'aac'].includes(getFileExtension(audioFile)), quality: await AudioQualityAnalyzer.analyze(audioFile) }; const passed = checks.fileSize && checks.duration >= 10 && checks.duration <= 3600 && checks.format && checks.quality.score !== 'poor'; return { passed, checks, recommendations: generateRecommendations(checks) }; } ``` ## Próximos Passos Como lidar com falhas de processamento Voltar ao guia de integração Endpoint para criar notas Código funcionando completo # Error handling Source: https://docs.connectvets.com.br/notes/integration/error-handling # Tratamento de Erros > Como lidar com falhas, códigos de erro e implementar estratégias de recovery robustas ## Visão Geral O **ConnectVets Notes** utiliza códigos de status HTTP padrão e retorna respostas estruturadas para todos os tipos de erro. Uma estratégia robusta de tratamento de erros é essencial para uma integração confiável. **Boa prática**: Sempre implemente **retry** automático com **backoff exponencial** para erros temporários. ## Códigos de Status HTTP ### Códigos de Sucesso (2xx) | Código | Significado | Exemplo | | - | - | - | | `200` | **OK** - Requisição bem-sucedida | `GET /notes` retorna lista | | `201` | **Created** - Recurso criado com sucesso | `POST /notes` cria nova nota | | `204` | **No Content** - Operação bem-sucedida, sem conteúdo | `DELETE /notes/{id}` | ### Códigos de Erro do Cliente (4xx) * `401` **Unauthorized** - API Key inválida * `403` **Forbidden** - Sem permissão para a operação * `429` **Too Many Requests** - Rate limit atingido * `400` **Bad Request** - Dados inválidos * `404` **Not Found** - Recurso não encontrado * `413` **Payload Too Large** - Arquivo muito grande * `422` **Unprocessable Entity** - Dados mal formados ### Códigos de Erro do Servidor (5xx) | Código | Significado | Ação Recomendada | | - | - | - | | `500` | **Internal Server Error** | Retry com backoff | | `502` | **Bad Gateway** | Retry após alguns segundos | | `503` | **Service Unavailable** | Retry após delay maior | | `504` | **Gateway Timeout** | Retry com timeout maior | ## Estrutura de Resposta de Erro ### Formato Padrão ```json theme={null} theme={null} { "error": { "code": "validation_error", "message": "O arquivo de áudio é obrigatório", "details": { "field": "audio", "reason": "missing_required_field" }, "request_id": "req_1234567890abcdef", "timestamp": "2024-02-14T18:25:43Z" } } ``` ### Códigos de Erro Específicos **`invalid_api_key`** ```json theme={null} theme={null} { "error": { "code": "invalid_api_key", "message": "API key inválida ou expirada", "details": { "provided_key": "cvn_live_abc123...", "hint": "Verifique se a chave está correta e ativa" } } } ``` **`insufficient_permissions`** ```json theme={null} theme={null} { "error": { "code": "insufficient_permissions", "message": "Chave não tem permissão para esta operação", "details": { "required_permission": "write", "current_permission": "read" } } } ``` **`file_too_large`** ```json theme={null} theme={null} { "error": { "code": "file_too_large", "message": "Arquivo excede o tamanho máximo permitido", "details": { "file_size": 104857600, "max_size": 104857600, "max_size_mb": 100 } } } ``` **`unsupported_format`** ```json theme={null} theme={null} { "error": { "code": "unsupported_format", "message": "Formato de arquivo não suportado", "details": { "provided_format": "txt", "supported_formats": ["mp3", "wav", "m4a", "aac"] } } } ``` **`audio_quality_poor`** ```json theme={null} theme={null} { "error": { "code": "audio_quality_poor", "message": "Qualidade do áudio insuficiente para processamento", "details": { "issues": ["volume_too_low", "excessive_noise"], "recommendations": [ "Aumentar volume do microfone", "Reduzir ruído de fundo" ] } } } ``` **`audio_duration_invalid`** ```json theme={null} theme={null} { "error": { "code": "audio_duration_invalid", "message": "Duração do áudio fora dos limites permitidos", "details": { "duration": 5, "min_duration": 10, "max_duration": 3600 } } } ``` **`quota_exceeded`** ```json theme={null} theme={null} { "error": { "code": "quota_exceeded", "message": "Cota mensal de processamento excedida", "details": { "current_usage": 1500, "quota_limit": 1000, "reset_date": "2024-03-01T00:00:00Z" } } } ``` **`subscription_expired`** ```json theme={null} theme={null} { "error": { "code": "subscription_expired", "message": "Assinatura expirada", "details": { "expired_at": "2024-02-01T00:00:00Z", "renewal_url": "https://notes.connectvets.com.br/billing" } } } ``` ## Implementação de Tratamento de Erros ### Cliente Base com Error Handling ```javascript theme={null} theme={null} class ConnectVetsClient { constructor(apiKey, options = {}) { this.apiKey = apiKey; this.baseUrl = options.baseUrl || 'https://api.connectvets.com'; this.maxRetries = options.maxRetries || 3; this.retryDelay = options.retryDelay || 1000; } async request(endpoint, options = {}) { const url = `${this.baseUrl}${endpoint}`; const config = { ...options, headers: { 'X-API-KEY': this.apiKey, ...options.headers } }; return this.executeWithRetry(() => fetch(url, config)); } async executeWithRetry(operation, attempt = 1) { try { const response = await operation(); // Verificar se a resposta é bem-sucedida if (response.ok) { return response.json(); } // Tratar erro baseado no status const errorData = await response.json().catch(() => ({})); const error = new ConnectVetsError(response.status, errorData, response); // Verificar se deve tentar novamente if (this.shouldRetry(error, attempt)) { return this.retryRequest(operation, attempt); } throw error; } catch (error) { // Erros de rede ou outros if (error instanceof ConnectVetsError) { throw error; } // Retry para erros de rede if (attempt < this.maxRetries) { return this.retryRequest(operation, attempt); } throw new ConnectVetsError(0, { code: 'network_error', message: error.message }); } } shouldRetry(error, attempt) { // Não retry se já atingiu o máximo if (attempt >= this.maxRetries) { return false; } // Retry para erros de servidor (5xx) if (error.status >= 500) { return true; } // Retry para rate limit if (error.status === 429) { return true; } // Retry para erros específicos const retryableCodes = [ 'service_unavailable', 'gateway_timeout', 'processing_timeout' ]; return retryableCodes.includes(error.code); } async retryRequest(operation, attempt) { const delay = this.calculateDelay(attempt); console.log(`⏳ Tentativa ${attempt + 1} em ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); return this.executeWithRetry(operation, attempt + 1); } calculateDelay(attempt) { // Backoff exponencial com jitter const baseDelay = this.retryDelay; const exponentialDelay = baseDelay * Math.pow(2, attempt - 1); const jitter = Math.random() * 0.1 * exponentialDelay; return Math.min(exponentialDelay + jitter, 30000); // Max 30s } } // Classe de erro customizada class ConnectVetsError extends Error { constructor(status, errorData, response = null) { const message = errorData.error?.message || errorData.message || 'Erro desconhecido'; super(message); this.name = 'ConnectVetsError'; this.status = status; this.code = errorData.error?.code || errorData.code || 'unknown_error'; this.details = errorData.error?.details || errorData.details || {}; this.requestId = errorData.error?.request_id || errorData.request_id; this.response = response; } isRetryable() { return this.status >= 500 || this.status === 429; } isAuthError() { return this.status === 401 || this.status === 403; } isValidationError() { return this.status === 400 || this.status === 422; } isQuotaError() { return this.code === 'quota_exceeded' || this.code === 'subscription_expired'; } } ``` ### Uso do Cliente com Error Handling ```javascript theme={null} theme={null} const client = new ConnectVetsClient(process.env.CONNECTVETS_API_KEY, { maxRetries: 3, retryDelay: 2000 }); async function createNoteWithErrorHandling(audioFile, metadata) { try { console.log('📤 Enviando áudio para processamento...'); const formData = new FormData(); formData.append('audio', audioFile); formData.append('metadata', JSON.stringify(metadata)); const note = await client.request('/notes', { method: 'POST', body: formData }); console.log('✅ Nota criada com sucesso:', note.id); return note; } catch (error) { console.error('❌ Erro ao criar nota:', error.message); // Tratar diferentes tipos de erro if (error.isAuthError()) { handleAuthError(error); } else if (error.isValidationError()) { handleValidationError(error); } else if (error.isQuotaError()) { handleQuotaError(error); } else if (error.isRetryable()) { handleRetryableError(error); } else { handleUnknownError(error); } throw error; } } function handleAuthError(error) { console.error('🔐 Erro de autenticação:', error.message); if (error.code === 'invalid_api_key') { // Notificar sobre API key inválida notifyUser('API Key inválida. Verifique suas configurações.'); } else if (error.code === 'insufficient_permissions') { // Notificar sobre permissões notifyUser('Sem permissão para esta operação. Upgrade necessário.'); } } function handleValidationError(error) { console.error('📝 Erro de validação:', error.message); const { details } = error; if (details.field === 'audio') { notifyUser('Problema com o arquivo de áudio. Verifique o formato e tamanho.'); } else if (details.field === 'metadata') { notifyUser('Metadados inválidos. Verifique os campos obrigatórios.'); } // Mostrar detalhes específicos console.log('📋 Detalhes:', details); } function handleQuotaError(error) { console.error('💰 Erro de cota:', error.message); if (error.code === 'quota_exceeded') { const { current_usage, quota_limit, reset_date } = error.details; notifyUser(`Cota excedida (${current_usage}/${quota_limit}). Renova em ${reset_date}`); } else if (error.code === 'subscription_expired') { const { renewal_url } = error.details; notifyUser('Assinatura expirada. Renove sua assinatura.', renewal_url); } } function handleRetryableError(error) { console.error('🔄 Erro temporário:', error.message); notifyUser('Erro temporário. Tentando novamente...'); } function handleUnknownError(error) { console.error('❓ Erro desconhecido:', error); // Reportar erro para monitoramento reportError(error); notifyUser('Erro interno. Nossa equipe foi notificada.'); } ``` ## Estratégias de Recovery ### Retry com Backoff Exponencial ```javascript theme={null} theme={null} class RetryHandler { static async executeWithRetry( operation, options = {} ) { const { maxRetries = 3, initialDelay = 1000, maxDelay = 30000, backoffFactor = 2, jitter = true } = options; let lastError; for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error; // Não retry para erros não retryable if (!this.isRetryable(error)) { throw error; } // Última tentativa if (attempt === maxRetries - 1) { break; } // Calcular delay const delay = this.calculateDelay( attempt, initialDelay, maxDelay, backoffFactor, jitter ); console.log(`⏳ Retry ${attempt + 1}/${maxRetries} em ${delay}ms`); await this.sleep(delay); } } throw lastError; } static isRetryable(error) { // Erros de rede if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') { return true; } // Erros HTTP retryable if (error.status >= 500 || error.status === 429) { return true; } // Códigos específicos retryable const retryableCodes = [ 'service_unavailable', 'gateway_timeout', 'processing_timeout', 'temporary_error' ]; return retryableCodes.includes(error.code); } static calculateDelay(attempt, initial, max, factor, jitter) { let delay = initial * Math.pow(factor, attempt); if (jitter) { // Adicionar jitter de ±25% const jitterAmount = delay * 0.25; delay += (Math.random() - 0.5) * jitterAmount * 2; } return Math.min(Math.max(delay, 0), max); } static sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } // Uso do RetryHandler async function uploadAudioWithRetry(audioFile, metadata) { return RetryHandler.executeWithRetry( () => client.createNote(audioFile, metadata), { maxRetries: 5, initialDelay: 2000, maxDelay: 60000, backoffFactor: 2, jitter: true } ); } ``` ### Circuit Breaker Pattern ```javascript theme={null} theme={null} class CircuitBreaker { constructor(options = {}) { this.failureThreshold = options.failureThreshold || 5; this.resetTimeout = options.resetTimeout || 60000; this.monitoringPeriod = options.monitoringPeriod || 10000; this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN this.failureCount = 0; this.lastFailureTime = null; this.successCount = 0; } async execute(operation) { if (this.state === 'OPEN') { if (this.shouldAttemptReset()) { this.state = 'HALF_OPEN'; this.successCount = 0; } else { throw new Error('Circuit breaker está OPEN. Serviço indisponível.'); } } try { const result = await operation(); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } onSuccess() { this.failureCount = 0; if (this.state === 'HALF_OPEN') { this.successCount++; // Após algumas operações bem-sucedidas, fechar o circuit if (this.successCount >= 3) { this.state = 'CLOSED'; console.log('🔄 Circuit breaker fechado. Serviço restaurado.'); } } } onFailure() { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.failureThreshold) { this.state = 'OPEN'; console.log('🚫 Circuit breaker aberto. Serviço marcado como indisponível.'); } } shouldAttemptReset() { return Date.now() - this.lastFailureTime >= this.resetTimeout; } } // Uso do Circuit Breaker const circuitBreaker = new CircuitBreaker({ failureThreshold: 3, resetTimeout: 30000 }); async function createNoteWithCircuitBreaker(audioFile, metadata) { return circuitBreaker.execute(async () => { return client.createNote(audioFile, metadata); }); } ``` ## Monitoramento e Alertas ### Sistema de Logs Estruturados ```javascript theme={null} theme={null} class ConnectVetsLogger { static log(level, message, data = {}) { const logEntry = { timestamp: new Date().toISOString(), level, message, service: 'connectvets-integration', ...data }; console.log(JSON.stringify(logEntry)); // Enviar para serviço de monitoramento if (level === 'ERROR') { this.reportError(logEntry); } } static info(message, data) { this.log('INFO', message, data); } static warn(message, data) { this.log('WARN', message, data); } static error(message, error, data = {}) { this.log('ERROR', message, { ...data, error: { message: error.message, stack: error.stack, code: error.code, status: error.status } }); } static reportError(logEntry) { // Integrar com serviços como Sentry, Datadog, etc. // sentry.captureException(logEntry); } } // Uso em operações async function processAudio(audioFile) { const startTime = Date.now(); const operationId = generateId(); ConnectVetsLogger.info('Iniciando processamento de áudio', { operation_id: operationId, file_size: audioFile.size, file_type: audioFile.type }); try { const result = await client.createNote(audioFile, metadata); ConnectVetsLogger.info('Processamento concluído', { operation_id: operationId, note_id: result.id, duration_ms: Date.now() - startTime }); return result; } catch (error) { ConnectVetsLogger.error('Falha no processamento', error, { operation_id: operationId, duration_ms: Date.now() - startTime }); throw error; } } ``` ### Métricas de Performance ```javascript theme={null} theme={null} class MetricsCollector { constructor() { this.metrics = { requests_total: 0, requests_success: 0, requests_failed: 0, response_times: [], error_codes: {} }; } recordRequest(duration, success, errorCode = null) { this.metrics.requests_total++; this.metrics.response_times.push(duration); // Manter apenas últimas 100 medições if (this.metrics.response_times.length > 100) { this.metrics.response_times.shift(); } if (success) { this.metrics.requests_success++; } else { this.metrics.requests_failed++; if (errorCode) { this.metrics.error_codes[errorCode] = (this.metrics.error_codes[errorCode] || 0) + 1; } } } getStats() { const responseTimes = this.metrics.response_times; return { total_requests: this.metrics.requests_total, success_rate: this.metrics.requests_success / this.metrics.requests_total * 100, avg_response_time: responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length, p95_response_time: this.percentile(responseTimes, 0.95), error_distribution: this.metrics.error_codes }; } percentile(arr, p) { const sorted = arr.slice().sort((a, b) => a - b); const index = Math.ceil(sorted.length * p) - 1; return sorted[index]; } } // Uso das métricas const metrics = new MetricsCollector(); async function monitoredRequest(operation) { const startTime = Date.now(); try { const result = await operation(); const duration = Date.now() - startTime; metrics.recordRequest(duration, true); return result; } catch (error) { const duration = Date.now() - startTime; metrics.recordRequest(duration, false, error.code); throw error; } } // Relatório periódico setInterval(() => { const stats = metrics.getStats(); console.log('📊 Estatísticas ConnectVets:', stats); // Alertar se success rate < 95% if (stats.success_rate < 95) { console.warn('⚠️ Alta taxa de erro detectada!', stats); } }, 300000); // A cada 5 minutos ``` ## Testes de Error Handling ### Testes Unitários ```javascript theme={null} theme={null} describe('ConnectVets Error Handling', () => { let client; beforeEach(() => { client = new ConnectVetsClient('test_key'); }); test('deve retry em erro 500', async () => { const mockFetch = jest.fn() .mockRejectedValueOnce(new Error('Network error')) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ id: 'note_123' }) }); global.fetch = mockFetch; const result = await client.request('/notes'); expect(mockFetch).toHaveBeenCalledTimes(2); expect(result.id).toBe('note_123'); }); test('deve lançar erro em API key inválida', async () => { const mockFetch = jest.fn().mockResolvedValue({ ok: false, status: 401, json: () => Promise.resolve({ error: { code: 'invalid_api_key', message: 'API key inválida' } }) }); global.fetch = mockFetch; await expect(client.request('/notes')).rejects.toThrow('API key inválida'); }); test('circuit breaker deve abrir após muitas falhas', async () => { const breaker = new CircuitBreaker({ failureThreshold: 2 }); const failingOperation = jest.fn().mockRejectedValue(new Error('Falha')); // Primeira falha await expect(breaker.execute(failingOperation)).rejects.toThrow(); expect(breaker.state).toBe('CLOSED'); // Segunda falha - deve abrir circuit await expect(breaker.execute(failingOperation)).rejects.toThrow(); expect(breaker.state).toBe('OPEN'); // Terceira tentativa deve falhar imediatamente await expect(breaker.execute(failingOperation)).rejects.toThrow('Circuit breaker está OPEN'); }); }); ``` ## Checklist de Error Handling * [ ] Cliente HTTP com tratamento de erro estruturado * [ ] Códigos de status HTTP mapeados corretamente * [ ] Respostas de erro parseadas e tratadas * [ ] Logs estruturados implementados * [ ] Métricas básicas coletadas * [ ] Retry automático com backoff exponencial * [ ] Circuit breaker para falhas consecutivas * [ ] Timeouts configurados adequadamente * [ ] Fallbacks para cenários críticos * [ ] Queue para operações não críticas * [ ] Alertas para alta taxa de erro * [ ] Dashboard de métricas em tempo real * [ ] Logs centralizados e pesquisáveis * [ ] Rastreamento de performance * [ ] Notificações automáticas para falhas críticas * [ ] Testes unitários para todos os cenários de erro * [ ] Testes de integração com mock de falhas * [ ] Testes de carga e stress * [ ] Simulação de falhas de rede * [ ] Validação de recovery automático ## Próximos Passos Voltar ao guia de integração Especificações técnicas de áudio Documentação completa da API Integrações robustas funcionando # Onboarding Source: https://docs.connectvets.com.br/notes/integration/onboarding # Onboarding e Primeiros Passos > Guia completo para integrar sua aplicação com ConnectVets Notes do zero ## Introdução ao Onboarding O processo de **onboarding** no ConnectVets Notes é projetado para colocar sua integração funcionando rapidamente, seguindo as melhores práticas de segurança e performance para aplicações veterinárias. **Tempo estimado**: 15-30 minutos para uma integração básica funcionando ## Pré-requisitos Antes de começar, certifique-se de ter: * ✅ Conta criada em [notes.connectvets.com.br](https://notes.connectvets.com.br) * ✅ Workspace/clínica configurado * ✅ Plano ativo (trial ou pago) * ✅ Node.js 16+ ou Python 3.8+ * ✅ Editor de código * ✅ Cliente HTTP (Postman, curl, etc.) * ✅ Acesso à internet ## Fluxo de Onboarding ### Passo 1: Criar API Key Faça login em [notes.connectvets.com.br](https://notes.connectvets.com.br) No menu lateral, clique em **Configurações** → **API Keys** Clique em **"Nova API Key"** e escolha: * **Nome**: Ex: "Integração Sistema Veterinário" * **Tipo**: `write` (recomendado para integração completa) * **Ambiente**: `test` para desenvolvimento, `live` para produção ⚠️ **IMPORTANTE**: A chave é exibida apenas uma vez! Copie e armazene com segurança. ### Formato da API Key ```bash theme={null} theme={null} # Estrutura da chave cvn_live_1234567890abcdef1234567890abcdef # Onde: cvn = Prefixo ConnectVets Notes live = Ambiente (live/test) 1234... = Token único ``` ### Passo 2: Configurar Ambiente ```bash theme={null} theme={null} # .env (NUNCA commitar este arquivo!) CONNECTVETS_API_KEY=cvn_test_sua_chave_aqui CONNECTVETS_BASE_URL=https://api.connectvets.com CONNECTVETS_ENVIRONMENT=development ``` ```bash theme={null} theme={null} # .env.example (pode commitar) CONNECTVETS_API_KEY=your_api_key_here CONNECTVETS_BASE_URL=https://api.connectvets.com CONNECTVETS_ENVIRONMENT=development ``` ### Passo 3: Primeiro Teste ```javascript JavaScript/Node.js theme={null} theme={null} // Testar conectividade com a API const CONNECTVETS_API_KEY = process.env.CONNECTVETS_API_KEY; const BASE_URL = 'https://api.connectvets.com'; async function testConnection() { try { const response = await fetch(`${BASE_URL}/notes`, { method: 'GET', headers: { 'X-API-KEY': CONNECTVETS_API_KEY, 'Content-Type': 'application/json' } }); if (response.ok) { const data = await response.json(); console.log('✅ Conexão estabelecida!'); console.log(`📋 Encontradas ${data.data.length} notas`); return data; } else { console.error('❌ Erro na conexão:', response.status); } } catch (error) { console.error('❌ Erro de rede:', error.message); } } testConnection(); ``` ```python Python theme={null} theme={null} import os import requests # Configuração API_KEY = os.getenv('CONNECTVETS_API_KEY') BASE_URL = 'https://api.connectvets.com' def test_connection(): """Testar conectividade com a API""" try: headers = { 'X-API-KEY': API_KEY, 'Content-Type': 'application/json' } response = requests.get(f'{BASE_URL}/notes', headers=headers) if response.status_code == 200: data = response.json() print('✅ Conexão estabelecida!') print(f'📋 Encontradas {len(data["data"])} notas') return data else: print(f'❌ Erro na conexão: {response.status_code}') print(response.text) except requests.RequestException as error: print(f'❌ Erro de rede: {error}') if __name__ == "__main__": test_connection() ``` ```bash cURL theme={null} theme={null} # Teste básico via curl curl -X GET "https://api.connectvets.com/notes" \ -H "X-API-KEY: cvn_test_sua_chave_aqui" \ -H "Content-Type: application/json" \ --verbose # Resposta esperada: HTTP 200 com lista de notas ``` ```go Go theme={null} theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" "os" ) type NotesResponse struct { Data []Note `json:"data"` } type Note struct { ID string `json:"id"` Content string `json:"content"` Status string `json:"status"` } func testConnection() error { apiKey := os.Getenv("CONNECTVETS_API_KEY") if apiKey == "" { return fmt.Errorf("CONNECTVETS_API_KEY não configurada") } req, err := http.NewRequest("GET", "https://api.connectvets.com/notes", nil) if err != nil { return err } req.Header.Set("X-API-KEY", apiKey) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode == 200 { body, _ := io.ReadAll(resp.Body) var notesResp NotesResponse json.Unmarshal(body, ¬esResp) fmt.Println("✅ Conexão estabelecida!") fmt.Printf("📋 Encontradas %d notas\n", len(notesResp.Data)) return nil } else { return fmt.Errorf("❌ Erro na conexão: %d", resp.StatusCode) } } func main() { if err := testConnection(); err != nil { fmt.Println(err) } } ``` ## Integração Básica ### Enviar Primeira Nota ```javascript JavaScript theme={null} theme={null} async function createNote(audioFile, metadata = {}) { const formData = new FormData(); // Arquivo de áudio formData.append('audio', audioFile); // Metadados da consulta formData.append('metadata', JSON.stringify({ patient_name: metadata.patientName || 'Teste', doctor_name: metadata.doctorName || 'Dr. Teste', consultation_date: metadata.date || new Date().toISOString(), species: metadata.species || 'Canino', ...metadata })); try { const response = await fetch(`${BASE_URL}/notes`, { method: 'POST', headers: { 'X-API-KEY': CONNECTVETS_API_KEY // NÃO definir Content-Type para FormData }, body: formData }); if (response.ok) { const note = await response.json(); console.log('✅ Nota criada:', note.id); console.log('🔄 Status:', note.status); return note; } else { const error = await response.json(); console.error('❌ Erro ao criar nota:', error); } } catch (error) { console.error('❌ Erro de rede:', error); } } // Exemplo de uso const audioInput = document.getElementById('audio-input'); audioInput.addEventListener('change', (event) => { const file = event.target.files[0]; if (file) { createNote(file, { patientName: 'Rex', doctorName: 'Dr. Silva', species: 'Canino' }); } }); ``` ```python Python theme={null} theme={null} import requests import json def create_note(audio_file_path, metadata=None): """Criar uma nova nota com arquivo de áudio""" if metadata is None: metadata = {} # Dados padrão default_metadata = { 'patient_name': 'Teste', 'doctor_name': 'Dr. Teste', 'consultation_date': '2024-02-14T10:30:00Z', 'species': 'Canino' } default_metadata.update(metadata) headers = { 'X-API-KEY': API_KEY } with open(audio_file_path, 'rb') as audio_file: files = { 'audio': audio_file, 'metadata': (None, json.dumps(default_metadata)) } response = requests.post( f'{BASE_URL}/notes', headers=headers, files=files ) if response.status_code == 201: note = response.json() print(f'✅ Nota criada: {note["id"]}') print(f'🔄 Status: {note["status"]}') return note else: print(f'❌ Erro ao criar nota: {response.status_code}') print(response.text) return None # Exemplo de uso note = create_note( 'caminho/para/audio.mp3', { 'patient_name': 'Rex', 'doctor_name': 'Dr. Silva', 'species': 'Canino' } ) ``` ### Acompanhar Processamento ```javascript theme={null} theme={null} async function pollNoteStatus(noteId, maxAttempts = 30) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const response = await fetch(`${BASE_URL}/notes/${noteId}`, { headers: { 'X-API-KEY': CONNECTVETS_API_KEY, 'Content-Type': 'application/json' } }); if (response.ok) { const note = await response.json(); console.log(`📊 Tentativa ${attempt}: Status ${note.status}`); switch (note.status) { case 'completed': console.log('✅ Processamento concluído!'); console.log('📝 Conteúdo:', note.sections); return note; case 'failed': console.log('❌ Processamento falhou'); console.log('💬 Erro:', note.error_message); return note; case 'processing': case 'transcribing': case 'analyzing': console.log('⏳ Ainda processando...'); await new Promise(resolve => setTimeout(resolve, 2000)); // Aguardar 2s break; default: console.log(`⏳ Status: ${note.status}`); await new Promise(resolve => setTimeout(resolve, 2000)); } } } catch (error) { console.error(`❌ Erro na tentativa ${attempt}:`, error); } } console.log('⚠️ Timeout: Processamento demorou mais que esperado'); return null; } // Usar após criar nota createNote(audioFile).then(note => { if (note) { pollNoteStatus(note.id); } }); ``` ## Configuração de Webhooks Para receber notificações automáticas quando as notas ficarem prontas: ### 1. Configurar Endpoint ```javascript theme={null} theme={null} // Endpoint para receber webhooks app.post('/webhooks/connectvets', express.raw({ type: 'application/json' }), (req, res) => { const event = req.body; console.log('🔔 Webhook recebido:', event.type); switch (event.type) { case 'note.completed': handleNoteCompleted(event.data); break; case 'note.failed': handleNoteFailed(event.data); break; default: console.log(`❓ Evento desconhecido: ${event.type}`); } res.status(200).send('OK'); }); function handleNoteCompleted(noteData) { console.log('✅ Nota processada:', noteData.id); console.log('📝 Seções:', noteData.sections.length); // Integrar com seu sistema updatePatientRecord(noteData); notifyVeterinarian(noteData); } function handleNoteFailed(noteData) { console.log('❌ Falha no processamento:', noteData.id); console.log('💬 Erro:', noteData.error_message); // Notificar erro alertAdministrator(noteData); } ``` ### 2. Registrar Webhook ```javascript theme={null} theme={null} async function registerWebhook(url, events = ['note.completed', 'note.failed']) { try { const response = await fetch(`${BASE_URL}/webhooks`, { method: 'POST', headers: { 'X-API-KEY': CONNECTVETS_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: url, events: events, active: true }) }); if (response.ok) { const webhook = await response.json(); console.log('✅ Webhook registrado:', webhook.id); return webhook; } } catch (error) { console.error('❌ Erro ao registrar webhook:', error); } } // Registrar webhook registerWebhook('https://meuapp.com/webhooks/connectvets'); ``` ## Exemplo de Aplicação Completa ### Setup Inicial ```javascript theme={null} theme={null} // config/connectvets.js class ConnectVetsClient { constructor(apiKey, baseUrl = 'https://api.connectvets.com') { this.apiKey = apiKey; this.baseUrl = baseUrl; } async request(endpoint, options = {}) { const url = `${this.baseUrl}${endpoint}`; const config = { ...options, headers: { 'X-API-KEY': this.apiKey, ...options.headers } }; const response = await fetch(url, config); if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Erro desconhecido' })); throw new Error(`API Error: ${response.status} - ${error.message}`); } return response.json(); } async createNote(audioFile, metadata = {}) { const formData = new FormData(); formData.append('audio', audioFile); formData.append('metadata', JSON.stringify(metadata)); return this.request('/notes', { method: 'POST', body: formData, headers: {} // Deixar browser definir Content-Type para FormData }); } async getNote(id) { return this.request(`/notes/${id}`); } async listNotes(filters = {}) { const params = new URLSearchParams(filters); return this.request(`/notes?${params}`); } } // Instanciar cliente const connectvets = new ConnectVetsClient(process.env.CONNECTVETS_API_KEY); module.exports = { connectvets }; ``` ### Integração com Sistema Veterinário ```javascript theme={null} theme={null} // services/consultationService.js const { connectvets } = require('../config/connectvets'); class ConsultationService { async processConsultation(consultation) { try { // 1. Preparar dados const metadata = { patient_name: consultation.patient.name, patient_id: consultation.patient.id, doctor_name: consultation.veterinarian.name, doctor_id: consultation.veterinarian.id, consultation_date: consultation.date, species: consultation.patient.species, breed: consultation.patient.breed, consultation_type: consultation.type }; // 2. Enviar áudio para processamento console.log('📤 Enviando áudio para ConnectVets...'); const note = await connectvets.createNote(consultation.audioFile, metadata); // 3. Salvar referência no banco await this.saveNoteReference(consultation.id, note.id); // 4. Iniciar monitoramento this.startNoteMonitoring(note.id, consultation.id); return note; } catch (error) { console.error('❌ Erro ao processar consulta:', error); throw error; } } async startNoteMonitoring(noteId, consultationId) { const checkStatus = async () => { try { const note = await connectvets.getNote(noteId); if (note.status === 'completed') { await this.handleCompletedNote(note, consultationId); } else if (note.status === 'failed') { await this.handleFailedNote(note, consultationId); } else { // Ainda processando, verificar novamente em 30s setTimeout(checkStatus, 30000); } } catch (error) { console.error('❌ Erro ao verificar status:', error); } }; checkStatus(); } async handleCompletedNote(note, consultationId) { console.log('✅ Nota processada para consulta:', consultationId); // Extrair informações das seções const sections = note.sections; const summary = sections.find(s => s.type === 'summary')?.content; const diagnosis = sections.find(s => s.type === 'diagnosis')?.content; const treatment = sections.find(s => s.type === 'treatment')?.content; // Atualizar prontuário await this.updateMedicalRecord(consultationId, { summary, diagnosis, treatment, full_transcript: note.transcript, connectvets_note_id: note.id }); // Notificar veterinário await this.notifyVeterinarian(consultationId, note); } async handleFailedNote(note, consultationId) { console.log('❌ Falha no processamento para consulta:', consultationId); // Registrar erro await this.logProcessingError(consultationId, note.error_message); // Notificar administrador await this.notifyAdmin(consultationId, note.error_message); } } module.exports = { ConsultationService }; ``` ## Checklist de Onboarding * [ ] Conta ConnectVets criada * [ ] Workspace configurado * [ ] API Key gerada e armazenada * [ ] Variáveis de ambiente configuradas * [ ] Primeiro teste de conectividade realizado * [ ] Cliente HTTP configurado * [ ] Primeira nota enviada com sucesso * [ ] Sistema de polling implementado * [ ] Tratamento de erros básico funcionando * [ ] Logs de debug ativados * [ ] Webhooks configurados (opcional) * [ ] Filtros e paginação implementados * [ ] Retry automático em caso de erro * [ ] Monitoramento de performance * [ ] Backup de dados críticos * [ ] API Key de produção configurada * [ ] Logs de produção configurados * [ ] Monitoramento ativo * [ ] Alertas de erro implementados * [ ] Documentação interna criada ## Próximos Passos Especificações técnicas para áudio Como lidar com falhas e erros Documentação completa da API Integrações reais funcionando ## Suporte Precisa de ajuda durante o onboarding? * 📧 **Email**: [suporte@connectvets.com.br](mailto:suporte@connectvets.com.br) * 💬 **Chat**: Disponível no dashboard * 📖 **Documentação**: Sempre atualizada * 🐛 **Bugs**: Reporte via GitHub Issues # Introduction Source: https://docs.connectvets.com.br/notes/introduction # ConnectVets Notes API > API para transcrição e análise inteligente de consultas veterinárias ConnectVets Notes ConnectVets Notes # API para Transcrição de Consultas Veterinárias A **ConnectVets Notes API** permite que você integre transcrição automática de áudio de consultas veterinárias em seu sistema, convertendo gravações em texto estruturado e organizando as informações em seções úteis. ## 🎯 Para quem é esta API? Integre transcrição inteligente em sistemas de gestão veterinária Automatize a documentação de consultas e melhore a eficiência Colete e analise dados de consultas para estudos científicos Crie aplicações inovadoras para o setor veterinário ## ⚡ Como funciona? Faça upload do arquivo de áudio da consulta via API Nossa IA especializada processa e transcreve o áudio Obtenha texto organizado em seções como diagnóstico, tratamento, etc. Use os dados em sua aplicação conforme necessário ## 🔥 Principais recursos Reconhecimento otimizado para terminologia veterinária brasileira Organização automática em anamnese, diagnóstico, tratamento, etc. Interface RESTful intuitiva com documentação completa Resultados em minutos, com webhooks para notificações ## 📋 Formatos de áudio suportados **Recomendado** Melhor qualidade de transcrição **Recomendado** Compressão sem perda **Compatível** Formato amplamente usado **Compatível** Compressão eficiente **Compatível** Formato Apple/AAC **Compatível** Formato web moderno ## 📞 Contato e Suporte **+55 31 8883-5141** Suporte direto para dúvidas e questões comerciais **discord.gg/uN4NxUGk** Comunidade de desenvolvedores e suporte técnico ## 🚀 Comece agora Configure sua primeira integração em minutos Explore todos os endpoints disponíveis *** **Precisa de ajuda?** Nossa equipe está disponível no [WhatsApp](https://wa.me/553185127147) ou no [Discord](https://discord.gg/uN4NxUGk) para tirar suas dúvidas. # Quickstart Source: https://docs.connectvets.com.br/notes/quickstart # Guia de Início Rápido > Configure sua primeira integração com ConnectVets Notes em minutos # Primeiros passos com a API Este guia te ajudará a fazer sua primeira chamada à API e processar um áudio de consulta veterinária em poucos minutos. ## Passo 1: Obtenha sua API Key Entre em contato conosco via [WhatsApp](https://wa.me/553185127147) para solicitar acesso à API Nossa equipe fornecerá sua API Key e instruções de acesso Use o endpoint base: `https://api-sandbox.connectvets.com.br/notes/v1` **Importante**: Mantenha sua API Key segura e nunca a exponha em código frontend. ## Passo 2: Teste a conexão Vamos verificar se sua API Key está funcionando: ```bash cURL theme={null} theme={null} curl -X GET "https://api-sandbox.connectvets.com.br/notes/v1/notes" \ -H "X-API-KEY: sua_api_key_aqui" \ -H "Content-Type: application/json" ``` ```javascript JavaScript theme={null} theme={null} const response = await fetch('https://api-sandbox.connectvets.com.br/notes/v1/notes', { method: 'GET', headers: { 'X-API-KEY': 'sua_api_key_aqui', 'Content-Type': 'application/json' } }); const data = await response.json(); console.log(data); ``` ```python Python theme={null} theme={null} import requests headers = { 'X-API-KEY': 'sua_api_key_aqui', 'Content-Type': 'application/json' } response = requests.get( 'https://api-sandbox.connectvets.com.br/notes/v1/notes', headers=headers ) print(response.json()) ``` **Resposta esperada:** ```json theme={null} theme={null} { "data": [], "meta": { "total": 0, "page": 1, "limit": 10 }, "status": "success" } ``` ## Passo 3: Envie seu primeiro áudio Agora vamos processar um arquivo de áudio: ```bash cURL theme={null} theme={null} curl -X POST "https://api-sandbox.connectvets.com.br/notes/v1/notes" \ -H "X-API-KEY: sua_api_key_aqui" \ -F "audio=@caminho/para/audio.mp3" \ -F "metadata={\"patient_name\":\"Rex\",\"owner_name\":\"João Silva\"}" ``` ```javascript JavaScript theme={null} theme={null} const formData = new FormData(); formData.append('audio', audioFile); // objeto File do input formData.append('metadata', JSON.stringify({ patient_name: "Rex", owner_name: "João Silva" })); const response = await fetch('https://api-sandbox.connectvets.com.br/notes/v1/notes', { method: 'POST', headers: { 'X-API-KEY': 'sua_api_key_aqui' }, body: formData }); const result = await response.json(); console.log('Nota criada:', result); ``` ```python Python theme={null} theme={null} import requests files = { 'audio': ('audio.mp3', open('caminho/para/audio.mp3', 'rb'), 'audio/mpeg') } data = { 'metadata': '{"patient_name":"Rex","owner_name":"João Silva"}' } headers = { 'X-API-KEY': 'sua_api_key_aqui' } response = requests.post( 'https://api-sandbox.connectvets.com.br/notes/v1/notes', headers=headers, files=files, data=data ) print(response.json()) ``` **Resposta esperada:** ```json theme={null} theme={null} { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Rex", "transcription_status": "pending", "created_at": "2024-01-15T10:30:00Z" }, "status": "success" } ``` ## Passo 4: Monitore o processamento O processamento é assíncrono. Verifique o status: ```bash cURL theme={null} theme={null} curl -X GET "https://api-sandbox.connectvets.com.br/notes/v1/notes/550e8400-e29b-41d4-a716-446655440000" \ -H "X-API-KEY: sua_api_key_aqui" ``` ```javascript JavaScript theme={null} theme={null} const noteId = "550e8400-e29b-41d4-a716-446655440000"; const checkStatus = async () => { const response = await fetch(`https://api-sandbox.connectvets.com.br/notes/v1/notes/${noteId}`, { headers: { 'X-API-KEY': 'sua_api_key_aqui' } }); const note = await response.json(); return note.data.transcription_status; }; // Verificar a cada 10 segundos const interval = setInterval(async () => { const status = await checkStatus(); console.log('Status:', status); if (status === 'completed' || status === 'failed') { clearInterval(interval); } }, 10000); ``` ```python Python theme={null} theme={null} import time import requests def check_status(note_id, api_key): response = requests.get( f'https://api-sandbox.connectvets.com.br/notes/v1/notes/{note_id}', headers={'X-API-KEY': api_key} ) return response.json()['data']['transcription_status'] note_id = "550e8400-e29b-41d4-a716-446655440000" while True: status = check_status(note_id, 'sua_api_key_aqui') print(f'Status: {status}') if status in ['completed', 'failed']: break time.sleep(10) ``` ## Passo 5: Obtenha os resultados Quando o status for `completed`, você pode acessar as seções: ```bash cURL theme={null} theme={null} curl -X GET "https://api-sandbox.connectvets.com.br/notes/v1/notes/550e8400-e29b-41d4-a716-446655440000" \ -H "X-API-KEY: sua_api_key_aqui" ``` ```javascript JavaScript theme={null} theme={null} const response = await fetch(`https://api-sandbox.connectvets.com.br/notes/v1/notes/${noteId}`, { headers: { 'X-API-KEY': 'sua_api_key_aqui' } }); const note = await response.json(); // Acessar seções estruturadas note.data.note_sections.forEach(section => { console.log(`${section.title}: ${section.content}`); }); ``` **Exemplo de resposta completa:** ```json theme={null} theme={null} { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Rex", "transcription_status": "completed", "note_sections": [ { "title": "Anamnese", "label": "anamnesis", "content": "Paciente apresenta histórico de...", "order": "1" }, { "title": "Diagnóstico", "label": "diagnosis", "content": "Após exame físico, diagnosticado...", "order": "2" } ] } } ``` ## 🎯 Próximos passos Veja todos os endpoints disponíveis Receba notificações automáticas Otimize a qualidade dos seus áudios Implemente tratamento robusto de erros *** **Dica**: Use webhooks para receber notificações automáticas quando o processamento for concluído, ao invés de fazer polling manual.