# PumaHelp — Documentação para desenvolvedores > API REST, widget embarcável e bot do Discord do PumaHelp, uma plataforma de suporte multi-inquilino. Documentação do PumaHelp, plataforma de suporte multi-inquilino. Ao gerar código de integração, considere: - A URL base é por organização: `https://{subdominio}.pumahelp.com/api`. Não existe host único. - Todo JSON usa snake_case, inclusive nos parâmetros de rota (`{public_id}`, `{user_id}`). - Há dois modos de autenticação: `Authorization: Bearer {jwt}` para usuários e `X-API-Key: {chave}` para integrações servidor a servidor. Uma chave carrega escopos (`ticket:read`, `ticket:create`...) e age como o usuário que a criou. - Filtros de ticket usam uma linguagem própria no parâmetro `query`, não parâmetros separados. Repetir a mesma chave significa "ou" (`status:open status:pending`); chaves diferentes, "e". - Para separar dados por cliente, filtre por `id`, `external_id` ou `group_id`. Filtrar por nome casa por trecho e traz homônimos. - Confirme campos e endpoints nesta documentação antes de assumir: a API muda e nomes parecidos de outros produtos de suporte não se aplicam aqui. ## API # API Documentation A **PumaHelp API** é uma API RESTful completa para gerenciamento de tickets de suporte, permitindo criar, atualizar e gerenciar tickets, comentários, usuários, grupos, macros, webhooks e muito mais. ## 🔗 Base URL Todas as requisições devem ser feitas para o subdomínio da sua organização: ``` https://{seu-app-name}.pumahelp.com/api ``` **Exemplo:** ``` https://acme.pumahelp.com/api ``` :::note O subdomínio é definido pelo **App Name** configurado na seção de **Configurações** do dashboard. Se não souber qual é, verifique em Configurações ou entre em contato com o suporte. ::: --- ## 🚀 Como Começar ### Passo 1: Obter Acesso Você tem duas opções para começar a usar a API: **Opção A: Usar credenciais de usuário (JWT)** - Ideal para: aplicações que já têm usuários cadastrados - Requer: email e senha de um usuário existente **Opção B: Criar API Key (Recomendado para integrações)** - Ideal para: sistemas externos, automações, integrações de longo prazo - Requer: acesso ao dashboard PumaHelp com permissões de admin/owner ### Passo 2: Fazer Sua Primeira Requisição **Com JWT:** ```bash # 1. Fazer login curl -X POST https://acme.pumahelp.com/api/v1/users/login \ -H "Content-Type: application/json" \ -d '{ "email": "seu-email@example.com", "password": "sua-senha" }' # 2. Usar o token recebido curl -X GET https://acme.pumahelp.com/api/v1/tickets \ -H "Authorization: Bearer SEU_TOKEN_AQUI" ``` **Com API Key:** ```bash # 1. Criar API Key no dashboard (Configurações → API Keys) # 2. Usar a API Key diretamente curl -X GET https://acme.pumahelp.com/api/v1/tickets \ -H "X-API-Key: SEU_API_KEY_SECRET" ``` ### Passo 3: Criar Seu Primeiro Ticket ```bash curl -X POST https://acme.pumahelp.com/api/v1/tickets \ -H "Authorization: Bearer SEU_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "subject": "Meu primeiro ticket via API", "priority": "normal", "type": "question", "comment": { "body": "Testando a integração com sucesso!", "public": true } }' ``` ### Passo 4: Próximos Passos - ✅ Configure [webhooks](#-webhooks) para receber notificações em tempo real - ✅ Configure [scopes adequados](#-sistema-de-scopes) para sua API Key - ✅ Implemente [tratamento de erros](#-códigos-de-erro) robusto --- ## 📝 Convenções de Nomenclatura A API utiliza **snake_case** para todos os campos de request e response. **Exemplo:** ```json { "public_id": 12345, "created_at": "2025-01-01T10:00:00Z", "assignee_id": "uuid", "public": true } ``` :::important Todos os nomes de campos devem usar **snake_case_lower** (letras minúsculas com underscores). ::: --- ## 🔐 Autenticação A PumaHelp API suporta **dois métodos de autenticação**: ### 1. JWT (JSON Web Token) Para usuários que fazem login via email/senha. **Como obter:** ```http POST https://acme.pumahelp.com/api/v1/users/login Content-Type: application/json { "email": "user@example.com", "password": "sua-senha" } ``` **Resposta:** ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "verified": true } ``` **Como usar:** ```http GET https://acme.pumahelp.com/api/v1/tickets Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ``` --- ### 2. API Key Para integrações automatizadas e sistemas externos. **Como obter:** 1. Faça login na plataforma PumaHelp 2. Navegue até Configurações → API Keys 3. Clique em "Criar Nova API Key" 4. Selecione os scopes necessários 5. Copie o secret (visível apenas uma vez!) **Como usar:** ```http GET https://acme.pumahelp.com/api/v1/tickets X-API-Key: seu-api-key-secret-aqui ``` :::important O secret da API Key é exibido **apenas uma vez** durante a criação. Guarde-o em local seguro! ::: --- ## 🔒 Sistema de Scopes A API utiliza um sistema granular de permissões baseado em **scopes** (no formato `resource:action`). ### Como Funcionam os Scopes | Tipo de Autenticação | Comportamento | |---------------------|---------------| | **JWT (Login)** | Scopes atribuídos automaticamente baseados na role do usuário | | **API Key** | Scopes devem ser explicitamente selecionados na criação | ### Listagem de Scopes Disponíveis Para obter a lista completa de scopes via API, consulte a seção [🔍 Escopos Disponíveis](#-escopos-disponíveis). --- ## 📊 Rate Limiting A API implementa rate limiting para proteger contra abuso: ### Limites por IP (Endpoints Públicos) - **100 requisições** a cada 10 segundos - Fila de até 20 requisições ### Limites por Organização (Endpoints Autenticados) - **1000 tokens** disponíveis - **100 tokens repostos** a cada 10 segundos **Headers de Resposta:** ```http X-RateLimit-Remaining: 987 Retry-After: 10 ``` **Response quando limite excedido: `429 Too Many Requests`** ```json { "errors": ["Rate limit exceeded"] } ``` --- ## 📝 Endpoints ### 🎫 Tickets #### Criar Ticket ```http POST https://acme.pumahelp.com/api/v1/tickets Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `ticket:create` **Request Body:** ```json { "subject": "Problema com login", "priority": "high", "type": "question", "group_id": "uuid-do-grupo", "requester": { "name": "Cliente Nome", "email": "cliente@example.com" }, "comment": { "body": "Não consigo fazer login no sistema", "public": true, "author_id": "uuid-autor", "uploads": ["uuid-arquivo-1", "uuid-arquivo-2"] }, "tags": ["login", "urgent"], "via": { "channel": "api" } } ``` | Campo | Tipo | Obrigatório | Descrição | |-------|------|-------------|-----------| | `subject` | string | Sim | Assunto do ticket | | `priority` | string | Não | Prioridade: `low`, `normal`, `high`, `urgent` (padrão: `normal`) | | `type` | string | Não | Tipo: `question`, `incident`, `problem`, `task` | | `group_id` | uuid | Não | ID do grupo responsável | | `requester` | object | Não | Dados do solicitante (se criar em nome de outro usuário) | | `requester.name` | string | Não | Nome do solicitante | | `requester.email` | string | Não | Email do solicitante | | `requester.external_id` | string | Não | ID externo do solicitante | | `requester.id` | uuid | Não | ID do solicitante | | `comment` | object | Sim | Primeiro comentário do ticket | | `comment.body` | string | Sim | Conteúdo do comentário | | `comment.public` | boolean | Não | Se visível para o cliente (padrão: true) | | `comment.author_id` | uuid | Não | ID do autor (se diferente do usuário autenticado) | | `comment.uploads` | array[uuid] | Não | IDs de arquivos anexados | | `tags` | array[string] | Não | Tags para categorização | | `via` | object | Não | Canal de origem | | `via.channel` | string | Não | Canal: `api`, `widget`, `discord` | **Response: `201 Created`** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "public_id": 12345, "subject": "Problema com login", "status": "new", "created_at": "2025-01-01T10:00:00Z", "conversation_id": "uuid-conversa" } ``` --- #### Listar Tickets ```http GET https://acme.pumahelp.com/api/v1/tickets?page=1&page_size=25 Authorization: Bearer {token} ``` **Scope:** `ticket:read` **Query Parameters:** | Parâmetro | Tipo | Descrição | Obrigatório | |-----------|------|-----------|--------| | `page` | integer | Número da página | Sim | | `page_size` | integer | Itens por página (max: 100) | Sim | | `query` | string | Filtros e busca (ver abaixo). Máx. 500 caracteres | Não | | `sort_by` | string | Campo para ordenação: `created_at`, `updated_at`, `priority`, `status` | Não | | `sort_order` | string | Ordem: `asc` ou `desc` | Não | | `include` | array[string] | Campos extras: `assignee`, `requester`, `last_comment`, `tags` | Não | ##### Sintaxe do parâmetro `query` A busca é uma sequência de termos `chave:valor` separados por espaço, mais palavras soltas. **Como os termos se combinam:** - **Repetir a mesma chave é "ou"**: `status:open status:pending` traz tickets abertos **ou** pendentes. Vale para todas as chaves — `assignee:joao assignee:maria` traz os tickets dos dois. A exceção é `subject`, onde repetir significa "e" (o assunto precisa conter os dois textos), assim como acontece com as palavras da busca livre. - **Chaves diferentes são "e"**: `status:open priority:high` traz apenas os que são abertos **e** de prioridade alta. - **Não existem operadores `AND`/`OR` escritos**. Se você escrever `status:open AND priority:high`, a palavra `AND` é tratada como texto de busca. Use apenas espaços. - **Palavras sem `chave:` são busca textual** no assunto do ticket: `boleto atrasado` procura tickets cujo assunto contenha "boleto" e "atrasado". Para uma frase exata, use aspas: `"nota fiscal"`. - **Valores com espaço precisam de aspas** (simples ou duplas): `subject:'erro no login'`. **Chaves disponíveis:** | Chave | Valores | Exemplo | |-------|---------|---------| | `status` | `new`, `open`, `pending`, `solved`, `closed` | `status:open` | | `priority` | `low`, `normal`, `high`, `urgent` | `priority:urgent` | | `type` | `question`, `incident`, `problem`, `task` | `type:incident` | | `tags` | nome da tag. Várias na mesma aspa = "e" | `tags:financeiro`, `tags:'fiscal urgente'` | | `assignee` | UUID, e-mail, `external_id`, parte do nome, `me`, ou `none`/`null` para não atribuídos | `assignee:me`, `assignee:none` | | `requester` | UUID, e-mail, `external_id`, parte do nome, ou `me` | `requester:me` | | `subject` | texto contido no assunto | `subject:'erro no login'` | | `group` / `group_id` | nome do grupo / UUID do grupo | `group:suporte` | | `id` | número do ticket | `id:1042` | | `archived` | `true` ou `false` | `archived:true` | | `created` / `updated` | data (`2026-01-31`) ou período: `today`, `yesterday`, `last_24_hours`, `last_7_days`, `last_30_days` | `created:last_7_days` | **Datas** aceitam também os operadores `>`, `>=`, `<` e `<=`, tanto com data quanto com período: `created>=2026-01-01 created<=2026-01-31`, `createdremovido" } ``` **Response: `204 No Content`** :::warning O texto entre as tags é substituído por caracteres especiais como *████████* permanentemente. ::: --- #### Marcar Comentário como Privado ```http PUT https://acme.pumahelp.com/api/v1/tickets/{public_id}/comments/{comment_id}/make_private Authorization: Bearer {token} ``` **Scope:** `comment:update` **Response: `204 No Content`** :::note Torna o comentário invisível para end-users. Apenas agentes e admins poderão visualizar. dependendo do meio de comunicação, o comentário pode continuar visível para o end-user (cliente). ::: --- ### 👥 Usuários #### Login ```http POST https://acme.pumahelp.com/api/v1/users/login Content-Type: application/json ``` **Rate Limit:** IP-based (100 req/10s) **Request Body:** ```json { "email": "usuario@example.com", "password": "sua-senha" } ``` | Campo | Tipo | Obrigatório | Descrição | |-------|------|-------------|-----------| | `email` | string | Sim | Email do usuário | | `password` | string | Sim | Senha do usuário | **Response: `200 OK`** ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "verified": true } ``` | Campo | Tipo | Descrição | |-------|------|-----------| | `access_token` | string | Token JWT para autenticação (válido por 24h) | | `refresh_token` | string | Token para renovar o access_token | | `verified` | boolean | Se o email do usuário foi verificado | --- #### Renovar Token (Refresh) ```http POST https://acme.pumahelp.com/api/v1/users/refresh Content-Type: application/json ``` **Rate Limit:** IP-based (100 req/10s) **Request Body:** ```json { "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` **Response: `201 Created`** ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` --- #### Logout ```http POST https://acme.pumahelp.com/api/v1/users/logout Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `session:delete:own` **Rate Limit:** IP-based **Request Body:** ```json { "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` **Response: `204 No Content`** --- #### Criar Usuário ```http POST https://acme.pumahelp.com/api/v1/users Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `user:create` **Request Body:** ```json { "name": "João Silva", "email": "joao@example.com", "role": "agent", "external_id": "crm-user-123", "verified": false, "group_ids": ["uuid-grupo-1", "uuid-grupo-2"] } ``` | Campo | Tipo | Obrigatório | Descrição | |-------|------|-------------|-----------| | `name` | string | Sim | Nome completo do usuário | | `email` | string | Não | Email do usuário (opcional) | | `role` | string | Sim | Role: `end-user`, `agent`, `admin`, `owner` | | `external_id` | string | Não | ID externo para integração com outros sistemas | | `verified` | boolean | Não | Se o email deve ser marcado como verificado (padrão: false) | | `group_ids` | array[uuid] | Não | IDs dos grupos aos quais o usuário pertence | **Response: `201 Created`** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "João Silva", "email": "joao@example.com", "role": "agent", "external_id": "crm-user-123", "verified": false } ``` --- #### Criar ou Atualizar Usuário (Upsert) ```http POST https://acme.pumahelp.com/api/v1/users/create_or_update Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `user:upsert` **Request Body:** ```json { "name": "João Silva", "email": "joao@example.com", "role": "agent", "external_id": "crm-user-123", "verified": false, "group_ids": ["uuid-grupo-1"] } ``` **Response: `200 OK` ou `201 Created`** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "João Silva", "email": "joao@example.com", "role": "agent", "external_id": "crm-user-123", "verified": false } ``` :::tip Este endpoint verifica se já existe um usuário com o email ou external_id informado. Se existir, atualiza; se não, cria. Ideal para sincronizações com sistemas externos. ::: --- #### Impersonificar Usuário ```http POST https://acme.pumahelp.com/api/v1/users/impersonate Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `user:impersonate` **Request Body:** ```json { "email": "joao@example.com", "external_id": "crm-user-123", "name": "João Silva", "role": "end-user", "verified": false, "group_ids": ["uuid-grupo-1"] } ``` | Campo | Tipo | Obrigatório | Descrição | |-------|------|-------------|-----------| | `email` | string | Condicional | Email do usuário (obrigatório se `external_id` não fornecido) | | `external_id` | string | Condicional | ID externo (obrigatório se `email` não fornecido) | | `name` | string | Não | Nome completo (usado apenas se criar novo usuário) | | `role` | string | Não | Role: `end-user`, `agent`, `admin`, `owner` (padrão: `end-user`) | | `verified` | boolean | Não | Se o email deve ser verificado (apenas para criação) | | `group_ids` | array[uuid] | Não | IDs dos grupos (apenas para criação) | **Response: `200 OK`** ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 86400 } ``` | Campo | Tipo | Descrição | |-------|------|-----------| | `access_token` | string | Token JWT para autenticação em nome do usuário | | `token_type` | string | Sempre "Bearer" | | `expires_in` | integer | Tempo de expiração em segundos | :::tip Este endpoint permite gerar um access token em nome de um usuário específico. Se o usuário não existir, ele será criado automaticamente. Ideal para implementações de "Login as User" ou integração com sistemas de SSO (Single Sign-On). ::: :::warning Este é um endpoint sensível que permite assumir a identidade de qualquer usuário. Disponível apenas para roles ADMIN e OWNER, ou via API Key com scope `user:impersonate`. ::: --- #### Listar Usuários ```http GET https://acme.pumahelp.com/api/v1/users?page=1&page_size=50 Authorization: Bearer {token} ``` **Scope:** `user:read` **Query Parameters:** | Parâmetro | Tipo | Descrição | Padrão | |-----------|------|-----------|--------| | `page` | integer | Número da página | 1 | | `page_size` | integer | Itens por página (max: 100) | 25 | | `roles` | array[string] | Filtrar por roles (pode enviar múltiplos) | - | | `query` | string | Busca por nome ou email | - | | `external_id` | string | Filtrar por ID externo | - | | `group_id` | uuid | Filtrar por grupo | - | | `active` | boolean | Filtrar por usuários ativos | - | **Exemplo com múltiplas roles:** ``` GET /v1/users?roles=agent&roles=admin ``` **Response: `200 OK`** ```json { "count": 150, "users": [ { "id": "uuid", "name": "Maria Santos", "email": "maria@example.com", "role": "agent", "external_id": "crm-123", "verified": true } ] } ``` --- #### Obter Próprio Perfil ```http GET https://acme.pumahelp.com/api/v1/users/me Authorization: Bearer {token} ``` **Scope:** `profile:read:own` **Response: `200 OK`** ```json { "id": "uuid", "name": "João Silva", "email": "joao@example.com", "role": "agent", "notes": "Notas internas sobre o usuário", "external_id": "crm-123", "verified": true, "created_at": "2025-01-01T10:00:00Z", "updated_at": "2025-01-15T14:30:00Z" } ``` --- #### Obter Usuário por ID ```http GET https://acme.pumahelp.com/api/v1/users/{user_id} Authorization: Bearer {token} ``` **Scope:** `user:read` **Response: `200 OK`** ```json { "id": "uuid", "name": "João Silva", "email": "joao@example.com", "role": "agent", "notes": "Notas internas", "external_id": "crm-123", "verified": true, "created_at": "2025-01-01T10:00:00Z", "updated_at": "2025-01-15T14:30:00Z" } ``` --- #### Atualizar Usuário ```http PUT https://acme.pumahelp.com/api/v1/users/{user_id} Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `user:update` **Request Body:** ```json { "name": "João Silva Santos", "role": "admin", "email": "joao.silva@example.com", "notes": "Promovido a admin em Jan/2025", "external_id": "crm-456", "verified": true, "group_ids": ["uuid-grupo-3"] } ``` | Campo | Tipo | Descrição | |-------|------|-----------| | `name` | string | Nome do usuário | | `role` | string | Nova role do usuário | | `email` | string | Novo email | | `notes` | string | Notas internas (não visível para end-users) | | `external_id` | string | ID externo | | `verified` | boolean | Status de verificação de email | | `group_ids` | array[uuid] | Grupos do usuário | :::note Todos os campos são opcionais. Apenas os campos enviados serão atualizados. ::: **Response: `200 OK`** ```json { "id": "uuid", "name": "João Silva Santos", "email": "joao.silva@example.com", "role": "admin", "external_id": "crm-456", "verified": true } ``` --- #### Deletar Usuário ```http DELETE https://acme.pumahelp.com/api/v1/users/{user_id} Authorization: Bearer {token} ``` **Scope:** `user:delete` **Response: `204 No Content`** --- #### Enviar Email de Verificação ```http POST https://acme.pumahelp.com/api/v1/users/{user_id}/email/send/verification Authorization: Bearer {token} ``` **Scope:** `user:manage` **Response: `204 No Content`** :::note Envia um email para o usuário com link para verificar o endereço de email. ::: --- #### Verificar Email ```http POST https://acme.pumahelp.com/api/v1/users/email/verify?token={verification_token} ``` **Rate Limit:** IP-based **Sem autenticação necessária** **Query Parameters:** - `token` - Token de verificação enviado por email **Response: `204 No Content`** --- #### Reenviar Email de Verificação ```http POST https://acme.pumahelp.com/api/v1/users/email/resend/verification?email={user_email} ``` **Rate Limit:** IP-based **Sem autenticação necessária** **Query Parameters:** - `email` - Email do usuário que precisa reenviar verificação **Response: `204 No Content`** --- #### Mesclar Sessão (Merge Session) ```http POST https://acme.pumahelp.com/api/v1/users/merge-session Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `session:merge` **Request Body:** ```json { "target_auth_token": "token-jwt-do-usuario-autenticado" } ``` **Response: `204 No Content`** :::tip Use este endpoint para mesclar a sessão de um usuário convidado (guest) com um usuário autenticado. Útil quando um visitante cria tickets como guest e depois faz login/cadastro. ::: --- ### 👤 Usuários Convidados (Guests) Usuários convidados permitem criar sessões temporárias para visitantes que ainda não possuem uma conta completa no sistema. Ideal para permitir que potenciais clientes criem tickets sem necessidade de registro completo. #### Criar Usuário Convidado ```http POST https://acme.pumahelp.com/api/v2/guests X-API-Key: {authorization_token} Content-Type: application/json ``` **Scope:** `guest:create` **Use Case:** Permitir que visitantes do seu site ou aplicativo abram tickets de suporte sem criar uma conta completa, reduzindo fricção no processo de obter ajuda. **Request Body:** ```json { "name": "Visitante João" } ``` **Campos do Request:** | Campo | Tipo | Obrigatório | Descrição | |-------|------|-------------|-----------| | `name` | string | Não | Nome do visitante. Se não fornecido, será gerado automaticamente | **Response: `201 Created`** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Visitante João", "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` **Campos do Response:** | Campo | Tipo | Descrição | |-------|------|-----------| | `id` | uuid | ID único do usuário convidado | | `name` | string | Nome do convidado | | `access_token` | string | Token JWT para autenticar requests em nome deste convidado | **Exemplo Completo:** ```bash # Criar guest user curl -X POST https://acme.pumahelp.com/api/v2/guests \ -H "X-API-Key: SEU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Visitante da Landing Page"}' # Usar o access_token do guest para criar ticket curl -X POST https://acme.pumahelp.com/api/v1/tickets \ -H "Authorization: Bearer TOKEN_DO_GUEST" \ -H "Content-Type: application/json" \ -d '{ "subject": "Dúvida sobre o produto", "priority": "normal", "type": "question", "comment": { "body": "Gostaria de saber mais sobre os planos disponíveis", "public": true } }' ``` :::tip Use este recurso para implementar um formulário de contato avançado ou chat widget onde visitantes possam criar tickets sem criar conta. ::: --- ### 👥 Grupos #### Criar Grupo ```http POST https://acme.pumahelp.com/api/v1/groups Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `group:create` **Request Body:** ```json { "name": "Suporte Técnico", "description": "Equipe responsável por suporte técnico" } ``` | Campo | Tipo | Obrigatório | Descrição | |-------|------|-------------|-----------| | `name` | string | Sim | Nome do grupo | | `description` | string | Não | Descrição do grupo | **Response: `201 Created`** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Suporte Técnico", "description": "Equipe responsável por suporte técnico", "default": false } ``` --- #### Listar Grupos ```http GET https://acme.pumahelp.com/api/v1/groups?page=1&page_size=50 Authorization: Bearer {token} ``` **Scope:** `group:read` **Query Parameters:** | Parâmetro | Tipo | Descrição | Obrigatório | |-----------|------|-----------|-------------| | `page` | integer | Número da página | Sim | | `page_size` | integer | Itens por página | Sim | | `query` | string | Busca por nome ou descrição | Não | | `user_id` | uuid | Filtrar grupos que contêm um usuário específico | Não | **Response: `200 OK`** ```json { "count": 3, "groups": [ { "id": "uuid", "name": "Suporte Técnico", "description": "Equipe responsável por suporte técnico", "default": false }, { "id": "uuid", "name": "Vendas", "description": "Equipe de vendas", "default": true } ] } ``` --- #### Listar Grupos com Usuários ```http GET https://acme.pumahelp.com/api/v1/groups/users?page=1&page_size=50 Authorization: Bearer {token} ``` **Scope:** `group:read` **Query Parameters:** | Parâmetro | Tipo | Descrição | Obrigatório | |-----------|------|-----------|-------------| | `page` | integer | Número da página | Sim | | `page_size` | integer | Itens por página | Sim | | `query` | string | Busca por nome ou descrição | Não | | `user_id` | uuid | Filtrar grupos que contêm um usuário específico | Não | **Response: `200 OK`** ```json { "count": 3, "groups": [ { "id": "uuid", "name": "Suporte Técnico", "description": "Equipe responsável por suporte técnico", "default": false, "users": [ { "id": "uuid", "name": "Maria Santos", "email": "maria@example.com", "role": "agent", "external_id": null, "verified": true }, { "id": "uuid", "name": "João Silva", "email": "joao@example.com", "role": "agent", "external_id": null, "verified": true } ] } ] } ``` :::tip Use este endpoint quando precisar dos membros de cada grupo. Mais eficiente que buscar grupo por grupo. ::: --- #### Obter Grupo por ID ```http GET https://acme.pumahelp.com/api/v1/groups/{group_id} Authorization: Bearer {token} ``` **Scope:** `group:read` **Response: `200 OK`** ```json { "id": "uuid", "name": "Suporte Técnico", "description": "Equipe responsável por suporte técnico", "default": false } ``` --- #### Atualizar Grupo ```http PUT https://acme.pumahelp.com/api/v1/groups/{group_id} Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `group:update` **Request Body:** ```json { "name": "Suporte Técnico Nível 2", "description": "Equipe de suporte avançado" } ``` | Campo | Tipo | Descrição | |-------|------|-----------| | `name` | string | Novo nome do grupo | | `description` | string | Nova descrição | :::note Todos os campos são opcionais. Apenas os campos enviados serão atualizados. ::: **Response: `204 No Content`** --- #### Deletar Grupo ```http DELETE https://acme.pumahelp.com/api/v1/groups/{group_id} Authorization: Bearer {token} ``` **Scope:** `group:delete` **Response: `204 No Content`** :::note Para adicionar ou remover usuários de um grupo, use o endpoint de atualização de usuário (`PUT /v1/users/{user_id}`) enviando o campo `group_ids`. apenas os grupos padrão não podem ser removidos. ::: --- ### 🤖 Macros #### Criar Macro ```http POST https://acme.pumahelp.com/api/v1/macros Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `macro:create` **Request Body:** ```json { "title": "Resposta Padrão - Problema de Login", "description": "Resposta automática para problemas de login", "type": "group", "group_ids": ["uuid-grupo-1", "uuid-grupo-2"], "actions": [ { "field": "status", "value": "pending" }, { "field": "priority", "value": "high" }, { "field": "comment_value", "value": "Por favor, tente limpar o cache do navegador e fazer login novamente." } ] } ``` | Campo | Tipo | Obrigatório | Descrição | |-------|------|-------------|-----------| | `title` | string | Sim | Título da macro | | `description` | string | Não | Descrição da macro | | `type` | string | Sim | Tipo: `user` (usuário) ou `group` (grupo) | | `group_ids` | array[uuid] | Não | IDs dos grupos que podem usar esta macro (obrigatório se type=group) | | `actions` | array[object] | Sim | Lista de ações a executar | | `actions[].field` | string | Sim | Campo a modificar: `comment_value`, `status`, `type`, `priority` | | `actions[].value` | any | Sim | Valor a aplicar (tipo depende do field) | **Response: `201 Created`** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Resposta Padrão - Problema de Login", "description": "Resposta automática para problemas de login", "type": "auto", "group_ids": ["uuid-grupo-1"], "actions": [ { "field": "status", "value": "pending" }, { "field": "comment_value", "value": "Por favor, tente limpar o cache..." } ], "created_at": "2025-01-01T10:00:00Z" } ``` --- #### Listar Macros ```http GET https://acme.pumahelp.com/api/v1/macros?page=1&page_size=50 Authorization: Bearer {token} ``` **Scope:** `macro:read` **Query Parameters:** | Parâmetro | Tipo | Descrição | Obrigatório | |-----------|------|-----------|-------------| | `page` | integer | Número da página | Sim | | `page_size` | integer | Itens por página | Sim | **Response: `200 OK`** ```json { "count": 5, "macros": [ { "id": "uuid", "title": "Resposta Padrão - Problema de Login", "description": "Resposta automática para problemas de login", "type": "user", "group_ids": [], "actions": [], "created_at": "2025-01-01T10:00:00Z" } ] } ``` --- #### Obter Macro por ID ```http GET https://acme.pumahelp.com/api/v1/macros/{macro_id} Authorization: Bearer {token} ``` **Scope:** `macro:read` **Response: `200 OK`** ```json { "id": "uuid", "title": "Resposta Padrão - Problema de Login", "description": "Resposta automática para problemas de login", "type": "user", "group_ids": ["uuid-grupo-1"], "actions": [ { "field": "status", "value": "pending" }, { "field": "priority", "value": "high" } ], "created_at": "2025-01-01T10:00:00Z" } ``` --- #### Atualizar Macro ```http PUT https://acme.pumahelp.com/api/v1/macros/{macro_id} Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `macro:update` **Request Body:** ```json { "title": "Resposta Padrão - Login Atualizada", "description": "Nova descrição", "type": "group", "group_ids": ["uuid-grupo-3"], "actions": [ { "field": "status", "value": "solved" } ] } ``` :::note Todos os campos são opcionais. Apenas os campos enviados serão atualizados. ::: **Response: `200 OK`** ```json { "id": "uuid", "title": "Resposta Padrão - Login Atualizada", "description": "Nova descrição", "type": "group", "group_ids": ["uuid-grupo-3"], "actions": [ { "field": "status", "value": "solved" } ], "created_at": "2025-01-01T10:00:00Z" } ``` --- #### Deletar Macro ```http DELETE https://acme.pumahelp.com/api/v1/macros/{macro_id} Authorization: Bearer {token} ``` **Scope:** `macro:delete` **Response: `204 No Content`** --- ### 👁️ Visualizações Visualizações são filtros de tickets salvos com nome — os atalhos da barra lateral do Inbox. Cada uma guarda uma `query` na mesma sintaxe do filtro de listagem (`status:open assignee:me …`) e, opcionalmente, uma ordenação. Existem dois escopos: | Escopo | Quem vê | Quem cria/edita/apaga | Limite | |--------|---------|-----------------------|--------| | `organization` | Todos os agentes da organização | Owner e Admin | 5 por organização | | `personal` | Apenas quem criou | O próprio usuário (Owner, Admin ou Agente) | 5 por usuário | Toda organização nasce com 5 visualizações padrão no escopo `organization` ("Seus tickets sem resolução", "Tickets não atribuídos", "Todos os tickets sem resolução", "Tickets resolvidos recentemente" e "Tickets pendentes"). Elas são comuns — podem ser renomeadas, alteradas ou apagadas por Owner/Admin. Macros como `assignee:me` são resolvidas para quem está chamando, então uma visualização da organização mostra (e conta) tickets diferentes para cada agente. #### Listar Visualizações ```http GET https://acme.pumahelp.com/api/v1/views Authorization: Bearer {token} ``` **Scope:** `view:read` Retorna as visualizações da organização mais as pessoais do usuário autenticado, ordenadas por `position`. **Response: `200 OK`** ```json { "count": 2, "views": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Seus tickets sem resolução", "query": "assignee:me status:new status:open status:pending", "sort_by": null, "sort_order": null, "scope": "organization", "position": 1, "created_at": "2025-01-01T10:00:00Z", "updated_at": null }, { "id": "660e8400-e29b-41d4-a716-446655440001", "name": "Urgentes do meu grupo", "query": "priority:urgent group:suporte", "sort_by": "created_at", "sort_order": "desc", "scope": "personal", "position": 1, "created_at": "2025-01-02T10:00:00Z", "updated_at": null } ] } ``` --- #### Contagem por Visualização ```http GET https://acme.pumahelp.com/api/v1/views/counts Authorization: Bearer {token} ``` **Scope:** `view:read` Retorna quantos tickets cada visualização listaria **para o usuário autenticado** — a mesma visibilidade da listagem de tickets (agentes veem apenas os tickets dos seus grupos). **Response: `200 OK`** ```json { "counts": [ { "view_id": "550e8400-e29b-41d4-a716-446655440000", "count": 12 }, { "view_id": "660e8400-e29b-41d4-a716-446655440001", "count": 3 } ] } ``` --- #### Criar Visualização ```http POST https://acme.pumahelp.com/api/v1/views Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `view:create` **Request Body:** ```json { "name": "Urgentes do meu grupo", "query": "priority:urgent group:suporte", "sort_by": "created_at", "sort_order": "desc", "scope": "personal" } ``` | Campo | Tipo | Obrigatório | Descrição | |-------|------|-------------|-----------| | `name` | string | Sim | Nome exibido (máx. 80). Único dentro do escopo | | `query` | string | Não | Filtro na sintaxe de `GET /v1/tickets?query=` (máx. 500). Vazio = todos os tickets visíveis | | `sort_by` | string | Não | `status`, `priority`, `created_at` ou `updated_at` | | `sort_order` | string | Não | `asc` ou `desc` | | `scope` | string | Sim | `organization` (somente Owner/Admin) ou `personal` | **Response: `201 Created`** — mesmo formato de um item da listagem. **Erros:** - `400` — limite de 5 atingido no escopo, nome já usado no escopo, ou campo inválido - `401` — agente tentando criar uma visualização `organization` --- #### Atualizar Visualização ```http PUT https://acme.pumahelp.com/api/v1/views/{view_id} Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `view:update` Todos os campos são opcionais; só os enviados são alterados. Envie `sort_by: ""` para remover a ordenação. ```json { "name": "Urgentes", "query": "priority:urgent status:open" } ``` **Response: `200 OK`** — a visualização atualizada. **Erros:** - `401` — agente tentando alterar uma visualização `organization` - `404` — visualização inexistente ou pessoal de outro usuário --- #### Deletar Visualização ```http DELETE https://acme.pumahelp.com/api/v1/views/{view_id} Authorization: Bearer {token} ``` **Scope:** `view:delete` Mesmas regras de posse do `PUT`. **Response: `204 No Content`** --- ### 🔗 Webhooks #### Criar Webhook ```http POST https://acme.pumahelp.com/api/v1/webhooks Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `webhook:create` **Request Body:** ```json { "name": "Notificação Slack", "url": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXX", "events": ["ticket.created", "ticket.updated", "user.created"] } ``` | Campo | Tipo | Obrigatório | Descrição | |-------|------|-------------|-----------| | `name` | string | Sim | Nome do webhook | | `url` | string | Sim | URL que receberá as notificações | | `events` | array[string] | Sim | Eventos a monitorar: `ticket.created`, `ticket.updated`, `user.created`, `user.updated`, `user.deleted` | **Response: `201 Created`** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Notificação Slack", "url": "https://hooks.slack.com/services/...", "events": ["ticket.created", "ticket.updated", "user.created"], "created_at": "2025-01-01T10:00:00Z", "key": "whk_live_a1b2c3d4e5_secretkeyhereXXXXXXXX" } ``` :::note Cada organização pode criar no máximo **5 webhooks**. Se você atingir esse limite, será necessário deletar um webhook existente antes de criar um novo. ::: :::important A `key` é gerada automaticamente durante a criação do webhook e é usada para assinar as requisições enviadas. Guarde-a de forma segura, pois será necessária para validar a autenticidade dos webhooks recebidos. ::: --- #### Listar Webhooks ```http GET https://acme.pumahelp.com/api/v1/webhooks?page=1&page_size=50 Authorization: Bearer {token} ``` **Scope:** `webhook:read` **Query Parameters:** | Parâmetro | Tipo | Descrição | Obrigatório | |-----------|------|-----------|-------------| | `page` | integer | Número da página | Sim | | `page_size` | integer | Itens por página | Sim | **Response: `200 OK`** ```json { "count": 2, "webhooks": [ { "id": "uuid", "name": "Notificação Slack", "url": "https://hooks.slack.com/services/...", "events": ["ticket.created", "ticket.updated"], "created_at": "2025-01-01T10:00:00Z", "key": "whk_live_a1b2c3d4e5_secretkeyhereXXXXXXXX" } ] } ``` --- #### Obter Webhook ```http GET https://acme.pumahelp.com/api/v1/webhooks/{webhook_id} Authorization: Bearer {token} ``` **Scope:** `webhook:read` **Response: `200 OK`** ```json { "id": "uuid", "name": "Notificação Slack", "url": "https://hooks.slack.com/services/...", "events": ["ticket.created", "ticket.updated", "user.created"], "created_at": "2025-01-01T10:00:00Z", "key": "whk_live_a1b2c3d4e5_secretkeyhereXXXXXXXX" } ``` --- #### Atualizar Webhook ```http PUT https://acme.pumahelp.com/api/v1/webhooks/{webhook_id} Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `webhook:update` **Request Body:** ```json { "name": "Notificação Slack Atualizada", "url": "https://hooks.slack.com/services/UPDATED", "events": ["ticket.created", "user.deleted"] } ``` | Campo | Tipo | Descrição | |-------|------|-----------| | `name` | string | Novo nome do webhook | | `url` | string | Nova URL | | `events` | array[string] | Novos eventos | :::note Todos os campos são opcionais. Apenas os campos enviados serão atualizados. ::: **Response: `204 No Content`** --- #### Deletar Webhook ```http DELETE https://acme.pumahelp.com/api/v1/webhooks/{webhook_id} Authorization: Bearer {token} ``` **Scope:** `webhook:delete` **Response: `204 No Content`** --- #### Rotacionar Chave do Webhook ```http PUT https://acme.pumahelp.com/api/v1/webhooks/rotate/{webhook_id} Authorization: Bearer {token} ``` **Scope:** `webhook:rotate` **Descrição:** Gera uma nova chave de assinatura para o webhook. A chave antiga é invalidada imediatamente. :::warning Após rotacionar a chave, atualize imediatamente sua aplicação que recebe os webhooks com a nova chave para validação de assinatura HMAC. Webhooks enviados após a rotação usarão a nova chave. ::: **Response: `200 OK`** ```json { "key": "whk_live_x9y8z7w6v5_newsecretkeyXXXXXXXX" } ``` | Campo | Tipo | Descrição | |-------|------|-----------| | `key` | string | Nova chave gerada (com prefixo `whk_live_`) | **Exemplo de validação HMAC em Node.js:** ```javascript const crypto = require('crypto'); function validateWebhook(body, signature, timestamp, webhookKey) { const stringToSign = `${timestamp}.${JSON.stringify(body)}`; const expectedSignature = crypto .createHmac('sha256', webhookKey) .update(stringToSign) .digest('base64'); return signature === expectedSignature; } ``` --- ### 🔑 API Keys #### Listar API Keys ```http GET https://acme.pumahelp.com/api/v1/keys?page=1&page_size=50 Authorization: Bearer {token} ``` **Scope:** `apikey:read` **Query Parameters:** | Parâmetro | Tipo | Descrição | Obrigatório | |-----------|------|-----------|-------------| | `page` | integer | Número da página | Sim | | `page_size` | integer | Itens por página | Sim | **Response: `200 OK`** ```json { "count": 3, "keys": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "prefix": "rk_live_", "name": "Integration - CRM", "description": "API Key para sincronização com CRM", "scopes": ["ticket:create", "ticket:read", "user:upsert"], "key_lookup": "a1b2c3d4e5f6", "created_at": "2025-01-01T10:00:00Z" } ] } ``` | Campo | Tipo | Descrição | |-------|------|-----------| | `id` | uuid | ID da API Key | | `prefix` | string | Prefixo da chave para identificação rápida | | `name` | string | Nome da API Key | | `description` | string | Descrição | | `scopes` | array[string] | Escopos atribuídos | | `key_lookup` | string | Lookup hash para busca interna | | `created_at` | datetime | Data de criação | --- #### Criar API Key ```http POST https://acme.pumahelp.com/api/v1/keys Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `apikey:create` **Request Body:** ```json { "name": "Integration - CRM", "description": "Para sincronização automática de tickets e usuários", "scopes": ["ticket:create", "ticket:read", "user:upsert"] } ``` | Campo | Tipo | Obrigatório | Descrição | |-------|------|-------------|-----------| | `name` | string | Sim | Nome da API Key | | `description` | string | Não | Descrição do uso da chave | | `scopes` | array[string] | Sim | Lista de escopos permitidos | **Response: `201 Created`** ```json { "key": "rk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" } ``` :::caution A chave completa (`key`) é exibida **apenas uma vez**! Guarde-a imediatamente em local seguro. Após esta resposta, você só verá o `prefix` e `key_lookup` ao listar as chaves. ::: --- #### Atualizar API Key ```http PUT https://acme.pumahelp.com/api/v1/keys/{key_id} Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `apikey:update` **Request Body:** ```json { "name": "Integration - CRM v2", "description": "Atualizado para nova integração", "scopes": ["ticket:create", "ticket:read", "ticket:update", "user:upsert"] } ``` | Campo | Tipo | Descrição | |-------|------|-----------| | `name` | string | Novo nome | | `description` | string | Nova descrição | | `scopes` | array[string] | Novos escopos | :::note Todos os campos são opcionais. Apenas os campos enviados serão atualizados. ::: **Response: `200 OK`** ```json { "id": "uuid", "prefix": "rk_live_", "name": "Integration - CRM v2", "description": "Atualizado para nova integração", "scopes": ["ticket:create", "ticket:read", "ticket:update", "user:upsert"], "key_lookup": "a1b2c3d4e5f6", "created_at": "2025-01-01T10:00:00Z" } ``` --- #### Rotacionar API Key ```http PUT https://acme.pumahelp.com/api/v1/keys/rotate/{key_id} Authorization: Bearer {token} ``` **Scope:** `apikey:rotate` **Response: `200 OK`** ```json { "key": "rk_live_x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4j3i2h1g0" } ``` :::warning Ao rotacionar, a chave antiga torna-se inválida imediatamente. Atualize seus sistemas com a nova chave antes de rotacionar! ::: --- #### Deletar API Key ```http DELETE https://acme.pumahelp.com/api/v1/keys/{key_id} Authorization: Bearer {token} ``` **Scope:** `apikey:delete` **Response: `204 No Content`** :::caution Esta ação é irreversível. Sistemas usando esta chave perderão acesso imediatamente. ::: --- ### 🏢 Organização #### Obter Organização ```http GET https://acme.pumahelp.com/api/v1/organizations Authorization: Bearer {token} ``` **Scope:** `organization:read` **Response: `200 OK`** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Acme Corporation", "app_name": "acme", "subscription_status": "active", "created_at": "2025-01-01T10:00:00Z" } ``` --- ### 📁 Upload de Arquivos #### Upload de Arquivo ```http POST https://acme.pumahelp.com/api/v1/uploads Authorization: Bearer {token} Content-Type: multipart/form-data ``` **Scope:** `file:upload` **Limite:** 50MB por arquivo **Request Body (multipart):** ``` file: (binary data) ``` **Response: `201 Created`** ```json { "id": "uuid", "file_name": "screenshot.png", "content_type": "image/png", "size": 102400, "content_url": "https://cdn.pumahelp.com/files/abc123xyz789" } ``` **Tipos de Arquivo Suportados:** - Imagens: `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp` - Documentos: `.pdf`, `.doc`, `.docx`, `.xls`, `.xlsx`, `.txt` - Outros: `.zip`, `.rar`, `.csv` **Exemplo:** ```bash curl -X POST https://acme.pumahelp.com/api/v1/uploads \ -H "Authorization: Bearer SEU_TOKEN" \ -F "file=@/caminho/para/arquivo.png" ``` **Exemplo com JavaScript:** ```javascript const formData = new FormData(); formData.append('file', fileInput.files[0]); fetch('https://acme.pumahelp.com/api/v1/uploads', { method: 'POST', headers: { 'Authorization': 'Bearer SEU_TOKEN' }, body: formData }) .then(res => res.json()) .then(data => { console.log('Arquivo enviado:', data.content_url); }); ``` :::note Após o upload, use o `id` retornado para anexar o arquivo a um comentário ao criar ou atualizar um ticket. ::: --- ### 🔍 Escopos Disponíveis #### Listar Escopos ```http GET https://acme.pumahelp.com/api/v1/scopes?category=ticket Authorization: Bearer {token} ``` **Scope:** Público (qualquer usuário autenticado) **Query Parameters:** | Parâmetro | Tipo | Descrição | Obrigatório | |-----------|------|-----------|-------------| | `category` | string | Filtrar por categoria | Não | **Response: `200 OK`** ```json { "count": 15, "categories_count": { "ticket": 5, "user": 4, "group": 3, "macro": 3 }, "scopes": [ { "scope": "ticket:read", "name": "Ler Tickets", "description": "Permite visualizar tickets", "category": "ticket" }, { "scope": "ticket:create", "name": "Criar Tickets", "description": "Permite criar novos tickets", "category": "ticket" } ] } ``` --- ### ⚙️ Configurações de Conta #### Alterar Email ```http POST https://acme.pumahelp.com/api/v1/account/email/change Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `account:update:own` **Request Body:** ```json { "new_email": "novoemail@example.com", "password": "senha-atual" } ``` **Response: `204 No Content`** --- #### Solicitar Redefinição de Senha ```http POST https://acme.pumahelp.com/api/v1/account/password/forgot Content-Type: application/json ``` **Rate Limit:** IP-based :::note Endpoint público. Envia email com token de redefinição. ::: **Request Body:** ```json { "email": "usuario@example.com" } ``` **Response: `204 No Content`** --- #### Redefinir Senha ```http POST https://acme.pumahelp.com/api/v1/account/password/reset Content-Type: application/json ``` **Rate Limit:** IP-based **Request Body:** ```json { "token": "token-recebido-por-email", "new_password": "nova-senha-segura" } ``` **Response: `204 No Content`** --- #### Alterar Senha ```http PUT https://acme.pumahelp.com/api/v1/account/password/change Authorization: Bearer {token} Content-Type: application/json ``` **Scope:** `account:update:own` **Request Body:** ```json { "current_password": "senha-atual", "new_password": "nova-senha-segura" } ``` **Response: `204 No Content`** --- ## ❌ Códigos de Erro A API retorna códigos HTTP padrão: | Código | Significado | Descrição | |--------|-------------|-----------| | `200` | OK | Requisição bem-sucedida | | `201` | Created | Recurso criado com sucesso | | `204` | No Content | Recurso excluído com sucesso | | `400` | Bad Request | Dados inválidos na requisição | | `401` | Unauthorized | Token inválido ou ausente | | `403` | Forbidden | Sem permissão (scope insuficiente) | | `404` | Not Found | Recurso não encontrado | | `409` | Conflict | Conflito (ex: email já existe) | | `422` | Unprocessable Entity | Validação falhou | | `429` | Too Many Requests | Rate limit excedido | | `500` | Internal Server Error | Erro no servidor | **Formato de Erro:** ```json { "errors": [ "O campo 'email' é obrigatório", "O campo 'password' deve ter no mínimo 8 caracteres" ] } ``` --- ## Quick Start ```bash # 1. Login (substitua 'acme' pelo seu subdomínio) curl -X POST https://acme.pumahelp.com/api/v1/users/login \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "password": "senha" }' # Response: # { # "access_token": "eyJhbGc...", # "refresh_token": "eyJhbGc..." # } # 2. Criar API Key (opcional, para integrações) curl -X POST https://acme.pumahelp.com/api/v1/keys \ -H "Authorization: Bearer SEU_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Minha Integração", "scopes": ["ticket:create", "ticket:read"] }' # 3. Criar Ticket (com JWT) curl -X POST https://acme.pumahelp.com/api/v1/tickets \ -H "Authorization: Bearer SEU_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "subject": "Meu primeiro ticket via API", "priority": "normal", "type": "question", "comment": { "body": "Testando a integração com a API", "public": true } }' # OU com API Key curl -X POST https://acme.pumahelp.com/api/v1/tickets \ -H "X-API-Key: SEU_API_KEY_SECRET" \ -H "Content-Type: application/json" \ -d '{...}' # 4. Listar Tickets curl -X GET "https://acme.pumahelp.com/api/v1/tickets?page=1&page_size=10" \ -H "Authorization: Bearer SEU_ACCESS_TOKEN" ``` :::tip Substitua `acme` em todos os exemplos pelo seu **App Name** configurado no dashboard. ::: --- ## 💡 Melhores Práticas ### Segurança - ✅ Use HTTPS sempre - ✅ Nunca exponha secrets no código versionado - ✅ Rotacione API Keys regularmente - ✅ Use princípio do menor privilégio (scopes mínimos necessários) - ✅ Valide assinaturas HMAC dos webhooks ### Performance - ✅ Use paginação adequada (`page_size` adequado) - ✅ Implemente retry logic com backoff exponencial - ✅ Cache dados que mudam pouco - ✅ Monitore headers `X-RateLimit-*` - ✅ Use webhooks em vez de polling ### Integração - ✅ Valide dados antes de enviar - ✅ Trate todos os códigos de erro - ✅ Implemente logs para auditoria - ✅ Teste em ambiente de desenvolvimento primeiro --- ## 🔧 Troubleshooting ### Erro 401: Unauthorized **Sintoma:** Todas as requisições retornam `401 Unauthorized` **Causas Comuns:** 1. Token JWT expirado 2. API Key inválida ou revogada 3. Header de autorização mal formatado **Soluções:** ```bash # Verifique o formato do header # ✅ Correto: Authorization: Bearer eyJhbGci... # ❌ Incorreto: Authorization: eyJhbGci... Authorization: bearer eyJhbGci... # Para API Key: # ✅ Correto: X-API-Key: rk_live_abc123... # Renovar token expirado: curl -X POST https://acme.pumahelp.com/api/v1/tokens/refresh \ -H "Content-Type: application/json" \ -d '{"refresh_token": "SEU_REFRESH_TOKEN"}' ``` --- ### Erro 403: Forbidden **Sintoma:** Request autenticado mas retorna `403 Forbidden` **Causas Comuns:** 1. API Key sem o scope necessário 2. Usuário sem permissão para a ação **Soluções:** ```bash # Verificar scopes da sua API Key: curl -X GET https://acme.pumahelp.com/api/v1/keys \ -H "Authorization: Bearer SEU_TOKEN" # Verificar scopes disponíveis: curl -X GET https://acme.pumahelp.com/api/v1/scopes \ -H "Authorization: Bearer SEU_TOKEN" # Atualizar API Key com scopes necessários ou criar nova ``` --- ### Erro 422: Unprocessable Entity **Sintoma:** Request retorna `422` com mensagem de validação **Causas Comuns:** 1. Campos obrigatórios faltando 2. Formato de dados inválido 3. Valor fora do range permitido **Exemplo de Response:** ```json { "errors": [ "O campo 'subject' é obrigatório", "O campo 'priority' deve ser: low, normal, high ou urgent" ] } ``` **Soluções:** - Revise a documentação do endpoint para campos obrigatórios - Verifique tipos de dados esperados - Valide valores enum (status, priority, type) --- ### Erro 429: Too Many Requests **Sintoma:** Requests bloqueados com `429 Too Many Requests` **Causa:** Rate limit excedido **Solução:** ```javascript // Implementar retry com backoff exponencial async function requestWithRetry(url, options, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { const response = await fetch(url, options); if (response.status === 429) { const retryAfter = response.headers.get('Retry-After') || 10; const delay = Math.pow(2, i) * 1000; // Backoff exponencial await new Promise(resolve => setTimeout(resolve, Math.max(delay, retryAfter * 1000))); continue; } return response; } throw new Error('Max retries exceeded'); } ``` --- ### Webhook Não Está Sendo Chamado **Sintomas:** - Webhook criado mas não recebe eventos - Eventos não aparecem nos logs **Checklist:** 1. ✅ URL está acessível publicamente (não localhost)? 2. ✅ URL usa HTTPS? 3. ✅ Servidor responde com `200 OK` em menos de 30 segundos? 4. ✅ Eventos selecionados estão corretos? **Testar Webhook:** ```bash # Verificar status do webhook: curl -X GET https://acme.pumahelp.com/api/v1/webhooks/{webhook_id} \ -H "Authorization: Bearer SEU_TOKEN" # Criar evento de teste (criar ticket): curl -X POST https://acme.pumahelp.com/api/v1/tickets \ -H "X-API-Key: SEU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"subject": "Teste Webhook", "priority": "normal", "type": "question", "comment": {"body": "Teste", "public": true}}' ``` --- ### FAQ **P: Posso usar a mesma API Key em múltiplos ambientes?** R: Não é recomendado. Crie API Keys separadas para desenvolvimento, staging e produção. **P: Quantos webhooks posso criar?** R: Máximo de 5 webhooks por organização. Recomendamos consolidar eventos relacionados em um único endpoint quando possível. **P: Como sei qual scope usar?** R: Consulte `GET /v1/scopes` para lista completa. Use o princípio do menor privilégio: apenas os scopes necessários. **P: Posso deletar permanentemente um ticket?** R: Não. DELETE arquiva o ticket (soft delete). Isso preserva histórico e auditoria. --- ### Status de Ticket **new** Ticket recém criado, aguardando primeira atribuição ou triagem. **open** Ticket em andamento, sendo trabalhado por um agente. **pending** Aguardando resposta do cliente ou informação externa. **solved** Ticket resolvido. Cliente pode reabrir se necessário. **closed** Ticket fechado. Não pode ser reaberto. --- ### Prioridades **low** - Baixa prioridade **normal** - Prioridade normal (padrão) **high** - Alta prioridade **urgent** - Urgente --- ### Tipos de Ticket **question** - Pergunta/dúvida **incident** - Problema/erro **problem** - Problema complexo que afeta múltiplos usuários **task** - Tarefa a ser realizada --- ### Roles de Usuário **end-user** Usuário final, cliente. Pode criar tickets e visualizar apenas seus próprios tickets. **agent** Agente de suporte. Pode visualizar, atualizar e resolver tickets. **admin** Administrador. Pode gerenciar usuários, grupos, configurações e tem acesso completo. **owner** Proprietário da organização. Acesso total incluindo configurações de billing e organização. --- ### Eventos de Webhook **ticket.created** - Novo ticket criado **ticket.updated** - Ticket atualizado (status, prioridade, assignee, etc) **user.created** - Novo usuário criado **user.updated** - Usuário atualizado **user.deleted** - Usuário deletado --- **Versão da API:** v2.1.0 **Última Atualização:** Dezembro de 2025 --- ## Widget # Widget Documentation Guia completo para integrar o widget de suporte PumaHelp em sua aplicação. ## 🚀 Início Rápido ### Instalação Adicione o script do widget ao seu HTML: ```html ``` ### Uso Básico **Opção 1: Web Component (HTML)** ```html ``` **Opção 2: API JavaScript** ```javascript const widget = window.PumaHelp.init({ appName: 'seu-subdominio', apiKey: 'key-xxxxxxxxxxxxx' }); ``` --- ## ⚙️ Configuração ### Parâmetros Obrigatórios | Parâmetro | Tipo | Descrição | |-----------|------|-----------| | `appName` | `string` | Seu subdomínio PumaHelp (ex: `minha-empresa`) | | `apiKey` | `string` | Sua chave de API (começa com `rk_live_`) | :::important **Segurança da API Key**: Para uso no widget frontend, crie uma **chave restrita** no dashboard do PumaHelp com **apenas a permissão "Criar Convidado (Guest)"**. Nunca use chaves com permissões administrativas no código client-side, pois elas ficam expostas no navegador. ::: ### Parâmetros Opcionais | Parâmetro | Tipo | Padrão | Descrição | |-----------|------|--------|-----------| | `theme` | `'light' \| 'dark'` | `'dark'` | Esquema de cores do widget | | `language` | `'pt-BR' \| 'en'` | `'pt-BR'` | Idioma da interface | | `color` | `string` | - | Cor primária (formato hex: `#FF6B35`) | | `icon` | Tipo do ícone | `'puma'` | Ícone do botão do widget | | `soundEnabled` | `boolean` | `true` | Habilitar sons de notificação | | `debug` | `boolean` | `false` | Habilitar logs de debug | | `translations` | `object` \| `string` (JSON) | - | Textos personalizados | | `embedded` | `boolean` | `false` | Modo embarcado (sem botão flutuante, ocupa 100% do container) | **Ícones disponíveis:** `'puma'`, `'emoji'`, `'question'`, `'lines'`, `'bell'`, `'send'` ### Inicialização em Container (Modo Embarcado) Você pode renderizar o widget dentro de um elemento específico da sua página, útil para criar painéis de suporte fixos. ```javascript // O container deve possuir position: relative e altura definida const container = document.getElementById('support-panel'); window.PumaHelp.init({ appName: 'minha-empresa', apiKey: 'key-xxx', container: container, // Elemento DOM onde o widget será renderizado // embedded: true // (Opcional) Automaticamente true se container for passado }); ``` ### Personalização de Textos Você pode personalizar todos os textos estáticos do widget passando um objeto ou string JSON. ```javascript window.PumaHelp.init({ // ... translations: { welcome: "Olá, como podemos ajudar?", sendMessage: "Escreva sua dúvida..." } // Também aceita JSON stringificado: // translations: '{"welcome": "Olá!"}' }); ``` **Chaves disponíveis:** | Chave | Padrão (pt-BR / en) | Descrição | |-------|---------------------|-----------| | `mainTitle` | `Suporte` | O título principal do widget (cabeçalho) | | `welcome` | `Olá! 👋` / `Hello! 👋` | Saudação no formulário de novo ticket | | `description` | `Como podemos...` | Descrição no formulário de novo ticket | | `startNewChat` | `Nova Conversa` / `New Conversation` | Texto do botão para iniciar chat | | `sendMessage` | `Digite uma mensagem...` | Placeholder para campos de mensagem | | `loading` | `Carregando...` | Texto de estado de carregamento | | `endTicket` | `O atendimento foi finalizado.` | Mensagem quando o ticket é fechado | | `startedAt` | `Iniciado em` / `Started at` | Texto do cabeçalho na visualização do chat (seguido da data) | ### Exemplos **Tema escuro com cor personalizada:** ```javascript window.PumaHelp.init({ appName: 'acme', apiKey: 'key-abc123', theme: 'dark', color: '#2563eb', language: 'en', translations: { mainTitle: 'Central de Ajuda', welcome: 'Olá!', sendMessage: 'Digite aqui...' } }); ``` ```html ``` --- ## 🛡️ Segurança e Boas Práticas ### API Key Restrita O widget é executado no **navegador do usuário** (client-side), portanto a `apiKey` fica **visível** no código-fonte da página. Para proteger seu sistema: #### ✅ O que fazer: 1. **Crie uma chave restrita** no dashboard do PumaHelp 2. Configure **apenas a permissão**: `Criar Convidado (Guest)` 3. Use essa chave no widget ```html > ``` #### ❌ O que NÃO fazer: - ❌ **Nunca** use chaves com permissões administrativas no widget - ❌ **Nunca** use chaves que podem: - Deletar tickets - Modificar configurações - Acessar dados de outros usuários - Executar operações privilegiadas --- ## 📘 Tipagem TypeScript Para projetos TypeScript, crie o arquivo `puma-widget.d.ts` na raiz do seu projeto (ou em `src/types/`): ### Arquivo de Tipos (`puma-widget.d.ts`) ```typescript import * as React from 'react'; // Atributos do Widget interface PumaWidgetAttributes { 'app-name'?: string; 'api-key'?: string; theme?: 'light' | 'dark'; language?: 'pt-BR' | 'en'; color?: string; icon?: 'puma' | 'emoji' | 'question' | 'lines' | 'bell' | 'send'; 'sound-enabled'?: string; debug?: string; translations?: string; embedded?: string; class?: string; className?: string; style?: React.CSSProperties; children?: React.ReactNode; } // Configuração do Widget interface WidgetConfig { appName: string; apiKey: string; theme?: 'light' | 'dark'; language?: 'pt-BR' | 'en'; color?: string; icon?: 'puma' | 'emoji' | 'question' | 'lines' | 'bell' | 'send'; soundEnabled?: boolean; debug?: boolean; translations?: Translations | string; embedded?: boolean; } // Traduções Customizáveis interface Translations { mainTitle?: string; welcome?: string; description?: string; startNewChat?: string; sendMessage?: string; loading?: string; endTicket?: string; startedAt?: string; [key: string]: string | undefined; } // Identidade do Usuário interface UserIdentity { accessToken: string; name?: string; } // Instância do Widget interface WidgetInstance { id: string; element: HTMLElement; isOpen: boolean; isHidden: boolean; open(): void; close(): void; toggle(): void; hide(): void; show(): void; destroy(): void; identify(identity: UserIdentity): void; logout(): void; on(event: string, callback: (event: CustomEvent) => void): () => void; } // API Global interface PumaHelpAPI { init(config: WidgetConfig & { container?: HTMLElement }): WidgetInstance; destroy(instanceId: string): boolean; getInstances(): WidgetInstance[]; destroyAll(): void; identify(identity: UserIdentity): void; logout(): void; } // Declaração Global declare global { interface Window { PumaHelp: PumaHelpAPI; } // Registro do Custom Element (DOM) interface HTMLElementTagNameMap { 'puma-help-widget': HTMLElement & { open(): void; close(): void; toggle(): void; hide(): void; show(): void; destroy(): void; }; } } // Declaração JSX para React/Next.js (React 18+) declare module 'react' { namespace JSX { interface IntrinsicElements { 'puma-help-widget': PumaWidgetAttributes; } } } export {}; ``` ### Uso em TypeScript ```typescript // Inicializar widget com tipagem const widget: WidgetInstance = window.PumaHelp.init({ appName: 'minha-empresa', apiKey: 'rk_live_xxxxx', theme: 'dark', language: 'pt-BR', }); // Identificar usuário autenticado window.PumaHelp.identify({ accessToken: user.token, name: user.name, }); // Escutar eventos com tipos widget.on('ticket-created', (event: CustomEvent<{ ticketId: string }>) => { console.log('Ticket criado:', event.detail.ticketId); }); // Controlar widget widget.open(); widget.close(); widget.toggle(); ``` ### Uso em React ```tsx import { useEffect, useRef } from 'react'; // Tipo para a referência do widget type WidgetRef = WidgetInstance | null; function App() { const widgetRef = useRef(null); useEffect(() => { // Inicializar widget widgetRef.current = window.PumaHelp.init({ appName: 'minha-empresa', apiKey: 'rk_live_xxxxx', }); // Cleanup return () => { widgetRef.current?.destroy(); }; }, []); // Identificar usuário quando logar const handleLogin = (user: { token: string; name: string }) => { window.PumaHelp.identify({ accessToken: user.token, name: user.name, }); }; return ; } ``` --- ## 🎨 Customização de CSS com `::part()` O widget utiliza **Shadow DOM** para isolamento completo de estilos, mas expõe **CSS Parts** específicos que permitem customização visual controlada usando o pseudo-elemento `::part()`. ### Parts Disponíveis | Part Name | Elemento | Descrição | |-----------|----------|-----------| | `puma-widget-wrapper` | Container Principal | Wrapper externo do widget inteiro | | `puma-widget-content` | Container do Conteúdo | Container da janela do chat (quando aberto) | | `puma-widget-trigger` | Botão de Acionamento | Botão flutuante que abre/fecha o widget | | `puma-widget-tickets-list` | Lista de Tickets | Container da lista de conversas | | `puma-widget-first-ticket-form` | Formulário Inicial | Formulário de criação de novo ticket | | `puma-widget-ticket-chat` | Chat de Ticket | Visualização de conversa ativa | | `puma-widget-message-item` | Item de Mensagem | Container de cada mensagem individual (agente ou usuário) | | `puma-widget-message-item-agent` | Mensagem do Agente | Mensagem enviada por um agente de suporte | | `puma-widget-message-item-user` | Mensagem do Usuário | Mensagem enviada pelo usuário final | ### Como Usar Use o seletor `::part()` no seu CSS para customizar os elementos expostos do widget: ```css /* Customizar o botão do widget */ puma-help-widget::part(puma-widget-trigger) { /* Seus estilos customizados */ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); } /* Customizar a janela do chat */ puma-help-widget::part(puma-widget-content) { border-radius: 12px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); } /* Customizar o wrapper principal */ puma-help-widget::part(puma-widget-wrapper) { /* Posicionamento, z-index, etc */ z-index: 99999; } ``` ### Exemplos Práticos **Exemplo 1: Botão com Estilo Personalizado** ```css puma-help-widget::part(puma-widget-trigger) { /* Adicionar uma borda */ border: 3px solid #fff; /* Sombra customizada */ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); /* Aumentar tamanho */ transform: scale(1.1); } /* Hover state */ puma-help-widget::part(puma-widget-trigger):hover { transform: scale(1.2); } ``` **Exemplo 2: Janela de Chat Customizada** ```css puma-help-widget::part(puma-widget-content) { /* Bordas arredondadas diferentes */ border-radius: 20px 20px 0 0; /* Sombra mais pronunciada */ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); /* Altura customizada */ max-height: 600px; } ``` **Exemplo 3: Lista de Tickets com Fundo Customizado** ```css puma-help-widget::part(puma-widget-tickets-list) { /* Padrão de fundo */ background-image: linear-gradient( 45deg, rgba(255, 255, 255, 0.05) 25%, transparent 25% ); } ``` **Exemplo 4: Customizar Mensagens Individuais** ```css /* Estilizar todas as mensagens */ puma-help-widget::part(puma-widget-message-item) { /* Adicionar animação de entrada */ animation: slideIn 0.3s ease-out; } /* Estilizar apenas mensagens do agente */ puma-help-widget::part(puma-widget-message-item-agent) { /* Espaçamento customizado */ margin-bottom: 12px; /* Efeito de destaque */ position: relative; } /* Estilizar apenas mensagens do usuário */ puma-help-widget::part(puma-widget-message-item-user) { /* Espaçamento customizado */ margin-bottom: 12px; } @keyframes slideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } ``` ### Customização de Tema via CSS Variables Você pode customizar **todas as cores e variáveis** do widget usando `::part()` no wrapper principal. #### Formato HSL (Recomendado) O widget utiliza variáveis CSS diretas. Para garantir compatibilidade correta, forneça a cor completa usando `hsl()`: ```css puma-help-widget::part(puma-widget-wrapper) { --primary: hsl(220 90% 56%); /* Azul */ --background: hsl(0 0% 100%); /* Branco */ --foreground: hsl(222 47% 11%); /* Texto escuro */ --muted: hsl(210 40% 96%); /* Cinza claro */ --border: hsl(214 32% 91%); /* Bordas */ } ``` #### Formato HEX (Mais Simples) Você também pode usar cores em **formato HEX** diretamente: ```css puma-help-widget::part(puma-widget-wrapper) { --primary: #3b82f6; /* Azul */ --primary-foreground: #ffffff; /* Branco */ --background: #ffffff; /* Fundo branco */ --foreground: #1f2937; /* Texto escuro */ --muted: #f3f4f6; /* Cinza claro */ --muted-foreground: #6b7280; /* Texto secundário */ --secondary: #e5e7eb; /* Bubbles do agente */ --secondary-foreground: #1f2937; /* Texto nas bubbles */ --border: #e5e7eb; /* Bordas */ --input: #e5e7eb; /* Fundo de inputs */ --ring: #3b82f6; /* Cor do foco */ --destructive: #ef4444; /* Erros */ } ``` ### CSS Variables Disponíveis | Variável | Descrição | Exemplo HEX | |----------|-----------|-------------| | `--primary` | Cor principal (botões, destaques) | `#3b82f6` | | `--primary-foreground` | Texto sobre cor primária | `#ffffff` | | `--background` | Fundo do widget | `#ffffff` | | `--foreground` | Cor do texto principal | `#1f2937` | | `--muted` | Fundo de elementos secundários | `#f3f4f6` | | `--muted-foreground` | Texto secundário | `#6b7280` | | `--secondary` | Bubbles do agente | `#e5e7eb` | | `--secondary-foreground` | Texto nas bubbles do agente | `#1f2937` | | `--border` | Cor das bordas | `#e5e7eb` | | `--input` | Fundo de inputs | `#e5e7eb` | | `--ring` | Cor do foco | `#3b82f6` | | `--destructive` | Erros e alertas | `#ef4444` | | `--radius` | Raio de bordas | `0.5rem` | | `--badge-bg` | Fundo do badge de notificação | `#ef4444` | | `--badge-fg` | Texto do badge de notificação | `#ffffff` | ### Exemplos de Temas **Tema Azul Corporativo (HEX):** ```css puma-help-widget::part(puma-widget-wrapper) { --primary: #2563eb; --primary-foreground: #ffffff; --background: #ffffff; --foreground: #1e293b; --muted: #f1f5f9; --muted-foreground: #64748b; --border: #e2e8f0; } ``` **Tema Roxo Escuro (HEX):** ```css puma-help-widget::part(puma-widget-wrapper) { --primary: #8b5cf6; --primary-foreground: #ffffff; --background: #1e1b2e; --foreground: #f5f3ff; --muted: #2e2942; --muted-foreground: #a5a0c2; --secondary: #362f50; --secondary-foreground: #f5f3ff; --border: #362f50; } ``` **Tema Verde Natureza (HEX):** ```css puma-help-widget::part(puma-widget-wrapper) { --primary: #22c55e; --primary-foreground: #ffffff; --background: #0f1a14; --foreground: #ecfdf5; --muted: #1a2e23; --muted-foreground: #86efac; --border: #1a2e23; } ``` **Exemplo: Personalizar Apenas o Badge:** ```css puma-help-widget::part(puma-widget-wrapper) { --badge-bg: #8b5cf6; /* Roxo */ --badge-fg: #ffffff; /* Texto branco */ } ``` ### Estilos Diferentes para Light e Dark Mode Use seletores de atributo para aplicar estilos diferentes baseado no tema: ```css /* Estilos para Light Mode */ puma-help-widget[theme="light"]::part(puma-widget-wrapper) { --primary: #2563eb; --background: #ffffff; --foreground: #1f2937; --muted: #f3f4f6; --border: #e5e7eb; } puma-help-widget[theme="light"]::part(puma-widget-content) { box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); } /* Estilos para Dark Mode */ puma-help-widget[theme="dark"]::part(puma-widget-wrapper) { --primary: #3b82f6; --background: #111827; --foreground: #f9fafb; --muted: #1f2937; --border: #374151; } puma-help-widget[theme="dark"]::part(puma-widget-content) { box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); border: 1px solid rgba(255, 255, 255, 0.1); } puma-help-widget[theme="dark"]::part(puma-widget-trigger) { border: 2px solid rgba(255, 255, 255, 0.2); } ``` ### Limitações :::important Os `::part()` permitem estilizar os elementos expostos e customizar CSS Variables. Porém, **não é possível acessar** elementos internos não expostos. ::: **O que você PODE fazer:** - ✅ Modificar layout, posicionamento, dimensões - ✅ Adicionar bordas, sombras, fundos - ✅ Aplicar transformações - ✅ Ajustar opacidade, visibilidade - ✅ **Customizar todas as cores do tema via CSS Variables** **O que você NÃO PODE fazer:** - ❌ Estilizar elementos filhos dentro dos parts - ❌ Acessar elementos internos não expostos - ❌ Sobrescrever completamente a estrutura do widget ### Compatibilidade O `::part()` é suportado em todos os navegadores modernos: - ✅ Chrome/Edge 73+ - ✅ Firefox 72+ - ✅ Safari 13.1+ - ✅ Opera 60+ Para navegadores mais antigos, os estilos customizados são simplesmente ignorados (graceful degradation). ### Combinando com Props de Configuração Você pode combinar `::part()` com as propriedades de configuração para máxima flexibilidade: ```html ``` ```css /* CSS */ puma-help-widget::part(puma-widget-trigger) { /* Customizações adicionais além da cor primária */ border: 2px solid rgba(255, 255, 255, 0.2); backdrop-filter: blur(10px); } ``` --- ## 📋 Métodos da API Todos os métodos disponíveis na instância do widget: ### Métodos de Controle ```javascript const widget = window.PumaHelp.init({ /* config */ }); // Abrir/fechar widget widget.open(); // Abre a interface do chat widget.close(); // Fecha a interface do chat widget.toggle(); // Alterna entre aberto/fechado // Mostrar/ocultar botão do widget widget.hide(); // Oculta o botão do widget widget.show(); // Mostra o botão do widget // Verificar estado console.log(widget.isOpen); // boolean console.log(widget.isHidden); // boolean ``` ### Métodos de Autenticação ```javascript // Identificar usuário autenticado widget.identify({ accessToken: 'seu-jwt-token', // Obrigatório name: 'João Silva' // Opcional (recomendado) }); // Logout (retorna ao modo visitante) widget.logout(); ``` ### Métodos de Ciclo de Vida ```javascript // Destruir instância única widget.destroy(); // Destruir todos os widgets na página window.PumaHelp.destroyAll(); ``` ### Listener de Eventos ```javascript // Ouvir eventos const unsubscribe = widget.on('ticket-created', (event) => { console.log('ID do Ticket:', event.detail.ticketId); }); // Parar de ouvir unsubscribe(); ``` --- ## 🎧 Referência de Eventos Inscreva-se em eventos do widget para rastrear interações do usuário: ### Eventos de Ciclo de Vida | Evento | Payload | Descrição | |--------|---------|-----------| | `ready` | - | Widget inicializado e pronto | | `error` | `{ error, message }` | Ocorreu um erro | | `destroy` | - | Widget destruído | ### Eventos de Estado | Evento | Payload | Descrição | |--------|---------|-----------| | `open` | - | Widget aberto | | `close` | - | Widget fechado | | `hide` | - | Botão do widget oculto | | `show` | - | Botão do widget mostrado | | `toggle` | - | Widget aberto/fechado | ### Eventos de Ação do Usuário | Evento | Payload | Descrição | |--------|---------|-----------| | `ticket-created` | `{ payload }` | Novo ticket criado (objeto completo do ticket) | | `ticket-updated` | `{ payload: { public_id, comment: { body, uploads } } }` | Ticket atualizado (mensagem enviada) | **Exemplos de payload:** ```javascript widget.on('ticket-created', (event) => { const { payload } = event.detail; // Objeto completo do ticket da API console.log(payload.public_id); console.log(payload.subject); console.log(payload.status); console.log(payload.created_at); }); widget.on('ticket-updated', (event) => { const { payload } = event.detail; console.log(payload.public_id); console.log(payload.comment.body); console.log(payload.comment.uploads); // Array de IDs de arquivos }); ``` ### Eventos de Autenticação | Evento | Payload | Descrição | |--------|---------|-----------| | `auth-error` | `{ code, message, canRetry }` | Erro de autenticação (ex: token expirado) | ### Eventos de Interceptação de Payload (Avançado) | Evento | Payload | Descrição | |--------|---------|-----------| | `before-ticket-create` | `{ payload }` | Interceptar/modificar dados do ticket antes da criação | | `before-ticket-update` | `{ payload }` | Interceptar/modificar dados antes de atualizar ticket | ### Exemplos de Eventos ```javascript const widget = window.PumaHelp.init({ /* config */ }); // Rastrear quando usuários criam tickets widget.on('ticket-created', (event) => { const { ticketId, subject } = event.detail; analytics.track('Ticket de Suporte Criado', { ticketId, subject }); }); // Lidar com expiração de token widget.on('auth-error', async (event) => { if (event.detail.canRetry) { const newToken = await refreshUserToken(); widget.identify({ accessToken: newToken }); } }); // Rastrear uso do widget widget.on('open', () => { analytics.track('Widget Aberto'); }); ``` --- ## 🎨 Interceptores de Payload (Avançado) Modifique dados do ticket/mensagem antes de enviar ao servidor: ### Antes de Criar Ticket ```javascript widget.on('before-ticket-create', (event) => { // Adicionar tags personalizadas event.detail.payload.tags = ['vip', user.plan]; // Modificar assunto const prefix = user.isPremium ? '[VIP]' : ''; event.detail.payload.subject = `${prefix} ${event.detail.payload.subject}`; }); ``` **Estrutura do payload:** ```typescript { subject: string; comment: { body: string }; via: { channel: 'widget' }; tags?: string[]; } ``` ### Antes de Atualizar Ticket (Enviar Mensagem) ```javascript widget.on('before-ticket-update', (event) => { // Adicionar informações de rastreamento const original = event.detail.payload.comment.body; event.detail.payload.comment.body = `${original}\n\n`; }); ``` **Estrutura do payload:** ```typescript { comment: { body: string }; } ``` **Casos de uso:** - Adicionar tags dinâmicas baseadas no contexto do usuário - Incluir metadados para analytics - Rastrear contexto da conversa --- ## 🔐 Autenticação de Usuário ### Padrão Recomendado **Separe configuração da autenticação:** ```javascript // 1. Inicializar widget no carregamento da página (config estática) window.PumaHelp.init({ appName: 'acme', apiKey: 'key-abc123' }); // 2. Identificar usuário quando fizer login (auth dinâmica) function onUserLogin(user) { window.PumaHelp.identify({ accessToken: user.jwtToken, name: user.fullName // Recomendado para fallback de visitante }); } // 3. Logout quando o usuário sair function onUserLogout() { window.PumaHelp.logout(); // Widget continua no modo visitante } ``` ### Tratamento de Expiração de Token Trate erros de autenticação quando o token expirar: ```javascript const widget = window.PumaHelp.init({ /* config */ }); widget.on('auth-error', async (event) => { const { code, message, canRetry } = event.detail; if (canRetry) { try { // Renovar token via seu backend const newToken = await fetch('/api/auth/refresh').then(r => r.json()); // Atualizar widget com novo token widget.identify({ accessToken: newToken.accessToken, name: currentUser.name }); } catch (error) { // Não conseguiu renovar - redirecionar para login window.location.href = '/login?expired=true'; } } }); ``` --- ## 🌐 Integração com Frameworks ### React ```tsx import { useEffect, useRef } from 'react'; function App() { const widgetRef = useRef(null); useEffect(() => { const widget = widgetRef.current; if (!widget) return; // Ouvir eventos const unsubscribe = widget.addEventListener('ready', () => { console.log('Widget pronto!'); }); return () => widget.removeEventListener('ready', unsubscribe); }, []); return ( ); } ``` ### Vue 3 ```vue ``` ### Angular ```typescript import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core'; @Component({ selector: 'app-root', template: ` ` }) export class AppComponent implements AfterViewInit { @ViewChild('widget') widget!: ElementRef; ngAfterViewInit() { const el = this.widget.nativeElement; el.addEventListener('ready', () => { console.log('Widget pronto!'); }); } } ``` --- ## 🎯 Casos de Uso Comuns ### Abrir widget programaticamente ```javascript // De um botão document.getElementById('help-btn').addEventListener('click', () => { const widget = document.querySelector('puma-help-widget'); widget.open(); }); // Ou com a API JavaScript const widget = window.PumaHelp.init({ /* config */ }); widget.open(); ``` ### Rastrear analytics ```javascript const widget = window.PumaHelp.init({ /* config */ }); widget.on('ticket-created', (e) => { gtag('event', 'ticket_suporte_criado', { ticket_id: e.detail.id }); }); widget.on('open', () => { gtag('event', 'widget_aberto'); }); ``` ### Aplicações multi-página (SPAs) ```javascript // Na mudança de rota, destruir e recriar se necessário function onRouteChange(route) { if (route === '/contato') { // Criar widget para a página de contato window.PumaHelp.init({ /* config */ }); } else { // Remover de outras páginas window.PumaHelp.destroyAll(); } } ``` --- ## 🐛 Debug Habilite o modo debug para ver logs do widget: ```javascript window.PumaHelp.init({ appName: 'acme', apiKey: 'key-abc123', debug: true // Loga todos os eventos e mudanças de estado }); ``` Ou use o atributo HTML: ```html ``` --- ## 🔊 Notificações Sonoras O widget reproduz notificações de áudio para interações do usuário: **Tipos de Som:** - **Som de Envio**: Reproduzido quando você envia uma mensagem ou cria um ticket - **Som de Recebimento**: Reproduzido quando um agente responde ao seu ticket **Configuração:** ```javascript // Habilitar sons (padrão) window.PumaHelp.init({ appName: 'acme', apiKey: 'key-abc123', soundEnabled: true }); // Desabilitar sons window.PumaHelp.init({ appName: 'acme', apiKey: 'key-abc123', soundEnabled: false }); ``` **Atributo HTML:** ```html ``` **Notas:** - Sons respeitam as políticas de autoplay do navegador - Volume é otimizado automaticamente (envio: 50%, recebimento: 30%) - Sons são carregados do CDN e funcionam tanto em desenvolvimento quanto em produção --- ## 📎 Anexos de Arquivos O widget suporta anexos de arquivos tanto no formulário "Nova Conversa" quanto na visualização do chat ativo. **Recursos:** - **Botão de Upload**: Usuários podem clicar no ícone de clipe de papel para selecionar arquivos de seu dispositivo. - **Copiar/Colar**: Usuários podem colar imagens diretamente da área de transferência (Ctrl+V) na área de entrada de mensagem. - **Múltiplos Arquivos**: Múltiplos arquivos podem ser anexados e enviados em uma única mensagem. - **Pré-visualização**: Arquivos anexados são mostrados em uma lista de pré-visualização onde podem ser removidos antes do envio. --- ## ❓ Perguntas Frequentes **P: Posso ter múltiplos widgets em uma página?** R: Sim! Cada chamada de `init()` ou elemento `` cria uma instância separada. :::important **Nota sobre Autenticação**: Embora visualmente independentes, todas as instâncias do widget na mesma página compartilham a mesma **sessão de usuário** (autenticação). Se um usuário fizer login em um widget, ele estará logado em todos. ::: **P: Como personalizo a aparência do widget?** R: Você tem duas opções: 1. **Configuração básica**: Use as props `theme`, `color` e `icon` para customizações rápidas. 2. **Customização avançada**: Use CSS Parts com `::part()` para estilizar elementos específicos (veja seção "Customização de CSS com `::part()`"). **P: Posso modificar o CSS do widget?** R: Sim! O widget expõe CSS Parts que podem ser estilizados usando `::part()`. Você pode customizar elementos como o botão (`puma-widget-trigger`), janela do chat (`puma-widget-content`), lista de tickets, e mais. Veja a documentação completa na seção "Customização de CSS com `::part()`". **P: O que acontece quando um usuário faz logout?** R: Chame `widget.logout()` para limpar a autenticação. O widget continua funcionando no modo visitante. **P: Como lidar com expiração de token?** R: Ouça o evento `auth-error` e renove o token via `widget.identify()`. **P: Posso modificar dados do ticket antes de enviar?** R: Sim! Use os eventos `before-ticket-create` ou `before-ticket-update`. --- **Versão:** 2.1.0 **Última Atualização:** Janeiro 2026 --- ## Bot do Discord # Bot do Discord Conecte seu servidor do Discord ao PumaHelp: membros abrem tickets sem sair do Discord, cada ticket vira uma thread privada, e as respostas da sua equipe — enviadas pelo dashboard — chegam direto na conversa. ## 🚀 Antes de começar Você vai precisar de: - **Administrador** no servidor do Discord onde o bot será instalado; - acesso de **Owner ou Admin** na sua organização no PumaHelp (para criar a API Key). A configuração completa leva cerca de 10 minutos. ## 1. Convide o bot para o servidor Use o link oficial de convite: **[discord.com/oauth2/authorize?client_id=1407072685565153350](https://discord.com/oauth2/authorize?client_id=1407072685565153350)** O Discord vai pedir sua confirmação das permissões que o bot usa: ver o canal escolhido e enviar mensagens nele, criar a thread privada de cada ticket e escrever dentro dela, publicar as mensagens formatadas (o card de abertura e as respostas da sua equipe), mencionar o cargo de suporte para trazê-lo à thread, arquivar e trancar a thread quando o ticket é fechado, e apagar mensagens com menções indevidas dentro dos tickets. ## 2. Crie a API Key no Dashboard A API Key é como o bot se autentica na sua organização — e criar a do bot leva dois cliques: 1. No dashboard, acesse **Configurações → API Keys** e clique em **Criar Chave**. 2. Em *"Para que esta chave será usada?"*, clique em **Bot do Discord** — as permissões necessárias são selecionadas automaticamente e o nome é preenchido. 3. Clique em **Gerar Chave**. 4. **Copie a chave agora.** Por segurança, ela é exibida uma única vez. Se perder, basta gerar outra. ## 3. Configure o bot com `/config` No seu servidor do Discord, digite **`/config`** (exige permissão de Administrador). Abre um painel — visível só para você — mostrando o estado de cada parte e o que ainda falta: | Seção | O que configurar | |---|---| | **Discord** | O canal onde as threads de ticket serão criadas e, opcionalmente, o cargo de suporte | | **PumaHelp** | O subdomínio da sua organização (ex.: `acme`) e a **API Key** do passo 2 | | **Categorias** | Os tipos de ticket que aparecem no menu de abertura | | **Painel** | Onde publicar a mensagem de abertura de tickets | :::tip Seguro por padrão Tudo é preenchido em janelas próprias (modais) — nenhuma credencial é digitada no chat nem fica visível para outros membros. Ao salvar, o bot valida a API Key na hora: se algo estiver errado, você fica sabendo imediatamente. E ao editar depois, campos de credencial deixados em branco mantêm o valor já salvo. ::: :::note E o webhook? Ao salvar a API Key, o bot **configura sozinho o webhook de retorno** na sua organização — é por ele que as respostas dos agentes chegam ao Discord. Você verá a confirmação na própria resposta do `/config` ("webhook criado" ou "reaproveitado"). Se a sua Key não tiver as permissões de webhook, veja a [configuração manual](#configurar-o-webhook-manualmente) no fim da página. ::: ### Sobre o cargo de suporte - **Com cargo**: ele é mencionado na abertura de cada ticket — é essa menção que adiciona a equipe à thread privada e a notifica. - **Sem cargo**: o ticket abre sem menção; apenas o solicitante (e quem tem a permissão *Gerenciar Threads*) enxerga a thread no Discord. A equipe acompanha e responde pelo dashboard normalmente. ## 4. Crie as categorias e publique o painel 1. Em `/config` → **Categorias**, crie ao menos uma (ex.: "Suporte Técnico", "Financeiro"). Cada categoria define o assunto e as tags do ticket criado no PumaHelp, e pode ser associada a um dos seus grupos. 2. De volta ao painel do `/config`, clique em **Publicar painel** e escolha o canal. A mensagem com o menu de abertura aparece lá, pronta para os membros usarem. ## 💬 No dia a dia - **Abrir ticket**: o membro escolhe a categoria no painel, descreve o problema e pode anexar até 10 arquivos (50 MB cada). Uma thread privada é criada na hora, e o ticket aparece no dashboard. - **Responder**: pelo botão **Responder** na thread — com texto, anexos ou ambos. - **Respostas da equipe**: enviadas pelo dashboard, chegam automaticamente à thread do Discord. - **Resolução**: quando o ticket é resolvido, o membro pode responder para reabri-lo; sem resposta, a thread é arquivada automaticamente. ## 🔑 Permissões da API Key (referência) O preset **Bot do Discord** seleciona exatamente estas: | Permissão | Para quê | |---|---| | `ticket:create` | abrir tickets a partir do Discord | | `ticket:update` | enviar as respostas e mudar o status do ticket | | `file:upload` | enviar os anexos postados no Discord | | `group:read` | listar seus grupos ao configurar categorias | | `organization:read` | validar a Key e identificar sua organização | | `webhook:read` | encontrar o webhook de retorno, se já existir | | `webhook:create` | criar o webhook de retorno automaticamente | ## ❓ Problemas comuns **O comando `/config` não aparece.** Recarregue o Discord (`Ctrl+R` no desktop, `Cmd+R` no macOS). E lembre: apenas administradores do servidor enxergam o comando. **"Não consegui validar a API Key" ao salvar.** Confira o subdomínio (só o nome, sem `.pumahelp.com`) e se a Key foi criada com o preset **Bot do Discord**. Se você montou a Key manualmente, ela precisa incluir a permissão `organization:read`. **As respostas dos agentes não chegam ao Discord.** Abra o `/config` → **PumaHelp** e salve novamente — o bot re-verifica e reconfigura o webhook de retorno. Se a mensagem avisar que a Key não tem as permissões de webhook, crie uma nova Key com o preset **Bot do Discord** (que já as inclui) ou siga a [configuração manual](#configurar-o-webhook-manualmente). **Eu usava `/configticket` e `/ticketoptions`.** Foram substituídos — ao usá-los, o bot responde com um botão que abre o `/config`, onde toda a configuração é feita agora. ## Configurar o webhook manualmente Só é necessário se a sua API Key **não** tiver as permissões `webhook:read` e `webhook:create` (Keys criadas antes do preset atual, por exemplo) e você preferir não criar outra: 1. No dashboard, acesse **Configurações → Webhooks** e crie um novo webhook. 2. **URL**: `https://bot.pumahelp.com/webhook` 3. **Evento**: marque `ticket.updated` — é o único que o bot usa. 4. Salve, **copie a Webhook Key** e informe-a no campo *Webhook Secret* do `/config` → **PumaHelp**. O valor digitado manualmente sempre tem precedência sobre o automático.