# Garantify — Documentação da API > API REST para emissão e gestão de certificados de garantia digital. Emita certificados, gerencie clientes, produtos e marcas, receba reclamações/trocas e notificações via webhook. Base URL de produção: `https://api-enterprise.garantify.com.br` Este arquivo é a referência completa da API pública em formato Markdown, otimizado para LLMs e assistentes de código. Documentação navegável: https://developers.garantify.com.br | OpenAPI Spec: https://api-enterprise.garantify.com.br/swagger/v1/swagger.json --- ## Índice - [Autenticação](#autenticação) - [Ambientes: live e sandbox](#ambientes-live-e-sandbox) - [Header de vendedor](#header-de-vendedor-x-garantify-vendedor-id) - [Idempotência](#idempotência) - [Rate limits](#rate-limits) - [Paginação, busca e ordenação](#paginação-busca-e-ordenação) - [Formato de erros](#formato-de-erros) - [Endpoints: Certificados](#endpoints-certificados) - [Endpoints: Clientes](#endpoints-clientes) - [Endpoints: Produtos](#endpoints-produtos) - [Endpoints: Marcas](#endpoints-marcas) - [Endpoints: Reclamações](#endpoints-reclamações) - [Endpoints: Vendedores](#endpoints-vendedores) - [Endpoints: Upload](#endpoints-upload) - [Endpoints: Reclamações V2 (pipeline customizável)](#endpoints-reclamações-v2-pipeline-customizável) - [Wallet do consumidor](#wallet-do-consumidor) - [Webhooks](#webhooks) - [Catálogo de códigos de erro](#catálogo-de-códigos-de-erro) - [Limites por plano](#limites-por-plano) - [Rotas obsoletas](#rotas-obsoletas) - [Armadilhas conhecidas](#armadilhas-conhecidas) --- ## Autenticação Toda requisição a `/api/v1/*` e `/api/v2/*` exige o header: ``` X-Garantify-API-Key: grtf_live_xxxxxxxxxxxx ``` Gerencie suas chaves no painel Garantify Enterprise. A chave é validada por hash SHA-256; nunca é armazenada em texto puro. Respostas de falha (todas `401 UNAUTHORIZED`): | Situação | Mensagem | |---|---| | Header ausente | `API key é obrigatória. Inclua o header X-Garantify-API-Key.` | | Chave desconhecida | `API key inválida.` | | Chave revogada | `API key revogada.` | | Chave expirada | `API key expirada.` | | Conta bloqueada | `Acesso revogado. Entre em contato com o administrador da conta.` | --- ## Ambientes: live e sandbox O ambiente é determinado pelo **prefixo da chave**, não por URL ou header. O endereço da API é o mesmo nos dois casos. | Prefixo | Ambiente | |---|---| | `grtf_live_` | Produção (`live`) | | `grtf_test_` | Sandbox (`sandbox`) | Todos os dados (certificados, chaves de idempotência, webhooks, pipelines) são particionados por ambiente. Um certificado emitido em sandbox nunca aparece em live e vice-versa. E-mails não são enviados em sandbox. Sandbox é um recurso de plano: disponível a partir do **Growth**. Trial e Starter não têm sandbox. --- ## Header de vendedor: `X-Garantify-Vendedor-Id` Header **opcional** que atribui a ação a um vendedor específico (quem emitiu o certificado, quem atendeu o cliente). Aceito em qualquer endpoint `/api/v1/*`. ``` X-Garantify-Vendedor-Id: d4e5f6a7-b8c9-4d0e-a1b2-c3d4e5f6a7b8 ``` O valor é o `id` retornado por `POST /api/v1/vendedores`. Sem o header, a ação fica sem emissor/atendente vinculado (`emissorId` nulo no certificado). **Configure uma vez no seu HTTP client** para que seja enviado automaticamente em todas as chamadas subsequentes. ### Nome legado `X-Garantify-Operador-Id` é o nome antigo do mesmo header. Continua aceito **permanentemente, sem prazo de remoção**. Se você integrou antes de setembro/2026, nada quebra. **Envie apenas um dos dois.** Se ambos forem enviados com valores **diferentes**, a requisição falha: ```json { "error": { "code": "HEADER_CONFLICT", "message": "Envie apenas X-Garantify-Vendedor-Id." } } ``` `400 Bad Request`. Se ambos vierem com o mesmo valor, passa normalmente. Quando só um está presente, `X-Garantify-Vendedor-Id` tem precedência. ### Erros do vendedor | Código | HTTP | Quando | |---|---|---| | `OPERADOR_NOT_FOUND` | 403 | Vendedor não existe no seu tenant, ou foi excluído | | `OPERADOR_BLOCKED` | 403 | Vendedor bloqueado — `Vendedor bloqueado. Entre em contato com o administrador.` | Os códigos mantêm o prefixo `OPERADOR_` de propósito: renomeá-los quebraria integrações que fazem `switch` neles. --- ## Idempotência Operações de escrita exigem o header `Idempotency-Key`, garantindo que retentativas por falha de rede não dupliquem recursos. ``` Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 ``` - Formato: 1–255 caracteres ASCII imprimíveis (recomendado: UUID v4). - TTL: **24 horas**. - Chave de deduplicação: `(sua conta, ambiente, chave)`. - O corpo da requisição é hasheado (SHA-256) e comparado. ### Obrigatório em `POST`/`PUT`/`PATCH`/`DELETE` de: **Certificados**, **Clientes**, **Marcas**, **Produtos**, **Reclamações**, **Vendedores**, e nos dois `POST` de **Reclamações V2**. ### Opcional em - `POST /api/v1/reclamacoes/{id}/responder` - `POST /api/v1/upload/imagem` (não usa idempotência) - `GET`, `HEAD`, `OPTIONS` sempre ignoram o header. ### Comportamentos | Situação | HTTP | Código | |---|---|---| | Header ausente onde é obrigatório | 400 | `idempotency_key_required` | | Formato inválido | 400 | `idempotency_key_invalid` | | Mesma chave, payload diferente | 422 | `idempotency_key_payload_mismatch` | | Mesma chave, requisição ainda em processamento | 409 | `idempotency_key_in_progress` | | Mesma chave, requisição concluída | *replay* | Retorna status, corpo e headers originais | ### Resultado indefinido (importante) Se a operação anterior retornou 5xx ou teve resposta truncada, o replay devolve **502** com: ```json { "error": { "code": "idempotency_request_uncertain", "message": "Operação anterior teve status indefinido (resposta truncada ou erro 5xx). Pode ter sido processada ou não. NÃO retente com a mesma Idempotency-Key — gere uma nova chave somente após confirmar o estado real via consulta GET." } } ``` Consulte o estado real via `GET` antes de gerar uma chave nova. --- ## Rate limits ### `/api/v1/*` — por segundo, conforme o plano | Plano | Requests/segundo | |---|---| | Trial | 10 | | Starter | 10 | | Growth | 50 | | Scale | 300 | | Custom | 3000 | Headers presentes em toda resposta: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (unix seconds). Ao exceder: `429` + header `Retry-After`. ```json { "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Limite de 50 requests por segundo excedido. Tente novamente em 1 segundo(s).", "retryAfter": 1 } } ``` ### `/api/v2/*` — 100 requests/minuto Aplicado apenas em `POST /api/v2/reclamacoes` e `POST /api/v2/reclamacoes/{id}/tramitar`. `GET` não é limitado por esse mecanismo. ```json { "error": { "code": "RATE_LIMITED", "message": "Rate limit excedido. Tente novamente em alguns segundos." } } ``` `429` + `Retry-After` com os segundos restantes do bucket. ### Outros limites - `POST /api/v1/clientes/{id}/wallet/regenerar` — 10 regenerações por 24h por cliente. --- ## Paginação, busca e ordenação Todos os endpoints de listagem usam paginação **por página (offset)**. Não há cursores. | Parâmetro | Default | Limites | |---|---|---| | `page` | 1 | `>= 1` | | `pageSize` | 20 (**50** em `/vendedores`) | 1–200 | ### Envelope V1 ```json { "total": 137, "page": 1, "pageSize": 20, "totalPages": 7, "items": [ ... ] } ``` ### Envelope V2 (sem `totalPages`) ```json { "items": [ ... ], "total": 137, "page": 1, "pageSize": 20 } ``` ### Ordenação — parâmetro `sort` Formato `campo` ou `campo:desc`. Default: `criadoEm desc`. Campos desconhecidos caem silenciosamente no default. | Endpoint | Campos aceitos | |---|---| | `GET /certificados` | `codigoCertificado`, `dataEmissao`, `dataExpiracao`, `status`, `criadoEm` | | `GET /clientes` | `nome`, `email`, `criadoEm` | | `GET /produtos` | `nome`, `codigoProduto`, `preco`, `criadoEm` | | `GET /marcas` | `nome`, `criadoEm` | ### Busca — parâmetro `search` Case-insensitive, busca parcial (`%termo%`). | Endpoint | Campos pesquisados | |---|---| | `/certificados` | `codigoCertificado`, `clienteNome`, `clienteEmail`, `produtoNome`, `produtoCodigo` | | `/clientes` | `nome`, `email`, `telefone` | | `/produtos` | `nome`, `codigoProduto` | | `/marcas` | `nome` | ### Filtro por metadata `GET /certificados`, `/clientes`, `/produtos` e `/reclamacoes` aceitam filtros exatos por metadata: ``` GET /api/v1/certificados?metadata.pedido=12345&metadata.lojaId=loja-sp-01 ``` --- ## Formato de erros Formato padrão em toda a API: ```json { "error": { "code": "VALIDATION_ERROR", "message": "Descrição legível do erro" } } ``` Use `error.code` para tratamento programático e `error.message` para exibir ao usuário. **Duas exceções ao formato** (documentadas nas seções correspondentes): 1. Erros de validação de paginação em V1 retornam `{"error": "texto"}` (string, não objeto). 2. `403` de `POST /api/v2/reclamacoes/{id}/tramitar` retorna `error` como string mais campos irmãos. --- ## Endpoints: Certificados Base: `/api/v1/certificados` · `Idempotency-Key` obrigatório em escritas. ### `POST /api/v1/certificados` — Emitir certificado **Assíncrono.** Retorna `202 Accepted` com `taskId`; a emissão real (validação, persistência, envio de e-mail) ocorre em segundo plano. Assine o webhook `certificate.created` para a confirmação, ou `task.failed` para falhas. | Campo | Tipo | Obrigatório | Observações | |---|---|---|---| | `produtoId` | string | **sim** | | | `clienteId` | string | não | Se ausente, `consumidorNome` passa a ser obrigatório | | `consumidorNome` | string | condicional | Obrigatório quando `clienteId` não é informado | | `consumidorEmail` | string | não | Deve ser válido se presente; sem e-mail o certificado é criado mas não enviado | | `consumidorTelefone` | string | não | | | `garantiaMeses` | number | não | `> 0`; default = valor cadastrado no produto | | `observacao` | string | não | | | `vendedorNome` / `vendedorEmail` / `vendedorTelefone` | string | não | | | `metadata` | object | não | Pares chave-valor (string) | | `dataEmissao` | string ISO 8601 | não | Default = agora (UTC). Não pode ser futura nem anterior a 10 anos | ```bash curl -X POST "https://api-enterprise.garantify.com.br/api/v1/certificados" \ -H "X-Garantify-API-Key: SUA_API_KEY" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \ -H "X-Garantify-Vendedor-Id: d4e5f6a7-b8c9-4d0e-a1b2-c3d4e5f6a7b8" \ -H "Content-Type: application/json" \ -d '{ "produtoId": "d5e6f7a8-9012-3cde-f456-7890abcdef01", "clienteId": "c4d5e6f7-8901-2bcd-ef34-567890abcdef", "garantiaMeses": 12, "metadata": { "pedido": "12345" } }' ``` ```json { "taskId": "task_3fa85f645717456...", "status": "Pending", "criadoEm": "2026-09-03T12:00:00Z" } ``` Erros: `400 QUOTA_EXCEEDED` (cota do plano esgotada), `400 TRIAL_EXPIRED`, `400 VALIDATION_ERROR`, `400 CLIENTE_OBRIGATORIO` (conta com [carteira com documento](#carteira-com-documento-link-único-da-loja) e requisição sem `clienteId`; recusada na hora, sem gerar `taskId`), `401`. ### `POST /api/v1/certificados/lote` — Emitir em lote **Assíncrono** (`202 Accepted` + `taskId`). | Campo | Tipo | Obrigatório | Observações | |---|---|---|---| | `consumidorId` | string | **sim** | | | `produtos` | array | **sim** | 1–200 itens | | `produtos[].produtoId` | string | **sim** | | | `produtos[].quantidade` | number | não | Default 1; 1–100 | | `produtos[].observacao` | string | não | | | `notaFiscal` | string | não | Máx. 100 chars | | `metadata` | object | não | | | `vendedorNome` / `vendedorEmail` / `vendedorTelefone` | string | não | Aplicado a todos do lote | | `dataEmissao` | string ISO 8601 | não | Aplicado a todo o lote | A cota é verificada contra a soma das quantidades. ### `GET /api/v1/certificados` — Listar Query: `page`, `pageSize`, `status`, `search`, `operadorId`, `sort`, `metadata.*` Item retornado: `id`, `codigoCertificado`, `consumidorNome`, `consumidorEmail`, `produtoNome`, `marcaNome`, `status`, `dataEmissao`, `dataExpiracao`, `metadata`, `emissorId`, `precoProduto`, `motivoCancelamento`, `dataCancelamento`. ### `GET /api/v1/certificados/{id}` — Obter por ID ### `GET /api/v1/certificados/codigo/{codigo}` — Obter por código público ### `POST /api/v1/certificados/{id}/cancelar` — Cancelar **Assíncrono** (`202` + `taskId`). Irreversível. Webhook `certificate.cancelled` confirma. Body: `{ "motivo": "string opcional, máx 500 chars" }` ### `POST /api/v1/certificados/cancelar-lote` — Cancelar em lote **Assíncrono.** Até 200 por chamada. Aceita IDs (UUID) ou códigos. Certificados já cancelados/expirados são ignorados silenciosamente. Body: `{ "certificados": ["id-ou-codigo", ...], "motivo": "opcional" }` ### `POST /api/v1/certificados/{id}/transferir` — Transferir titularidade **Síncrono** (`200`). Body: `{ "email": "novo@titular.com", "novoConsumidorId": "opcional" }` Resposta: `certificadoId`, `destinatarioId`, `destinatarioNome`, `destinatarioTelefone`, `destinatarioEmail`, `destinatarioWalletUrl`, `destinatarioCriado` (boolean). Dispara webhook `certificate.transferred`. Na [carteira com documento](#carteira-com-documento-link-único-da-loja), `novoConsumidorId` é obrigatório (sem ele: `400 CLIENTE_OBRIGATORIO`; o `email` não é usado para localizar nem criar o destinatário) e `destinatarioWalletUrl` é o link da loja. ### `PATCH /api/v1/certificados/{id}/nota-fiscal` — Anexar nota fiscal Body: `{ "notaFiscalKey": "chave-do-arquivo" }` ### `DELETE /api/v1/certificados/{id}/nota-fiscal` — Remover nota fiscal ### `POST /api/v1/certificados/{id}/enviar-email` — Reenviar e-mail Sem body. Retorna `200` com `{ "message": "...", "enviado": true|false }`. Quando o envio de e-mails está desabilitado para a conta, retorna `200` com `enviado: false`. --- ## Endpoints: Clientes Base: `/api/v1/clientes` · `Idempotency-Key` obrigatório em escritas. ### `POST /api/v1/clientes` — Cadastrar cliente | Campo | Tipo | Obrigatório | Observações | |---|---|---|---| | `nome` | string | **sim** | | | `telefone` | string | **sim** | Mínimo 10 dígitos | | `email` | string | não | Deve ser válido se presente | | `documento` | string | não* | CPF, com ou sem máscara. *Obrigatório na [carteira com documento](#carteira-com-documento-link-único-da-loja) | | `endereco` | string | não | | | `metadata` | object | não | | Se `X-Garantify-Vendedor-Id` estiver presente, o cliente é vinculado a esse vendedor (`revendedoraId`). Dispara `client.created` e gera a wallet automaticamente (`wallet.created`). Erros de CPF: `400 DOCUMENTO_INVALIDO` (CPF inválido), `400 DOCUMENTO_OBRIGATORIO` (carteira com documento ligada e CPF ausente), `409 DOCUMENTO_DUPLICADO` (CPF já cadastrado em outro cliente da conta; vale com a modalidade ligada ou não). ### `GET /api/v1/clientes` — Listar Query: `page`, `pageSize`, `search`, `operadorId`, `sort`, `metadata.*` `search` casa nome, e-mail, telefone ou CPF (com ou sem máscara). Item: `id`, `nome`, `email`, `telefone`, `documento`, `endereco`, `metadata`, `criadoEm`, `criadoPorId`, `revendedoraId`, `revendedoraTipo`. `documento` vem sempre como 11 dígitos sem máscara, ou `null`. ### `GET /api/v1/clientes/{id}` — Obter Inclui `documento` e `walletUrl` (link da loja na carteira com documento). ### `PUT /api/v1/clientes/{id}` — Atualizar Todos os campos opcionais: `nome` (máx 200), `email`, `telefone` (máx 20), `documento`, `endereco`, `metadata`. `documento` ausente ou `null` mantém o CPF atual; `""` remove o CPF, o que só é permitido com a carteira com documento desligada (senão `400 DOCUMENTO_OBRIGATORIO`). Mesmos erros de CPF do cadastro. ### `DELETE /api/v1/clientes/{id}` — Excluir Exclusão permanente. Retorna `409 CONFLICT` se o cliente tiver certificados ativos ou trocas em aberto. ### `GET /api/v1/clientes/{id}/wallet/qrcode` — QR Code da wallet Retorna **PNG binário** (`image/png`), não JSON. O QR contém a URL `https://wallet.garantify.app/w/{token}` (na carteira com documento, o link da loja). `404 WALLET_TOKEN_NOT_FOUND` se o cliente não tem wallet. ### `POST /api/v1/clientes/{id}/wallet/gerar` — Gerar wallet Body opcional: `{ "enviarEmail": true }`. Retorna `{ "walletUrl": "...", "emailEnviado": true }`. `409` se o cliente já tem wallet — use `regenerar`. Na carteira com documento a wallet continua sendo gerada, mas o `walletUrl` devolvido (e o de `wallet.created`) é o link da loja. ### `POST /api/v1/clientes/{id}/wallet/regenerar` — Regenerar link Invalida o token atual atomicamente e emite um novo. Body opcional: `{ "reenviarEmail": true }`. Rate limit: 10 por 24h por cliente → `429` + `Retry-After`. Dispara `wallet.regenerated`. Na carteira com documento retorna `409 CARTEIRA_MODO_DOCUMENTO`: não há link individual para regenerar. ### Carteira com documento (link único da loja) Modalidade opcional da conta, desligada por padrão e ligada pelo lojista no painel (Configurações > Carteira de garantias). Com ela, o consumidor entra por um único link da loja, `https://{slug}.garantify.app/{caminho}`, informando CPF e telefone, em vez do link individual `https://wallet.garantify.app/w/{token}`. Com a modalidade ligada: - `documento` (CPF) passa a ser obrigatório no cadastro e na edição de clientes (`400 DOCUMENTO_OBRIGATORIO`). - Emissão de certificado sem `clienteId` e transferência sem `novoConsumidorId` retornam `400 CLIENTE_OBRIGATORIO`. - Todo `walletUrl` devolvido pela API e pelos webhooks (`wallet.*`, `destinatarioWalletUrl` de `certificate.transferred`, `walletUrlDestino` de `wallet.certificado_transferido`) é o link da loja. - Os links individuais deixam de abrir a carteira e `POST /wallet/regenerar` retorna `409 CARTEIRA_MODO_DOCUMENTO`. Se a conta usa ou pode vir a usar essa modalidade, cadastre o cliente com `documento` antes de emitir, emita e transfira sempre por ID de cliente e use o `walletUrl` que a API devolve em vez de montar o link. --- ## Endpoints: Produtos Base: `/api/v1/produtos` · `Idempotency-Key` obrigatório em escritas. ### `POST /api/v1/produtos` | Campo | Tipo | Obrigatório | Observações | |---|---|---|---| | `nome` | string | **sim** | Máx 200 | | `marcaId` | string | **sim** | | | `garantiaMeses` | number | **sim** | `> 0` | | `codigoProduto` | string | não | Único por conta | | `descricao` | string | não | | | `fotoUrl` | string | não | URI absoluta | | `marcaNome` | string | não | | | `preco` | number | não | `>= 0` | | `metadata` | object | não | | | `emEstoque` | boolean | não | | | `noCatalogo` | boolean | não | | ### `GET /api/v1/produtos` — Listar Query: `page`, `pageSize`, `search`, `marcaId`, `codigo`, `sort`, `metadata.*` Item: `id`, `nome`, `codigoProduto`, `descricao`, `fotoUrl`, `marcaId`, `marcaNome`, `preco`, `garantiaMeses`, `emEstoque`, `noCatalogo`, `metadata`, `criadoEm`. ### `GET /api/v1/produtos/{id}` — Obter ### `PUT /api/v1/produtos/{id}` — Atualizar **Propagação para certificados ativos:** quando `nome`, `codigoProduto`, `marcaId` ou `fotoUrl` mudam, todos os certificados **ativos** desse produto são atualizados e um webhook `certificate.updated` é disparado para cada um. O preço registrado no certificado no momento da emissão nunca é alterado. --- ## Endpoints: Marcas Base: `/api/v1/marcas` · `Idempotency-Key` obrigatório em escritas. | Método | Rota | Descrição | |---|---|---| | `POST` | `/api/v1/marcas` | Cadastrar marca | | `GET` | `/api/v1/marcas` | Listar (query: `page`, `pageSize`, `search`, `sort`) | | `GET` | `/api/v1/marcas/{id}` | Obter | | `PUT` | `/api/v1/marcas/{id}` | Atualizar | | Campo | Tipo | Obrigatório | Observações | |---|---|---|---| | `nome` | string | **sim** (no POST) | Máx 200 | | `endereco` | string | não | | | `telefone` | string | não | Máx 20 | | `email` | string | não | Deve ser válido | | `website` | string | não | URI absoluta | Marcas **não** suportam filtro `metadata.*`. --- ## Endpoints: Reclamações Base: `/api/v1/reclamacoes` · `Idempotency-Key` obrigatório em escritas (exceto `responder`). ### `POST /api/v1/reclamacoes` — Abrir reclamação | Campo | Tipo | Obrigatório | Observações | |---|---|---|---| | `certificadoId` | string | **sim** | | | `consumidorNome` | string | **sim** | Máx 200 | | `descricao` | string | **sim** | Máx 2000 | | `consumidorEmail` | string | não | Fallback: e-mail do certificado | | `consumidorTelefone` | string | não | Mín. 10 dígitos se presente | | `fotosUrls` | array | não | **Máx 3** | | `fotosThumbnailKeys` | array | não | | | `metadata` | object | não | | ### `GET /api/v1/reclamacoes` — Listar Query: `page`, `pageSize`, `status`, `search`, `sort`, `metadata.*` Item: `id`, `protocolo`, `certificadoId`, `codigoCertificado`, `consumidorNome`, `consumidorEmail`, `produtoNome`, `descricao`, `motivo`, `status`, `visualizada`, `produtoSubstituido`, `certificadoSubstitutoId`, `historico[]`, `criadoEm`, `currentStage`, `statusPipeline`, `pipelineVersion`. ### `GET /api/v1/reclamacoes/{id}` — Obter Retorna a reclamação enriquecida com `acoesExternas[]` — as ações que o seu sistema pode executar no estado atual. ### `POST /api/v1/reclamacoes/{id}/responder` — Executar ação externa Permite que a integração responda/tramite a reclamação, quando a etapa atual permite resposta externa. As ações válidas vêm em `acoesExternas` do `GET /{id}`. `Idempotency-Key` é **opcional** aqui. ```json { "acao": "slug-da-acao", "texto": "opcional", "autor": { "nome": "opcional", "id": "opcional" } } ``` Sucesso `200`: `{ "id", "statusPipeline", "currentStage", "acoesExternas": [...] }` Erro `409 ESTADO_ALTERADO` (a reclamação mudou desde a consulta) devolve as ações atualizadas junto: ```json { "error": { "code": "ESTADO_ALTERADO", "message": "A situação da reclamação mudou. Consulte as ações disponíveis atuais." }, "acoesExternas": [ ... ] } ``` Outros erros: `404 RECLAMACAO_NOT_FOUND`, `409 ACAO_INDISPONIVEL`, `400 TEXTO_OBRIGATORIO`, `400 TEXTO_NAO_ACEITO`, `400 TEXTO_LONGO`. --- ## Endpoints: Vendedores Base: `/api/v1/vendedores` · `Idempotency-Key` obrigatório em escritas. Vendedores identificam **quem emitiu cada certificado**. Não fazem login no painel — existem apenas para rastreabilidade via API. O `id` retornado no `POST` é o valor a enviar no header `X-Garantify-Vendedor-Id`. > **Rota antiga:** `/api/v1/operadores` é um **alias permanente** desta rota. Ver [Rotas obsoletas](#rotas-obsoletas). | Método | Rota | Descrição | Sucesso | |---|---|---|---| | `POST` | `/api/v1/vendedores` | Cadastrar | `201` + header `Location` | | `GET` | `/api/v1/vendedores` | Listar | `200` | | `GET` | `/api/v1/vendedores/{id}` | Obter | `200` | | `PUT` | `/api/v1/vendedores/{id}` | Atualizar | `200` | | `DELETE` | `/api/v1/vendedores/{id}` | Desativar | `200` | | `PUT` | `/api/v1/vendedores/{id}/bloquear` | Bloquear | `200` | | `PUT` | `/api/v1/vendedores/{id}/desbloquear` | Desbloquear | `200` | ### Corpo (POST) | Campo | Tipo | Obrigatório | Observações | |---|---|---|---| | `nome` | string | **sim** | Máx 200 | | `identificadorExterno` | string | **sim** | Máx 100, único por conta (ex: matrícula, código no ERP) | | `email` | string | não | | | `telefone` | string | não | Máx 20 | | `cargo` | string | não | | | `metadata` | object | não | | | `roleId` | string | não | | No `PUT` todos os campos são opcionais, mais `ativo` (boolean). ### Listagem Query: `page` (default 1), `pageSize` (**default 50**, 1–200), `ativo` (boolean). Não aceita `search`, `sort` nem `metadata.*`. Item: `id`, `nome`, `email`, `telefone`, `cargo`, `identificadorExterno`, `ativo`, `criadoEm`. ### Bloqueio vs desativação - **Desativar** (`DELETE`): marca como inativo, reversível via `PUT` com `ativo: true`. - **Bloquear** (`PUT /bloquear`): impede o vendedor de executar ações — requisições com `X-Garantify-Vendedor-Id` apontando para ele passam a retornar `403 OPERADOR_BLOCKED`. `409 CONFLICT` no POST/PUT quando `identificadorExterno` já existe. `400 PLAN_LIMIT` ao exceder `MaxMembrosEquipe` do plano. Webhooks: `vendedor.created`, `vendedor.updated`, `vendedor.deleted`, `vendedor.blocked`, `vendedor.unblocked`. --- ## Endpoints: Upload ### `POST /api/v1/upload/imagem` `multipart/form-data`, campo **`arquivo`**. **Não usa `Idempotency-Key`.** - Formatos: PNG, JPEG, WebP - Tamanho máximo: **10 MB** ```bash curl -X POST "https://api-enterprise.garantify.com.br/api/v1/upload/imagem" \ -H "X-Garantify-API-Key: SUA_API_KEY" \ -F "arquivo=@produto.jpg" ``` ```json { "fotoUrl": "https://images.garantify.com.br/....webp", "thumbnailUrl": "https://images.garantify.com.br/..._thumb.webp", "fotoEmailUrl": "https://images.garantify.com.br/....email.jpg" } ``` Três variantes são geradas: principal (WebP 1200px), thumbnail (WebP 300px) e uma versão JPEG com fundo branco para e-mail (compatibilidade com clientes legados). Erros: `400 VALIDATION_ERROR` — `Nenhum arquivo enviado.` / `Arquivo muito grande. Máximo: 10MB.` / `Apenas imagens são aceitas.` --- ## Endpoints: Reclamações V2 (pipeline customizável) Base: `/api/v2/reclamacoes` API para contas com **pipeline de trocas customizável** habilitado. Permite fluxos de estados definidos pelo tenant, com transições nomeadas. Requer `Idempotency-Key` nos dois `POST`. Rate limit de **100 req/min** nos `POST` (não nos `GET`). ### `POST /api/v2/reclamacoes` — Criar | Campo | Tipo | Obrigatório | Observações | |---|---|---|---| | `certificadoId` | string | **sim** | | | `descricao` | string | **sim** | 5–2000 chars | | `fotosUrls` | array | não | Máx 3; HTTPS de `images.garantify.com.br` | | `camposPersonalizados` | object | não | | `201 Created`: ```json { "id": "uuid", "protocolo": "...", "certificadoId": "...", "stageAtualSlug": "...", "stageAtualNome": "...", "pipelineRevisao": 3, "prazoExpiraEm": "2026-09-10T12:00:00Z", "versao": 1, "criadoEm": "...", "atualizadoEm": "..." } ``` Dispara `troca_v2.criada`. ### `GET /api/v2/reclamacoes` — Listar Query: `page`, `pageSize`, `stage`, `dataInicio`, `dataFim`. Item: `id`, `protocolo`, `stageAtualSlug`, `stageAtualNome`, `prazoExpiraEm`, `criadoEm`. ### `GET /api/v2/reclamacoes/{id}` — Detalhe Retorna a reclamação com `currentStage`, `pipelineRevisao`, `tramitacaoApiPermitida` (boolean), `transicoesCandidatas[]` (transições válidas a partir do estado atual) e `historico[]`. **Eventos internos nunca são retornados.** O histórico é filtrado para: `criacao`, `transicao`, `prazo_expirado`, `certificado_emitido`, `email_enviado_consumidor`. Eventos de IA, HTTP externo, override humano e falhas ficam ocultos. `404 NOT_FOUND` quando não existe. ### `POST /api/v2/reclamacoes/{id}/tramitar` — Tramitar Move a reclamação para outro estado. Só funciona se a etapa atual tiver `permiteTramitacaoApi: true` (visível no `GET /{id}`). | Campo | Tipo | Obrigatório | Observações | |---|---|---|---| | `transicaoSlug` | string | **sim** | Máx 100; deve estar em `transicoesCandidatas` | | `motivo` | string | não | Máx 2000 | | `parametrosAcoes` | object | não | Parâmetros para ações da transição | **Erro `403` tem formato diferente do padrão** (`error` é string): ```json { "error": "tramitacao_api_nao_permitida", "stageAtual": "aguardando-analise", "tramitacaoApiPermitida": false, "message": "..." } ``` Outros: `400`, `401`, `404`, `409`, `422`, `429`. --- ## Wallet do consumidor A wallet é a carteira digital do consumidor final, acessada por link único (`https://wallet.garantify.app/w/{token}`). **Não usa API key** — a autenticação é por cookie de sessão + CSRF, e o consumidor entra com o próprio telefone. Você normalmente não integra com esses endpoints: eles são consumidos pelo frontend da wallet. Estão documentados aqui porque disparam webhooks que a sua integração recebe (`wallet.*`). | Método | Rota | Descrição | |---|---|---| | `POST` | `/wallet/{token}/auth` | Autentica o consumidor (telefone como senha) | | `GET` | `/wallet/{token}/branding` | Identidade visual da loja (público, sempre 200) | | `GET` | `/wallet/{token}/certificados` | Lista certificados do consumidor | | `GET` | `/wallet/{token}/certificados/{codigo}` | Detalhe do certificado | | `GET` | `/wallet/{token}/certificados/{codigo}/nota-fiscal` | URL temporária da nota fiscal (5 min) | | `POST` | `/wallet/{token}/transferir/buscar` | Busca destinatários por telefone | | `POST` | `/wallet/{token}/certificados/{codigo}/transferir` | Transfere o certificado | | `GET` | `/wallet/{token}/reclamacoes` | Lista reclamações do consumidor | | `GET` | `/wallet/{token}/reclamacoes/{id}` | Detalhe da reclamação | | `POST` | `/wallet/{token}/reclamacoes` | Abre reclamação | | `POST` | `/wallet/{token}/reclamacoes-v2` | Abre reclamação no pipeline V2 | | `POST` | `/wallet/{token}/reclamacoes/fotos` | Envia foto (máx 3/sessão, 5 MB) | | `POST` | `/wallet/{token}/reclamacoes/{id}/responder` | Responde ação externa | | `POST` | `/wallet/{token}/logout` | Encerra a sessão | | `GET` | `/carteira/{slug}/branding` | Identidade visual da loja pelo slug + `modoDocumento` (público; slug inexistente ou modalidade desligada devolve 200 com `modoDocumento: false`) | | `POST` | `/carteira/{slug}/auth` | Login pelo link da loja: `{ "cpf", "telefone" }` | Gere e regenere links de wallet pela API em `/api/v1/clientes/{id}/wallet/gerar` e `/regenerar`. Na [carteira com documento](#carteira-com-documento-link-único-da-loja), o consumidor entra pelo link da loja (`https://{slug}.garantify.app/{caminho}`, que redireciona para `https://wallet.garantify.app/c/{slug}`) com CPF e telefone. CPF inexistente e telefone errado recebem a mesma resposta (`401 CREDENCIAIS_INVALIDAS`); após falhas repetidas o CPF fica bloqueado temporariamente (`423 CPF_BLOQUEADO`). Com a modalidade ligada, `POST /wallet/{token}/auth` retorna `403 CARTEIRA_MODO_DOCUMENTO` e a transferência pela wallet exige CPF, telefone e nome do destinatário. --- ## Webhooks Notificações HTTP `POST` em tempo real. Configure endpoints no painel. ### Formato da entrega ``` POST https://seu-endpoint.com/webhooks/garantify Content-Type: application/json X-Garantify-Event: certificate.created X-Garantify-Signature: t=1788457405,v1=a3f5c... X-Correlation-Id: ... ``` ```json { "id": "evt_3fa85f645717456...", "type": "certificate.created", "createdAt": "2026-09-03T12:00:00Z", "data": { } } ``` ### Validação de assinatura `X-Garantify-Signature` traz `t={unixSeconds},v1={hmacHex}`, onde o HMAC é: ``` HMAC-SHA256(secret, "{timestamp}.{payloadBruto}") ``` Calcule sobre o **corpo bruto** da requisição, antes de qualquer parse JSON. Compare em tempo constante. O secret tem formato `grtf_whsec_{ambiente}_{64 hex}`. Exemplo em Node.js: ```javascript const crypto = require('crypto'); function validarAssinatura(rawBody, signatureHeader, secret) { const [tPart, v1Part] = signatureHeader.split(','); const timestamp = tPart.split('=')[1]; const assinatura = v1Part.split('=')[1]; const esperado = crypto .createHmac('sha256', secret) .update(`${timestamp}.${rawBody}`) .digest('hex'); return crypto.timingSafeEqual(Buffer.from(assinatura), Buffer.from(esperado)); } ``` ### Catálogo completo de eventos **Certificados** | Evento | Quando | |---|---| | `certificate.created` | Certificado emitido com sucesso | | `certificates.batch_created` | Lote de certificados emitido (note o plural) | | `certificate.cancelled` | Certificado cancelado | | `certificate.updated` | Dados do produto propagados ao certificado ativo | | `certificate.expired` | Certificado atingiu a data de expiração (job diário) | | `certificate.transferred` | Titularidade transferida | **Reclamações / trocas** | Evento | Quando | |---|---| | `exchange.created` | Reclamação aberta | | `exchange.updated` | Reclamação atualizada | | `exchange.deadline_expired` | Prazo da etapa expirou | | `exchange.deadline_approaching` | Prazo se aproximando | | `exchange.action_required` | Ação externa disponível para a integração | | `exchange.external_action_executed` | Ação externa executada | | `exchange.ai_decision` | Decisão automatizada por IA | **Reclamações V2** | Evento | Quando | |---|---| | `troca_v2.criada` | Reclamação V2 criada | | `troca_v2.status.changed` | Mudança de estado (note os dois pontos) | | `troca_v2.finalizada` | Fluxo concluído | | `troca_v2.cert_emitido` | Certificado substituto emitido pelo fluxo | **Clientes, produtos e marcas** `client.created` · `client.updated` · `client.deleted` · `product.created` · `product.updated` · `product.deleted` · `brand.created` · `brand.updated` · `brand.deleted` **Vendedores** `vendedor.created` · `vendedor.updated` · `vendedor.deleted` · `vendedor.blocked` · `vendedor.unblocked` > Os eventos antigos `operador.*` foram **renomeados** para `vendedor.*` e **não têm alias**. Diferente da rota e do header, que permanecem compatíveis para sempre. Não havia nenhuma assinatura ativa em `operador.*` no momento da mudança. **Wallet do consumidor** `wallet.created` · `wallet.regenerated` · `wallet.auth_failed` · `wallet.reclamacao_criada` · `wallet.certificado_transferido` > Nos eventos `client.created` e `client.updated`, `data` inclui `documento` (CPF com 11 dígitos, ou `null`). Na [carteira com documento](#carteira-com-documento-link-único-da-loja), o `walletUrl` dos eventos `wallet.*`, o `walletUrlDestino` de `wallet.certificado_transferido` e o `destinatarioWalletUrl` de `certificate.transferred` trazem o link da loja. **Sistema** | Evento | Quando | |---|---| | `task.failed` | Operação assíncrona (emissão/cancelamento) falhou após todas as retentativas | ### Entrega e retentativas Falhas são retentadas com backoff. Webhooks pausados acumulam entregas pendentes e são reenfileirados na reativação. O limite de webhooks configuráveis depende do plano — exceder retorna `400 PLAN_LIMIT`. --- ## Catálogo de códigos de erro ### Autenticação e autorização | Código | HTTP | Significado | |---|---|---| | `UNAUTHORIZED` | 401 | API key ausente, inválida, revogada, expirada; ou conta bloqueada | | `FORBIDDEN` | 403 | Sem permissão para a ação | | `HEADER_CONFLICT` | 400 | `X-Garantify-Vendedor-Id` e `X-Garantify-Operador-Id` enviados com valores diferentes | | `OPERADOR_NOT_FOUND` | 403 | Vendedor do header não existe nesta conta | | `OPERADOR_BLOCKED` | 403 | Vendedor do header está bloqueado | | `SANDBOX_NOT_AVAILABLE` | 403 | Plano não inclui sandbox | ### Idempotência | Código | HTTP | Significado | |---|---|---| | `idempotency_key_required` | 400 | Header obrigatório ausente | | `idempotency_key_invalid` | 400 | Formato inválido (1–255 ASCII imprimíveis) | | `idempotency_key_payload_mismatch` | 422 | Mesma chave com corpo diferente | | `idempotency_key_in_progress` | 409 | Requisição anterior ainda em processamento | | `idempotency_request_uncertain` | 502 | Resultado anterior indefinido — consulte o estado via GET antes de gerar nova chave | | `IDEMPOTENCY_KEY_REQUIRED` | 400 | Variante usada em `/api/v2/*` | | `IDEMPOTENCY_KEY_INVALID` | 400 | Chave acima de 255 chars em `/api/v2/*` | ### Limites | Código | HTTP | Significado | |---|---|---| | `RATE_LIMIT_EXCEEDED` | 429 | Limite por segundo do plano (`/api/v1/*`) | | `RATE_LIMITED` | 429 | Limite de 100/min (`/api/v2/*`) | | `QUOTA_EXCEEDED` | 400 | Cota mensal de certificados esgotada | | `TRIAL_EXPIRED` | 400 | Período de teste terminou | | `PLAN_LIMIT` | 400 | Limite do plano atingido (equipe, webhooks, API keys) | ### Validação e recursos | Código | HTTP | Significado | |---|---|---| | `VALIDATION_ERROR` | 400 | Campo inválido ou ausente | | `BAD_REQUEST` | 400 | Requisição malformada | | `NOT_FOUND` | 404 | Recurso não encontrado | | `CONFLICT` | 409 | Conflito de estado | | `WALLET_TOKEN_NOT_FOUND` | 404 | Cliente não possui wallet | | `INTERNAL_ERROR` | 500 | Erro interno | ### Clientes e carteira com documento | Código | HTTP | Significado | |---|---|---| | `DOCUMENTO_INVALIDO` | 400 | `documento` não é um CPF válido | | `DOCUMENTO_OBRIGATORIO` | 400 | Carteira com documento ligada e cliente sem CPF | | `DOCUMENTO_DUPLICADO` | 409 | CPF já cadastrado em outro cliente da conta | | `CLIENTE_OBRIGATORIO` | 400 | Carteira com documento ligada: emissão sem `clienteId` ou transferência sem `novoConsumidorId` | | `CARTEIRA_MODO_DOCUMENTO` | 409 | Carteira com documento ligada: não há link individual para regenerar | ### Reclamações — ação externa | Código | HTTP | Significado | |---|---|---| | `RECLAMACAO_NOT_FOUND` | 404 | Reclamação não encontrada | | `ACAO_INDISPONIVEL` | 409 | Ação não habilitada para o canal API | | `ESTADO_ALTERADO` | 409 | Estado mudou desde a consulta (resposta traz `acoesExternas` atualizadas) | | `TEXTO_OBRIGATORIO` | 400 | A ação exige texto | | `TEXTO_NAO_ACEITO` | 400 | A ação não aceita texto | | `TEXTO_LONGO` | 400 | Texto acima do limite | ### Reclamações V2 | Código | HTTP | Significado | |---|---|---| | `TROCA_LOCKED` | 409 | Reclamação em edição concorrente | | `TRANSICAO_INVALIDA` | 409 | Transição não válida a partir do estado atual | | `TRAMITACAO_HUMANA_NAO_PERMITIDA` | 409 | Etapa não permite tramitação manual | | `PIPELINE_V2_HTTP_BLOCKED` | 422 | Chamada HTTP do fluxo bloqueada por allowlist | | `PIPELINE_V2_HTTP_UNEXPECTED_STATUS` | 422 | Status inesperado na chamada externa do fluxo | | `PIPELINE_V2_HTTP_TIMEOUT` | 422 | Timeout na chamada externa do fluxo | --- ## Limites por plano | Recurso | Trial | Starter | Growth | Scale | |---|---|---|---|---| | Certificados | **10 no total** | 100/mês | 500/mês | 3.000/mês | | Duração | **15 dias** | — | — | — | | Requests/segundo | 10 | 10 | 50 | 300 | | Sandbox | não | não | **sim** | **sim** | | Webhooks | 0 | 0 | 3 | 10 | | Membros de equipe | 5 | 10 | 20 | 50 | | API keys | 1 | 1 | 3 | 10 | | Tokens MCP | 0 | 0 | 2 | 5 | | Pipeline customizável | não | não | sim | sim | | Campos personalizados | não | não | sim | sim | | Relatórios avançados | não | não | sim | sim | | RBAC customizado | não | não | não | sim | | E-mail white-label | não | não | não | sim | | Domínio de e-mail próprio | não | não | não | sim | | Créditos de IA/mês | 10 | 20 | 100 | 200 | | Preço (R$/mês) | 0 | 57 | 247 | 697 | | Suporte | Comunidade | E-mail | Prioritário | Dedicado | O plano **Custom** tem limites negociados e sem teto prático (rate limit 3.000/s, suporte dedicado com SLA). **Trial:** o limite é **10 certificados OU 15 dias, o que vier primeiro**. Não é uma cota mensal renovável — é um total acumulado. Após expirar, emissões retornam `TRIAL_EXPIRED`. --- ## Rotas obsoletas ### `/api/v1/operadores` → `/api/v1/vendedores` A rota foi **renomeada** em setembro/2026 para refletir a nomenclatura correta. A rota antiga é um **alias permanente**: - **Continua funcionando indefinidamente.** Não há data de remoção. - Mesmos corpos de requisição e resposta. - Mesmos códigos de erro (`OPERADOR_NOT_FOUND`, `OPERADOR_BLOCKED`). - O header `X-Garantify-Operador-Id` continua aceito. Requisições ao path antigo recebem headers indicando a substituição (RFC 8594): ``` Deprecation: true Link: ; rel="successor-version" ``` No OpenAPI Spec, os endpoints `/api/v1/operadores*` estão marcados com `deprecated: true`. **Recomendação:** migre para `/api/v1/vendedores` e `X-Garantify-Vendedor-Id` quando for conveniente. Nada quebra se você não migrar. **Exceção — webhooks:** os eventos `operador.*` foram renomeados para `vendedor.*` **sem alias**. Se você assinava eventos `operador.*`, atualize para `vendedor.*`. --- ## Armadilhas conhecidas Comportamentos não óbvios que valem atenção ao integrar: 1. **Emissão e cancelamento são assíncronos.** Retornam `202 Accepted` com `taskId`, não o certificado. Use os webhooks `certificate.created` / `certificate.cancelled` para saber o resultado, ou `task.failed` para falhas. Transferência, nota fiscal e reenvio de e-mail são síncronos. 2. **Erros de paginação em V1 retornam string, não objeto:** `{"error": "O parâmetro 'page' deve ser maior ou igual a 1."}`. Todo o restante da API usa `{"error": {"code", "message"}}`. 3. **O `403` de tramitação V2 tem formato próprio** — `error` é string e há campos irmãos (`stageAtual`, `tramitacaoApiPermitida`). 4. **O filtro `operadorId` em `GET /certificados` e `GET /clientes` é aplicado após a paginação.** Os campos `total` e `totalPages` refletem a contagem **sem** o filtro. Para contagens exatas por vendedor, agregue no seu lado. 5. **Três grafias diferentes para "chave de idempotência ausente"** convivem na API: `idempotency_key_required` (v1, minúsculo), `IDEMPOTENCY_KEY_REQUIRED` (v2), `IDEMPOTENCY_KEY_AUSENTE` (wallet). Trate os três se for genérico. 6. **`certificates.batch_created` é o único evento com substantivo no plural** e `troca_v2.status.changed` o único com dois pontos separadores. Não normalize os nomes por regex. 7. **O rate limit de `/api/v1/*` é por instância de execução.** Sob concorrência alta o teto efetivo pode ser maior que o nominal. Não dependa disso — implemente backoff ao receber `429`. 8. **`GET /api/v1/certificados/codigo/{codigo}` busca pelo código público** e não é escopado por conta na consulta. 9. **Após `idempotency_request_uncertain` (502), NÃO retente com a mesma chave.** Consulte o estado real via `GET` e só então gere uma chave nova. 10. **Upload de imagem não aceita `Idempotency-Key`** — é o único endpoint de escrita da v1 sem idempotência. --- ## Fluxo típico de integração ``` 1. POST /api/v1/marcas → marcaId 2. POST /api/v1/produtos → produtoId (usa marcaId) 3. POST /api/v1/vendedores → vendedorId (opcional, para rastreabilidade) 4. POST /api/v1/clientes → clienteId 5. POST /api/v1/certificados → taskId (header X-Garantify-Vendedor-Id opcional) └─ webhook certificate.created confirma a emissão 6. GET /api/v1/certificados → acompanhar ``` Reclamações chegam por `exchange.created`; responda ações externas via `POST /api/v1/reclamacoes/{id}/responder` quando `acoesExternas` listar a ação. --- *Documentação gerada para consumo por LLMs. Última atualização: setembro/2026. Para a versão navegável com exemplos em 8 linguagens, acesse https://developers.garantify.com.br*