{"schema_version":1,"generated_at":"2026-10-15T18:11:28.400Z","commit":"51c633ccf217","locales":["pt-br","en","es","fr"],"default_locale":"pt-br","categories":[{"id":"marketing","label":{"pt-br":"Marketing e QR Codes","en":"Marketing & QR codes","es":"Marketing y códigos QR","fr":"Marketing et QR codes"}},{"id":"documents","label":{"pt-br":"Documentos","en":"Documents","es":"Documentos","fr":"Documents"}},{"id":"security","label":{"pt-br":"Segurança","en":"Security","es":"Seguridad","fr":"Sécurité"}},{"id":"ai","label":{"pt-br":"Inteligência artificial","en":"Artificial intelligence","es":"Inteligencia artificial","fr":"Intelligence artificielle"}},{"id":"integrations","label":{"pt-br":"Integrações","en":"Integrations","es":"Integraciones","fr":"Intégrations"}}],"pages":{"pt-br":[{"slug":"agentes-de-ia","url":"https://docs.respawntech.dev/agentes-de-ia","title":"Agentes de IA (MCP)","description":"Conecte Claude, Cursor, VS Code e outros agentes às ferramentas pelo servidor MCP, com custo visível e limite de gasto por chave.","sections":[{"anchor":"top","heading":"","markdown":"O servidor **MCP** (Model Context Protocol) deixa um agente de IA descobrir e usar as ferramentas da plataforma, como criar um QR, converter um PDF ou buscar na base de conhecimento, com a sua conta e os seus créditos. Ele usa as mesmas regras da API: os mesmos preços, escopos e franquias.\n\n<Callout title=\"Liberação gradual\">\n  O servidor MCP está sendo liberado aos poucos. Enquanto ele não estiver ativo no seu ambiente, `/mcp` responde `404` com o código `mcp.disabled`.\n</Callout>"},{"anchor":"conexao","heading":"Conexão","markdown":"|                    |                                                |\n| ------------------ | ---------------------------------------------- |\n| URL                | <McpUrl />                                     |\n| Transporte         | Streamable HTTP, sem sessão, respostas JSON    |\n| Autenticação       | `Authorization: Bearer <token de API>`         |\n| Escopo obrigatório | `mcp:use`, além dos escopos de cada ferramenta |\n\nNunca coloque o token na URL (`?token=`, `?api_key=`…): o servidor recusa com `400`. Conectores de produtos como Claude.ai e ChatGPT, que pedem login em vez de token, ainda não são suportados."},{"anchor":"chave","heading":"Crie uma chave para o agente","markdown":"No [dashboard](https://dashboard.respawntech.dev), em **APIs**, crie um token só para o agente. Os modelos prontos preenchem escopos e controles:\n\n* **Agente de suporte**: busca na base de conhecimento e consulta de saldo.\n* **Agente completo**: todas as ferramentas.\n* **Somente leitura**: só consultas.\n\nUma chave de agente deve ter um **limite diário de gasto** (sugestão: <Limit name=\"mcpSuggestedDailyLimit\" />) e uma **data de expiração**. Veja [Controles da chave](/autenticacao#controles-da-chave)."},{"anchor":"clientes","heading":"Configure o cliente","markdown":"<McpClientSnippets />\n\nClientes que só falam stdio usam a ponte `mcp-remote`, como no último exemplo."},{"anchor":"ferramentas","heading":"Ferramentas","markdown":"O agente só vê as ferramentas permitidas pelos escopos da chave. As descrições que ele recebe estão em inglês por padrão; use `lang` (abaixo) para outro idioma.\n\n<McpToolsTable />\n\nFerramentas **destrutivas** pedem confirmação ao usuário na maioria dos clientes. As marcadas com dry-run aceitam `dry_run: true` e devolvem só a estimativa de custo."},{"anchor":"parametros","heading":"Parâmetros da conexão","markdown":"Todos são opcionais e vão na query string da URL. Eles só **restringem** o que a chave permite, nunca ampliam. Um parâmetro desconhecido ou com valor inválido responde `400`, para um erro de digitação não virar \"sem filtro\".\n\n<McpParamsTable />\n\nPor exemplo, um agente de suporte que só lê a base de conhecimento, com respostas em português: <McpUrl query=\"toolsets=rag&access=read&lang=pt\" />\n\nParâmetros repetidos em várias conexões podem virar um **perfil** no dashboard (`profile=<slug>`), e uma chave pode ter um perfil padrão."},{"anchor":"custos","heading":"Custos e controle de gasto","markdown":"* Cada chamada custa o mesmo que a operação equivalente na API, e as leituras medidas entram na mesma [franquia diária](/cobranca-e-limites#franquia-diaria).\n* Toda resposta traz o custo em `_meta[\"tech.respawn/cost\"]`: créditos cobrados, estimativa (em dry-run), saldo depois e quanto resta do limite do dia.\n* `simulate=true` na URL faz todas as ferramentas que cobram devolverem só a estimativa. É o jeito de testar um agente sem gastar.\n* Passar do limite diário da chave responde `billing.spend_limit_reached`; passar do teto por chamada (`max_call_credits`) responde `billing.call_limit_exceeded`.\n* O **botão de pânico** no dashboard desliga o acesso de todos os agentes da conta na hora (`mcp.agents_disabled`).\n* O extrato do dashboard separa o gasto por canal (API, MCP, dashboard) e por chave."},{"anchor":"idempotencia","heading":"Idempotência","markdown":"Agentes repetem chamadas com frequência. Ferramentas que criam ou alteram algo aceitam `idempotency_key`; sem ele, uma chamada idêntica (mesma chave, ferramenta e argumentos) em até <Limit name=\"mcpIdempotencyWindow\" /> devolve o resultado anterior sem executar nem cobrar de novo (`_meta[\"tech.respawn/replayed\"]`)."},{"anchor":"limites","heading":"Limites","markdown":"| Limite                      | Valor                                                                       |\n| --------------------------- | --------------------------------------------------------------------------- |\n| Requisições                 | <Limit name=\"rateLimitPerMinute\" /> por minuto por chave, somadas às da API |\n| Corpo da requisição         | <Limit name=\"mcpRequestMax\" />                                              |\n| Resultado de uma ferramenta | <Limit name=\"mcpResultMax\" />                                               |\n| Chamadas de `tools_search`  | <Limit name=\"mcpToolsSearchPerMinute\" /> por minuto                         |"},{"anchor":"erros","heading":"Erros","markdown":"Erros de negócio voltam como resultado da ferramenta, com uma mensagem que diz ao agente o que fazer e o código em `_meta[\"tech.respawn/error\"]`. Erros da conexão voltam como HTTP, no formato `{\"error\": {\"code\", \"message\"}}`.\n\n<ErrorTable group=\"agents\" />\n\nOs erros das ferramentas são os mesmos da API; veja [Códigos de erro](/erros)."}]},{"slug":"autenticacao","url":"https://docs.respawntech.dev/autenticacao","title":"Autenticação","description":"Como autenticar nas ferramentas com token de API, escopos e requisitos de conta.","sections":[{"anchor":"token-de-api","heading":"Token de API","markdown":"Todas as rotas em `/v1/tools/*` exigem um **token de API**, criado no [dashboard](https://dashboard.respawntech.dev) em **APIs**.\n\n| Forma                         | Exemplo                              |\n| ----------------------------- | ------------------------------------ |\n| Header dedicado (recomendado) | `X-API-Token: rsk_live_...`          |\n| Authorization                 | `Authorization: ApiKey rsk_live_...` |\n| Authorization (alternativo)   | `Authorization: Token rsk_live_...`  |\n\n<Callout title=\"Boas práticas\">\n  Guarde o token na variável de ambiente `RESPAWN_API_TOKEN`, crie um token por integração com os **escopos mínimos** necessários e revogue os que não usar mais.\n</Callout>"},{"anchor":"requisitos-da-conta","heading":"Requisitos da conta","markdown":"Antes de qualquer chamada às ferramentas, a conta precisa ter:\n\n* **E-mail verificado** — caso contrário, `auth.email_not_verified` (403).\n* **Saldo não negativo** — conta suspensa retorna `billing.user_bankrupt` (402).\n* **Créditos suficientes** — operações pagas sem saldo retornam `billing.insufficient_credits` (402)."},{"anchor":"escopos","heading":"Escopos","markdown":"Cada rota aceita um conjunto de escopos; basta o token ter **um** deles. Se faltar permissão, a API retorna `scope.missing` (403) com `details.required` e `details.provided`. Os escopos aceitos por cada endpoint aparecem no card do endpoint.\n\n<ScopeTable />\n\n<Callout type=\"warn\" title=\"PDF é mais restritivo na leitura\">\n  Rotas de leitura de PDF aceitam apenas `tools:pdf:read` ou `tools:manage`. O escopo genérico `tools:read` e o `tools:pdf:manage` **não** dão acesso à leitura de PDF.\n</Callout>"},{"anchor":"combinações-recomendadas","heading":"Combinações recomendadas","markdown":"| Caso de uso                                  | Escopos sugeridos                                                        |\n| -------------------------------------------- | ------------------------------------------------------------------------ |\n| Gerar e gerenciar QR codes                   | `tools:qr:dynamic:manage`                                                |\n| Painel somente leitura de QRs                | `tools:qr:dynamic:read`                                                  |\n| Pipeline de PDF                              | `tools:pdf:manage` + `tools:pdf:read`                                    |\n| Verificar URLs antes de exibir               | `tools:safety:read`                                                      |\n| Assistente que busca na base de conhecimento | `tools:rag:query`                                                        |\n| Enviar e manter documentos da base           | `tools:rag:manage`                                                       |\n| [Agente de IA (MCP)](/agentes-de-ia)         | `mcp:use` + os escopos das ferramentas que ele vai usar + `account:read` |\n| Integração completa                          | `tools:manage` (mais simples, menos seguro)                              |"},{"anchor":"controles-da-chave","heading":"Controles da chave","markdown":"Além dos escopos, cada token pode ter controles configurados no dashboard. Eles valem em qualquer canal, na API e no [MCP](/agentes-de-ia):\n\n* **Expiração**: depois da data, o token para de funcionar (`auth.unauthorized`).\n* **Limite diário de gasto**: uma cobrança que passaria do limite do dia é recusada com `billing.spend_limit_reached` (402), com `details.limit`, `details.spent_today` e `details.required`. O contador zera à meia-noite UTC.\n* **Restrição por base de conhecimento**: o token só enxerga as bases escolhidas; as outras respondem `not_found`, e ele não pode criar bases novas (`forbidden`).\n* **Modelos prontos**: ao criar a chave, \"Somente leitura\", \"Agente de suporte\" e \"Agente completo\" preenchem escopos e controles de uma vez.\n\nExpiração, limite e bases podem ser alterados depois; os escopos, não. Para mudar escopos, crie outra chave."},{"anchor":"erros-de-autenticação","heading":"Erros de autenticação","markdown":"<ErrorTable group=\"auth\" />"},{"anchor":"dashboard-vs-token-de-api","heading":"Dashboard vs. token de API","markdown":"* **Dashboard** — a navegação no painel não consome a franquia diária nem debita leituras medidas.\n* **Token de API** — leituras medidas compartilham a [franquia diária](/cobranca-e-limites#franquia-diaria) de <Limit name=\"dailyQuota\" /> requisições (<Limit name=\"dailyQuotaPremium\" /> em contas premium). Depois disso, cada leitura debita créditos."},{"anchor":"exemplo","heading":"Exemplo","markdown":"Criar um QR com dry-run para validar o token, os escopos e o custo sem debitar nada:\n\n<ApiExamples\n  method=\"POST\"\n  path=\"/v1/tools/image/qr-code/dynamic\"\n  headers={{ 'X-Dry-Run': 'true', 'X-Include-Cost': 'true' }}\n  body={`{\n\"name\": \"Campanha\",\n\"target_url\": \"https://example.com\"\n}`}\n/>"}]},{"slug":"cobranca-e-limites","url":"https://docs.respawntech.dev/cobranca-e-limites","title":"Cobrança e limites","description":"Créditos, dry-run, idempotência, rate limit, franquia diária de leituras e timeouts.","sections":[{"anchor":"modelo-de-créditos","heading":"Modelo de créditos","markdown":"* O saldo é exibido em **créditos** no dashboard, e todos os valores desta documentação estão em créditos.\n* Operações pagas debitam na confirmação — nunca no dry-run.\n* As regras de preço de cada ferramenta ficam na página da ferramenta: [QR Dinâmico](/ferramentas/qr-dinamico#custos), [PDF Convert](/ferramentas/pdf-convert#custos), [Base de conhecimento](/ferramentas/base-de-conhecimento#custos), [Áudio](/ferramentas/audio#custos).\n* Cada token pode ter um [limite diário de gasto](/autenticacao#controles-da-chave): passar dele responde `billing.spend_limit_reached`.\n\n<Callout title=\"Preço exato\">\n  Custos de criação podem somar adicionais (slug customizado, design, retenção de PDF…). Faça um dry-run com o payload real para ver o total exato.\n</Callout>"},{"anchor":"dry-run","heading":"Dry-run e preview de custo","markdown":"| Header                 | Efeito                                             |\n| ---------------------- | -------------------------------------------------- |\n| `X-Dry-Run: true`      | Valida e calcula o custo sem persistir nem debitar |\n| `X-Preview-Cost: true` | Alias de `X-Dry-Run`                               |\n| `X-Include-Cost: true` | Inclui `meta.cost` na resposta                     |\n\nDry-run é aceito nas operações pagas marcadas no card de cada endpoint e nas **leituras medidas** (exceto downloads). A resposta é `200` e nada é persistido nem debitado.\n\nAs rotas de [Áudio](/ferramentas/audio#dry-run) pedem o dry-run com `?dry_run=true` na query, não com o header. O áudio é cobrado por minuto de áudio, por 1.000 caracteres ou por segundo gerado, e o valor da transcrição, da fala e dos demais jobs é devolvido se o motor falhar."},{"anchor":"orçamento-de-uma-operação-paga","heading":"Orçamento de uma operação paga","markdown":"Em `POST` de criação e conversão, `data` traz o orçamento item a item:\n\n```json\n{\n  \"success\": true,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"data\": {\n    \"total_cost_credits\": 15000,\n    \"total_cost_units\": 15000,\n    \"items\": [\n      { \"description\": { \"key\": \"billing.pdf.convert_base\" }, \"cost_credits\": 10000, \"type\": \"base\" },\n      { \"description\": { \"key\": \"billing.pdf.retention.7\" }, \"cost_credits\": 5000, \"type\": \"storage\" }\n    ],\n    \"is_monthly_recurring\": false,\n    \"currency\": \"CREDITS\"\n  }\n}\n```\n\n<Callout type=\"warn\" title=\"Valores em unidades\">\n  Apesar do nome, `total_cost_credits` e `cost_credits` do orçamento (e `cost_credits` das conversões de PDF) vêm em **unidades internas**: divida por 10.000 para obter créditos. No exemplo, 15.000 unidades = 1,5 crédito.\n</Callout>\n\nNo QR Dinâmico o orçamento também traz `slot_usage` (uso de slots e se a criação abrirá um novo slot pago) e `recurring_cost`."},{"anchor":"preview-de-uma-leitura-medida","heading":"Preview de uma leitura medida","markdown":"Em leituras medidas, o dry-run não executa a consulta — mostra apenas a posição na franquia e se a leitura seria cobrada:\n\n```json\n{\n  \"success\": true,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"data\": {\n    \"operation\": \"qr_dynamic.analytics\",\n    \"metering\": {\n      \"within_quota\": true,\n      \"quota_limit\": 2000,\n      \"quota_used\": 125,\n      \"quota_remaining\": 1875,\n      \"cost_units\": 300,\n      \"would_charge\": false\n    }\n  },\n  \"meta\": {\n    \"cost\": {\n      \"units\": 300,\n      \"credits\": 0.03,\n      \"charged\": false,\n      \"within_quota\": true,\n      \"quota_limit\": 2000,\n      \"quota_used\": 125,\n      \"quota_remaining\": 1875\n    }\n  }\n}\n```\n\nCom `X-Include-Cost: true` numa leitura real, o mesmo objeto `meta.cost` acompanha a resposta normal (`charged: true` quando a leitura foi cobrada)."},{"anchor":"idempotência","heading":"Idempotência","markdown":"Envie `X-Idempotency-Key` (UUID recomendado) em qualquer rota de ferramentas:\n\n* a chave vale por **<Limit name=\"idempotencyTtl\" />**;\n* mesma chave + mesmo payload → a resposta original é reaproveitada, sem nova cobrança;\n* mesma chave + payload diferente → `idempotency.conflict` (409).\n\nUse sempre em `POST` de criação e conversão para que retries de rede não cobrem duas vezes.\n\nExceção: as rotas da [Base de conhecimento](/ferramentas/base-de-conhecimento#limites) ignoram o header, porque já são idempotentes (conteúdo repetido não é reindexado nem cobrado)."},{"anchor":"rate-limit","heading":"Rate limit","markdown":"Cada token de API pode fazer **<Limit name=\"rateLimitPerMinute\" /> requisições por minuto** nas rotas `/v1/tools/*`. Toda resposta informa a situação:\n\n```http\nX-RateLimit-Limit: 60\nX-RateLimit-Remaining: 42\nX-RateLimit-Reset: 1712345678\n```\n\nAo exceder, a API retorna `rate_limit.exceeded` (429). Aguarde até `X-RateLimit-Reset` antes de reenviar."},{"anchor":"retry","heading":"Repetir com espera (retry)","markdown":"Repita só o que pode dar certo na tentativa seguinte:\n\n* **`429`**: espere até o horário de `X-RateLimit-Reset` (segundos Unix).\n* **`5xx` e falhas de rede**: espere cada vez mais, com um pouco de aleatoriedade (*backoff* exponencial com *jitter*): cerca de 1 s, 2 s, 4 s… até 30 s, no máximo 5 tentativas.\n* **Demais `4xx`** (validação, escopo, saldo): não repita. A resposta não muda até você corrigir a requisição.\n\nEm `POST` que cria ou cobra algo, envie **a mesma** `X-Idempotency-Key` em todas as tentativas: se a primeira chegou e só a resposta se perdeu, a repetição devolve o resultado original sem cobrar de novo.\n\n<Tabs groupId=\"code-language\" persist items={['JavaScript', 'Python']}>\n  <Tab value=\"JavaScript\">\n    ```js\n    // Repete 429, 5xx e falhas de rede; devolve na hora os demais erros.\n    async function withRetry(send, attempts = 5) {\n      for (let attempt = 1; ; attempt++) {\n        let response;\n        try {\n          response = await send();\n        } catch (error) {\n          if (attempt === attempts) throw error; // falha de rede\n        }\n        if (response && response.status !== 429 && response.status < 500) return response;\n        if (attempt === attempts) return response;\n\n        const reset = Number(response?.headers.get('X-RateLimit-Reset'));\n        const wait =\n          response?.status === 429 && reset\n            ? Math.max(0, reset * 1000 - Date.now())\n            : Math.min(2 ** (attempt - 1) * 1000, 30_000) * (0.5 + Math.random());\n        await new Promise((resolve) => setTimeout(resolve, wait));\n      }\n    }\n\n    const key = crypto.randomUUID(); // a mesma chave em todas as tentativas\n    const response = await withRetry(() =>\n      fetch(url, {\n        method: 'POST',\n        headers: {\n          'X-API-Token': process.env.RESPAWN_API_TOKEN,\n          'X-Idempotency-Key': key,\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify(payload),\n      }),\n    );\n    ```\n  </Tab>\n\n  <Tab value=\"Python\">\n    ```python\n    import os, random, time, uuid\n    import requests\n\n    # Repete 429, 5xx e falhas de rede; devolve na hora os demais erros.\n    def with_retry(send, attempts=5):\n        for attempt in range(1, attempts + 1):\n            try:\n                response = send()\n            except (requests.ConnectionError, requests.Timeout):\n                if attempt == attempts:\n                    raise\n                response = None\n            if response is not None and response.status_code != 429 and response.status_code < 500:\n                return response\n            if attempt == attempts:\n                return response\n            reset = response.headers.get(\"X-RateLimit-Reset\") if response is not None else None\n            if response is not None and response.status_code == 429 and reset:\n                wait = max(0, int(reset) - time.time())\n            else:\n                wait = min(2 ** (attempt - 1), 30) * (0.5 + random.random())\n            time.sleep(wait)\n\n    key = str(uuid.uuid4())  # a mesma chave em todas as tentativas\n    response = with_retry(lambda: requests.post(\n        url,\n        json=payload,\n        headers={\"X-API-Token\": os.environ[\"RESPAWN_API_TOKEN\"], \"X-Idempotency-Key\": key},\n        timeout=30,\n    ))\n    ```\n  </Tab>\n</Tabs>"},{"anchor":"franquia-diaria","heading":"Franquia diária de leituras","markdown":"Leituras feitas com **token de API** (listagens, status, analytics, downloads…) entram em uma franquia diária compartilhada entre todas as ferramentas:\n\n| Item                    | Valor                                  |\n| ----------------------- | -------------------------------------- |\n| Leituras grátis por dia | **<Limit name=\"dailyQuota\" />**        |\n| Contas premium          | **<Limit name=\"dailyQuotaPremium\" />** |\n| Reset                   | <Limit name=\"quotaReset\" />            |\n\nToda leitura medida devolve a posição na franquia:\n\n```http\nX-API-Quota-Limit: 2000\nX-API-Quota-Used: 847\nX-API-Quota-Remaining: 1153\n```\n\nRequisições feitas pelo [dashboard](https://dashboard.respawntech.dev) **não** entram na franquia nem são cobradas."},{"anchor":"leituras-medidas","heading":"Leituras medidas","markdown":"Depois de esgotar a franquia, cada leitura via API debita:\n\n<MeteredReadsTable />\n\nOperações de escrita sem custo próprio (`PATCH`, ativar, desativar, `DELETE`) e o dry-run de criação são sempre gratuitos.\n\n<Callout type=\"idea\" title=\"Evite polling\">\n  A franquia cobre a maioria das integrações. Para acompanhar mudanças de estado, prefira [webhooks](/webhooks) em vez de consultar o status repetidamente.\n</Callout>"},{"anchor":"timeouts","heading":"Timeouts","markdown":"* Maioria das rotas: **<Limit name=\"requestTimeout\" />**.\n* Downloads de arquivo: **<Limit name=\"downloadTimeout\" />**."}]},{"slug":"erros","url":"https://docs.respawntech.dev/erros","title":"Códigos de erro","description":"Envelope de resposta, formato de erro e referência de todos os códigos retornados pelas ferramentas.","sections":[{"anchor":"resposta-de-sucesso","heading":"Resposta de sucesso","markdown":"```json\n{\n  \"success\": true,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"data\": {},\n  \"meta\": {}\n}\n```\n\n| Campo      | Descrição                                        |\n| ---------- | ------------------------------------------------ |\n| `success`  | `true` em respostas OK                           |\n| `trace_id` | ID de correlação (também no header `X-Trace-Id`) |\n| `data`     | Recurso ou resultado                             |\n| `meta`     | Opcional — custo (`meta.cost`), paginação etc.   |\n\nCom `X-Include-Cost: true` ou em dry-run, `meta` traz o custo da operação — veja os formatos em [Dry-run](/cobranca-e-limites#dry-run). Listagens trazem a paginação dentro de `data` (`total` e, no PDF, `page` e `page_size`), não em `meta`."},{"anchor":"formato-de-erro","heading":"Formato de erro","markdown":"```json\n{\n  \"success\": false,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"error\": {\n    \"code\": \"billing.insufficient_credits\",\n    \"message\": \"Créditos insuficientes para esta operação.\",\n    \"details\": { \"required\": 100, \"available\": 50 }\n  }\n}\n```\n\n| Campo           | Descrição                                                                                       |\n| --------------- | ----------------------------------------------------------------------------------------------- |\n| `error.code`    | Identificador estável — trate erros por ele, nunca pela `message`                               |\n| `error.message` | Texto legível; pode mudar entre versões                                                         |\n| `error.details` | Contexto estruturado opcional; em `validation.failed`, `details.fields` traz os erros por campo |\n| `trace_id`      | Informe em tickets de suporte                                                                   |"},{"anchor":"erros-comuns-a-todas-as-ferramentas","heading":"Erros comuns a todas as ferramentas","markdown":""},{"anchor":"autenticação-e-permissões","heading":"Autenticação e permissões","markdown":"<ErrorTable group=\"auth\" />"},{"anchor":"créditos-e-conta","heading":"Créditos e conta","markdown":"<ErrorTable group=\"billing\" />"},{"anchor":"requisição-e-limites","heading":"Requisição e limites","markdown":"<ErrorTable group=\"request\" />"},{"anchor":"agentes-de-ia-mcp","heading":"Agentes de IA (MCP)","markdown":"<ErrorTable group=\"agents\" />"},{"anchor":"plataforma","heading":"Plataforma","markdown":"<ErrorTable group=\"platform\" />"},{"anchor":"vídeo","heading":"Vídeo","markdown":"<ErrorTable group=\"video\" />"},{"anchor":"erros-por-ferramenta","heading":"Erros por ferramenta","markdown":"<ToolErrorSections />\n\nJobs assíncronos (PDF) também expõem `error_code` no próprio recurso quando `status` é `failed`; códigos marcados como **assíncrono** aparecem apenas ali."},{"anchor":"boas-práticas","heading":"Boas práticas","markdown":"1. Trate `402` com um fluxo de recarga no seu sistema.\n2. Em `429`, respeite `X-RateLimit-Reset` antes de tentar de novo.\n3. Use `X-Dry-Run` para capturar `validation.failed` sem custo.\n4. Repita apenas erros `5xx` e de rede, sempre com a mesma `X-Idempotency-Key`.\n5. Registre o `trace_id` de toda resposta com erro."}]},{"slug":"","url":"https://docs.respawntech.dev/","title":"Documentação da API","description":"Integre as ferramentas da Respawn Tech Dev com uma API REST previsível — autenticação por token, cobrança em créditos e dry-run em todas as operações pagas.","sections":[{"anchor":"ferramentas","heading":"Ferramentas","markdown":"<ToolCards />"},{"anchor":"primeiros-passos","heading":"Primeiros passos","markdown":"<Steps>\n  <Step>\n    ### Crie um token de API\n\n    No [dashboard](https://dashboard.respawntech.dev), abra **APIs** e crie um token com os escopos da ferramenta que vai usar. Guarde-o na variável de ambiente `RESPAWN_API_TOKEN`. Veja [Autenticação](/autenticacao).\n  </Step>\n\n  <Step>\n    ### Estime o custo com dry-run\n\n    Envie `X-Dry-Run: true` antes de uma operação paga para validar o payload e ver o custo sem debitar créditos. Veja [Cobrança e limites](/cobranca-e-limites#dry-run).\n  </Step>\n\n  <Step>\n    ### Faça a chamada\n\n    Todas as rotas de ferramentas usam o prefixo `https://api.respawntech.dev/v1/tools`. Cada endpoint traz exemplos prontos em cURL, JavaScript e Python.\n\n    <ApiExamples method=\"GET\" path=\"/v1/tools/image/qr-code/dynamic\" query={{ limit: '20' }} />\n  </Step>\n</Steps>\n\n## Headers comuns\n\n| Header              | Valor          | Efeito                                                                 |\n| ------------------- | -------------- | ---------------------------------------------------------------------- |\n| `X-API-Token`       | `rsk_live_...` | Autenticação (recomendado)                                             |\n| `X-Dry-Run`         | `true`         | Simula a operação sem persistir nem debitar                            |\n| `X-Include-Cost`    | `true`         | Inclui `meta.cost` na resposta                                         |\n| `X-Idempotency-Key` | UUID           | Evita cobrança duplicada em reenvios (<Limit name=\"idempotencyTtl\" />) |\n\nRespostas trazem `X-Trace-Id` (use em tickets de suporte), `X-RateLimit-*` e, em leituras medidas, `X-API-Quota-*`.\n\n## Guias\n\n<Cards>\n  <Card title=\"Autenticação\" description=\"Token de API, escopos e requisitos da conta.\" href=\"/autenticacao\" />\n\n  <Card title=\"Cobrança e limites\" description=\"Créditos, dry-run, idempotência, rate limit e franquia diária.\" href=\"/cobranca-e-limites\" />\n\n  <Card title=\"Webhooks\" description=\"Eventos da conta, assinatura Standard Webhooks, retries e callbacks de jobs.\" href=\"/webhooks\" />\n\n  <Card title=\"Códigos de erro\" description=\"Envelope de resposta e todos os códigos por ferramenta.\" href=\"/erros\" />\n\n  <Card title=\"Agentes de IA (MCP)\" description=\"Conecte Claude, Cursor e outros agentes às ferramentas, com limite de gasto.\" href=\"/agentes-de-ia\" />\n</Cards>"}]},{"slug":"usar-com-ia","url":"https://docs.respawntech.dev/usar-com-ia","title":"Usar com IA","description":"Dê ao seu assistente de IA acesso a esta documentação pelo MCP de documentação (público, grátis, sem token), por llms.txt ou pelas páginas em Markdown.","sections":[{"anchor":"top","heading":"","markdown":"Assistentes de IA escrevem integrações melhores quando consultam a documentação em vez de adivinhar. Há três formas de entregar estas páginas a um agente, da mais completa à mais simples: o **MCP de documentação**, o **llms.txt** e as **páginas em Markdown**."},{"anchor":"dois-servidores","heading":"Dois servidores MCP","markdown":"|              | MCP de documentação                                    | [MCP da conta](/agentes-de-ia)                                                    |\n| ------------ | ------------------------------------------------------ | --------------------------------------------------------------------------------- |\n| Para quê     | Responder perguntas sobre a API enquanto você programa | Usar as ferramentas com a sua conta (criar QR, converter PDF, transcrever áudio…) |\n| URL          | <McpUrl server=\"docs\" />                               | <McpUrl />                                                                        |\n| Autenticação | Nenhuma                                                | Token de API com `mcp:use`                                                        |\n| Custo        | Grátis                                                 | O mesmo da API                                                                    |\n| Acesso       | Só leitura da documentação pública                     | Age na conta, com escopos e limite de gasto                                       |\n\nOs dois podem ficar conectados ao mesmo tempo: um agente consulta a documentação e depois usa o MCP da conta para executar.\n\n<Callout title=\"Liberação gradual\">\n  O MCP de documentação está sendo liberado aos poucos. Enquanto ele não estiver ativo, <McpUrl server=\"docs\" /> responde `404` com o código `mcp.disabled`. O llms.txt e as páginas em Markdown já funcionam.\n</Callout>"},{"anchor":"conexao","heading":"Conexão","markdown":"|              |                                                                           |\n| ------------ | ------------------------------------------------------------------------- |\n| URL          | <McpUrl server=\"docs\" />                                                  |\n| Transporte   | Streamable HTTP                                                           |\n| Autenticação | Nenhuma (não envie token)                                                 |\n| Custo        | Grátis                                                                    |\n| Limite       | <Limit name=\"mcpDocsRateLimitPerMinute\" /> requisições por minuto por IP  |\n| Idioma       | Parâmetro `lang` opcional em cada ferramenta: `pt-br`, `en`, `es` ou `fr` |\n\nAs respostas vêm do mesmo conteúdo deste site, incluindo os preços em vigor."},{"anchor":"clientes","heading":"Configure o cliente","markdown":"<McpDocsClientSnippets />\n\nNo **Claude.ai** e no **ChatGPT**, adicione um conector personalizado (custom connector) e informe só a URL <McpUrl server=\"docs\" />. Não é preciso login nem token. Clientes que só falam stdio usam a ponte `mcp-remote`, como no último exemplo."},{"anchor":"ferramentas","heading":"Ferramentas","markdown":"<McpDocsToolsTable />\n\nUm fluxo típico: `docs_search` com a pergunta, `docs_get_page` na seção encontrada e `docs_get_endpoint` ou `docs_get_pricing` para os detalhes exatos antes de escrever o código."},{"anchor":"llms-txt","heading":"Sem MCP: llms.txt e Markdown","markdown":"Para agentes que só leem a web:\n\n* [`/llms.txt`](/llms.txt): índice no formato [llmstxt.org](https://llmstxt.org), com cada página em cada idioma e o link para a versão em Markdown.\n* [`/llms-full.txt`](/llms-full.txt): todas as páginas em um único arquivo de texto.\n* Páginas em Markdown: `/md/<página>.md` em português e `/md/<idioma>/<página>.md` nos outros idiomas, por exemplo [`/md/webhooks.md`](/md/webhooks.md) ou [`/md/en/webhooks.md`](/md/en/webhooks.md). A página inicial é `/md/index.md`.\n* [`/ai/bundle.json`](/ai/bundle.json): a documentação inteira em JSON estruturado (seções, ferramentas, endpoints, preços, erros, escopos e limites). É a base do MCP de documentação."}]},{"slug":"webhooks","url":"https://docs.respawntech.dev/webhooks","title":"Webhooks","description":"Como a plataforma envia webhooks — eventos da conta, callbacks de jobs e Webhook Relay — e como validar a assinatura Standard Webhooks.","sections":[{"anchor":"top","heading":"","markdown":"A plataforma faz um `POST` na sua URL quando algo acontece. Todos os envios passam pelo mesmo motor de entrega: a mensagem é gravada, cada destino recebe uma entrega com retries próprios e cada chamada HTTP fica registrada. Existem três origens:\n\n| Origem                                           | Onde se configura                                              | Custo                               |\n| ------------------------------------------------ | -------------------------------------------------------------- | ----------------------------------- |\n| **Eventos da conta** (ex.: `qr.dynamic.scanned`) | **Webhooks** no [dashboard](https://dashboard.respawntech.dev) | Grátis                              |\n| **Callbacks de jobs** (ex.: conversão de PDF)    | `webhook_url` em cada requisição                               | Grátis                              |\n| **Webhook Relay** (seus próprios eventos)        | API do [Webhook Relay](/ferramentas/webhook-relay)             | Por entrega além da franquia mensal |"},{"anchor":"eventos","heading":"Eventos da conta","markdown":"Cadastre um endpoint em **Webhooks** no [dashboard](https://dashboard.respawntech.dev) e escolha os eventos. Cada endpoint tem o seu `secret`, mostrado na criação e depois pela opção de revelar o secret.\n\nEsses endpoints são do [Webhook Relay](/ferramentas/webhook-relay#projetos): um endpoint de qualquer projeto que assine um evento da conta o recebe, grátis. A antiga API de webhooks da conta está obsoleta; veja [API legada da conta](/ferramentas/webhook-relay#api-legada).\n\n<WebhookEventsTable />"},{"anchor":"requisicao","heading":"Requisição enviada","markdown":"```http\nPOST <url do endpoint>\nContent-Type: application/json\nUser-Agent: RespawnTech-Webhook/2.0\nwebhook-id: msg_2Vf8tK1pQ9zX\nwebhook-timestamp: 1790338000\nwebhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=\nX-Webhook-Signature: sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd\n```\n\n| Header                | Conteúdo                                                                                                 |\n| --------------------- | -------------------------------------------------------------------------------------------------------- |\n| `webhook-id`          | Identificador da mensagem. É o **mesmo em todos os retries**: use para descartar duplicatas.             |\n| `webhook-timestamp`   | Momento do envio desta tentativa, em segundos Unix.                                                      |\n| `webhook-signature`   | `v1,<base64>`, uma assinatura por secret ativo, separadas por espaço (duas durante uma troca de secret). |\n| `X-Webhook-Signature` | Assinatura legada `sha256=<hex>`, sem timestamp. Mantida para receptores antigos.                        |\n\nOs headers seguem o padrão [Standard Webhooks](https://www.standardwebhooks.com), então qualquer biblioteca desse padrão valida a requisição. Endpoints do Webhook Relay podem ter headers extras (`custom_headers`)."},{"anchor":"corpo","heading":"Corpo","markdown":"Eventos da conta e mensagens do Webhook Relay usam o mesmo envelope:\n\n```json\n{\n  \"event_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"type\": \"qr.dynamic.scanned\",\n  \"timestamp\": 1790338000,\n  \"payload\": {\n    \"resource_id\": \"…\",\n    \"slug\": \"promo-verao\",\n    \"ip\": \"hash_anônimo\",\n    \"user_agent\": \"Mozilla/5.0 …\",\n    \"referer\": \"https://…\"\n  }\n}\n```\n\nO formato de `payload` varia conforme o `type`. Callbacks de jobs mantêm o corpo próprio (veja [Callback de jobs](#pdf-callback)), e mensagens recebidas de provedores pelo Webhook Relay chegam com o corpo original do provedor, sem alteração."},{"anchor":"verificar-assinatura","heading":"Validar a assinatura","markdown":"O conteúdo assinado é `webhook-id + \".\" + webhook-timestamp + \".\" + corpo bruto`, com HMAC-SHA256 e o resultado em base64. Para validar:\n\n1. Leia o **corpo bruto**, antes de qualquer parse de JSON (reformatar o JSON muda a assinatura).\n2. Recuse timestamps com mais de <Limit name=\"webhookSignatureTolerance\" /> de diferença do seu relógio (proteção contra replay).\n3. Calcule a assinatura e compare, **em tempo constante**, com cada item de `webhook-signature`. Basta um bater.\n\nCom uma biblioteca Standard Webhooks, use o `standard_webhooks_secret` (formato `whsec_…`). Sem biblioteca, a chave do HMAC são os bytes do `secret` bruto.\n\n<Tabs groupId=\"code-language\" persist items={['JavaScript', 'Python', 'Go']}>\n  <Tab value=\"JavaScript\">\n    ```js\n    // npm i standardwebhooks\n    import express from 'express';\n    import { Webhook } from 'standardwebhooks';\n\n    const wh = new Webhook(process.env.WEBHOOK_SECRET); // whsec_…\n    const app = express();\n\n    app.post('/hook', express.raw({ type: '*/*' }), (req, res) => {\n      let event;\n      try {\n        event = wh.verify(req.body, req.headers); // lança erro se inválido\n      } catch {\n        return res.sendStatus(401);\n      }\n      // processe `event` de forma assíncrona e responda rápido\n      res.sendStatus(204);\n    });\n    ```\n  </Tab>\n\n  <Tab value=\"Python\">\n    ```python\n    # pip install standardwebhooks\n    import os\n    from flask import Flask, request\n    from standardwebhooks.webhooks import Webhook\n\n    wh = Webhook(os.environ[\"WEBHOOK_SECRET\"])  # whsec_…\n    app = Flask(__name__)\n\n    @app.post(\"/hook\")\n    def hook():\n        try:\n            event = wh.verify(request.get_data(), dict(request.headers))\n        except Exception:\n            return \"\", 401\n        # processe `event` de forma assíncrona e responda rápido\n        return \"\", 204\n    ```\n  </Tab>\n\n  <Tab value=\"Go\">\n    ```go\n    // Sem biblioteca: secret é o valor bruto (não o whsec_).\n    func verify(secret string, r *http.Request, body []byte) bool {\n    \tid, ts := r.Header.Get(\"webhook-id\"), r.Header.Get(\"webhook-timestamp\")\n    \tt, err := strconv.ParseInt(ts, 10, 64)\n    \tif err != nil || math.Abs(time.Since(time.Unix(t, 0)).Seconds()) > 300 {\n    \t\treturn false\n    \t}\n    \tmac := hmac.New(sha256.New, []byte(secret))\n    \tmac.Write([]byte(id + \".\" + ts + \".\"))\n    \tmac.Write(body)\n    \twant := \"v1,\" + base64.StdEncoding.EncodeToString(mac.Sum(nil))\n    \tfor _, sig := range strings.Fields(r.Header.Get(\"webhook-signature\")) {\n    \t\tif hmac.Equal([]byte(sig), []byte(want)) {\n    \t\t\treturn true\n    \t\t}\n    \t}\n    \treturn false\n    }\n    ```\n  </Tab>\n</Tabs>\n\n<Callout title=\"Receptores antigos\">\n  Quem já valida `X-Webhook-Signature` continua funcionando: é `sha256=` + o HMAC-SHA256 em hexadecimal do corpo bruto, com o `secret` bruto como chave. Esse header não tem timestamp; prefira `webhook-signature` em integrações novas.\n</Callout>"},{"anchor":"secrets","heading":"Secrets e rotação","markdown":"| Destino                          | Onde obter o secret                                                                                                                                 |\n| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Endpoint de eventos da conta     | Na criação, ou pela opção de revelar o secret em **Webhooks** no dashboard.                                                                         |\n| Endpoint do Webhook Relay        | Na criação, ou em [Revelar secret](/ferramentas/webhook-relay#webhooks-endpoints-secret).                                                           |\n| Callback de jobs (`webhook_url`) | Secret de assinatura da conta, único para todos os callbacks: `GET /v1/account/webhooks/signing-secret` (rota da conta, com a sessão do dashboard). |\n\nTodo secret aparece em duas formas: `secret` (bruto) e `standard_webhooks_secret` (`whsec_` + base64 dos mesmos bytes). As duas validam as mesmas assinaturas.\n\nAo [trocar o secret](/ferramentas/webhook-relay#webhooks-endpoints-rotate_secret) de um endpoint, o antigo continua assinando por <Limit name=\"webhookSecretOverlap\" />: nesse período `webhook-signature` traz as duas assinaturas, e você atualiza o receptor sem perder eventos."},{"anchor":"retries","heading":"Entrega e retries","markdown":"Só uma resposta **2xx** conta como sucesso. Redirecionamentos (3xx) não são seguidos, e 3xx, 4xx, 5xx, timeouts e erros de rede são tentados de novo neste calendário:\n\n| Tentativa | Espera antes dela |\n| --------- | ----------------- |\n| 1         | —                 |\n| 2         | 5 segundos        |\n| 3         | 5 minutos         |\n| 4         | 30 minutos        |\n| 5         | 2 horas           |\n| 6         | 5 horas           |\n| 7 e 8     | 10 horas          |\n\nSão cerca de 27 horas no total, com variação de ±10%. Casos especiais:\n\n* **`429` e `503`** com `Retry-After`: a próxima tentativa respeita o header quando ele pede mais tempo (até 10 horas).\n* **`410 Gone`**: a entrega para e o endpoint é desativado.\n* Depois da última tentativa, a entrega fica `exhausted`. No Webhook Relay, dá para [reenviar a mensagem](/ferramentas/webhook-relay#webhooks-messages-retry) ou [recuperar as falhas de um endpoint](/ferramentas/webhook-relay#webhooks-endpoints-recover).\n\nBoas práticas no receptor:\n\n* Responda **2xx** o mais rápido possível e processe o evento de forma assíncrona.\n* **Descarte duplicatas pelo `webhook-id`**: uma entrega pode chegar mais de uma vez (ex.: seu servidor processou, mas a resposta se perdeu).\n* Não dependa da ordem de chegada; use o `timestamp` do corpo quando a ordem importar."},{"anchor":"desativacao","heading":"Desativação automática","markdown":"Um endpoint que falha sem parar por <Limit name=\"webhookDisableAfter\" /> é desativado automaticamente (`is_active: false`, com `disabled_reason`), assim como um que responde `410`. Depois de corrigir o receptor, reative o endpoint (no dashboard ou com `is_active: true` no Webhook Relay); isso zera a sequência de falhas.\n\nO histórico de mensagens, entregas e tentativas fica guardado por <Limit name=\"webhookLogRetention\" />."},{"anchor":"pdf-callback","heading":"Callback de jobs","markdown":"Em `POST /convert` ou `POST /batch` do [PDF Convert](/ferramentas/pdf-convert), envie `webhook_url` no body. Quando o job termina, a API faz um `POST` nessa URL:\n\n```json\n{\n  \"job_id\": \"job_abc123\",\n  \"success\": true,\n  \"result_url\": \"https://…\",\n  \"metadata\": { \"conversion_id\": \"…\" }\n}\n```\n\nEm falha, `success` é `false` e o campo `error` traz a mensagem. O callback tem os mesmos headers, assinatura e retries dos outros webhooks, assinado com o **secret de assinatura da conta** (veja [Secrets e rotação](#secrets)). A `webhook_url` precisa ser uma URL pública `http` ou `https`; caso contrário a requisição é recusada com `webhook.invalid_url`."},{"anchor":"webhook-relay","heading":"Seus próprios eventos","markdown":"Para enviar webhooks da **sua** aplicação para os seus clientes, com a mesma assinatura, retries e log, ou para receber webhooks do Stripe, GitHub e outros e redistribuí-los, use o [Webhook Relay](/ferramentas/webhook-relay)."}]},{"slug":"ferramentas/audio","url":"https://docs.respawntech.dev/ferramentas/audio","title":"Áudio","description":"Transcrição, voz sintética, efeitos e músicas, análise, conversão, resumo, tradução e revoice de áudio, por REST ou MCP, com jobs assíncronos e preço por minuto.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"audio\" />"},{"anchor":"visao-geral","heading":"Visão geral","markdown":"Toda operação de áudio é **assíncrona** e cobrada na hora:\n\n1. `POST` na operação → **202 Accepted** com `job_id`, `status: pending` e `poll_after_seconds`.\n2. Um worker processa em background.\n3. Consulte `GET /jobs/{id}` até o `status` ser `completed` (ou `failed`, e a cobrança é devolvida). Pronto, a resposta traz o resultado e um **link assinado** que expira.\n\nOs mesmos casos de uso estão no [MCP](/agentes-de-ia) como as tools `audio_*` (toolset `audio`); a REST e o MCP compartilham formatos, preços e limites.\n\n<Callout type=\"warn\" title=\"Cobrança ao aceitar o job\">\n  Os créditos são debitados quando o job é aceito. Falha do motor, etapa que não foi necessária e preço calculado a mais são **devolvidos** automaticamente; nada é cobrado se o pedido for recusado antes (`audio.too_long`, `audio.price_above_max`…).\n</Callout>\n\n<EndpointList tool=\"audio\" />"},{"anchor":"entrada","heading":"Entrada do áudio","markdown":"As operações que recebem um áudio pedem **exatamente um** destes campos:\n\n| Campo          | O que é                                                                                                                                                                    |\n| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `audio_url`    | URL pública `http(s)` do arquivo. Endereços de rede interna ou privada são recusados (`audio.input_blocked`) e o download tem limite de <Limit name=\"audioFetchTimeout\" /> |\n| `audio_base64` | O áudio inline em base64, até <Limit name=\"audioInlineMax\" /> decodificado                                                                                                 |\n| `file_id`      | Id de um arquivo de áudio já enviado para a conta                                                                                                                          |\n\nFormatos aceitos: mp3, wav, ogg, m4a, flac e webm. O limite é de <Limit name=\"audioMaxDuration\" /> de áudio e <Limit name=\"audioInputMax\" /> por arquivo por chamada. Em `summarize` e `translate` você pode mandar, no lugar do áudio, o `job_id` de uma transcrição já pronta: a transcrição é reaproveitada e só a operação nova é cobrada.\n\nTodas as rotas de operação aceitam `max_credits` (em unidades; 10.000 unidades = 1 crédito): se o preço passar disso, o pedido é recusado com `audio.price_above_max` e nada é cobrado. O header `Idempotency-Key` torna a repetição segura: a mesma chave devolve o primeiro job em vez de cobrar de novo."},{"anchor":"custos","heading":"Custos","markdown":"<PricingTable tool=\"audio\" group=\"speech-to-text\" />\n\n<PricingTable tool=\"audio\" group=\"text-to-speech\" />\n\n<PricingTable tool=\"audio\" group=\"processing\" />\n\n<PricingTable tool=\"audio\" group=\"revoice\" />\n\nO \"minuto iniciado\" arredonda para cima. Um áudio cuja duração não dá para ler do cabeçalho é cobrado pelo teto de <Limit name=\"audioMaxDuration\" /> e a diferença volta no fim do job. Consultas aos jobs seguem a [franquia diária](/cobranca-e-limites#franquia-diaria). O preço exato de uma chamada sai do [dry-run](#dry-run)."},{"anchor":"dry-run","heading":"Dry-run","markdown":"Com `?dry_run=true` na query (nas rotas de áudio é a query, não o header `X-Dry-Run`), as operações pagas respondem `200` com o preço e não cobram nem criam job:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"service_code\": \"audio_transcribe_minute\",\n    \"units\": 3,\n    \"rate\": 15000,\n    \"total_units\": 45000,\n    \"duration_ms\": 125000,\n    \"duration_known\": true,\n    \"parts\": [{ \"service_code\": \"audio_transcribe_minute\", \"units\": 3, \"rate\": 15000, \"total\": 45000 }]\n  }\n}\n```\n\n`rate` e `total_units` estão em unidades (10.000 unidades = 1 crédito); `parts` detalha cada serviço cobrado. Em `speak`, o orçamento traz `chars`, `voice`, `language` e `format`."},{"anchor":"operações","heading":"Operações","markdown":""},{"anchor":"transcrever-áudio","heading":"Transcrever áudio","markdown":"<Endpoint id=\"audio.transcribe\" example={{ body: `{ \"audio_url\": \"https://example.com/episodio-12.mp3\", \"format\": \"text\", \"language\": \"pt\" }` }}>\n  `format`: `text` (padrão), `srt` ou `vtt`. `language` é opcional (código ISO 639-1; sem ele o idioma é detectado). Com `diarize: true` (ou `speakers`, o número conhecido de falantes) cada trecho diz quem falou (`Speaker 1`, `Speaker 2`…), por um adicional por minuto.\n</Endpoint>"},{"anchor":"gerar-fala-a-partir-de-texto","heading":"Gerar fala a partir de texto","markdown":"<Endpoint id=\"audio.speak\" example={{ body: `{ \"text\": \"Olá! Seu pedido saiu para entrega.\", \"voice\": \"pt-BR-dora\", \"format\": \"mp3\", \"speed\": 1 }` }}>\n  `text` aceita até <Limit name=\"audioTtsCharsMax\" /> caracteres (acima disso o pedido é recusado antes de cobrar). `voice` é um id do [catálogo](#vozes-do-catalogo) ou uma voz clonada da conta (`vc_…`, [só no MCP](#vozes-clonadas)); sem `voice`, vale a voz padrão do `language` (`pt-BR`, `en-US`, `en-GB`, `es-ES`, `fr-FR` ou `it-IT`). `format`: `mp3` (padrão) ou `wav`; `speed`: 0,5 a 2.\n</Endpoint>"},{"anchor":"vozes-do-catalogo","heading":"Listar as vozes","markdown":"<Endpoint id=\"audio.voices\" example={{ query: { language: 'pt-BR' } }}>\n  Catálogo fechado de vozes do `speak` e do `revoice`. `language` filtra por idioma. Não é cobrado.\n</Endpoint>"},{"anchor":"gerar-efeito-sonoro-ou-música","heading":"Gerar efeito sonoro ou música","markdown":"<Endpoint id=\"audio.generate\" example={{ body: `{ \"kind\": \"effect\", \"prompt\": \"porta de madeira rangendo devagar\", \"duration_seconds\": 4, \"format\": \"mp3\" }` }}>\n  `kind`: `effect` (1 a <Limit name=\"audioEffectMax\" />, padrão 5 s) ou `music` (instrumental, 1 a <Limit name=\"audioMusicMax\" />, padrão 30 s). O áudio sai com exatamente `duration_seconds`. Não há fala nem canto, e descrições que imitam artistas, vozes ou obras existentes são recusadas antes de cobrar (`audio.input_blocked`). `seed` repete um som: o mesmo pedido com a `seed` que um job informou gera o mesmo áudio. A geração leva alguns minutos.\n</Endpoint>"},{"anchor":"analisar-áudio","heading":"Analisar áudio","markdown":"<Endpoint id=\"audio.analyze\" example={{ body: `{ \"audio_url\": \"https://example.com/episodio-12.mp3\" }` }}>\n  Sem IA de transcrição: devolve em `analysis` a duração, o formato e codec, bitrate, taxa de amostragem, canais, loudness integrado (LUFS), pico real, faixa de loudness, os trechos de silêncio (`start_ms`/`end_ms`) e o idioma falado. Preço fixo por chamada.\n</Endpoint>"},{"anchor":"converter-ou-normalizar-áudio","heading":"Converter ou normalizar áudio","markdown":"<Endpoint id=\"audio.convert\" example={{ body: `{ \"audio_url\": \"https://example.com/episodio-12.wav\", \"format\": \"mp3\", \"bitrate_kbps\": 128, \"normalize\": true }` }}>\n  `format` (obrigatório): `mp3`, `wav`, `ogg`, `m4a` ou `flac`. `bitrate_kbps` (32 a 320) vale só para formatos com perda. `normalize: true` nivela o loudness em -14 LUFS (padrão de streaming, -1,5 dBTP). As tags de metadados são removidas.\n</Endpoint>"},{"anchor":"resumir-áudio","heading":"Resumir áudio","markdown":"<Endpoint id=\"audio.summarize\" example={{ body: `{ \"job_id\": \"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64\", \"output_language\": \"pt-BR\" }` }}>\n  Mande o `job_id` de uma transcrição pronta (o áudio não é processado de novo) ou um áudio novo (transcrito primeiro e cobrado junto). O resultado vem em `summary`: o resumo e os tópicos em ordem (`title`, `start_ms`, `points`), mais um Markdown para baixar. O texto da transcrição (nunca o áudio) é enviado ao modelo de linguagem da plataforma.\n</Endpoint>"},{"anchor":"traduzir-áudio","heading":"Traduzir áudio","markdown":"<Endpoint id=\"audio.translate\" example={{ body: `{ \"job_id\": \"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64\", \"target_language\": \"en\", \"format\": \"srt\" }` }}>\n  `target_language` é obrigatório. A tradução sai em texto, SRT ou VTT com os tempos originais (e os falantes, se a transcrição tinha `diarize`); `source_language` informa de qual idioma ela veio. Não gera áudio dublado: para isso, use o [revoice](#revoice).\n</Endpoint>"},{"anchor":"revoice","heading":"Falar a gravação de novo com outra voz","markdown":"<Endpoint id=\"audio.revoice\" example={{ body: `{ \"audio_url\": \"https://example.com/aviso.mp3\", \"voice_id\": \"pt-BR-alex\", \"format\": \"mp3\" }` }}>\n  Num só job: transcreve a gravação, traduz o texto quando a voz fala outro idioma e sintetiza o resultado. Informe `voice_id` (do catálogo) ou `language` (usa a voz padrão dele). O áudio novo não mantém o tempo nem o ritmo do original (sem sincronia labial), e só vozes do catálogo ou clonadas da própria conta valem: nada de imitar uma pessoa real. O resultado traz o texto falado (`transcript` e `text_download_url`) e o que cada etapa custou em `revoice.stages`.\n</Endpoint>"},{"anchor":"listar-jobs","heading":"Listar jobs","markdown":"<Endpoint id=\"audio.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  Os jobs mais recentes primeiro, com `limit` (padrão 20, até <Limit name=\"pageSizeMax\" />) e `offset`. A lista traz só o status; o resultado e o link ficam em `GET /jobs/{id}`. Jobs e arquivos são apagados depois da retenção (<Limit name=\"audioRetention\" />).\n</Endpoint>"},{"anchor":"resultado","heading":"Status, resultado e download","markdown":"<Endpoint id=\"audio.get\">\n  Não há rota de download separada: o resultado e o arquivo vêm deste endpoint. Com `status: completed`, a resposta traz o conteúdo pelo tipo de job — `transcript` (também inline até 32 KB; `truncated: true` avisa o corte) para transcrição e tradução, `summary`, `analysis`, `conversion`, `generation` ou `revoice` — e `download_url` com `download_expires_at`. O link é assinado e expira: consulte de novo para receber um link novo. Em `failed`, o `error_code` explica o motivo e a cobrança é devolvida. A leitura é medida na [franquia diária](/cobranca-e-limites#franquia-diaria).\n\n  `status`: `pending`, `processing`, `completed` ou `failed`.\n</Endpoint>"},{"anchor":"vozes-clonadas","heading":"Vozes clonadas e consentimento (só MCP)","markdown":"Enquanto não existir uma rota REST, a clonagem de voz está **somente nas tools MCP** `voice_create`, `voice_list` e `voice_delete` (scope `tools:audio:speak`). A voz clonada serve só à conta que a criou, em `audio_speak` (`voice: \"vc_…\"`, preço de voz clonada) e em `audio_revoice` (`voice_id`).\n\n* **`voice_create`** clona a voz **do titular da conta** a partir de uma amostra de <Limit name=\"audioVoiceSample\" />. O consentimento é obrigatório: chame primeiro com `dry_run` para ler `consent_terms`, `consent_version` e `consent_phrase`; grave a amostra **com você sozinho lendo a frase em voz alta**; então chame com `consent.accepted: true` e a `consent_version`. Amostras sem a frase, com mais de uma voz ou fora da duração são recusadas e o valor é devolvido (`audio.voice_sample_rejected`). Sem o aceite, a resposta é `audio.consent_required`. É assíncrono: acompanhe com `voice_list` até a voz ficar `completed`.\n* **`voice_list`** lista as vozes da conta (`voice_id`, nome, idioma, `status`, validade). Grátis.\n* **`voice_delete`** apaga a voz de verdade: a amostra criptografada e o registro de consentimento são destruídos e a voz para de funcionar na hora (jobs na fila nela falham e são devolvidos). Grátis."},{"anchor":"uso-responsavel","heading":"Uso responsável","markdown":"* **Só a sua voz, com o seu consentimento.** Clonar a voz de outra pessoa, mesmo com a gravação em mãos, não é permitido; a verificação da frase falada e de um único falante existe para impedir isso.\n* **Nada de imitação.** Nem a fala, nem o `revoice`, nem a geração de sons aceitam imitar artistas, vozes ou obras existentes.\n* **Limites:** até <Limit name=\"audioVoicesMax\" /> vozes clonadas por conta, <Limit name=\"audioVoiceCreatesPerDay\" /> criações por dia e <Limit name=\"audioVoiceUsesPerHour\" /> usos por hora (`audio.voice_limit`, `audio.voice_rate_limited`).\n* **Dados guardados:** a amostra fica criptografada por até <Limit name=\"audioVoiceSampleRetention\" /> ou até o `voice_delete`; cada criação, recusa, uso e exclusão é auditado, sem conteúdo de voz. O áudio de entrada e de saída dos demais jobs expira em <Limit name=\"audioRetention\" />.\n* **Aviso ao ouvinte:** se você publica áudio sintético, avise que a voz é gerada, principalmente quando for uma voz clonada."},{"anchor":"scopes","heading":"Scopes","markdown":"| Scope                    | Operações                                                                         |\n| ------------------------ | --------------------------------------------------------------------------------- |\n| `tools:audio:transcribe` | `transcribe`, `summarize`, `translate`                                            |\n| `tools:audio:speak`      | `speak`, `generate`, `revoice` (no MCP, também `voice_create` e `voice_delete`)   |\n| `tools:audio:process`    | `analyze`, `convert`                                                              |\n| `tools:audio:read`       | `voices`, `jobs`, `jobs/{id}` (os três scopes acima também leem os próprios jobs) |\n\n`tools:manage` cobre tudo. A lista completa fica em [Autenticação](/autenticacao)."},{"anchor":"erros","heading":"Erros","markdown":"`audio.*` na resposta ou, em jobs que falham depois de aceitos, no `error_code` do job.\n\n<ErrorTable tool=\"audio\" />"}]},{"slug":"ferramentas/base-de-conhecimento","url":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento","title":"Base de conhecimento","description":"Busca híbrida (semântica + palavras-chave) nos seus documentos, com rerank e feedback de relevância, para RAG e assistentes de IA.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"base-de-conhecimento\" />"},{"anchor":"visão-geral","heading":"Visão geral","markdown":"Uma **base** (collection) guarda documentos de texto ou Markdown. Cada documento é dividido em **seções** (pelos títulos) e **trechos** (pedaços de \\~96 tokens), que são indexados de duas formas: por significado (embeddings) e por palavras-chave. A busca combina as duas e devolve os trechos mais relevantes, prontos para você passar a um modelo de linguagem (RAG) ou mostrar ao usuário.\n\n<Steps>\n  <Step>\n    **Crie uma base** com o idioma dos documentos (melhora a busca por palavras-chave).\n  </Step>\n\n  <Step>\n    **Alimente a base.** Envie texto ou Markdown, importe arquivos (PDF, Office, imagens) ou conecte uma fonte que se sincroniza sozinha (site, sitemap, repositório do GitHub, pasta do Google Drive). A indexação é assíncrona: acompanhe o `status` até `ready`. Reenviar com o mesmo `external_id` substitui a versão anterior sem tirar a base do ar.\n  </Step>\n\n  <Step>\n    **Busque** com a pergunta do usuário e use os trechos devolvidos como contexto da resposta.\n  </Step>\n\n  <Step>\n    **Responda com citações** (opcional): `answer` devolve um texto em que cada afirmação aponta para o trecho de origem, ou diz que não sabe. Para o seu site, embuta o [widget](#widget).\n  </Step>\n\n  <Step>\n    **Registre feedback** (opcional): trechos confirmados sobem nas próximas buscas parecidas e os rejeitados somem.\n  </Step>\n</Steps>\n\n<EndpointList tool=\"base-de-conhecimento\" />"},{"anchor":"custos","heading":"Custos","markdown":"<PricingTable tool=\"base-de-conhecimento\" group=\"per-use\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"free-quota\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"blocks\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"volume\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"storage\" />\n\n<Callout title=\"Como funcionam os blocos\">\n  Buscas e rerank têm uma franquia grátis por dia. Depois dela, a chamada que abre um bloco debita o bloco inteiro de uma vez (10 chamadas no primeiro, depois 20, 50 e 100 conforme o uso pago do dia), e as próximas do mesmo bloco não cobram de novo. Os contadores zeram à meia-noite UTC, e as chamadas não usadas de um bloco expiram sem estorno. A ferramenta MCP `rag_get_usage` e o `dry_run` das buscas mostram quanto a próxima chamada cobra. Consultar bases, documentos e feedback é grátis e não entra na [franquia diária de leituras](/cobranca-e-limites#franquia-diaria).\n</Callout>\n\nA ingestão é cobrada **quando termina**, pelo número real de trechos; o `estimate` da resposta é uma aproximação. Conteúdo idêntico ao já indexado não é cobrado de novo. Use `X-Dry-Run: true` no envio de documento para ver a estimativa sem enviar nada."},{"anchor":"bases","heading":"Bases","markdown":""},{"anchor":"criar-base","heading":"Criar base","markdown":"<Endpoint\n  id=\"rag.collections.create\"\n  example={{ body: `{\n\"name\": \"Central de ajuda\",\n\"description\": \"Artigos públicos de suporte\",\n\"language\": \"portuguese\",\n\"default_mode\": \"fast\"\n}` }}\n>\n  * `name` (obrigatório, até 120 caracteres) e `description` (até 1.000).\n  * `language`: `simple` (padrão), `portuguese`, `english`, `spanish` ou `french`. Define o dicionário da busca por palavras-chave.\n  * `default_mode`: `fast` (padrão) ou `accurate` (rerank extra, mais preciso e um pouco mais caro).\n  * Ajustes avançados: `chunk_size_tokens` (16–480, padrão 96), `chunk_overlap_tokens` (padrão 16), `section_max_tokens` (padrão 768), `max_per_document` (0–20, padrão 2), `dense_weight` e `lexical_weight` (0–10, padrão 1; não podem ser os dois 0) e `feedback_enabled` (padrão `true`).\n</Endpoint>"},{"anchor":"listar-bases","heading":"Listar bases","markdown":"<Endpoint id=\"rag.collections.list\">\n  Devolve também o uso da conta em relação aos limites (`usage`).\n</Endpoint>"},{"anchor":"consultar-base","heading":"Consultar base","markdown":"<Endpoint id=\"rag.collections.get\" />"},{"anchor":"filtros-disponíveis","heading":"Filtros disponíveis","markdown":"<Endpoint id=\"rag.collections.facets\">\n  Lista as chaves de metadados dos documentos da base e os valores mais comuns de cada uma, para você saber o que pode usar em `filters` nas buscas. `documents` é quantos documentos prontos com metadados foram analisados e `keys` é o total de chaves filtráveis. Se `sampled` for `true`, a base tem mais de 5000 documentos e a contagem vem só dos 5000 mais recentes. Em `facets`, cada item traz a `key`, os `documents` que a têm, `distinct` (total de valores distintos) e `values[]` (os valores mais comuns, cada um com `value` e `documents`). `array: true` indica que a chave guarda uma lista; filtre com `{\"chave\": [\"valor\"]}`.\n</Endpoint>"},{"anchor":"alterar-base","heading":"Alterar base","markdown":"<Endpoint id=\"rag.collections.update\" example={{ body: `{ \"default_mode\": \"accurate\" }` }}>\n  Envie só os campos que quer mudar. O modelo de embeddings não pode ser trocado depois da criação. Mudanças no tamanho dos trechos só valem para documentos enviados depois; para aplicar à base inteira, use a reindexação.\n</Endpoint>"},{"anchor":"reindexar-base","heading":"Reindexar base","markdown":"<Endpoint id=\"rag.collections.reindex\">\n  Reprocessa todos os documentos com as configurações atuais da base. Cada trecho é cobrado como numa ingestão nova.\n</Endpoint>"},{"anchor":"painel-de-saúde-da-base","heading":"Painel de saúde da base","markdown":"<Endpoint id=\"rag.collections.overview\" example={{ query: { rows: 'categoria', days: '30' } }}>\n  Resumo para manter a base em ordem, sem custo: contagem de documentos por `status`, quantos estão desatualizados (`stale`), nunca foram consultados ou não têm metadados; uma matriz de documentos por metadado (`rows` e `cols` são chaves de metadado, até 20 valores por eixo); a atividade dos últimos `days` dias (1 a 180, padrão 30); e `attention`, com até 5 exemplos de documentos com falha, desatualizados, duplicados ou contraditórios e com feedback negativo. A resposta fica em cache por 60 s.\n</Endpoint>"},{"anchor":"índice-dos-documentos","heading":"Índice dos documentos","markdown":"<Endpoint id=\"rag.collections.index\" example={{ query: { fields: 'categoria', order: 'title' } }}>\n  Lista compacta dos documentos `ready`, sem conteúdo, para o agente escolher o que ler. `filters` (JSON de metadados), `fields` (até 20 chaves de metadado a copiar), `order` (`updated_at_desc`, padrão, ou `title`) e `max_bytes` (padrão 4096, máximo 32768). `omitted` conta os documentos cortados pelo limite.\n</Endpoint>"},{"anchor":"achar-duplicados-e-contradições","heading":"Achar duplicados e contradições","markdown":"<Endpoint id=\"rag.collections.lint\" example={{ body: `{ \"limit\": 20 }` }}>\n  Compara os 500 documentos mais recentes (opcionalmente filtrados por `filters`) com os embeddings que já existem; não chama modelo e não escreve nada. `duplicate` aparece com similaridade a partir de 0,75; `possible_contradiction`, entre 0,4 e 0,75 quando os dois têm o mesmo metadado `tipo`. Cada achado diz qual documento manter (`keep`, o mais novo) e o que fazer (`action`). `limit` vai de 1 a 100 (padrão 20) e `include_content: true` devolve o início de cada documento.\n</Endpoint>"},{"anchor":"trocar-o-modelo-de-embeddings","heading":"Trocar o modelo de embeddings","markdown":"<Endpoint id=\"rag.collections.model\" example={{ body: `{ \"embedding_model\": \"BAAI/bge-m3\" }` }}>\n  Troca entre `intfloat/multilingual-e5-small` (padrão) e `BAAI/bge-m3` (Alta precisão) reindexando a base **em segundo plano**; você não precisa chamar a reindexação. A troca custa 50% do preço normal de ingestão, e voltar ao modelo anterior é grátis enquanto ela roda e por 24 h depois, uma vez. Use `X-Dry-Run: true` para ver a estimativa. Durante a troca a busca continua funcionando (a consulta é embutida com os dois modelos) e documentos novos são recusados com `rag.model_switch_in_progress`. Acompanhe pelo campo `model_switch` da base (`running`, `paused` ou `done`); se o saldo acabar, ela pausa com `paused_reason: insufficient_credits` e retoma quando você recarregar.\n</Endpoint>"},{"anchor":"marcar-documentos-como-desatualizados","heading":"Marcar documentos como desatualizados","markdown":"<Endpoint id=\"rag.collections.changes\" example={{ body: `{ \"paths\": [\"docs/api/\", \"src/auth.ts\"], \"commit\": \"a1b2c3d\" }` }}>\n  Para bases ligadas a código ou a arquivos: informe os `paths` que mudaram (arquivo exato ou prefixo `dir/`, até 1000) e os documentos que declararam esses `paths` ao serem enviados ficam com `stale: true`. Nada é reindexado nem cobrado; reenviar o documento limpa a marca. Use `exclude_stale` na busca para ignorá-los. `commit` (até 64 caracteres) evita marcar o que já foi conferido naquele commit.\n</Endpoint>"},{"anchor":"excluir-base","heading":"Excluir base","markdown":"<Endpoint id=\"rag.collections.delete\">\n  A exclusão é assíncrona e apaga documentos, trechos e feedback.\n</Endpoint>"},{"anchor":"documentos","heading":"Documentos","markdown":""},{"anchor":"adicionar-ou-substituir-documento","heading":"Adicionar ou substituir documento","markdown":"<Endpoint\n  id=\"rag.documents.add\"\n  example={{ body: `{\n\"external_id\": \"artigo-42\",\n\"title\": \"Como trocar a senha\",\n\"content\": \"# Conta\\\\n\\\\n## Como trocar a senha\\\\n\\\\nAcesse Configurações > Segurança…\",\n\"source_uri\": \"https://ajuda.example.com/artigos/42\",\n\"metadata\": { \"categoria\": \"conta\" }\n}` }}\n>\n  * `content` (obrigatório): texto puro ou Markdown em UTF-8, até <Limit name=\"ragDocumentMax\" />. Para PDF, Office e imagens, use [Importar arquivo](#rag-documents-file).\n  * `external_id` (opcional, até 200 bytes): o seu identificador. Reenviar com o mesmo valor **substitui** o documento; a versão anterior continua respondendo às buscas (`processing: true`) até a nova ficar pronta.\n  * `paths` (até 50) e `commit`: os arquivos de onde o documento vem, para [marcá-lo como desatualizado](#rag-collections-changes) quando mudarem.\n  * Segredos reconhecidos no texto (chaves de API, tokens) são trocados por `[REDACTED:tipo]` antes de indexar; `redactions` conta quantos.\n  * `metadata`: objeto com até 50 chaves (4 KB, até 3 níveis), usado nos filtros da busca. Não é criptografado; não coloque dados sensíveis.\n  * Acompanhe com [Consultar documento](#rag-documents-get) até `status: ready` (ou `failed`, com `error_code`). Antes de aceitar, a API confere se o saldo cobre a estimativa.\n</Endpoint>"},{"anchor":"importar-arquivo-pdf-office-imagem","heading":"Importar arquivo (PDF, Office, imagem)","markdown":"<Endpoint id=\"rag.documents.file\">\n  Envio `multipart/form-data` com `file` (obrigatório, até 50 MB) e, opcionalmente, `title`, `external_id`, `source_uri`, `metadata` (objeto JSON em texto) e `ocr` (padrão `true`; desligue para PDFs que já têm texto). Formatos: `.pdf`, `.docx`, `.pptx`, `.xlsx`, `.html`, `.md`, `.csv`, `.asciidoc` e imagens (`.png`, `.jpg`, `.tiff`, `.bmp`, `.webp`). O arquivo é convertido em Markdown (tabelas preservadas, imagens só pelo texto do OCR) e depois indexado como qualquer documento, com o mesmo limite de <Limit name=\"ragDocumentMax\" /> para o texto resultante. A resposta é `202` com o `id` da importação; consulte-a até `completed` (traz o `document_id`) ou `failed`. Sem conversão disponível, a resposta é `503` (`rag.service_unavailable`).\n</Endpoint>"},{"anchor":"consultar-importação-de-arquivo","heading":"Consultar importação de arquivo","markdown":"<Endpoint id=\"rag.imports.get\" />"},{"anchor":"listar-documentos","heading":"Listar documentos","markdown":"<Endpoint id=\"rag.documents.list\" example={{ query: { status: 'ready', limit: '20' } }}>\n  Filtro opcional por `status` (`pending`, `processing`, `ready`, `failed`, `deleting`). Paginação por cursor: passe o `next_cursor` recebido em `after`. `limit` padrão 20, máximo 100.\n</Endpoint>"},{"anchor":"consultar-documento","heading":"Consultar documento","markdown":"<Endpoint id=\"rag.documents.get\" />"},{"anchor":"alterar-título-ou-metadados","heading":"Alterar título ou metadados","markdown":"<Endpoint id=\"rag.documents.update\" example={{ body: `{ \"metadata\": { \"categoria\": \"seguranca\" } }` }}>\n  Não reprocessa o conteúdo e não cobra nada.\n</Endpoint>"},{"anchor":"ver-seções-indexadas","heading":"Ver seções indexadas","markdown":"<Endpoint id=\"rag.documents.sections\">\n  Mostra como o documento foi dividido, útil para ajustar títulos e tamanho dos trechos.\n</Endpoint>"},{"anchor":"reprocessar-documento-com-falha","heading":"Reprocessar documento com falha","markdown":"<Endpoint id=\"rag.documents.retry\">\n  Só para documentos com `status: failed`; os demais respondem `rag.document_not_failed`.\n</Endpoint>"},{"anchor":"excluir-documento","heading":"Excluir documento","markdown":"<Endpoint id=\"rag.documents.delete\" />"},{"anchor":"fontes-sincronizadas","heading":"Fontes sincronizadas","markdown":"Uma **fonte** importa páginas de fora e as mantém em dia: a cada sincronização, páginas novas ou alteradas são reindexadas, as inalteradas (mesmo conteúdo) não são cobradas de novo, e as que sumiram da origem são removidas da base. Cada página vira um documento comum (`external_id` `src:<id da fonte>:…`, com `source_id` e `source_url` nos metadados), cobrado por trecho como numa ingestão manual; se faltar saldo ou cota, a sincronização falha e mostra o motivo.\n\n| `kind`    | `start_url`                      | O que importa                                                                                                                                                             |\n| --------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `site`    | página inicial                   | Rastreia links do mesmo host até `max_depth`, respeitando o `robots.txt`; HTML vira Markdown e arquivos PDF/Office linkados são convertidos.                              |\n| `sitemap` | URL do sitemap (ou índice)       | Importa as páginas listadas, até `max_pages`. `max_depth` não se aplica.                                                                                                  |\n| `github`  | `https://github.com/dono/repo`   | Repositório **público**; `config.ref` (padrão `HEAD`) e `config.paths` (prefixos) filtram. Lê `.md`, `.mdx`, `.txt`, `.rst` e converte os formatos de arquivo suportados. |\n| `gdrive`  | URL de uma pasta `/folders/<id>` | Pasta compartilhada como \"qualquer pessoa com o link\"; Docs viram Markdown. Só existe se o servidor tiver a integração ligada.                                            |"},{"anchor":"listar-fontes-sincronizadas","heading":"Listar fontes sincronizadas","markdown":"<Endpoint id=\"rag.sources.list\" />"},{"anchor":"criar-fonte-sincronizada","heading":"Criar fonte sincronizada","markdown":"<Endpoint\n  id=\"rag.sources.create\"\n  example={{ body: `{\n\"kind\": \"site\",\n\"name\": \"Central de ajuda\",\n\"start_url\": \"https://ajuda.example.com\",\n\"max_pages\": 100,\n\"max_depth\": 3,\n\"frequency_hours\": 24\n}` }}\n>\n  * `name` (até 200) e `start_url` (http ou https) são obrigatórios; `kind` padrão `site`.\n  * `max_pages`: 1 a 1000 (padrão 100). `max_depth`: 1 a 10 (padrão 3).\n  * `frequency_hours`: `0` (só manual), `6`, `24` (padrão) ou `168`.\n  * A primeira sincronização começa na hora. Páginas e arquivos de até 20 MB cada; um arquivo que não converte é pulado sem falhar a sincronização.\n</Endpoint>"},{"anchor":"alterar-fonte","heading":"Alterar fonte","markdown":"<Endpoint id=\"rag.sources.update\" example={{ body: `{ \"frequency_hours\": 168, \"enabled\": true }` }}>\n  Todos os campos são opcionais. Com `enabled: false` ou `frequency_hours: 0`, `next_sync_at` fica nulo.\n</Endpoint>"},{"anchor":"sincronizar-agora","heading":"Sincronizar agora","markdown":"<Endpoint id=\"rag.sources.sync\">\n  Responde `409` se a fonte já estiver sincronizando. `pages_total`, `pages_new`, `pages_changed` e `pages_removed` resumem a execução.\n</Endpoint>"},{"anchor":"histórico-de-sincronizações","heading":"Histórico de sincronizações","markdown":"<Endpoint id=\"rag.sources.syncs\" />"},{"anchor":"excluir-fonte","heading":"Excluir fonte","markdown":"<Endpoint id=\"rag.sources.delete\" />"},{"anchor":"busca","heading":"Busca","markdown":""},{"anchor":"modos-de-busca-e-reranker","heading":"Modos de busca e reranker","markdown":"* **`fast`** (padrão): combina a busca por significado e a por palavras-chave. O `score` é uma fusão de posições (valores pequenos, perto de 0,03), bom para ordenar, não para comparar com um limiar.\n* **`accurate`**: depois da fusão, um **reranker** relê os 20 melhores candidatos junto com a pergunta e devolve um `score` calibrado de 0 a 1; só nele `min_score` tem efeito. Cobra o adicional de rerank. Se o rerank estiver indisponível, a busca cai para `fast` sem o adicional (`mode_used`).\n* **Reranker**: `rerank_provider` da base (e `provider` no rerank avulso e no `decide`) escolhe entre `bge` (padrão, cross-encoder) e `laya`, o juiz de relevância da Respawn, que pergunta se o trecho responde à consulta e devolve a probabilidade. O Laya costuma separar melhor trechos que só se parecem com a pergunta, mas é mais lento e, se estiver fora do ar, o `accurate` cai para `fast` e o rerank avulso responde `503` (`rag.service_unavailable`). O preço é o mesmo do `bge`.\n* Se o serviço de embeddings falhar, a busca ainda responde só por palavras-chave (`mode_used: lexical`), cobrada como cache."},{"anchor":"buscar-na-base","heading":"Buscar na base","markdown":"<Endpoint\n  id=\"rag.query\"\n  example={{ body: `{\n\"query\": \"como mudo minha senha?\",\n\"top_k\": 5,\n\"filters\": { \"categoria\": \"conta\" }\n}` }}\n>\n  * `query` (obrigatório, até <Limit name=\"ragQueryCharsMax\" /> caracteres).\n  * `top_k`: quantos trechos devolver (padrão 5, máximo <Limit name=\"ragTopKMax\" />).\n  * `mode`: `fast` ou `accurate`; sem ele, vale o padrão da base. Se o rerank do modo `accurate` estiver indisponível, a busca cai para `fast` sem cobrar o adicional; `mode_used` mostra o que foi usado.\n  * `filters`: só trechos de documentos cujos metadados contêm estes valores.\n  * `min_score` (só no `accurate`) e `max_per_document` (0–20) refinam o resultado.\n  * `updated_after` e `updated_before` (RFC 3339) filtram pela data de atualização do documento, e `exclude_stale: true` ignora os marcados como desatualizados. Cada resultado traz `updated_at`, `stale`, `paths`, `commit`, `source_uri` e `also_in` (outros documentos com o mesmo texto, colapsados num só resultado).\n  * `highlights` marca, em posições de caractere Unicode, os termos encontrados em `content`. Consultas repetidas em até <Limit name=\"ragQueryCacheTtl\" /> vêm do cache (`cached: true`) e custam menos.\n\n  <Callout type=\"warn\" title=\"Conteúdo do usuário\">\n    Os trechos devolvidos são conteúdo dos seus documentos. Ao passá-los a um modelo de linguagem, trate-os como dados, não como instruções.\n  </Callout>\n</Endpoint>"},{"anchor":"feedback-de-relevância","heading":"Feedback de relevância","markdown":""},{"anchor":"registrar-feedback-de-relevância","heading":"Registrar feedback de relevância","markdown":"<Endpoint\n  id=\"rag.feedback.add\"\n  example={{ body: `{\n\"query\": \"como mudo minha senha?\",\n\"section_id\": \"b2c4d6e8-0a1b-4c3d-8e5f-6a7b8c9d0e1f\",\n\"label\": \"positive\",\n\"end_user_ref\": \"usuario-123\"\n}` }}\n>\n  * `label`: `positive` (o trecho responde à pergunta) ou `negative`.\n  * `end_user_ref` (opcional): identifica quem avaliou, quando o voto vem do seu usuário final. É guardado só como hash.\n  * Até <Limit name=\"ragFeedbackPerMinute\" /> votos por minuto. Perguntas parecidas são agrupadas, e o voto passa a valer para todo o grupo.\n</Endpoint>"},{"anchor":"listar-feedback","heading":"Listar feedback","markdown":"<Endpoint id=\"rag.feedback.list\" />"},{"anchor":"apagar-um-grupo-de-feedback","heading":"Apagar um grupo de feedback","markdown":"<Endpoint id=\"rag.feedback.delete\" />"},{"anchor":"rerank-avulso","heading":"Rerank avulso","markdown":""},{"anchor":"reordenar-textos-rerank","heading":"Reordenar textos (rerank)","markdown":"<Endpoint\n  id=\"rag.rerank\"\n  example={{ body: `{\n\"query\": \"prazo de reembolso\",\n\"documents\": [\"Entregas em até 5 dias úteis.\", \"Frete grátis acima de R$ 200.\", \"Reembolso em até 7 dias após a devolução.\"],\n\"top_k\": 2\n}` }}\n>\n  Ordena **a sua** lista de textos pela relevância à pergunta, sem base envolvida. Até <Limit name=\"ragRerankDocumentsMax\" /> textos por chamada (cada um é cortado em 2.000 caracteres). `index` aponta para a posição no array enviado.\n</Endpoint>"},{"anchor":"resposta-com-citações","heading":"Resposta com citações","markdown":""},{"anchor":"responder-com-citações","heading":"Responder com citações","markdown":"<Endpoint\n  id=\"rag.answer\"\n  example={{ body: `{\n\"query\": \"como mudo minha senha?\",\n\"top_k\": 5,\n\"min_score\": 0.35,\n\"generate\": false\n}` }}\n>\n  Faz uma busca `accurate` e responde **só** com trechos de `score` igual ou acima de `min_score` (0 a 1, padrão 0,35, um ponto de partida que vale calibrar na sua base).\n\n  * Com resposta, `answered: true` e cada `[n]` do texto aponta para uma `citations[]` (`section_id`, `document_id`, `title`, `heading_path`, `source_uri`, `score`, `excerpt`). Nunca há resposta sem citação.\n  * Sem trecho acima do limiar, `answered: false`, `reason: no_passage_above_threshold`, nenhuma citação, e o webhook `rag.question.unanswered` é disparado. Se o rerank não pôde conferir a relevância, `reason: relevance_unverified` (não afirma nada e não dispara o webhook).\n  * `generate: false` (padrão) monta a resposta citando os 3 melhores trechos. Com `generate: true`, o provedor de IA padrão da plataforma escreve o texto a partir dos trechos; sem marcador de citação válido ou sem provedor, volta ao modo extrativo (`generated: false`). Com texto gerado, os trechos da base seguem para o provedor de IA da plataforma.\n  * Com `Accept: text/event-stream`, a resposta vem em SSE: `citations`, vários `delta` (pedaços do texto) e `done` (o resultado completo). O texto é gerado inteiro antes, o SSE só o fatia.\n  * Cobrança de uma busca `accurate`; a geração não tem cobrança à parte. Use o `query_id` no [feedback](#rag-feedback-add) para alimentar o CTR do analytics.\n</Endpoint>"},{"anchor":"widget","heading":"Widget de chat embutível","markdown":"O widget é um botão de chat em JavaScript puro (sem CDN, em shadow DOM, só escreve texto) que responde com citações usando a sua base. Ele usa uma **chave pública** (`rpk_…`) que só consegue perguntar numa base: não lê outras, não escreve nada.\n\n```html\n<script src=\"https://api.respawntech.dev/widget/rag.js\" data-key=\"rpk_...\" async></script>\n```\n\nAtributos: `data-key` (obrigatório), `data-title`, `data-placeholder` e `data-dont-know` (texto quando não há resposta). As perguntas feitas pelo widget são cobradas do dono da base, como o `answer`."},{"anchor":"criar-chave-pública-widget","heading":"Criar chave pública (widget)","markdown":"<Endpoint\n  id=\"rag.publickeys.create\"\n  example={{ body: `{\n\"name\": \"Site institucional\",\n\"allowed_origins\": [\"https://www.example.com\", \"https://*.example.com\"],\n\"rate_per_minute\": 30,\n\"generate\": false\n}` }}\n>\n  * `allowed_origins` (obrigatório, 1 a 20): `https://site.com` ou `https://*.site.com`, sem caminho. O servidor recusa pedidos cujo `Origin` não esteja na lista.\n  * `rate_per_minute`: 1 a 600 (padrão 30), por chave. `generate` liga o texto gerado por IA nas respostas dessa chave.\n  * Até 20 chaves por base. A `key` aparece **uma única vez**; guardamos só o hash.\n\n  <Callout type=\"warn\" title=\"Origin não é autenticação\">\n    Só o navegador respeita o `Origin`; um cliente fora dele pode forjá-lo. O que limita o abuso é o limite por minuto da chave, e o custo recai sobre você. Revogue a chave se vir uso estranho.\n  </Callout>\n</Endpoint>"},{"anchor":"listar-chaves-públicas","heading":"Listar chaves públicas","markdown":"<Endpoint id=\"rag.publickeys.list\" />"},{"anchor":"revogar-chave-pública","heading":"Revogar chave pública","markdown":"<Endpoint id=\"rag.publickeys.revoke\" />"},{"anchor":"responder-pelo-widget-rota-pública","heading":"Responder pelo widget (rota pública)","markdown":"<Endpoint id=\"rag.public.answer\" example={{ body: `{ \"query\": \"como mudo minha senha?\" }` }}>\n  Sem token de API: envie a chave no header `X-Respawn-Public-Key` e o navegador manda o `Origin`. A base vem da chave, e `top_k`, `min_score` e `generate` não são aceitos no pedido. Responde como o `answer` (JSON ou SSE), com CORS liberado para a origem permitida. Erros: `401` (chave ausente, desconhecida ou revogada), `403` (origem não permitida) e `429` (limite por minuto).\n</Endpoint>"},{"anchor":"script-do-widget","heading":"Script do widget","markdown":"<Endpoint id=\"rag.widget\" />"},{"anchor":"analytics-de-perguntas","heading":"Analytics de perguntas","markdown":""},{"anchor":"analytics-das-perguntas","heading":"Analytics das perguntas","markdown":"<Endpoint id=\"rag.analytics\" example={{ query: { days: '30' } }}>\n  Mostra o que os usuários perguntam e o que a base não sabe responder, a partir das chamadas de `answer` e do widget. `days` vai de 1 a 90 (padrão 30).\n\n  * `answer_rate`: respondidas ÷ total de perguntas.\n  * `feedback_ctr`: respondidas que receberam voto (pelo `query_id`) ÷ respondidas.\n  * `positive_rate`: votos positivos ÷ votos.\n  * `top_questions` e `unanswered_questions`: até 20 perguntas agrupadas cada, para você descobrir o que escrever na base.\n\n  As perguntas ficam cifradas com a chave da base. O widget não tem voto próprio: o CTR só conta votos enviados pela API.\n</Endpoint>"},{"anchor":"roteamento","heading":"Roteamento","markdown":""},{"anchor":"decidir-entre-opções-roteamento","heading":"Decidir entre opções (roteamento)","markdown":"<Endpoint\n  id=\"rag.decide\"\n  example={{ body: `{\n\"text\": \"quero cancelar minha assinatura\",\n\"options\": [\n  { \"label\": \"cobranca\", \"description\": \"pagamentos, faturas e cancelamentos\" },\n  { \"label\": \"suporte\", \"description\": \"problemas técnicos\" }\n],\n\"threshold\": 0.5,\n\"min_margin\": 0.1\n}` }}\n>\n  Classifica um texto entre 2 e 50 opções sem usar um LLM, pontuando cada `label: description` com o reranker (`provider`: `bge` ou `laya`). Serve para rotear pedidos. Se a melhor opção não passar de `threshold` (padrão 0,5) ou não ficar `min_margin` (padrão 0,1) à frente da segunda, `decided` é `false`: trate como \"não sei\". O `ranking` completo vem sempre. Preço e franquia do rerank avulso.\n</Endpoint>"},{"anchor":"preços-e-limites-da-conta","heading":"Preços e limites da conta","markdown":""},{"anchor":"consultar-preços","heading":"Consultar preços","markdown":"<Endpoint id=\"rag.pricing\" />"},{"anchor":"consultar-limites","heading":"Consultar limites","markdown":"<Endpoint id=\"rag.limits\" />\n\nValores vigentes da sua conta (já com o bônus do plano), úteis para validar um envio antes de fazê-lo."},{"anchor":"eventos-webhooks","heading":"Eventos (webhooks)","markdown":"Cadastre um endpoint no dashboard para receber:\n\n* `rag.document.ready` e `rag.document.failed`: `collection_id`, `document_id`, `external_id` e `version`; o de falha traz também `error_code`. Valem para documentos enviados, importados de arquivo e vindos de fontes.\n* `rag.question.unanswered`: `collection_id`, `question`, `query_id` e `source` (`api` ou `widget`), quando nenhum trecho passou do `min_score`. Bom para alimentar uma fila de \"o que escrever\".\n\nO envio é de melhor esforço, como nos demais eventos. Veja a [lista de eventos](/webhooks)."},{"anchor":"usar-pelo-mcp","heading":"Usar pelo MCP","markdown":"As mesmas operações estão disponíveis como ferramentas MCP do grupo `rag` ([lista completa](/agentes-de-ia)): `rag_answer`, `rag_decide`, `rag_index`, `rag_lint`, `rag_mark_changed` e `rag_retry_document` são as novas. As de leitura (`rag_index`, `rag_lint`) são gratuitas; use `rag_access=query` na conexão para esconder as que alteram a base."},{"anchor":"erros","heading":"Erros","markdown":"<ErrorTable tool=\"base-de-conhecimento\" />\n\nOs códigos sem status HTTP aparecem no campo `error_code` de um documento com `status: failed`."},{"anchor":"limites","heading":"Limites","markdown":"| Limite                                     | Valor                                        |\n| ------------------------------------------ | -------------------------------------------- |\n| Tamanho de um documento                    | <Limit name=\"ragDocumentMax\" />              |\n| Bases por conta                            | <Limit name=\"ragCollectionsMax\" />           |\n| Trechos indexados por conta                | <Limit name=\"ragChunksMax\" />                |\n| Documentos em processamento ao mesmo tempo | <Limit name=\"ragPendingMax\" />               |\n| Tamanho da pergunta                        | <Limit name=\"ragQueryCharsMax\" /> caracteres |\n| Trechos por busca (`top_k`)                | <Limit name=\"ragTopKMax\" />                  |\n| Textos por rerank avulso                   | <Limit name=\"ragRerankDocumentsMax\" />       |\n| Votos de feedback por minuto               | <Limit name=\"ragFeedbackPerMinute\" />        |\n| Arquivo importado                          | 50 MB                                        |\n| Páginas por fonte                          | 1.000 (padrão 100)                           |\n| Chaves públicas por base                   | 20                                           |\n\nAs rotas da base de conhecimento ignoram o header `X-Idempotency-Key`: elas já são idempotentes (conteúdo repetido não é reindexado nem cobrado). Uma chave restrita a bases específicas só enxerga essas bases e não pode criar novas (`forbidden`)."}]},{"slug":"ferramentas/ci-error-extract","url":"https://docs.respawntech.dev/ferramentas/ci-error-extract","title":"Extrator de erro de CI","description":"Extraia de um log de CI só as linhas que explicam a falha.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"ci-error-extract\" />"},{"anchor":"visão-geral","heading":"Visão geral","markdown":"Envie o log de um job de CI e receba apenas as linhas do erro. Serve para agentes e bots que precisam entender por que um build falhou sem carregar milhares de linhas no contexto. Também está disponível no [MCP](/agentes-de-ia) como `ci_error_extract`.\n\n<EndpointList tool=\"ci-error-extract\" />"},{"anchor":"custos","heading":"Custos","markdown":"Cada extração é uma leitura medida: entra na [franquia diária](/cobranca-e-limites#franquia-diaria) e, depois dela, debita o valor indicado no endpoint. A cobrança vale só para chaves de API."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"extrair-o-erro-de-um-log","heading":"Extrair o erro de um log","markdown":"<Endpoint id=\"ci.extract\" example={{ body: `{ \"log\": \"npm ERR! code ELIFECYCLE\\\\nnpm ERR! errno 1\\\\nFAIL src/app.test.ts\", \"max_lines\": 40 }` }}>\n  `log` (obrigatório) é o texto do log. `max_lines` (opcional, padrão 40, máximo 200) limita quantas linhas do erro voltam.\n</Endpoint>"},{"anchor":"erros","heading":"Erros","markdown":"<ErrorTable tool=\"ci-error-extract\" />"}]},{"slug":"ferramentas/memoria-de-aprendizados","url":"https://docs.respawntech.dev/ferramentas/memoria-de-aprendizados","title":"Memória de aprendizados","description":"Memória de longo prazo para agentes de IA: salve o que aprendeu em cada projeto e recupere antes da próxima tarefa.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"memoria-de-aprendizados\" />"},{"anchor":"como-funciona","heading":"Como funciona","markdown":"Um aprendizado é um texto curto com um `kind`, ligado a um `project`:\n\n| `kind`       | Para guardar                                 |\n| ------------ | -------------------------------------------- |\n| `pitfall`    | Armadilhas e erros que não devem se repetir. |\n| `decision`   | Decisões tomadas e o motivo.                 |\n| `convention` | Convenções do projeto.                       |\n| `howto`      | Como fazer algo (rodar, publicar, depurar).  |\n| `other`      | O que não cabe nos outros.                   |\n\nOs aprendizados ficam numa [base de conhecimento](/ferramentas/base-de-conhecimento) chamada `agent-learnings`, **criada sozinha** na primeira gravação. O `recall` faz uma busca semântica pela tarefa e devolve só os itens do `project` pedido, com a `score` de cada um.\n\nTambém está disponível no [MCP](/agentes-de-ia) como `learning_save`, `learning_recall` e `learning_forget` (toolset `learnings`).\n\n<EndpointList tool=\"memoria-de-aprendizados\" />"},{"anchor":"ingestao","heading":"Duplicados e ingestão assíncrona","markdown":"* **Deduplicação:** o `save` compara por hash e por similaridade semântica. Se já existe um aprendizado igual ou parecido, nada novo é gravado e a resposta traz `duplicate_of` com o id do existente.\n* **Ingestão assíncrona:** o texto é indexado em segundo plano. Um `recall` logo depois de um `save` pode ainda não trazer o item; tente de novo em alguns segundos."},{"anchor":"custos","heading":"Custos","markdown":"A cobrança é a da [base de conhecimento](/ferramentas/base-de-conhecimento): o `save` como a ingestão (por trecho), o `recall` como a busca (franquia diária + blocos) e o armazenamento por dia. Esquecer é grátis. A memória segue as mesmas regras de liberação da base de conhecimento (`rag.disabled` e demais erros `rag.*`)."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"salvar-aprendizado","heading":"Salvar aprendizado","markdown":"<Endpoint id=\"learnings.save\" example={{ body: `{ \"project\": \"respawn-tech-docs\", \"kind\": \"pitfall\", \"text\": \"O check:backend lê ../respawn-tech-backend; rode só com o backend já junto.\", \"source\": \"conversa de 06/10\" }` }}>\n  `project`, `kind` e `text` são obrigatórios; `source` é opcional e diz de onde veio. A resposta traz o `id` do aprendizado ou, se for repetido, `duplicate_of`.\n</Endpoint>"},{"anchor":"recuperar-aprendizados","heading":"Recuperar aprendizados","markdown":"<Endpoint id=\"learnings.recall\" example={{ body: `{ \"project\": \"respawn-tech-docs\", \"task\": \"rodar os checks antes de abrir o PR\", \"top_k\": 5 }` }}>\n  `project` e `task` são obrigatórios; `top_k` limita quantos itens voltam. Cada item traz `id`, `kind`, `text`, `source` e `score`. Lembre que a ingestão é assíncrona: um item recém-salvo pode ainda não aparecer.\n</Endpoint>"},{"anchor":"esquecer-aprendizado","heading":"Esquecer aprendizado","markdown":"<Endpoint id=\"learnings.forget\" />"}]},{"slug":"ferramentas/notify","url":"https://docs.respawntech.dev/ferramentas/notify","title":"Notificações","description":"Um agente avisa o seu humano por e-mail, webhook assinado ou ntfy, com canais, prioridade e histórico de envios.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"notify\" />"},{"anchor":"como-funciona","heading":"Como funciona","markdown":"Uma notificação tem `title`, `message` e `priority` (`normal` ou `high`), e opcionalmente uma `url` para o humano abrir. Ela sai por um **canal**:\n\n* **Canal escolhido:** o `channel` do pedido, pelo nome do canal.\n* **Canal padrão:** o canal marcado com `is_default`, quando o pedido não traz `channel`.\n* **E-mail da conta:** sem canal nenhum configurado, a notificação vai para o e-mail da conta.\n\nTambém está disponível no [MCP](/agentes-de-ia) como a tool `notify` (toolset `notify`): o agente avisa que terminou uma tarefa longa ou que precisa de uma decisão.\n\n<EndpointList tool=\"notify\" />"},{"anchor":"canais","heading":"Canais","markdown":"Cada canal tem um `name` único na conta e um `kind`:\n\n| `kind`    | Campos            | Como entrega                                                                        |\n| --------- | ----------------- | ----------------------------------------------------------------------------------- |\n| `email`   | nenhum            | Envia **só para o e-mail da conta**. Não é possível escolher outro destinatário.    |\n| `webhook` | `url`, `secret`   | Faz um `POST` na URL, assinado como no [Webhook Relay](/ferramentas/webhook-relay). |\n| `ntfy`    | `server`, `topic` | Publica no tópico do [ntfy](https://ntfy.sh), no servidor informado.                |\n\n`is_default` marca o canal usado quando o pedido não indica um, e `enabled: false` desativa o canal sem apagá-lo. O `secret` do webhook nunca volta nas respostas: guarde-o ao criar o canal.\n\n<Callout type=\"warn\" title=\"O tópico do ntfy é a senha\">\n  Em servidores públicos do ntfy, quem souber o tópico lê as notificações. Use um nome longo e imprevisível, ou um servidor seu.\n</Callout>"},{"anchor":"limites","heading":"Limites, retry e histórico","markdown":"* **Rate limit:** há um limite de notificações por período; acima dele o envio falha com `notify.rate_limited`.\n* **Retry do webhook:** uma entrega que falha é tentada de novo algumas vezes antes de virar `notify.delivery_failed`. O receptor deve tratar repetições e conferir a assinatura.\n* **Histórico:** `GET /messages` lista os envios (canal, prioridade, status e data), paginado com `page` e `page_size` (até 100). O histórico **não guarda o título nem o corpo da mensagem**."},{"anchor":"custos","heading":"Custos","markdown":"Cada envio pela API é cobrado conforme o valor indicado no endpoint. Canais e histórico são grátis, e a falha na entrega não é cobrada."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"enviar-notificação","heading":"Enviar notificação","markdown":"<Endpoint id=\"notify.send\" example={{ body: `{ \"title\": \"Deploy concluído\", \"message\": \"A versão 2.4 está no ar.\", \"priority\": \"normal\" }` }}>\n  `title` e `message` são obrigatórios; `priority` é `normal` ou `high`; `url` e `channel` são opcionais. Sem `channel`, usa o canal padrão e, sem nenhum canal, o e-mail da conta.\n</Endpoint>"},{"anchor":"histórico-de-envios","heading":"Histórico de envios","markdown":"<Endpoint id=\"notify.messages\" example={{ query: { page: \"1\", page_size: \"20\" } }}>\n  Paginado por `page` e `page_size` (até 100). Não traz o corpo das mensagens.\n</Endpoint>"},{"anchor":"listar-canais","heading":"Listar canais","markdown":"<Endpoint id=\"notify.channels.list\" />"},{"anchor":"criar-canal","heading":"Criar canal","markdown":"<Endpoint id=\"notify.channels.create\" example={{ body: `{ \"name\": \"celular\", \"kind\": \"ntfy\", \"server\": \"https://ntfy.sh\", \"topic\": \"meu-topico-secreto\", \"is_default\": true }` }}>\n  `name` e `kind` são obrigatórios. `url` e `secret` valem para `webhook`; `server` e `topic`, para `ntfy`. Campos que não combinam com o `kind` respondem `notify.invalid_channel`.\n</Endpoint>"},{"anchor":"consultar-canal","heading":"Consultar canal","markdown":"<Endpoint id=\"notify.channels.get\" />"},{"anchor":"alterar-canal","heading":"Alterar canal","markdown":"<Endpoint id=\"notify.channels.update\" example={{ body: `{ \"enabled\": false }` }}>\n  Envie só os campos que mudam. Um canal desativado responde `notify.channel_disabled` quando escolhido num envio.\n</Endpoint>"},{"anchor":"excluir-canal","heading":"Excluir canal","markdown":"<Endpoint id=\"notify.channels.delete\" />"},{"anchor":"testar-canal","heading":"Testar canal","markdown":"<Endpoint id=\"notify.channels.test\">\n  Envia uma notificação de teste pelo canal, para conferir a configuração antes de depender dele.\n</Endpoint>"},{"anchor":"erros","heading":"Erros","markdown":"<ErrorTable tool=\"notify\" />"}]},{"slug":"ferramentas/pdf-convert","url":"https://docs.respawntech.dev/ferramentas/pdf-convert","title":"PDF Convert","description":"API de conversão de HTML em PDF, PNG ou JPG — fluxo assíncrono, batch, retenção e custos.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"pdf-convert\" />"},{"anchor":"visão-geral","heading":"Visão geral","markdown":"O PDF Convert é **assíncrono**:\n\n1. `POST /convert` ou `POST /batch` → **202 Accepted** com o `conversion_id` (o `{id}` das demais rotas).\n2. Um worker renderiza em background (Chromium headless).\n3. Acompanhe pelo [callback `webhook_url`](/webhooks#pdf-callback) ou consultando `GET /{id}`.\n4. Baixe com `GET /{id}/download` quando `status` for `completed`.\n\n<Callout type=\"warn\" title=\"Cobrança ao enfileirar\">\n  Os créditos são debitados quando o job é aceito. Se o enfileiramento de um batch falhar depois do débito, o valor é estornado automaticamente.\n</Callout>\n\n<EndpointList tool=\"pdf-convert\" />"},{"anchor":"custos","heading":"Custos","markdown":"<PricingTable tool=\"pdf-convert\" group=\"document\" />\n\n<PricingTable tool=\"pdf-convert\" group=\"addons\" />\n\n<PricingTable tool=\"pdf-convert\" group=\"batch-discounts\" />\n\nO batch aceita até **<Limit name=\"pdfBatchMax\" /> itens**. O retry cobra o custo integral novamente, inclusive os adicionais acima. Salvar templates e enviar fontes não custa créditos. Consultas e downloads via API seguem a [franquia diária](/cobranca-e-limites#franquia-diaria)."},{"anchor":"conversão","heading":"Conversão","markdown":""},{"anchor":"converter-html-em-pdfpngjpg","heading":"Converter HTML em PDF/PNG/JPG","markdown":"<Endpoint\n  id=\"pdf.convert\"\n  example={{\n  headers: { 'X-Idempotency-Key': '550e8400-e29b-41d4-a716-446655440001' },\n  body: `{\n\"html\": \"<html><body><h1>Fatura</h1></body></html>\",\n\"css\": \"body { font-family: sans-serif; }\",\n\"format\": \"pdf\",\n\"page_size\": \"A4\",\n\"retention_days\": 7\n}`,\n}}\n>\n  Envie **`html` ou `url`** (um dos dois, nunca os dois). Com `url`, o Chromium abre a página pública e a renderiza; `css`, `google_fonts_url` e `data` são recusados nesse modo (`pdf.invalid_option`).\n\n  | Campo                                                         | Limite / valores                                                                                                          |\n  | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |\n  | `html`                                                        | máx. <Limit name=\"pdfHtmlMax\" />                                                                                          |\n  | `url`                                                         | alternativa a `html`: endereço público `http`/`https`, até 2048 caracteres (`pdf.invalid_url` se for privado ou inválido) |\n  | `css`                                                         | máx. <Limit name=\"pdfCssMax\" />                                                                                           |\n  | `format`                                                      | `pdf` (padrão), `png`, `jpg` / `jpeg`                                                                                     |\n  | `file_name`                                                   | nome do arquivo gerado                                                                                                    |\n  | `title`, `author`, `subject`, `keywords`                      | metadados do documento, até 1000 caracteres cada                                                                          |\n  | `header_html` / `footer_html`                                 | cabeçalho e rodapé repetidos em cada página                                                                               |\n  | `google_fonts_url`                                            | URL de CSS do Google Fonts a carregar                                                                                     |\n  | `wait_selector` / `wait_delay`                                | espera um seletor aparecer / um atraso em ms antes de renderizar                                                          |\n  | `enable_js` / `custom_js`                                     | executa JavaScript em sandbox (SPAs, gráficos) / script extra                                                             |\n  | `data`                                                        | objeto com variáveis para placeholders `{{campo}}` no HTML                                                                |\n  | `retention_days`                                              | `0` (efêmero, \\~24h), `1`–`30` ou `-1` (permanente)                                                                       |\n  | `webhook_url`                                                 | recebe o [callback](/webhooks#pdf-callback) ao terminar                                                                   |\n  | `template_id`, `template_version`, `font_ids`, `post_process` | [templates salvos](#templates), [fontes](#fontes) e [pós-processamento](#pos-processamento)                               |\n\n  Tamanho de página, margens, mídia, numeração e qualidade estão em [Opções de página e render](#opcoes-de-pagina).\n\n  Guarde o `conversion_id`: é o `{id}` usado para consultar status e baixar o arquivo.\n</Endpoint>"},{"anchor":"batch-com-template","heading":"Batch com template","markdown":"<Endpoint\n  id=\"pdf.batch\"\n  example={{\n  headers: { 'X-Idempotency-Key': '550e8400-e29b-41d4-a716-446655440002' },\n  body: `{\n\"template\": {\n  \"html\": \"<html><body><h1>{{title}}</h1></body></html>\",\n  \"css\": \"h1 { color: #333; }\"\n},\n\"config\": { \"page_size\": \"A4\", \"format\": \"pdf\", \"retention_days\": 7 },\n\"items\": [\n  { \"custom_id\": \"fatura-001\", \"file_name\": \"fatura-001.pdf\", \"data\": { \"title\": \"Fatura 001\" } },\n  { \"custom_id\": \"fatura-002\", \"file_name\": \"fatura-002.pdf\", \"data\": { \"title\": \"Fatura 002\" } }\n],\n\"delivery\": { \"method\": \"zip\", \"webhook_url\": \"https://example.com/hooks/pdf\" }\n}`,\n}}\n>\n  | Campo                  | Descrição                                                                                                                                                                                                                                                        |\n  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n  | `template.html`        | **obrigatório** — HTML com placeholders `{{campo}}` (sintaxe de Go template: `{{if}}`, `{{range}}` também funcionam)                                                                                                                                             |\n  | `template.css`         | CSS compartilhado                                                                                                                                                                                                                                                |\n  | `items[]`              | **obrigatório**, 1 a <Limit name=\"pdfBatchMax\" /> itens: `custom_id` (**obrigatório**, seu identificador), `file_name` e `data`                                                                                                                                  |\n  | `config`               | opções aplicadas a todos os itens: `page_size`, `orientation`, margens, `format`, `scale`, `retention_days`, `google_fonts_url`, `header_html`, `footer_html`, `wait_selector`, `wait_delay`, `title`, `author`, `enable_js`, `custom_js` e `file_name_template` |\n  | `delivery.method`      | `zip` ou `list`: como o resultado é entregue ao terminar                                                                                                                                                                                                         |\n  | `delivery.webhook_url` | recebe o [callback](/webhooks#pdf-callback) ao terminar                                                                                                                                                                                                          |\n\n  O batch não aceita `url`, `template_id`, `font_ids` nem `post_process`: ele usa sempre o `template.html` do corpo. Os descontos de volume se aplicam sobre base, JavaScript e retenção. Acompanhe o progresso em [Detalhe do batch](#pdf-batch-get).\n</Endpoint>"},{"anchor":"reprocessar-falha","heading":"Reprocessar falha","markdown":"<Endpoint\n  id=\"pdf.retry\"\n  example={{\n  body: `{\n\"html\": \"<html><body><h1>Fatura corrigida</h1></body></html>\",\n\"format\": \"pdf\",\n\"page_size\": \"A4\"\n}`,\n}}\n>\n  Permitido apenas quando `status` é `failed`. O corpo aceita as mesmas opções de [Converter](#pdf-convert), então dá para corrigir o HTML ou as opções antes de tentar de novo. O `webhook_url` do corpo é ignorado: a conversão mantém o que foi enviado originalmente. O custo é cobrado por inteiro outra vez.\n</Endpoint>"},{"anchor":"opcoes-de-pagina","heading":"Opções de página e render","markdown":"Valem para `POST /convert` e `POST /retry`. Valores fora da lista devolvem `pdf.invalid_option` (ou `pdf.invalid_page_size` para o tamanho do papel).\n\n| Campo                                                        | Valores                                                                                                                                               |\n| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `page_size`                                                  | `A3`, `A4` (padrão), `A5`, `Letter`, `Legal`, `Tabloid` ou `Custom`                                                                                   |\n| `paper_width`, `paper_height`                                | papel personalizado, em **polegadas** (1 a 200). Só combinam com `page_size` vazio ou `Custom`; usados com outro tamanho, dão `pdf.invalid_page_size` |\n| `orientation`                                                | `portrait` (padrão) ou `landscape`                                                                                                                    |\n| `margin_top`, `margin_bottom`, `margin_left`, `margin_right` | margens da página                                                                                                                                     |\n| `scale`                                                      | escala do render                                                                                                                                      |\n| `print_background`                                           | imprime cores e imagens de fundo (padrão `true`)                                                                                                      |\n| `media_type`                                                 | `print` ou `screen`: qual `@media` o CSS enxerga                                                                                                      |\n| `color_scheme`                                               | `light` ou `dark`: valor de `prefers-color-scheme`                                                                                                    |\n| `page_numbers`                                               | liga o contador de páginas na posição: `header-left`, `header-center`, `header-right`, `footer-left`, `footer-center` ou `footer-right`               |\n| `page_number_format`                                         | texto do contador, com `{page}` e `{total}` (padrão `{page} / {total}`, até 200 caracteres). Exige `page_numbers`                                     |\n| `jpeg_quality`                                               | de 1 a 100 (padrão 90). Só vale com `format: jpg`                                                                                                     |\n\nExemplo: fatura em papel de 8 x 11 polegadas, com contador no rodapé:\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n  \"url\": \"https://example.com/relatorio\",\n  \"page_size\": \"Custom\",\n  \"paper_width\": 8,\n  \"paper_height\": 11,\n  \"media_type\": \"print\",\n  \"page_numbers\": \"footer-center\",\n  \"page_number_format\": \"Página {page} de {total}\"\n}'\n```"},{"anchor":"pos-processamento","heading":"Pós-processamento","markdown":"`post_process` age sobre o PDF já renderizado e **só vale com `format: pdf`** (com `png` ou `jpg`, `pdf.invalid_option`). Cada recurso é cobrado à parte, veja [Custos](#custos).\n\n| Campo                                   | Descrição                                                                                                                   |\n| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |\n| `post_process.watermark.text`           | texto da marca d'água, até 200 caracteres                                                                                   |\n| `post_process.watermark.image`          | alternativa ao texto: imagem PNG ou JPEG em base64, até 1 MiB. Envie **exatamente um** entre `text` e `image`               |\n| `watermark.font_size`                   | de 1 a 300                                                                                                                  |\n| `watermark.color`                       | cor no formato `#rrggbb`                                                                                                    |\n| `watermark.opacity`, `watermark.scale`  | de 0 a 1                                                                                                                    |\n| `watermark.rotation`                    | de -180 a 180 graus                                                                                                         |\n| `watermark.position`                    | `center`, `top-left`, `top-center`, `top-right`, `left`, `right`, `bottom-left`, `bottom-center` ou `bottom-right`          |\n| `watermark.on_top`                      | `true` desenha a marca por cima do conteúdo                                                                                 |\n| `post_process.pdfa`                     | `true` converte para PDF/A-2b (arquivamento de longo prazo)                                                                 |\n| `post_process.protection.user_password` | **obrigatória** se houver `protection`, até 128 caracteres: senha para abrir o arquivo                                      |\n| `protection.owner_password`             | senha de proprietário, até 128 caracteres                                                                                   |\n| `protection.permissions`                | o que o leitor pode fazer: `print`, `copy`, `modify`, `annotate`, `fill_forms`, `assemble`. Lista vazia = nenhuma permissão |\n\n`pdfa` e `protection` não se combinam (o PDF/A proíbe criptografia): a API responde `pdf.invalid_option`. As senhas são guardadas cifradas só para o worker aplicá-las e não voltam nas respostas.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n  \"html\": \"<html><body><h1>Contrato</h1></body></html>\",\n  \"format\": \"pdf\",\n  \"post_process\": {\n    \"watermark\": { \"text\": \"CONFIDENCIAL\", \"color\": \"#cc0000\", \"opacity\": 0.2, \"rotation\": -45, \"position\": \"center\" },\n    \"protection\": { \"user_password\": \"senha-do-leitor\", \"permissions\": [\"print\"] }\n  }\n}'\n```"},{"anchor":"templates","heading":"Templates salvos","markdown":"Guarde o HTML de um documento recorrente na conta e renderize-o só com `template_id` e `data`. Cada alteração de conteúdo cria uma **versão** nova; as anteriores continuam consultáveis.\n\n* Em `POST /convert`, `template_id` não combina com `html` nem `url` (`pdf.invalid_option`). `template_version` escolhe a versão; `0` ou omitido usa a mais recente.\n* O `css`, o `header_html` e o `footer_html` do template valem como padrão; se você os enviar na conversão, os seus prevalecem.\n* Template ou versão inexistente: `pdf.template_not_found`. Nome de até 120 caracteres; o `html` segue os limites de <Limit name=\"pdfHtmlMax\" /> e o `css` de <Limit name=\"pdfCssMax\" />.\n* Leituras usam o escopo `tools:pdf:read`; escritas, `tools:pdf:manage` (ou `tools:manage`). Nenhuma delas cobra créditos nem consome a franquia diária.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n  \"template_id\": \"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a\",\n  \"template_version\": 0,\n  \"data\": { \"numero\": \"001\" }\n}'\n```"},{"anchor":"listar-templates-salvos","heading":"Listar templates salvos","markdown":"<Endpoint id=\"pdf.templates.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  `limit` (padrão e máximo 100) e `offset`. A resposta traz `items` e `total`.\n</Endpoint>"},{"anchor":"salvar-template","heading":"Salvar template","markdown":"<Endpoint\n  id=\"pdf.templates.create\"\n  example={{\n  body: `{\n\"name\": \"Fatura padrão\",\n\"description\": \"Fatura mensal com logotipo\",\n\"html\": \"<html><body><h1>Fatura {{numero}}</h1></body></html>\",\n\"css\": \"h1 { color: #333; }\",\n\"header_html\": \"\",\n\"footer_html\": \"\"\n}`,\n}}\n>\n  `name` (até 120 caracteres) e `html` são obrigatórios; `description`, `css`, `header_html` e `footer_html` são opcionais. Cria a versão 1.\n</Endpoint>"},{"anchor":"consultar-template","heading":"Consultar template","markdown":"<Endpoint id=\"pdf.templates.get\" example={{ query: { version: '2' } }}>\n  `version` opcional (padrão: a versão atual). Devolve o `template` e o conteúdo da `version`.\n</Endpoint>"},{"anchor":"listar-versões-do-template","heading":"Listar versões do template","markdown":"<Endpoint id=\"pdf.templates.versions\" />"},{"anchor":"renomear-template-ou-salvar-nova-versão","heading":"Renomear template ou salvar nova versão","markdown":"<Endpoint\n  id=\"pdf.templates.update\"\n  example={{\n  body: `{\n\"name\": \"Fatura padrão v2\",\n\"html\": \"<html><body><h1>Fatura {{numero}}</h1><p>{{cliente}}</p></body></html>\",\n\"css\": \"h1 { color: #333; }\"\n}`,\n}}\n>\n  Envie `name` e/ou `description` para renomear. Com `html`, é salva uma **nova versão** com o `html`, `css`, `header_html` e `footer_html` do corpo, e `current_version` sobe. Os campos de conteúdo omitidos ficam vazios na nova versão: reenvie o conteúdo completo.\n</Endpoint>"},{"anchor":"excluir-template","heading":"Excluir template","markdown":"<Endpoint id=\"pdf.templates.delete\">\n  Apaga o template e todas as versões. As conversões já geradas não são afetadas.\n</Endpoint>"},{"anchor":"fontes","heading":"Fontes","markdown":"Envie fontes próprias (`woff2` ou `ttf`) e use-as no HTML com `font-family`: passe os ids em `font_ids` (até 20 por conversão) e a plataforma injeta o `@font-face` correspondente. Fonte inexistente em `font_ids`: `pdf.font_not_found`. O adicional de fontes é cobrado uma vez por documento que usa `font_ids`.\n\n* Até **50 fontes por conta** (`pdf.font_limit`).\n* Uma combinação de `family` + `weight` + `style` só pode existir uma vez (`pdf.font_exists`, 409).\n* O formato é detectado pelo conteúdo do arquivo, não pela extensão."},{"anchor":"listar-fontes","heading":"Listar fontes","markdown":"<Endpoint id=\"pdf.fonts.list\" />"},{"anchor":"enviar-fonte","heading":"Enviar fonte","markdown":"<Endpoint id=\"pdf.fonts.upload\">\n  Envio `multipart/form-data` com `file` (obrigatório, `woff2` ou `ttf`, até 5 MiB), `family` (obrigatório, 1 a 64 letras, números, espaços, `-` ou `_`), `weight` (`100` a `900` em passos de 100; padrão `400`) e `style` (`normal` ou `italic`; padrão `normal`). Qualquer violação devolve `pdf.font_invalid`.\n\n  ```bash\n  curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/fonts' \\\n    -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n    -F 'file=@Inter-Regular.woff2' \\\n    -F 'family=Inter' \\\n    -F 'weight=400' \\\n    -F 'style=normal'\n  ```\n\n  Depois, converta com a fonte:\n\n  ```bash\n  curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n    -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"html\": \"<html><body style=\\\"font-family: Inter\\\"><h1>Olá</h1></body></html>\",\n    \"font_ids\": [\"2b3c4d5e-6f70-4a8b-9c0d-1e2f3a4b5c6d\"]\n  }'\n  ```\n</Endpoint>"},{"anchor":"excluir-fonte","heading":"Excluir fonte","markdown":"<Endpoint id=\"pdf.fonts.delete\">\n  Remove a fonte e o arquivo armazenado.\n</Endpoint>"},{"anchor":"consulta-e-download","heading":"Consulta e download","markdown":""},{"anchor":"listar-conversões","heading":"Listar conversões","markdown":"<Endpoint id=\"pdf.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  Query: `limit` (padrão 20), `offset` e `exclude_batch=true` para ocultar itens de batch. Na listagem, `file_url` e `error_message` não são preenchidos — consulte o [status](#pdf-get) de cada conversão.\n</Endpoint>"},{"anchor":"status-da-conversão","heading":"Status da conversão","markdown":"<Endpoint id=\"pdf.get\">\n  `status`: `pending`, `processing`, `completed` ou `failed`. Em falha, `error_code` e `error_message` explicam o motivo. `data` traz as variáveis enviadas e `file_url` aparece quando o job termina — para baixar, use sempre [Baixar arquivo](#pdf-download).\n</Endpoint>"},{"anchor":"baixar-arquivo","heading":"Baixar arquivo","markdown":"<Endpoint id=\"pdf.download\" example={{ response: 'binary' }}>\n  Requer `status: completed`. O arquivo é descriptografado e descomprimido durante o download. `X-Dry-Run` não se aplica a downloads.\n\n  O campo `file_url` da conversão (ou do detalhe do batch) é um **link assinado** com `?sig=&exp=`: ele baixa o arquivo sem o cabeçalho `X-API-Token` e sem consumir a franquia, então serve para entregar o link a um cliente final. Ele vale até a data `exp`; depois dela, consulte a conversão de novo para obter outro.\n</Endpoint>"},{"anchor":"batches","heading":"Batches","markdown":""},{"anchor":"listar-batches","heading":"Listar batches","markdown":"<Endpoint id=\"pdf.batches\" example={{ query: { limit: '20', offset: '0' } }}>\n  `status` do batch: `processing`, `delivering`, `completed`, `partially_failed` ou `failed`.\n</Endpoint>"},{"anchor":"detalhe-do-batch","heading":"Detalhe do batch","markdown":"<Endpoint id=\"pdf.batch-get\" />"},{"anchor":"download-zip-do-batch","heading":"Download ZIP do batch","markdown":"<Endpoint id=\"pdf.batch-download\" example={{ response: 'binary' }}>\n  Disponível quando o batch termina, inclusive parcialmente se alguns itens falharem. Também aceita o link assinado `?sig=&exp=`, sem token.\n</Endpoint>"},{"anchor":"erros","heading":"Erros","markdown":"<ErrorTable tool=\"pdf-convert\" />"},{"anchor":"peculiaridades","heading":"Peculiaridades","markdown":"* Um batch usa um único template HTML; cada item de `items[].data` preenche os placeholders. `custom_id` ajuda a relacionar os arquivos gerados com os seus registros.\n* Para PDFs recorrentes, salve um [template](#templates) em vez de reenviar o HTML a cada chamada.\n* `retention_days: 0` é efêmero (\\~24h) — ideal para previews.\n* Prefira `webhook_url` a consultar o status em loop: cada consulta via API conta na franquia diária."}]},{"slug":"ferramentas/qr-dinamico","url":"https://docs.respawntech.dev/ferramentas/qr-dinamico","title":"QR Dinâmico","description":"API de QR codes dinâmicos — criação, gestão, analytics, custos e redirect público.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"qr-dinamico\" />"},{"anchor":"visão-geral","heading":"Visão geral","markdown":"O QR dinâmico separa a **imagem impressa** (link curto estável) do **destino** (editável sem reimprimir). Fluxo típico:\n\n1. `POST` com `X-Dry-Run: true` para validar o payload e ver o custo.\n2. `POST` definitivo com `X-Idempotency-Key` → recebe a imagem do QR, o `resource_id` e o `slug`.\n3. Use a imagem retornada (ou gere a sua apontando para `full_url`, disponível em `GET /{id}`).\n4. Acompanhe os scans via analytics ou pelo webhook `qr.dynamic.scanned`. O webhook é de melhor esforço (em picos de tráfego, alguns eventos podem não ser enviados); para contagens exatas, use o analytics.\n\n<Callout type=\"warn\" title=\"Redirect público cobra por scan\">\n  `GET /r/{short_id}` não usa autenticação, mas cada scan pode debitar créditos da conta dona do QR. Sem saldo, o QR pode ser desativado. Scans repetidos do mesmo visitante no mesmo QR em até 30 segundos são cobrados uma única vez: continuam redirecionando, mas não entram no analytics, não disparam o webhook nem consomem o `scan_limit`.\n</Callout>\n\n<EndpointList tool=\"qr-dinamico\" />"},{"anchor":"custos","heading":"Custos","markdown":"<PricingTable tool=\"qr-dinamico\" group=\"creation\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"domain\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"batch\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"scans\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"storage\" />\n\nLeituras via API (listagem, detalhe, analytics, scans, verificação de slug) seguem a [franquia diária](/cobranca-e-limites#franquia-diaria). `PATCH`, ativar, desativar e `DELETE` são gratuitos."},{"anchor":"criação-e-gestão","heading":"Criação e gestão","markdown":""},{"anchor":"criar-qr-dinâmico","heading":"Criar QR dinâmico","markdown":"<Endpoint\n  id=\"qr.create\"\n  example={{\n  headers: { 'X-Idempotency-Key': '550e8400-e29b-41d4-a716-446655440000' },\n  body: `{\n\"name\": \"Campanha verão\",\n\"target_url\": \"https://loja.example.com/promo\",\n\"custom_slug\": \"promo-verao\",\n\"foreground_color\": \"#000000\",\n\"background_color\": \"#ffffff\"\n}`,\n}}\n>\n  Campos principais: `name` e `target_url` (o `target_url` é opcional quando há [`content`](#conteudo-hospedado)). As cores e o estilo ficam no próprio corpo (`foreground_color`, `background_color`, `module_style`, `eye_style`, `size`, `logo_data_url`…). Opcionais: `custom_slug`, `custom_domain_id` ([domínio próprio](#dominio-proprio)), `expires_at`, `scan_limit`, `skip_image_generation`, `utm_forwarding`, `folder_id` e `tags` ([organização](#organizacao)), `frame` e `dpi` ([impressão](#impressao)) e as opções de [comportamento do scan](#comportamento-do-scan) — cada uma pode somar custo (veja [Custos](#custos)). `response_types` escolhe os formatos da imagem (`svg`, `png`, `pdf` e `eps`; padrão `svg`): `svg` e `eps` vêm como texto, `png` e `pdf` em base64.\n\n  A resposta traz as imagens, o `resource_id` (id do QR) e o `slug`. Com `skip_image_generation: true`, vem um único item só com `resource_id` e `slug`.\n</Endpoint>"},{"anchor":"listar-qrs","heading":"Listar QRs","markdown":"<Endpoint id=\"qr.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  Query: `limit` (padrão 20, máx. <Limit name=\"pageSizeMax\" />), `offset`, `search`, `folder_id` (um UUID, ou `none` para os QRs sem pasta), `tags` (separadas por vírgula; o QR precisa ter todas) e `status` (`active` ou `paused`).\n</Endpoint>"},{"anchor":"detalhe","heading":"Detalhe","markdown":"<Endpoint id=\"qr.get\" />"},{"anchor":"atualizar-destino-nome-ou-design","heading":"Atualizar destino, nome ou design","markdown":"<Endpoint\n  id=\"qr.update\"\n  example={{\n  body: `{\n\"name\": \"Campanha atualizada\",\n\"target_url\": \"https://loja.example.com/nova-promo\"\n}`,\n}}\n>\n  Mudar nome, destino e cores é grátis. Ligar uma opção paga que estava desligada — `utm_forwarding`, `scan_limit` (de 0 para um valor), gradiente (`foreground_color_end`), olhos customizados (`eye_style` ou `eye_dot_style` `shield`/`flower`) ou logo — cobra o mesmo valor da [criação](#custos), uma única vez por atualização. Alterar uma opção já ligada ou desligá-la é grátis, sem estorno. Aceita `X-Dry-Run`, `X-Include-Cost` e `X-Idempotency-Key`; sem saldo a resposta é `402` e nada muda, uma atualização idêntica já cobrada responde `409`, e uma cor inválida é recusada (`400`) antes de cobrar. A nova `target_url` passa pelas verificações anti-malware e anti-loop. Como em ativar, desativar e excluir, a resposta é apenas uma confirmação — consulte `GET /{id}` para ver o recurso atualizado.\n</Endpoint>"},{"anchor":"reativar","heading":"Reativar","markdown":"<Endpoint id=\"qr.activate\" />"},{"anchor":"desativar","heading":"Desativar","markdown":"<Endpoint id=\"qr.deactivate\">\n  Um QR desativado mostra uma página de QR inativo no redirect público e continua ocupando slot até ser excluído.\n</Endpoint>"},{"anchor":"excluir","heading":"Excluir","markdown":"<Endpoint id=\"qr.delete\">\n  Encerra a cobrança de armazenamento do slot.\n</Endpoint>"},{"anchor":"verificar-disponibilidade-de-slug","heading":"Verificar disponibilidade de slug","markdown":"<Endpoint id=\"qr.check-slug\" example={{ pathParams: { slug: 'promo-verao' } }} />"},{"anchor":"analytics","heading":"Analytics","markdown":""},{"anchor":"overview-de-analytics","heading":"Overview de analytics","markdown":"<Endpoint id=\"qr.analytics\" example={{ query: { period: 'last_30d' } }}>\n  Agregações com cache. Query `period`: `last_7d`, `last_30d` (padrão), `last_90d` ou `all`; valores desconhecidos contam como 30 dias. `scan_history` traz os 20 scans mais recentes.\n</Endpoint>"},{"anchor":"scans-por-período","heading":"Scans por período","markdown":"<Endpoint id=\"qr.analytics-period\" example={{ query: { period: 'last_30d' } }}>\n  Retorna os totais e `period_stats` (scans por dia). Os campos `period`, `period_scans`, `period_cost` e `unique_scans` vêm zerados nesta rota — use o [overview](#qr-analytics) se precisar deles.\n</Endpoint>"},{"anchor":"distribuição-por-dispositivo","heading":"Distribuição por dispositivo","markdown":"<Endpoint id=\"qr.analytics-device\" example={{ query: { period: 'last_30d' } }}>\n  `device` é `Mobile`, `Desktop` ou `Tablet`. Assim como em scans por período, os campos de período vêm zerados.\n</Endpoint>"},{"anchor":"histórico-de-scans","heading":"Histórico de scans","markdown":"<Endpoint id=\"qr.scans\" example={{ query: { limit: '50', offset: '0', period: 'last_30d' } }}>\n  Paginação: `limit` (padrão 50, máx. <Limit name=\"pageSizeMax\" />), `offset` e `period` (padrão: últimos 30 dias). `location` vem como `Unknown` quando cidade e país são desconhecidos.\n</Endpoint>"},{"anchor":"redirect-público","heading":"Redirect público","markdown":"<Endpoint id=\"qr.redirect\" example={false}>\n  * **Ativo:** `307` para a `target_url` (com a query string repassada, se UTM forwarding estiver ativo). Em contas sem plano premium, o scan passa antes por uma página intermediária da marca que redireciona em 3 segundos.\n  * **Inexistente, inativo, expirado, sem saldo, acima do limite de scans ou com destino inseguro:** redireciona para a página de QR indisponível, com o motivo em `?reason=` (`unknown`, `inactive`, `expired`, `bankrupt`, `limit_reached`, `safety_risk`, `rate_limited`).\n  * **Muitos acessos do mesmo visitante:** acima de 600 por minuto (somando todos os QRs), redireciona para a página de QR indisponível com `?reason=rate_limited`. Acima de 30 scans por minuto no mesmo QR, o redirect continua, mas os scans extras não são contados nem cobrados.\n  * As respostas nunca são cacheadas (`Cache-Control: no-store`).\n\n  ```bash\n  curl -sS -I 'https://api.respawntech.dev/r/promo-verao'\n  ```\n</Endpoint>"},{"anchor":"dominio-proprio","heading":"Domínio próprio","markdown":"Em vez de `api.respawntech.dev/r/promo-verao`, o QR pode viver em `https://qr.suaempresa.com/promo-verao`. Você aponta um **subdomínio seu** para a Respawn Tech com um registro CNAME; o certificado HTTPS é emitido automaticamente (Cloudflare for SaaS). O `hostname` precisa ser um subdomínio com pelo menos três rótulos (`qr.suaempresa.com`): um domínio raiz (apex) não aceita CNAME, e porta, caminho, IP e curinga são recusados.\n\nO escopo é `tools:qr:domains:manage` (ou `tools:qr:dynamic:manage` / `tools:manage`). O limite de domínios depende do plano (`qr_domain.plan_limit`) e um domínio ativo é cobrado a cada 30 dias (veja [Custos](#custos)).\n\n<Steps>\n  <Step>\n    ### Cadastre o domínio\n\n    ```bash\n    curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains' \\\n      -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n      -H 'Content-Type: application/json' \\\n      -d '{\n        \"hostname\": \"qr.suaempresa.com\",\n        \"validation_method\": \"http\",\n        \"root_url\": \"https://www.suaempresa.com\"\n      }'\n    ```\n\n    `validation_method` é `http` (padrão) ou `txt`: é como o certificado é validado. A resposta (`201`) traz `status: \"pending\"` e a lista `records` com tudo o que você precisa publicar. Guarde o `id`.\n  </Step>\n\n  <Step>\n    ### Publique o CNAME\n\n    No painel de DNS do seu domínio, crie o registro do item com `purpose: \"routing\"`. O destino (`value`) vem na resposta; não o copie de exemplos.\n\n    | Tipo    | Nome                          | Valor                           |\n    | ------- | ----------------------------- | ------------------------------- |\n    | `CNAME` | `qr` (ou `qr.suaempresa.com`) | o `value` do registro `routing` |\n\n    No **Cloudflare**, crie o CNAME na zona `suaempresa.com` e deixe o proxy **desligado** (nuvem cinza, \"somente DNS\") até o domínio ficar `active`; só então, se quiser, ligue o proxy. Em outros provedores, é um CNAME comum para o subdomínio.\n  </Step>\n\n  <Step>\n    ### Valide o certificado\n\n    Se você escolheu `http`, o registro `purpose: \"certificate\"` com `type: \"HTTP\"` é validado sozinho assim que o CNAME propagar, sem nada a publicar. Se escolheu `txt`, publique também os registros `TXT` que vierem em `records` (`ownership` e `certificate`), no mesmo painel de DNS.\n  </Step>\n\n  <Step>\n    ### Verifique até ficar ativo\n\n    A plataforma consulta o domínio periodicamente; para não esperar, force a verificação:\n\n    ```bash\n    curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains/$DOMAIN_ID/refresh' \\\n      -H \"X-API-Token: $RESPAWN_API_TOKEN\"\n    ```\n\n    O `status` passa de `pending` para `active` (a cobrança do período acontece nesse momento). `failed` indica que o provedor recusou o hostname (veja `last_error`); `suspended`, que o período não pôde ser cobrado — os scans vão para a página de fallback até a próxima cobrança bem-sucedida.\n  </Step>\n\n  <Step>\n    ### Crie QRs no domínio\n\n    Mande o `id` do domínio em `custom_domain_id`. Um domínio ainda `pending` já é aceito, para você imprimir enquanto o DNS propaga, mas os scans só redirecionam depois que ele fica `active`.\n\n    ```bash\n    curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n      -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n      -H 'Content-Type: application/json' \\\n      -d '{\n        \"name\": \"Cartaz da loja\",\n        \"target_url\": \"https://loja.example.com/promo\",\n        \"custom_slug\": \"promo-verao\",\n        \"custom_domain_id\": \"'\"$DOMAIN_ID\"'\"\n      }'\n    ```\n\n    O `full_url` do QR passa a ser `https://qr.suaempresa.com/promo-verao`. O slug só precisa ser único dentro do domínio.\n  </Step>\n</Steps>\n\n`root_url` é para onde vai a raiz do domínio (`https://qr.suaempresa.com/`) e `not_found_url`, para onde vão slugs desconhecidos; sem eles, vale a página de fallback da plataforma. Nenhum dos dois pode apontar de volta para o próprio domínio. Só `root_url` e `not_found_url` mudam depois do cadastro (`PATCH`); para trocar o hostname, remova e cadastre de novo.\n\n<Callout type=\"info\" title=\"Remover um domínio\">\n  Um domínio com QRs ainda vinculados não pode ser removido (`qr_domain.in_use`). Exclua os QRs primeiro; depois de removido o domínio, o `full_url` deles deixa de funcionar.\n</Callout>"},{"anchor":"cadastre-o-domínio","heading":"Listar domínios próprios","markdown":"<Endpoint id=\"qr.domains-list\" />"},{"anchor":"publique-o-cname","heading":"Detalhe do domínio","markdown":"<Endpoint id=\"qr.domains-get\" />"},{"anchor":"valide-o-certificado","heading":"Cadastrar domínio próprio","markdown":"<Endpoint\n  id=\"qr.domains-create\"\n  example={{\n  body: `{\n\"hostname\": \"qr.suaempresa.com\",\n\"validation_method\": \"http\",\n\"root_url\": \"https://www.suaempresa.com\"\n}`,\n}}\n/>"},{"anchor":"verifique-até-ficar-ativo","heading":"Editar páginas de fallback do domínio","markdown":"<Endpoint\n  id=\"qr.domains-update\"\n  example={{\n  body: `{\n\"root_url\": \"https://www.suaempresa.com\",\n\"not_found_url\": \"https://www.suaempresa.com/ops\"\n}`,\n}}\n>\n  Mande `\"\"` para limpar um dos campos.\n</Endpoint>"},{"anchor":"crie-qrs-no-domínio","heading":"Verificar o domínio agora","markdown":"<Endpoint id=\"qr.domains-refresh\" />"},{"anchor":"listar-domínios-próprios","heading":"Remover domínio","markdown":"<Endpoint id=\"qr.domains-delete\" />\n\n## Comportamento do scan\n\nPor padrão o scan é um redirect para a `target_url`. Estes campos, aceitos no `POST` e no `PATCH`, mudam o que acontece no scan. A avaliação é nesta ordem: **senha** (o scan só segue depois dela), **regras** (a primeira que casar vence), **teste A/B** (para os scans que nenhuma regra pegou) e, por fim, a `target_url`. Cada opção é cobrada uma vez (veja [Custos](#custos)).\n\nNum `PATCH`, cada campo enviado **substitui** a sua parte: `[]` limpa as regras ou as variantes, `{}` limpa os pixels e `\"\"` limpa uma URL ou a senha."},{"anchor":"detalhe-do-domínio","heading":"Regras de redirect","markdown":"`redirect_rules` (até 20) manda o scan para uma URL própria quando todas as condições da regra batem. Uma regra sem nenhuma condição é recusada.\n\n| Campo                    | Significado                                                                                       |\n| ------------------------ | ------------------------------------------------------------------------------------------------- |\n| `id`                     | Nome da regra (`[a-zA-Z0-9_-]`, até 40); aparece nos scans e em `rule_stats` do analytics         |\n| `target_url`             | Destino quando a regra casa (URL absoluta)                                                        |\n| `countries`              | Países ISO 3166-1 alfa-2 em maiúsculas (`[\"BR\", \"PT\"]`)                                           |\n| `devices`                | `mobile`, `tablet`, `desktop`                                                                     |\n| `os`                     | `ios`, `android`, `windows`, `macos`, `linux`, `other`                                            |\n| `languages`              | Idioma preferido do visitante: `pt` casa `pt` e `pt-br`; `pt-br` só `pt-br`                       |\n| `days`                   | Dias da semana, `0` (domingo) a `6` (sábado), no `timezone`                                       |\n| `start_time`, `end_time` | Janela do dia (`HH:MM`); se o fim vier antes do início, atravessa a meia-noite. Os dois ou nenhum |\n| `timezone`               | Fuso IANA (`America/Sao_Paulo`); UTC se vazio                                                     |\n| `from`, `until`          | Período em que a regra vale (data e hora RFC 3339)                                                |\n\nCada lista aceita até 50 valores. Dentro de uma regra as condições se somam (E); dentro de uma lista, basta um valor (OU)."},{"anchor":"cadastrar-domínio-próprio","heading":"Teste A/B","markdown":"`ab_variants` (de 2 a 10) divide, por peso, os scans que nenhuma regra pegou. Cada variante tem `id` único, `target_url` e `weight` de 1 a 1000; a fatia de uma variante é o seu peso dividido pela soma. O analytics devolve `variant_stats` com os scans de cada variante."},{"anchor":"editar-páginas-de-fallback-do-domínio","heading":"Senha","markdown":"`password` (de 4 a 72 caracteres) esconde o QR atrás de uma página de senha. A senha é guardada com hash; as respostas trazem apenas `scan_behavior.has_password`. Depois de digitar a senha certa, o visitante segue o fluxo normal e essa página não fica em cache. Após 5 senhas erradas, o visitante fica bloqueado naquele QR por 15 minutos."},{"anchor":"verificar-o-domínio-agora","heading":"Pixels","markdown":"`pixels` dispara as tags numa página leve antes do redirect: `ga4_measurement_id` (`G-XXXXXXX`), `meta_pixel_id` (só dígitos) e `gtm_container_id` (`GTM-XXXXXX`)."},{"anchor":"remover-domínio","heading":"Redirect de expirado e esgotado","markdown":"`expired_url` e `limit_reached_url` (URLs absolutas) substituem a página de QR indisponível quando o QR expirou (`expires_at`) ou usou todo o `scan_limit`.\n\n### Exemplo completo\n\nCopie, troque as URLs e rode:\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Campanha com regras\",\n    \"target_url\": \"https://loja.example.com/global\",\n    \"redirect_rules\": [\n      {\n        \"id\": \"br-mobile\",\n        \"target_url\": \"https://loja.example.com/br\",\n        \"countries\": [\"BR\"],\n        \"devices\": [\"mobile\"]\n      },\n      {\n        \"id\": \"horario-comercial\",\n        \"target_url\": \"https://loja.example.com/atendimento\",\n        \"days\": [1, 2, 3, 4, 5],\n        \"start_time\": \"09:00\",\n        \"end_time\": \"18:00\",\n        \"timezone\": \"America/Sao_Paulo\"\n      }\n    ],\n    \"ab_variants\": [\n      { \"id\": \"a\", \"target_url\": \"https://loja.example.com/lp-a\", \"weight\": 50 },\n      { \"id\": \"b\", \"target_url\": \"https://loja.example.com/lp-b\", \"weight\": 50 }\n    ],\n    \"pixels\": { \"ga4_measurement_id\": \"G-ABCD1234\" },\n    \"expired_url\": \"https://loja.example.com/encerrada\",\n    \"limit_reached_url\": \"https://loja.example.com/esgotado\"\n  }'\n```\n\nPara tirar tudo depois, sem recriar o QR (troque `$QR_ID` pelo `resource_id` da criação):\n\n```bash\ncurl -sS -X PATCH \"https://api.respawntech.dev/v1/tools/image/qr-code/dynamic/$QR_ID\" \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{ \"redirect_rules\": [], \"ab_variants\": [], \"pixels\": {}, \"password\": \"\" }'\n```\n\n`GET /{id}` devolve a configuração em `scan_behavior` (`redirect_rules`, `ab_variants`, `has_password`, `pixels`, `expired_url`, `limit_reached_url`, `content`)."},{"anchor":"comportamento-do-scan","heading":"Conteúdo hospedado","markdown":"Com `content`, o próprio `/r/{short_id}` serve uma página leve (ou um redirect calculado) no lugar da `target_url`. O `type` escolhe o formato e só o campo de mesmo nome é usado. Se uma regra de redirect ou um teste A/B casar, ele vale no lugar do conteúdo. As páginas não executam scripts e ficam em cache por 5 minutos (revalidadas por ETag). `{}` limpa o conteúdo num `PATCH`.\n\n| `type`      | Campo                                                                                     | O scan mostra                                                                                                         |\n| ----------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |\n| `vcard`     | `vcard` (`FirstName`, `LastName`, `Org`, `Title`, `Phone`, `Email`, `Website`, `Address`) | Página de contato e download do `.vcf` (`?format=vcf`). Exige nome ou organização                                     |\n| `file`      | `file` (`url`, `name`)                                                                    | Abre o arquivo ou PDF hospedado (URL absoluta)                                                                        |\n| `links`     | `links` (`title`, `description`, `avatar_url`, `links[]` com `label` e `url`)             | Link na bio, de 1 a 50 links                                                                                          |\n| `menu`      | `menu` (`title`, `currency`, `sections[]` → `items[]` com `name`, `description`, `price`) | Cardápio, até 30 seções e 300 itens                                                                                   |\n| `wifi`      | `wifi` (`SSID`, `Password`, `Security`, `Hidden`)                                         | Nome da rede e senha para copiar                                                                                      |\n| `app_store` | `app_store` (`ios_url`, `android_url`, `fallback_url`)                                    | Redirect para a App Store ou o Google Play conforme o celular; `fallback_url` atende desktop e sistemas desconhecidos |\n\nOs campos de `vcard` e `wifi` usam esses nomes em PascalCase, exatamente como na tabela; os demais usam `snake_case`. O `target_url` deixa de ser obrigatório. Ele assume o link do arquivo ou da loja e, nos tipos de página, cai na página de fallback da plataforma.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Link na bio\",\n    \"content\": {\n      \"type\": \"links\",\n      \"links\": {\n        \"title\": \"Loja Exemplo\",\n        \"links\": [\n          { \"label\": \"Site\", \"url\": \"https://loja.example.com\" },\n          { \"label\": \"Instagram\", \"url\": \"https://instagram.com/lojaexemplo\" }\n        ]\n      }\n    }\n  }'\n```\n\nExemplo de cartão de contato:\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Cartão da Ana\",\n    \"content\": {\n      \"type\": \"vcard\",\n      \"vcard\": { \"FirstName\": \"Ana\", \"LastName\": \"Souza\", \"Org\": \"Loja Exemplo\", \"Phone\": \"+5511999990000\", \"Email\": \"ana@loja.example.com\" }\n    }\n  }'\n```\n\n## Molduras e impressão\n\n`frame` desenha uma chamada para ação em volta do código e `dpi` deixa a imagem pronta para impressão. Os dois valem para qualquer `response_types`.\n\n* `frame`: `text` (até 32 caracteres; encolhe para caber), `color`, `text_color` (hex) e `position` (`top` ou `bottom`).\n* `dpi`: de 72 a 1200. No **PDF** a página fica do tamanho exato do QR, em vetor; no **SVG** a largura e a altura saem em mm; o **PNG** carrega a resolução de impressão no arquivo. Sem `dpi`, o PDF mantém o layout A4.\n* `eps`: formato vetorial novo, com a moldura e fonte Helvetica-Bold com acentos. Sai sem logo.\n\nNo PDF e no EPS, os olhos saem quadrados e só os estilos de módulo `dots` e quadrado viram vetor; o gradiente não é aplicado.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Cartaz A5\",\n    \"target_url\": \"https://loja.example.com/promo\",\n    \"response_types\": [\"pdf\", \"eps\"],\n    \"frame\": { \"text\": \"Escaneie aqui\", \"color\": \"#111111\", \"text_color\": \"#FFFFFF\", \"position\": \"bottom\" },\n    \"dpi\": 300\n  }'\n```\n\n## Organização, edição em massa e lote"},{"anchor":"regras","heading":"Pastas e tags","markdown":"`folder_id` guarda o QR numa pasta e `tags` (até 20 por QR, 40 caracteres cada, sem vírgula) o etiqueta; os dois entram no `POST` e no `PATCH` (`folder_id: \"\"` volta para a raiz, `tags: []` limpa). A listagem filtra por `folder_id`, `tags`, `status` e `search`."},{"anchor":"ab","heading":"Listar pastas","markdown":"<Endpoint id=\"qr.folders-list\" />"},{"anchor":"senha","heading":"Criar pasta","markdown":"<Endpoint id=\"qr.folders-create\" example={{ body: '{ \"name\": \"Campanha de verão\" }' }}>\n  O nome tem de 1 a 80 caracteres e é único na conta (`dynamic_qr.folder_taken`).\n</Endpoint>"},{"anchor":"pixels","heading":"Renomear pasta","markdown":"<Endpoint id=\"qr.folders-rename\" example={{ body: '{ \"name\": \"Verão 2027\" }' }} />"},{"anchor":"fallback","heading":"Excluir pasta","markdown":"<Endpoint id=\"qr.folders-delete\" />"},{"anchor":"exemplo-completo","heading":"Editar vários QRs de uma vez","markdown":"<Endpoint\n  id=\"qr.bulk\"\n  example={{\n  body: `{\n\"ids\": [\"8f3a2b1c-4d5e-6f70-8192-a3b4c5d6e7f8\"],\n\"action\": \"add_tags\",\n\"tags\": [\"verao\"]\n}`,\n}}\n>\n  Uma ação em até 500 QRs, de graça. `action` é `activate`, `deactivate`, `move` (com `folder_id`; `null` manda para a raiz), `set_target` (com `target_url`), `add_tags` ou `remove_tags` (com `tags`). A resposta traz o resultado de cada QR em `items`, então uma falha não derruba as demais. Aceita `X-Idempotency-Key`.\n</Endpoint>\n\n### Criar QRs em lote\n\n<Endpoint\n  id=\"qr.batches-create\"\n  example={{\n  headers: { 'X-Idempotency-Key': '7f1c2e84-5b3a-4d6c-9e01-a2b3c4d5e6f7' },\n  body: `{\n\"csv\": \"name,target_url,custom_slug,tags\\\\nLoja Centro,https://loja.example.com/centro,loja-centro,varejo|sp\\\\nLoja Norte,https://loja.example.com/norte,loja-norte,varejo\",\n\"defaults\": { \"utm_forwarding\": true }\n}`,\n}}\n>\n  Até 1.000 linhas por lote, de um destes jeitos:\n\n  * JSON com `rows` (uma lista de objetos com os campos do QR) ou `csv` (o texto do CSV), mais `defaults` com os valores usados nas linhas que não os trazem (`folder_id`, `tags`, `scan_limit`, `expires_at`, `utm_forwarding`, cores, `custom_domain_id`);\n  * corpo `text/csv` direto, com `folder_id` e `tags` como parâmetros de query.\n\n  O CSV tem uma linha de cabeçalho e as colunas `name`, `target_url` (obrigatória), `custom_slug`, `folder_id`, `tags` (separadas por `|`), `scan_limit`, `expires_at`, `utm_forwarding`, `foreground_color` e `background_color`:\n\n  ```csv\n  name,target_url,custom_slug,tags\n  Loja Centro,https://loja.example.com/centro,loja-centro,varejo|sp\n  Loja Norte,https://loja.example.com/norte,loja-norte,varejo\n  ```\n\n  ```bash\n  curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/batches?tags=campanha' \\\n    -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n    -H 'X-Idempotency-Key: 7f1c2e84-5b3a-4d6c-9e01-a2b3c4d5e6f7' \\\n    -H 'Content-Type: text/csv' \\\n    --data-binary @lojas.csv\n  ```\n\n  As linhas inválidas são reportadas e **não** são cobradas; as válidas são cobradas de uma vez, com desconto por volume (a partir de 100, 500 e 1.000 linhas), e criadas em segundo plano (`202`). Uma linha que falhar depois é estornada. Acompanhe consultando o detalhe do lote até `pending` chegar a 0. Com o mesmo `X-Idempotency-Key` e as mesmas linhas, o retry devolve o mesmo lote, cobrado uma vez. Acima do limite, a resposta é `413` (`dynamic_qr.batch_too_large`).\n</Endpoint>\n\n### Listar lotes\n\n<Endpoint id=\"qr.batches-list\" />\n\n### Detalhe do lote\n\n<Endpoint id=\"qr.batches-get\">\n  `items` traz cada linha (`row`, `status`, `qr_id`, `slug`, `full_url` ou `error_code` e `error_message`). `charged_units`, `discount_units` e `refunded_units` estão nas unidades internas de crédito (10.000 unidades = 1 crédito).\n</Endpoint>\n\n### Exportar scans em CSV\n\n<Endpoint id=\"qr.scans-export\" example={{ query: { period: 'last_30d' } }}>\n  Query: `period` (`last_7d`, `last_30d` padrão, `last_90d` ou `all`) ou `from` e `to` (`YYYY-MM-DD`, UTC, inclusive), que substituem o período. Cobrada como o [histórico de scans](#qr-scans). Salve com `curl -o scans.csv`.\n</Endpoint>\n\n### Apagar o histórico de scans\n\n<Endpoint id=\"qr.scans-purge\" example={{ query: { before: '2026-01-01' } }}>\n  Apaga as linhas de scan (todas, ou as anteriores a `before`, `YYYY-MM-DD` UTC). `total_scans` e `scan_limit` são mantidos. É irreversível.\n</Endpoint>"},{"anchor":"conteudo-hospedado","heading":"Erros","markdown":"<ErrorTable tool=\"qr-dinamico\" />"},{"anchor":"impressao","heading":"Peculiaridades e limites","markdown":"* URLs maliciosas são bloqueadas automaticamente (`dynamic_qr.malicious_url`).\n* Proteção anti-loop: o destino não pode apontar para o próprio domínio de redirect.\n* O primeiro QR não ocupa slot pago; os demais ocupam slots em grupos de 3.\n* `skip_image_generation: true` reduz o custo quando você gera a imagem do QR por conta própria.\n* Slug: 3–50 caracteres `[a-zA-Z0-9-]`. Listagens: até <Limit name=\"pageSizeMax\" /> itens por página."}]},{"slug":"ferramentas/realtime","url":"https://docs.respawntech.dev/ferramentas/realtime","title":"Realtime","description":"Publique e receba mensagens em tempo real por WebSocket ou SSE, com tokens emitidos no seu servidor, presença, histórico por canal, webhook de canais e reconexão sem perder mensagens.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"realtime\" />"},{"anchor":"como-funciona","heading":"Como funciona","markdown":"O Realtime leva mensagens do seu servidor, ou de outros clientes, para os navegadores conectados, sem você manter servidores de WebSocket.\n\n* **Canal:** um nome livre de até <Limit name=\"realtimeChannelNameMax\" /> caracteres, com letras, números e `: _ - .` (`chat:lobby`, `pedido:123`). Quem assina o canal recebe o que for publicado nele. Não há nada para criar antes.\n* **Token:** credencial de curta duração que o **seu servidor** emite com a sua chave de API e entrega ao cliente. Ele diz quem é o cliente (`client_id`, opcional) e em quais canais pode assinar, publicar e usar a presença (`capabilities`).\n* **`connect_url`:** o endereço WebSocket para onde o cliente conecta, devolvido na mesma resposta do token e já com o `?token=`.\n* **Mensagem:** `data` JSON publicada num canal, com `id`, `timestamp` (milissegundos Unix) e o `client_id` de quem publicou.\n* **Presença:** a lista de quem está conectado e \"presente\" num canal, com dados livres de cada membro (um apelido, um status).\n\nO fluxo é sempre o mesmo:\n\n<Steps>\n  <Step>\n    O navegador pede um token ao **seu servidor**, que já sabe quem é o usuário.\n  </Step>\n\n  <Step>\n    O seu servidor [emite o token](#realtime-token) na API com as capabilities daquele usuário e devolve ao navegador a resposta inteira: `token`, `expires_at` e `connect_url`.\n  </Step>\n\n  <Step>\n    O navegador conecta em `connect_url`, assina canais e publica.\n  </Step>\n\n  <Step>\n    Se a conexão cair, o navegador pede um token novo e [reconecta](#reconexao), recuperando as mensagens perdidas com `rewind`.\n  </Step>\n</Steps>\n\n<Callout type=\"warn\" title=\"Use sempre o connect_url da resposta\">\n  Nunca grave o endereço do WebSocket no código ou na configuração. O `connect_url` pode mudar entre ambientes (teste e produção) e quando a plataforma redistribui as conexões; um token só vale no endereço que veio com ele.\n</Callout>\n\nA sua chave de API (`X-API-Token`) fica só no servidor. O navegador usa apenas o token de conexão, que expira e só enxerga os canais que você liberou.\n\n<EndpointList tool=\"realtime\" />"},{"anchor":"token-e-capabilities","heading":"Token e capabilities","markdown":""},{"anchor":"emitir-token-de-conexão","heading":"Emitir token de conexão","markdown":"<Endpoint id=\"realtime.token\" example={{ body: `{ \"client_id\": \"user-42\", \"capabilities\": { \"chat:*\": [\"subscribe\", \"publish\", \"presence\"], \"alerts\": [\"subscribe\"] }, \"ttl_seconds\": 3600 }` }}>\n  * `capabilities` (obrigatório): objeto de padrão de canal para a lista de operações permitidas, até <Limit name=\"realtimeTokenPatternsMax\" /> padrões. O padrão é um nome exato (`alerts`), um prefixo terminado em `*` (`chat:*`) ou só `*` (todos os canais). As operações são `subscribe`, `publish`, `presence` (entrar na presença; ler a presença exige `subscribe`) ou `*` (todas). Sem capability para o canal, a operação falha com `realtime.capability_denied`.\n  * `client_id` (opcional, até <Limit name=\"realtimeClientIdMax\" /> caracteres): identifica o cliente nas mensagens que ele publica e na presença. Use o id do seu usuário; não aceite esse valor vindo do navegador.\n  * `ttl_seconds` (opcional): validade do token, de <Limit name=\"realtimeTokenTtlMin\" /> até <Limit name=\"realtimeTokenTtlMax\" />, padrão <Limit name=\"realtimeTokenTtlDefault\" />. O token só é checado ao conectar: uma conexão aberta continua valendo depois que ele expira, mas a próxima reconexão exige um token novo.\n\n  A resposta traz `token`, `expires_at` e `connect_url` (que já inclui `?token=`). Quando a plataforma precisa ligar um nó antes de aceitar conexões, a resposta também traz `retry_after_ms`: o token é válido, mas **espere esse tempo antes de conectar**.\n</Endpoint>\n\nDê a cada cliente só o que ele precisa: um chat com `{\"chat:lobby\": [\"subscribe\", \"publish\", \"presence\"]}`, um painel somente leitura com `{\"metricas:*\": [\"subscribe\"]}`. O token é assinado com HMAC (HS256) e só vale para o Realtime.\n\n<Callout title=\"Token do inspetor\">\n  O dashboard usa a rota `POST /v1/tools/realtime/inspector-token` para abrir o inspetor ao vivo (assinatura de todos os canais, sem publicar, 15 minutos). Ela só aceita a sessão do dashboard; com uma chave de API responde `realtime.session_required`.\n</Callout>"},{"anchor":"conexao-pelo-navegador","heading":"Conexão pelo navegador","markdown":"No seu servidor, exponha uma rota que emite o token e repassa a resposta. O exemplo abaixo usa `RESPAWN_API_TOKEN` e uma rota `/realtime-token`; troque o `client_id` pelo usuário autenticado.\n\n<Tabs groupId=\"code-language\" persist items={['Node.js', 'Python']}>\n  <Tab value=\"Node.js\">\n    ```js\n    import { createServer } from 'node:http';\n\n    createServer(async (req, res) => {\n      if (req.url !== '/realtime-token') return res.writeHead(404).end();\n\n      const api = await fetch('https://api.respawntech.dev/v1/tools/realtime/tokens', {\n        method: 'POST',\n        headers: { 'X-API-Token': process.env.RESPAWN_API_TOKEN, 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          client_id: 'user-42', // o usuário autenticado\n          capabilities: { 'chat:*': ['subscribe', 'publish', 'presence'] },\n        }),\n      });\n      const { data } = await api.json();\n\n      // token, expires_at, connect_url e retry_after_ms seguem juntos para o navegador\n      res.writeHead(api.status, { 'Content-Type': 'application/json' }).end(JSON.stringify(data));\n    }).listen(3000);\n    ```\n  </Tab>\n\n  <Tab value=\"Python\">\n    ```python\n    import os\n    import requests\n    from flask import Flask, jsonify\n\n    app = Flask(__name__)\n\n    @app.post(\"/realtime-token\")\n    def realtime_token():\n        api = requests.post(\n            \"https://api.respawntech.dev/v1/tools/realtime/tokens\",\n            headers={\"X-API-Token\": os.environ[\"RESPAWN_API_TOKEN\"]},\n            json={\n                \"client_id\": \"user-42\",  # o usuário autenticado\n                \"capabilities\": {\"chat:*\": [\"subscribe\", \"publish\", \"presence\"]},\n            },\n            timeout=30,\n        )\n        # token, expires_at, connect_url e retry_after_ms seguem juntos para o navegador\n        return jsonify(api.json()[\"data\"]), api.status_code\n    ```\n  </Tab>\n</Tabs>\n\nNo navegador, peça o token ao seu servidor e conecte no `connect_url` que veio nele. Este HTML funciona colado em um arquivo e servido pelo mesmo servidor da rota `/realtime-token` (em teste, use uma chave do ambiente de teste); ele já reconecta sozinho. Se preferir não escrever o protocolo à mão, use o [SDK JavaScript](#sdk).\n\n```html\n<!doctype html>\n<meta charset=\"utf-8\" />\n<title>Realtime</title>\n<ul id=\"log\"></ul>\n<input id=\"text\" placeholder=\"Mensagem\" />\n<button id=\"send\">Enviar</button>\n\n<script>\n  const CHANNEL = 'chat:lobby';\n  const REWIND = 50; // mensagens recuperadas a cada (re)conexão\n  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n  const seen = new Set(); // ids já exibidos: o rewind pode repetir mensagens\n  let ws = null;\n  let attempt = 0;\n\n  function show(text) {\n    const li = document.createElement('li');\n    li.textContent = text;\n    document.getElementById('log').append(li);\n  }\n\n  async function connect() {\n    // Um token novo a cada conexão: cobre expiração e o close 4001\n    const res = await fetch('/realtime-token', { method: 'POST' });\n    if (!res.ok) return retry();\n    const { connect_url, retry_after_ms } = await res.json();\n    if (retry_after_ms) await sleep(retry_after_ms);\n\n    ws = new WebSocket(connect_url); // o connect_url já leva o ?token=\n\n    ws.onmessage = (event) => {\n      const frame = JSON.parse(event.data);\n      if (frame.action === 'connected') {\n        attempt = 0;\n        ws.send(JSON.stringify({ action: 'subscribe', channel: CHANNEL, rewind: REWIND }));\n      } else if (frame.action === 'message') {\n        if (seen.has(frame.id)) return;\n        seen.add(frame.id);\n        show(`${frame.client_id ?? '?'}: ${JSON.stringify(frame.data)}`);\n      } else if (frame.action === 'error') {\n        console.warn('realtime error', frame.code, frame.message);\n      }\n    };\n\n    ws.onclose = (event) => {\n      // 4003: Realtime desativado para a conta, não reconecte.\n      if (event.code === 4003) return;\n      // 1012: reinício do nó, reconecte já. 1013 e 4029: backoff.\n      // 4001: token inválido, o próximo connect() já emite outro.\n      retry(event.code === 1012 ? 0 : undefined);\n    };\n  }\n\n  async function retry(delay) {\n    attempt += 1;\n    // backoff exponencial com jitter, teto de 30 s\n    const wait = delay ?? Math.random() * Math.min(30000, 500 * 2 ** attempt);\n    await sleep(wait);\n    connect();\n  }\n\n  document.getElementById('send').onclick = () => {\n    const input = document.getElementById('text');\n    if (ws?.readyState !== WebSocket.OPEN || !input.value) return;\n    ws.send(JSON.stringify({ action: 'publish', channel: CHANNEL, data: { text: input.value } }));\n    input.value = '';\n  };\n\n  connect();\n</script>\n```"},{"anchor":"protocolo","heading":"Protocolo","markdown":"As mensagens da conexão são frames de texto JSON, cada um com um campo `action`. Depois de conectar, espere o frame `connected` antes de assinar. O nome do canal segue a regra `^[A-Za-z0-9:_\\-.]{1,128}$`."},{"anchor":"do-cliente-para-o-servidor","heading":"Do cliente para o servidor","markdown":"| `action`      | Campos                            | O que faz                                                                                                                                                                                            |\n| ------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `subscribe`   | `channel`, `rewind?`              | Assina o canal (exige `subscribe` no token). Com `rewind` (número de mensagens, até <Limit name=\"realtimeRewindMax\" />), reenvia as últimas mensagens do canal antes das novas. Responde `attached`. |\n| `unsubscribe` | `channel`                         | Deixa de receber o canal. Responde `detached`.                                                                                                                                                       |\n| `publish`     | `channel`, `name?`, `data`, `id?` | Publica `data` (JSON) no canal (exige `publish`). `name` classifica a mensagem. Se enviar `id`, o servidor responde `ack` com ele.                                                                   |\n| `presence`    | `op`, `channel`, `data?`, `id?`   | Operações de [presença](#presenca): `op` é `enter`, `update`, `leave` ou `get`.                                                                                                                      |"},{"anchor":"do-servidor-para-o-cliente","heading":"Do servidor para o cliente","markdown":"| `action`    | Campos                                                      | Quando                                                                                                       |\n| ----------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |\n| `connected` | `connection_id`                                             | A conexão foi aceita; já pode assinar.                                                                       |\n| `attached`  | `channel`                                                   | A assinatura do canal está ativa (depois do `rewind`, se pedido).                                            |\n| `detached`  | `channel`                                                   | A assinatura foi encerrada.                                                                                  |\n| `message`   | `channel`, `id`, `name?`, `data`, `timestamp`, `client_id?` | Uma mensagem do canal, inclusive as que você mesmo publicou. `timestamp` é um inteiro em milissegundos Unix. |\n| `ack`       | `id`, `channel?`                                            | A publicação (ou operação de presença) com esse `id` foi aceita.                                             |\n| `presence`  | `op`, `channel`, `member` ou `members`                      | Evento de presença (`enter`, `update`, `leave`) ou resposta ao `get` (`sync`).                               |\n| `error`     | `code`, `message`, `channel?`, `id?`                        | Uma operação falhou. Os `code` estão em [Erros](#erros). A conexão continua aberta.                          |\n\nExemplo de troca:\n\n```json\n← { \"action\": \"connected\", \"connection_id\": \"c_8f3a1d\" }\n→ { \"action\": \"subscribe\", \"channel\": \"chat:lobby\", \"rewind\": 20 }\n← { \"action\": \"attached\", \"channel\": \"chat:lobby\" }\n→ { \"action\": \"publish\", \"channel\": \"chat:lobby\", \"id\": \"m-1\", \"data\": { \"text\": \"Olá!\" } }\n← { \"action\": \"message\", \"channel\": \"chat:lobby\", \"id\": \"1790338000000-0\", \"data\": { \"text\": \"Olá!\" }, \"timestamp\": 1790338000000, \"client_id\": \"user-42\" }\n← { \"action\": \"ack\", \"id\": \"m-1\" }\n```\n\nO `id` do `message` é gerado pelo servidor e é o que você usa para descartar repetições. O `id` do `publish` é só seu, para casar com o `ack`."},{"anchor":"close-codes","heading":"Close codes","markdown":"| Código         | Significado                                                         | O que fazer                                                         |\n| -------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- |\n| `1012`         | O nó está reiniciando (deploy ou manutenção).                       | Reconecte já, com um pequeno jitter.                                |\n| `1013`         | O nó está cheio.                                                    | Reconecte com backoff exponencial.                                  |\n| `4001`         | Token inválido ou expirado (`realtime.token_invalid`).              | Emita um token novo e reconecte; repetir o mesmo token não adianta. |\n| `4003`         | Realtime desativado para a conta (`realtime.disabled`).             | Não reconecte.                                                      |\n| `4029`         | A conta atingiu o limite de conexões (`realtime.connection_limit`). | Reconecte com backoff exponencial.                                  |\n| Qualquer outro | Queda de rede ou erro inesperado.                                   | Reconecte com backoff exponencial.                                  |"},{"anchor":"sse","heading":"SSE: leitura sem WebSocket","markdown":"Para clientes que não conseguem abrir um WebSocket, o mesmo token abre um fluxo `text/event-stream` **só de leitura**, no mesmo servidor do `connect_url`:\n\n```text\nGET https://<host do connect_url>/v1/sse?token=<token>&channel=chat:lobby&channel=alerts&rewind=20\n```\n\n* Informe cada canal em um `channel=` (ou vários em `channels=a,b`), até <Limit name=\"realtimeChannelsPerConnection\" />. O token precisa de `subscribe` em todos.\n* Cada `data:` é um frame JSON igual ao do WebSocket (`connected`, `attached`, `message`, `presence`, `error`); linhas `: ping` mantêm a conexão viva.\n* O fluxo conta no limite de conexões da conta e termina quando o token expira (antes vem um frame `error` com `realtime.token_invalid`) ou quando o nó é reiniciado.\n* Para publicar, use a [API](#publicar) ou um WebSocket.\n\n```js\nconst url = new URL(connectUrl); // o connect_url da resposta do token\nconst sse = new EventSource(\n  `https://${url.host}/v1/sse?token=${encodeURIComponent(token)}&channel=chat:lobby&rewind=20`,\n);\nsse.onmessage = (event) => {\n  const frame = JSON.parse(event.data);\n  if (frame.action === 'message') console.log(frame.data);\n};\n```"},{"anchor":"presenca","heading":"Presença","markdown":"A presença diz quem está num canal agora. Um cliente com a capability `presence` **entra** com dados livres (`{\"nick\": \"ana\"}`), pode **atualizar** esses dados e **sai** ao deixar o canal ou quando a conexão cai. Quem assina o canal recebe os eventos, e `get` (ou a API) devolve a lista completa.\n\n| Frame do cliente                                                                 | Exige       | Efeito                                        |\n| -------------------------------------------------------------------------------- | ----------- | --------------------------------------------- |\n| `{\"action\":\"presence\",\"op\":\"enter\",\"channel\":\"chat:lobby\",\"data\":{…},\"id\":\"p1\"}` | `presence`  | Entra na presença; responde `ack`.            |\n| `{\"action\":\"presence\",\"op\":\"update\",\"channel\":\"chat:lobby\",\"data\":{…}}`          | `presence`  | Atualiza os dados do membro.                  |\n| `{\"action\":\"presence\",\"op\":\"leave\",\"channel\":\"chat:lobby\"}`                      | `presence`  | Sai da presença.                              |\n| `{\"action\":\"presence\",\"op\":\"get\",\"channel\":\"chat:lobby\",\"id\":\"p2\"}`              | `subscribe` | Responde `op: \"sync\"` com todos os `members`. |\n\nOs assinantes recebem `{\"action\":\"presence\",\"op\":\"enter|update|leave\",\"channel\":\"chat:lobby\",\"member\":{\"connection_id\":\"…\",\"client_id\":\"user-42\",\"data\":{…},\"timestamp\":1790338000000}}`. Cada conexão é um membro (o mesmo usuário em duas abas conta duas vezes). Um canal comporta até <Limit name=\"realtimePresenceMembers\" /> membros; acima disso, `realtime.presence_limit`. Se um nó cair sem avisar, os membros dele somem da lista em até 30 segundos."},{"anchor":"listar-a-presença-do-canal","heading":"Listar a presença do canal","markdown":"<Endpoint id=\"realtime.presence\" example={{ pathParams: { channel: 'chat:lobby' } }}>\n  Devolve os membros do canal neste instante. É útil para mostrar \"quem está online\" a partir do seu servidor, sem abrir uma conexão.\n</Endpoint>"},{"anchor":"reconexao","heading":"Reconexão","markdown":"Conexões caem: o nó reinicia, a rede oscila, o token expira. Toda aplicação precisa reconectar sozinha. O exemplo da seção [Conexão pelo navegador](#conexao-pelo-navegador) já faz isso (o [SDK](#sdk) também); as regras são estas e os [close codes](#protocolo) dizem quando reconectar."},{"anchor":"backoff","heading":"Backoff","markdown":"Use backoff exponencial **com jitter** (um valor aleatório até o teto da tentativa) e teto de <Limit name=\"realtimeBackoffMax\" />: por exemplo, 0,5 s, 1 s, 2 s, 4 s… Sem o jitter, milhares de clientes reconectam no mesmo instante depois de uma queda e derrubam o nó de novo. Zere a contagem quando receber `connected`."},{"anchor":"token-novo-e-retry_after_ms","heading":"Token novo e `retry_after_ms`","markdown":"Peça um token novo a cada reconexão: ele é barato (grátis) e evita a conexão com token vencido. Se a resposta trouxer `retry_after_ms`, espere esse tempo antes de conectar; ele vale em lugar do backoff daquela tentativa. Use sempre o `connect_url` da resposta mais recente."},{"anchor":"recuperar-mensagens-com-rewind","heading":"Recuperar mensagens com `rewind`","markdown":"Uma conexão nova não sabe o que passou enquanto estava fora. Depois de `connected`, assine de novo cada canal com `rewind`: o servidor reenvia as últimas mensagens antes de entregar as novas, e o `attached` marca o fim da recuperação. Como o `rewind` pode repetir mensagens que você já exibiu, descarte as que tiverem um `id` já visto. Entre de novo na presença de cada canal em que estava.\n\nEscolha o `rewind` pelo tempo que a conexão costuma ficar fora (até <Limit name=\"realtimeRewindMax\" /> mensagens). Se o canal teve mais mensagens que isso durante a queda, a diferença se perdeu: o histórico guarda só as últimas."},{"anchor":"historico","heading":"Histórico","markdown":"Cada canal guarda as últimas <Limit name=\"realtimeHistoryPageMax\" /> mensagens publicadas, por até <Limit name=\"realtimeHistoryRetention\" />. O `rewind` usa esse histórico; para consultá-lo a partir do seu servidor, use a API."},{"anchor":"consultar-histórico-do-canal","heading":"Consultar histórico do canal","markdown":"<Endpoint id=\"realtime.history\" example={{ pathParams: { channel: 'chat:lobby' }, query: { limit: '50' } }}>\n  Devolve as mensagens **da mais antiga para a mais nova**, em `messages` (`id`, `name`, `data`, `timestamp`, `client_id`), junto de `channel` e do `limit` aplicado. Não há cursor nem páginas: `limit` vale no máximo <Limit name=\"realtimeHistoryPageMax\" /> (o padrão é o máximo). É uma leitura medida: entra na [franquia diária de leituras](/cobranca-e-limites#franquia-diaria) da conta.\n</Endpoint>"},{"anchor":"publicar","heading":"Publicar mensagem pelo servidor","markdown":"Publicar pela API é útil para eventos que nascem no seu backend (um pedido pago, um alerta). A mensagem chega aos assinantes como qualquer outra.\n\n<Endpoint id=\"realtime.publish\" example={{ pathParams: { channel: 'chat:lobby' }, body: `{ \"name\": \"message\", \"data\": { \"text\": \"Pedido aprovado\" }, \"client_id\": \"sistema\" }` }}>\n  * `data`: qualquer JSON, até <Limit name=\"realtimeMessageMax\" /> (somados ao `name`).\n  * `name` (opcional, até 128 caracteres): classifica a mensagem.\n  * `client_id` (opcional): o remetente que os assinantes veem.\n\n  A resposta traz `id`, `timestamp` (milissegundos Unix) e `charged`: `true` quando a mensagem passou da franquia mensal e foi cobrada.\n\n  Com `X-Dry-Run: true`, a API valida o canal e o tamanho, não publica nem cobra, e responde `200` com a estimativa:\n\n  ```json\n  { \"success\": true, \"data\": { \"cost_units\": 0, \"within_quota\": true, \"free_used\": 18250, \"free_monthly\": 200000, \"would_charge\": false } }\n  ```\n\n  `cost_units` usa a escala do backend (10.000 unidades = 1 crédito) e é `0` dentro da franquia.\n</Endpoint>"},{"anchor":"canais","heading":"Canais e estatísticas","markdown":""},{"anchor":"listar-canais-ativos","heading":"Listar canais ativos","markdown":"<Endpoint id=\"realtime.channels\" example={{ query: { limit: '50' } }}>\n  Lista os canais com assinantes ou com uma publicação dentro da janela do histórico, com `name`, `subscribers` (a contagem atrasa até 10 segundos) e `last_publish_at`. Use `limit` (1 a <Limit name=\"realtimeChannelListPageMax\" />, padrão 50) e `offset`; a resposta traz `total` e, quando há mais páginas, `next_offset` para a chamada seguinte.\n</Endpoint>"},{"anchor":"consultar-uso-e-conexões","heading":"Consultar uso e conexões","markdown":"<Endpoint id=\"realtime.stats\">\n  Mostra as `connections` abertas agora (somadas em todos os nós), os `active_channels` (com pelo menos um assinante), o `month` (UTC) e as `messages_this_month`, junto da franquia: `free_monthly_messages` e `free_remaining`.\n</Endpoint>"},{"anchor":"webhooks-de-canais","heading":"Webhooks de canais","markdown":"Cada conta pode ter **um** webhook de canais, que avisa o seu servidor quando um canal fica ocupado ou vazio, quando alguém entra ou sai da presença e (opcionalmente) a cada mensagem. É uma configuração do Realtime, separada dos [webhooks da conta](/webhooks)."},{"anchor":"consultar-o-webhook-de-canais","heading":"Consultar o webhook de canais","markdown":"<Endpoint id=\"realtime.webhook.get\">\n  Devolve a configuração atual com o `secret` e a `standard_key` (o `secret` no formato `whsec_…`, para as bibliotecas Standard Webhooks). Sem webhook configurado, responde `404`.\n</Endpoint>"},{"anchor":"configurar-o-webhook-de-canais","heading":"Configurar o webhook de canais","markdown":"<Endpoint id=\"realtime.webhook.put\" example={{ body: `{ \"url\": \"https://example.com/hooks/realtime\", \"events\": [\"channel.occupied\", \"channel.vacated\", \"channel.message\"], \"channels\": [\"chat:*\"], \"enabled\": true }` }}>\n  * `url`: URL pública `http` ou `https`; endereços de rede interna ou privada são recusados.\n  * `events`: ao menos um dos eventos da tabela abaixo.\n  * `channels`: padrões de canal (nome, prefixo com `*` ou `*`), até <Limit name=\"realtimeWebhookChannelsMax\" />. Vazio quer dizer todos os canais, **menos** `channel.message`: as mensagens são opt-in e exigem ao menos um padrão.\n  * `enabled` (opcional, padrão `true`): `false` pausa as entregas sem apagar a configuração.\n\n  A chamada cria o webhook ou substitui o atual, e o `secret` é mantido. A configuração é lida em cache por até 30 segundos.\n</Endpoint>"},{"anchor":"trocar-o-secret-do-webhook","heading":"Trocar o secret do webhook","markdown":"<Endpoint id=\"realtime.webhook.rotate\">\n  Gera um novo `secret` (e a `standard_key` correspondente). O anterior deixa de valer; atualize o seu receptor logo em seguida.\n</Endpoint>"},{"anchor":"remover-o-webhook-de-canais","heading":"Remover o webhook de canais","markdown":"<Endpoint id=\"realtime.webhook.delete\">\n  Apaga a configuração; nenhum evento é mais enviado.\n</Endpoint>"},{"anchor":"eventos","heading":"Eventos","markdown":"<WebhookEventsTable source=\"realtime\" />"},{"anchor":"entrega","heading":"Entrega","markdown":"A plataforma faz um `POST` JSON na sua URL:\n\n```json\n{\n  \"id\": \"evt_5b0c9a7e-3c1f-4a52-9d57-2a6f1f0e8a11\",\n  \"type\": \"presence.member_added\",\n  \"timestamp\": 1790338000000,\n  \"account_id\": \"7a1d4c8e-2f6b-4e9a-b3c5-0d8f1a6e4b27\",\n  \"data\": {\n    \"channel\": \"chat:lobby\",\n    \"member\": { \"connection_id\": \"c_8f3a1d\", \"client_id\": \"user-42\", \"data\": { \"nick\": \"ana\" }, \"timestamp\": 1790338000000 }\n  }\n}\n```\n\n`data.channel` está em todos os eventos; `data.member` vem nos eventos de presença e `data.message` (`id`, `name`, `data`, `timestamp`, `client_id`) em `channel.message`. O `timestamp` é em milissegundos Unix.\n\n* **Assinatura:** Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature`), com a `standard_key` da configuração; veja [Validar a assinatura](/webhooks#verificar-assinatura).\n* **Tentativas:** até 3 (esperas de 1 s e de 5 s) em erro de rede, `5xx` ou `429`; outras respostas encerram a entrega. Cada tentativa espera até <Limit name=\"realtimeWebhookTimeout\" />.\n* **Processamento:** a entrega é assíncrona, por 4 trabalhadores; se a fila encher, o evento é descartado. Trate o webhook como um aviso e use o `id` para descartar duplicatas."},{"anchor":"sdk","heading":"SDK JavaScript","markdown":"O pacote `@respawn/realtime` implementa o protocolo acima, sem dependências, para o navegador e o Node 22 ou mais novo. Ele renova o token 30 segundos antes de expirar, reconecta conforme os [close codes](#protocolo), retoma os canais com `rewind` (100 mensagens, sem repetir as já entregues) e entra de novo na presença.\n\n```bash\nnpm install @respawn/realtime\n```"},{"anchor":"no-servidor","heading":"No servidor","markdown":"`RealtimeRest` usa a chave de API (nunca a envie ao navegador) para emitir tokens, publicar, ler o histórico e a presença. Passe `baseUrl` com o endereço da API:\n\n```ts\nimport { RealtimeRest } from '@respawn/realtime';\n\nconst rest = new RealtimeRest({\n  apiKey: process.env.RESPAWN_API_TOKEN!,\n  baseUrl: 'https://api.respawntech.dev',\n});\n\n// rota que o navegador chama (Express): devolve token, expires_at e connect_url\napp.post('/realtime/token', async (req, res) => {\n  res.json(\n    await rest.createToken({\n      client_id: req.user.id,\n      capabilities: { 'room:*': ['subscribe', 'publish', 'presence'] },\n      ttl_seconds: 600,\n    }),\n  );\n});\n\nawait rest.publish('room:1', 'pedido', { id: 42, status: 'pago' });\nconst history = await rest.history('room:1', { limit: 20 });\nconst members = await rest.presence('room:1');\n```"},{"anchor":"no-navegador","heading":"No navegador","markdown":"`Realtime` pede o token ao seu servidor por `authUrl` (um `POST` com cookies) ou por `authCallback`:\n\n```ts\nimport { Realtime } from '@respawn/realtime';\n\nconst rt = new Realtime({ authUrl: '/realtime/token' });\nrt.on('state', (state, err) => console.log('conexão:', state, err?.code));\n\nconst room = rt.channel('room:1');\n\nawait room.subscribe((m) => console.log(m.name, m.data), { rewind: 20 });\nawait room.publish('chat', { text: 'oi' });\n\nawait room.presence.watch((members) => render(members));\nawait room.presence.enter({ nick: 'ana' });\n```\n\nOs estados são `initialized`, `connecting`, `connected`, `disconnected`, `closed` (você chamou `close()`) e `failed` (por exemplo, close `4003`). Se o intervalo fora do ar passou da janela do histórico, o canal emite o evento `gap`. Publicações feitas sem conexão ficam na fila; uma já enviada quando a conexão cai falha com `sdk.connection_lost` (pode ou não ter sido publicada)."},{"anchor":"react","heading":"React","markdown":"Com `@respawn/realtime/react`:\n\n```tsx\nimport { useState } from 'react';\nimport { Realtime } from '@respawn/realtime';\nimport { RealtimeProvider, useChannel, usePresence } from '@respawn/realtime/react';\n\nconst rt = new Realtime({ authUrl: '/realtime/token' });\n\nexport function App() {\n  return (\n    <RealtimeProvider client={rt}>\n      <Room />\n    </RealtimeProvider>\n  );\n}\n\nfunction Room() {\n  const [msgs, setMsgs] = useState<{ id: string; data: unknown }[]>([]);\n  const { publish } = useChannel('room:1', (m) => setMsgs((all) => [...all, m]), { rewind: 20 });\n  const { members } = usePresence('room:1', { nick: 'ana' });\n\n  return (\n    <div>\n      <p>{members.length} online</p>\n      <ul>{msgs.map((m) => <li key={m.id}>{JSON.stringify(m.data)}</li>)}</ul>\n      <button onClick={() => publish('chat', { text: 'oi' })}>Enviar</button>\n    </div>\n  );\n}\n```"},{"anchor":"limites","heading":"Limites","markdown":"| Limite                               | Valor                                                                                                                               |\n| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |\n| Mensagens grátis por mês (por conta) | <Limit name=\"realtimeFreeMessages\" />                                                                                               |\n| Tamanho de uma mensagem (`data`)     | <Limit name=\"realtimeMessageMax\" />                                                                                                 |\n| Tamanho do nome do canal             | <Limit name=\"realtimeChannelNameMax\" /> caracteres                                                                                  |\n| Canais assinados por conexão         | <Limit name=\"realtimeChannelsPerConnection\" />                                                                                      |\n| Publicações por segundo (por conta)  | <Limit name=\"realtimePublishPerSecond\" />                                                                                           |\n| Conexões simultâneas (por conta)     | <Limit name=\"realtimeConnectionsPerAccount\" />                                                                                      |\n| Membros na presença de um canal      | <Limit name=\"realtimePresenceMembers\" />                                                                                            |\n| Validade do token                    | <Limit name=\"realtimeTokenTtlDefault\" /> (padrão), de <Limit name=\"realtimeTokenTtlMin\" /> até <Limit name=\"realtimeTokenTtlMax\" /> |\n| Padrões de canal por token           | até <Limit name=\"realtimeTokenPatternsMax\" />                                                                                       |\n| Retenção do histórico                | <Limit name=\"realtimeHistoryRetention\" />                                                                                           |\n| Mensagens guardadas e por `rewind`   | até <Limit name=\"realtimeHistoryPageMax\" />                                                                                         |\n| Canais por página (`GET /channels`)  | até <Limit name=\"realtimeChannelListPageMax\" />                                                                                     |\n| Padrões de canal do webhook          | até <Limit name=\"realtimeWebhookChannelsMax\" />                                                                                     |\n\nOs valores são os padrões da plataforma e podem ser ajustados por ela. As requisições à API também seguem o [limite de requisições](/cobranca-e-limites) da conta."},{"anchor":"erros","heading":"Erros","markdown":"Os erros da API REST (`realtime.channel_invalid`, `realtime.message_too_large`…) chegam como resposta HTTP, no formato de [Erros](/erros). Na conexão WebSocket ou SSE, os mesmos códigos chegam por um frame `error` (a conexão continua aberta) ou por um close code (veja [Protocolo](#protocolo)).\n\n<ErrorTable tool=\"realtime\" />\n\nErros de validação comuns (`validation.failed`, `not_found`) estão em [Erros](/erros)."},{"anchor":"preco","heading":"Preço","markdown":"<PricingTable tool=\"realtime\" group=\"per-message\" />\n\n<PricingTable tool=\"realtime\" group=\"free-quota\" />\n\nA cobrança é por mensagem publicada, uma vez, pelas mensagens que passam da franquia mensal; o número de assinantes não muda o custo. Emitir tokens, manter conexões abertas, usar a presença, listar canais e configurar o webhook são grátis. A consulta ao histórico é uma leitura medida: entra na franquia diária de leituras e, depois dela, custa o valor da [tabela de leituras](/cobranca-e-limites#franquia-diaria). Sem saldo para uma mensagem fora da franquia, a publicação falha com `realtime.insufficient_credits`."}]},{"slug":"ferramentas/verificacao-de-url","url":"https://docs.respawntech.dev/ferramentas/verificacao-de-url","title":"Verificação de URL","description":"Verifique se uma URL é maliciosa e obtenha o favicon de um domínio via proxy.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"verificacao-de-url\" />"},{"anchor":"visão-geral","heading":"Visão geral","markdown":"Use antes de exibir links gerados por usuários ou de redirecionar alguém para um destino externo. É a mesma verificação que o [QR Dinâmico](/ferramentas/qr-dinamico) aplica às URLs de destino.\n\n<EndpointList tool=\"verificacao-de-url\" />"},{"anchor":"custos","heading":"Custos","markdown":"As duas operações são leituras medidas: entram na [franquia diária](/cobranca-e-limites#franquia-diaria) e, depois dela, debitam o valor indicado em cada endpoint."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"verificar-url","heading":"Verificar URL","markdown":"<Endpoint id=\"safety.check\" example={{ query: { url: 'https://example.com' } }}>\n  Query: `url` (obrigatório, URL absoluta). Resultados são mantidos em cache por 1 hora.\n\n  <Callout type=\"warn\" title=\"Resposta fora do envelope padrão\">\n    Esta rota responde `{ \"is_safe\": … }` diretamente, sem `success`/`data`. Se o serviço de verificação estiver indisponível, a resposta é `is_safe: true` (*fail-open*), para não bloquear o seu fluxo. Em dry-run a resposta volta ao envelope padrão.\n  </Callout>\n</Endpoint>"},{"anchor":"favicon-de-um-domínio","heading":"Favicon de um domínio","markdown":"<Endpoint id=\"safety.favicon\" example={{ query: { domain: 'example.com' }, response: 'binary' }}>\n  Query: `domain` (obrigatório).\n</Endpoint>"},{"anchor":"erros","heading":"Erros","markdown":"<ErrorTable tool=\"verificacao-de-url\" />"}]},{"slug":"ferramentas/web-to-markdown","url":"https://docs.respawntech.dev/ferramentas/web-to-markdown","title":"Página web para Markdown","description":"Baixe uma página web e receba o conteúdo principal em Markdown, com opção de gravar na base de conhecimento.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"web-to-markdown\" />"},{"anchor":"visão-geral","heading":"Visão geral","markdown":"Informe uma URL e receba o texto principal da página em Markdown, com título, autor e contagem de palavras. Com `collection_id`, o conteúdo também é gravado numa [base de conhecimento](/ferramentas/base-de-conhecimento). Também está disponível no [MCP](/agentes-de-ia) como `web_to_markdown`.\n\n<EndpointList tool=\"web-to-markdown\" />"},{"anchor":"custos","heading":"Custos","markdown":"A conversão é uma leitura medida: entra na [franquia diária](/cobranca-e-limites#franquia-diaria) e, depois dela, debita o valor indicado no endpoint. Ao gravar numa base, a gravação é cobrada como uma ingestão da [base de conhecimento](/ferramentas/base-de-conhecimento)."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"converter-página-em-markdown","heading":"Converter página em Markdown","markdown":"<Endpoint id=\"web.markdown\" example={{ body: `{ \"url\": \"https://example.com/blog/post\" }` }}>\n  `url` (obrigatório) é o endereço `http` ou `https` da página. `collection_id` (opcional) grava o conteúdo nessa base e exige também o escopo `tools:rag:manage`.\n\n  A resposta traz `url`, `final_url` (depois de redirecionamentos), `title`, `byline`, `markdown` e `word_count`. Com `collection_id`, vêm também `document` (`document_id`, `status` e `duplicate`) e, em dry-run, `estimate`.\n\n  <Callout type=\"warn\" title=\"Só HTML estático\">\n    A página é lida como o servidor a entrega, sem executar JavaScript: páginas que montam o conteúdo no navegador respondem `tools.web.no_content`. Endereços internos e privados são recusados (`tools.web.blocked_url`).\n  </Callout>\n</Endpoint>"},{"anchor":"erros","heading":"Erros","markdown":"<ErrorTable tool=\"web-to-markdown\" />"}]},{"slug":"ferramentas/webhook-relay","url":"https://docs.respawntech.dev/ferramentas/webhook-relay","title":"Webhook Relay","description":"Envie webhooks assinados para os seus clientes, com retry automático, log de tentativas e replay, e redistribua webhooks de Stripe, GitHub e outros provedores.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"webhook-relay\" />"},{"anchor":"visao-geral","heading":"Visão geral","markdown":"O Webhook Relay é a infraestrutura de webhooks da sua aplicação. Em vez de manter filas, retries e assinaturas, você publica um evento com uma chamada e a plataforma entrega em cada endpoint inscrito, com o mesmo motor que envia os [webhooks da conta](/webhooks).\n\n* **Projeto:** agrupa endpoints, fontes, rotas, tipos de evento e mensagens, para separar configurações (uma loja, um cliente, um ambiente). Toda conta tem um projeto padrão; veja [Projetos](#projetos).\n* **Endpoint:** a URL que recebe os eventos (normalmente do seu cliente, ou um canal do Slack, Discord ou Teams), com os eventos que assina e um `secret` próprio.\n* **Mensagem:** um evento que você publica (`event_type` + `payload`).\n* **Entrega:** uma mensagem a caminho de um endpoint. Uma mensagem para 3 endpoints gera 3 entregas, cada uma com seus retries.\n* **Tentativa:** cada chamada HTTP de uma entrega, registrada com status, latência e o começo da resposta.\n* **Fonte e rota:** para **receber** webhooks de terceiros (Stripe, GitHub…) numa URL da plataforma e redistribuí-los para os seus endpoints. Veja [Receber webhooks de terceiros](#ingestao).\n\nTodas as requisições enviadas seguem o padrão [Standard Webhooks](https://www.standardwebhooks.com) (headers `webhook-id`, `webhook-timestamp` e `webhook-signature`). O formato, a verificação da assinatura e o calendário de retries estão no guia [Webhooks](/webhooks)."},{"anchor":"primeiros-passos","heading":"Primeiros passos","markdown":"<Steps>\n  <Step>\n    **Crie um endpoint** com a URL de destino e os eventos que ele assina. Guarde o `secret` da resposta (use `standard_webhooks_secret` com as bibliotecas oficiais). Sem `project_id`, ele vai para o projeto padrão.\n  </Step>\n\n  <Step>\n    **Teste a conexão** com o [ping de teste](#webhooks-endpoints-test): um evento `webhook.test` grátis.\n  </Step>\n\n  <Step>\n    **Envie uma mensagem** com `event_type` e `payload`. Use `X-Dry-Run: true` antes para ver quantas entregas ela gera e quanto custa.\n  </Step>\n\n  <Step>\n    **Verifique a assinatura** no receptor e responda `2xx` rápido. Veja os exemplos em [Validar a assinatura](/webhooks#verificar-assinatura).\n  </Step>\n\n  <Step>\n    **Acompanhe** as entregas no [log de mensagens](#mensagens) e reenvie o que falhou.\n  </Step>\n</Steps>\n\n<EndpointList tool=\"webhook-relay\" />"},{"anchor":"custos","heading":"Custos","markdown":"<PricingTable tool=\"webhook-relay\" group=\"per-delivery\" />\n\n<PricingTable tool=\"webhook-relay\" group=\"free-quota\" />\n\nA cobrança é feita no envio, por mensagem, pelas entregas que passam da franquia (`charged_units`, em unidades de 1/10.000 de crédito). Se a mensagem não puder ser gravada, o valor é estornado. O `payload` é validado antes de qualquer cobrança.\n\nA franquia e a cobrança são **por conta**: todos os projetos dividem a mesma franquia mensal, e uma entrega custa o mesmo em qualquer projeto.\n\n<Callout title=\"Leituras do log\">\n  Configurar projetos, endpoints, tipos de evento, fontes e rotas é grátis. As rotas de [listar](#webhooks-messages-list) e [consultar](#webhooks-messages-get) mensagens são leituras medidas: entram na [franquia diária de leituras](/cobranca-e-limites#franquia-diaria) e, depois dela, custam o valor da tabela de leituras.\n</Callout>"},{"anchor":"consultar-uso-do-mês","heading":"Consultar uso do mês","markdown":"<Endpoint id=\"webhooks.usage\">\n  Mostra o mês atual (UTC, de `period_start` a `period_end`): a franquia (`free_deliveries`), as entregas já usadas por API e ingestão (`used_deliveries`) e o que resta (`free_left`). `delivery_units` e `read_units` são os preços atuais de uma entrega e de uma leitura do log, em unidades; `blocked_retention_days` é quanto tempo uma entrega espera por créditos (veja [Sem créditos](#sem-creditos)).\n</Endpoint>"},{"anchor":"projetos","heading":"Projetos","markdown":"Tudo o que você configura no Webhook Relay pertence a um projeto: endpoints, fontes (e suas rotas), tipos de evento, mensagens e o layout do canvas no dashboard. Use projetos para separar configurações, por exemplo uma por loja ou por cliente.\n\n* Toda conta tem um **projeto padrão** (`is_default: true`, chamado `Default`), criado no primeiro uso. Ele não pode ser excluído.\n* `project_id` é **opcional** em toda a API: sem ele, criações e envios usam o projeto padrão. Integrações que não conhecem projetos continuam funcionando.\n* Uma mensagem só chega aos endpoints do projeto dela e é validada com os tipos de evento desse projeto. Nomes de tipo de evento são únicos por projeto.\n* As listagens (`GET /endpoints`, `/sources`, `/event-types`, `/routes` e `/messages`) aceitam `?project_id=` como filtro; sem ele, trazem todos os projetos.\n* Eventos da plataforma (ex.: `qr.dynamic.scanned`) geram uma mensagem em cada projeto que tem endpoints inscritos, sempre grátis. Callbacks de jobs (`webhook_url`) não pertencem a nenhum projeto (`project_id: null`).\n* Limites e cobrança continuam por conta: veja [Limites](#limites). Por padrão não há limite de projetos."},{"anchor":"listar-projetos","heading":"Listar projetos","markdown":"<Endpoint id=\"webhooks.projects.list\" example={{ query: { q: 'loja', page: '1', page_size: '20' } }}>\n  Paginado e com busca: `q` procura no nome (sem diferenciar maiúsculas), `page` começa em 1 e `page_size` vai até <Limit name=\"webhookProjectsPageMax\" /> (padrão <Limit name=\"webhookProjectsPageDefault\" />). Cada projeto traz `stats`: fontes, endpoints, mensagens das últimas 24 horas, entregas do mês e `failing_endpoints` (endpoints falhando ou desativados automaticamente; um endpoint pausado por você não conta).\n</Endpoint>"},{"anchor":"criar-projeto","heading":"Criar projeto","markdown":"<Endpoint id=\"webhooks.projects.create\" example={{ body: `{ \"name\": \"Loja\", \"description\": \"Eventos da loja virtual\" }` }}>\n  `name` (obrigatório) tem até <Limit name=\"webhookProjectNameMax\" /> caracteres; `description`, até <Limit name=\"webhookProjectDescriptionMax\" />.\n</Endpoint>"},{"anchor":"consultar-projeto","heading":"Consultar projeto","markdown":"<Endpoint id=\"webhooks.projects.get\" />"},{"anchor":"alterar-projeto","heading":"Alterar projeto","markdown":"<Endpoint id=\"webhooks.projects.update\" example={{ body: `{ \"name\": \"Loja principal\" }` }}>\n  Envie só os campos que quer mudar.\n</Endpoint>"},{"anchor":"excluir-projeto","heading":"Excluir projeto","markdown":"<Endpoint id=\"webhooks.projects.delete\">\n  Exclui os endpoints do projeto (as entregas pendentes são canceladas), as fontes, as rotas, os tipos de evento e o layout. As mensagens continuam no log geral, com `project_id: null`.\n</Endpoint>"},{"anchor":"duplicar-projeto","heading":"Duplicar projeto","markdown":"<Endpoint id=\"webhooks.projects.duplicate\" example={{ body: `{ \"name\": \"Loja (cópia)\" }` }}>\n  Cria um projeto novo com a mesma descrição e cópias de tudo, já ligadas entre si:\n\n  * **Endpoints** com **secrets novos** (mesma URL, eventos, headers, limite de envio, formato e estado ativo; o histórico de falhas não é copiado).\n  * **Fontes** com **`ingest_url` novas** (mesmas configurações e o mesmo secret do provedor). Cadastre as URLs novas no provedor.\n  * Rotas, tipos de evento e o layout do canvas.\n\n  Mensagens, entregas e contadores não são copiados. Os limites da conta são conferidos antes: se a cópia passaria de algum, a resposta é `webhook.limit_reached` e nada é criado.\n</Endpoint>"},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"criar-endpoint","heading":"Criar endpoint","markdown":"<Endpoint\n  id=\"webhooks.endpoints.create\"\n  example={{ body: `{\n\"url\": \"https://api.example.com/hooks/respawn\",\n\"description\": \"Pedidos da loja\",\n\"events\": [\"order.paid\", \"order.refunded\"],\n\"custom_headers\": { \"X-Tenant\": \"loja-1\" }\n}` }}\n>\n  * `url` (obrigatório): URL pública `http` ou `https`. Endereços privados, locais ou de metadados de nuvem são recusados (`webhook.invalid_url`).\n  * `events`: tipos de evento que o endpoint recebe das mensagens da API e da plataforma; `[\"*\"]` assina todos. Sem eventos, o endpoint só recebe o que as [rotas](#ingestao) mandam para ele.\n  * `project_id`: projeto do endpoint (padrão: o projeto padrão). Um endpoint não muda de projeto.\n  * `format`: `raw` (padrão), `slack`, `discord` ou `teams`. Veja [Slack, Discord e Teams](#formatos).\n  * `custom_headers`: até <Limit name=\"webhookCustomHeadersMax\" /> headers extras enviados em toda entrega (ex.: um token do seu cliente). Headers de assinatura, `Content-Type`, `User-Agent` e `Host` não podem ser sobrescritos.\n  * `rate_limit_per_sec`: teto de envios por segundo para este endpoint (`0` = sem teto próprio).\n  * O `secret` vem só nesta resposta; depois, use [Revelar secret](#webhooks-endpoints-secret). Até <Limit name=\"webhookEndpointsMax\" /> endpoints por conta, somando todos os projetos.\n</Endpoint>"},{"anchor":"listar-endpoints","heading":"Listar endpoints","markdown":"<Endpoint id=\"webhooks.endpoints.list\" example={{ query: { project_id: '3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53' } }}>\n  Filtro opcional por `project_id`. Um endpoint desativado automaticamente traz `is_active: false` e `disabled_reason`; um que está falhando traz `failing_since`.\n</Endpoint>"},{"anchor":"consultar-endpoint","heading":"Consultar endpoint","markdown":"<Endpoint id=\"webhooks.endpoints.get\" />"},{"anchor":"alterar-endpoint","heading":"Alterar endpoint","markdown":"<Endpoint id=\"webhooks.endpoints.update\" example={{ body: `{ \"events\": [\"*\"], \"is_active\": true }` }}>\n  Envie só os campos que quer mudar (inclusive `format`). `is_active: true` reativa um endpoint desativado e zera a sequência de falhas.\n</Endpoint>"},{"anchor":"excluir-endpoint","heading":"Excluir endpoint","markdown":"<Endpoint id=\"webhooks.endpoints.delete\">\n  As entregas pendentes para o endpoint são canceladas.\n</Endpoint>"},{"anchor":"revelar-secret","heading":"Revelar secret","markdown":"<Endpoint id=\"webhooks.endpoints.secret\">\n  `secret` é a chave bruta (HMAC do header legado `X-Webhook-Signature`); `standard_webhooks_secret` é a mesma chave no formato `whsec_…` aceito pelas bibliotecas Standard Webhooks.\n</Endpoint>"},{"anchor":"trocar-secret","heading":"Trocar secret","markdown":"<Endpoint id=\"webhooks.endpoints.rotate_secret\">\n  Durante <Limit name=\"webhookSecretOverlap\" /> as entregas levam duas assinaturas em `webhook-signature` (secret novo e antigo), para você trocar o secret do receptor sem perder eventos.\n</Endpoint>"},{"anchor":"enviar-ping-de-teste","heading":"Enviar ping de teste","markdown":"<Endpoint id=\"webhooks.endpoints.test\">\n  Envia um evento `webhook.test` assinado, grátis e fora da franquia. Acompanhe o resultado em [Consultar mensagem](#webhooks-messages-get) com o `message_id` devolvido.\n</Endpoint>"},{"anchor":"reenviar-entregas-com-falha","heading":"Reenviar entregas com falha","markdown":"<Endpoint id=\"webhooks.endpoints.recover\" example={{ query: { since: '2026-09-24T00:00:00Z' } }}>\n  Reenvia para este endpoint todas as entregas que terminaram com falha (`failed`, `exhausted` ou `canceled`) desde `since` (RFC 3339), até <Limit name=\"webhookRecoverMax\" /> por chamada. Use depois de corrigir um receptor que ficou fora do ar. Cada entrega reenviada é cobrada como nova.\n</Endpoint>"},{"anchor":"formatos","heading":"Slack, Discord e Teams","markdown":"Com `format` em `slack`, `discord` ou `teams`, cadastre como `url` o webhook de entrada do canal (Incoming Webhook do Slack ou do Teams, webhook do Discord). A plataforma transforma cada entrega numa mensagem de texto na hora de enviar:\n\n* O corpo vira `{\"text\": \"…\"}` (Slack e Teams) ou `{\"content\": \"…\"}` (Discord), com `Content-Type: application/json`.\n* O texto traz o tipo do evento em negrito (e o nome da fonte, nas mensagens recebidas de um provedor), seguido do corpo num bloco de código: JSON formatado ou o texto bruto.\n* Textos longos são cortados com `…` para caber no limite da plataforma: <Limit name=\"webhookSlackTextMax\" /> caracteres no Slack, <Limit name=\"webhookDiscordTextMax\" /> no Discord e <Limit name=\"webhookTeamsTextMax\" /> no Teams.\n* Os headers de assinatura continuam sendo enviados, calculados sobre esse corpo; os headers originais de uma fonte com `forward_original_headers` não são repassados.\n\nA mensagem guardada no log não muda: o log, o reenvio e a recuperação funcionam igual aos endpoints `raw`."},{"anchor":"mensagens","heading":"Mensagens","markdown":""},{"anchor":"enviar-mensagem","heading":"Enviar mensagem","markdown":"<Endpoint\n  id=\"webhooks.messages.send\"\n  example={{ headers: { 'X-Dry-Run': 'true' }, body: `{\n\"event_type\": \"order.paid\",\n\"payload\": { \"order_id\": \"ped_1042\", \"amount\": 199.9 },\n\"idempotency_key\": \"order-paid-ped_1042\"\n}` }}\n>\n  * `event_type` (obrigatório): `^[a-z0-9][a-z0-9._-]{0,127}$`, ex.: `order.paid`. Vai para os endpoints do projeto que assinam esse tipo (ou `*`).\n  * `payload` (obrigatório): qualquer JSON, até <Limit name=\"webhookPayloadMax\" />. Se o tipo de evento tiver [JSON Schema](#tipos-de-evento) no projeto, o payload precisa segui-lo (`webhook.payload_invalid`, sem cobrança).\n  * `project_id` (opcional): projeto da mensagem (padrão: o projeto padrão).\n  * `endpoint_ids` (opcional): restringe o envio a estes endpoints, entre os do projeto que assinam o evento.\n  * `idempotency_key` (ou o header `X-Idempotency-Key`): repetir a chave devolve a primeira mensagem com `duplicate: true` e não cobra nada. Use para reenviar com segurança depois de um timeout.\n  * Com `X-Dry-Run: true`, nada é enviado nem cobrado: a resposta mostra `deliveries`, `free_deliveries` e `charged_units`.\n\n  O receptor recebe o envelope `{ \"event_id\", \"type\", \"timestamp\", \"payload\" }` descrito em [Webhooks](/webhooks#corpo); `event_id` é o `message_id` e `webhook-id` é `msg_<message_id>`.\n</Endpoint>"},{"anchor":"enviar-mensagens-em-lote","heading":"Enviar mensagens em lote","markdown":"<Endpoint\n  id=\"webhooks.messages.batch\"\n  example={{ body: `{\n\"messages\": [\n  { \"event_type\": \"order.paid\", \"payload\": { \"order_id\": \"ped_1042\", \"amount\": 199.9 }, \"idempotency_key\": \"order-paid-ped_1042\" },\n  { \"event_type\": \"order.paid\", \"payload\": { \"order_id\": \"ped_1043\" }, \"idempotency_key\": \"order-paid-ped_1043\" }\n]\n}` }}\n>\n  Até <Limit name=\"webhookBatchMax\" /> mensagens, cada uma com os mesmos campos do envio único. O `project_id` fica no nível de cima do body e vale para todas as mensagens do lote. Cada mensagem é validada, deduplicada e cobrada sozinha: uma falha aparece em `error` no item dela e não impede as outras. Aceita `X-Dry-Run: true`.\n</Endpoint>"},{"anchor":"listar-mensagens","heading":"Listar mensagens","markdown":"<Endpoint id=\"webhooks.messages.list\" example={{ query: { event_type: 'order.paid', failed: 'true', limit: '20' } }}>\n  Mais recentes primeiro, com as entregas de cada mensagem (sem o corpo). Paginação: passe o `next_before` recebido em `before`. `limit` de 1 a 100 (padrão 20). As mensagens ficam no log por <Limit name=\"webhookLogRetention\" />.\n\n  * `project_id`: só as mensagens desse projeto. Sem ele, o log traz todos os projetos, além das mensagens sem projeto (`project_id: null`: callbacks de jobs e mensagens de projetos excluídos).\n  * `event_type` e `endpoint_id`: filtros exatos.\n  * `failed=true`: só mensagens com ao menos uma entrega `failed`, `exhausted` ou `blocked_no_credits`.\n  * `q`: busca, de 1 a <Limit name=\"webhookSearchQueryMax\" /> caracteres, combinável com os outros filtros. Um UUID encontra a mensagem com esse id. Outro texto encontra a chave de idempotência ou o tipo de evento iguais ao texto, ou o texto dentro do corpo (sem diferenciar maiúsculas, exceto letras acentuadas); a busca no corpo só olha as mensagens dos últimos <Limit name=\"webhookSearchBodyWindow\" />.\n</Endpoint>"},{"anchor":"consultar-mensagem","heading":"Consultar mensagem","markdown":"<Endpoint id=\"webhooks.messages.get\">\n  Traz o corpo enviado e, por entrega, cada tentativa: `http_status`, `latency_ms`, o começo da resposta (`response_body`) ou o erro de rede (`error`). `next_attempt_at` aparece enquanto a entrega está pendente.\n\n  | `status` da entrega  | Significado                                                                                              |\n  | -------------------- | -------------------------------------------------------------------------------------------------------- |\n  | `pending`            | Aguardando a próxima tentativa (`next_attempt_at`).                                                      |\n  | `sending`            | Tentativa em andamento.                                                                                  |\n  | `succeeded`          | O endpoint respondeu `2xx`.                                                                              |\n  | `exhausted`          | Todas as tentativas falharam. Pode ser reenviada.                                                        |\n  | `failed`             | Parou de vez: endpoint respondeu `410 Gone`, foi desativado ou a URL ficou inválida. Pode ser reenviada. |\n  | `canceled`           | Cancelada, por exemplo porque o endpoint foi excluído com a entrega pendente.                            |\n  | `blocked_no_credits` | Mensagem recebida de um provedor sem saldo para a entrega (veja [Sem créditos](#sem-creditos)).          |\n</Endpoint>"},{"anchor":"reenviar-mensagem","heading":"Reenviar mensagem","markdown":"<Endpoint id=\"webhooks.messages.retry\" example={{ body: `{ \"endpoint_id\": \"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34\" }` }}>\n  Sem body, envia a mensagem de novo para os endpoints cuja entrega terminou com falha (`failed`, `exhausted` ou `canceled`) e que estão ativos.\n\n  Com `endpoint_id`, envia só para esse endpoint, qualquer que tenha sido o resultado anterior (mesmo que a entrega tenha dado certo). Ele precisa ser um endpoint ativo do projeto da mensagem; caso contrário, e para mensagens sem projeto, a resposta é `400`. As entregas novas são cobradas como um envio.\n</Endpoint>"},{"anchor":"tipos-de-evento","heading":"Tipos de evento","markdown":"Declarar tipos de evento é opcional: sem declaração, qualquer `event_type` válido é aceito. Declare para documentar os eventos da sua integração e, com `schema`, validar o `payload` antes de cobrar e enviar. Cada tipo pertence a um projeto e só vale para as mensagens desse projeto."},{"anchor":"declarar-tipo-de-evento","heading":"Declarar tipo de evento","markdown":"<Endpoint\n  id=\"webhooks.event_types.create\"\n  example={{ body: `{\n\"name\": \"order.paid\",\n\"description\": \"Pedido pago\",\n\"schema\": {\n  \"type\": \"object\",\n  \"required\": [\"order_id\", \"amount\"],\n  \"properties\": {\n    \"order_id\": { \"type\": \"string\" },\n    \"amount\": { \"type\": \"number\" }\n  }\n}\n}` }}\n>\n  `schema` é um JSON Schema aplicado ao `payload` de cada envio desse tipo; um payload fora do schema responde `webhook.payload_invalid` e não é cobrado. `project_id` é opcional (padrão: o projeto padrão); o nome é único dentro do projeto. Até <Limit name=\"webhookEventTypesMax\" /> tipos por conta, somando todos os projetos.\n</Endpoint>"},{"anchor":"listar-tipos-de-evento","heading":"Listar tipos de evento","markdown":"<Endpoint id=\"webhooks.event_types.list\">\n  Filtro opcional por `project_id`.\n</Endpoint>"},{"anchor":"excluir-tipo-de-evento","heading":"Excluir tipo de evento","markdown":"<Endpoint id=\"webhooks.event_types.delete\">\n  Os envios desse tipo continuam aceitos, sem validação de schema.\n</Endpoint>"},{"anchor":"ingestao","heading":"Receber webhooks de terceiros","markdown":"Uma **fonte** é uma URL da plataforma (`ingest_url`) que você cadastra no provedor (Stripe, GitHub, Shopify…). Cada evento recebido tem a assinatura do provedor verificada, é gravado e redistribuído pelas **rotas** da fonte para os seus endpoints, com a mesma assinatura, retries e log das mensagens da API. Assim, um único webhook do provedor pode alimentar vários serviços, e o provedor nunca fica esperando pelo seu receptor.\n\n| `provider`          | Como a requisição é verificada                                                                                                  |\n| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |\n| `stripe`            | Header `Stripe-Signature` com o signing secret do Stripe (`whsec_…`), tolerância de <Limit name=\"webhookSignatureTolerance\" />. |\n| `github`            | Header `X-Hub-Signature-256` com o secret do webhook do GitHub.                                                                 |\n| `shopify`           | Header `X-Shopify-Hmac-Sha256` com o secret do app Shopify.                                                                     |\n| `hmac_generic`      | Header `X-Webhook-Signature: sha256=<hex>` (HMAC-SHA256 do corpo).                                                              |\n| `standard_webhooks` | Headers `webhook-id`, `webhook-timestamp` e `webhook-signature`.                                                                |\n| `basic_auth`        | Header `Authorization: Basic …`; o secret é `usuario:senha`.                                                                    |\n| `none`              | Sem verificação: qualquer um com a URL pode enviar. Evite em produção.                                                          |\n\n`hmac_generic`, `standard_webhooks` e `basic_auth` servem para qualquer sistema que não esteja na lista: escolha o que o sistema de origem sabe enviar."},{"anchor":"criar-fonte-de-ingestão","heading":"Criar fonte de ingestão","markdown":"<Endpoint\n  id=\"webhooks.sources.create\"\n  example={{ body: `{\n\"name\": \"Stripe produção\",\n\"provider\": \"stripe\",\n\"verification_secret\": \"whsec_...\",\n\"event_type_path\": \"type\",\n\"dedupe_key_path\": \"id\"\n}` }}\n>\n  * `name` e `provider` (obrigatórios); `project_id` é opcional (padrão: o projeto padrão).\n  * `verification_secret`: o secret que o provedor usa para assinar. Ele nunca volta nas respostas (`has_verification_secret`). Pode ficar para depois, porque vários provedores só mostram o secret depois que você cadastra a `ingest_url`: enquanto ele não for definido em [Definir secret do provedor](#webhooks-sources-verification_secret), a fonte responde `401` a toda requisição (exceto com `provider: \"none\"`).\n  * `event_type_path`: caminho no JSON recebido de onde vem o tipo do evento (ex.: `type`, `data.object.status`). Sem ele, o tipo é `ingest`.\n  * `dedupe_key_path`: caminho do identificador do evento (ex.: `id`). Um reenvio do provedor com a mesma chave responde `200` sem gerar novas entregas.\n  * `forward_original_headers`: repassa os headers do provedor (ex.: `Stripe-Signature`) junto com o corpo idêntico, além da nossa assinatura. Útil quando o seu receptor já valida a assinatura do provedor. Cookies, `Authorization` e headers de proxy nunca são repassados.\n  * Cadastre a `ingest_url` da resposta no provedor. Até <Limit name=\"webhookSourcesMax\" /> fontes por conta, somando todos os projetos.\n</Endpoint>"},{"anchor":"listar-fontes","heading":"Listar fontes","markdown":"<Endpoint id=\"webhooks.sources.list\">\n  Filtro opcional por `project_id`.\n</Endpoint>"},{"anchor":"consultar-fonte","heading":"Consultar fonte","markdown":"<Endpoint id=\"webhooks.sources.get\" />"},{"anchor":"alterar-fonte","heading":"Alterar fonte","markdown":"<Endpoint id=\"webhooks.sources.update\" example={{ body: `{ \"provider\": \"hmac_generic\", \"forward_original_headers\": true }` }}>\n  Envie só os campos que quer mudar, inclusive `provider` (por exemplo, para passar uma fonte `none` a `hmac_generic`). Ao trocar para um provedor com assinatura, envie o `verification_secret` na mesma requisição, para a fonte não ficar sem secret (sem ele, ela responde `401`). Para trocar só o secret, use [Definir secret do provedor](#webhooks-sources-verification_secret); um secret em branco é recusado. Uma fonte não muda de projeto.\n</Endpoint>"},{"anchor":"excluir-fonte","heading":"Excluir fonte","markdown":"<Endpoint id=\"webhooks.sources.delete\">\n  As rotas da fonte são excluídas junto e a `ingest_url` para de responder.\n</Endpoint>"},{"anchor":"trocar-url-de-ingestão","heading":"Trocar URL de ingestão","markdown":"<Endpoint id=\"webhooks.sources.rotate_token\">\n  Gera uma nova `ingest_url`; a anterior deixa de funcionar na hora. Atualize a URL no provedor logo em seguida.\n</Endpoint>"},{"anchor":"definir-secret-do-provedor","heading":"Definir secret do provedor","markdown":"<Endpoint id=\"webhooks.sources.verification_secret\" example={{ body: `{ \"secret\": \"whsec_...\" }` }}>\n  Um secret vazio só é aceito com `provider: \"none\"`.\n</Endpoint>"},{"anchor":"consultar-último-evento-recebido","heading":"Consultar último evento recebido","markdown":"<Endpoint id=\"webhooks.sources.sample\">\n  Devolve o último corpo recebido pela fonte, com o tipo de evento lido e a hora. Útil para descobrir os campos do provedor antes de configurar `event_type_path`, `dedupe_key_path` e os filtros das rotas.\n</Endpoint>"},{"anchor":"enviar-evento-de-teste-pela-fonte","heading":"Enviar evento de teste pela fonte","markdown":"<Endpoint id=\"webhooks.sources.test\" example={{ body: `{ \"event_type\": \"invoice.paid\" }` }}>\n  Faz um evento passar pela fonte como se o provedor o tivesse enviado: o tipo é lido em `event_type_path` e as rotas (com os filtros) decidem para onde ele vai. O corpo é o último recebido pela fonte ou, se ela ainda não recebeu nada, um exemplo do provedor. `event_type` (opcional) é gravado no caminho de `event_type_path`; se o corpo não tiver esse campo, o tipo usado é `test.event`.\n\n  É grátis e fica fora da franquia mensal. Não verifica assinatura nem deduplica, mas conta no limite de <Limit name=\"webhookIngestPerMinute\" /> requisições por minuto da fonte.\n</Endpoint>"},{"anchor":"criar-rota","heading":"Criar rota","markdown":"<Endpoint\n  id=\"webhooks.routes.create\"\n  example={{ body: `{\n\"source_id\": \"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27\",\n\"endpoint_id\": \"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34\",\n\"event_types\": [\"checkout.session.completed\", \"invoice.paid\"]\n}` }}\n>\n  Liga uma fonte a um endpoint **do mesmo projeto** (fonte e endpoint de projetos diferentes respondem `400`). `event_types` restringe a rota a esses tipos, até <Limit name=\"webhookRouteEventTypesMax\" />; vazio, a rota leva todos os eventos da fonte. Até <Limit name=\"webhookRoutesPerSourceMax\" /> rotas por fonte.\n</Endpoint>"},{"anchor":"listar-rotas","heading":"Listar rotas","markdown":"<Endpoint id=\"webhooks.routes.list\" example={{ query: { source_id: 'a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27' } }}>\n  Filtros opcionais por `source_id` e `project_id`.\n</Endpoint>"},{"anchor":"alterar-rota","heading":"Alterar rota","markdown":"<Endpoint id=\"webhooks.routes.update\" example={{ body: `{ \"event_types\": [\"invoice.paid\"], \"is_active\": false }` }}>\n  Muda os tipos de evento da rota (lista vazia = todos) ou a pausa com `is_active: false`. Envie só os campos que quer mudar.\n</Endpoint>"},{"anchor":"excluir-rota","heading":"Excluir rota","markdown":"<Endpoint id=\"webhooks.routes.delete\" />"},{"anchor":"receber-evento-de-um-provedor","heading":"Receber evento de um provedor","markdown":"<Endpoint id=\"webhooks.ingest\" example={{ pathParams: { token: 'src_4f8a2c6e1b9d3f7a5c0e8b2d6f4a1c9e7b3d5f0a2c8e6b4d' }, headers: { 'Stripe-Signature': 't=1790338000,v1=5257a869…' }, body: `{ \"id\": \"evt_1Q2w3E\", \"type\": \"checkout.session.completed\", \"data\": { \"object\": { \"id\": \"cs_test_a1B2\" } } }` }}>\n  É a URL que o provedor chama; você não precisa chamá-la. O corpo é guardado e repassado byte a byte, com o `Content-Type` original. As respostas são pensadas para o provedor decidir se tenta de novo:\n\n  | Status | Quando                                                                               |\n  | ------ | ------------------------------------------------------------------------------------ |\n  | `200`  | Aceito, duplicado (reenvio do provedor) ou sem rota para o tipo.                     |\n  | `401`  | Assinatura inválida, ou secret do provedor ainda não definido; nada é gravado.       |\n  | `404`  | Fonte desconhecida ou desativada, ou ferramenta não liberada na conta.               |\n  | `429`  | Mais de <Limit name=\"webhookIngestPerMinute\" /> requisições por minuto para a fonte. |\n  | `5xx`  | Falha ao gravar; o provedor deve tentar de novo.                                     |\n</Endpoint>"},{"anchor":"exemplo-stripe","heading":"Exemplo: Stripe","markdown":"<Steps>\n  <Step>\n    [Crie um endpoint](#webhooks-endpoints-create) para cada serviço seu que precisa dos eventos do Stripe. Os eventos chegam pelas rotas, então `events` pode ficar vazio.\n  </Step>\n\n  <Step>\n    [Crie uma fonte](#webhooks-sources-create) com `provider: \"stripe\"`, `event_type_path: \"type\"` e `dedupe_key_path: \"id\"`, ainda sem o secret.\n  </Step>\n\n  <Step>\n    No painel do Stripe, adicione um endpoint de webhook com a `ingest_url` e copie o **signing secret** (`whsec_…`) para [Definir secret do provedor](#webhooks-sources-verification_secret).\n  </Step>\n\n  <Step>\n    [Crie uma rota](#webhooks-routes-create) da fonte para cada endpoint, com `event_types` quando um serviço só precisa de alguns eventos. Confira o caminho com [Enviar evento de teste pela fonte](#webhooks-sources-test).\n  </Step>\n</Steps>\n\nOs seus serviços passam a receber os eventos assinados pela plataforma (valide com o secret de cada endpoint). Se preferirem validar a assinatura do próprio Stripe, ative `forward_original_headers` na fonte: o `Stripe-Signature` e o corpo idêntico chegam junto."},{"anchor":"sem-creditos","heading":"Sem créditos","markdown":"O provedor nunca é recusado por falta de saldo. As entregas que seriam cobradas ficam com status `blocked_no_credits` (contadas em `blocked` na resposta ao provedor) e são cobradas e liberadas automaticamente assim que a conta tiver créditos. Se o saldo não voltar em <Limit name=\"webhookBlockedRetention\" />, elas terminam como `failed`."},{"anchor":"canvas","heading":"Canvas do dashboard","markdown":"No dashboard, cada projeto aparece como um canvas com as fontes, os endpoints e as rotas entre eles. Estas rotas alimentam essa tela; numa integração pela API, as listagens acima costumam bastar. As três aceitam `?project_id=` (padrão: o projeto padrão)."},{"anchor":"consultar-mapa-do-fluxo","heading":"Consultar mapa do fluxo","markdown":"<Endpoint id=\"webhooks.graph\" example={{ query: { project_id: '3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53' } }}>\n  Fontes, endpoints e rotas do projeto com os contadores das últimas 24 horas: por fonte, mensagens recebidas, a última e os tipos de evento recentes; por endpoint e por rota, entregas, falhas e o status da última.\n</Endpoint>"},{"anchor":"consultar-layout-do-canvas","heading":"Consultar layout do canvas","markdown":"<Endpoint id=\"webhooks.layout.get\" />"},{"anchor":"salvar-layout-do-canvas","heading":"Salvar layout do canvas","markdown":"<Endpoint\n  id=\"webhooks.layout.save\"\n  example={{ body: `{\n\"positions\": {\n  \"source:a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27\": { \"x\": 0, \"y\": 160 },\n  \"endpoint:8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34\": { \"x\": 520, \"y\": 200 }\n}\n}` }}\n>\n  `positions` guarda as coordenadas de cada cartão: chaves `platform`, `source:<id>` ou `endpoint:<id>`. Cartões sem posição são arrumados automaticamente pelo dashboard.\n</Endpoint>"},{"anchor":"alertas","heading":"Alertas","markdown":"O dono da conta recebe notificações no dashboard e por e-mail. Cada tipo pode ser desligado nas preferências de notificação (categoria webhooks).\n\n| Tipo                        | Quando                                                                                                                                       |\n| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |\n| `webhook_endpoint_failing`  | Um endpoint começa a falhar (primeira tentativa com erro depois de estar saudável). Não repete até uma resposta `2xx` encerrar a sequência.  |\n| `webhook_endpoint_disabled` | A desativação automática desliga um endpoint que falha há <Limit name=\"webhookDisableAfter\" /> (não quando você o pausa nem por `410 Gone`). |\n| `webhook_quota`             | As entregas do mês atingem <Limit name=\"webhookQuotaAlerts\" /> da franquia mensal, uma vez por limiar e por mês.                             |\n\nOs alertas nunca atrasam nem bloqueiam um envio."},{"anchor":"retries","heading":"Entrega e retries","markdown":"O calendário de retries, o que conta como sucesso, `Retry-After`, `410 Gone` e a desativação automática de endpoints que falham por <Limit name=\"webhookDisableAfter\" /> seguidos estão no guia [Webhooks](/webhooks#retries). Retries automáticos não são cobrados de novo; só [Reenviar mensagem](#webhooks-messages-retry) e [Reenviar entregas com falha](#webhooks-endpoints-recover) geram entregas novas."},{"anchor":"api-legada","heading":"API legada da conta","markdown":"As rotas antigas de webhooks da conta (`/v1/account/webhooks`) estão **obsoletas**: respondem com os headers `Deprecation: true` e `Link` apontando para os projetos do Webhook Relay. Elas continuam funcionando, e os endpoints criados por elas ficam no projeto padrão. Use o Webhook Relay em integrações novas. A rota do secret dos callbacks de jobs não é obsoleta (veja [Secrets e rotação](/webhooks#secrets))."},{"anchor":"erros","heading":"Erros","markdown":"<ErrorTable tool=\"webhook-relay\" />\n\nErros de validação comuns (`validation.failed`, `not_found`) e de saldo (`billing.insufficient_credits`) estão em [Erros](/erros)."},{"anchor":"limites","heading":"Limites","markdown":"| Limite                                     | Valor                                              |\n| ------------------------------------------ | -------------------------------------------------- |\n| Entregas grátis por mês (por conta)        | <Limit name=\"webhookFreeDeliveries\" />             |\n| Tamanho do `payload` / corpo recebido      | <Limit name=\"webhookPayloadMax\" />                 |\n| Projetos por conta                         | <Limit name=\"webhookProjectsMax\" />                |\n| Endpoints por conta                        | <Limit name=\"webhookEndpointsMax\" />               |\n| Headers customizados por endpoint          | <Limit name=\"webhookCustomHeadersMax\" />           |\n| Tipos de evento por conta                  | <Limit name=\"webhookEventTypesMax\" />              |\n| Mensagens por lote                         | <Limit name=\"webhookBatchMax\" />                   |\n| Entregas por recuperação                   | <Limit name=\"webhookRecoverMax\" />                 |\n| Fontes por conta                           | <Limit name=\"webhookSourcesMax\" />                 |\n| Rotas por fonte                            | <Limit name=\"webhookRoutesPerSourceMax\" />         |\n| Tipos de evento por rota                   | <Limit name=\"webhookRouteEventTypesMax\" />         |\n| Requisições recebidas por fonte            | <Limit name=\"webhookIngestPerMinute\" /> por minuto |\n| Espera por créditos (`blocked_no_credits`) | <Limit name=\"webhookBlockedRetention\" />           |\n| Retenção do log de mensagens               | <Limit name=\"webhookLogRetention\" />               |\n| Janela da busca no corpo das mensagens     | <Limit name=\"webhookSearchBodyWindow\" />           |\n\nOs limites por conta somam todos os projetos. Os valores são os padrões da plataforma e podem ser ajustados por ela."}]}],"en":[{"slug":"agentes-de-ia","url":"https://docs.respawntech.dev/en/agentes-de-ia","title":"AI agents (MCP)","description":"Connect Claude, Cursor, VS Code and other agents to the tools through the MCP server, with visible costs and a per-key spend limit.","sections":[{"anchor":"top","heading":"","markdown":"The **MCP** (Model Context Protocol) server lets an AI agent discover and use the platform's tools, such as creating a QR code, converting a PDF or searching a knowledge base, with your account and your credits. It follows the same rules as the API: same prices, scopes and free quotas.\n\n<Callout title=\"Gradual rollout\">\n  The MCP server is being rolled out gradually. Until it is active in your environment, `/mcp` responds `404` with the code `mcp.disabled`.\n</Callout>"},{"anchor":"conexao","heading":"Connection","markdown":"|                |                                            |\n| -------------- | ------------------------------------------ |\n| URL            | <McpUrl />                                 |\n| Transport      | Streamable HTTP, stateless, JSON responses |\n| Authentication | `Authorization: Bearer <API token>`        |\n| Required scope | `mcp:use`, plus each tool's scopes         |\n\nNever put the token in the URL (`?token=`, `?api_key=`…): the server rejects it with `400`. Connectors in products such as Claude.ai and ChatGPT, which ask you to sign in instead of using a token, are not supported yet."},{"anchor":"chave","heading":"Create a key for the agent","markdown":"In the [dashboard](https://dashboard.respawntech.dev), under **APIs**, create a token just for the agent. The presets fill in scopes and controls:\n\n* **Support agent**: knowledge base search and balance lookup.\n* **Full agent**: every tool.\n* **Read-only**: lookups only.\n\nAn agent key should have a **daily spend limit** (suggestion: <Limit name=\"mcpSuggestedDailyLimit\" />) and an **expiration date**. See [Key controls](/en/autenticacao#controles-da-chave)."},{"anchor":"clientes","heading":"Configure the client","markdown":"<McpClientSnippets />\n\nClients that only speak stdio use the `mcp-remote` bridge, as in the last example."},{"anchor":"ferramentas","heading":"Tools","markdown":"The agent only sees the tools allowed by the key's scopes. The descriptions it receives are in English by default; use `lang` (below) for another language.\n\n<McpToolsTable />\n\n**Destructive** tools ask the user for confirmation in most clients. Those marked with dry-run accept `dry_run: true` and return only the cost estimate."},{"anchor":"parametros","heading":"Connection parameters","markdown":"All are optional and go in the URL's query string. They only **narrow** what the key allows, never widen it. An unknown parameter or an invalid value responds `400`, so a typo doesn't silently become \"no filter\".\n\n<McpParamsTable />\n\nFor example, a support agent that only reads the knowledge base, with responses in Portuguese: <McpUrl query=\"toolsets=rag&access=read&lang=pt\" />\n\nParameters you repeat across connections can become a **profile** in the dashboard (`profile=<slug>`), and a key can have a default profile."},{"anchor":"custos","heading":"Costs and spend control","markdown":"* Each call costs the same as the equivalent API operation, and metered reads count toward the same [daily free quota](/en/cobranca-e-limites#franquia-diaria).\n* Every response includes the cost in `_meta[\"tech.respawn/cost\"]`: credits charged, estimate (on dry-run), balance afterwards and how much of the day's limit is left.\n* `simulate=true` in the URL makes every paid tool return only the estimate. It's the way to test an agent without spending.\n* Going over the key's daily limit responds `billing.spend_limit_reached`; going over the per-call cap (`max_call_credits`) responds `billing.call_limit_exceeded`.\n* The **panic button** in the dashboard cuts off access for every agent on the account immediately (`mcp.agents_disabled`).\n* The dashboard statement breaks down spending by channel (API, MCP, dashboard) and by key."},{"anchor":"idempotencia","heading":"Idempotency","markdown":"Agents often repeat calls. Tools that create or change something accept `idempotency_key`; without it, an identical call (same key, tool and arguments) within <Limit name=\"mcpIdempotencyWindow\" /> returns the previous result without running or charging again (`_meta[\"tech.respawn/replayed\"]`)."},{"anchor":"limites","heading":"Limits","markdown":"| Limit                | Value                                                                       |\n| -------------------- | --------------------------------------------------------------------------- |\n| Requests             | <Limit name=\"rateLimitPerMinute\" /> per minute per key, shared with the API |\n| Request body         | <Limit name=\"mcpRequestMax\" />                                              |\n| Tool result          | <Limit name=\"mcpResultMax\" />                                               |\n| `tools_search` calls | <Limit name=\"mcpToolsSearchPerMinute\" /> per minute                         |"},{"anchor":"erros","heading":"Errors","markdown":"Business errors come back as the tool result, with a message telling the agent what to do and the code in `_meta[\"tech.respawn/error\"]`. Connection errors come back as HTTP, in the format `{\"error\": {\"code\", \"message\"}}`.\n\n<ErrorTable group=\"agents\" />\n\nTool errors are the same as in the API; see [Error codes](/en/erros)."}]},{"slug":"autenticacao","url":"https://docs.respawntech.dev/en/autenticacao","title":"Authentication","description":"How to authenticate with the tools using an API token, scopes and account requirements.","sections":[{"anchor":"api-token","heading":"API token","markdown":"Every route under `/v1/tools/*` requires an **API token**, created in the [dashboard](https://dashboard.respawntech.dev) under **APIs**.\n\n| Method                         | Example                              |\n| ------------------------------ | ------------------------------------ |\n| Dedicated header (recommended) | `X-API-Token: rsk_live_...`          |\n| Authorization                  | `Authorization: ApiKey rsk_live_...` |\n| Authorization (alternative)    | `Authorization: Token rsk_live_...`  |\n\n<Callout title=\"Best practices\">\n  Store the token in the `RESPAWN_API_TOKEN` environment variable, create one token per integration with the **minimum scopes** required, and revoke the ones you no longer use.\n</Callout>"},{"anchor":"account-requirements","heading":"Account requirements","markdown":"Before any call to the tools, the account must have:\n\n* **A verified email** — otherwise, `auth.email_not_verified` (403).\n* **A non-negative balance** — a suspended account returns `billing.user_bankrupt` (402).\n* **Enough credits** — paid operations without balance return `billing.insufficient_credits` (402)."},{"anchor":"scopes","heading":"Scopes","markdown":"Each route accepts a set of scopes; the token only needs **one** of them. If a permission is missing, the API returns `scope.missing` (403) with `details.required` and `details.provided`. The scopes accepted by each endpoint are shown on the endpoint card.\n\n<ScopeTable />\n\n<Callout type=\"warn\" title=\"PDF is stricter on reads\">\n  PDF read routes accept only `tools:pdf:read` or `tools:manage`. The generic `tools:read` scope and `tools:pdf:manage` do **not** grant access to PDF reads.\n</Callout>"},{"anchor":"recommended-combinations","heading":"Recommended combinations","markdown":"| Use case                                     | Suggested scopes                                                 |\n| -------------------------------------------- | ---------------------------------------------------------------- |\n| Create and manage QR codes                   | `tools:qr:dynamic:manage`                                        |\n| Read-only QR dashboard                       | `tools:qr:dynamic:read`                                          |\n| PDF pipeline                                 | `tools:pdf:manage` + `tools:pdf:read`                            |\n| Check URLs before displaying them            | `tools:safety:read`                                              |\n| Assistant that searches the knowledge base   | `tools:rag:query`                                                |\n| Upload and maintain knowledge base documents | `tools:rag:manage`                                               |\n| [AI agent (MCP)](/en/agentes-de-ia)          | `mcp:use` + the scopes of the tools it will use + `account:read` |\n| Full integration                             | `tools:manage` (simpler, less secure)                            |"},{"anchor":"controles-da-chave","heading":"Key controls","markdown":"Besides scopes, each token can have controls configured in the dashboard. They apply on every channel, both the API and [MCP](/en/agentes-de-ia):\n\n* **Expiration**: after the date, the token stops working (`auth.unauthorized`).\n* **Daily spend limit**: a charge that would exceed the day's limit is rejected with `billing.spend_limit_reached` (402), with `details.limit`, `details.spent_today` and `details.required`. The counter resets at midnight UTC.\n* **Knowledge base restriction**: the token only sees the selected knowledge bases; others respond `not_found`, and it cannot create new ones (`forbidden`).\n* **Presets**: when creating the key, \"Read-only\", \"Support agent\" and \"Full agent\" fill in scopes and controls in one go.\n\nExpiration, limit and knowledge bases can be changed later; scopes cannot. To change scopes, create another key."},{"anchor":"authentication-errors","heading":"Authentication errors","markdown":"<ErrorTable group=\"auth\" />"},{"anchor":"dashboard-vs-api-token","heading":"Dashboard vs. API token","markdown":"* **Dashboard** — browsing the dashboard does not consume the daily free quota or debit metered reads.\n* **API token** — metered reads share the [daily free quota](/en/cobranca-e-limites#franquia-diaria) of <Limit name=\"dailyQuota\" /> requests (<Limit name=\"dailyQuotaPremium\" /> on premium accounts). After that, each read debits credits."},{"anchor":"example","heading":"Example","markdown":"Create a QR code with dry-run to validate the token, the scopes and the cost without debiting anything:\n\n<ApiExamples\n  method=\"POST\"\n  path=\"/v1/tools/image/qr-code/dynamic\"\n  headers={{ 'X-Dry-Run': 'true', 'X-Include-Cost': 'true' }}\n  body={`{\n\"name\": \"Campanha\",\n\"target_url\": \"https://example.com\"\n}`}\n/>"}]},{"slug":"cobranca-e-limites","url":"https://docs.respawntech.dev/en/cobranca-e-limites","title":"Billing and limits","description":"Credits, dry-run, idempotency, rate limit, daily free read quota and timeouts.","sections":[{"anchor":"credit-model","heading":"Credit model","markdown":"* Your balance is shown in **credits** in the dashboard, and every amount in this documentation is in credits.\n* Paid operations are debited on confirmation — never on dry-run.\n* Each tool's pricing rules live on the tool's page: [Dynamic QR](/en/ferramentas/qr-dinamico#custos), [PDF Convert](/en/ferramentas/pdf-convert#custos), [Knowledge base](/en/ferramentas/base-de-conhecimento#custos), [Audio](/en/ferramentas/audio#custos).\n* Each token can have a [daily spend limit](/en/autenticacao#controles-da-chave): going over it responds `billing.spend_limit_reached`.\n\n<Callout title=\"Exact price\">\n  Creation costs may include add-ons (custom slug, design, PDF retention…). Run a dry-run with the real payload to see the exact total.\n</Callout>"},{"anchor":"dry-run","heading":"Dry-run and cost preview","markdown":"| Header                 | Effect                                                           |\n| ---------------------- | ---------------------------------------------------------------- |\n| `X-Dry-Run: true`      | Validates and calculates the cost without persisting or debiting |\n| `X-Preview-Cost: true` | Alias for `X-Dry-Run`                                            |\n| `X-Include-Cost: true` | Includes `meta.cost` in the response                             |\n\nDry-run is accepted on the paid operations flagged on each endpoint card and on **metered reads** (except downloads). The response is `200` and nothing is persisted or debited.\n\nThe [Audio](/en/ferramentas/audio#dry-run) routes ask for the dry-run with `?dry_run=true` in the query, not with the header. Audio is charged per minute of audio, per 1,000 characters or per generated second, and the amount for transcription, speech and the other jobs is refunded if the engine fails."},{"anchor":"quote-for-a-paid-operation","heading":"Quote for a paid operation","markdown":"On creation and conversion `POST`s, `data` contains an itemized quote:\n\n```json\n{\n  \"success\": true,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"data\": {\n    \"total_cost_credits\": 15000,\n    \"total_cost_units\": 15000,\n    \"items\": [\n      { \"description\": { \"key\": \"billing.pdf.convert_base\" }, \"cost_credits\": 10000, \"type\": \"base\" },\n      { \"description\": { \"key\": \"billing.pdf.retention.7\" }, \"cost_credits\": 5000, \"type\": \"storage\" }\n    ],\n    \"is_monthly_recurring\": false,\n    \"currency\": \"CREDITS\"\n  }\n}\n```\n\n<Callout type=\"warn\" title=\"Values in units\">\n  Despite their names, the quote's `total_cost_credits` and `cost_credits` (and `cost_credits` on PDF conversions) are in **internal units**: divide by 10,000 to get credits. In the example, 15,000 units = 1.5 credits.\n</Callout>\n\nFor Dynamic QR, the quote also includes `slot_usage` (slot usage and whether the creation will open a new paid slot) and `recurring_cost`."},{"anchor":"preview-of-a-metered-read","heading":"Preview of a metered read","markdown":"On metered reads, dry-run does not run the query — it only shows your position in the quota and whether the read would be charged:\n\n```json\n{\n  \"success\": true,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"data\": {\n    \"operation\": \"qr_dynamic.analytics\",\n    \"metering\": {\n      \"within_quota\": true,\n      \"quota_limit\": 2000,\n      \"quota_used\": 125,\n      \"quota_remaining\": 1875,\n      \"cost_units\": 300,\n      \"would_charge\": false\n    }\n  },\n  \"meta\": {\n    \"cost\": {\n      \"units\": 300,\n      \"credits\": 0.03,\n      \"charged\": false,\n      \"within_quota\": true,\n      \"quota_limit\": 2000,\n      \"quota_used\": 125,\n      \"quota_remaining\": 1875\n    }\n  }\n}\n```\n\nWith `X-Include-Cost: true` on a real read, the same `meta.cost` object comes with the normal response (`charged: true` when the read was charged)."},{"anchor":"idempotency","heading":"Idempotency","markdown":"Send `X-Idempotency-Key` (UUID recommended) on any tool route:\n\n* the key is valid for **<Limit name=\"idempotencyTtl\" />**;\n* same key + same payload → the original response is reused, with no new charge;\n* same key + different payload → `idempotency.conflict` (409).\n\nAlways use it on creation and conversion `POST`s so network retries don't charge twice.\n\nException: [Knowledge base](/en/ferramentas/base-de-conhecimento#limites) routes ignore the header because they are already idempotent (repeated content is neither reindexed nor charged)."},{"anchor":"rate-limit","heading":"Rate limit","markdown":"Each API token can make **<Limit name=\"rateLimitPerMinute\" /> requests per minute** on `/v1/tools/*` routes. Every response reports the current status:\n\n```http\nX-RateLimit-Limit: 60\nX-RateLimit-Remaining: 42\nX-RateLimit-Reset: 1712345678\n```\n\nWhen exceeded, the API returns `rate_limit.exceeded` (429). Wait until `X-RateLimit-Reset` before retrying."},{"anchor":"retry","heading":"Retrying with backoff","markdown":"Only retry what can succeed on the next attempt:\n\n* **`429`**: wait until the time in `X-RateLimit-Reset` (Unix seconds).\n* **`5xx` and network failures**: wait a little longer each time, with some randomness (exponential backoff with jitter): about 1 s, 2 s, 4 s… up to 30 s, at most 5 attempts.\n* **Other `4xx`** (validation, scope, balance): don't retry. The response won't change until you fix the request.\n\nOn a `POST` that creates or charges something, send **the same** `X-Idempotency-Key` on every attempt: if the first one arrived and only the response was lost, the retry returns the original result without charging again.\n\n<Tabs groupId=\"code-language\" persist items={['JavaScript', 'Python']}>\n  <Tab value=\"JavaScript\">\n    ```js\n    // Retries 429, 5xx and network failures; returns other errors right away.\n    async function withRetry(send, attempts = 5) {\n      for (let attempt = 1; ; attempt++) {\n        let response;\n        try {\n          response = await send();\n        } catch (error) {\n          if (attempt === attempts) throw error; // network failure\n        }\n        if (response && response.status !== 429 && response.status < 500) return response;\n        if (attempt === attempts) return response;\n\n        const reset = Number(response?.headers.get('X-RateLimit-Reset'));\n        const wait =\n          response?.status === 429 && reset\n            ? Math.max(0, reset * 1000 - Date.now())\n            : Math.min(2 ** (attempt - 1) * 1000, 30_000) * (0.5 + Math.random());\n        await new Promise((resolve) => setTimeout(resolve, wait));\n      }\n    }\n\n    const key = crypto.randomUUID(); // the same key on every attempt\n    const response = await withRetry(() =>\n      fetch(url, {\n        method: 'POST',\n        headers: {\n          'X-API-Token': process.env.RESPAWN_API_TOKEN,\n          'X-Idempotency-Key': key,\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify(payload),\n      }),\n    );\n    ```\n  </Tab>\n\n  <Tab value=\"Python\">\n    ```python\n    import os, random, time, uuid\n    import requests\n\n    # Retries 429, 5xx and network failures; returns other errors right away.\n    def with_retry(send, attempts=5):\n        for attempt in range(1, attempts + 1):\n            try:\n                response = send()\n            except (requests.ConnectionError, requests.Timeout):\n                if attempt == attempts:\n                    raise\n                response = None\n            if response is not None and response.status_code != 429 and response.status_code < 500:\n                return response\n            if attempt == attempts:\n                return response\n            reset = response.headers.get(\"X-RateLimit-Reset\") if response is not None else None\n            if response is not None and response.status_code == 429 and reset:\n                wait = max(0, int(reset) - time.time())\n            else:\n                wait = min(2 ** (attempt - 1), 30) * (0.5 + random.random())\n            time.sleep(wait)\n\n    key = str(uuid.uuid4())  # the same key on every attempt\n    response = with_retry(lambda: requests.post(\n        url,\n        json=payload,\n        headers={\"X-API-Token\": os.environ[\"RESPAWN_API_TOKEN\"], \"X-Idempotency-Key\": key},\n        timeout=30,\n    ))\n    ```\n  </Tab>\n</Tabs>"},{"anchor":"franquia-diaria","heading":"Daily free read quota","markdown":"Reads made with an **API token** (listings, status, analytics, downloads…) count toward a daily free quota shared across all tools:\n\n| Item               | Value                                  |\n| ------------------ | -------------------------------------- |\n| Free reads per day | **<Limit name=\"dailyQuota\" />**        |\n| Premium accounts   | **<Limit name=\"dailyQuotaPremium\" />** |\n| Reset              | <Limit name=\"quotaReset\" />            |\n\nEvery metered read returns your position in the quota:\n\n```http\nX-API-Quota-Limit: 2000\nX-API-Quota-Used: 847\nX-API-Quota-Remaining: 1153\n```\n\nRequests made from the [dashboard](https://dashboard.respawntech.dev) do **not** count toward the quota and are not charged."},{"anchor":"metered-reads","heading":"Metered reads","markdown":"Once the quota is used up, each API read debits:\n\n<MeteredReadsTable />\n\nWrite operations with no cost of their own (`PATCH`, activate, deactivate, `DELETE`) and creation dry-runs are always free.\n\n<Callout type=\"idea\" title=\"Avoid polling\">\n  The quota covers most integrations. To track state changes, prefer [webhooks](/en/webhooks) over repeatedly polling the status.\n</Callout>"},{"anchor":"timeouts","heading":"Timeouts","markdown":"* Most routes: **<Limit name=\"requestTimeout\" />**.\n* File downloads: **<Limit name=\"downloadTimeout\" />**."}]},{"slug":"erros","url":"https://docs.respawntech.dev/en/erros","title":"Error codes","description":"Response envelope, error format and a reference of every code returned by the tools.","sections":[{"anchor":"success-response","heading":"Success response","markdown":"```json\n{\n  \"success\": true,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"data\": {},\n  \"meta\": {}\n}\n```\n\n| Field      | Description                                      |\n| ---------- | ------------------------------------------------ |\n| `success`  | `true` on OK responses                           |\n| `trace_id` | Correlation ID (also in the `X-Trace-Id` header) |\n| `data`     | Resource or result                               |\n| `meta`     | Optional — cost (`meta.cost`), pagination, etc.  |\n\nWith `X-Include-Cost: true` or on dry-run, `meta` contains the operation cost — see the formats in [Dry-run](/en/cobranca-e-limites#dry-run). Listings return pagination inside `data` (`total` and, for PDF, `page` and `page_size`), not in `meta`."},{"anchor":"error-format","heading":"Error format","markdown":"```json\n{\n  \"success\": false,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"error\": {\n    \"code\": \"billing.insufficient_credits\",\n    \"message\": \"Créditos insuficientes para esta operação.\",\n    \"details\": { \"required\": 100, \"available\": 50 }\n  }\n}\n```\n\n| Field           | Description                                                                                     |\n| --------------- | ----------------------------------------------------------------------------------------------- |\n| `error.code`    | Stable identifier — handle errors by it, never by `message`                                     |\n| `error.message` | Human-readable text; may change between versions                                                |\n| `error.details` | Optional structured context; on `validation.failed`, `details.fields` contains per-field errors |\n| `trace_id`      | Include it in support tickets                                                                   |"},{"anchor":"errors-common-to-all-tools","heading":"Errors common to all tools","markdown":""},{"anchor":"authentication-and-permissions","heading":"Authentication and permissions","markdown":"<ErrorTable group=\"auth\" />"},{"anchor":"credits-and-account","heading":"Credits and account","markdown":"<ErrorTable group=\"billing\" />"},{"anchor":"request-and-limits","heading":"Request and limits","markdown":"<ErrorTable group=\"request\" />"},{"anchor":"ai-agents-mcp","heading":"AI agents (MCP)","markdown":"<ErrorTable group=\"agents\" />"},{"anchor":"platform","heading":"Platform","markdown":"<ErrorTable group=\"platform\" />"},{"anchor":"video","heading":"Video","markdown":"<ErrorTable group=\"video\" />"},{"anchor":"errors-by-tool","heading":"Errors by tool","markdown":"<ToolErrorSections />\n\nAsynchronous jobs (PDF) also expose `error_code` on the resource itself when `status` is `failed`; codes marked as **asynchronous** appear only there."},{"anchor":"best-practices","heading":"Best practices","markdown":"1. Handle `402` with a top-up flow in your system.\n2. On `429`, respect `X-RateLimit-Reset` before trying again.\n3. Use `X-Dry-Run` to catch `validation.failed` at no cost.\n4. Retry only `5xx` and network errors, always with the same `X-Idempotency-Key`.\n5. Log the `trace_id` of every error response."}]},{"slug":"","url":"https://docs.respawntech.dev/en","title":"API Documentation","description":"Integrate Respawn Tech Dev tools through a predictable REST API — token authentication, credit-based billing and dry-run on every paid operation.","sections":[{"anchor":"tools","heading":"Tools","markdown":"<ToolCards />"},{"anchor":"getting-started","heading":"Getting started","markdown":"<Steps>\n  <Step>\n    ### Create an API token\n\n    In the [dashboard](https://dashboard.respawntech.dev), open **APIs** and create a token with the scopes for the tool you'll use. Store it in the `RESPAWN_API_TOKEN` environment variable. See [Authentication](/en/autenticacao).\n  </Step>\n\n  <Step>\n    ### Estimate the cost with dry-run\n\n    Send `X-Dry-Run: true` before a paid operation to validate the payload and see the cost without debiting credits. See [Billing and limits](/en/cobranca-e-limites#dry-run).\n  </Step>\n\n  <Step>\n    ### Make the call\n\n    All tool routes use the `https://api.respawntech.dev/v1/tools` prefix. Every endpoint includes ready-to-use examples in cURL, JavaScript and Python.\n\n    <ApiExamples method=\"GET\" path=\"/v1/tools/image/qr-code/dynamic\" query={{ limit: '20' }} />\n  </Step>\n</Steps>\n\n## Common headers\n\n| Header              | Value          | Effect                                                                  |\n| ------------------- | -------------- | ----------------------------------------------------------------------- |\n| `X-API-Token`       | `rsk_live_...` | Authentication (recommended)                                            |\n| `X-Dry-Run`         | `true`         | Simulates the operation without persisting or debiting                  |\n| `X-Include-Cost`    | `true`         | Includes `meta.cost` in the response                                    |\n| `X-Idempotency-Key` | UUID           | Prevents duplicate charges on retries (<Limit name=\"idempotencyTtl\" />) |\n\nResponses include `X-Trace-Id` (use it in support tickets), `X-RateLimit-*` and, on metered reads, `X-API-Quota-*`.\n\n## Guides\n\n<Cards>\n  <Card title=\"Authentication\" description=\"API token, scopes and account requirements.\" href=\"/en/autenticacao\" />\n\n  <Card title=\"Billing and limits\" description=\"Credits, dry-run, idempotency, rate limit and daily free quota.\" href=\"/en/cobranca-e-limites\" />\n\n  <Card title=\"Webhooks\" description=\"Account events, Standard Webhooks signature, retries and job callbacks.\" href=\"/en/webhooks\" />\n\n  <Card title=\"Error codes\" description=\"Response envelope and every code, by tool.\" href=\"/en/erros\" />\n\n  <Card title=\"AI agents (MCP)\" description=\"Connect Claude, Cursor and other agents to the tools, with a spend limit.\" href=\"/en/agentes-de-ia\" />\n</Cards>"}]},{"slug":"usar-com-ia","url":"https://docs.respawntech.dev/en/usar-com-ia","title":"Use with AI","description":"Give your AI assistant access to these docs through the documentation MCP (public, free, no token), llms.txt or the Markdown pages.","sections":[{"anchor":"top","heading":"","markdown":"AI assistants write better integrations when they look up the documentation instead of guessing. There are three ways to hand these pages to an agent, from the most complete to the simplest: the **documentation MCP**, **llms.txt** and the **Markdown pages**."},{"anchor":"dois-servidores","heading":"Two MCP servers","markdown":"|                | Documentation MCP                             | [Account MCP](/en/agentes-de-ia)                                                |\n| -------------- | --------------------------------------------- | ------------------------------------------------------------------------------- |\n| Purpose        | Answer questions about the API while you code | Use the tools with your account (create a QR, convert a PDF, transcribe audio…) |\n| URL            | <McpUrl server=\"docs\" />                      | <McpUrl />                                                                      |\n| Authentication | None                                          | API token with `mcp:use`                                                        |\n| Cost           | Free                                          | Same as the API                                                                 |\n| Access         | Read-only public documentation                | Acts on the account, with scopes and a spend limit                              |\n\nBoth can be connected at the same time: an agent looks up the docs, then uses the account MCP to act.\n\n<Callout title=\"Gradual release\">\n  The documentation MCP is being rolled out gradually. Until it is active, <McpUrl server=\"docs\" /> answers `404` with the `mcp.disabled` code. llms.txt and the Markdown pages already work.\n</Callout>"},{"anchor":"conexao","heading":"Connection","markdown":"|                |                                                                       |\n| -------------- | --------------------------------------------------------------------- |\n| URL            | <McpUrl server=\"docs\" />                                              |\n| Transport      | Streamable HTTP                                                       |\n| Authentication | None (don't send a token)                                             |\n| Cost           | Free                                                                  |\n| Limit          | <Limit name=\"mcpDocsRateLimitPerMinute\" /> requests per minute per IP |\n| Language       | Optional `lang` parameter on every tool: `pt-br`, `en`, `es` or `fr`  |\n\nAnswers come from the same content as this site, including the current prices."},{"anchor":"clientes","heading":"Configure the client","markdown":"<McpDocsClientSnippets />\n\nIn **Claude.ai** and **ChatGPT**, add a custom connector and enter only the URL <McpUrl server=\"docs\" />. No login or token is needed. Clients that only speak stdio use the `mcp-remote` bridge, as in the last example."},{"anchor":"ferramentas","heading":"Tools","markdown":"<McpDocsToolsTable />\n\nA typical flow: `docs_search` with the question, `docs_get_page` on the section found, then `docs_get_endpoint` or `docs_get_pricing` for the exact details before writing code."},{"anchor":"llms-txt","heading":"Without MCP: llms.txt and Markdown","markdown":"For agents that only read the web:\n\n* [`/llms.txt`](/llms.txt): index in the [llmstxt.org](https://llmstxt.org) format, with every page in every language and a link to its Markdown version.\n* [`/llms-full.txt`](/llms-full.txt): every page in a single text file.\n* Markdown pages: `/md/<page>.md` in Portuguese and `/md/<language>/<page>.md` in the other languages, for example [`/md/webhooks.md`](/md/webhooks.md) or [`/md/en/webhooks.md`](/md/en/webhooks.md). The home page is `/md/index.md`.\n* [`/ai/bundle.json`](/ai/bundle.json): the whole documentation as structured JSON (sections, tools, endpoints, prices, errors, scopes and limits). It powers the documentation MCP."}]},{"slug":"webhooks","url":"https://docs.respawntech.dev/en/webhooks","title":"Webhooks","description":"How the platform sends webhooks — account events, job callbacks and Webhook Relay — and how to verify the Standard Webhooks signature.","sections":[{"anchor":"top","heading":"","markdown":"The platform makes a `POST` to your URL when something happens. Every send goes through the same delivery engine: the message is stored, each destination gets a delivery with its own retries, and every HTTP call is logged. There are three origins:\n\n| Origin                                         | Where it is configured                                             | Cost                                       |\n| ---------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------ |\n| **Account events** (e.g. `qr.dynamic.scanned`) | **Webhooks** in the [dashboard](https://dashboard.respawntech.dev) | Free                                       |\n| **Job callbacks** (e.g. PDF conversion)        | `webhook_url` in each request                                      | Free                                       |\n| **Webhook Relay** (your own events)            | The [Webhook Relay](/en/ferramentas/webhook-relay) API             | Per delivery beyond the monthly free quota |"},{"anchor":"eventos","heading":"Account events","markdown":"Register an endpoint under **Webhooks** in the [dashboard](https://dashboard.respawntech.dev) and choose the events. Each endpoint has its own `secret`, shown on creation and afterwards through the reveal-secret option.\n\nThese endpoints belong to [Webhook Relay](/en/ferramentas/webhook-relay#projetos): an endpoint of any project that subscribes to an account event receives it, for free. The old account webhooks API is deprecated; see [Legacy account API](/en/ferramentas/webhook-relay#api-legada).\n\n<WebhookEventsTable />"},{"anchor":"requisicao","heading":"Request sent","markdown":"```http\nPOST <endpoint url>\nContent-Type: application/json\nUser-Agent: RespawnTech-Webhook/2.0\nwebhook-id: msg_2Vf8tK1pQ9zX\nwebhook-timestamp: 1790338000\nwebhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=\nX-Webhook-Signature: sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd\n```\n\n| Header                | Content                                                                                         |\n| --------------------- | ----------------------------------------------------------------------------------------------- |\n| `webhook-id`          | Message identifier. It is the **same across every retry**: use it to drop duplicates.           |\n| `webhook-timestamp`   | Time of this attempt, in Unix seconds.                                                          |\n| `webhook-signature`   | `v1,<base64>`, one signature per active secret, space separated (two during a secret rotation). |\n| `X-Webhook-Signature` | Legacy `sha256=<hex>` signature, without timestamp. Kept for older receivers.                   |\n\nThe headers follow the [Standard Webhooks](https://www.standardwebhooks.com) scheme, so any library for that standard verifies the request. Webhook Relay endpoints may carry extra headers (`custom_headers`)."},{"anchor":"corpo","heading":"Body","markdown":"Account events and Webhook Relay messages use the same envelope:\n\n```json\n{\n  \"event_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"type\": \"qr.dynamic.scanned\",\n  \"timestamp\": 1790338000,\n  \"payload\": {\n    \"resource_id\": \"…\",\n    \"slug\": \"promo-verao\",\n    \"ip\": \"anonymous_hash\",\n    \"user_agent\": \"Mozilla/5.0 …\",\n    \"referer\": \"https://…\"\n  }\n}\n```\n\nThe shape of `payload` depends on `type`. Job callbacks keep their own body (see [Job callback](#pdf-callback)), and messages received from providers through Webhook Relay arrive with the provider's original body, unchanged."},{"anchor":"verificar-assinatura","heading":"Verifying the signature","markdown":"The signed content is `webhook-id + \".\" + webhook-timestamp + \".\" + raw body`, HMAC-SHA256, base64-encoded. To verify:\n\n1. Read the **raw body**, before any JSON parsing (reformatting the JSON changes the signature).\n2. Reject timestamps more than <Limit name=\"webhookSignatureTolerance\" /> away from your clock (replay protection).\n3. Compute the signature and compare it, **in constant time**, with each item of `webhook-signature`. One match is enough.\n\nWith a Standard Webhooks library, use `standard_webhooks_secret` (the `whsec_…` form). Without a library, the HMAC key is the bytes of the raw `secret`.\n\n<Tabs groupId=\"code-language\" persist items={['JavaScript', 'Python', 'Go']}>\n  <Tab value=\"JavaScript\">\n    ```js\n    // npm i standardwebhooks\n    import express from 'express';\n    import { Webhook } from 'standardwebhooks';\n\n    const wh = new Webhook(process.env.WEBHOOK_SECRET); // whsec_…\n    const app = express();\n\n    app.post('/hook', express.raw({ type: '*/*' }), (req, res) => {\n      let event;\n      try {\n        event = wh.verify(req.body, req.headers); // throws when invalid\n      } catch {\n        return res.sendStatus(401);\n      }\n      // process `event` asynchronously and answer fast\n      res.sendStatus(204);\n    });\n    ```\n  </Tab>\n\n  <Tab value=\"Python\">\n    ```python\n    # pip install standardwebhooks\n    import os\n    from flask import Flask, request\n    from standardwebhooks.webhooks import Webhook\n\n    wh = Webhook(os.environ[\"WEBHOOK_SECRET\"])  # whsec_…\n    app = Flask(__name__)\n\n    @app.post(\"/hook\")\n    def hook():\n        try:\n            event = wh.verify(request.get_data(), dict(request.headers))\n        except Exception:\n            return \"\", 401\n        # process `event` asynchronously and answer fast\n        return \"\", 204\n    ```\n  </Tab>\n\n  <Tab value=\"Go\">\n    ```go\n    // Without a library: secret is the raw value (not the whsec_ form).\n    func verify(secret string, r *http.Request, body []byte) bool {\n    \tid, ts := r.Header.Get(\"webhook-id\"), r.Header.Get(\"webhook-timestamp\")\n    \tt, err := strconv.ParseInt(ts, 10, 64)\n    \tif err != nil || math.Abs(time.Since(time.Unix(t, 0)).Seconds()) > 300 {\n    \t\treturn false\n    \t}\n    \tmac := hmac.New(sha256.New, []byte(secret))\n    \tmac.Write([]byte(id + \".\" + ts + \".\"))\n    \tmac.Write(body)\n    \twant := \"v1,\" + base64.StdEncoding.EncodeToString(mac.Sum(nil))\n    \tfor _, sig := range strings.Fields(r.Header.Get(\"webhook-signature\")) {\n    \t\tif hmac.Equal([]byte(sig), []byte(want)) {\n    \t\t\treturn true\n    \t\t}\n    \t}\n    \treturn false\n    }\n    ```\n  </Tab>\n</Tabs>\n\n<Callout title=\"Older receivers\">\n  Receivers that already verify `X-Webhook-Signature` keep working: it is `sha256=` + the hex HMAC-SHA256 of the raw body, keyed with the raw `secret`. This header has no timestamp; prefer `webhook-signature` in new integrations.\n</Callout>"},{"anchor":"secrets","heading":"Secrets and rotation","markdown":"| Destination                  | Where to get the secret                                                                                                                    |\n| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |\n| Account event endpoint       | On creation, or through the reveal-secret option under **Webhooks** in the dashboard.                                                      |\n| Webhook Relay endpoint       | On creation, or with [Reveal secret](/en/ferramentas/webhook-relay#webhooks-endpoints-secret).                                             |\n| Job callback (`webhook_url`) | The account signing secret, one for every callback: `GET /v1/account/webhooks/signing-secret` (account route, with the dashboard session). |\n\nEvery secret comes in two forms: `secret` (raw) and `standard_webhooks_secret` (`whsec_` + base64 of the same bytes). Both verify the same signatures.\n\nWhen you [rotate an endpoint's secret](/en/ferramentas/webhook-relay#webhooks-endpoints-rotate_secret), the old one keeps signing for <Limit name=\"webhookSecretOverlap\" />: during that window `webhook-signature` carries both signatures, so you can update the receiver without losing events."},{"anchor":"retries","heading":"Delivery and retries","markdown":"Only a **2xx** response counts as success. Redirects (3xx) are not followed, and 3xx, 4xx, 5xx, timeouts and network errors are retried on this schedule:\n\n| Attempt | Wait before it |\n| ------- | -------------- |\n| 1       | —              |\n| 2       | 5 seconds      |\n| 3       | 5 minutes      |\n| 4       | 30 minutes     |\n| 5       | 2 hours        |\n| 6       | 5 hours        |\n| 7 and 8 | 10 hours       |\n\nAbout 27 hours in total, with ±10% jitter. Special cases:\n\n* **`429` and `503`** with `Retry-After`: the next attempt honors the header when it asks for longer (up to 10 hours).\n* **`410 Gone`**: the delivery stops and the endpoint is disabled.\n* After the last attempt, the delivery is `exhausted`. With Webhook Relay you can [retry the message](/en/ferramentas/webhook-relay#webhooks-messages-retry) or [recover an endpoint's failures](/en/ferramentas/webhook-relay#webhooks-endpoints-recover).\n\nGood practices in the receiver:\n\n* Answer **2xx** as fast as possible and process the event asynchronously.\n* **Drop duplicates by `webhook-id`**: a delivery can arrive more than once (e.g. your server processed it but the response was lost).\n* Don't rely on arrival order; use the body's `timestamp` when order matters."},{"anchor":"desativacao","heading":"Automatic disabling","markdown":"An endpoint that keeps failing for <Limit name=\"webhookDisableAfter\" /> is disabled automatically (`is_active: false`, with `disabled_reason`), as is one that answers `410`. After fixing the receiver, re-enable the endpoint (in the dashboard or with `is_active: true` in Webhook Relay); this clears the failing streak.\n\nThe history of messages, deliveries and attempts is kept for <Limit name=\"webhookLogRetention\" />."},{"anchor":"pdf-callback","heading":"Job callback","markdown":"In `POST /convert` or `POST /batch` of [PDF Convert](/en/ferramentas/pdf-convert), send `webhook_url` in the body. When the job finishes, the API makes a `POST` to that URL:\n\n```json\n{\n  \"job_id\": \"job_abc123\",\n  \"success\": true,\n  \"result_url\": \"https://…\",\n  \"metadata\": { \"conversion_id\": \"…\" }\n}\n```\n\nOn failure, `success` is `false` and the `error` field holds the message. The callback has the same headers, signature and retries as the other webhooks, signed with the **account signing secret** (see [Secrets and rotation](#secrets)). `webhook_url` must be a public `http` or `https` URL; otherwise the request is refused with `webhook.invalid_url`."},{"anchor":"webhook-relay","heading":"Your own events","markdown":"To send webhooks from **your** application to your customers, with the same signature, retries and log, or to receive webhooks from Stripe, GitHub and others and fan them out, use [Webhook Relay](/en/ferramentas/webhook-relay)."}]},{"slug":"ferramentas/audio","url":"https://docs.respawntech.dev/en/ferramentas/audio","title":"Audio","description":"Transcription, synthetic voice, effects and music, analysis, conversion, summary, translation and revoice of audio, over REST or MCP, with asynchronous jobs and per-minute pricing.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"audio\" />"},{"anchor":"visao-geral","heading":"Overview","markdown":"Every audio operation is **asynchronous** and charged up front:\n\n1. `POST` to the operation → **202 Accepted** with `job_id`, `status: pending` and `poll_after_seconds`.\n2. A worker processes it in the background.\n3. Poll `GET /jobs/{id}` until `status` is `completed` (or `failed`, and the charge is refunded). Once done, the response carries the result and a **signed link** that expires.\n\nThe same use cases are in the [MCP](/en/agentes-de-ia) as the `audio_*` tools (toolset `audio`); REST and MCP share formats, prices and limits.\n\n<Callout type=\"warn\" title=\"Charged when the job is accepted\">\n  Credits are debited when the job is accepted. Engine failures, stages that turned out unnecessary and overestimated prices are **refunded** automatically; nothing is charged if the request is refused beforehand (`audio.too_long`, `audio.price_above_max`…).\n</Callout>\n\n<EndpointList tool=\"audio\" />"},{"anchor":"entrada","heading":"Audio input","markdown":"Operations that take an audio require **exactly one** of these fields:\n\n| Field          | What it is                                                                                                                                                                  |\n| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `audio_url`    | Public `http(s)` URL of the file. Internal or private network addresses are refused (`audio.input_blocked`) and the download has a <Limit name=\"audioFetchTimeout\" /> limit |\n| `audio_base64` | The audio inline as base64, up to <Limit name=\"audioInlineMax\" /> decoded                                                                                                   |\n| `file_id`      | Id of an audio file already uploaded to the account                                                                                                                         |\n\nAccepted formats: mp3, wav, ogg, m4a, flac and webm. The limit is <Limit name=\"audioMaxDuration\" /> of audio and <Limit name=\"audioInputMax\" /> per file per call. In `summarize` and `translate` you can send, instead of the audio, the `job_id` of a finished transcription: the transcript is reused and only the new operation is charged.\n\nEvery operation route accepts `max_credits` (in units; 10,000 units = 1 credit): if the price exceeds it, the request is refused with `audio.price_above_max` and nothing is charged. The `Idempotency-Key` header makes retries safe: the same key returns the first job instead of charging again."},{"anchor":"custos","heading":"Costs","markdown":"<PricingTable tool=\"audio\" group=\"speech-to-text\" />\n\n<PricingTable tool=\"audio\" group=\"text-to-speech\" />\n\n<PricingTable tool=\"audio\" group=\"processing\" />\n\n<PricingTable tool=\"audio\" group=\"revoice\" />\n\nA \"started minute\" rounds up. An audio whose length cannot be read from its header is charged at the <Limit name=\"audioMaxDuration\" /> cap and the difference is refunded when the job ends. Job reads follow the [daily quota](/en/cobranca-e-limites#franquia-diaria). The exact price of a call comes from the [dry-run](#dry-run)."},{"anchor":"dry-run","heading":"Dry-run","markdown":"With `?dry_run=true` in the query (on the audio routes it is the query, not the `X-Dry-Run` header), paid operations answer `200` with the price and neither charge nor create a job:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"service_code\": \"audio_transcribe_minute\",\n    \"units\": 3,\n    \"rate\": 15000,\n    \"total_units\": 45000,\n    \"duration_ms\": 125000,\n    \"duration_known\": true,\n    \"parts\": [{ \"service_code\": \"audio_transcribe_minute\", \"units\": 3, \"rate\": 15000, \"total\": 45000 }]\n  }\n}\n```\n\n`rate` and `total_units` are in units (10,000 units = 1 credit); `parts` breaks down each charged service. For `speak`, the quote carries `chars`, `voice`, `language` and `format`."},{"anchor":"operations","heading":"Operations","markdown":""},{"anchor":"transcribe-audio","heading":"Transcribe audio","markdown":"<Endpoint id=\"audio.transcribe\" example={{ body: `{ \"audio_url\": \"https://example.com/episode-12.mp3\", \"format\": \"text\", \"language\": \"pt\" }` }}>\n  `format`: `text` (default), `srt` or `vtt`. `language` is optional (ISO 639-1 code; without it the language is detected). With `diarize: true` (or `speakers`, the known number of speakers) each piece says who spoke (`Speaker 1`, `Speaker 2`…), for an extra per-minute charge.\n</Endpoint>"},{"anchor":"generate-speech-from-text","heading":"Generate speech from text","markdown":"<Endpoint id=\"audio.speak\" example={{ body: `{ \"text\": \"Hello! Your order is out for delivery.\", \"voice\": \"pt-BR-dora\", \"format\": \"mp3\", \"speed\": 1 }` }}>\n  `text` takes up to <Limit name=\"audioTtsCharsMax\" /> characters (above that the request is refused before charging). `voice` is an id from the [catalog](#vozes-do-catalogo) or a cloned voice of the account (`vc_…`, [MCP only](#vozes-clonadas)); without `voice`, the default voice of `language` applies (`pt-BR`, `en-US`, `en-GB`, `es-ES`, `fr-FR` or `it-IT`). `format`: `mp3` (default) or `wav`; `speed`: 0.5 to 2.\n</Endpoint>"},{"anchor":"vozes-do-catalogo","heading":"List the voices","markdown":"<Endpoint id=\"audio.voices\" example={{ query: { language: 'pt-BR' } }}>\n  Closed catalog of voices for `speak` and `revoice`. `language` filters by language. Free.\n</Endpoint>"},{"anchor":"generate-a-sound-effect-or-music","heading":"Generate a sound effect or music","markdown":"<Endpoint id=\"audio.generate\" example={{ body: `{ \"kind\": \"effect\", \"prompt\": \"wooden door creaking slowly\", \"duration_seconds\": 4, \"format\": \"mp3\" }` }}>\n  `kind`: `effect` (1 to <Limit name=\"audioEffectMax\" />, default 5 s) or `music` (instrumental, 1 to <Limit name=\"audioMusicMax\" />, default 30 s). The audio comes out with exactly `duration_seconds`. There is no speech or singing, and descriptions that imitate artists, voices or existing works are refused before charging (`audio.input_blocked`). `seed` repeats a sound: the same request with the `seed` a job reported yields the same audio. Generation takes a few minutes.\n</Endpoint>"},{"anchor":"analyze-audio","heading":"Analyze audio","markdown":"<Endpoint id=\"audio.analyze\" example={{ body: `{ \"audio_url\": \"https://example.com/episode-12.mp3\" }` }}>\n  No transcription AI: returns in `analysis` the duration, format and codec, bitrate, sample rate, channels, integrated loudness (LUFS), true peak, loudness range, the silent stretches (`start_ms`/`end_ms`) and the spoken language. Flat price per call.\n</Endpoint>"},{"anchor":"convert-or-normalize-audio","heading":"Convert or normalize audio","markdown":"<Endpoint id=\"audio.convert\" example={{ body: `{ \"audio_url\": \"https://example.com/episode-12.wav\", \"format\": \"mp3\", \"bitrate_kbps\": 128, \"normalize\": true }` }}>\n  `format` (required): `mp3`, `wav`, `ogg`, `m4a` or `flac`. `bitrate_kbps` (32 to 320) only applies to lossy formats. `normalize: true` levels the loudness to -14 LUFS (streaming standard, -1.5 dBTP). Metadata tags are dropped.\n</Endpoint>"},{"anchor":"summarize-audio","heading":"Summarize audio","markdown":"<Endpoint id=\"audio.summarize\" example={{ body: `{ \"job_id\": \"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64\", \"output_language\": \"en\" }` }}>\n  Send the `job_id` of a finished transcription (the audio is not processed again) or a new audio (transcribed first and charged together). The result comes in `summary`: the summary and the topics in order (`title`, `start_ms`, `points`), plus a Markdown file to download. The transcript text (never the audio) is sent to the platform's language model.\n</Endpoint>"},{"anchor":"translate-audio","heading":"Translate audio","markdown":"<Endpoint id=\"audio.translate\" example={{ body: `{ \"job_id\": \"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64\", \"target_language\": \"en\", \"format\": \"srt\" }` }}>\n  `target_language` is required. The translation comes as text, SRT or VTT with the original timings (and the speakers, if the transcript used `diarize`); `source_language` tells which language it came from. It does not produce dubbed audio: for that, use [revoice](#revoice).\n</Endpoint>"},{"anchor":"revoice","heading":"Speak the recording again in another voice","markdown":"<Endpoint id=\"audio.revoice\" example={{ body: `{ \"audio_url\": \"https://example.com/notice.mp3\", \"voice_id\": \"pt-BR-alex\", \"format\": \"mp3\" }` }}>\n  In a single job: transcribes the recording, translates the text when the voice speaks another language and synthesizes the result. Give `voice_id` (from the catalog) or `language` (uses its default voice). The new audio does not keep the original timing or rhythm (no lip sync), and only catalog voices or your own account's cloned voices work: no imitating a real person. The result carries the spoken text (`transcript` and `text_download_url`) and what each stage cost in `revoice.stages`.\n</Endpoint>"},{"anchor":"list-jobs","heading":"List jobs","markdown":"<Endpoint id=\"audio.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  Most recent jobs first, with `limit` (default 20, up to <Limit name=\"pageSizeMax\" />) and `offset`. The list carries only the status; the result and the link live in `GET /jobs/{id}`. Jobs and files are deleted after the retention period (<Limit name=\"audioRetention\" />).\n</Endpoint>"},{"anchor":"resultado","heading":"Status, result and download","markdown":"<Endpoint id=\"audio.get\">\n  There is no separate download route: the result and the file come from this endpoint. With `status: completed`, the response carries the content by job type — `transcript` (also inline up to 32 KB; `truncated: true` flags the cut) for transcription and translation, `summary`, `analysis`, `conversion`, `generation` or `revoice` — and `download_url` with `download_expires_at`. The link is signed and expires: query again to get a fresh link. On `failed`, `error_code` explains why and the charge is refunded. The read is metered against the [daily quota](/en/cobranca-e-limites#franquia-diaria).\n\n  `status`: `pending`, `processing`, `completed` or `failed`.\n</Endpoint>"},{"anchor":"vozes-clonadas","heading":"Cloned voices and consent (MCP only)","markdown":"Until a REST route exists, voice cloning is **only in the MCP tools** `voice_create`, `voice_list` and `voice_delete` (scope `tools:audio:speak`). A cloned voice serves only the account that created it, in `audio_speak` (`voice: \"vc_…\"`, cloned-voice price) and in `audio_revoice` (`voice_id`).\n\n* **`voice_create`** clones the **account holder's own voice** from a <Limit name=\"audioVoiceSample\" /> sample. Consent is mandatory: call with `dry_run` first to read `consent_terms`, `consent_version` and `consent_phrase`; record the sample with **you alone reading the phrase aloud**; then call with `consent.accepted: true` and the `consent_version`. Samples without the phrase, with more than one voice or outside the duration are rejected and the charge is refunded (`audio.voice_sample_rejected`). Without the acceptance, the answer is `audio.consent_required`. It is asynchronous: follow with `voice_list` until the voice is `completed`.\n* **`voice_list`** lists the account's voices (`voice_id`, name, language, `status`, expiry). Free.\n* **`voice_delete`** really deletes the voice: the encrypted sample and the consent record are destroyed and the voice stops working at once (jobs queued in it fail and are refunded). Free."},{"anchor":"uso-responsavel","heading":"Responsible use","markdown":"* **Only your voice, with your consent.** Cloning another person's voice, even with their recording in hand, is not allowed; the check for the spoken phrase and a single speaker exists to prevent that.\n* **No imitation.** Neither speech, `revoice` nor sound generation accepts imitating existing artists, voices or works.\n* **Limits:** up to <Limit name=\"audioVoicesMax\" /> cloned voices per account, <Limit name=\"audioVoiceCreatesPerDay\" /> creations per day and <Limit name=\"audioVoiceUsesPerHour\" /> uses per hour (`audio.voice_limit`, `audio.voice_rate_limited`).\n* **Stored data:** the sample stays encrypted for up to <Limit name=\"audioVoiceSampleRetention\" /> or until `voice_delete`; every creation, rejection, use and deletion is audited, with no voice content. The input and output audio of other jobs expires in <Limit name=\"audioRetention\" />.\n* **Tell your listeners:** if you publish synthetic audio, say the voice is generated, especially when it is a cloned voice."},{"anchor":"scopes","heading":"Scopes","markdown":"| Scope                    | Operations                                                                      |\n| ------------------------ | ------------------------------------------------------------------------------- |\n| `tools:audio:transcribe` | `transcribe`, `summarize`, `translate`                                          |\n| `tools:audio:speak`      | `speak`, `generate`, `revoice` (in MCP, also `voice_create` and `voice_delete`) |\n| `tools:audio:process`    | `analyze`, `convert`                                                            |\n| `tools:audio:read`       | `voices`, `jobs`, `jobs/{id}` (the three scopes above also read their own jobs) |\n\n`tools:manage` covers everything. The full list is in [Authentication](/en/autenticacao)."},{"anchor":"erros","heading":"Errors","markdown":"`audio.*` in the response or, for jobs that fail after being accepted, in the job's `error_code`.\n\n<ErrorTable tool=\"audio\" />"}]},{"slug":"ferramentas/base-de-conhecimento","url":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento","title":"Knowledge base","description":"Hybrid search (semantic + keyword) over your documents, with rerank and relevance feedback, for RAG and AI assistants.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"base-de-conhecimento\" />"},{"anchor":"overview","heading":"Overview","markdown":"A **knowledge base** (collection) stores text or Markdown documents. Each document is split into **sections** (by headings) and **chunks** (pieces of \\~96 tokens), which are indexed in two ways: by meaning (embeddings) and by keywords. Search combines both and returns the most relevant chunks, ready to pass to a language model (RAG) or show to the user.\n\n<Steps>\n  <Step>\n    **Create a knowledge base** with the language of your documents (it improves keyword search).\n  </Step>\n\n  <Step>\n    **Feed the knowledge base.** Send text or Markdown, import files (PDF, Office, images) or connect a source that syncs by itself (website, sitemap, GitHub repository, Google Drive folder). Indexing is asynchronous: poll `status` until it is `ready`. Re-uploading with the same `external_id` replaces the previous version without taking the knowledge base offline.\n  </Step>\n\n  <Step>\n    **Search** with the user's question and use the returned chunks as context for the answer.\n  </Step>\n\n  <Step>\n    **Answer with citations** (optional): `answer` returns a text in which every claim points to its source chunk, or says it doesn't know. For your website, embed the [widget](#widget).\n  </Step>\n\n  <Step>\n    **Record feedback** (optional): confirmed chunks rank higher in similar future searches, and rejected ones drop out.\n  </Step>\n</Steps>\n\n<EndpointList tool=\"base-de-conhecimento\" />"},{"anchor":"custos","heading":"Costs","markdown":"<PricingTable tool=\"base-de-conhecimento\" group=\"per-use\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"free-quota\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"blocks\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"volume\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"storage\" />\n\n<Callout title=\"How blocks work\">\n  Searches and rerank have a daily free quota. After it, the call that opens a block debits the whole block at once (10 calls for the first one, then 20, 50 and 100 as the day's paid usage grows), and the next calls in that block are not charged again. Counters reset at midnight UTC, and unused calls in a block expire without a refund. The `rag_get_usage` MCP tool and the searches' `dry_run` show what the next call will charge. Reading knowledge bases, documents and feedback is free and does not count toward the [daily free read quota](/en/cobranca-e-limites#franquia-diaria).\n</Callout>\n\nIngestion is charged **when it finishes**, based on the actual number of chunks; the `estimate` in the response is an approximation. Content identical to what is already indexed is not charged again. Use `X-Dry-Run: true` when adding a document to see the estimate without sending anything."},{"anchor":"knowledge-bases","heading":"Knowledge bases","markdown":""},{"anchor":"create-knowledge-base","heading":"Create knowledge base","markdown":"<Endpoint\n  id=\"rag.collections.create\"\n  example={{ body: `{\n\"name\": \"Help center\",\n\"description\": \"Public support articles\",\n\"language\": \"english\",\n\"default_mode\": \"fast\"\n}` }}\n>\n  * `name` (required, up to 120 characters) and `description` (up to 1,000).\n  * `language`: `simple` (default), `portuguese`, `english`, `spanish` or `french`. Sets the dictionary used by keyword search.\n  * `default_mode`: `fast` (default) or `accurate` (extra rerank, more precise and slightly more expensive).\n  * Advanced settings: `chunk_size_tokens` (16–480, default 96), `chunk_overlap_tokens` (default 16), `section_max_tokens` (default 768), `max_per_document` (0–20, default 2), `dense_weight` and `lexical_weight` (0–10, default 1; they can't both be 0) and `feedback_enabled` (default `true`).\n</Endpoint>"},{"anchor":"list-knowledge-bases","heading":"List knowledge bases","markdown":"<Endpoint id=\"rag.collections.list\">\n  Also returns the account's usage against its limits (`usage`).\n</Endpoint>"},{"anchor":"retrieve-knowledge-base","heading":"Retrieve knowledge base","markdown":"<Endpoint id=\"rag.collections.get\" />"},{"anchor":"available-filters","heading":"Available filters","markdown":"<Endpoint id=\"rag.collections.facets\">\n  Lists the metadata keys on the knowledge base's documents and the most common values of each, so you know what you can use in `filters` when searching. `documents` is how many ready documents with metadata were scanned and `keys` is the total number of filterable keys. If `sampled` is `true`, the knowledge base has more than 5000 documents and the counts come only from the 5000 most recent ones. In `facets`, each item has the `key`, the number of `documents` that have it, `distinct` (total distinct values) and `values[]` (the most common values, each with `value` and `documents`). `array: true` means the key holds a list; filter with `{\"key\": [\"value\"]}`.\n</Endpoint>"},{"anchor":"update-knowledge-base","heading":"Update knowledge base","markdown":"<Endpoint id=\"rag.collections.update\" example={{ body: `{ \"default_mode\": \"accurate\" }` }}>\n  Send only the fields you want to change. The embeddings model can't be changed after creation. Changes to chunk size only apply to documents uploaded afterwards; to apply them to the whole knowledge base, reindex it.\n</Endpoint>"},{"anchor":"reindex-knowledge-base","heading":"Reindex knowledge base","markdown":"<Endpoint id=\"rag.collections.reindex\">\n  Reprocesses every document with the knowledge base's current settings. Each chunk is charged as in a new ingestion.\n</Endpoint>"},{"anchor":"knowledge-base-health-dashboard","heading":"Knowledge base health dashboard","markdown":"<Endpoint id=\"rag.collections.overview\" example={{ query: { rows: 'category', days: '30' } }}>\n  Free summary to keep the knowledge base tidy: document counts by `status`, how many are outdated (`stale`), have never been queried or have no metadata; a matrix of documents by metadata (`rows` and `cols` are metadata keys, up to 20 values per axis); activity over the last `days` days (1 to 180, default 30); and `attention`, with up to 5 examples of failed, outdated, duplicated or contradictory documents and documents with negative feedback. The response is cached for 60 s.\n</Endpoint>"},{"anchor":"document-index","heading":"Document index","markdown":"<Endpoint id=\"rag.collections.index\" example={{ query: { fields: 'category', order: 'title' } }}>\n  Compact list of `ready` documents, without content, so the agent can choose what to read. `filters` (metadata JSON), `fields` (up to 20 metadata keys to copy), `order` (`updated_at_desc`, default, or `title`) and `max_bytes` (default 4096, maximum 32768). `omitted` counts the documents cut off by the limit.\n</Endpoint>"},{"anchor":"find-duplicates-and-contradictions","heading":"Find duplicates and contradictions","markdown":"<Endpoint id=\"rag.collections.lint\" example={{ body: `{ \"limit\": 20 }` }}>\n  Compares the 500 most recent documents (optionally filtered by `filters`) using the embeddings that already exist; it calls no model and writes nothing. `duplicate` appears at similarity of 0.75 or higher; `possible_contradiction`, between 0.4 and 0.75 when both have the same `tipo` metadata. Each finding says which document to keep (`keep`, the newest) and what to do (`action`). `limit` ranges from 1 to 100 (default 20) and `include_content: true` returns the start of each document.\n</Endpoint>"},{"anchor":"switch-the-embeddings-model","heading":"Switch the embeddings model","markdown":"<Endpoint id=\"rag.collections.model\" example={{ body: `{ \"embedding_model\": \"BAAI/bge-m3\" }` }}>\n  Switches between `intfloat/multilingual-e5-small` (default) and `BAAI/bge-m3` (High precision) by reindexing the knowledge base **in the background**; you don't need to call reindex. The switch costs 50% of the normal ingestion price, and going back to the previous model is free while it runs and for 24 h afterwards, once. Use `X-Dry-Run: true` to see the estimate. During the switch search keeps working (the query is embedded with both models) and new documents are refused with `rag.model_switch_in_progress`. Track it through the knowledge base's `model_switch` field (`running`, `paused` or `done`); if the balance runs out, it pauses with `paused_reason: insufficient_credits` and resumes when you top up.\n</Endpoint>"},{"anchor":"mark-documents-as-outdated","heading":"Mark documents as outdated","markdown":"<Endpoint id=\"rag.collections.changes\" example={{ body: `{ \"paths\": [\"docs/api/\", \"src/auth.ts\"], \"commit\": \"a1b2c3d\" }` }}>\n  For knowledge bases tied to code or files: send the `paths` that changed (exact file or `dir/` prefix, up to 1000) and the documents that declared those `paths` when uploaded get `stale: true`. Nothing is reindexed or charged; re-uploading the document clears the mark. Use `exclude_stale` in search to skip them. `commit` (up to 64 characters) avoids marking what was already checked at that commit.\n</Endpoint>"},{"anchor":"delete-knowledge-base","heading":"Delete knowledge base","markdown":"<Endpoint id=\"rag.collections.delete\">\n  Deletion is asynchronous and removes documents, chunks and feedback.\n</Endpoint>"},{"anchor":"documents","heading":"Documents","markdown":""},{"anchor":"add-or-replace-document","heading":"Add or replace document","markdown":"<Endpoint\n  id=\"rag.documents.add\"\n  example={{ body: `{\n\"external_id\": \"article-42\",\n\"title\": \"How to change your password\",\n\"content\": \"# Account\\\\n\\\\n## How to change your password\\\\n\\\\nGo to Settings > Security…\",\n\"source_uri\": \"https://help.example.com/articles/42\",\n\"metadata\": { \"category\": \"account\" }\n}` }}\n>\n  * `content` (required): plain text or Markdown in UTF-8, up to <Limit name=\"ragDocumentMax\" />. For PDF, Office and images, use [Import file](#rag-documents-file).\n  * `external_id` (optional, up to 200 bytes): your own identifier. Re-uploading with the same value **replaces** the document; the previous version keeps answering searches (`processing: true`) until the new one is ready.\n  * `paths` (up to 50) and `commit`: the files the document comes from, so it can be [marked as outdated](#rag-collections-changes) when they change.\n  * Recognized secrets in the text (API keys, tokens) are replaced with `[REDACTED:type]` before indexing; `redactions` counts how many.\n  * `metadata`: object with up to 50 keys (4 KB, up to 3 levels deep), used by search filters. It is not encrypted; don't put sensitive data in it.\n  * Poll [Retrieve document](#rag-documents-get) until `status: ready` (or `failed`, with an `error_code`). Before accepting the document, the API checks that your balance covers the estimate.\n</Endpoint>"},{"anchor":"import-file-pdf-office-image","heading":"Import file (PDF, Office, image)","markdown":"<Endpoint id=\"rag.documents.file\">\n  `multipart/form-data` upload with `file` (required, up to 50 MB) and, optionally, `title`, `external_id`, `source_uri`, `metadata` (JSON object as text) and `ocr` (default `true`; turn it off for PDFs that already have text). Formats: `.pdf`, `.docx`, `.pptx`, `.xlsx`, `.html`, `.md`, `.csv`, `.asciidoc` and images (`.png`, `.jpg`, `.tiff`, `.bmp`, `.webp`). The file is converted to Markdown (tables preserved, images only through their OCR text) and then indexed like any document, with the same <Limit name=\"ragDocumentMax\" /> limit for the resulting text. The response is `202` with the import `id`; poll it until `completed` (it carries the `document_id`) or `failed`. When no conversion is available, the response is `503` (`rag.service_unavailable`).\n</Endpoint>"},{"anchor":"retrieve-file-import","heading":"Retrieve file import","markdown":"<Endpoint id=\"rag.imports.get\" />"},{"anchor":"list-documents","heading":"List documents","markdown":"<Endpoint id=\"rag.documents.list\" example={{ query: { status: 'ready', limit: '20' } }}>\n  Optional `status` filter (`pending`, `processing`, `ready`, `failed`, `deleting`). Cursor pagination: pass the `next_cursor` you received as `after`. `limit` defaults to 20, maximum 100.\n</Endpoint>"},{"anchor":"retrieve-document","heading":"Retrieve document","markdown":"<Endpoint id=\"rag.documents.get\" />"},{"anchor":"update-title-or-metadata","heading":"Update title or metadata","markdown":"<Endpoint id=\"rag.documents.update\" example={{ body: `{ \"metadata\": { \"category\": \"security\" } }` }}>\n  Doesn't reprocess the content and costs nothing.\n</Endpoint>"},{"anchor":"view-indexed-sections","heading":"View indexed sections","markdown":"<Endpoint id=\"rag.documents.sections\">\n  Shows how the document was split, useful for tuning headings and chunk size.\n</Endpoint>"},{"anchor":"retry-failed-document","heading":"Retry failed document","markdown":"<Endpoint id=\"rag.documents.retry\">\n  Only for documents with `status: failed`; others respond `rag.document_not_failed`.\n</Endpoint>"},{"anchor":"delete-document","heading":"Delete document","markdown":"<Endpoint id=\"rag.documents.delete\" />"},{"anchor":"synced-sources","heading":"Synced sources","markdown":"A **source** imports pages from outside and keeps them up to date: on each sync, new or changed pages are reindexed, unchanged ones (same content) are not charged again, and pages that disappeared from the origin are removed from the knowledge base. Each page becomes a regular document (`external_id` `src:<source id>:…`, with `source_id` and `source_url` in the metadata), charged per chunk as in a manual ingestion; if balance or quota runs out, the sync fails and shows the reason.\n\n| `kind`    | `start_url`                     | What it imports                                                                                                                                                         |\n| --------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `site`    | home page                       | Crawls links on the same host up to `max_depth`, respecting `robots.txt`; HTML becomes Markdown and linked PDF/Office files are converted.                              |\n| `sitemap` | sitemap (or index) URL          | Imports the listed pages, up to `max_pages`. `max_depth` does not apply.                                                                                                |\n| `github`  | `https://github.com/owner/repo` | **Public** repository; `config.ref` (default `HEAD`) and `config.paths` (prefixes) filter. Reads `.md`, `.mdx`, `.txt`, `.rst` and converts the supported file formats. |\n| `gdrive`  | URL of a `/folders/<id>` folder | Folder shared as \"anyone with the link\"; Docs become Markdown. Only available if the server has the integration enabled.                                                |"},{"anchor":"list-synced-sources","heading":"List synced sources","markdown":"<Endpoint id=\"rag.sources.list\" />"},{"anchor":"create-synced-source","heading":"Create synced source","markdown":"<Endpoint\n  id=\"rag.sources.create\"\n  example={{ body: `{\n\"kind\": \"site\",\n\"name\": \"Help center\",\n\"start_url\": \"https://help.example.com\",\n\"max_pages\": 100,\n\"max_depth\": 3,\n\"frequency_hours\": 24\n}` }}\n>\n  * `name` (up to 200) and `start_url` (http or https) are required; `kind` defaults to `site`.\n  * `max_pages`: 1 to 1000 (default 100). `max_depth`: 1 to 10 (default 3).\n  * `frequency_hours`: `0` (manual only), `6`, `24` (default) or `168`.\n  * The first sync starts right away. Pages and files up to 20 MB each; a file that fails to convert is skipped without failing the sync.\n</Endpoint>"},{"anchor":"update-source","heading":"Update source","markdown":"<Endpoint id=\"rag.sources.update\" example={{ body: `{ \"frequency_hours\": 168, \"enabled\": true }` }}>\n  All fields are optional. With `enabled: false` or `frequency_hours: 0`, `next_sync_at` becomes null.\n</Endpoint>"},{"anchor":"sync-now","heading":"Sync now","markdown":"<Endpoint id=\"rag.sources.sync\">\n  Responds `409` if the source is already syncing. `pages_total`, `pages_new`, `pages_changed` and `pages_removed` summarize the run.\n</Endpoint>"},{"anchor":"sync-history","heading":"Sync history","markdown":"<Endpoint id=\"rag.sources.syncs\" />"},{"anchor":"delete-source","heading":"Delete source","markdown":"<Endpoint id=\"rag.sources.delete\" />"},{"anchor":"search","heading":"Search","markdown":""},{"anchor":"search-modes-and-reranker","heading":"Search modes and reranker","markdown":"* **`fast`** (default): combines semantic and keyword search. The `score` is a rank fusion (small values, around 0.03), good for ordering, not for comparing against a threshold.\n* **`accurate`**: after the fusion, a **reranker** rereads the top 20 candidates together with the question and returns a calibrated `score` from 0 to 1; only on it does `min_score` take effect. It charges the rerank add-on. If rerank is unavailable, the search falls back to `fast` without the add-on (`mode_used`).\n* **Reranker**: the knowledge base's `rerank_provider` (and `provider` in standalone rerank and in `decide`) chooses between `bge` (default, cross-encoder) and `laya`, Respawn's relevance judge, which asks whether the chunk answers the query and returns the probability. Laya usually separates chunks that merely resemble the question better, but it is slower and, if it is down, `accurate` falls back to `fast` and standalone rerank responds `503` (`rag.service_unavailable`). The price is the same as `bge`.\n* If the embeddings service fails, search still responds by keywords only (`mode_used: lexical`), charged as cache."},{"anchor":"query-knowledge-base","heading":"Query knowledge base","markdown":"<Endpoint\n  id=\"rag.query\"\n  example={{ body: `{\n\"query\": \"how do I change my password?\",\n\"top_k\": 5,\n\"filters\": { \"category\": \"account\" }\n}` }}\n>\n  * `query` (required, up to <Limit name=\"ragQueryCharsMax\" /> characters).\n  * `top_k`: how many chunks to return (default 5, maximum <Limit name=\"ragTopKMax\" />).\n  * `mode`: `fast` or `accurate`; if omitted, the knowledge base's default applies. If rerank is unavailable in `accurate` mode, the search falls back to `fast` without charging the add-on; `mode_used` shows which one was used.\n  * `filters`: only chunks from documents whose metadata contains these values.\n  * `min_score` (only in `accurate`) and `max_per_document` (0–20) refine the result.\n  * `updated_after` and `updated_before` (RFC 3339) filter by the document's update date, and `exclude_stale: true` skips documents marked as outdated. Each result carries `updated_at`, `stale`, `paths`, `commit`, `source_uri` and `also_in` (other documents with the same text, collapsed into a single result).\n  * `highlights` marks, in Unicode character positions, the terms found in `content`. Repeated queries within <Limit name=\"ragQueryCacheTtl\" /> are served from cache (`cached: true`) and cost less.\n\n  <Callout type=\"warn\" title=\"User content\">\n    The returned chunks are content from your documents. When passing them to a language model, treat them as data, not instructions.\n  </Callout>\n</Endpoint>"},{"anchor":"relevance-feedback","heading":"Relevance feedback","markdown":""},{"anchor":"record-relevance-feedback","heading":"Record relevance feedback","markdown":"<Endpoint\n  id=\"rag.feedback.add\"\n  example={{ body: `{\n\"query\": \"how do I change my password?\",\n\"section_id\": \"b2c4d6e8-0a1b-4c3d-8e5f-6a7b8c9d0e1f\",\n\"label\": \"positive\",\n\"end_user_ref\": \"user-123\"\n}` }}\n>\n  * `label`: `positive` (the chunk answers the question) or `negative`.\n  * `end_user_ref` (optional): identifies who rated it, when the vote comes from your end user. Only a hash is stored.\n  * Up to <Limit name=\"ragFeedbackPerMinute\" /> votes per minute. Similar questions are grouped, and a vote applies to the whole group.\n</Endpoint>"},{"anchor":"list-feedback","heading":"List feedback","markdown":"<Endpoint id=\"rag.feedback.list\" />"},{"anchor":"delete-a-feedback-group","heading":"Delete a feedback group","markdown":"<Endpoint id=\"rag.feedback.delete\" />"},{"anchor":"standalone-rerank","heading":"Standalone rerank","markdown":""},{"anchor":"rerank-texts","heading":"Rerank texts","markdown":"<Endpoint\n  id=\"rag.rerank\"\n  example={{ body: `{\n\"query\": \"refund deadline\",\n\"documents\": [\"Delivery within 5 business days.\", \"Free shipping on orders over $40.\", \"Refunds within 7 days of the return.\"],\n\"top_k\": 2\n}` }}\n>\n  Orders **your own** list of texts by relevance to the question, with no knowledge base involved. Up to <Limit name=\"ragRerankDocumentsMax\" /> texts per call (each is truncated at 2,000 characters). `index` points to the position in the array you sent.\n</Endpoint>"},{"anchor":"answer-with-citations","heading":"Answer with citations","markdown":""},{"anchor":"answer-with-citations-1","heading":"Answer with citations","markdown":"<Endpoint\n  id=\"rag.answer\"\n  example={{ body: `{\n\"query\": \"how do I change my password?\",\n\"top_k\": 5,\n\"min_score\": 0.35,\n\"generate\": false\n}` }}\n>\n  Runs an `accurate` search and answers **only** with chunks whose `score` is at or above `min_score` (0 to 1, default 0.35, a starting point worth calibrating on your knowledge base).\n\n  * With an answer, `answered: true` and each `[n]` in the text points to a `citations[]` entry (`section_id`, `document_id`, `title`, `heading_path`, `source_uri`, `score`, `excerpt`). There is never an answer without a citation.\n  * With no chunk above the threshold, `answered: false`, `reason: no_passage_above_threshold`, no citations, and the `rag.question.unanswered` webhook is fired. If rerank couldn't verify relevance, `reason: relevance_unverified` (it asserts nothing and doesn't fire the webhook).\n  * `generate: false` (default) builds the answer by quoting the 3 best chunks. With `generate: true`, the platform's default AI provider writes the text from the chunks; with no valid citation marker or no provider, it falls back to extractive mode (`generated: false`). With generated text, the knowledge base chunks are sent to the platform's AI provider.\n  * With `Accept: text/event-stream`, the response comes as SSE: `citations`, several `delta` (pieces of the text) and `done` (the full result). The text is generated in full beforehand; SSE only slices it.\n  * Charged as an `accurate` search; generation has no separate charge. Use the `query_id` in [feedback](#rag-feedback-add) to feed the analytics CTR.\n</Endpoint>"},{"anchor":"widget","heading":"Embeddable chat widget","markdown":"The widget is a chat button in plain JavaScript (no CDN, in shadow DOM, only writes text) that answers with citations using your knowledge base. It uses a **public key** (`rpk_…`) that can only ask questions on one knowledge base: it doesn't read others and writes nothing.\n\n```html\n<script src=\"https://api.respawntech.dev/widget/rag.js\" data-key=\"rpk_...\" async></script>\n```\n\nAttributes: `data-key` (required), `data-title`, `data-placeholder` and `data-dont-know` (text when there is no answer). Questions asked through the widget are charged to the knowledge base owner, like `answer`."},{"anchor":"create-public-key-widget","heading":"Create public key (widget)","markdown":"<Endpoint\n  id=\"rag.publickeys.create\"\n  example={{ body: `{\n\"name\": \"Corporate site\",\n\"allowed_origins\": [\"https://www.example.com\", \"https://*.example.com\"],\n\"rate_per_minute\": 30,\n\"generate\": false\n}` }}\n>\n  * `allowed_origins` (required, 1 to 20): `https://site.com` or `https://*.site.com`, with no path. The server refuses requests whose `Origin` is not on the list.\n  * `rate_per_minute`: 1 to 600 (default 30), per key. `generate` turns on AI-generated text in this key's answers.\n  * Up to 20 keys per knowledge base. The `key` is shown **only once**; we store only the hash.\n\n  <Callout type=\"warn\" title=\"Origin is not authentication\">\n    Only the browser honors `Origin`; a client outside it can forge it. What limits abuse is the key's per-minute limit, and the cost falls on you. Revoke the key if you see unusual use.\n  </Callout>\n</Endpoint>"},{"anchor":"list-public-keys","heading":"List public keys","markdown":"<Endpoint id=\"rag.publickeys.list\" />"},{"anchor":"revoke-public-key","heading":"Revoke public key","markdown":"<Endpoint id=\"rag.publickeys.revoke\" />"},{"anchor":"answer-through-the-widget-public-route","heading":"Answer through the widget (public route)","markdown":"<Endpoint id=\"rag.public.answer\" example={{ body: `{ \"query\": \"how do I change my password?\" }` }}>\n  No API token: send the key in the `X-Respawn-Public-Key` header and the browser sends the `Origin`. The knowledge base comes from the key, and `top_k`, `min_score` and `generate` are not accepted in the request. Responds like `answer` (JSON or SSE), with CORS enabled for the allowed origin. Errors: `401` (key missing, unknown or revoked), `403` (origin not allowed) and `429` (per-minute limit).\n</Endpoint>"},{"anchor":"widget-script","heading":"Widget script","markdown":"<Endpoint id=\"rag.widget\" />"},{"anchor":"question-analytics","heading":"Question analytics","markdown":""},{"anchor":"question-analytics-1","heading":"Question analytics","markdown":"<Endpoint id=\"rag.analytics\" example={{ query: { days: '30' } }}>\n  Shows what users ask and what the knowledge base can't answer, based on `answer` calls and the widget. `days` ranges from 1 to 90 (default 30).\n\n  * `answer_rate`: answered ÷ total questions.\n  * `feedback_ctr`: answered questions that received a vote (by `query_id`) ÷ answered.\n  * `positive_rate`: positive votes ÷ votes.\n  * `top_questions` and `unanswered_questions`: up to 20 grouped questions each, so you can find out what to write in the knowledge base.\n\n  Questions are encrypted with the knowledge base's key. The widget has no vote of its own: CTR only counts votes sent through the API.\n</Endpoint>"},{"anchor":"routing","heading":"Routing","markdown":""},{"anchor":"decide-between-options-routing","heading":"Decide between options (routing)","markdown":"<Endpoint\n  id=\"rag.decide\"\n  example={{ body: `{\n\"text\": \"I want to cancel my subscription\",\n\"options\": [\n  { \"label\": \"billing\", \"description\": \"payments, invoices and cancellations\" },\n  { \"label\": \"support\", \"description\": \"technical problems\" }\n],\n\"threshold\": 0.5,\n\"min_margin\": 0.1\n}` }}\n>\n  Classifies a text among 2 to 50 options without using an LLM, scoring each `label: description` with the reranker (`provider`: `bge` or `laya`). Useful for routing requests. If the best option doesn't exceed `threshold` (default 0.5) or isn't `min_margin` (default 0.1) ahead of the second, `decided` is `false`: treat it as \"don't know\". The full `ranking` is always returned. Standalone rerank price and quota.\n</Endpoint>"},{"anchor":"pricing-and-account-limits","heading":"Pricing and account limits","markdown":""},{"anchor":"retrieve-pricing","heading":"Retrieve pricing","markdown":"<Endpoint id=\"rag.pricing\" />"},{"anchor":"retrieve-limits","heading":"Retrieve limits","markdown":"<Endpoint id=\"rag.limits\" />\n\nValues in effect for your account (already including the plan bonus), useful for validating an upload before making it."},{"anchor":"events-webhooks","heading":"Events (webhooks)","markdown":"Register an endpoint in the dashboard to receive:\n\n* `rag.document.ready` and `rag.document.failed`: `collection_id`, `document_id`, `external_id` and `version`; the failure one also carries `error_code`. They apply to uploaded documents, file imports and documents from sources.\n* `rag.question.unanswered`: `collection_id`, `question`, `query_id` and `source` (`api` or `widget`), when no chunk passed `min_score`. Good for feeding a \"what to write\" queue.\n\nDelivery is best-effort, as with other events. See the [event list](/en/webhooks)."},{"anchor":"using-it-through-mcp","heading":"Using it through MCP","markdown":"The same operations are available as MCP tools in the `rag` group ([full list](/en/agentes-de-ia)): `rag_answer`, `rag_decide`, `rag_index`, `rag_lint`, `rag_mark_changed` and `rag_retry_document` are the new ones. The read ones (`rag_index`, `rag_lint`) are free; use `rag_access=query` on the connection to hide the ones that change the knowledge base."},{"anchor":"errors","heading":"Errors","markdown":"<ErrorTable tool=\"base-de-conhecimento\" />\n\nCodes without an HTTP status show up in the `error_code` field of a document with `status: failed`."},{"anchor":"limites","heading":"Limits","markdown":"| Limit                                 | Value                                        |\n| ------------------------------------- | -------------------------------------------- |\n| Document size                         | <Limit name=\"ragDocumentMax\" />              |\n| Knowledge bases per account           | <Limit name=\"ragCollectionsMax\" />           |\n| Indexed chunks per account            | <Limit name=\"ragChunksMax\" />                |\n| Documents processing at the same time | <Limit name=\"ragPendingMax\" />               |\n| Query length                          | <Limit name=\"ragQueryCharsMax\" /> characters |\n| Chunks per search (`top_k`)           | <Limit name=\"ragTopKMax\" />                  |\n| Texts per standalone rerank           | <Limit name=\"ragRerankDocumentsMax\" />       |\n| Feedback votes per minute             | <Limit name=\"ragFeedbackPerMinute\" />        |\n| Imported file                         | 50 MB                                        |\n| Pages per source                      | 1,000 (default 100)                          |\n| Public keys per knowledge base        | 20                                           |\n\nKnowledge base routes ignore the `X-Idempotency-Key` header: they are already idempotent (repeated content is neither reindexed nor charged). A key restricted to specific knowledge bases only sees those and can't create new ones (`forbidden`)."}]},{"slug":"ferramentas/ci-error-extract","url":"https://docs.respawntech.dev/en/ferramentas/ci-error-extract","title":"CI Error Extractor","description":"Extract from a CI log only the lines that explain the failure.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"ci-error-extract\" />"},{"anchor":"overview","heading":"Overview","markdown":"Send the log of a CI job and get back only the error lines. Useful for agents and bots that need to understand why a build failed without loading thousands of lines into their context. Also available in the [MCP](/en/agentes-de-ia) as `ci_error_extract`.\n\n<EndpointList tool=\"ci-error-extract\" />"},{"anchor":"custos","heading":"Costs","markdown":"Each extraction is a metered read: it counts toward the [daily free quota](/en/cobranca-e-limites#franquia-diaria) and, once it's used up, debits the amount shown on the endpoint. Billing applies to API keys only."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"extract-the-error-from-a-log","heading":"Extract the error from a log","markdown":"<Endpoint id=\"ci.extract\" example={{ body: `{ \"log\": \"npm ERR! code ELIFECYCLE\\\\nnpm ERR! errno 1\\\\nFAIL src/app.test.ts\", \"max_lines\": 40 }` }}>\n  `log` (required) is the log text. `max_lines` (optional, default 40, max 200) caps how many error lines are returned.\n</Endpoint>"},{"anchor":"errors","heading":"Errors","markdown":"<ErrorTable tool=\"ci-error-extract\" />"}]},{"slug":"ferramentas/memoria-de-aprendizados","url":"https://docs.respawntech.dev/en/ferramentas/memoria-de-aprendizados","title":"Learnings memory","description":"Long-term memory for AI agents: save what was learned in each project and recall it before the next task.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"memoria-de-aprendizados\" />"},{"anchor":"como-funciona","heading":"How it works","markdown":"A learning is a short text with a `kind`, tied to a `project`:\n\n| `kind`       | To keep                                     |\n| ------------ | ------------------------------------------- |\n| `pitfall`    | Pitfalls and mistakes that must not repeat. |\n| `decision`   | Decisions made and why.                     |\n| `convention` | Project conventions.                        |\n| `howto`      | How to do something (run, publish, debug).  |\n| `other`      | Whatever doesn't fit the others.            |\n\nLearnings live in a [knowledge base](/en/ferramentas/base-de-conhecimento) called `agent-learnings`, **created automatically** on the first save. `recall` runs a semantic search for the task and returns only the items of the requested `project`, with each one's `score`.\n\nIt is also available in the [MCP](/en/agentes-de-ia) as `learning_save`, `learning_recall` and `learning_forget` (`learnings` toolset).\n\n<EndpointList tool=\"memoria-de-aprendizados\" />"},{"anchor":"ingestao","heading":"Duplicates and asynchronous ingestion","markdown":"* **Deduplication:** `save` compares by hash and by semantic similarity. If an equal or similar learning already exists, nothing new is stored and the response carries `duplicate_of` with the existing id.\n* **Asynchronous ingestion:** the text is indexed in the background. A `recall` right after a `save` may not return the item yet; try again in a few seconds."},{"anchor":"custos","heading":"Costs","markdown":"Billing is the [knowledge base](/en/ferramentas/base-de-conhecimento)'s: `save` like ingestion (per chunk), `recall` like search (daily free quota + blocks) and storage per day. Forgetting is free. The memory follows the same enablement rules as the knowledge base (`rag.disabled` and the other `rag.*` errors)."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"save-a-learning","heading":"Save a learning","markdown":"<Endpoint id=\"learnings.save\" example={{ body: `{ \"project\": \"respawn-tech-docs\", \"kind\": \"pitfall\", \"text\": \"check:backend reads ../respawn-tech-backend; run it only once the backend is merged.\", \"source\": \"conversation of 10/06\" }` }}>\n  `project`, `kind` and `text` are required; `source` is optional and says where it came from. The response carries the learning's `id` or, for a repeat, `duplicate_of`.\n</Endpoint>"},{"anchor":"recall-learnings","heading":"Recall learnings","markdown":"<Endpoint id=\"learnings.recall\" example={{ body: `{ \"project\": \"respawn-tech-docs\", \"task\": \"run the checks before opening the PR\", \"top_k\": 5 }` }}>\n  `project` and `task` are required; `top_k` caps how many items come back. Each item carries `id`, `kind`, `text`, `source` and `score`. Remember ingestion is asynchronous: a just-saved item may not show up yet.\n</Endpoint>"},{"anchor":"forget-a-learning","heading":"Forget a learning","markdown":"<Endpoint id=\"learnings.forget\" />"}]},{"slug":"ferramentas/notify","url":"https://docs.respawntech.dev/en/ferramentas/notify","title":"Notifications","description":"An agent alerts its human by email, signed webhook or ntfy, with channels, priority and a delivery history.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"notify\" />"},{"anchor":"como-funciona","heading":"How it works","markdown":"A notification has a `title`, a `message` and a `priority` (`normal` or `high`), and optionally a `url` for the human to open. It goes out through a **channel**:\n\n* **Chosen channel:** the request's `channel`, by channel name.\n* **Default channel:** the channel flagged `is_default`, when the request has no `channel`.\n* **Account email:** with no channel configured at all, the notification goes to the account's email.\n\nIt is also available in the [MCP](/en/agentes-de-ia) as the `notify` tool (`notify` toolset): the agent reports that it finished a long task or needs a decision.\n\n<EndpointList tool=\"notify\" />"},{"anchor":"canais","heading":"Channels","markdown":"Each channel has a `name`, unique in the account, and a `kind`:\n\n| `kind`    | Fields            | How it delivers                                                                             |\n| --------- | ----------------- | ------------------------------------------------------------------------------------------- |\n| `email`   | none              | Sends **only to the account's email**. You can't choose another recipient.                  |\n| `webhook` | `url`, `secret`   | Makes a `POST` to the URL, signed as in the [Webhook Relay](/en/ferramentas/webhook-relay). |\n| `ntfy`    | `server`, `topic` | Publishes to the [ntfy](https://ntfy.sh) topic on the given server.                         |\n\n`is_default` marks the channel used when the request doesn't name one, and `enabled: false` turns a channel off without deleting it. The webhook `secret` is never returned in responses: store it when you create the channel.\n\n<Callout type=\"warn\" title=\"The ntfy topic is the password\">\n  On public ntfy servers, anyone who knows the topic can read the notifications. Use a long, unguessable name, or your own server.\n</Callout>"},{"anchor":"limites","heading":"Limits, retry and history","markdown":"* **Rate limit:** there is a limit of notifications per period; above it the send fails with `notify.rate_limited`.\n* **Webhook retry:** a failed delivery is retried a few times before becoming `notify.delivery_failed`. The receiver must handle repeats and check the signature.\n* **History:** `GET /messages` lists the sends (channel, priority, status and date), paginated with `page` and `page_size` (up to 100). The history **does not store the title or the body of the message**."},{"anchor":"custos","heading":"Costs","markdown":"Each send through the API is billed at the amount shown on the endpoint. Channels and history are free, and a failed delivery is not billed."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"send-a-notification","heading":"Send a notification","markdown":"<Endpoint id=\"notify.send\" example={{ body: `{ \"title\": \"Deploy finished\", \"message\": \"Version 2.4 is live.\", \"priority\": \"normal\" }` }}>\n  `title` and `message` are required; `priority` is `normal` or `high`; `url` and `channel` are optional. Without `channel` it uses the default channel and, with no channel at all, the account's email.\n</Endpoint>"},{"anchor":"delivery-history","heading":"Delivery history","markdown":"<Endpoint id=\"notify.messages\" example={{ query: { page: \"1\", page_size: \"20\" } }}>\n  Paginated by `page` and `page_size` (up to 100). It doesn't include the message bodies.\n</Endpoint>"},{"anchor":"list-channels","heading":"List channels","markdown":"<Endpoint id=\"notify.channels.list\" />"},{"anchor":"create-a-channel","heading":"Create a channel","markdown":"<Endpoint id=\"notify.channels.create\" example={{ body: `{ \"name\": \"phone\", \"kind\": \"ntfy\", \"server\": \"https://ntfy.sh\", \"topic\": \"my-secret-topic\", \"is_default\": true }` }}>\n  `name` and `kind` are required. `url` and `secret` apply to `webhook`; `server` and `topic`, to `ntfy`. Fields that don't match the `kind` return `notify.invalid_channel`.\n</Endpoint>"},{"anchor":"get-a-channel","heading":"Get a channel","markdown":"<Endpoint id=\"notify.channels.get\" />"},{"anchor":"update-a-channel","heading":"Update a channel","markdown":"<Endpoint id=\"notify.channels.update\" example={{ body: `{ \"enabled\": false }` }}>\n  Send only the fields that change. A disabled channel returns `notify.channel_disabled` when chosen in a send.\n</Endpoint>"},{"anchor":"delete-a-channel","heading":"Delete a channel","markdown":"<Endpoint id=\"notify.channels.delete\" />"},{"anchor":"test-a-channel","heading":"Test a channel","markdown":"<Endpoint id=\"notify.channels.test\">\n  Sends a test notification through the channel, to check the configuration before you rely on it.\n</Endpoint>"},{"anchor":"errors","heading":"Errors","markdown":"<ErrorTable tool=\"notify\" />"}]},{"slug":"ferramentas/pdf-convert","url":"https://docs.respawntech.dev/en/ferramentas/pdf-convert","title":"PDF Convert","description":"API to convert HTML into PDF, PNG or JPG — asynchronous flow, batch, retention and costs.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"pdf-convert\" />"},{"anchor":"overview","heading":"Overview","markdown":"PDF Convert is **asynchronous**:\n\n1. `POST /convert` or `POST /batch` → **202 Accepted** with the `conversion_id` (the `{id}` used by the other routes).\n2. A worker renders it in the background (headless Chromium).\n3. Track it through the [`webhook_url` callback](/en/webhooks#pdf-callback) or by calling `GET /{id}`.\n4. Download it with `GET /{id}/download` once `status` is `completed`.\n\n<Callout type=\"warn\" title=\"Charged on enqueue\">\n  Credits are debited when the job is accepted. If enqueuing a batch fails after the debit, the amount is refunded automatically.\n</Callout>\n\n<EndpointList tool=\"pdf-convert\" />"},{"anchor":"custos","heading":"Costs","markdown":"<PricingTable tool=\"pdf-convert\" group=\"document\" />\n\n<PricingTable tool=\"pdf-convert\" group=\"addons\" />\n\n<PricingTable tool=\"pdf-convert\" group=\"batch-discounts\" />\n\nA batch accepts up to **<Limit name=\"pdfBatchMax\" /> items**. A retry charges the full cost again, add-ons included. Saving templates and uploading fonts costs no credits. API lookups and downloads follow the [daily free quota](/en/cobranca-e-limites#franquia-diaria)."},{"anchor":"conversion","heading":"Conversion","markdown":""},{"anchor":"convert-html-to-pdfpngjpg","heading":"Convert HTML to PDF/PNG/JPG","markdown":"<Endpoint\n  id=\"pdf.convert\"\n  example={{\n  headers: { 'X-Idempotency-Key': '550e8400-e29b-41d4-a716-446655440001' },\n  body: `{\n\"html\": \"<html><body><h1>Fatura</h1></body></html>\",\n\"css\": \"body { font-family: sans-serif; }\",\n\"format\": \"pdf\",\n\"page_size\": \"A4\",\n\"retention_days\": 7\n}`,\n}}\n>\n  Send **either `html` or `url`** (never both). With `url`, Chromium opens the public page and renders it; `css`, `google_fonts_url` and `data` are rejected in that mode (`pdf.invalid_option`).\n\n  | Field                                                         | Limit / values                                                                                                        |\n  | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |\n  | `html`                                                        | max. <Limit name=\"pdfHtmlMax\" />                                                                                      |\n  | `url`                                                         | alternative to `html`: public `http`/`https` address, up to 2048 characters (`pdf.invalid_url` if private or invalid) |\n  | `css`                                                         | max. <Limit name=\"pdfCssMax\" />                                                                                       |\n  | `format`                                                      | `pdf` (default), `png`, `jpg` / `jpeg`                                                                                |\n  | `file_name`                                                   | name of the generated file                                                                                            |\n  | `title`, `author`, `subject`, `keywords`                      | document metadata, up to 1000 characters each                                                                         |\n  | `header_html` / `footer_html`                                 | header and footer repeated on every page                                                                              |\n  | `google_fonts_url`                                            | Google Fonts CSS URL to load                                                                                          |\n  | `wait_selector` / `wait_delay`                                | waits for a selector to appear / a delay in ms before rendering                                                       |\n  | `enable_js` / `custom_js`                                     | runs sandboxed JavaScript (SPAs, charts) / extra script                                                               |\n  | `data`                                                        | object with variables for `{{campo}}` placeholders in the HTML                                                        |\n  | `retention_days`                                              | `0` (ephemeral, \\~24h), `1`–`30` or `-1` (permanent)                                                                  |\n  | `webhook_url`                                                 | receives the [callback](/en/webhooks#pdf-callback) when finished                                                      |\n  | `template_id`, `template_version`, `font_ids`, `post_process` | [saved templates](#templates), [fonts](#fontes) and [post-processing](#pos-processamento)                             |\n\n  Page size, margins, media, numbering and quality are in [Page and render options](#opcoes-de-pagina).\n\n  Keep the `conversion_id`: it's the `{id}` used to check the status and download the file.\n</Endpoint>"},{"anchor":"batch-with-template","heading":"Batch with template","markdown":"<Endpoint\n  id=\"pdf.batch\"\n  example={{\n  headers: { 'X-Idempotency-Key': '550e8400-e29b-41d4-a716-446655440002' },\n  body: `{\n\"template\": {\n  \"html\": \"<html><body><h1>{{title}}</h1></body></html>\",\n  \"css\": \"h1 { color: #333; }\"\n},\n\"config\": { \"page_size\": \"A4\", \"format\": \"pdf\", \"retention_days\": 7 },\n\"items\": [\n  { \"custom_id\": \"fatura-001\", \"file_name\": \"fatura-001.pdf\", \"data\": { \"title\": \"Fatura 001\" } },\n  { \"custom_id\": \"fatura-002\", \"file_name\": \"fatura-002.pdf\", \"data\": { \"title\": \"Fatura 002\" } }\n],\n\"delivery\": { \"method\": \"zip\", \"webhook_url\": \"https://example.com/hooks/pdf\" }\n}`,\n}}\n>\n  | Field                  | Description                                                                                                                                                                                                                                                    |\n  | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n  | `template.html`        | **required** — HTML with `{{campo}}` placeholders (Go template syntax: `{{if}}` and `{{range}}` also work)                                                                                                                                                     |\n  | `template.css`         | Shared CSS                                                                                                                                                                                                                                                     |\n  | `items[]`              | **required**, 1 to <Limit name=\"pdfBatchMax\" /> items: `custom_id` (**required**, your identifier), `file_name` and `data`                                                                                                                                     |\n  | `config`               | options applied to every item: `page_size`, `orientation`, margins, `format`, `scale`, `retention_days`, `google_fonts_url`, `header_html`, `footer_html`, `wait_selector`, `wait_delay`, `title`, `author`, `enable_js`, `custom_js` and `file_name_template` |\n  | `delivery.method`      | `zip` or `list`: how the result is delivered when finished                                                                                                                                                                                                     |\n  | `delivery.webhook_url` | receives the [callback](/en/webhooks#pdf-callback) when finished                                                                                                                                                                                               |\n\n  The batch does not accept `url`, `template_id`, `font_ids` or `post_process`: it always uses the `template.html` of the body. Volume discounts apply to the base, JavaScript and retention costs. Track progress in [Batch details](#pdf-batch-get).\n</Endpoint>"},{"anchor":"retry-a-failure","heading":"Retry a failure","markdown":"<Endpoint\n  id=\"pdf.retry\"\n  example={{\n  body: `{\n\"html\": \"<html><body><h1>Fatura corrigida</h1></body></html>\",\n\"format\": \"pdf\",\n\"page_size\": \"A4\"\n}`,\n}}\n>\n  Allowed only when `status` is `failed`. The body accepts the same options as [Convert](#pdf-convert), so you can fix the HTML or the options before trying again. The `webhook_url` in the body is ignored: the conversion keeps the one originally sent. The full cost is charged again.\n</Endpoint>"},{"anchor":"opcoes-de-pagina","heading":"Page and render options","markdown":"They apply to `POST /convert` and `POST /retry`. Values outside the list return `pdf.invalid_option` (or `pdf.invalid_page_size` for the paper size).\n\n| Field                                                        | Values                                                                                                                                                      |\n| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `page_size`                                                  | `A3`, `A4` (default), `A5`, `Letter`, `Legal`, `Tabloid` or `Custom`                                                                                        |\n| `paper_width`, `paper_height`                                | custom paper, in **inches** (1 to 200). They only combine with an empty `page_size` or `Custom`; used with another size they return `pdf.invalid_page_size` |\n| `orientation`                                                | `portrait` (default) or `landscape`                                                                                                                         |\n| `margin_top`, `margin_bottom`, `margin_left`, `margin_right` | page margins                                                                                                                                                |\n| `scale`                                                      | render scale                                                                                                                                                |\n| `print_background`                                           | prints background colors and images (default `true`)                                                                                                        |\n| `media_type`                                                 | `print` or `screen`: which `@media` the CSS sees                                                                                                            |\n| `color_scheme`                                               | `light` or `dark`: value of `prefers-color-scheme`                                                                                                          |\n| `page_numbers`                                               | turns on the page counter at the position: `header-left`, `header-center`, `header-right`, `footer-left`, `footer-center` or `footer-right`                 |\n| `page_number_format`                                         | counter text, with `{page}` and `{total}` (default `{page} / {total}`, up to 200 characters). Requires `page_numbers`                                       |\n| `jpeg_quality`                                               | 1 to 100 (default 90). Only applies with `format: jpg`                                                                                                      |\n\nExample: a report on 8 x 11 inch paper with a counter in the footer:\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n  \"url\": \"https://example.com/relatorio\",\n  \"page_size\": \"Custom\",\n  \"paper_width\": 8,\n  \"paper_height\": 11,\n  \"media_type\": \"print\",\n  \"page_numbers\": \"footer-center\",\n  \"page_number_format\": \"Page {page} of {total}\"\n}'\n```"},{"anchor":"pos-processamento","heading":"Post-processing","markdown":"`post_process` acts on the already rendered PDF and **only applies with `format: pdf`** (with `png` or `jpg`, `pdf.invalid_option`). Each feature is charged separately, see [Costs](#custos).\n\n| Field                                   | Description                                                                                                          |\n| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |\n| `post_process.watermark.text`           | watermark text, up to 200 characters                                                                                 |\n| `post_process.watermark.image`          | alternative to the text: PNG or JPEG image in base64, up to 1 MiB. Send **exactly one** of `text` and `image`        |\n| `watermark.font_size`                   | 1 to 300                                                                                                             |\n| `watermark.color`                       | color in `#rrggbb` format                                                                                            |\n| `watermark.opacity`, `watermark.scale`  | 0 to 1                                                                                                               |\n| `watermark.rotation`                    | -180 to 180 degrees                                                                                                  |\n| `watermark.position`                    | `center`, `top-left`, `top-center`, `top-right`, `left`, `right`, `bottom-left`, `bottom-center` or `bottom-right`   |\n| `watermark.on_top`                      | `true` draws the mark over the content                                                                               |\n| `post_process.pdfa`                     | `true` converts to PDF/A-2b (long-term archiving)                                                                    |\n| `post_process.protection.user_password` | **required** when there is a `protection`, up to 128 characters: password to open the file                           |\n| `protection.owner_password`             | owner password, up to 128 characters                                                                                 |\n| `protection.permissions`                | what the reader may do: `print`, `copy`, `modify`, `annotate`, `fill_forms`, `assemble`. Empty list = no permissions |\n\n`pdfa` and `protection` cannot be combined (PDF/A forbids encryption): the API returns `pdf.invalid_option`. Passwords are stored encrypted only so the worker can apply them and never come back in responses.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n  \"html\": \"<html><body><h1>Contrato</h1></body></html>\",\n  \"format\": \"pdf\",\n  \"post_process\": {\n    \"watermark\": { \"text\": \"CONFIDENTIAL\", \"color\": \"#cc0000\", \"opacity\": 0.2, \"rotation\": -45, \"position\": \"center\" },\n    \"protection\": { \"user_password\": \"reader-password\", \"permissions\": [\"print\"] }\n  }\n}'\n```"},{"anchor":"templates","heading":"Saved templates","markdown":"Store the HTML of a recurring document in the account and render it with just `template_id` and `data`. Each content change creates a new **version**; earlier ones remain available.\n\n* In `POST /convert`, `template_id` cannot be combined with `html` or `url` (`pdf.invalid_option`). `template_version` picks the version; `0` or omitted uses the latest.\n* The template's `css`, `header_html` and `footer_html` act as defaults; if you send them in the conversion, yours win.\n* Unknown template or version: `pdf.template_not_found`. Name up to 120 characters; `html` follows the <Limit name=\"pdfHtmlMax\" /> limit and `css` the <Limit name=\"pdfCssMax\" /> one.\n* Reads use the `tools:pdf:read` scope; writes, `tools:pdf:manage` (or `tools:manage`). None of them charges credits or uses the daily free quota.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n  \"template_id\": \"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a\",\n  \"template_version\": 0,\n  \"data\": { \"numero\": \"001\" }\n}'\n```"},{"anchor":"list-saved-templates","heading":"List saved templates","markdown":"<Endpoint id=\"pdf.templates.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  `limit` (default and maximum 100) and `offset`. The response has `items` and `total`.\n</Endpoint>"},{"anchor":"save-template","heading":"Save template","markdown":"<Endpoint\n  id=\"pdf.templates.create\"\n  example={{\n  body: `{\n\"name\": \"Fatura padrão\",\n\"description\": \"Fatura mensal com logotipo\",\n\"html\": \"<html><body><h1>Fatura {{numero}}</h1></body></html>\",\n\"css\": \"h1 { color: #333; }\",\n\"header_html\": \"\",\n\"footer_html\": \"\"\n}`,\n}}\n>\n  `name` (up to 120 characters) and `html` are required; `description`, `css`, `header_html` and `footer_html` are optional. Creates version 1.\n</Endpoint>"},{"anchor":"get-template","heading":"Get template","markdown":"<Endpoint id=\"pdf.templates.get\" example={{ query: { version: '2' } }}>\n  Optional `version` (default: the current one). Returns the `template` and the content of the `version`.\n</Endpoint>"},{"anchor":"list-template-versions","heading":"List template versions","markdown":"<Endpoint id=\"pdf.templates.versions\" />"},{"anchor":"rename-template-or-save-a-new-version","heading":"Rename template or save a new version","markdown":"<Endpoint\n  id=\"pdf.templates.update\"\n  example={{\n  body: `{\n\"name\": \"Fatura padrão v2\",\n\"html\": \"<html><body><h1>Fatura {{numero}}</h1><p>{{cliente}}</p></body></html>\",\n\"css\": \"h1 { color: #333; }\"\n}`,\n}}\n>\n  Send `name` and/or `description` to rename. With `html`, a **new version** is saved with the `html`, `css`, `header_html` and `footer_html` of the body, and `current_version` goes up. Omitted content fields are empty in the new version: resend the full content.\n</Endpoint>"},{"anchor":"delete-template","heading":"Delete template","markdown":"<Endpoint id=\"pdf.templates.delete\">\n  Deletes the template and all its versions. Conversions already generated are not affected.\n</Endpoint>"},{"anchor":"fontes","heading":"Fonts","markdown":"Upload your own fonts (`woff2` or `ttf`) and use them in the HTML with `font-family`: pass the ids in `font_ids` (up to 20 per conversion) and the platform injects the matching `@font-face`. A missing font in `font_ids`: `pdf.font_not_found`. The fonts add-on is charged once per document that uses `font_ids`.\n\n* Up to **50 fonts per account** (`pdf.font_limit`).\n* A `family` + `weight` + `style` combination can only exist once (`pdf.font_exists`, 409).\n* The format is detected from the file content, not from the extension."},{"anchor":"list-fonts","heading":"List fonts","markdown":"<Endpoint id=\"pdf.fonts.list\" />"},{"anchor":"upload-font","heading":"Upload font","markdown":"<Endpoint id=\"pdf.fonts.upload\">\n  Send `multipart/form-data` with `file` (required, `woff2` or `ttf`, up to 5 MiB), `family` (required, 1 to 64 letters, digits, spaces, `-` or `_`), `weight` (`100` to `900` in steps of 100; default `400`) and `style` (`normal` or `italic`; default `normal`). Any violation returns `pdf.font_invalid`.\n\n  ```bash\n  curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/fonts' \\\n    -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n    -F 'file=@Inter-Regular.woff2' \\\n    -F 'family=Inter' \\\n    -F 'weight=400' \\\n    -F 'style=normal'\n  ```\n\n  Then convert with the font:\n\n  ```bash\n  curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n    -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"html\": \"<html><body style=\\\"font-family: Inter\\\"><h1>Hello</h1></body></html>\",\n    \"font_ids\": [\"2b3c4d5e-6f70-4a8b-9c0d-1e2f3a4b5c6d\"]\n  }'\n  ```\n</Endpoint>"},{"anchor":"delete-font","heading":"Delete font","markdown":"<Endpoint id=\"pdf.fonts.delete\">\n  Removes the font and its stored file.\n</Endpoint>"},{"anchor":"lookup-and-download","heading":"Lookup and download","markdown":""},{"anchor":"list-conversions","heading":"List conversions","markdown":"<Endpoint id=\"pdf.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  Query: `limit` (default 20), `offset` and `exclude_batch=true` to hide batch items. In the listing, `file_url` and `error_message` are not populated — check the [status](#pdf-get) of each conversion.\n</Endpoint>"},{"anchor":"conversion-status","heading":"Conversion status","markdown":"<Endpoint id=\"pdf.get\">\n  `status`: `pending`, `processing`, `completed` or `failed`. On failure, `error_code` and `error_message` explain why. `data` contains the variables you sent, and `file_url` appears when the job finishes — to download, always use [Download file](#pdf-download).\n</Endpoint>"},{"anchor":"download-file","heading":"Download file","markdown":"<Endpoint id=\"pdf.download\" example={{ response: 'binary' }}>\n  Requires `status: completed`. The file is decrypted and decompressed during download. `X-Dry-Run` does not apply to downloads.\n\n  The conversion's `file_url` (or the one in the batch details) is a **signed link** with `?sig=&exp=`: it downloads the file without the `X-API-Token` header and without using the daily free quota, so you can hand the link to an end customer. It is valid until the `exp` date; after that, fetch the conversion again to get another one.\n</Endpoint>"},{"anchor":"batches","heading":"Batches","markdown":""},{"anchor":"list-batches","heading":"List batches","markdown":"<Endpoint id=\"pdf.batches\" example={{ query: { limit: '20', offset: '0' } }}>\n  Batch `status`: `processing`, `delivering`, `completed`, `partially_failed` or `failed`.\n</Endpoint>"},{"anchor":"batch-details","heading":"Batch details","markdown":"<Endpoint id=\"pdf.batch-get\" />"},{"anchor":"download-batch-zip","heading":"Download batch ZIP","markdown":"<Endpoint id=\"pdf.batch-download\" example={{ response: 'binary' }}>\n  Available once the batch finishes, including partially if some items failed. It also accepts the signed `?sig=&exp=` link, no token needed.\n</Endpoint>"},{"anchor":"errors","heading":"Errors","markdown":"<ErrorTable tool=\"pdf-convert\" />"},{"anchor":"quirks","heading":"Quirks","markdown":"* A batch uses a single HTML template; each item in `items[].data` fills in the placeholders. `custom_id` helps you match generated files to your own records.\n* For recurring PDFs, save a [template](#templates) instead of resending the HTML on every call.\n* `retention_days: 0` is ephemeral (\\~24h) — ideal for previews.\n* Prefer `webhook_url` over polling the status in a loop: every API lookup counts toward the daily free quota."}]},{"slug":"ferramentas/qr-dinamico","url":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico","title":"Dynamic QR","description":"Dynamic QR code API — creation, management, analytics, costs and public redirect.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"qr-dinamico\" />"},{"anchor":"overview","heading":"Overview","markdown":"A dynamic QR code separates the **printed image** (a stable short link) from the **destination** (editable without reprinting). Typical flow:\n\n1. `POST` with `X-Dry-Run: true` to validate the payload and see the cost.\n2. Final `POST` with `X-Idempotency-Key` → you get the QR image, the `resource_id` and the `slug`.\n3. Use the returned image (or generate your own pointing to `full_url`, available from `GET /{id}`).\n4. Track scans through analytics or the `qr.dynamic.scanned` webhook. The webhook is best-effort (during traffic spikes, some events may not be sent); use analytics for exact counts.\n\n<Callout type=\"warn\" title=\"Public redirect charges per scan\">\n  `GET /r/{short_id}` requires no authentication, but each scan may debit credits from the account that owns the QR code. Without balance, the QR code may be deactivated. Repeat scans of the same QR code by the same visitor within 30 seconds are charged only once: they still redirect, but are not recorded in analytics, do not fire the webhook and do not consume the `scan_limit`.\n</Callout>\n\n<EndpointList tool=\"qr-dinamico\" />"},{"anchor":"custos","heading":"Costs","markdown":"<PricingTable tool=\"qr-dinamico\" group=\"creation\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"domain\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"batch\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"scans\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"storage\" />\n\nAPI reads (listing, details, analytics, scans, slug check) follow the [daily free quota](/en/cobranca-e-limites#franquia-diaria). `PATCH`, activate, deactivate and `DELETE` are free."},{"anchor":"creation-and-management","heading":"Creation and management","markdown":""},{"anchor":"create-a-dynamic-qr-code","heading":"Create a dynamic QR code","markdown":"<Endpoint\n  id=\"qr.create\"\n  example={{\n  headers: { 'X-Idempotency-Key': '550e8400-e29b-41d4-a716-446655440000' },\n  body: `{\n\"name\": \"Campanha verão\",\n\"target_url\": \"https://loja.example.com/promo\",\n\"custom_slug\": \"promo-verao\",\n\"foreground_color\": \"#000000\",\n\"background_color\": \"#ffffff\"\n}`,\n}}\n>\n  Main fields: `name` and `target_url` (`target_url` is optional when there is [`content`](#conteudo-hospedado)). Colors and style go in the body itself (`foreground_color`, `background_color`, `module_style`, `eye_style`, `size`, `logo_data_url`…). Optional: `custom_slug`, `custom_domain_id` ([custom domain](#dominio-proprio)), `expires_at`, `scan_limit`, `skip_image_generation`, `utm_forwarding`, `folder_id` and `tags` ([organization](#organizacao)), `frame` and `dpi` ([printing](#impressao)) and the [scan behavior](#comportamento-do-scan) options — each may add to the cost (see [Costs](#custos)). `response_types` selects the image formats (`svg`, `png`, `pdf` and `eps`; default `svg`): `svg` and `eps` are returned as text, `png` and `pdf` as base64.\n\n  The response includes the images, the `resource_id` (the QR code's id) and the `slug`. With `skip_image_generation: true`, it returns a single item with only `resource_id` and `slug`.\n</Endpoint>"},{"anchor":"list-qr-codes","heading":"List QR codes","markdown":"<Endpoint id=\"qr.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  Query: `limit` (default 20, max. <Limit name=\"pageSizeMax\" />), `offset`, `search`, `folder_id` (a UUID, or `none` for QR codes without a folder), `tags` (comma-separated; the QR code must have all of them) and `status` (`active` or `paused`).\n</Endpoint>"},{"anchor":"details","heading":"Details","markdown":"<Endpoint id=\"qr.get\" />"},{"anchor":"update-destination-name-or-design","heading":"Update destination, name or design","markdown":"<Endpoint\n  id=\"qr.update\"\n  example={{\n  body: `{\n\"name\": \"Campanha atualizada\",\n\"target_url\": \"https://loja.example.com/nova-promo\"\n}`,\n}}\n>\n  Changing the name, destination and colors is free. Turning on a paid option that was off — `utm_forwarding`, `scan_limit` (from 0 to a value), gradient (`foreground_color_end`), custom eyes (`eye_style` or `eye_dot_style` `shield`/`flower`) or a logo — costs the same as at [creation](#custos), once per update. Changing an option that is already on, or turning it off, is free with no refund. Accepts `X-Dry-Run`, `X-Include-Cost` and `X-Idempotency-Key`; without balance the response is `402` and nothing changes, an identical update already charged answers `409`, and an invalid color is rejected (`400`) before charging. The new `target_url` goes through the anti-malware and anti-loop checks. As with activate, deactivate and delete, the response is only a confirmation — call `GET /{id}` to see the updated resource.\n</Endpoint>"},{"anchor":"reactivate","heading":"Reactivate","markdown":"<Endpoint id=\"qr.activate\" />"},{"anchor":"deactivate","heading":"Deactivate","markdown":"<Endpoint id=\"qr.deactivate\">\n  A deactivated QR code shows an inactive QR page on the public redirect and keeps occupying a slot until it's deleted.\n</Endpoint>"},{"anchor":"delete","heading":"Delete","markdown":"<Endpoint id=\"qr.delete\">\n  Ends the slot's storage charges.\n</Endpoint>"},{"anchor":"check-slug-availability","heading":"Check slug availability","markdown":"<Endpoint id=\"qr.check-slug\" example={{ pathParams: { slug: 'promo-verao' } }} />"},{"anchor":"analytics","heading":"Analytics","markdown":""},{"anchor":"analytics-overview","heading":"Analytics overview","markdown":"<Endpoint id=\"qr.analytics\" example={{ query: { period: 'last_30d' } }}>\n  Cached aggregations. `period` query: `last_7d`, `last_30d` (default), `last_90d` or `all`; unknown values count as 30 days. `scan_history` contains the 20 most recent scans.\n</Endpoint>"},{"anchor":"scans-by-period","heading":"Scans by period","markdown":"<Endpoint id=\"qr.analytics-period\" example={{ query: { period: 'last_30d' } }}>\n  Returns the totals and `period_stats` (scans per day). The `period`, `period_scans`, `period_cost` and `unique_scans` fields are zeroed on this route — use the [overview](#qr-analytics) if you need them.\n</Endpoint>"},{"anchor":"device-breakdown","heading":"Device breakdown","markdown":"<Endpoint id=\"qr.analytics-device\" example={{ query: { period: 'last_30d' } }}>\n  `device` is `Mobile`, `Desktop` or `Tablet`. As with scans by period, the period fields are zeroed.\n</Endpoint>"},{"anchor":"scan-history","heading":"Scan history","markdown":"<Endpoint id=\"qr.scans\" example={{ query: { limit: '50', offset: '0', period: 'last_30d' } }}>\n  Pagination: `limit` (default 50, max. <Limit name=\"pageSizeMax\" />), `offset` and `period` (default: last 30 days). `location` is `Unknown` when both city and country are unknown.\n</Endpoint>"},{"anchor":"public-redirect","heading":"Public redirect","markdown":"<Endpoint id=\"qr.redirect\" example={false}>\n  * **Active:** `307` to the `target_url` (with the query string forwarded, if UTM forwarding is enabled). On accounts without a premium plan, the scan first goes through a branded interstitial page that redirects after 3 seconds.\n  * **Nonexistent, inactive, expired, out of balance, over the scan limit or with an unsafe destination:** redirects to the unavailable QR page, with the reason in `?reason=` (`unknown`, `inactive`, `expired`, `bankrupt`, `limit_reached`, `safety_risk`, `rate_limited`).\n  * **Too many requests from one visitor:** above 600 per minute (across all QR codes), redirects to the unavailable QR page with `?reason=rate_limited`. Above 30 scans per minute on the same QR code, the redirect still works, but the extra scans are neither counted nor charged.\n  * Responses are never cached (`Cache-Control: no-store`).\n\n  ```bash\n  curl -sS -I 'https://api.respawntech.dev/r/promo-verao'\n  ```\n</Endpoint>"},{"anchor":"dominio-proprio","heading":"Custom domain","markdown":"Instead of `api.respawntech.dev/r/promo-verao`, the QR code can live at `https://qr.suaempresa.com/promo-verao`. You point a **subdomain of your own** to Respawn Tech with a CNAME record; the HTTPS certificate is issued automatically (Cloudflare for SaaS). The `hostname` must be a subdomain with at least three labels (`qr.suaempresa.com`): a root (apex) domain does not accept CNAME, and port, path, IP and wildcard are rejected.\n\nThe scope is `tools:qr:domains:manage` (or `tools:qr:dynamic:manage` / `tools:manage`). The domain limit depends on the plan (`qr_domain.plan_limit`) and an active domain is charged every 30 days (see [Costs](#custos)).\n\n<Steps>\n  <Step>\n    ### Register the domain\n\n    ```bash\n    curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains' \\\n      -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n      -H 'Content-Type: application/json' \\\n      -d '{\n        \"hostname\": \"qr.suaempresa.com\",\n        \"validation_method\": \"http\",\n        \"root_url\": \"https://www.suaempresa.com\"\n      }'\n    ```\n\n    `validation_method` is `http` (default) or `txt`: it is how the certificate is validated. The response (`201`) returns `status: \"pending\"` and the `records` list with everything you need to publish. Keep the `id`.\n  </Step>\n\n  <Step>\n    ### Publish the CNAME\n\n    In your domain's DNS panel, create the record for the item with `purpose: \"routing\"`. The target (`value`) comes in the response; do not copy it from examples.\n\n    | Type    | Name                          | Value                               |\n    | ------- | ----------------------------- | ----------------------------------- |\n    | `CNAME` | `qr` (or `qr.suaempresa.com`) | the `value` of the `routing` record |\n\n    On **Cloudflare**, create the CNAME in the `suaempresa.com` zone and leave the proxy **off** (gray cloud, \"DNS only\") until the domain is `active`; only then, if you want, turn the proxy on. On other providers, it is a regular CNAME for the subdomain.\n  </Step>\n\n  <Step>\n    ### Validate the certificate\n\n    If you chose `http`, the record with `purpose: \"certificate\"` and `type: \"HTTP\"` is validated on its own as soon as the CNAME propagates, with nothing to publish. If you chose `txt`, also publish the `TXT` records that come in `records` (`ownership` and `certificate`), in the same DNS panel.\n  </Step>\n\n  <Step>\n    ### Verify until it is active\n\n    The platform checks the domain periodically; to avoid waiting, force the verification:\n\n    ```bash\n    curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains/$DOMAIN_ID/refresh' \\\n      -H \"X-API-Token: $RESPAWN_API_TOKEN\"\n    ```\n\n    The `status` goes from `pending` to `active` (the period is charged at that moment). `failed` means the provider rejected the hostname (see `last_error`); `suspended`, that the period could not be charged — scans go to the fallback page until the next successful charge.\n  </Step>\n\n  <Step>\n    ### Create QR codes on the domain\n\n    Send the domain's `id` in `custom_domain_id`. A domain that is still `pending` is already accepted, so you can print while DNS propagates, but scans only redirect once it becomes `active`.\n\n    ```bash\n    curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n      -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n      -H 'Content-Type: application/json' \\\n      -d '{\n        \"name\": \"Cartaz da loja\",\n        \"target_url\": \"https://loja.example.com/promo\",\n        \"custom_slug\": \"promo-verao\",\n        \"custom_domain_id\": \"'\"$DOMAIN_ID\"'\"\n      }'\n    ```\n\n    The QR code's `full_url` becomes `https://qr.suaempresa.com/promo-verao`. The slug only needs to be unique within the domain.\n  </Step>\n</Steps>\n\n`root_url` is where the domain root (`https://qr.suaempresa.com/`) goes and `not_found_url` is where unknown slugs go; without them, the platform's fallback page applies. Neither can point back to the domain itself. Only `root_url` and `not_found_url` can be changed after registration (`PATCH`); to change the hostname, remove it and register again.\n\n<Callout type=\"info\" title=\"Removing a domain\">\n  A domain that still has QR codes linked to it cannot be removed (`qr_domain.in_use`). Delete the QR codes first; once the domain is removed, their `full_url` stops working.\n</Callout>"},{"anchor":"register-the-domain","heading":"List custom domains","markdown":"<Endpoint id=\"qr.domains-list\" />"},{"anchor":"publish-the-cname","heading":"Domain details","markdown":"<Endpoint id=\"qr.domains-get\" />"},{"anchor":"validate-the-certificate","heading":"Register a custom domain","markdown":"<Endpoint\n  id=\"qr.domains-create\"\n  example={{\n  body: `{\n\"hostname\": \"qr.suaempresa.com\",\n\"validation_method\": \"http\",\n\"root_url\": \"https://www.suaempresa.com\"\n}`,\n}}\n/>"},{"anchor":"verify-until-it-is-active","heading":"Edit the domain's fallback pages","markdown":"<Endpoint\n  id=\"qr.domains-update\"\n  example={{\n  body: `{\n\"root_url\": \"https://www.suaempresa.com\",\n\"not_found_url\": \"https://www.suaempresa.com/ops\"\n}`,\n}}\n>\n  Send `\"\"` to clear one of the fields.\n</Endpoint>"},{"anchor":"create-qr-codes-on-the-domain","heading":"Verify the domain now","markdown":"<Endpoint id=\"qr.domains-refresh\" />"},{"anchor":"list-custom-domains","heading":"Remove a domain","markdown":"<Endpoint id=\"qr.domains-delete\" />\n\n## Scan behavior\n\nBy default a scan is a redirect to the `target_url`. These fields, accepted on `POST` and `PATCH`, change what happens on a scan. Evaluation is in this order: **password** (the scan only continues after it), **rules** (the first one that matches wins), **A/B test** (for scans that no rule caught) and, finally, the `target_url`. Each option is charged once (see [Costs](#custos)).\n\nOn a `PATCH`, each field you send **replaces** its part: `[]` clears the rules or the variants, `{}` clears the pixels and `\"\"` clears a URL or the password."},{"anchor":"domain-details","heading":"Redirect rules","markdown":"`redirect_rules` (up to 20) sends the scan to its own URL when all of the rule's conditions match. A rule with no conditions is rejected.\n\n| Field                    | Meaning                                                                                            |\n| ------------------------ | -------------------------------------------------------------------------------------------------- |\n| `id`                     | Rule name (`[a-zA-Z0-9_-]`, up to 40); appears in scans and in the analytics `rule_stats`          |\n| `target_url`             | Destination when the rule matches (absolute URL)                                                   |\n| `countries`              | ISO 3166-1 alpha-2 countries in uppercase (`[\"BR\", \"PT\"]`)                                         |\n| `devices`                | `mobile`, `tablet`, `desktop`                                                                      |\n| `os`                     | `ios`, `android`, `windows`, `macos`, `linux`, `other`                                             |\n| `languages`              | Visitor's preferred language: `pt` matches `pt` and `pt-br`; `pt-br` matches only `pt-br`          |\n| `days`                   | Days of the week, `0` (Sunday) to `6` (Saturday), in the `timezone`                                |\n| `start_time`, `end_time` | Time-of-day window (`HH:MM`); if the end is before the start, it crosses midnight. Both or neither |\n| `timezone`               | IANA time zone (`America/Sao_Paulo`); UTC if empty                                                 |\n| `from`, `until`          | Period in which the rule applies (RFC 3339 date and time)                                          |\n\nEach list accepts up to 50 values. Within a rule the conditions add up (AND); within a list, one value is enough (OR)."},{"anchor":"register-a-custom-domain","heading":"A/B test","markdown":"`ab_variants` (2 to 10) splits, by weight, the scans that no rule caught. Each variant has a unique `id`, a `target_url` and a `weight` from 1 to 1000; a variant's share is its weight divided by the sum. Analytics returns `variant_stats` with each variant's scans."},{"anchor":"edit-the-domains-fallback-pages","heading":"Password","markdown":"`password` (4 to 72 characters) hides the QR code behind a password page. The password is stored hashed; responses only include `scan_behavior.has_password`. After entering the right password, the visitor follows the normal flow and this page is not cached. After 5 wrong passwords, the visitor is blocked on that QR code for 15 minutes."},{"anchor":"verify-the-domain-now","heading":"Pixels","markdown":"`pixels` fires the tags on a lightweight page before the redirect: `ga4_measurement_id` (`G-XXXXXXX`), `meta_pixel_id` (digits only) and `gtm_container_id` (`GTM-XXXXXX`)."},{"anchor":"remove-a-domain","heading":"Redirect for expired and exhausted","markdown":"`expired_url` and `limit_reached_url` (absolute URLs) replace the unavailable QR page when the QR code has expired (`expires_at`) or used up its entire `scan_limit`.\n\n### Full example\n\nCopy it, swap the URLs and run:\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Campanha com regras\",\n    \"target_url\": \"https://loja.example.com/global\",\n    \"redirect_rules\": [\n      {\n        \"id\": \"br-mobile\",\n        \"target_url\": \"https://loja.example.com/br\",\n        \"countries\": [\"BR\"],\n        \"devices\": [\"mobile\"]\n      },\n      {\n        \"id\": \"horario-comercial\",\n        \"target_url\": \"https://loja.example.com/atendimento\",\n        \"days\": [1, 2, 3, 4, 5],\n        \"start_time\": \"09:00\",\n        \"end_time\": \"18:00\",\n        \"timezone\": \"America/Sao_Paulo\"\n      }\n    ],\n    \"ab_variants\": [\n      { \"id\": \"a\", \"target_url\": \"https://loja.example.com/lp-a\", \"weight\": 50 },\n      { \"id\": \"b\", \"target_url\": \"https://loja.example.com/lp-b\", \"weight\": 50 }\n    ],\n    \"pixels\": { \"ga4_measurement_id\": \"G-ABCD1234\" },\n    \"expired_url\": \"https://loja.example.com/encerrada\",\n    \"limit_reached_url\": \"https://loja.example.com/esgotado\"\n  }'\n```\n\nTo remove everything later, without recreating the QR code (replace `$QR_ID` with the `resource_id` from creation):\n\n```bash\ncurl -sS -X PATCH \"https://api.respawntech.dev/v1/tools/image/qr-code/dynamic/$QR_ID\" \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{ \"redirect_rules\": [], \"ab_variants\": [], \"pixels\": {}, \"password\": \"\" }'\n```\n\n`GET /{id}` returns the configuration in `scan_behavior` (`redirect_rules`, `ab_variants`, `has_password`, `pixels`, `expired_url`, `limit_reached_url`, `content`)."},{"anchor":"comportamento-do-scan","heading":"Hosted content","markdown":"With `content`, `/r/{short_id}` itself serves a lightweight page (or a computed redirect) instead of the `target_url`. The `type` selects the format and only the field of the same name is used. If a redirect rule or an A/B test matches, it takes precedence over the content. Pages do not run scripts and are cached for 5 minutes (revalidated by ETag). `{}` clears the content on a `PATCH`.\n\n| `type`      | Field                                                                                      | The scan shows                                                                                                     |\n| ----------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |\n| `vcard`     | `vcard` (`FirstName`, `LastName`, `Org`, `Title`, `Phone`, `Email`, `Website`, `Address`)  | Contact page and `.vcf` download (`?format=vcf`). Requires a name or organization                                  |\n| `file`      | `file` (`url`, `name`)                                                                     | Opens the hosted file or PDF (absolute URL)                                                                        |\n| `links`     | `links` (`title`, `description`, `avatar_url`, `links[]` with `label` and `url`)           | Link in bio, 1 to 50 links                                                                                         |\n| `menu`      | `menu` (`title`, `currency`, `sections[]` → `items[]` with `name`, `description`, `price`) | Menu, up to 30 sections and 300 items                                                                              |\n| `wifi`      | `wifi` (`SSID`, `Password`, `Security`, `Hidden`)                                          | Network name and password to copy                                                                                  |\n| `app_store` | `app_store` (`ios_url`, `android_url`, `fallback_url`)                                     | Redirect to the App Store or Google Play depending on the phone; `fallback_url` serves desktop and unknown systems |\n\nThe `vcard` and `wifi` fields use these PascalCase names, exactly as in the table; the others use `snake_case`. The `target_url` is no longer required. It takes the file or store link and, for the page types, falls back to the platform's fallback page.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Link na bio\",\n    \"content\": {\n      \"type\": \"links\",\n      \"links\": {\n        \"title\": \"Loja Exemplo\",\n        \"links\": [\n          { \"label\": \"Site\", \"url\": \"https://loja.example.com\" },\n          { \"label\": \"Instagram\", \"url\": \"https://instagram.com/lojaexemplo\" }\n        ]\n      }\n    }\n  }'\n```\n\nContact card example:\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Cartão da Ana\",\n    \"content\": {\n      \"type\": \"vcard\",\n      \"vcard\": { \"FirstName\": \"Ana\", \"LastName\": \"Souza\", \"Org\": \"Loja Exemplo\", \"Phone\": \"+5511999990000\", \"Email\": \"ana@loja.example.com\" }\n    }\n  }'\n```\n\n## Frames and printing\n\n`frame` draws a call to action around the code and `dpi` makes the image print-ready. Both work with any `response_types`.\n\n* `frame`: `text` (up to 32 characters; shrinks to fit), `color`, `text_color` (hex) and `position` (`top` or `bottom`).\n* `dpi`: from 72 to 1200. In the **PDF** the page is exactly the size of the QR code, in vector; in the **SVG** the width and height are in mm; the **PNG** carries the print resolution in the file. Without `dpi`, the PDF keeps the A4 layout.\n* `eps`: new vector format, with the frame and Helvetica-Bold font with accents. It comes out without a logo.\n\nIn PDF and EPS, the eyes come out square and only the `dots` and square module styles become vector; the gradient is not applied.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Cartaz A5\",\n    \"target_url\": \"https://loja.example.com/promo\",\n    \"response_types\": [\"pdf\", \"eps\"],\n    \"frame\": { \"text\": \"Escaneie aqui\", \"color\": \"#111111\", \"text_color\": \"#FFFFFF\", \"position\": \"bottom\" },\n    \"dpi\": 300\n  }'\n```\n\n## Organization, bulk editing and batches"},{"anchor":"regras","heading":"Folders and tags","markdown":"`folder_id` stores the QR code in a folder and `tags` (up to 20 per QR code, 40 characters each, no commas) labels it; both go in `POST` and `PATCH` (`folder_id: \"\"` goes back to the root, `tags: []` clears). The listing filters by `folder_id`, `tags`, `status` and `search`."},{"anchor":"ab","heading":"List folders","markdown":"<Endpoint id=\"qr.folders-list\" />"},{"anchor":"senha","heading":"Create a folder","markdown":"<Endpoint id=\"qr.folders-create\" example={{ body: '{ \"name\": \"Campanha de verão\" }' }}>\n  The name is 1 to 80 characters and unique per account (`dynamic_qr.folder_taken`).\n</Endpoint>"},{"anchor":"pixels","heading":"Rename a folder","markdown":"<Endpoint id=\"qr.folders-rename\" example={{ body: '{ \"name\": \"Verão 2027\" }' }} />"},{"anchor":"fallback","heading":"Delete a folder","markdown":"<Endpoint id=\"qr.folders-delete\" />"},{"anchor":"full-example","heading":"Edit several QR codes at once","markdown":"<Endpoint\n  id=\"qr.bulk\"\n  example={{\n  body: `{\n\"ids\": [\"8f3a2b1c-4d5e-6f70-8192-a3b4c5d6e7f8\"],\n\"action\": \"add_tags\",\n\"tags\": [\"verao\"]\n}`,\n}}\n>\n  One action on up to 500 QR codes, free of charge. `action` is `activate`, `deactivate`, `move` (with `folder_id`; `null` sends to the root), `set_target` (with `target_url`), `add_tags` or `remove_tags` (with `tags`). The response includes each QR code's result in `items`, so one failure does not bring down the others. Accepts `X-Idempotency-Key`.\n</Endpoint>\n\n### Create QR codes in batches\n\n<Endpoint\n  id=\"qr.batches-create\"\n  example={{\n  headers: { 'X-Idempotency-Key': '7f1c2e84-5b3a-4d6c-9e01-a2b3c4d5e6f7' },\n  body: `{\n\"csv\": \"name,target_url,custom_slug,tags\\\\nLoja Centro,https://loja.example.com/centro,loja-centro,varejo|sp\\\\nLoja Norte,https://loja.example.com/norte,loja-norte,varejo\",\n\"defaults\": { \"utm_forwarding\": true }\n}`,\n}}\n>\n  Up to 1,000 rows per batch, in one of these ways:\n\n  * JSON with `rows` (a list of objects with the QR code fields) or `csv` (the CSV text), plus `defaults` with the values used on rows that don't include them (`folder_id`, `tags`, `scan_limit`, `expires_at`, `utm_forwarding`, colors, `custom_domain_id`);\n  * a `text/csv` body directly, with `folder_id` and `tags` as query parameters.\n\n  The CSV has a header row and the columns `name`, `target_url` (required), `custom_slug`, `folder_id`, `tags` (separated by `|`), `scan_limit`, `expires_at`, `utm_forwarding`, `foreground_color` and `background_color`:\n\n  ```csv\n  name,target_url,custom_slug,tags\n  Loja Centro,https://loja.example.com/centro,loja-centro,varejo|sp\n  Loja Norte,https://loja.example.com/norte,loja-norte,varejo\n  ```\n\n  ```bash\n  curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/batches?tags=campanha' \\\n    -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n    -H 'X-Idempotency-Key: 7f1c2e84-5b3a-4d6c-9e01-a2b3c4d5e6f7' \\\n    -H 'Content-Type: text/csv' \\\n    --data-binary @lojas.csv\n  ```\n\n  Invalid rows are reported and **not** charged; valid ones are charged at once, with a volume discount (from 100, 500 and 1,000 rows), and created in the background (`202`). A row that fails later is refunded. Track progress by querying the batch details until `pending` reaches 0. With the same `X-Idempotency-Key` and the same rows, the retry returns the same batch, charged once. Above the limit, the response is `413` (`dynamic_qr.batch_too_large`).\n</Endpoint>\n\n### List batches\n\n<Endpoint id=\"qr.batches-list\" />\n\n### Batch details\n\n<Endpoint id=\"qr.batches-get\">\n  `items` returns each row (`row`, `status`, `qr_id`, `slug`, `full_url` or `error_code` and `error_message`). `charged_units`, `discount_units` and `refunded_units` are in internal credit units (10,000 units = 1 credit).\n</Endpoint>\n\n### Export scans as CSV\n\n<Endpoint id=\"qr.scans-export\" example={{ query: { period: 'last_30d' } }}>\n  Query: `period` (`last_7d`, `last_30d` default, `last_90d` or `all`) or `from` and `to` (`YYYY-MM-DD`, UTC, inclusive), which replace the period. Charged like the [scan history](#qr-scans). Save with `curl -o scans.csv`.\n</Endpoint>\n\n### Purge scan history\n\n<Endpoint id=\"qr.scans-purge\" example={{ query: { before: '2026-01-01' } }}>\n  Deletes the scan rows (all of them, or those before `before`, `YYYY-MM-DD` UTC). `total_scans` and `scan_limit` are kept. It is irreversible.\n</Endpoint>"},{"anchor":"conteudo-hospedado","heading":"Errors","markdown":"<ErrorTable tool=\"qr-dinamico\" />"},{"anchor":"impressao","heading":"Quirks and limits","markdown":"* Malicious URLs are blocked automatically (`dynamic_qr.malicious_url`).\n* Anti-loop protection: the destination cannot point to the redirect domain itself.\n* The first QR code doesn't occupy a paid slot; the rest occupy slots in groups of 3.\n* `skip_image_generation: true` lowers the cost when you generate the QR image yourself.\n* Slug: 3–50 characters `[a-zA-Z0-9-]`. Listings: up to <Limit name=\"pageSizeMax\" /> items per page."}]},{"slug":"ferramentas/realtime","url":"https://docs.respawntech.dev/en/ferramentas/realtime","title":"Realtime","description":"Publish and receive real-time messages over WebSocket or SSE, with tokens issued on your server, presence, per-channel history, a channel webhook and reconnection without losing messages.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"realtime\" />"},{"anchor":"como-funciona","heading":"How it works","markdown":"Realtime carries messages from your server, or from other clients, to connected browsers, without you running WebSocket servers.\n\n* **Channel:** a free-form name of up to <Limit name=\"realtimeChannelNameMax\" /> characters, with letters, digits and `: _ - .` (`chat:lobby`, `order:123`). Anyone who subscribes to the channel receives what is published to it. There is nothing to create beforehand.\n* **Token:** a short-lived credential that **your server** issues with your API key and hands to the client. It says who the client is (`client_id`, optional) and which channels it may subscribe to, publish to and use presence on (`capabilities`).\n* **`connect_url`:** the WebSocket address the client connects to, returned in the same response as the token and already carrying `?token=`.\n* **Message:** JSON `data` published to a channel, with an `id`, a `timestamp` (Unix milliseconds) and the `client_id` of whoever published it.\n* **Presence:** the list of who is connected and \"present\" on a channel, with free-form data for each member (a nickname, a status).\n\nThe flow is always the same:\n\n<Steps>\n  <Step>\n    The browser asks **your server** for a token; your server already knows who the user is.\n  </Step>\n\n  <Step>\n    Your server [issues the token](#realtime-token) through the API with that user's capabilities and returns the whole response to the browser: `token`, `expires_at` and `connect_url`.\n  </Step>\n\n  <Step>\n    The browser connects to `connect_url`, subscribes to channels and publishes.\n  </Step>\n\n  <Step>\n    If the connection drops, the browser asks for a new token and [reconnects](#reconexao), recovering the missed messages with `rewind`.\n  </Step>\n</Steps>\n\n<Callout type=\"warn\" title=\"Always use the connect_url from the response\">\n  Never hardcode the WebSocket address in code or configuration. The `connect_url` can change between environments (test and production) and when the platform redistributes connections; a token is only valid at the address that came with it.\n</Callout>\n\nYour API key (`X-API-Token`) stays on the server only. The browser uses just the connection token, which expires and only sees the channels you allowed.\n\n<EndpointList tool=\"realtime\" />"},{"anchor":"token-e-capabilities","heading":"Token and capabilities","markdown":""},{"anchor":"issue-a-connection-token","heading":"Issue a connection token","markdown":"<Endpoint id=\"realtime.token\" example={{ body: `{ \"client_id\": \"user-42\", \"capabilities\": { \"chat:*\": [\"subscribe\", \"publish\", \"presence\"], \"alerts\": [\"subscribe\"] }, \"ttl_seconds\": 3600 }` }}>\n  * `capabilities` (required): an object mapping a channel pattern to the list of allowed operations, up to <Limit name=\"realtimeTokenPatternsMax\" /> patterns. A pattern is an exact name (`alerts`), a prefix ending in `*` (`chat:*`) or just `*` (every channel). The operations are `subscribe`, `publish`, `presence` (enter presence; reading presence requires `subscribe`) or `*` (all). Without a capability for the channel, the operation fails with `realtime.capability_denied`.\n  * `client_id` (optional, up to <Limit name=\"realtimeClientIdMax\" /> characters): identifies the client in the messages it publishes and in presence. Use your user's id; do not accept this value from the browser.\n  * `ttl_seconds` (optional): token lifetime, from <Limit name=\"realtimeTokenTtlMin\" /> up to <Limit name=\"realtimeTokenTtlMax\" />, default <Limit name=\"realtimeTokenTtlDefault\" />. The token is only checked when connecting: an open connection stays valid after it expires, but the next reconnection requires a new token.\n\n  The response carries `token`, `expires_at` and `connect_url` (which already includes `?token=`). When the platform needs to start a node before accepting connections, the response also carries `retry_after_ms`: the token is valid, but **wait that long before connecting**.\n</Endpoint>\n\nGive each client only what it needs: a chat with `{\"chat:lobby\": [\"subscribe\", \"publish\", \"presence\"]}`, a read-only dashboard with `{\"metrics:*\": [\"subscribe\"]}`. The token is signed with HMAC (HS256) and is only valid for Realtime.\n\n<Callout title=\"Inspector token\">\n  The dashboard uses the `POST /v1/tools/realtime/inspector-token` route to open the live inspector (subscription to every channel, no publishing, 15 minutes). It only accepts the dashboard session; with an API key it answers `realtime.session_required`.\n</Callout>"},{"anchor":"conexao-pelo-navegador","heading":"Connecting from the browser","markdown":"On your server, expose a route that issues the token and passes the response on. The example below uses `RESPAWN_API_TOKEN` and a `/realtime-token` route; replace the `client_id` with the authenticated user.\n\n<Tabs groupId=\"code-language\" persist items={['Node.js', 'Python']}>\n  <Tab value=\"Node.js\">\n    ```js\n    import { createServer } from 'node:http';\n\n    createServer(async (req, res) => {\n      if (req.url !== '/realtime-token') return res.writeHead(404).end();\n\n      const api = await fetch('https://api.respawntech.dev/v1/tools/realtime/tokens', {\n        method: 'POST',\n        headers: { 'X-API-Token': process.env.RESPAWN_API_TOKEN, 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          client_id: 'user-42', // the authenticated user\n          capabilities: { 'chat:*': ['subscribe', 'publish', 'presence'] },\n        }),\n      });\n      const { data } = await api.json();\n\n      // token, expires_at, connect_url and retry_after_ms go together to the browser\n      res.writeHead(api.status, { 'Content-Type': 'application/json' }).end(JSON.stringify(data));\n    }).listen(3000);\n    ```\n  </Tab>\n\n  <Tab value=\"Python\">\n    ```python\n    import os\n    import requests\n    from flask import Flask, jsonify\n\n    app = Flask(__name__)\n\n    @app.post(\"/realtime-token\")\n    def realtime_token():\n        api = requests.post(\n            \"https://api.respawntech.dev/v1/tools/realtime/tokens\",\n            headers={\"X-API-Token\": os.environ[\"RESPAWN_API_TOKEN\"]},\n            json={\n                \"client_id\": \"user-42\",  # the authenticated user\n                \"capabilities\": {\"chat:*\": [\"subscribe\", \"publish\", \"presence\"]},\n            },\n            timeout=30,\n        )\n        # token, expires_at, connect_url and retry_after_ms go together to the browser\n        return jsonify(api.json()[\"data\"]), api.status_code\n    ```\n  </Tab>\n</Tabs>\n\nIn the browser, ask your server for the token and connect to the `connect_url` that came with it. This HTML works pasted into a file and served by the same server as the `/realtime-token` route (in testing, use a key from the test environment); it already reconnects on its own. If you would rather not write the protocol by hand, use the [JavaScript SDK](#sdk).\n\n```html\n<!doctype html>\n<meta charset=\"utf-8\" />\n<title>Realtime</title>\n<ul id=\"log\"></ul>\n<input id=\"text\" placeholder=\"Message\" />\n<button id=\"send\">Send</button>\n\n<script>\n  const CHANNEL = 'chat:lobby';\n  const REWIND = 50; // messages recovered on each (re)connection\n  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n  const seen = new Set(); // ids already shown: rewind can repeat messages\n  let ws = null;\n  let attempt = 0;\n\n  function show(text) {\n    const li = document.createElement('li');\n    li.textContent = text;\n    document.getElementById('log').append(li);\n  }\n\n  async function connect() {\n    // A new token on every connection: covers expiry and close 4001\n    const res = await fetch('/realtime-token', { method: 'POST' });\n    if (!res.ok) return retry();\n    const { connect_url, retry_after_ms } = await res.json();\n    if (retry_after_ms) await sleep(retry_after_ms);\n\n    ws = new WebSocket(connect_url); // connect_url already carries ?token=\n\n    ws.onmessage = (event) => {\n      const frame = JSON.parse(event.data);\n      if (frame.action === 'connected') {\n        attempt = 0;\n        ws.send(JSON.stringify({ action: 'subscribe', channel: CHANNEL, rewind: REWIND }));\n      } else if (frame.action === 'message') {\n        if (seen.has(frame.id)) return;\n        seen.add(frame.id);\n        show(`${frame.client_id ?? '?'}: ${JSON.stringify(frame.data)}`);\n      } else if (frame.action === 'error') {\n        console.warn('realtime error', frame.code, frame.message);\n      }\n    };\n\n    ws.onclose = (event) => {\n      // 4003: Realtime disabled for the account, do not reconnect.\n      if (event.code === 4003) return;\n      // 1012: node restarting, reconnect now. 1013 and 4029: backoff.\n      // 4001: invalid token, the next connect() already issues another one.\n      retry(event.code === 1012 ? 0 : undefined);\n    };\n  }\n\n  async function retry(delay) {\n    attempt += 1;\n    // exponential backoff with jitter, capped at 30 s\n    const wait = delay ?? Math.random() * Math.min(30000, 500 * 2 ** attempt);\n    await sleep(wait);\n    connect();\n  }\n\n  document.getElementById('send').onclick = () => {\n    const input = document.getElementById('text');\n    if (ws?.readyState !== WebSocket.OPEN || !input.value) return;\n    ws.send(JSON.stringify({ action: 'publish', channel: CHANNEL, data: { text: input.value } }));\n    input.value = '';\n  };\n\n  connect();\n</script>\n```"},{"anchor":"protocolo","heading":"Protocol","markdown":"The connection's messages are JSON text frames, each with an `action` field. After connecting, wait for the `connected` frame before subscribing. The channel name follows the rule `^[A-Za-z0-9:_\\-.]{1,128}$`."},{"anchor":"client-to-server","heading":"Client to server","markdown":"| `action`      | Fields                            | What it does                                                                                                                                                                                                                     |\n| ------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `subscribe`   | `channel`, `rewind?`              | Subscribes to the channel (requires `subscribe` on the token). With `rewind` (a number of messages, up to <Limit name=\"realtimeRewindMax\" />), it resends the channel's latest messages before the new ones. Answers `attached`. |\n| `unsubscribe` | `channel`                         | Stops receiving the channel. Answers `detached`.                                                                                                                                                                                 |\n| `publish`     | `channel`, `name?`, `data`, `id?` | Publishes `data` (JSON) to the channel (requires `publish`). `name` classifies the message. If you send an `id`, the server answers `ack` with it.                                                                               |\n| `presence`    | `op`, `channel`, `data?`, `id?`   | [Presence](#presenca) operations: `op` is `enter`, `update`, `leave` or `get`.                                                                                                                                                   |"},{"anchor":"server-to-client","heading":"Server to client","markdown":"| `action`    | Fields                                                      | When                                                                                                                 |\n| ----------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |\n| `connected` | `connection_id`                                             | The connection was accepted; you can subscribe now.                                                                  |\n| `attached`  | `channel`                                                   | The channel subscription is active (after the `rewind`, if requested).                                               |\n| `detached`  | `channel`                                                   | The subscription ended.                                                                                              |\n| `message`   | `channel`, `id`, `name?`, `data`, `timestamp`, `client_id?` | A message on the channel, including the ones you published yourself. `timestamp` is an integer in Unix milliseconds. |\n| `ack`       | `id`, `channel?`                                            | The publish (or presence operation) with that `id` was accepted.                                                     |\n| `presence`  | `op`, `channel`, `member` or `members`                      | A presence event (`enter`, `update`, `leave`) or the answer to `get` (`sync`).                                       |\n| `error`     | `code`, `message`, `channel?`, `id?`                        | An operation failed. The `code` values are in [Errors](#erros). The connection stays open.                           |\n\nExample exchange:\n\n```json\n← { \"action\": \"connected\", \"connection_id\": \"c_8f3a1d\" }\n→ { \"action\": \"subscribe\", \"channel\": \"chat:lobby\", \"rewind\": 20 }\n← { \"action\": \"attached\", \"channel\": \"chat:lobby\" }\n→ { \"action\": \"publish\", \"channel\": \"chat:lobby\", \"id\": \"m-1\", \"data\": { \"text\": \"Hello!\" } }\n← { \"action\": \"message\", \"channel\": \"chat:lobby\", \"id\": \"1790338000000-0\", \"data\": { \"text\": \"Hello!\" }, \"timestamp\": 1790338000000, \"client_id\": \"user-42\" }\n← { \"action\": \"ack\", \"id\": \"m-1\" }\n```\n\nThe `message` `id` is generated by the server and is what you use to drop repeats. The `publish` `id` is only yours, to match the `ack`."},{"anchor":"close-codes","heading":"Close codes","markdown":"| Code      | Meaning                                                                 | What to do                                                               |\n| --------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| `1012`    | The node is restarting (deploy or maintenance).                         | Reconnect now, with a small jitter.                                      |\n| `1013`    | The node is full.                                                       | Reconnect with exponential backoff.                                      |\n| `4001`    | Invalid or expired token (`realtime.token_invalid`).                    | Issue a new token and reconnect; repeating the same token will not help. |\n| `4003`    | Realtime disabled for the account (`realtime.disabled`).                | Do not reconnect.                                                        |\n| `4029`    | The account reached its connection limit (`realtime.connection_limit`). | Reconnect with exponential backoff.                                      |\n| Any other | Network drop or unexpected error.                                       | Reconnect with exponential backoff.                                      |"},{"anchor":"sse","heading":"SSE: reading without WebSocket","markdown":"For clients that cannot open a WebSocket, the same token opens a **read-only** `text/event-stream` on the same server as the `connect_url`:\n\n```text\nGET https://<connect_url host>/v1/sse?token=<token>&channel=chat:lobby&channel=alerts&rewind=20\n```\n\n* Pass each channel in its own `channel=` (or several in `channels=a,b`), up to <Limit name=\"realtimeChannelsPerConnection\" />. The token needs `subscribe` on all of them.\n* Each `data:` is a JSON frame identical to the WebSocket's (`connected`, `attached`, `message`, `presence`, `error`); `: ping` lines keep the connection alive.\n* The stream counts toward the account's connection limit and ends when the token expires (an `error` frame with `realtime.token_invalid` comes first) or when the node restarts.\n* To publish, use the [API](#publicar) or a WebSocket.\n\n```js\nconst url = new URL(connectUrl); // the connect_url from the token response\nconst sse = new EventSource(\n  `https://${url.host}/v1/sse?token=${encodeURIComponent(token)}&channel=chat:lobby&rewind=20`,\n);\nsse.onmessage = (event) => {\n  const frame = JSON.parse(event.data);\n  if (frame.action === 'message') console.log(frame.data);\n};\n```"},{"anchor":"presenca","heading":"Presence","markdown":"Presence tells you who is on a channel right now. A client with the `presence` capability **enters** with free-form data (`{\"nick\": \"ana\"}`), can **update** that data and **leaves** when it leaves the channel or when the connection drops. Channel subscribers receive the events, and `get` (or the API) returns the full list.\n\n| Client frame                                                                     | Requires    | Effect                                           |\n| -------------------------------------------------------------------------------- | ----------- | ------------------------------------------------ |\n| `{\"action\":\"presence\",\"op\":\"enter\",\"channel\":\"chat:lobby\",\"data\":{…},\"id\":\"p1\"}` | `presence`  | Enters presence; answers `ack`.                  |\n| `{\"action\":\"presence\",\"op\":\"update\",\"channel\":\"chat:lobby\",\"data\":{…}}`          | `presence`  | Updates the member's data.                       |\n| `{\"action\":\"presence\",\"op\":\"leave\",\"channel\":\"chat:lobby\"}`                      | `presence`  | Leaves presence.                                 |\n| `{\"action\":\"presence\",\"op\":\"get\",\"channel\":\"chat:lobby\",\"id\":\"p2\"}`              | `subscribe` | Answers `op: \"sync\"` with every `members` entry. |\n\nSubscribers receive `{\"action\":\"presence\",\"op\":\"enter|update|leave\",\"channel\":\"chat:lobby\",\"member\":{\"connection_id\":\"…\",\"client_id\":\"user-42\",\"data\":{…},\"timestamp\":1790338000000}}`. Each connection is one member (the same user in two tabs counts twice). A channel holds up to <Limit name=\"realtimePresenceMembers\" /> members; beyond that, `realtime.presence_limit`. If a node dies without warning, its members disappear from the list within 30 seconds."},{"anchor":"list-channel-presence","heading":"List channel presence","markdown":"<Endpoint id=\"realtime.presence\" example={{ pathParams: { channel: 'chat:lobby' } }}>\n  Returns the channel's members at this moment. It is useful for showing \"who is online\" from your server, without opening a connection.\n</Endpoint>"},{"anchor":"reconexao","heading":"Reconnection","markdown":"Connections drop: the node restarts, the network flickers, the token expires. Every application needs to reconnect on its own. The example in [Connecting from the browser](#conexao-pelo-navegador) already does it (so does the [SDK](#sdk)); these are the rules, and the [close codes](#protocolo) tell you when to reconnect."},{"anchor":"backoff","heading":"Backoff","markdown":"Use exponential backoff **with jitter** (a random value up to the attempt's cap) and a cap of <Limit name=\"realtimeBackoffMax\" />: for example, 0.5 s, 1 s, 2 s, 4 s… Without jitter, thousands of clients reconnect at the same instant after an outage and take the node down again. Reset the count when you receive `connected`."},{"anchor":"new-token-and-retry_after_ms","heading":"New token and `retry_after_ms`","markdown":"Ask for a new token on every reconnection: it is cheap (free) and avoids connecting with an expired token. If the response carries `retry_after_ms`, wait that long before connecting; it replaces that attempt's backoff. Always use the `connect_url` from the most recent response."},{"anchor":"recovering-messages-with-rewind","heading":"Recovering messages with `rewind`","markdown":"A new connection does not know what happened while it was away. After `connected`, subscribe again to each channel with `rewind`: the server resends the latest messages before delivering the new ones, and `attached` marks the end of the recovery. Since `rewind` can repeat messages you already showed, drop any with an `id` you have seen. Enter presence again on each channel you were on.\n\nChoose `rewind` based on how long the connection is usually away (up to <Limit name=\"realtimeRewindMax\" /> messages). If the channel had more messages than that during the outage, the difference is lost: the history keeps only the latest ones."},{"anchor":"historico","heading":"History","markdown":"Each channel keeps the latest <Limit name=\"realtimeHistoryPageMax\" /> published messages, for up to <Limit name=\"realtimeHistoryRetention\" />. `rewind` uses this history; to query it from your server, use the API."},{"anchor":"get-channel-history","heading":"Get channel history","markdown":"<Endpoint id=\"realtime.history\" example={{ pathParams: { channel: 'chat:lobby' }, query: { limit: '50' } }}>\n  Returns the messages **from oldest to newest**, in `messages` (`id`, `name`, `data`, `timestamp`, `client_id`), along with the `channel` and the `limit` applied. There is no cursor and no paging: `limit` is at most <Limit name=\"realtimeHistoryPageMax\" /> (the default is the maximum). It is a metered read: it counts toward the account's [daily read allowance](/en/cobranca-e-limites#franquia-diaria).\n</Endpoint>"},{"anchor":"publicar","heading":"Publish a message from the server","markdown":"Publishing through the API is useful for events that originate in your backend (a paid order, an alert). The message reaches subscribers like any other.\n\n<Endpoint id=\"realtime.publish\" example={{ pathParams: { channel: 'chat:lobby' }, body: `{ \"name\": \"message\", \"data\": { \"text\": \"Order approved\" }, \"client_id\": \"system\" }` }}>\n  * `data`: any JSON, up to <Limit name=\"realtimeMessageMax\" /> (added to the `name`).\n  * `name` (optional, up to 128 characters): classifies the message.\n  * `client_id` (optional): the sender subscribers see.\n\n  The response carries `id`, `timestamp` (Unix milliseconds) and `charged`: `true` when the message went past the monthly allowance and was billed.\n\n  With `X-Dry-Run: true`, the API validates the channel and the size, neither publishes nor charges, and answers `200` with the estimate:\n\n  ```json\n  { \"success\": true, \"data\": { \"cost_units\": 0, \"within_quota\": true, \"free_used\": 18250, \"free_monthly\": 200000, \"would_charge\": false } }\n  ```\n\n  `cost_units` uses the backend scale (10,000 units = 1 credit) and is `0` within the allowance.\n</Endpoint>"},{"anchor":"canais","heading":"Channels and statistics","markdown":""},{"anchor":"list-active-channels","heading":"List active channels","markdown":"<Endpoint id=\"realtime.channels\" example={{ query: { limit: '50' } }}>\n  Lists the channels with subscribers or with a publish within the history window, with `name`, `subscribers` (the count lags up to 10 seconds) and `last_publish_at`. Use `limit` (1 to <Limit name=\"realtimeChannelListPageMax\" />, default 50) and `offset`; the response carries `total` and, when there are more pages, `next_offset` for the next call.\n</Endpoint>"},{"anchor":"get-usage-and-connections","heading":"Get usage and connections","markdown":"<Endpoint id=\"realtime.stats\">\n  Shows the `connections` open right now (summed across every node), the `active_channels` (with at least one subscriber), the `month` (UTC) and the `messages_this_month`, along with the allowance: `free_monthly_messages` and `free_remaining`.\n</Endpoint>"},{"anchor":"webhooks-de-canais","heading":"Channel webhooks","markdown":"Each account can have **one** channel webhook, which notifies your server when a channel becomes occupied or empty, when someone enters or leaves presence and (optionally) on every message. It is a Realtime setting, separate from the [account webhooks](/en/webhooks)."},{"anchor":"get-the-channel-webhook","heading":"Get the channel webhook","markdown":"<Endpoint id=\"realtime.webhook.get\">\n  Returns the current configuration with the `secret` and the `standard_key` (the `secret` in the `whsec_…` format, for Standard Webhooks libraries). With no webhook configured, it answers `404`.\n</Endpoint>"},{"anchor":"configure-the-channel-webhook","heading":"Configure the channel webhook","markdown":"<Endpoint id=\"realtime.webhook.put\" example={{ body: `{ \"url\": \"https://example.com/hooks/realtime\", \"events\": [\"channel.occupied\", \"channel.vacated\", \"channel.message\"], \"channels\": [\"chat:*\"], \"enabled\": true }` }}>\n  * `url`: a public `http` or `https` URL; internal or private network addresses are rejected.\n  * `events`: at least one of the events in the table below.\n  * `channels`: channel patterns (a name, a prefix with `*` or `*`), up to <Limit name=\"realtimeWebhookChannelsMax\" />. Empty means every channel, **except** `channel.message`: messages are opt-in and require at least one pattern.\n  * `enabled` (optional, default `true`): `false` pauses deliveries without deleting the configuration.\n\n  The call creates the webhook or replaces the current one, and the `secret` is kept. The configuration is cached for up to 30 seconds.\n</Endpoint>"},{"anchor":"rotate-the-webhook-secret","heading":"Rotate the webhook secret","markdown":"<Endpoint id=\"realtime.webhook.rotate\">\n  Generates a new `secret` (and the matching `standard_key`). The previous one stops being valid; update your receiver right after.\n</Endpoint>"},{"anchor":"remove-the-channel-webhook","heading":"Remove the channel webhook","markdown":"<Endpoint id=\"realtime.webhook.delete\">\n  Deletes the configuration; no more events are sent.\n</Endpoint>"},{"anchor":"events","heading":"Events","markdown":"<WebhookEventsTable source=\"realtime\" />"},{"anchor":"delivery","heading":"Delivery","markdown":"The platform makes a JSON `POST` to your URL:\n\n```json\n{\n  \"id\": \"evt_5b0c9a7e-3c1f-4a52-9d57-2a6f1f0e8a11\",\n  \"type\": \"presence.member_added\",\n  \"timestamp\": 1790338000000,\n  \"account_id\": \"7a1d4c8e-2f6b-4e9a-b3c5-0d8f1a6e4b27\",\n  \"data\": {\n    \"channel\": \"chat:lobby\",\n    \"member\": { \"connection_id\": \"c_8f3a1d\", \"client_id\": \"user-42\", \"data\": { \"nick\": \"ana\" }, \"timestamp\": 1790338000000 }\n  }\n}\n```\n\n`data.channel` is in every event; `data.member` comes with presence events and `data.message` (`id`, `name`, `data`, `timestamp`, `client_id`) with `channel.message`. The `timestamp` is in Unix milliseconds.\n\n* **Signature:** Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature`), using the configuration's `standard_key`; see [Verify the signature](/en/webhooks#verificar-assinatura).\n* **Attempts:** up to 3 (waits of 1 s and 5 s) on a network error, `5xx` or `429`; other responses end the delivery. Each attempt waits up to <Limit name=\"realtimeWebhookTimeout\" />.\n* **Processing:** delivery is asynchronous, by 4 workers; if the queue fills up, the event is dropped. Treat the webhook as a notification and use the `id` to drop duplicates."},{"anchor":"sdk","heading":"JavaScript SDK","markdown":"The `@respawn/realtime` package implements the protocol above, with no dependencies, for the browser and Node 22 or newer. It renews the token 30 seconds before it expires, reconnects according to the [close codes](#protocolo), resumes channels with `rewind` (100 messages, without repeating those already delivered) and re-enters presence.\n\n```bash\nnpm install @respawn/realtime\n```"},{"anchor":"on-the-server","heading":"On the server","markdown":"`RealtimeRest` uses the API key (never send it to the browser) to issue tokens, publish, and read history and presence. Pass `baseUrl` with the API address:\n\n```ts\nimport { RealtimeRest } from '@respawn/realtime';\n\nconst rest = new RealtimeRest({\n  apiKey: process.env.RESPAWN_API_TOKEN!,\n  baseUrl: 'https://api.respawntech.dev',\n});\n\n// route the browser calls (Express): returns token, expires_at and connect_url\napp.post('/realtime/token', async (req, res) => {\n  res.json(\n    await rest.createToken({\n      client_id: req.user.id,\n      capabilities: { 'room:*': ['subscribe', 'publish', 'presence'] },\n      ttl_seconds: 600,\n    }),\n  );\n});\n\nawait rest.publish('room:1', 'order', { id: 42, status: 'paid' });\nconst history = await rest.history('room:1', { limit: 20 });\nconst members = await rest.presence('room:1');\n```"},{"anchor":"in-the-browser","heading":"In the browser","markdown":"`Realtime` asks your server for the token through `authUrl` (a `POST` with cookies) or `authCallback`:\n\n```ts\nimport { Realtime } from '@respawn/realtime';\n\nconst rt = new Realtime({ authUrl: '/realtime/token' });\nrt.on('state', (state, err) => console.log('connection:', state, err?.code));\n\nconst room = rt.channel('room:1');\n\nawait room.subscribe((m) => console.log(m.name, m.data), { rewind: 20 });\nawait room.publish('chat', { text: 'hi' });\n\nawait room.presence.watch((members) => render(members));\nawait room.presence.enter({ nick: 'ana' });\n```\n\nThe states are `initialized`, `connecting`, `connected`, `disconnected`, `closed` (you called `close()`) and `failed` (for example, close `4003`). If the time offline went past the history window, the channel emits the `gap` event. Publishes made while disconnected are queued; one already sent when the connection drops fails with `sdk.connection_lost` (it may or may not have been published)."},{"anchor":"react","heading":"React","markdown":"With `@respawn/realtime/react`:\n\n```tsx\nimport { useState } from 'react';\nimport { Realtime } from '@respawn/realtime';\nimport { RealtimeProvider, useChannel, usePresence } from '@respawn/realtime/react';\n\nconst rt = new Realtime({ authUrl: '/realtime/token' });\n\nexport function App() {\n  return (\n    <RealtimeProvider client={rt}>\n      <Room />\n    </RealtimeProvider>\n  );\n}\n\nfunction Room() {\n  const [msgs, setMsgs] = useState<{ id: string; data: unknown }[]>([]);\n  const { publish } = useChannel('room:1', (m) => setMsgs((all) => [...all, m]), { rewind: 20 });\n  const { members } = usePresence('room:1', { nick: 'ana' });\n\n  return (\n    <div>\n      <p>{members.length} online</p>\n      <ul>{msgs.map((m) => <li key={m.id}>{JSON.stringify(m.data)}</li>)}</ul>\n      <button onClick={() => publish('chat', { text: 'hi' })}>Send</button>\n    </div>\n  );\n}\n```"},{"anchor":"limites","heading":"Limits","markdown":"| Limit                                  | Value                                                                                                                                    |\n| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |\n| Free messages per month (per account)  | <Limit name=\"realtimeFreeMessages\" />                                                                                                    |\n| Message size (`data`)                  | <Limit name=\"realtimeMessageMax\" />                                                                                                      |\n| Channel name length                    | <Limit name=\"realtimeChannelNameMax\" /> characters                                                                                       |\n| Channels subscribed per connection     | <Limit name=\"realtimeChannelsPerConnection\" />                                                                                           |\n| Publishes per second (per account)     | <Limit name=\"realtimePublishPerSecond\" />                                                                                                |\n| Simultaneous connections (per account) | <Limit name=\"realtimeConnectionsPerAccount\" />                                                                                           |\n| Members in a channel's presence        | <Limit name=\"realtimePresenceMembers\" />                                                                                                 |\n| Token lifetime                         | <Limit name=\"realtimeTokenTtlDefault\" /> (default), from <Limit name=\"realtimeTokenTtlMin\" /> up to <Limit name=\"realtimeTokenTtlMax\" /> |\n| Channel patterns per token             | up to <Limit name=\"realtimeTokenPatternsMax\" />                                                                                          |\n| History retention                      | <Limit name=\"realtimeHistoryRetention\" />                                                                                                |\n| Messages kept and per `rewind`         | up to <Limit name=\"realtimeHistoryPageMax\" />                                                                                            |\n| Channels per page (`GET /channels`)    | up to <Limit name=\"realtimeChannelListPageMax\" />                                                                                        |\n| Webhook channel patterns               | up to <Limit name=\"realtimeWebhookChannelsMax\" />                                                                                        |\n\nThe values are the platform defaults and the platform can adjust them. API requests also follow the account's [rate limit](/en/cobranca-e-limites)."},{"anchor":"erros","heading":"Errors","markdown":"REST API errors (`realtime.channel_invalid`, `realtime.message_too_large`…) arrive as an HTTP response, in the format described in [Errors](/en/erros). On the WebSocket or SSE connection, the same codes arrive in an `error` frame (the connection stays open) or as a close code (see [Protocol](#protocolo)).\n\n<ErrorTable tool=\"realtime\" />\n\nCommon validation errors (`validation.failed`, `not_found`) are in [Errors](/en/erros)."},{"anchor":"preco","heading":"Pricing","markdown":"<PricingTable tool=\"realtime\" group=\"per-message\" />\n\n<PricingTable tool=\"realtime\" group=\"free-quota\" />\n\nBilling is per published message, once, for the messages that go past the monthly allowance; the number of subscribers does not change the cost. Issuing tokens, keeping connections open, using presence, listing channels and configuring the webhook are free. Querying the history is a metered read: it counts toward the daily read allowance and, after it, costs the value in the [reads table](/en/cobranca-e-limites#franquia-diaria). With no balance for a message beyond the allowance, the publish fails with `realtime.insufficient_credits`."}]},{"slug":"ferramentas/verificacao-de-url","url":"https://docs.respawntech.dev/en/ferramentas/verificacao-de-url","title":"URL Safety Check","description":"Check whether a URL is malicious and fetch a domain's favicon through a proxy.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"verificacao-de-url\" />"},{"anchor":"overview","heading":"Overview","markdown":"Use it before displaying user-generated links or redirecting someone to an external destination. It's the same check that [Dynamic QR](/en/ferramentas/qr-dinamico) applies to destination URLs.\n\n<EndpointList tool=\"verificacao-de-url\" />"},{"anchor":"custos","heading":"Costs","markdown":"Both operations are metered reads: they count toward the [daily free quota](/en/cobranca-e-limites#franquia-diaria) and, once it's used up, debit the amount shown on each endpoint."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"check-a-url","heading":"Check a URL","markdown":"<Endpoint id=\"safety.check\" example={{ query: { url: 'https://example.com' } }}>\n  Query: `url` (required, absolute URL). Results are cached for 1 hour.\n\n  <Callout type=\"warn\" title=\"Response outside the standard envelope\">\n    This route returns `{ \"is_safe\": … }` directly, without `success`/`data`. If the safety service is unavailable, the response is `is_safe: true` (*fail-open*), so your flow isn't blocked. On dry-run, the response uses the standard envelope.\n  </Callout>\n</Endpoint>"},{"anchor":"domain-favicon","heading":"Domain favicon","markdown":"<Endpoint id=\"safety.favicon\" example={{ query: { domain: 'example.com' }, response: 'binary' }}>\n  Query: `domain` (required).\n</Endpoint>"},{"anchor":"errors","heading":"Errors","markdown":"<ErrorTable tool=\"verificacao-de-url\" />"}]},{"slug":"ferramentas/web-to-markdown","url":"https://docs.respawntech.dev/en/ferramentas/web-to-markdown","title":"Web Page to Markdown","description":"Fetch a web page and get its main content as Markdown, optionally saving it to a knowledge base.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"web-to-markdown\" />"},{"anchor":"overview","heading":"Overview","markdown":"Provide a URL and get the main text of the page as Markdown, with title, author and word count. With `collection_id`, the content is also saved to a [knowledge base](/en/ferramentas/base-de-conhecimento). Also available in the [MCP](/en/agentes-de-ia) as `web_to_markdown`.\n\n<EndpointList tool=\"web-to-markdown\" />"},{"anchor":"custos","heading":"Costs","markdown":"The conversion is a metered read: it counts toward the [daily free quota](/en/cobranca-e-limites#franquia-diaria) and, once it's used up, debits the amount shown on the endpoint. When saving to a base, the write is billed as a [knowledge base](/en/ferramentas/base-de-conhecimento) ingestion."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"convert-a-page-to-markdown","heading":"Convert a page to Markdown","markdown":"<Endpoint id=\"web.markdown\" example={{ body: `{ \"url\": \"https://example.com/blog/post\" }` }}>\n  `url` (required) is the `http` or `https` address of the page. `collection_id` (optional) saves the content to that base and also requires the `tools:rag:manage` scope.\n\n  The response carries `url`, `final_url` (after redirects), `title`, `byline`, `markdown` and `word_count`. With `collection_id`, it also carries `document` (`document_id`, `status` and `duplicate`) and, on dry-run, `estimate`.\n\n  <Callout type=\"warn\" title=\"Static HTML only\">\n    The page is read as the server delivers it, without running JavaScript: pages that build their content in the browser respond `tools.web.no_content`. Internal and private addresses are refused (`tools.web.blocked_url`).\n  </Callout>\n</Endpoint>"},{"anchor":"errors","heading":"Errors","markdown":"<ErrorTable tool=\"web-to-markdown\" />"}]},{"slug":"ferramentas/webhook-relay","url":"https://docs.respawntech.dev/en/ferramentas/webhook-relay","title":"Webhook Relay","description":"Send signed webhooks to your customers, with automatic retries, an attempt log and replay, and fan out webhooks from Stripe, GitHub and other providers.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"webhook-relay\" />"},{"anchor":"visao-geral","heading":"Overview","markdown":"Webhook Relay is the webhook infrastructure for your application. Instead of running queues, retries and signatures yourself, you publish an event with one call and the platform delivers it to every subscribed endpoint, using the same engine that sends the [account webhooks](/en/webhooks).\n\n* **Project:** groups endpoints, sources, routes, event types and messages, to keep setups apart (a store, a customer, an environment). Every account has a default project; see [Projects](#projetos).\n* **Endpoint:** the URL that receives events (usually your customer's, or a Slack, Discord or Teams channel), with the events it subscribes to and its own `secret`.\n* **Message:** an event you publish (`event_type` + `payload`).\n* **Delivery:** one message on its way to one endpoint. A message to 3 endpoints creates 3 deliveries, each with its own retries.\n* **Attempt:** each HTTP call of a delivery, logged with status, latency and the start of the response.\n* **Source and route:** to **receive** third-party webhooks (Stripe, GitHub…) at a platform URL and fan them out to your endpoints. See [Receiving third-party webhooks](#ingestao).\n\nEvery request follows the [Standard Webhooks](https://www.standardwebhooks.com) scheme (`webhook-id`, `webhook-timestamp` and `webhook-signature` headers). The format, signature verification and retry schedule are in the [Webhooks](/en/webhooks) guide."},{"anchor":"primeiros-passos","heading":"Getting started","markdown":"<Steps>\n  <Step>\n    **Create an endpoint** with the destination URL and the events it subscribes to. Store the `secret` from the response (use `standard_webhooks_secret` with the official libraries). Without `project_id`, it goes to the default project.\n  </Step>\n\n  <Step>\n    **Test the connection** with the [test ping](#webhooks-endpoints-test): a free `webhook.test` event.\n  </Step>\n\n  <Step>\n    **Send a message** with `event_type` and `payload`. Use `X-Dry-Run: true` first to see how many deliveries it creates and what it costs.\n  </Step>\n\n  <Step>\n    **Verify the signature** in the receiver and answer `2xx` quickly. See the examples in [Verifying the signature](/en/webhooks#verificar-assinatura).\n  </Step>\n\n  <Step>\n    **Track** deliveries in the [message log](#mensagens) and retry what failed.\n  </Step>\n</Steps>\n\n<EndpointList tool=\"webhook-relay\" />"},{"anchor":"custos","heading":"Costs","markdown":"<PricingTable tool=\"webhook-relay\" group=\"per-delivery\" />\n\n<PricingTable tool=\"webhook-relay\" group=\"free-quota\" />\n\nBilling happens at send time, per message, for the deliveries beyond the free quota (`charged_units`, in units of 1/10,000 credit). If the message cannot be stored, the amount is refunded. The `payload` is validated before any charge.\n\nThe free quota and billing are **per account**: every project shares the same monthly free quota, and a delivery costs the same in any project.\n\n<Callout title=\"Log reads\">\n  Configuring projects, endpoints, event types, sources and routes is free. The [list](#webhooks-messages-list) and [retrieve](#webhooks-messages-get) message routes are metered reads: they count toward the [daily free read quota](/en/cobranca-e-limites#franquia-diaria) and, after it, cost the value in the reads table.\n</Callout>"},{"anchor":"retrieve-this-months-usage","heading":"Retrieve this month's usage","markdown":"<Endpoint id=\"webhooks.usage\">\n  Shows the current month (UTC, from `period_start` to `period_end`): the free quota (`free_deliveries`), the deliveries already used by the API and ingestion (`used_deliveries`) and what is left (`free_left`). `delivery_units` and `read_units` are the current prices of one delivery and one log read, in units; `blocked_retention_days` is how long a delivery waits for credits (see [Out of credits](#sem-creditos)).\n</Endpoint>"},{"anchor":"projetos","heading":"Projects","markdown":"Everything you configure in Webhook Relay belongs to a project: endpoints, sources (and their routes), event types, messages and the canvas layout in the dashboard. Use projects to keep setups apart, for example one per store or per customer.\n\n* Every account has a **default project** (`is_default: true`, named `Default`), created on first use. It cannot be deleted.\n* `project_id` is **optional** across the API: without it, creates and sends use the default project. Integrations that don't know about projects keep working.\n* A message only reaches the endpoints of its project and is validated against that project's event types. Event type names are unique per project.\n* The lists (`GET /endpoints`, `/sources`, `/event-types`, `/routes` and `/messages`) take `?project_id=` as a filter; without it, they return every project.\n* Platform events (e.g. `qr.dynamic.scanned`) create one message in each project that has subscribed endpoints, always free. Job callbacks (`webhook_url`) belong to no project (`project_id: null`).\n* Limits and billing stay per account: see [Limits](#limites). By default there is no project limit."},{"anchor":"list-projects","heading":"List projects","markdown":"<Endpoint id=\"webhooks.projects.list\" example={{ query: { q: 'store', page: '1', page_size: '20' } }}>\n  Paged and searchable: `q` searches the name (case-insensitive), `page` starts at 1 and `page_size` goes up to <Limit name=\"webhookProjectsPageMax\" /> (default <Limit name=\"webhookProjectsPageDefault\" />). Each project has `stats`: sources, endpoints, messages of the last 24 hours, deliveries this month and `failing_endpoints` (endpoints failing or disabled automatically; an endpoint you paused does not count).\n</Endpoint>"},{"anchor":"create-project","heading":"Create project","markdown":"<Endpoint id=\"webhooks.projects.create\" example={{ body: `{ \"name\": \"Store\", \"description\": \"Online store events\" }` }}>\n  `name` (required) has up to <Limit name=\"webhookProjectNameMax\" /> characters; `description`, up to <Limit name=\"webhookProjectDescriptionMax\" />.\n</Endpoint>"},{"anchor":"retrieve-project","heading":"Retrieve project","markdown":"<Endpoint id=\"webhooks.projects.get\" />"},{"anchor":"update-project","heading":"Update project","markdown":"<Endpoint id=\"webhooks.projects.update\" example={{ body: `{ \"name\": \"Main store\" }` }}>\n  Send only the fields you want to change.\n</Endpoint>"},{"anchor":"delete-project","heading":"Delete project","markdown":"<Endpoint id=\"webhooks.projects.delete\">\n  Deletes the project's endpoints (pending deliveries are canceled), sources, routes, event types and layout. Its messages stay in the global log, with `project_id: null`.\n</Endpoint>"},{"anchor":"duplicate-project","heading":"Duplicate project","markdown":"<Endpoint id=\"webhooks.projects.duplicate\" example={{ body: `{ \"name\": \"Store (copy)\" }` }}>\n  Creates a new project with the same description and copies of everything, already wired together:\n\n  * **Endpoints** with **new secrets** (same URL, events, headers, send rate, format and active flag; the failure history is not copied).\n  * **Sources** with **new `ingest_url`s** (same settings and the same provider secret). Register the new URLs with the provider.\n  * Routes, event types and the canvas layout.\n\n  Messages, deliveries and counters are not copied. The account limits are checked first: if the copy would go over one, the response is `webhook.limit_reached` and nothing is created.\n</Endpoint>"},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"create-endpoint","heading":"Create endpoint","markdown":"<Endpoint\n  id=\"webhooks.endpoints.create\"\n  example={{ body: `{\n\"url\": \"https://api.example.com/hooks/respawn\",\n\"description\": \"Store orders\",\n\"events\": [\"order.paid\", \"order.refunded\"],\n\"custom_headers\": { \"X-Tenant\": \"store-1\" }\n}` }}\n>\n  * `url` (required): public `http` or `https` URL. Private, local or cloud-metadata addresses are refused (`webhook.invalid_url`).\n  * `events`: event types the endpoint receives from API and platform messages; `[\"*\"]` subscribes to all. With no events, the endpoint only receives what [routes](#ingestao) send it.\n  * `project_id`: the endpoint's project (default: the default project). An endpoint does not move between projects.\n  * `format`: `raw` (default), `slack`, `discord` or `teams`. See [Slack, Discord and Teams](#formatos).\n  * `custom_headers`: up to <Limit name=\"webhookCustomHeadersMax\" /> extra headers sent with every delivery (e.g. a token of your customer). Signature headers, `Content-Type`, `User-Agent` and `Host` cannot be overridden.\n  * `rate_limit_per_sec`: cap of sends per second for this endpoint (`0` = no own cap).\n  * The `secret` is only returned in this response; afterwards, use [Reveal secret](#webhooks-endpoints-secret). Up to <Limit name=\"webhookEndpointsMax\" /> endpoints per account, across every project.\n</Endpoint>"},{"anchor":"list-endpoints","heading":"List endpoints","markdown":"<Endpoint id=\"webhooks.endpoints.list\" example={{ query: { project_id: '3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53' } }}>\n  Optional `project_id` filter. An automatically disabled endpoint has `is_active: false` and `disabled_reason`; one that is failing has `failing_since`.\n</Endpoint>"},{"anchor":"retrieve-endpoint","heading":"Retrieve endpoint","markdown":"<Endpoint id=\"webhooks.endpoints.get\" />"},{"anchor":"update-endpoint","heading":"Update endpoint","markdown":"<Endpoint id=\"webhooks.endpoints.update\" example={{ body: `{ \"events\": [\"*\"], \"is_active\": true }` }}>\n  Send only the fields you want to change (including `format`). `is_active: true` re-enables a disabled endpoint and clears its failing streak.\n</Endpoint>"},{"anchor":"delete-endpoint","heading":"Delete endpoint","markdown":"<Endpoint id=\"webhooks.endpoints.delete\">\n  Pending deliveries to the endpoint are canceled.\n</Endpoint>"},{"anchor":"reveal-secret","heading":"Reveal secret","markdown":"<Endpoint id=\"webhooks.endpoints.secret\">\n  `secret` is the raw key (HMAC of the legacy `X-Webhook-Signature` header); `standard_webhooks_secret` is the same key in the `whsec_…` form accepted by the Standard Webhooks libraries.\n</Endpoint>"},{"anchor":"rotate-secret","heading":"Rotate secret","markdown":"<Endpoint id=\"webhooks.endpoints.rotate_secret\">\n  For <Limit name=\"webhookSecretOverlap\" />, deliveries carry two signatures in `webhook-signature` (new and old secret), so you can switch the receiver's secret without losing events.\n</Endpoint>"},{"anchor":"send-test-ping","heading":"Send test ping","markdown":"<Endpoint id=\"webhooks.endpoints.test\">\n  Sends a signed `webhook.test` event, free and outside the quota. Follow the result in [Retrieve message](#webhooks-messages-get) with the returned `message_id`.\n</Endpoint>"},{"anchor":"replay-failed-deliveries","heading":"Replay failed deliveries","markdown":"<Endpoint id=\"webhooks.endpoints.recover\" example={{ query: { since: '2026-09-24T00:00:00Z' } }}>\n  Resends to this endpoint every delivery that ended in failure (`failed`, `exhausted` or `canceled`) since `since` (RFC 3339), up to <Limit name=\"webhookRecoverMax\" /> per call. Use it after fixing a receiver that was down. Each replayed delivery is charged as a new one.\n</Endpoint>"},{"anchor":"formatos","heading":"Slack, Discord and Teams","markdown":"With `format` set to `slack`, `discord` or `teams`, register the channel's incoming webhook as the `url` (Slack or Teams Incoming Webhook, Discord webhook). The platform turns each delivery into a text message when it is sent:\n\n* The body becomes `{\"text\": \"…\"}` (Slack and Teams) or `{\"content\": \"…\"}` (Discord), with `Content-Type: application/json`.\n* The text has the event type in bold (and the source name, for messages received from a provider), followed by the body in a code block: pretty-printed JSON or the raw text.\n* Long texts are cut with `…` to fit the platform's limit: <Limit name=\"webhookSlackTextMax\" /> characters on Slack, <Limit name=\"webhookDiscordTextMax\" /> on Discord and <Limit name=\"webhookTeamsTextMax\" /> on Teams.\n* The signature headers are still sent, computed over that body; the original headers of a source with `forward_original_headers` are not forwarded.\n\nThe message stored in the log does not change: the log, retry and recovery work exactly as for `raw` endpoints."},{"anchor":"mensagens","heading":"Messages","markdown":""},{"anchor":"send-message","heading":"Send message","markdown":"<Endpoint\n  id=\"webhooks.messages.send\"\n  example={{ headers: { 'X-Dry-Run': 'true' }, body: `{\n\"event_type\": \"order.paid\",\n\"payload\": { \"order_id\": \"ord_1042\", \"amount\": 199.9 },\n\"idempotency_key\": \"order-paid-ord_1042\"\n}` }}\n>\n  * `event_type` (required): `^[a-z0-9][a-z0-9._-]{0,127}$`, e.g. `order.paid`. It goes to the project's endpoints that subscribe to that type (or `*`).\n  * `payload` (required): any JSON, up to <Limit name=\"webhookPayloadMax\" />. If the event type has a [JSON Schema](#tipos-de-evento) in the project, the payload must match it (`webhook.payload_invalid`, not charged).\n  * `project_id` (optional): the message's project (default: the default project).\n  * `endpoint_ids` (optional): restricts the send to these endpoints, among the project's endpoints subscribed to the event.\n  * `idempotency_key` (or the `X-Idempotency-Key` header): repeating the key returns the first message with `duplicate: true` and charges nothing. Use it to resend safely after a timeout.\n  * With `X-Dry-Run: true`, nothing is sent or charged: the response shows `deliveries`, `free_deliveries` and `charged_units`.\n\n  The receiver gets the `{ \"event_id\", \"type\", \"timestamp\", \"payload\" }` envelope described in [Webhooks](/en/webhooks#corpo); `event_id` is the `message_id` and `webhook-id` is `msg_<message_id>`.\n</Endpoint>"},{"anchor":"send-messages-in-batch","heading":"Send messages in batch","markdown":"<Endpoint\n  id=\"webhooks.messages.batch\"\n  example={{ body: `{\n\"messages\": [\n  { \"event_type\": \"order.paid\", \"payload\": { \"order_id\": \"ord_1042\", \"amount\": 199.9 }, \"idempotency_key\": \"order-paid-ord_1042\" },\n  { \"event_type\": \"order.paid\", \"payload\": { \"order_id\": \"ord_1043\" }, \"idempotency_key\": \"order-paid-ord_1043\" }\n]\n}` }}\n>\n  Up to <Limit name=\"webhookBatchMax\" /> messages, each with the same fields as a single send. `project_id` goes at the top level of the body and applies to every message of the batch. Each message is validated, deduplicated and charged on its own: a failure shows up in `error` on its item and does not stop the others. Accepts `X-Dry-Run: true`.\n</Endpoint>"},{"anchor":"list-messages","heading":"List messages","markdown":"<Endpoint id=\"webhooks.messages.list\" example={{ query: { event_type: 'order.paid', failed: 'true', limit: '20' } }}>\n  Newest first, with each message's deliveries (without the body). Pagination: pass the `next_before` you received as `before`. `limit` from 1 to 100 (default 20). Messages stay in the log for <Limit name=\"webhookLogRetention\" />.\n\n  * `project_id`: only that project's messages. Without it, the log returns every project, plus the messages without a project (`project_id: null`: job callbacks and messages of deleted projects).\n  * `event_type` and `endpoint_id`: exact filters.\n  * `failed=true`: only messages with at least one `failed`, `exhausted` or `blocked_no_credits` delivery.\n  * `q`: search, 1 to <Limit name=\"webhookSearchQueryMax\" /> characters, combinable with the other filters. A UUID finds the message with that id. Other text matches an idempotency key or event type equal to it, or text inside the body (case-insensitive, except for accented letters); the body search only covers messages from the last <Limit name=\"webhookSearchBodyWindow\" />.\n</Endpoint>"},{"anchor":"retrieve-message","heading":"Retrieve message","markdown":"<Endpoint id=\"webhooks.messages.get\">\n  Returns the body sent and, per delivery, every attempt: `http_status`, `latency_ms`, the start of the response (`response_body`) or the network error (`error`). `next_attempt_at` shows while the delivery is pending.\n\n  | Delivery `status`    | Meaning                                                                                                     |\n  | -------------------- | ----------------------------------------------------------------------------------------------------------- |\n  | `pending`            | Waiting for the next attempt (`next_attempt_at`).                                                           |\n  | `sending`            | Attempt in progress.                                                                                        |\n  | `succeeded`          | The endpoint answered `2xx`.                                                                                |\n  | `exhausted`          | Every attempt failed. Can be retried.                                                                       |\n  | `failed`             | Stopped for good: the endpoint answered `410 Gone`, was disabled or its URL became invalid. Can be retried. |\n  | `canceled`           | Canceled, for example because the endpoint was deleted while the delivery was pending.                      |\n  | `blocked_no_credits` | Message received from a provider without balance for the delivery (see [Out of credits](#sem-creditos)).    |\n</Endpoint>"},{"anchor":"retry-message","heading":"Retry message","markdown":"<Endpoint id=\"webhooks.messages.retry\" example={{ body: `{ \"endpoint_id\": \"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34\" }` }}>\n  Without a body, sends the message again to the active endpoints whose delivery ended in failure (`failed`, `exhausted` or `canceled`).\n\n  With `endpoint_id`, sends it to that endpoint only, whatever the previous outcome (even if the delivery succeeded). It must be an active endpoint of the message's project; otherwise, and for messages without a project, the response is `400`. The new deliveries are charged like a send.\n</Endpoint>"},{"anchor":"tipos-de-evento","heading":"Event types","markdown":"Declaring event types is optional: without a declaration, any valid `event_type` is accepted. Declare them to document your integration's events and, with `schema`, validate the `payload` before charging and sending. Each type belongs to a project and only applies to that project's messages."},{"anchor":"declare-event-type","heading":"Declare event type","markdown":"<Endpoint\n  id=\"webhooks.event_types.create\"\n  example={{ body: `{\n\"name\": \"order.paid\",\n\"description\": \"Order paid\",\n\"schema\": {\n  \"type\": \"object\",\n  \"required\": [\"order_id\", \"amount\"],\n  \"properties\": {\n    \"order_id\": { \"type\": \"string\" },\n    \"amount\": { \"type\": \"number\" }\n  }\n}\n}` }}\n>\n  `schema` is a JSON Schema applied to the `payload` of every send of that type; a payload that does not match answers `webhook.payload_invalid` and is not charged. `project_id` is optional (default: the default project); the name is unique within the project. Up to <Limit name=\"webhookEventTypesMax\" /> types per account, across every project.\n</Endpoint>"},{"anchor":"list-event-types","heading":"List event types","markdown":"<Endpoint id=\"webhooks.event_types.list\">\n  Optional `project_id` filter.\n</Endpoint>"},{"anchor":"delete-event-type","heading":"Delete event type","markdown":"<Endpoint id=\"webhooks.event_types.delete\">\n  Sends of that type are still accepted, without schema validation.\n</Endpoint>"},{"anchor":"ingestao","heading":"Receiving third-party webhooks","markdown":"A **source** is a platform URL (`ingest_url`) that you register with the provider (Stripe, GitHub, Shopify…). Every event received has the provider's signature verified, is stored and fanned out through the source's **routes** to your endpoints, with the same signature, retries and log as API messages. One provider webhook can feed several services, and the provider never waits on your receiver.\n\n| `provider`          | How the request is verified                                                                                                    |\n| ------------------- | ------------------------------------------------------------------------------------------------------------------------------ |\n| `stripe`            | `Stripe-Signature` header with the Stripe signing secret (`whsec_…`), tolerance of <Limit name=\"webhookSignatureTolerance\" />. |\n| `github`            | `X-Hub-Signature-256` header with the GitHub webhook secret.                                                                   |\n| `shopify`           | `X-Shopify-Hmac-Sha256` header with the Shopify app secret.                                                                    |\n| `hmac_generic`      | `X-Webhook-Signature: sha256=<hex>` header (HMAC-SHA256 of the body).                                                          |\n| `standard_webhooks` | `webhook-id`, `webhook-timestamp` and `webhook-signature` headers.                                                             |\n| `basic_auth`        | `Authorization: Basic …` header; the secret is `user:password`.                                                                |\n| `none`              | No verification: anyone with the URL can post. Avoid in production.                                                            |\n\n`hmac_generic`, `standard_webhooks` and `basic_auth` work with any system that is not in the list: pick the one the sending system supports."},{"anchor":"create-ingestion-source","heading":"Create ingestion source","markdown":"<Endpoint\n  id=\"webhooks.sources.create\"\n  example={{ body: `{\n\"name\": \"Stripe production\",\n\"provider\": \"stripe\",\n\"verification_secret\": \"whsec_...\",\n\"event_type_path\": \"type\",\n\"dedupe_key_path\": \"id\"\n}` }}\n>\n  * `name` and `provider` (required); `project_id` is optional (default: the default project).\n  * `verification_secret`: the secret the provider signs with. It is never returned (`has_verification_secret`). It can be set later, because several providers only show the secret after you register the `ingest_url`: until it is set in [Set provider secret](#webhooks-sources-verification_secret), the source answers `401` to every request (except with `provider: \"none\"`).\n  * `event_type_path`: path in the received JSON holding the event type (e.g. `type`, `data.object.status`). Without it, the type is `ingest`.\n  * `dedupe_key_path`: path of the event identifier (e.g. `id`). A provider resend with the same key answers `200` without creating new deliveries.\n  * `forward_original_headers`: forwards the provider's headers (e.g. `Stripe-Signature`) along with the identical body, next to our signature. Useful when your receiver already validates the provider's signature. Cookies, `Authorization` and proxy headers are never forwarded.\n  * Register the `ingest_url` from the response with the provider. Up to <Limit name=\"webhookSourcesMax\" /> sources per account, across every project.\n</Endpoint>"},{"anchor":"list-sources","heading":"List sources","markdown":"<Endpoint id=\"webhooks.sources.list\">\n  Optional `project_id` filter.\n</Endpoint>"},{"anchor":"retrieve-source","heading":"Retrieve source","markdown":"<Endpoint id=\"webhooks.sources.get\" />"},{"anchor":"update-source","heading":"Update source","markdown":"<Endpoint id=\"webhooks.sources.update\" example={{ body: `{ \"provider\": \"hmac_generic\", \"forward_original_headers\": true }` }}>\n  Send only the fields you want to change, including `provider` (for example, to switch a `none` source to `hmac_generic`). When switching to a signed provider, send `verification_secret` in the same request so the source is never left without a secret (without one it answers `401`). To change only the secret, use [Set provider secret](#webhooks-sources-verification_secret); a blank secret is refused. A source does not move between projects.\n</Endpoint>"},{"anchor":"delete-source","heading":"Delete source","markdown":"<Endpoint id=\"webhooks.sources.delete\">\n  The source's routes are deleted with it and the `ingest_url` stops answering.\n</Endpoint>"},{"anchor":"rotate-ingestion-url","heading":"Rotate ingestion URL","markdown":"<Endpoint id=\"webhooks.sources.rotate_token\">\n  Issues a new `ingest_url`; the previous one stops working immediately. Update the URL at the provider right away.\n</Endpoint>"},{"anchor":"set-provider-secret","heading":"Set provider secret","markdown":"<Endpoint id=\"webhooks.sources.verification_secret\" example={{ body: `{ \"secret\": \"whsec_...\" }` }}>\n  An empty secret is only accepted with `provider: \"none\"`.\n</Endpoint>"},{"anchor":"retrieve-last-received-event","heading":"Retrieve last received event","markdown":"<Endpoint id=\"webhooks.sources.sample\">\n  Returns the last body the source received, with the event type read from it and the time. Useful to discover the provider's fields before setting `event_type_path`, `dedupe_key_path` and the route filters.\n</Endpoint>"},{"anchor":"send-a-test-event-through-the-source","heading":"Send a test event through the source","markdown":"<Endpoint id=\"webhooks.sources.test\" example={{ body: `{ \"event_type\": \"invoice.paid\" }` }}>\n  Runs an event through the source as if the provider had sent it: the type is read at `event_type_path` and the routes (with their filters) decide where it goes. The body is the last one the source received or, if it has received nothing yet, a provider sample. `event_type` (optional) is written at the `event_type_path` location; if the body lacks that field, the type used is `test.event`.\n\n  It is free and outside the monthly free quota. It skips signature verification and deduplication, but counts toward the source's limit of <Limit name=\"webhookIngestPerMinute\" /> requests per minute.\n</Endpoint>"},{"anchor":"create-route","heading":"Create route","markdown":"<Endpoint\n  id=\"webhooks.routes.create\"\n  example={{ body: `{\n\"source_id\": \"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27\",\n\"endpoint_id\": \"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34\",\n\"event_types\": [\"checkout.session.completed\", \"invoice.paid\"]\n}` }}\n>\n  Connects a source to an endpoint **of the same project** (a source and an endpoint of different projects answer `400`). `event_types` restricts the route to those types, up to <Limit name=\"webhookRouteEventTypesMax\" />; empty, the route takes every event of the source. Up to <Limit name=\"webhookRoutesPerSourceMax\" /> routes per source.\n</Endpoint>"},{"anchor":"list-routes","heading":"List routes","markdown":"<Endpoint id=\"webhooks.routes.list\" example={{ query: { source_id: 'a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27' } }}>\n  Optional `source_id` and `project_id` filters.\n</Endpoint>"},{"anchor":"update-route","heading":"Update route","markdown":"<Endpoint id=\"webhooks.routes.update\" example={{ body: `{ \"event_types\": [\"invoice.paid\"], \"is_active\": false }` }}>\n  Changes the route's event types (empty list = all) or pauses it with `is_active: false`. Send only the fields you want to change.\n</Endpoint>"},{"anchor":"delete-route","heading":"Delete route","markdown":"<Endpoint id=\"webhooks.routes.delete\" />"},{"anchor":"receive-a-provider-event","heading":"Receive a provider event","markdown":"<Endpoint id=\"webhooks.ingest\" example={{ pathParams: { token: 'src_4f8a2c6e1b9d3f7a5c0e8b2d6f4a1c9e7b3d5f0a2c8e6b4d' }, headers: { 'Stripe-Signature': 't=1790338000,v1=5257a869…' }, body: `{ \"id\": \"evt_1Q2w3E\", \"type\": \"checkout.session.completed\", \"data\": { \"object\": { \"id\": \"cs_test_a1B2\" } } }` }}>\n  This is the URL the provider calls; you don't need to call it. The body is stored and forwarded byte for byte, with the original `Content-Type`. The responses are designed for the provider to decide whether to retry:\n\n  | Status | When                                                                                  |\n  | ------ | ------------------------------------------------------------------------------------- |\n  | `200`  | Accepted, duplicate (provider resend) or no route for the type.                       |\n  | `401`  | Invalid signature, or provider secret not set yet; nothing is stored.                 |\n  | `404`  | Unknown or disabled source, or tool not enabled for the account.                      |\n  | `429`  | More than <Limit name=\"webhookIngestPerMinute\" /> requests per minute for the source. |\n  | `5xx`  | Storage failed; the provider should retry.                                            |\n</Endpoint>"},{"anchor":"exemplo-stripe","heading":"Example: Stripe","markdown":"<Steps>\n  <Step>\n    [Create an endpoint](#webhooks-endpoints-create) for each of your services that needs Stripe events. The events arrive through routes, so `events` can stay empty.\n  </Step>\n\n  <Step>\n    [Create a source](#webhooks-sources-create) with `provider: \"stripe\"`, `event_type_path: \"type\"` and `dedupe_key_path: \"id\"`, still without the secret.\n  </Step>\n\n  <Step>\n    In the Stripe dashboard, add a webhook endpoint with the `ingest_url` and copy the **signing secret** (`whsec_…`) into [Set provider secret](#webhooks-sources-verification_secret).\n  </Step>\n\n  <Step>\n    [Create a route](#webhooks-routes-create) from the source to each endpoint, with `event_types` when a service only needs some events. Check the path with [Send a test event through the source](#webhooks-sources-test).\n  </Step>\n</Steps>\n\nYour services now receive the events signed by the platform (validate them with each endpoint's secret). If they prefer to validate Stripe's own signature, enable `forward_original_headers` on the source: `Stripe-Signature` and the identical body arrive too."},{"anchor":"sem-creditos","heading":"Out of credits","markdown":"The provider is never refused for lack of balance. Deliveries that would be charged get the `blocked_no_credits` status (counted in `blocked` in the response to the provider) and are charged and released automatically as soon as the account has credits. If the balance does not come back within <Limit name=\"webhookBlockedRetention\" />, they end as `failed`."},{"anchor":"canvas","heading":"Dashboard canvas","markdown":"In the dashboard, each project is shown as a canvas with its sources, endpoints and the routes between them. These routes feed that screen; for an API integration, the lists above are usually enough. All three take `?project_id=` (default: the default project)."},{"anchor":"retrieve-flow-map","heading":"Retrieve flow map","markdown":"<Endpoint id=\"webhooks.graph\" example={{ query: { project_id: '3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53' } }}>\n  The project's sources, endpoints and routes with the counters of the last 24 hours: per source, messages received, the latest one and the recent event types; per endpoint and per route, deliveries, failures and the status of the latest one.\n</Endpoint>"},{"anchor":"retrieve-canvas-layout","heading":"Retrieve canvas layout","markdown":"<Endpoint id=\"webhooks.layout.get\" />"},{"anchor":"save-canvas-layout","heading":"Save canvas layout","markdown":"<Endpoint\n  id=\"webhooks.layout.save\"\n  example={{ body: `{\n\"positions\": {\n  \"source:a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27\": { \"x\": 0, \"y\": 160 },\n  \"endpoint:8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34\": { \"x\": 520, \"y\": 200 }\n}\n}` }}\n>\n  `positions` stores each card's coordinates: keys `platform`, `source:<id>` or `endpoint:<id>`. Cards without a position are placed automatically by the dashboard.\n</Endpoint>"},{"anchor":"alertas","heading":"Alerts","markdown":"The account owner is notified in the dashboard and by e-mail. Each type can be turned off in the notification preferences (webhooks category).\n\n| Type                        | When                                                                                                                                                 |\n| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `webhook_endpoint_failing`  | An endpoint starts failing (first failed attempt after being healthy). Not sent again until a `2xx` response ends the streak.                        |\n| `webhook_endpoint_disabled` | Automatic disabling turns off an endpoint that has been failing for <Limit name=\"webhookDisableAfter\" /> (not when you pause it, nor on `410 Gone`). |\n| `webhook_quota`             | The month's deliveries reach <Limit name=\"webhookQuotaAlerts\" /> of the monthly free quota, once per threshold and month.                            |\n\nAlerts never delay or block a send."},{"anchor":"retries","heading":"Delivery and retries","markdown":"The retry schedule, what counts as success, `Retry-After`, `410 Gone` and the automatic disabling of endpoints that fail for <Limit name=\"webhookDisableAfter\" /> in a row are in the [Webhooks](/en/webhooks#retries) guide. Automatic retries are not charged again; only [Retry message](#webhooks-messages-retry) and [Replay failed deliveries](#webhooks-endpoints-recover) create new deliveries."},{"anchor":"api-legada","heading":"Legacy account API","markdown":"The old account webhook routes (`/v1/account/webhooks`) are **deprecated**: they answer with the `Deprecation: true` header and a `Link` header pointing to the Webhook Relay projects. They keep working, and the endpoints they create live in the default project. Use Webhook Relay for new integrations. The job callback secret route is not deprecated (see [Secrets and rotation](/en/webhooks#secrets))."},{"anchor":"erros","heading":"Errors","markdown":"<ErrorTable tool=\"webhook-relay\" />\n\nCommon validation errors (`validation.failed`, `not_found`) and balance errors (`billing.insufficient_credits`) are in [Errors](/en/erros)."},{"anchor":"limites","heading":"Limits","markdown":"| Limit                                   | Value                                              |\n| --------------------------------------- | -------------------------------------------------- |\n| Free deliveries per month (per account) | <Limit name=\"webhookFreeDeliveries\" />             |\n| `payload` / received body size          | <Limit name=\"webhookPayloadMax\" />                 |\n| Projects per account                    | <Limit name=\"webhookProjectsMax\" />                |\n| Endpoints per account                   | <Limit name=\"webhookEndpointsMax\" />               |\n| Custom headers per endpoint             | <Limit name=\"webhookCustomHeadersMax\" />           |\n| Event types per account                 | <Limit name=\"webhookEventTypesMax\" />              |\n| Messages per batch                      | <Limit name=\"webhookBatchMax\" />                   |\n| Deliveries per recovery                 | <Limit name=\"webhookRecoverMax\" />                 |\n| Sources per account                     | <Limit name=\"webhookSourcesMax\" />                 |\n| Routes per source                       | <Limit name=\"webhookRoutesPerSourceMax\" />         |\n| Event types per route                   | <Limit name=\"webhookRouteEventTypesMax\" />         |\n| Requests received per source            | <Limit name=\"webhookIngestPerMinute\" /> per minute |\n| Wait for credits (`blocked_no_credits`) | <Limit name=\"webhookBlockedRetention\" />           |\n| Message log retention                   | <Limit name=\"webhookLogRetention\" />               |\n| Message body search window              | <Limit name=\"webhookSearchBodyWindow\" />           |\n\nPer-account limits count every project. These are the platform defaults, which the platform may adjust."}]}],"es":[{"slug":"agentes-de-ia","url":"https://docs.respawntech.dev/es/agentes-de-ia","title":"Agentes de IA (MCP)","description":"Conecta Claude, Cursor, VS Code y otros agentes a las herramientas mediante el servidor MCP, con costo visible y límite de gasto por clave.","sections":[{"anchor":"top","heading":"","markdown":"El servidor **MCP** (Model Context Protocol) permite que un agente de IA descubra y use las herramientas de la plataforma, como crear un QR, convertir un PDF o buscar en la base de conocimiento, con tu cuenta y tus créditos. Sigue las mismas reglas que la API: los mismos precios, scopes y cuotas gratuitas.\n\n<Callout title=\"Despliegue gradual\">\n  El servidor MCP se está habilitando de forma gradual. Mientras no esté activo en tu entorno, `/mcp` responde `404` con el código `mcp.disabled`.\n</Callout>"},{"anchor":"conexao","heading":"Conexión","markdown":"|                   |                                                     |\n| ----------------- | --------------------------------------------------- |\n| URL               | <McpUrl />                                          |\n| Transporte        | Streamable HTTP, sin sesión, respuestas JSON        |\n| Autenticación     | `Authorization: Bearer <token de API>`              |\n| Scope obligatorio | `mcp:use`, además de los scopes de cada herramienta |\n\nNunca pongas el token en la URL (`?token=`, `?api_key=`…): el servidor lo rechaza con `400`. Los conectores de productos como Claude.ai y ChatGPT, que piden iniciar sesión en lugar de un token, todavía no son compatibles."},{"anchor":"chave","heading":"Crea una clave para el agente","markdown":"En el [dashboard](https://dashboard.respawntech.dev), en **APIs**, crea un token solo para el agente. Las plantillas rellenan scopes y controles:\n\n* **Agente de soporte**: búsqueda en la base de conocimiento y consulta de saldo.\n* **Agente completo**: todas las herramientas.\n* **Solo lectura**: solo consultas.\n\nUna clave de agente debe tener un **límite diario de gasto** (sugerencia: <Limit name=\"mcpSuggestedDailyLimit\" />) y una **fecha de expiración**. Consulta [Controles de la clave](/es/autenticacao#controles-da-chave)."},{"anchor":"clientes","heading":"Configura el cliente","markdown":"<McpClientSnippets />\n\nLos clientes que solo hablan stdio usan el puente `mcp-remote`, como en el último ejemplo."},{"anchor":"ferramentas","heading":"Herramientas","markdown":"El agente solo ve las herramientas permitidas por los scopes de la clave. Las descripciones que recibe están en inglés por defecto; usa `lang` (abajo) para otro idioma.\n\n<McpToolsTable />\n\nLas herramientas **destructivas** piden confirmación al usuario en la mayoría de los clientes. Las marcadas con dry-run aceptan `dry_run: true` y devuelven solo la estimación de costo."},{"anchor":"parametros","heading":"Parámetros de la conexión","markdown":"Todos son opcionales y van en la query string de la URL. Solo **restringen** lo que permite la clave, nunca lo amplían. Un parámetro desconocido o con un valor inválido responde `400`, para que un error de tipeo no se convierta en \"sin filtro\".\n\n<McpParamsTable />\n\nPor ejemplo, un agente de soporte que solo lee la base de conocimiento, con respuestas en portugués: <McpUrl query=\"toolsets=rag&access=read&lang=pt\" />\n\nLos parámetros que repites en varias conexiones pueden convertirse en un **perfil** en el dashboard (`profile=<slug>`), y una clave puede tener un perfil predeterminado."},{"anchor":"custos","heading":"Costos y control de gasto","markdown":"* Cada llamada cuesta lo mismo que la operación equivalente en la API, y las lecturas medidas cuentan para la misma [cuota diaria gratuita](/es/cobranca-e-limites#franquia-diaria).\n* Cada respuesta incluye el costo en `_meta[\"tech.respawn/cost\"]`: créditos cobrados, estimación (en dry-run), saldo resultante y cuánto queda del límite del día.\n* `simulate=true` en la URL hace que todas las herramientas de pago devuelvan solo la estimación. Es la forma de probar un agente sin gastar.\n* Superar el límite diario de la clave responde `billing.spend_limit_reached`; superar el tope por llamada (`max_call_credits`) responde `billing.call_limit_exceeded`.\n* El **botón de pánico** del dashboard corta al instante el acceso de todos los agentes de la cuenta (`mcp.agents_disabled`).\n* El extracto del dashboard desglosa el gasto por canal (API, MCP, dashboard) y por clave."},{"anchor":"idempotencia","heading":"Idempotencia","markdown":"Los agentes repiten llamadas con frecuencia. Las herramientas que crean o modifican algo aceptan `idempotency_key`; sin él, una llamada idéntica (misma clave, herramienta y argumentos) en menos de <Limit name=\"mcpIdempotencyWindow\" /> devuelve el resultado anterior sin ejecutar ni cobrar de nuevo (`_meta[\"tech.respawn/replayed\"]`)."},{"anchor":"limites","heading":"Límites","markdown":"| Límite                       | Valor                                                                             |\n| ---------------------------- | --------------------------------------------------------------------------------- |\n| Solicitudes                  | <Limit name=\"rateLimitPerMinute\" /> por minuto por clave, sumadas a las de la API |\n| Cuerpo de la solicitud       | <Limit name=\"mcpRequestMax\" />                                                    |\n| Resultado de una herramienta | <Limit name=\"mcpResultMax\" />                                                     |\n| Llamadas a `tools_search`    | <Limit name=\"mcpToolsSearchPerMinute\" /> por minuto                               |"},{"anchor":"erros","heading":"Errores","markdown":"Los errores de negocio vuelven como resultado de la herramienta, con un mensaje que le dice al agente qué hacer y el código en `_meta[\"tech.respawn/error\"]`. Los errores de la conexión vuelven como HTTP, en el formato `{\"error\": {\"code\", \"message\"}}`.\n\n<ErrorTable group=\"agents\" />\n\nLos errores de las herramientas son los mismos que en la API; consulta [Códigos de error](/es/erros)."}]},{"slug":"autenticacao","url":"https://docs.respawntech.dev/es/autenticacao","title":"Autenticación","description":"Cómo autenticarse en las herramientas con token de API, scopes y requisitos de la cuenta.","sections":[{"anchor":"token-de-api","heading":"Token de API","markdown":"Todas las rutas en `/v1/tools/*` requieren un **token de API**, creado en el [dashboard](https://dashboard.respawntech.dev) en **APIs**.\n\n| Forma                         | Ejemplo                              |\n| ----------------------------- | ------------------------------------ |\n| Header dedicado (recomendado) | `X-API-Token: rsk_live_...`          |\n| Authorization                 | `Authorization: ApiKey rsk_live_...` |\n| Authorization (alternativo)   | `Authorization: Token rsk_live_...`  |\n\n<Callout title=\"Buenas prácticas\">\n  Guarda el token en la variable de entorno `RESPAWN_API_TOKEN`, crea un token por integración con los **scopes mínimos** necesarios y revoca los que ya no uses.\n</Callout>"},{"anchor":"requisitos-de-la-cuenta","heading":"Requisitos de la cuenta","markdown":"Antes de cualquier llamada a las herramientas, la cuenta debe tener:\n\n* **Correo electrónico verificado** — de lo contrario, `auth.email_not_verified` (403).\n* **Saldo no negativo** — una cuenta suspendida devuelve `billing.user_bankrupt` (402).\n* **Créditos suficientes** — las operaciones de pago sin saldo devuelven `billing.insufficient_credits` (402)."},{"anchor":"scopes","heading":"Scopes","markdown":"Cada ruta acepta un conjunto de scopes; basta con que el token tenga **uno** de ellos. Si falta un permiso, la API devuelve `scope.missing` (403) con `details.required` y `details.provided`. Los scopes aceptados por cada endpoint aparecen en la tarjeta del endpoint.\n\n<ScopeTable />\n\n<Callout type=\"warn\" title=\"PDF es más restrictivo en la lectura\">\n  Las rutas de lectura de PDF aceptan solo `tools:pdf:read` o `tools:manage`. El scope genérico `tools:read` y `tools:pdf:manage` **no** dan acceso a la lectura de PDF.\n</Callout>"},{"anchor":"combinaciones-recomendadas","heading":"Combinaciones recomendadas","markdown":"| Caso de uso                                    | Scopes sugeridos                                                          |\n| ---------------------------------------------- | ------------------------------------------------------------------------- |\n| Generar y gestionar códigos QR                 | `tools:qr:dynamic:manage`                                                 |\n| Panel de solo lectura de QRs                   | `tools:qr:dynamic:read`                                                   |\n| Pipeline de PDF                                | `tools:pdf:manage` + `tools:pdf:read`                                     |\n| Verificar URLs antes de mostrarlas             | `tools:safety:read`                                                       |\n| Asistente que busca en la base de conocimiento | `tools:rag:query`                                                         |\n| Subir y mantener documentos de la base         | `tools:rag:manage`                                                        |\n| [Agente de IA (MCP)](/es/agentes-de-ia)        | `mcp:use` + los scopes de las herramientas que va a usar + `account:read` |\n| Integración completa                           | `tools:manage` (más simple, menos seguro)                                 |"},{"anchor":"controles-da-chave","heading":"Controles de la clave","markdown":"Además de los scopes, cada token puede tener controles configurados en el dashboard. Se aplican en cualquier canal, en la API y en el [MCP](/es/agentes-de-ia):\n\n* **Expiración**: pasada la fecha, el token deja de funcionar (`auth.unauthorized`).\n* **Límite diario de gasto**: un cobro que superaría el límite del día se rechaza con `billing.spend_limit_reached` (402), con `details.limit`, `details.spent_today` y `details.required`. El contador se reinicia a medianoche UTC.\n* **Restricción por base de conocimiento**: el token solo ve las bases elegidas; las demás responden `not_found`, y no puede crear bases nuevas (`forbidden`).\n* **Plantillas**: al crear la clave, \"Solo lectura\", \"Agente de soporte\" y \"Agente completo\" rellenan scopes y controles de una vez.\n\nLa expiración, el límite y las bases se pueden cambiar después; los scopes, no. Para cambiar los scopes, crea otra clave."},{"anchor":"errores-de-autenticación","heading":"Errores de autenticación","markdown":"<ErrorTable group=\"auth\" />"},{"anchor":"dashboard-vs-token-de-api","heading":"Dashboard vs. token de API","markdown":"* **Dashboard** — la navegación en el panel no consume la cuota diaria gratuita ni debita lecturas medidas.\n* **Token de API** — las lecturas medidas comparten la [cuota diaria gratuita](/es/cobranca-e-limites#franquia-diaria) de <Limit name=\"dailyQuota\" /> solicitudes (<Limit name=\"dailyQuotaPremium\" /> en cuentas premium). Después de eso, cada lectura debita créditos."},{"anchor":"ejemplo","heading":"Ejemplo","markdown":"Crear un QR con dry-run para validar el token, los scopes y el costo sin debitar nada:\n\n<ApiExamples\n  method=\"POST\"\n  path=\"/v1/tools/image/qr-code/dynamic\"\n  headers={{ 'X-Dry-Run': 'true', 'X-Include-Cost': 'true' }}\n  body={`{\n\"name\": \"Campanha\",\n\"target_url\": \"https://example.com\"\n}`}\n/>"}]},{"slug":"cobranca-e-limites","url":"https://docs.respawntech.dev/es/cobranca-e-limites","title":"Cobros y límites","description":"Créditos, dry-run, idempotencia, rate limit, cuota diaria gratuita de lecturas y timeouts.","sections":[{"anchor":"modelo-de-créditos","heading":"Modelo de créditos","markdown":"* El saldo se muestra en **créditos** en el dashboard, y todos los valores de esta documentación están en créditos.\n* Las operaciones de pago se debitan al confirmarse — nunca en el dry-run.\n* Las reglas de precio de cada herramienta están en la página de la herramienta: [QR Dinámico](/es/ferramentas/qr-dinamico#custos), [PDF Convert](/es/ferramentas/pdf-convert#custos), [Base de conocimiento](/es/ferramentas/base-de-conhecimento#custos), [Audio](/es/ferramentas/audio#custos).\n* Cada token puede tener un [límite diario de gasto](/es/autenticacao#controles-da-chave): superarlo responde `billing.spend_limit_reached`.\n\n<Callout title=\"Precio exacto\">\n  Los costos de creación pueden sumar adicionales (slug personalizado, diseño, retención de PDF…). Haz un dry-run con el payload real para ver el total exacto.\n</Callout>"},{"anchor":"dry-run","heading":"Dry-run y vista previa de costo","markdown":"| Header                 | Efecto                                             |\n| ---------------------- | -------------------------------------------------- |\n| `X-Dry-Run: true`      | Valida y calcula el costo sin persistir ni debitar |\n| `X-Preview-Cost: true` | Alias de `X-Dry-Run`                               |\n| `X-Include-Cost: true` | Incluye `meta.cost` en la respuesta                |\n\nEl dry-run se acepta en las operaciones de pago marcadas en la tarjeta de cada endpoint y en las **lecturas medidas** (excepto descargas). La respuesta es `200` y no se persiste ni se debita nada.\n\nLas rutas de [Audio](/es/ferramentas/audio#dry-run) piden el dry-run con `?dry_run=true` en la query, no con el header. El audio se cobra por minuto de audio, por 1.000 caracteres o por segundo generado, y el valor de la transcripción, del habla y de los demás jobs se devuelve si el motor falla."},{"anchor":"presupuesto-de-una-operación-de-pago","heading":"Presupuesto de una operación de pago","markdown":"En los `POST` de creación y conversión, `data` incluye el presupuesto detallado por ítem:\n\n```json\n{\n  \"success\": true,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"data\": {\n    \"total_cost_credits\": 15000,\n    \"total_cost_units\": 15000,\n    \"items\": [\n      { \"description\": { \"key\": \"billing.pdf.convert_base\" }, \"cost_credits\": 10000, \"type\": \"base\" },\n      { \"description\": { \"key\": \"billing.pdf.retention.7\" }, \"cost_credits\": 5000, \"type\": \"storage\" }\n    ],\n    \"is_monthly_recurring\": false,\n    \"currency\": \"CREDITS\"\n  }\n}\n```\n\n<Callout type=\"warn\" title=\"Valores en unidades\">\n  A pesar del nombre, `total_cost_credits` y `cost_credits` del presupuesto (y `cost_credits` de las conversiones de PDF) vienen en **unidades internas**: divide entre 10.000 para obtener créditos. En el ejemplo, 15.000 unidades = 1,5 créditos.\n</Callout>\n\nEn QR Dinámico, el presupuesto también incluye `slot_usage` (uso de slots y si la creación abrirá un nuevo slot de pago) y `recurring_cost`."},{"anchor":"vista-previa-de-una-lectura-medida","heading":"Vista previa de una lectura medida","markdown":"En las lecturas medidas, el dry-run no ejecuta la consulta — solo muestra la posición en la cuota diaria y si la lectura se cobraría:\n\n```json\n{\n  \"success\": true,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"data\": {\n    \"operation\": \"qr_dynamic.analytics\",\n    \"metering\": {\n      \"within_quota\": true,\n      \"quota_limit\": 2000,\n      \"quota_used\": 125,\n      \"quota_remaining\": 1875,\n      \"cost_units\": 300,\n      \"would_charge\": false\n    }\n  },\n  \"meta\": {\n    \"cost\": {\n      \"units\": 300,\n      \"credits\": 0.03,\n      \"charged\": false,\n      \"within_quota\": true,\n      \"quota_limit\": 2000,\n      \"quota_used\": 125,\n      \"quota_remaining\": 1875\n    }\n  }\n}\n```\n\nCon `X-Include-Cost: true` en una lectura real, el mismo objeto `meta.cost` acompaña la respuesta normal (`charged: true` cuando la lectura se cobró)."},{"anchor":"idempotencia","heading":"Idempotencia","markdown":"Envía `X-Idempotency-Key` (se recomienda un UUID) en cualquier ruta de herramientas:\n\n* la clave es válida por **<Limit name=\"idempotencyTtl\" />**;\n* misma clave + mismo payload → se reutiliza la respuesta original, sin un nuevo cobro;\n* misma clave + payload distinto → `idempotency.conflict` (409).\n\nÚsala siempre en los `POST` de creación y conversión para que los reintentos de red no cobren dos veces.\n\nExcepción: las rutas de la [Base de conocimiento](/es/ferramentas/base-de-conhecimento#limites) ignoran el header, porque ya son idempotentes (el contenido repetido no se reindexa ni se cobra)."},{"anchor":"rate-limit","heading":"Rate limit","markdown":"Cada token de API puede hacer **<Limit name=\"rateLimitPerMinute\" /> solicitudes por minuto** en las rutas `/v1/tools/*`. Cada respuesta informa el estado:\n\n```http\nX-RateLimit-Limit: 60\nX-RateLimit-Remaining: 42\nX-RateLimit-Reset: 1712345678\n```\n\nAl excederlo, la API devuelve `rate_limit.exceeded` (429). Espera hasta `X-RateLimit-Reset` antes de reenviar."},{"anchor":"retry","heading":"Reintentar con espera (retry)","markdown":"Reintenta solo lo que puede salir bien en el siguiente intento:\n\n* **`429`**: espera hasta la hora de `X-RateLimit-Reset` (segundos Unix).\n* **`5xx` y fallos de red**: espera cada vez más, con algo de aleatoriedad (*backoff* exponencial con *jitter*): unos 1 s, 2 s, 4 s… hasta 30 s, como máximo 5 intentos.\n* **Demás `4xx`** (validación, scope, saldo): no reintentes. La respuesta no cambia hasta que corrijas la solicitud.\n\nEn un `POST` que crea o cobra algo, envía **la misma** `X-Idempotency-Key` en todos los intentos: si el primero llegó y solo se perdió la respuesta, el reintento devuelve el resultado original sin cobrar de nuevo.\n\n<Tabs groupId=\"code-language\" persist items={['JavaScript', 'Python']}>\n  <Tab value=\"JavaScript\">\n    ```js\n    // Reintenta 429, 5xx y fallos de red; devuelve los demás errores de inmediato.\n    async function withRetry(send, attempts = 5) {\n      for (let attempt = 1; ; attempt++) {\n        let response;\n        try {\n          response = await send();\n        } catch (error) {\n          if (attempt === attempts) throw error; // fallo de red\n        }\n        if (response && response.status !== 429 && response.status < 500) return response;\n        if (attempt === attempts) return response;\n\n        const reset = Number(response?.headers.get('X-RateLimit-Reset'));\n        const wait =\n          response?.status === 429 && reset\n            ? Math.max(0, reset * 1000 - Date.now())\n            : Math.min(2 ** (attempt - 1) * 1000, 30_000) * (0.5 + Math.random());\n        await new Promise((resolve) => setTimeout(resolve, wait));\n      }\n    }\n\n    const key = crypto.randomUUID(); // la misma clave en todos los intentos\n    const response = await withRetry(() =>\n      fetch(url, {\n        method: 'POST',\n        headers: {\n          'X-API-Token': process.env.RESPAWN_API_TOKEN,\n          'X-Idempotency-Key': key,\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify(payload),\n      }),\n    );\n    ```\n  </Tab>\n\n  <Tab value=\"Python\">\n    ```python\n    import os, random, time, uuid\n    import requests\n\n    # Reintenta 429, 5xx y fallos de red; devuelve los demás errores de inmediato.\n    def with_retry(send, attempts=5):\n        for attempt in range(1, attempts + 1):\n            try:\n                response = send()\n            except (requests.ConnectionError, requests.Timeout):\n                if attempt == attempts:\n                    raise\n                response = None\n            if response is not None and response.status_code != 429 and response.status_code < 500:\n                return response\n            if attempt == attempts:\n                return response\n            reset = response.headers.get(\"X-RateLimit-Reset\") if response is not None else None\n            if response is not None and response.status_code == 429 and reset:\n                wait = max(0, int(reset) - time.time())\n            else:\n                wait = min(2 ** (attempt - 1), 30) * (0.5 + random.random())\n            time.sleep(wait)\n\n    key = str(uuid.uuid4())  # la misma clave en todos los intentos\n    response = with_retry(lambda: requests.post(\n        url,\n        json=payload,\n        headers={\"X-API-Token\": os.environ[\"RESPAWN_API_TOKEN\"], \"X-Idempotency-Key\": key},\n        timeout=30,\n    ))\n    ```\n  </Tab>\n</Tabs>"},{"anchor":"franquia-diaria","heading":"Cuota diaria gratuita de lecturas","markdown":"Las lecturas hechas con **token de API** (listados, estado, analytics, descargas…) cuentan para una cuota diaria gratuita compartida entre todas las herramientas:\n\n| Ítem                       | Valor                                  |\n| -------------------------- | -------------------------------------- |\n| Lecturas gratuitas por día | **<Limit name=\"dailyQuota\" />**        |\n| Cuentas premium            | **<Limit name=\"dailyQuotaPremium\" />** |\n| Reinicio                   | <Limit name=\"quotaReset\" />            |\n\nCada lectura medida devuelve la posición en la cuota:\n\n```http\nX-API-Quota-Limit: 2000\nX-API-Quota-Used: 847\nX-API-Quota-Remaining: 1153\n```\n\nLas solicitudes hechas desde el [dashboard](https://dashboard.respawntech.dev) **no** cuentan para la cuota ni se cobran."},{"anchor":"lecturas-medidas","heading":"Lecturas medidas","markdown":"Una vez agotada la cuota, cada lectura vía API debita:\n\n<MeteredReadsTable />\n\nLas operaciones de escritura sin costo propio (`PATCH`, activar, desactivar, `DELETE`) y el dry-run de creación siempre son gratuitos.\n\n<Callout type=\"idea\" title=\"Evita el polling\">\n  La cuota cubre la mayoría de las integraciones. Para seguir cambios de estado, prefiere los [webhooks](/es/webhooks) en lugar de consultar el estado repetidamente.\n</Callout>"},{"anchor":"timeouts","heading":"Timeouts","markdown":"* La mayoría de las rutas: **<Limit name=\"requestTimeout\" />**.\n* Descargas de archivos: **<Limit name=\"downloadTimeout\" />**."}]},{"slug":"erros","url":"https://docs.respawntech.dev/es/erros","title":"Códigos de error","description":"Envelope de respuesta, formato de error y referencia de todos los códigos devueltos por las herramientas.","sections":[{"anchor":"respuesta-exitosa","heading":"Respuesta exitosa","markdown":"```json\n{\n  \"success\": true,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"data\": {},\n  \"meta\": {}\n}\n```\n\n| Campo      | Descripción                                           |\n| ---------- | ----------------------------------------------------- |\n| `success`  | `true` en respuestas correctas                        |\n| `trace_id` | ID de correlación (también en el header `X-Trace-Id`) |\n| `data`     | Recurso o resultado                                   |\n| `meta`     | Opcional — costo (`meta.cost`), paginación, etc.      |\n\nCon `X-Include-Cost: true` o en dry-run, `meta` incluye el costo de la operación — consulta los formatos en [Dry-run](/es/cobranca-e-limites#dry-run). Los listados incluyen la paginación dentro de `data` (`total` y, en PDF, `page` y `page_size`), no en `meta`."},{"anchor":"formato-de-error","heading":"Formato de error","markdown":"```json\n{\n  \"success\": false,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"error\": {\n    \"code\": \"billing.insufficient_credits\",\n    \"message\": \"Créditos insuficientes para esta operação.\",\n    \"details\": { \"required\": 100, \"available\": 50 }\n  }\n}\n```\n\n| Campo           | Descripción                                                                                            |\n| --------------- | ------------------------------------------------------------------------------------------------------ |\n| `error.code`    | Identificador estable — maneja los errores por él, nunca por el `message`                              |\n| `error.message` | Texto legible; puede cambiar entre versiones                                                           |\n| `error.details` | Contexto estructurado opcional; en `validation.failed`, `details.fields` incluye los errores por campo |\n| `trace_id`      | Inclúyelo en los tickets de soporte                                                                    |"},{"anchor":"errores-comunes-a-todas-las-herramientas","heading":"Errores comunes a todas las herramientas","markdown":""},{"anchor":"autenticación-y-permisos","heading":"Autenticación y permisos","markdown":"<ErrorTable group=\"auth\" />"},{"anchor":"créditos-y-cuenta","heading":"Créditos y cuenta","markdown":"<ErrorTable group=\"billing\" />"},{"anchor":"solicitud-y-límites","heading":"Solicitud y límites","markdown":"<ErrorTable group=\"request\" />"},{"anchor":"agentes-de-ia-mcp","heading":"Agentes de IA (MCP)","markdown":"<ErrorTable group=\"agents\" />"},{"anchor":"plataforma","heading":"Plataforma","markdown":"<ErrorTable group=\"platform\" />"},{"anchor":"vídeo","heading":"Vídeo","markdown":"<ErrorTable group=\"video\" />"},{"anchor":"errores-por-herramienta","heading":"Errores por herramienta","markdown":"<ToolErrorSections />\n\nLos jobs asíncronos (PDF) también exponen `error_code` en el propio recurso cuando `status` es `failed`; los códigos marcados como **asíncrono** aparecen solo ahí."},{"anchor":"buenas-prácticas","heading":"Buenas prácticas","markdown":"1. Maneja `402` con un flujo de recarga en tu sistema.\n2. En `429`, respeta `X-RateLimit-Reset` antes de volver a intentar.\n3. Usa `X-Dry-Run` para detectar `validation.failed` sin costo.\n4. Reintenta solo los errores `5xx` y de red, siempre con la misma `X-Idempotency-Key`.\n5. Registra el `trace_id` de toda respuesta con error."}]},{"slug":"","url":"https://docs.respawntech.dev/es","title":"Documentación de la API","description":"Integra las herramientas de Respawn Tech Dev con una API REST predecible — autenticación por token, cobro en créditos y dry-run en todas las operaciones de pago.","sections":[{"anchor":"herramientas","heading":"Herramientas","markdown":"<ToolCards />"},{"anchor":"primeros-pasos","heading":"Primeros pasos","markdown":"<Steps>\n  <Step>\n    ### Crea un token de API\n\n    En el [dashboard](https://dashboard.respawntech.dev), abre **APIs** y crea un token con los scopes de la herramienta que vas a usar. Guárdalo en la variable de entorno `RESPAWN_API_TOKEN`. Consulta [Autenticación](/es/autenticacao).\n  </Step>\n\n  <Step>\n    ### Estima el costo con dry-run\n\n    Envía `X-Dry-Run: true` antes de una operación de pago para validar el payload y ver el costo sin debitar créditos. Consulta [Cobros y límites](/es/cobranca-e-limites#dry-run).\n  </Step>\n\n  <Step>\n    ### Haz la llamada\n\n    Todas las rutas de herramientas usan el prefijo `https://api.respawntech.dev/v1/tools`. Cada endpoint incluye ejemplos listos en cURL, JavaScript y Python.\n\n    <ApiExamples method=\"GET\" path=\"/v1/tools/image/qr-code/dynamic\" query={{ limit: '20' }} />\n  </Step>\n</Steps>\n\n## Headers comunes\n\n| Header              | Valor          | Efecto                                                                |\n| ------------------- | -------------- | --------------------------------------------------------------------- |\n| `X-API-Token`       | `rsk_live_...` | Autenticación (recomendado)                                           |\n| `X-Dry-Run`         | `true`         | Simula la operación sin persistir ni debitar                          |\n| `X-Include-Cost`    | `true`         | Incluye `meta.cost` en la respuesta                                   |\n| `X-Idempotency-Key` | UUID           | Evita cobros duplicados en reenvíos (<Limit name=\"idempotencyTtl\" />) |\n\nLas respuestas incluyen `X-Trace-Id` (úsalo en tickets de soporte), `X-RateLimit-*` y, en lecturas medidas, `X-API-Quota-*`.\n\n## Guías\n\n<Cards>\n  <Card title=\"Autenticación\" description=\"Token de API, scopes y requisitos de la cuenta.\" href=\"/es/autenticacao\" />\n\n  <Card title=\"Cobros y límites\" description=\"Créditos, dry-run, idempotencia, rate limit y cuota diaria gratuita.\" href=\"/es/cobranca-e-limites\" />\n\n  <Card title=\"Webhooks\" description=\"Eventos de la cuenta, firma Standard Webhooks, reintentos y callbacks de jobs.\" href=\"/es/webhooks\" />\n\n  <Card title=\"Códigos de error\" description=\"Envelope de respuesta y todos los códigos por herramienta.\" href=\"/es/erros\" />\n\n  <Card title=\"Agentes de IA (MCP)\" description=\"Conecta Claude, Cursor y otros agentes a las herramientas, con límite de gasto.\" href=\"/es/agentes-de-ia\" />\n</Cards>"}]},{"slug":"usar-com-ia","url":"https://docs.respawntech.dev/es/usar-com-ia","title":"Usar con IA","description":"Da a tu asistente de IA acceso a esta documentación con el MCP de documentación (público, gratis, sin token), con llms.txt o con las páginas en Markdown.","sections":[{"anchor":"top","heading":"","markdown":"Los asistentes de IA escriben mejores integraciones cuando consultan la documentación en lugar de adivinar. Hay tres formas de entregar estas páginas a un agente, de la más completa a la más simple: el **MCP de documentación**, el **llms.txt** y las **páginas en Markdown**."},{"anchor":"dois-servidores","heading":"Dos servidores MCP","markdown":"|               | MCP de documentación                                | [MCP de la cuenta](/es/agentes-de-ia)                                                   |\n| ------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------- |\n| Para qué      | Responder preguntas sobre la API mientras programas | Usar las herramientas con tu cuenta (crear un QR, convertir un PDF, transcribir audio…) |\n| URL           | <McpUrl server=\"docs\" />                            | <McpUrl />                                                                              |\n| Autenticación | Ninguna                                             | Token de API con `mcp:use`                                                              |\n| Costo         | Gratis                                              | El mismo que la API                                                                     |\n| Acceso        | Solo lectura de la documentación pública            | Actúa en la cuenta, con alcances y límite de gasto                                      |\n\nLos dos pueden estar conectados a la vez: un agente consulta la documentación y después usa el MCP de la cuenta para ejecutar.\n\n<Callout title=\"Habilitación gradual\">\n  El MCP de documentación se está habilitando de forma gradual. Mientras no esté activo, <McpUrl server=\"docs\" /> responde `404` con el código `mcp.disabled`. El llms.txt y las páginas en Markdown ya funcionan.\n</Callout>"},{"anchor":"conexao","heading":"Conexión","markdown":"|               |                                                                           |\n| ------------- | ------------------------------------------------------------------------- |\n| URL           | <McpUrl server=\"docs\" />                                                  |\n| Transporte    | Streamable HTTP                                                           |\n| Autenticación | Ninguna (no envíes token)                                                 |\n| Costo         | Gratis                                                                    |\n| Límite        | <Limit name=\"mcpDocsRateLimitPerMinute\" /> solicitudes por minuto por IP  |\n| Idioma        | Parámetro `lang` opcional en cada herramienta: `pt-br`, `en`, `es` o `fr` |\n\nLas respuestas salen del mismo contenido de este sitio, incluidos los precios vigentes."},{"anchor":"clientes","heading":"Configura el cliente","markdown":"<McpDocsClientSnippets />\n\nEn **Claude.ai** y **ChatGPT**, añade un conector personalizado (custom connector) e indica solo la URL <McpUrl server=\"docs\" />. No hace falta login ni token. Los clientes que solo hablan stdio usan el puente `mcp-remote`, como en el último ejemplo."},{"anchor":"ferramentas","heading":"Herramientas","markdown":"<McpDocsToolsTable />\n\nUn flujo típico: `docs_search` con la pregunta, `docs_get_page` en la sección encontrada y `docs_get_endpoint` o `docs_get_pricing` para los detalles exactos antes de escribir el código."},{"anchor":"llms-txt","heading":"Sin MCP: llms.txt y Markdown","markdown":"Para agentes que solo leen la web:\n\n* [`/llms.txt`](/llms.txt): índice en el formato [llmstxt.org](https://llmstxt.org), con cada página en cada idioma y el enlace a su versión en Markdown.\n* [`/llms-full.txt`](/llms-full.txt): todas las páginas en un solo archivo de texto.\n* Páginas en Markdown: `/md/<página>.md` en portugués y `/md/<idioma>/<página>.md` en los demás idiomas, por ejemplo [`/md/webhooks.md`](/md/webhooks.md) o [`/md/es/webhooks.md`](/md/es/webhooks.md). La página de inicio es `/md/index.md`.\n* [`/ai/bundle.json`](/ai/bundle.json): toda la documentación en JSON estructurado (secciones, herramientas, endpoints, precios, errores, alcances y límites). Es la base del MCP de documentación."}]},{"slug":"webhooks","url":"https://docs.respawntech.dev/es/webhooks","title":"Webhooks","description":"Cómo envía webhooks la plataforma — eventos de la cuenta, callbacks de jobs y Webhook Relay — y cómo validar la firma Standard Webhooks.","sections":[{"anchor":"top","heading":"","markdown":"La plataforma hace un `POST` a tu URL cuando ocurre algo. Todos los envíos pasan por el mismo motor de entrega: el mensaje se guarda, cada destino recibe una entrega con sus propios reintentos y cada llamada HTTP queda registrada. Hay tres orígenes:\n\n| Origen                                                 | Dónde se configura                                                | Costo                                             |\n| ------------------------------------------------------ | ----------------------------------------------------------------- | ------------------------------------------------- |\n| **Eventos de la cuenta** (p. ej. `qr.dynamic.scanned`) | **Webhooks** en el [dashboard](https://dashboard.respawntech.dev) | Gratis                                            |\n| **Callbacks de jobs** (p. ej. conversión de PDF)       | `webhook_url` en cada solicitud                                   | Gratis                                            |\n| **Webhook Relay** (tus propios eventos)                | API de [Webhook Relay](/es/ferramentas/webhook-relay)             | Por entrega más allá de la cuota mensual gratuita |"},{"anchor":"eventos","heading":"Eventos de la cuenta","markdown":"Registra un endpoint en **Webhooks** en el [dashboard](https://dashboard.respawntech.dev) y elige los eventos. Cada endpoint tiene su `secret`, que se muestra al crearlo y después con la opción de revelar el secret.\n\nEstos endpoints son de [Webhook Relay](/es/ferramentas/webhook-relay#projetos): un endpoint de cualquier proyecto suscrito a un evento de la cuenta lo recibe, gratis. La antigua API de webhooks de la cuenta está obsoleta; consulta [API heredada de la cuenta](/es/ferramentas/webhook-relay#api-legada).\n\n<WebhookEventsTable />"},{"anchor":"requisicao","heading":"Solicitud enviada","markdown":"```http\nPOST <url del endpoint>\nContent-Type: application/json\nUser-Agent: RespawnTech-Webhook/2.0\nwebhook-id: msg_2Vf8tK1pQ9zX\nwebhook-timestamp: 1790338000\nwebhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=\nX-Webhook-Signature: sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd\n```\n\n| Header                | Contenido                                                                                               |\n| --------------------- | ------------------------------------------------------------------------------------------------------- |\n| `webhook-id`          | Identificador del mensaje. Es el **mismo en todos los reintentos**: úsalo para descartar duplicados.    |\n| `webhook-timestamp`   | Momento de este intento, en segundos Unix.                                                              |\n| `webhook-signature`   | `v1,<base64>`, una firma por secret activo, separadas por espacio (dos durante una rotación de secret). |\n| `X-Webhook-Signature` | Firma heredada `sha256=<hex>`, sin timestamp. Se mantiene para receptores antiguos.                     |\n\nLos headers siguen el estándar [Standard Webhooks](https://www.standardwebhooks.com), así que cualquier biblioteca de ese estándar valida la solicitud. Los endpoints de Webhook Relay pueden llevar headers extra (`custom_headers`)."},{"anchor":"corpo","heading":"Cuerpo","markdown":"Los eventos de la cuenta y los mensajes de Webhook Relay usan el mismo sobre:\n\n```json\n{\n  \"event_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"type\": \"qr.dynamic.scanned\",\n  \"timestamp\": 1790338000,\n  \"payload\": {\n    \"resource_id\": \"…\",\n    \"slug\": \"promo-verao\",\n    \"ip\": \"hash_anonimo\",\n    \"user_agent\": \"Mozilla/5.0 …\",\n    \"referer\": \"https://…\"\n  }\n}\n```\n\nEl formato de `payload` depende del `type`. Los callbacks de jobs mantienen su propio cuerpo (consulta [Callback de jobs](#pdf-callback)), y los mensajes recibidos de proveedores por Webhook Relay llegan con el cuerpo original del proveedor, sin cambios."},{"anchor":"verificar-assinatura","heading":"Validar la firma","markdown":"El contenido firmado es `webhook-id + \".\" + webhook-timestamp + \".\" + cuerpo en bruto`, con HMAC-SHA256 y el resultado en base64. Para validar:\n\n1. Lee el **cuerpo en bruto**, antes de cualquier parseo de JSON (reformatear el JSON cambia la firma).\n2. Rechaza timestamps con más de <Limit name=\"webhookSignatureTolerance\" /> de diferencia con tu reloj (protección contra replay).\n3. Calcula la firma y compárala, **en tiempo constante**, con cada elemento de `webhook-signature`. Basta con que coincida uno.\n\nCon una biblioteca Standard Webhooks, usa el `standard_webhooks_secret` (formato `whsec_…`). Sin biblioteca, la clave del HMAC son los bytes del `secret` en bruto.\n\n<Tabs groupId=\"code-language\" persist items={['JavaScript', 'Python', 'Go']}>\n  <Tab value=\"JavaScript\">\n    ```js\n    // npm i standardwebhooks\n    import express from 'express';\n    import { Webhook } from 'standardwebhooks';\n\n    const wh = new Webhook(process.env.WEBHOOK_SECRET); // whsec_…\n    const app = express();\n\n    app.post('/hook', express.raw({ type: '*/*' }), (req, res) => {\n      let event;\n      try {\n        event = wh.verify(req.body, req.headers); // lanza un error si no es válido\n      } catch {\n        return res.sendStatus(401);\n      }\n      // procesa `event` de forma asíncrona y responde rápido\n      res.sendStatus(204);\n    });\n    ```\n  </Tab>\n\n  <Tab value=\"Python\">\n    ```python\n    # pip install standardwebhooks\n    import os\n    from flask import Flask, request\n    from standardwebhooks.webhooks import Webhook\n\n    wh = Webhook(os.environ[\"WEBHOOK_SECRET\"])  # whsec_…\n    app = Flask(__name__)\n\n    @app.post(\"/hook\")\n    def hook():\n        try:\n            event = wh.verify(request.get_data(), dict(request.headers))\n        except Exception:\n            return \"\", 401\n        # procesa `event` de forma asíncrona y responde rápido\n        return \"\", 204\n    ```\n  </Tab>\n\n  <Tab value=\"Go\">\n    ```go\n    // Sin biblioteca: secret es el valor en bruto (no el whsec_).\n    func verify(secret string, r *http.Request, body []byte) bool {\n    \tid, ts := r.Header.Get(\"webhook-id\"), r.Header.Get(\"webhook-timestamp\")\n    \tt, err := strconv.ParseInt(ts, 10, 64)\n    \tif err != nil || math.Abs(time.Since(time.Unix(t, 0)).Seconds()) > 300 {\n    \t\treturn false\n    \t}\n    \tmac := hmac.New(sha256.New, []byte(secret))\n    \tmac.Write([]byte(id + \".\" + ts + \".\"))\n    \tmac.Write(body)\n    \twant := \"v1,\" + base64.StdEncoding.EncodeToString(mac.Sum(nil))\n    \tfor _, sig := range strings.Fields(r.Header.Get(\"webhook-signature\")) {\n    \t\tif hmac.Equal([]byte(sig), []byte(want)) {\n    \t\t\treturn true\n    \t\t}\n    \t}\n    \treturn false\n    }\n    ```\n  </Tab>\n</Tabs>\n\n<Callout title=\"Receptores antiguos\">\n  Quien ya valida `X-Webhook-Signature` sigue funcionando: es `sha256=` + el HMAC-SHA256 en hexadecimal del cuerpo en bruto, con el `secret` en bruto como clave. Este header no tiene timestamp; prefiere `webhook-signature` en integraciones nuevas.\n</Callout>"},{"anchor":"secrets","heading":"Secrets y rotación","markdown":"| Destino                          | Dónde obtener el secret                                                                                                                                   |\n| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Endpoint de eventos de la cuenta | Al crearlo, o con la opción de revelar el secret en **Webhooks** del dashboard.                                                                           |\n| Endpoint de Webhook Relay        | Al crearlo, o con [Revelar secret](/es/ferramentas/webhook-relay#webhooks-endpoints-secret).                                                              |\n| Callback de jobs (`webhook_url`) | Secret de firma de la cuenta, único para todos los callbacks: `GET /v1/account/webhooks/signing-secret` (ruta de la cuenta, con la sesión del dashboard). |\n\nTodo secret viene en dos formas: `secret` (en bruto) y `standard_webhooks_secret` (`whsec_` + base64 de los mismos bytes). Ambas validan las mismas firmas.\n\nAl [rotar el secret](/es/ferramentas/webhook-relay#webhooks-endpoints-rotate_secret) de un endpoint, el anterior sigue firmando durante <Limit name=\"webhookSecretOverlap\" />: en ese periodo `webhook-signature` trae las dos firmas y actualizas el receptor sin perder eventos."},{"anchor":"retries","heading":"Entrega y reintentos","markdown":"Solo una respuesta **2xx** cuenta como éxito. Las redirecciones (3xx) no se siguen, y los 3xx, 4xx, 5xx, timeouts y errores de red se reintentan con este calendario:\n\n| Intento | Espera antes |\n| ------- | ------------ |\n| 1       | —            |\n| 2       | 5 segundos   |\n| 3       | 5 minutos    |\n| 4       | 30 minutos   |\n| 5       | 2 horas      |\n| 6       | 5 horas      |\n| 7 y 8   | 10 horas     |\n\nSon unas 27 horas en total, con una variación de ±10%. Casos especiales:\n\n* **`429` y `503`** con `Retry-After`: el siguiente intento respeta el header cuando pide más tiempo (hasta 10 horas).\n* **`410 Gone`**: la entrega se detiene y el endpoint se desactiva.\n* Tras el último intento, la entrega queda `exhausted`. En Webhook Relay puedes [reenviar el mensaje](/es/ferramentas/webhook-relay#webhooks-messages-retry) o [recuperar los fallos de un endpoint](/es/ferramentas/webhook-relay#webhooks-endpoints-recover).\n\nBuenas prácticas en el receptor:\n\n* Responde **2xx** lo antes posible y procesa el evento de forma asíncrona.\n* **Descarta duplicados por `webhook-id`**: una entrega puede llegar más de una vez (p. ej., tu servidor la procesó pero la respuesta se perdió).\n* No dependas del orden de llegada; usa el `timestamp` del cuerpo cuando el orden importe."},{"anchor":"desativacao","heading":"Desactivación automática","markdown":"Un endpoint que falla sin parar durante <Limit name=\"webhookDisableAfter\" /> se desactiva automáticamente (`is_active: false`, con `disabled_reason`), igual que uno que responde `410`. Después de corregir el receptor, reactiva el endpoint (en el dashboard o con `is_active: true` en Webhook Relay); eso reinicia la racha de fallos.\n\nEl historial de mensajes, entregas e intentos se guarda durante <Limit name=\"webhookLogRetention\" />."},{"anchor":"pdf-callback","heading":"Callback de jobs","markdown":"En `POST /convert` o `POST /batch` de [PDF Convert](/es/ferramentas/pdf-convert), envía `webhook_url` en el body. Cuando el job termina, la API hace un `POST` a esa URL:\n\n```json\n{\n  \"job_id\": \"job_abc123\",\n  \"success\": true,\n  \"result_url\": \"https://…\",\n  \"metadata\": { \"conversion_id\": \"…\" }\n}\n```\n\nEn caso de fallo, `success` es `false` y el campo `error` trae el mensaje. El callback tiene los mismos headers, firma y reintentos que los demás webhooks, firmado con el **secret de firma de la cuenta** (consulta [Secrets y rotación](#secrets)). La `webhook_url` debe ser una URL pública `http` o `https`; si no, la solicitud se rechaza con `webhook.invalid_url`."},{"anchor":"webhook-relay","heading":"Tus propios eventos","markdown":"Para enviar webhooks de **tu** aplicación a tus clientes, con la misma firma, reintentos y registro, o para recibir webhooks de Stripe, GitHub y otros y redistribuirlos, usa [Webhook Relay](/es/ferramentas/webhook-relay)."}]},{"slug":"ferramentas/audio","url":"https://docs.respawntech.dev/es/ferramentas/audio","title":"Audio","description":"Transcripción, voz sintética, efectos y música, análisis, conversión, resumen, traducción y revoice de audio, por REST o MCP, con jobs asíncronos y precio por minuto.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"audio\" />"},{"anchor":"visao-geral","heading":"Visión general","markdown":"Toda operación de audio es **asíncrona** y se cobra al instante:\n\n1. `POST` a la operación → **202 Accepted** con `job_id`, `status: pending` y `poll_after_seconds`.\n2. Un worker procesa en segundo plano.\n3. Consulta `GET /jobs/{id}` hasta que `status` sea `completed` (o `failed`, y el cobro se devuelve). Al terminar, la respuesta trae el resultado y un **enlace firmado** que caduca.\n\nLos mismos casos de uso están en el [MCP](/es/agentes-de-ia) como las tools `audio_*` (toolset `audio`); REST y MCP comparten formatos, precios y límites.\n\n<Callout type=\"warn\" title=\"Cobro al aceptar el job\">\n  Los créditos se debitan cuando el job es aceptado. Los fallos del motor, las etapas que no hicieron falta y el precio calculado de más se **devuelven** automáticamente; no se cobra nada si el pedido se rechaza antes (`audio.too_long`, `audio.price_above_max`…).\n</Callout>\n\n<EndpointList tool=\"audio\" />"},{"anchor":"entrada","heading":"Entrada del audio","markdown":"Las operaciones que reciben un audio piden **exactamente uno** de estos campos:\n\n| Campo          | Qué es                                                                                                                                                                              |\n| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `audio_url`    | URL pública `http(s)` del archivo. Las direcciones de red interna o privada se rechazan (`audio.input_blocked`) y la descarga tiene un límite de <Limit name=\"audioFetchTimeout\" /> |\n| `audio_base64` | El audio inline en base64, hasta <Limit name=\"audioInlineMax\" /> decodificado                                                                                                       |\n| `file_id`      | Id de un archivo de audio ya subido a la cuenta                                                                                                                                     |\n\nFormatos aceptados: mp3, wav, ogg, m4a, flac y webm. El límite es de <Limit name=\"audioMaxDuration\" /> de audio y <Limit name=\"audioInputMax\" /> por archivo por llamada. En `summarize` y `translate` puedes enviar, en lugar del audio, el `job_id` de una transcripción ya lista: la transcripción se reutiliza y solo se cobra la operación nueva.\n\nTodas las rutas de operación aceptan `max_credits` (en unidades; 10.000 unidades = 1 crédito): si el precio lo supera, el pedido se rechaza con `audio.price_above_max` y no se cobra nada. El header `Idempotency-Key` hace segura la repetición: la misma clave devuelve el primer job en lugar de cobrar de nuevo."},{"anchor":"custos","heading":"Costos","markdown":"<PricingTable tool=\"audio\" group=\"speech-to-text\" />\n\n<PricingTable tool=\"audio\" group=\"text-to-speech\" />\n\n<PricingTable tool=\"audio\" group=\"processing\" />\n\n<PricingTable tool=\"audio\" group=\"revoice\" />\n\nEl \"minuto iniciado\" se redondea hacia arriba. Un audio cuya duración no se puede leer del encabezado se cobra por el tope de <Limit name=\"audioMaxDuration\" /> y la diferencia vuelve al final del job. Las consultas a los jobs siguen la [franquicia diaria](/es/cobranca-e-limites#franquia-diaria). El precio exacto de una llamada sale del [dry-run](#dry-run)."},{"anchor":"dry-run","heading":"Dry-run","markdown":"Con `?dry_run=true` en la query (en las rutas de audio es la query, no el header `X-Dry-Run`), las operaciones de pago responden `200` con el precio y no cobran ni crean job:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"service_code\": \"audio_transcribe_minute\",\n    \"units\": 3,\n    \"rate\": 15000,\n    \"total_units\": 45000,\n    \"duration_ms\": 125000,\n    \"duration_known\": true,\n    \"parts\": [{ \"service_code\": \"audio_transcribe_minute\", \"units\": 3, \"rate\": 15000, \"total\": 45000 }]\n  }\n}\n```\n\n`rate` y `total_units` están en unidades (10.000 unidades = 1 crédito); `parts` detalla cada servicio cobrado. En `speak`, el presupuesto trae `chars`, `voice`, `language` y `format`."},{"anchor":"operaciones","heading":"Operaciones","markdown":""},{"anchor":"transcribir-audio","heading":"Transcribir audio","markdown":"<Endpoint id=\"audio.transcribe\" example={{ body: `{ \"audio_url\": \"https://example.com/episodio-12.mp3\", \"format\": \"text\", \"language\": \"es\" }` }}>\n  `format`: `text` (por defecto), `srt` o `vtt`. `language` es opcional (código ISO 639-1; sin él se detecta el idioma). Con `diarize: true` (o `speakers`, el número conocido de hablantes) cada fragmento dice quién habló (`Speaker 1`, `Speaker 2`…), con un adicional por minuto.\n</Endpoint>"},{"anchor":"generar-voz-a-partir-de-texto","heading":"Generar voz a partir de texto","markdown":"<Endpoint id=\"audio.speak\" example={{ body: `{ \"text\": \"¡Hola! Tu pedido salió a entrega.\", \"voice\": \"pt-BR-dora\", \"format\": \"mp3\", \"speed\": 1 }` }}>\n  `text` acepta hasta <Limit name=\"audioTtsCharsMax\" /> caracteres (por encima, el pedido se rechaza antes de cobrar). `voice` es un id del [catálogo](#vozes-do-catalogo) o una voz clonada de la cuenta (`vc_…`, [solo en MCP](#vozes-clonadas)); sin `voice`, vale la voz por defecto de `language` (`pt-BR`, `en-US`, `en-GB`, `es-ES`, `fr-FR` o `it-IT`). `format`: `mp3` (por defecto) o `wav`; `speed`: 0,5 a 2.\n</Endpoint>"},{"anchor":"vozes-do-catalogo","heading":"Listar las voces","markdown":"<Endpoint id=\"audio.voices\" example={{ query: { language: 'pt-BR' } }}>\n  Catálogo cerrado de voces de `speak` y de `revoice`. `language` filtra por idioma. No se cobra.\n</Endpoint>"},{"anchor":"generar-efecto-de-sonido-o-música","heading":"Generar efecto de sonido o música","markdown":"<Endpoint id=\"audio.generate\" example={{ body: `{ \"kind\": \"effect\", \"prompt\": \"puerta de madera crujiendo despacio\", \"duration_seconds\": 4, \"format\": \"mp3\" }` }}>\n  `kind`: `effect` (1 a <Limit name=\"audioEffectMax\" />, por defecto 5 s) o `music` (instrumental, 1 a <Limit name=\"audioMusicMax\" />, por defecto 30 s). El audio sale con exactamente `duration_seconds`. No hay habla ni canto, y las descripciones que imitan artistas, voces u obras existentes se rechazan antes de cobrar (`audio.input_blocked`). `seed` repite un sonido: el mismo pedido con la `seed` que informó un job genera el mismo audio. La generación tarda unos minutos.\n</Endpoint>"},{"anchor":"analizar-audio","heading":"Analizar audio","markdown":"<Endpoint id=\"audio.analyze\" example={{ body: `{ \"audio_url\": \"https://example.com/episodio-12.mp3\" }` }}>\n  Sin IA de transcripción: devuelve en `analysis` la duración, el formato y códec, bitrate, frecuencia de muestreo, canales, loudness integrado (LUFS), pico real, rango de loudness, los tramos de silencio (`start_ms`/`end_ms`) y el idioma hablado. Precio fijo por llamada.\n</Endpoint>"},{"anchor":"convertir-o-normalizar-audio","heading":"Convertir o normalizar audio","markdown":"<Endpoint id=\"audio.convert\" example={{ body: `{ \"audio_url\": \"https://example.com/episodio-12.wav\", \"format\": \"mp3\", \"bitrate_kbps\": 128, \"normalize\": true }` }}>\n  `format` (obligatorio): `mp3`, `wav`, `ogg`, `m4a` o `flac`. `bitrate_kbps` (32 a 320) vale solo para formatos con pérdida. `normalize: true` nivela el loudness en -14 LUFS (estándar de streaming, -1,5 dBTP). Las etiquetas de metadatos se eliminan.\n</Endpoint>"},{"anchor":"resumir-audio","heading":"Resumir audio","markdown":"<Endpoint id=\"audio.summarize\" example={{ body: `{ \"job_id\": \"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64\", \"output_language\": \"es\" }` }}>\n  Envía el `job_id` de una transcripción lista (el audio no se procesa de nuevo) o un audio nuevo (se transcribe primero y se cobra junto). El resultado llega en `summary`: el resumen y los temas en orden (`title`, `start_ms`, `points`), más un Markdown para descargar. El texto de la transcripción (nunca el audio) se envía al modelo de lenguaje de la plataforma.\n</Endpoint>"},{"anchor":"traducir-audio","heading":"Traducir audio","markdown":"<Endpoint id=\"audio.translate\" example={{ body: `{ \"job_id\": \"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64\", \"target_language\": \"en\", \"format\": \"srt\" }` }}>\n  `target_language` es obligatorio. La traducción sale como texto, SRT o VTT con los tiempos originales (y los hablantes, si la transcripción usó `diarize`); `source_language` informa de qué idioma viene. No genera audio doblado: para eso, usa [revoice](#revoice).\n</Endpoint>"},{"anchor":"revoice","heading":"Volver a hablar la grabación con otra voz","markdown":"<Endpoint id=\"audio.revoice\" example={{ body: `{ \"audio_url\": \"https://example.com/aviso.mp3\", \"voice_id\": \"pt-BR-alex\", \"format\": \"mp3\" }` }}>\n  En un solo job: transcribe la grabación, traduce el texto cuando la voz habla otro idioma y sintetiza el resultado. Indica `voice_id` (del catálogo) o `language` (usa su voz por defecto). El audio nuevo no mantiene el tiempo ni el ritmo del original (sin sincronía labial), y solo valen voces del catálogo o clonadas de tu propia cuenta: nada de imitar a una persona real. El resultado trae el texto hablado (`transcript` y `text_download_url`) y lo que costó cada etapa en `revoice.stages`.\n</Endpoint>"},{"anchor":"listar-jobs","heading":"Listar jobs","markdown":"<Endpoint id=\"audio.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  Los jobs más recientes primero, con `limit` (por defecto 20, hasta <Limit name=\"pageSizeMax\" />) y `offset`. La lista trae solo el estado; el resultado y el enlace están en `GET /jobs/{id}`. Los jobs y archivos se borran después de la retención (<Limit name=\"audioRetention\" />).\n</Endpoint>"},{"anchor":"resultado","heading":"Estado, resultado y descarga","markdown":"<Endpoint id=\"audio.get\">\n  No hay una ruta de descarga aparte: el resultado y el archivo vienen de este endpoint. Con `status: completed`, la respuesta trae el contenido según el tipo de job — `transcript` (también inline hasta 32 KB; `truncated: true` avisa del corte) para transcripción y traducción, `summary`, `analysis`, `conversion`, `generation` o `revoice` — y `download_url` con `download_expires_at`. El enlace está firmado y caduca: consulta de nuevo para recibir uno nuevo. En `failed`, `error_code` explica el motivo y el cobro se devuelve. La lectura se mide en la [franquicia diaria](/es/cobranca-e-limites#franquia-diaria).\n\n  `status`: `pending`, `processing`, `completed` o `failed`.\n</Endpoint>"},{"anchor":"vozes-clonadas","heading":"Voces clonadas y consentimiento (solo MCP)","markdown":"Mientras no exista una ruta REST, la clonación de voz está **solo en las tools MCP** `voice_create`, `voice_list` y `voice_delete` (scope `tools:audio:speak`). La voz clonada sirve solo a la cuenta que la creó, en `audio_speak` (`voice: \"vc_…\"`, precio de voz clonada) y en `audio_revoice` (`voice_id`).\n\n* **`voice_create`** clona la voz **del titular de la cuenta** a partir de una muestra de <Limit name=\"audioVoiceSample\" />. El consentimiento es obligatorio: llama primero con `dry_run` para leer `consent_terms`, `consent_version` y `consent_phrase`; graba la muestra **tú solo, leyendo la frase en voz alta**; luego llama con `consent.accepted: true` y la `consent_version`. Las muestras sin la frase, con más de una voz o fuera de la duración se rechazan y el cobro se devuelve (`audio.voice_sample_rejected`). Sin la aceptación, la respuesta es `audio.consent_required`. Es asíncrono: sigue con `voice_list` hasta que la voz quede `completed`.\n* **`voice_list`** lista las voces de la cuenta (`voice_id`, nombre, idioma, `status`, vencimiento). Gratis.\n* **`voice_delete`** borra la voz de verdad: la muestra cifrada y el registro de consentimiento se destruyen y la voz deja de funcionar al instante (los jobs en cola con ella fallan y se devuelven). Gratis."},{"anchor":"uso-responsavel","heading":"Uso responsable","markdown":"* **Solo tu voz, con tu consentimiento.** Clonar la voz de otra persona, aunque tengas su grabación, no está permitido; la verificación de la frase hablada y de un único hablante existe para impedirlo.\n* **Nada de imitación.** Ni el habla, ni el `revoice`, ni la generación de sonidos aceptan imitar artistas, voces u obras existentes.\n* **Límites:** hasta <Limit name=\"audioVoicesMax\" /> voces clonadas por cuenta, <Limit name=\"audioVoiceCreatesPerDay\" /> creaciones por día y <Limit name=\"audioVoiceUsesPerHour\" /> usos por hora (`audio.voice_limit`, `audio.voice_rate_limited`).\n* **Datos guardados:** la muestra permanece cifrada hasta <Limit name=\"audioVoiceSampleRetention\" /> o hasta `voice_delete`; cada creación, rechazo, uso y eliminación se audita, sin contenido de voz. El audio de entrada y de salida de los demás jobs caduca en <Limit name=\"audioRetention\" />.\n* **Avisa a quien escucha:** si publicas audio sintético, di que la voz es generada, sobre todo cuando sea una voz clonada."},{"anchor":"scopes","heading":"Scopes","markdown":"| Scope                    | Operaciones                                                                              |\n| ------------------------ | ---------------------------------------------------------------------------------------- |\n| `tools:audio:transcribe` | `transcribe`, `summarize`, `translate`                                                   |\n| `tools:audio:speak`      | `speak`, `generate`, `revoice` (en MCP, también `voice_create` y `voice_delete`)         |\n| `tools:audio:process`    | `analyze`, `convert`                                                                     |\n| `tools:audio:read`       | `voices`, `jobs`, `jobs/{id}` (los tres scopes anteriores también leen sus propios jobs) |\n\n`tools:manage` cubre todo. La lista completa está en [Autenticación](/es/autenticacao)."},{"anchor":"erros","heading":"Errores","markdown":"`audio.*` en la respuesta o, en jobs que fallan después de aceptados, en el `error_code` del job.\n\n<ErrorTable tool=\"audio\" />"}]},{"slug":"ferramentas/base-de-conhecimento","url":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento","title":"Base de conocimiento","description":"Búsqueda híbrida (semántica + palabras clave) en tus documentos, con rerank y feedback de relevancia, para RAG y asistentes de IA.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"base-de-conhecimento\" />"},{"anchor":"visión-general","heading":"Visión general","markdown":"Una **base** (collection) guarda documentos de texto o Markdown. Cada documento se divide en **secciones** (por los títulos) y **fragmentos** (trozos de \\~96 tokens), que se indexan de dos formas: por significado (embeddings) y por palabras clave. La búsqueda combina ambas y devuelve los fragmentos más relevantes, listos para pasarlos a un modelo de lenguaje (RAG) o mostrarlos al usuario.\n\n<Steps>\n  <Step>\n    **Crea una base** con el idioma de los documentos (mejora la búsqueda por palabras clave).\n  </Step>\n\n  <Step>\n    **Alimenta la base.** Envía texto o Markdown, importa archivos (PDF, Office, imágenes) o conecta una fuente que se sincroniza sola (sitio web, sitemap, repositorio de GitHub, carpeta de Google Drive). La indexación es asíncrona: sigue el `status` hasta `ready`. Reenviar con el mismo `external_id` reemplaza la versión anterior sin dejar la base fuera de servicio.\n  </Step>\n\n  <Step>\n    **Busca** con la pregunta del usuario y usa los fragmentos devueltos como contexto de la respuesta.\n  </Step>\n\n  <Step>\n    **Responde con citas** (opcional): `answer` devuelve un texto en el que cada afirmación apunta al fragmento de origen, o dice que no sabe. Para tu sitio, incrusta el [widget](#widget).\n  </Step>\n\n  <Step>\n    **Registra feedback** (opcional): los fragmentos confirmados suben en las próximas búsquedas similares y los rechazados desaparecen.\n  </Step>\n</Steps>\n\n<EndpointList tool=\"base-de-conhecimento\" />"},{"anchor":"custos","heading":"Costos","markdown":"<PricingTable tool=\"base-de-conhecimento\" group=\"per-use\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"free-quota\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"blocks\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"volume\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"storage\" />\n\n<Callout title=\"Cómo funcionan los bloques\">\n  Las búsquedas y el rerank tienen una cuota gratuita por día. Una vez agotada, la llamada que abre un bloque debita el bloque entero de una vez (10 llamadas en el primero, luego 20, 50 y 100 según el uso pagado del día), y las siguientes del mismo bloque no se vuelven a cobrar. Los contadores se reinician a medianoche UTC, y las llamadas no usadas de un bloque vencen sin reembolso. La herramienta MCP `rag_get_usage` y el `dry_run` de las búsquedas muestran cuánto cobrará la próxima llamada. Consultar bases, documentos y feedback es gratis y no cuenta para la [cuota diaria gratuita de lecturas](/es/cobranca-e-limites#franquia-diaria).\n</Callout>\n\nLa ingesta se cobra **cuando termina**, según el número real de fragmentos; el `estimate` de la respuesta es una aproximación. El contenido idéntico al ya indexado no se vuelve a cobrar. Usa `X-Dry-Run: true` al enviar un documento para ver la estimación sin enviar nada."},{"anchor":"bases","heading":"Bases","markdown":""},{"anchor":"crear-base","heading":"Crear base","markdown":"<Endpoint\n  id=\"rag.collections.create\"\n  example={{ body: `{\n\"name\": \"Centro de ayuda\",\n\"description\": \"Artículos públicos de soporte\",\n\"language\": \"spanish\",\n\"default_mode\": \"fast\"\n}` }}\n>\n  * `name` (obligatorio, hasta 120 caracteres) y `description` (hasta 1.000).\n  * `language`: `simple` (predeterminado), `portuguese`, `english`, `spanish` o `french`. Define el diccionario de la búsqueda por palabras clave.\n  * `default_mode`: `fast` (predeterminado) o `accurate` (rerank adicional, más preciso y algo más caro).\n  * Ajustes avanzados: `chunk_size_tokens` (16–480, predeterminado 96), `chunk_overlap_tokens` (predeterminado 16), `section_max_tokens` (predeterminado 768), `max_per_document` (0–20, predeterminado 2), `dense_weight` y `lexical_weight` (0–10, predeterminado 1; no pueden ser ambos 0) y `feedback_enabled` (predeterminado `true`).\n</Endpoint>"},{"anchor":"listar-bases","heading":"Listar bases","markdown":"<Endpoint id=\"rag.collections.list\">\n  También devuelve el uso de la cuenta respecto a los límites (`usage`).\n</Endpoint>"},{"anchor":"consultar-base","heading":"Consultar base","markdown":"<Endpoint id=\"rag.collections.get\" />"},{"anchor":"filtros-disponibles","heading":"Filtros disponibles","markdown":"<Endpoint id=\"rag.collections.facets\">\n  Lista las claves de metadatos de los documentos de la base y los valores más comunes de cada una, para que sepas qué puedes usar en `filters` al buscar. `documents` es cuántos documentos listos con metadatos se analizaron y `keys` es el total de claves filtrables. Si `sampled` es `true`, la base tiene más de 5000 documentos y el recuento sale solo de los 5000 más recientes. En `facets`, cada elemento trae la `key`, los `documents` que la tienen, `distinct` (total de valores distintos) y `values[]` (los valores más comunes, cada uno con `value` y `documents`). `array: true` indica que la clave guarda una lista; filtra con `{\"clave\": [\"valor\"]}`.\n</Endpoint>"},{"anchor":"modificar-base","heading":"Modificar base","markdown":"<Endpoint id=\"rag.collections.update\" example={{ body: `{ \"default_mode\": \"accurate\" }` }}>\n  Envía solo los campos que quieres cambiar. El modelo de embeddings no se puede cambiar después de la creación. Los cambios en el tamaño de los fragmentos solo se aplican a los documentos enviados después; para aplicarlos a toda la base, usa la reindexación.\n</Endpoint>"},{"anchor":"reindexar-base","heading":"Reindexar base","markdown":"<Endpoint id=\"rag.collections.reindex\">\n  Reprocesa todos los documentos con la configuración actual de la base. Cada fragmento se cobra como en una ingesta nueva.\n</Endpoint>"},{"anchor":"panel-de-salud-de-la-base","heading":"Panel de salud de la base","markdown":"<Endpoint id=\"rag.collections.overview\" example={{ query: { rows: 'categoria', days: '30' } }}>\n  Resumen para mantener la base en orden, sin costo: recuento de documentos por `status`, cuántos están desactualizados (`stale`), nunca se consultaron o no tienen metadatos; una matriz de documentos por metadato (`rows` y `cols` son claves de metadatos, hasta 20 valores por eje); la actividad de los últimos `days` días (1 a 180, predeterminado 30); y `attention`, con hasta 5 ejemplos de documentos con fallo, desactualizados, duplicados o contradictorios y con feedback negativo. La respuesta se guarda en caché por 60 s.\n</Endpoint>"},{"anchor":"índice-de-los-documentos","heading":"Índice de los documentos","markdown":"<Endpoint id=\"rag.collections.index\" example={{ query: { fields: 'categoria', order: 'title' } }}>\n  Lista compacta de los documentos `ready`, sin contenido, para que el agente elija qué leer. `filters` (JSON de metadatos), `fields` (hasta 20 claves de metadatos a copiar), `order` (`updated_at_desc`, predeterminado, o `title`) y `max_bytes` (predeterminado 4096, máximo 32768). `omitted` cuenta los documentos recortados por el límite.\n</Endpoint>"},{"anchor":"encontrar-duplicados-y-contradicciones","heading":"Encontrar duplicados y contradicciones","markdown":"<Endpoint id=\"rag.collections.lint\" example={{ body: `{ \"limit\": 20 }` }}>\n  Compara los 500 documentos más recientes (opcionalmente filtrados por `filters`) con los embeddings que ya existen; no llama a ningún modelo y no escribe nada. `duplicate` aparece con similitud a partir de 0,75; `possible_contradiction`, entre 0,4 y 0,75 cuando ambos tienen el mismo metadato `tipo`. Cada hallazgo indica qué documento conservar (`keep`, el más nuevo) y qué hacer (`action`). `limit` va de 1 a 100 (predeterminado 20) e `include_content: true` devuelve el inicio de cada documento.\n</Endpoint>"},{"anchor":"cambiar-el-modelo-de-embeddings","heading":"Cambiar el modelo de embeddings","markdown":"<Endpoint id=\"rag.collections.model\" example={{ body: `{ \"embedding_model\": \"BAAI/bge-m3\" }` }}>\n  Cambia entre `intfloat/multilingual-e5-small` (predeterminado) y `BAAI/bge-m3` (Alta precisión) reindexando la base **en segundo plano**; no necesitas llamar a la reindexación. El cambio cuesta el 50 % del precio normal de ingesta, y volver al modelo anterior es gratis mientras corre y durante 24 h después, una sola vez. Usa `X-Dry-Run: true` para ver la estimación. Durante el cambio la búsqueda sigue funcionando (la consulta se incrusta con los dos modelos) y los documentos nuevos se rechazan con `rag.model_switch_in_progress`. Sigue el progreso con el campo `model_switch` de la base (`running`, `paused` o `done`); si el saldo se agota, se pausa con `paused_reason: insufficient_credits` y se reanuda cuando recargues.\n</Endpoint>"},{"anchor":"marcar-documentos-como-desactualizados","heading":"Marcar documentos como desactualizados","markdown":"<Endpoint id=\"rag.collections.changes\" example={{ body: `{ \"paths\": [\"docs/api/\", \"src/auth.ts\"], \"commit\": \"a1b2c3d\" }` }}>\n  Para bases vinculadas a código o a archivos: indica los `paths` que cambiaron (archivo exacto o prefijo `dir/`, hasta 1000) y los documentos que declararon esos `paths` al enviarse quedan con `stale: true`. No se reindexa ni se cobra nada; reenviar el documento quita la marca. Usa `exclude_stale` en la búsqueda para ignorarlos. `commit` (hasta 64 caracteres) evita marcar lo que ya se comprobó en ese commit.\n</Endpoint>"},{"anchor":"eliminar-base","heading":"Eliminar base","markdown":"<Endpoint id=\"rag.collections.delete\">\n  La eliminación es asíncrona y borra documentos, fragmentos y feedback.\n</Endpoint>"},{"anchor":"documentos","heading":"Documentos","markdown":""},{"anchor":"añadir-o-reemplazar-documento","heading":"Añadir o reemplazar documento","markdown":"<Endpoint\n  id=\"rag.documents.add\"\n  example={{ body: `{\n\"external_id\": \"articulo-42\",\n\"title\": \"Cómo cambiar la contraseña\",\n\"content\": \"# Cuenta\\\\n\\\\n## Cómo cambiar la contraseña\\\\n\\\\nVe a Configuración > Seguridad…\",\n\"source_uri\": \"https://ayuda.example.com/articulos/42\",\n\"metadata\": { \"categoria\": \"cuenta\" }\n}` }}\n>\n  * `content` (obligatorio): texto plano o Markdown en UTF-8, hasta <Limit name=\"ragDocumentMax\" />. Para PDF, Office e imágenes, usa [Importar archivo](#rag-documents-file).\n  * `external_id` (opcional, hasta 200 bytes): tu propio identificador. Reenviar con el mismo valor **reemplaza** el documento; la versión anterior sigue respondiendo a las búsquedas (`processing: true`) hasta que la nueva esté lista.\n  * `paths` (hasta 50) y `commit`: los archivos de los que viene el documento, para [marcarlo como desactualizado](#rag-collections-changes) cuando cambien.\n  * Los secretos reconocidos en el texto (claves de API, tokens) se sustituyen por `[REDACTED:tipo]` antes de indexar; `redactions` cuenta cuántos.\n  * `metadata`: objeto con hasta 50 claves (4 KB, hasta 3 niveles), usado en los filtros de búsqueda. No se cifra; no pongas datos sensibles.\n  * Sigue el progreso con [Consultar documento](#rag-documents-get) hasta `status: ready` (o `failed`, con `error_code`). Antes de aceptar, la API comprueba que el saldo cubra la estimación.\n</Endpoint>"},{"anchor":"importar-archivo-pdf-office-imagen","heading":"Importar archivo (PDF, Office, imagen)","markdown":"<Endpoint id=\"rag.documents.file\">\n  Envío `multipart/form-data` con `file` (obligatorio, hasta 50 MB) y, opcionalmente, `title`, `external_id`, `source_uri`, `metadata` (objeto JSON en texto) y `ocr` (predeterminado `true`; desactívalo para PDF que ya tienen texto). Formatos: `.pdf`, `.docx`, `.pptx`, `.xlsx`, `.html`, `.md`, `.csv`, `.asciidoc` e imágenes (`.png`, `.jpg`, `.tiff`, `.bmp`, `.webp`). El archivo se convierte a Markdown (tablas conservadas, imágenes solo por el texto del OCR) y luego se indexa como cualquier documento, con el mismo límite de <Limit name=\"ragDocumentMax\" /> para el texto resultante. La respuesta es `202` con el `id` de la importación; consúltala hasta `completed` (trae el `document_id`) o `failed`. Si no hay conversión disponible, la respuesta es `503` (`rag.service_unavailable`).\n</Endpoint>"},{"anchor":"consultar-importación-de-archivo","heading":"Consultar importación de archivo","markdown":"<Endpoint id=\"rag.imports.get\" />"},{"anchor":"listar-documentos","heading":"Listar documentos","markdown":"<Endpoint id=\"rag.documents.list\" example={{ query: { status: 'ready', limit: '20' } }}>\n  Filtro opcional por `status` (`pending`, `processing`, `ready`, `failed`, `deleting`). Paginación por cursor: pasa el `next_cursor` recibido en `after`. `limit` predeterminado 20, máximo 100.\n</Endpoint>"},{"anchor":"consultar-documento","heading":"Consultar documento","markdown":"<Endpoint id=\"rag.documents.get\" />"},{"anchor":"modificar-título-o-metadatos","heading":"Modificar título o metadatos","markdown":"<Endpoint id=\"rag.documents.update\" example={{ body: `{ \"metadata\": { \"categoria\": \"seguridad\" } }` }}>\n  No reprocesa el contenido y no cobra nada.\n</Endpoint>"},{"anchor":"ver-secciones-indexadas","heading":"Ver secciones indexadas","markdown":"<Endpoint id=\"rag.documents.sections\">\n  Muestra cómo se dividió el documento; útil para ajustar títulos y tamaño de los fragmentos.\n</Endpoint>"},{"anchor":"reprocesar-documento-fallido","heading":"Reprocesar documento fallido","markdown":"<Endpoint id=\"rag.documents.retry\">\n  Solo para documentos con `status: failed`; los demás responden `rag.document_not_failed`.\n</Endpoint>"},{"anchor":"eliminar-documento","heading":"Eliminar documento","markdown":"<Endpoint id=\"rag.documents.delete\" />"},{"anchor":"fuentes-sincronizadas","heading":"Fuentes sincronizadas","markdown":"Una **fuente** importa páginas de fuera y las mantiene al día: en cada sincronización, las páginas nuevas o modificadas se reindexan, las sin cambios (mismo contenido) no se vuelven a cobrar, y las que desaparecieron del origen se eliminan de la base. Cada página se convierte en un documento común (`external_id` `src:<id de la fuente>:…`, con `source_id` y `source_url` en los metadatos), cobrado por fragmento como en una ingesta manual; si falta saldo o cuota, la sincronización falla y muestra el motivo.\n\n| `kind`    | `start_url`                        | Qué importa                                                                                                                                                                           |\n| --------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `site`    | página inicial                     | Rastrea enlaces del mismo host hasta `max_depth`, respetando el `robots.txt`; el HTML se convierte a Markdown y los archivos PDF/Office enlazados se convierten.                      |\n| `sitemap` | URL del sitemap (o índice)         | Importa las páginas listadas, hasta `max_pages`. `max_depth` no se aplica.                                                                                                            |\n| `github`  | `https://github.com/dueño/repo`    | Repositorio **público**; `config.ref` (predeterminado `HEAD`) y `config.paths` (prefijos) filtran. Lee `.md`, `.mdx`, `.txt`, `.rst` y convierte los formatos de archivo compatibles. |\n| `gdrive`  | URL de una carpeta `/folders/<id>` | Carpeta compartida como \"cualquiera con el enlace\"; los Docs se convierten a Markdown. Solo existe si el servidor tiene la integración activada.                                      |"},{"anchor":"listar-fuentes-sincronizadas","heading":"Listar fuentes sincronizadas","markdown":"<Endpoint id=\"rag.sources.list\" />"},{"anchor":"crear-fuente-sincronizada","heading":"Crear fuente sincronizada","markdown":"<Endpoint\n  id=\"rag.sources.create\"\n  example={{ body: `{\n\"kind\": \"site\",\n\"name\": \"Centro de ayuda\",\n\"start_url\": \"https://ayuda.example.com\",\n\"max_pages\": 100,\n\"max_depth\": 3,\n\"frequency_hours\": 24\n}` }}\n>\n  * `name` (hasta 200) y `start_url` (http o https) son obligatorios; `kind` predeterminado `site`.\n  * `max_pages`: 1 a 1000 (predeterminado 100). `max_depth`: 1 a 10 (predeterminado 3).\n  * `frequency_hours`: `0` (solo manual), `6`, `24` (predeterminado) o `168`.\n  * La primera sincronización empieza al instante. Páginas y archivos de hasta 20 MB cada uno; un archivo que no se convierte se omite sin hacer fallar la sincronización.\n</Endpoint>"},{"anchor":"modificar-fuente","heading":"Modificar fuente","markdown":"<Endpoint id=\"rag.sources.update\" example={{ body: `{ \"frequency_hours\": 168, \"enabled\": true }` }}>\n  Todos los campos son opcionales. Con `enabled: false` o `frequency_hours: 0`, `next_sync_at` queda nulo.\n</Endpoint>"},{"anchor":"sincronizar-ahora","heading":"Sincronizar ahora","markdown":"<Endpoint id=\"rag.sources.sync\">\n  Responde `409` si la fuente ya se está sincronizando. `pages_total`, `pages_new`, `pages_changed` y `pages_removed` resumen la ejecución.\n</Endpoint>"},{"anchor":"historial-de-sincronizaciones","heading":"Historial de sincronizaciones","markdown":"<Endpoint id=\"rag.sources.syncs\" />"},{"anchor":"eliminar-fuente","heading":"Eliminar fuente","markdown":"<Endpoint id=\"rag.sources.delete\" />"},{"anchor":"búsqueda","heading":"Búsqueda","markdown":""},{"anchor":"modos-de-búsqueda-y-reranker","heading":"Modos de búsqueda y reranker","markdown":"* **`fast`** (predeterminado): combina la búsqueda por significado y la de palabras clave. El `score` es una fusión de posiciones (valores pequeños, cerca de 0,03), bueno para ordenar, no para comparar con un umbral.\n* **`accurate`**: tras la fusión, un **reranker** relee los 20 mejores candidatos junto con la pregunta y devuelve un `score` calibrado de 0 a 1; solo en él `min_score` tiene efecto. Cobra el adicional de rerank. Si el rerank no está disponible, la búsqueda pasa a `fast` sin el adicional (`mode_used`).\n* **Reranker**: `rerank_provider` de la base (y `provider` en el rerank independiente y en `decide`) elige entre `bge` (predeterminado, cross-encoder) y `laya`, el juez de relevancia de Respawn, que pregunta si el fragmento responde a la consulta y devuelve la probabilidad. Laya suele separar mejor los fragmentos que solo se parecen a la pregunta, pero es más lento y, si está fuera de servicio, `accurate` pasa a `fast` y el rerank independiente responde `503` (`rag.service_unavailable`). El precio es el mismo que el de `bge`.\n* Si el servicio de embeddings falla, la búsqueda aún responde solo por palabras clave (`mode_used: lexical`), cobrada como caché."},{"anchor":"buscar-en-la-base","heading":"Buscar en la base","markdown":"<Endpoint\n  id=\"rag.query\"\n  example={{ body: `{\n\"query\": \"¿cómo cambio mi contraseña?\",\n\"top_k\": 5,\n\"filters\": { \"categoria\": \"cuenta\" }\n}` }}\n>\n  * `query` (obligatorio, hasta <Limit name=\"ragQueryCharsMax\" /> caracteres).\n  * `top_k`: cuántos fragmentos devolver (predeterminado 5, máximo <Limit name=\"ragTopKMax\" />).\n  * `mode`: `fast` o `accurate`; si se omite, vale el predeterminado de la base. Si el rerank del modo `accurate` no está disponible, la búsqueda pasa a `fast` sin cobrar el adicional; `mode_used` indica cuál se usó.\n  * `filters`: solo fragmentos de documentos cuyos metadatos contienen estos valores.\n  * `min_score` (solo en `accurate`) y `max_per_document` (0–20) afinan el resultado.\n  * `updated_after` y `updated_before` (RFC 3339) filtran por la fecha de actualización del documento, y `exclude_stale: true` ignora los marcados como desactualizados. Cada resultado trae `updated_at`, `stale`, `paths`, `commit`, `source_uri` y `also_in` (otros documentos con el mismo texto, colapsados en un solo resultado).\n  * `highlights` marca, en posiciones de carácter Unicode, los términos encontrados en `content`. Las consultas repetidas en menos de <Limit name=\"ragQueryCacheTtl\" /> se responden desde la caché (`cached: true`) y cuestan menos.\n\n  <Callout type=\"warn\" title=\"Contenido del usuario\">\n    Los fragmentos devueltos son contenido de tus documentos. Al pasarlos a un modelo de lenguaje, trátalos como datos, no como instrucciones.\n  </Callout>\n</Endpoint>"},{"anchor":"feedback-de-relevancia","heading":"Feedback de relevancia","markdown":""},{"anchor":"registrar-feedback-de-relevancia","heading":"Registrar feedback de relevancia","markdown":"<Endpoint\n  id=\"rag.feedback.add\"\n  example={{ body: `{\n\"query\": \"¿cómo cambio mi contraseña?\",\n\"section_id\": \"b2c4d6e8-0a1b-4c3d-8e5f-6a7b8c9d0e1f\",\n\"label\": \"positive\",\n\"end_user_ref\": \"usuario-123\"\n}` }}\n>\n  * `label`: `positive` (el fragmento responde a la pregunta) o `negative`.\n  * `end_user_ref` (opcional): identifica quién evaluó, cuando el voto viene de tu usuario final. Solo se guarda como hash.\n  * Hasta <Limit name=\"ragFeedbackPerMinute\" /> votos por minuto. Las preguntas similares se agrupan, y el voto pasa a valer para todo el grupo.\n</Endpoint>"},{"anchor":"listar-feedback","heading":"Listar feedback","markdown":"<Endpoint id=\"rag.feedback.list\" />"},{"anchor":"eliminar-un-grupo-de-feedback","heading":"Eliminar un grupo de feedback","markdown":"<Endpoint id=\"rag.feedback.delete\" />"},{"anchor":"rerank-independiente","heading":"Rerank independiente","markdown":""},{"anchor":"reordenar-textos-rerank","heading":"Reordenar textos (rerank)","markdown":"<Endpoint\n  id=\"rag.rerank\"\n  example={{ body: `{\n\"query\": \"plazo de reembolso\",\n\"documents\": [\"Entregas en hasta 5 días hábiles.\", \"Envío gratis en compras superiores a 40 €.\", \"Reembolso en hasta 7 días tras la devolución.\"],\n\"top_k\": 2\n}` }}\n>\n  Ordena **tu propia** lista de textos por relevancia respecto a la pregunta, sin ninguna base de por medio. Hasta <Limit name=\"ragRerankDocumentsMax\" /> textos por llamada (cada uno se corta en 2.000 caracteres). `index` apunta a la posición en el array enviado.\n</Endpoint>"},{"anchor":"respuesta-con-citas","heading":"Respuesta con citas","markdown":""},{"anchor":"responder-con-citas","heading":"Responder con citas","markdown":"<Endpoint\n  id=\"rag.answer\"\n  example={{ body: `{\n\"query\": \"¿cómo cambio mi contraseña?\",\n\"top_k\": 5,\n\"min_score\": 0.35,\n\"generate\": false\n}` }}\n>\n  Hace una búsqueda `accurate` y responde **solo** con fragmentos de `score` igual o superior a `min_score` (0 a 1, predeterminado 0,35, un punto de partida que conviene calibrar en tu base).\n\n  * Con respuesta, `answered: true` y cada `[n]` del texto apunta a una `citations[]` (`section_id`, `document_id`, `title`, `heading_path`, `source_uri`, `score`, `excerpt`). Nunca hay respuesta sin cita.\n  * Sin fragmento por encima del umbral, `answered: false`, `reason: no_passage_above_threshold`, ninguna cita, y se dispara el webhook `rag.question.unanswered`. Si el rerank no pudo comprobar la relevancia, `reason: relevance_unverified` (no afirma nada y no dispara el webhook).\n  * `generate: false` (predeterminado) arma la respuesta citando los 3 mejores fragmentos. Con `generate: true`, el proveedor de IA predeterminado de la plataforma escribe el texto a partir de los fragmentos; sin marcador de cita válido o sin proveedor, vuelve al modo extractivo (`generated: false`). Con texto generado, los fragmentos de la base van al proveedor de IA de la plataforma.\n  * Con `Accept: text/event-stream`, la respuesta llega en SSE: `citations`, varios `delta` (trozos del texto) y `done` (el resultado completo). El texto se genera entero antes, el SSE solo lo trocea.\n  * Cobro de una búsqueda `accurate`; la generación no tiene cobro aparte. Usa el `query_id` en el [feedback](#rag-feedback-add) para alimentar el CTR del analytics.\n</Endpoint>"},{"anchor":"widget","heading":"Widget de chat incrustable","markdown":"El widget es un botón de chat en JavaScript puro (sin CDN, en shadow DOM, solo escribe texto) que responde con citas usando tu base. Usa una **clave pública** (`rpk_…`) que solo puede preguntar en una base: no lee otras, no escribe nada.\n\n```html\n<script src=\"https://api.respawntech.dev/widget/rag.js\" data-key=\"rpk_...\" async></script>\n```\n\nAtributos: `data-key` (obligatorio), `data-title`, `data-placeholder` y `data-dont-know` (texto cuando no hay respuesta). Las preguntas hechas por el widget se cobran al dueño de la base, como `answer`."},{"anchor":"crear-clave-pública-widget","heading":"Crear clave pública (widget)","markdown":"<Endpoint\n  id=\"rag.publickeys.create\"\n  example={{ body: `{\n\"name\": \"Sitio institucional\",\n\"allowed_origins\": [\"https://www.example.com\", \"https://*.example.com\"],\n\"rate_per_minute\": 30,\n\"generate\": false\n}` }}\n>\n  * `allowed_origins` (obligatorio, 1 a 20): `https://site.com` o `https://*.site.com`, sin ruta. El servidor rechaza solicitudes cuyo `Origin` no esté en la lista.\n  * `rate_per_minute`: 1 a 600 (predeterminado 30), por clave. `generate` activa el texto generado por IA en las respuestas de esa clave.\n  * Hasta 20 claves por base. La `key` aparece **una sola vez**; guardamos solo el hash.\n\n  <Callout type=\"warn\" title=\"Origin no es autenticación\">\n    Solo el navegador respeta el `Origin`; un cliente fuera de él puede falsificarlo. Lo que limita el abuso es el límite por minuto de la clave, y el costo recae sobre ti. Revoca la clave si ves un uso extraño.\n  </Callout>\n</Endpoint>"},{"anchor":"listar-claves-públicas","heading":"Listar claves públicas","markdown":"<Endpoint id=\"rag.publickeys.list\" />"},{"anchor":"revocar-clave-pública","heading":"Revocar clave pública","markdown":"<Endpoint id=\"rag.publickeys.revoke\" />"},{"anchor":"responder-desde-el-widget-ruta-pública","heading":"Responder desde el widget (ruta pública)","markdown":"<Endpoint id=\"rag.public.answer\" example={{ body: `{ \"query\": \"¿cómo cambio mi contraseña?\" }` }}>\n  Sin token de API: envía la clave en el header `X-Respawn-Public-Key` y el navegador manda el `Origin`. La base viene de la clave, y `top_k`, `min_score` y `generate` no se aceptan en la solicitud. Responde como `answer` (JSON o SSE), con CORS habilitado para el origen permitido. Errores: `401` (clave ausente, desconocida o revocada), `403` (origen no permitido) y `429` (límite por minuto).\n</Endpoint>"},{"anchor":"script-del-widget","heading":"Script del widget","markdown":"<Endpoint id=\"rag.widget\" />"},{"anchor":"analytics-de-preguntas","heading":"Analytics de preguntas","markdown":""},{"anchor":"analytics-de-las-preguntas","heading":"Analytics de las preguntas","markdown":"<Endpoint id=\"rag.analytics\" example={{ query: { days: '30' } }}>\n  Muestra qué preguntan los usuarios y qué no sabe responder la base, a partir de las llamadas a `answer` y del widget. `days` va de 1 a 90 (predeterminado 30).\n\n  * `answer_rate`: respondidas ÷ total de preguntas.\n  * `feedback_ctr`: respondidas que recibieron voto (por el `query_id`) ÷ respondidas.\n  * `positive_rate`: votos positivos ÷ votos.\n  * `top_questions` y `unanswered_questions`: hasta 20 preguntas agrupadas cada una, para que descubras qué escribir en la base.\n\n  Las preguntas se cifran con la clave de la base. El widget no tiene voto propio: el CTR solo cuenta los votos enviados por la API.\n</Endpoint>"},{"anchor":"enrutamiento","heading":"Enrutamiento","markdown":""},{"anchor":"decidir-entre-opciones-enrutamiento","heading":"Decidir entre opciones (enrutamiento)","markdown":"<Endpoint\n  id=\"rag.decide\"\n  example={{ body: `{\n\"text\": \"quiero cancelar mi suscripción\",\n\"options\": [\n  { \"label\": \"cobro\", \"description\": \"pagos, facturas y cancelaciones\" },\n  { \"label\": \"soporte\", \"description\": \"problemas técnicos\" }\n],\n\"threshold\": 0.5,\n\"min_margin\": 0.1\n}` }}\n>\n  Clasifica un texto entre 2 y 50 opciones sin usar un LLM, puntuando cada `label: description` con el reranker (`provider`: `bge` o `laya`). Sirve para enrutar solicitudes. Si la mejor opción no supera `threshold` (predeterminado 0,5) o no queda `min_margin` (predeterminado 0,1) por delante de la segunda, `decided` es `false`: trátalo como \"no sé\". El `ranking` completo viene siempre. Precio y cuota del rerank independiente.\n</Endpoint>"},{"anchor":"precios-y-límites-de-la-cuenta","heading":"Precios y límites de la cuenta","markdown":""},{"anchor":"consultar-precios","heading":"Consultar precios","markdown":"<Endpoint id=\"rag.pricing\" />"},{"anchor":"consultar-límites","heading":"Consultar límites","markdown":"<Endpoint id=\"rag.limits\" />\n\nValores vigentes de tu cuenta (ya con la bonificación del plan), útiles para validar un envío antes de hacerlo."},{"anchor":"eventos-webhooks","heading":"Eventos (webhooks)","markdown":"Registra un endpoint en el dashboard para recibir:\n\n* `rag.document.ready` y `rag.document.failed`: `collection_id`, `document_id`, `external_id` y `version`; el de fallo trae también `error_code`. Valen para documentos enviados, importados desde archivo y provenientes de fuentes.\n* `rag.question.unanswered`: `collection_id`, `question`, `query_id` y `source` (`api` o `widget`), cuando ningún fragmento superó el `min_score`. Útil para alimentar una cola de \"qué escribir\".\n\nEl envío es de mejor esfuerzo, como en los demás eventos. Consulta la [lista de eventos](/es/webhooks)."},{"anchor":"usar-por-mcp","heading":"Usar por MCP","markdown":"Las mismas operaciones están disponibles como herramientas MCP del grupo `rag` ([lista completa](/es/agentes-de-ia)): `rag_answer`, `rag_decide`, `rag_index`, `rag_lint`, `rag_mark_changed` y `rag_retry_document` son las nuevas. Las de lectura (`rag_index`, `rag_lint`) son gratuitas; usa `rag_access=query` en la conexión para ocultar las que modifican la base."},{"anchor":"errores","heading":"Errores","markdown":"<ErrorTable tool=\"base-de-conhecimento\" />\n\nLos códigos sin estado HTTP aparecen en el campo `error_code` de un documento con `status: failed`."},{"anchor":"limites","heading":"Límites","markdown":"| Límite                                      | Valor                                        |\n| ------------------------------------------- | -------------------------------------------- |\n| Tamaño de un documento                      | <Limit name=\"ragDocumentMax\" />              |\n| Bases por cuenta                            | <Limit name=\"ragCollectionsMax\" />           |\n| Fragmentos indexados por cuenta             | <Limit name=\"ragChunksMax\" />                |\n| Documentos en procesamiento al mismo tiempo | <Limit name=\"ragPendingMax\" />               |\n| Tamaño de la pregunta                       | <Limit name=\"ragQueryCharsMax\" /> caracteres |\n| Fragmentos por búsqueda (`top_k`)           | <Limit name=\"ragTopKMax\" />                  |\n| Textos por rerank independiente             | <Limit name=\"ragRerankDocumentsMax\" />       |\n| Votos de feedback por minuto                | <Limit name=\"ragFeedbackPerMinute\" />        |\n| Archivo importado                           | 50 MB                                        |\n| Páginas por fuente                          | 1.000 (predeterminado 100)                   |\n| Claves públicas por base                    | 20                                           |\n\nLas rutas de la base de conocimiento ignoran el header `X-Idempotency-Key`: ya son idempotentes (el contenido repetido no se reindexa ni se cobra). Una clave restringida a bases específicas solo ve esas bases y no puede crear otras (`forbidden`)."}]},{"slug":"ferramentas/ci-error-extract","url":"https://docs.respawntech.dev/es/ferramentas/ci-error-extract","title":"Extractor de errores de CI","description":"Extraiga de un log de CI solo las líneas que explican el fallo.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"ci-error-extract\" />"},{"anchor":"visión-general","heading":"Visión general","markdown":"Envíe el log de un job de CI y reciba solo las líneas del error. Sirve para agentes y bots que necesitan entender por qué falló un build sin cargar miles de líneas en el contexto. También está disponible en el [MCP](/es/agentes-de-ia) como `ci_error_extract`.\n\n<EndpointList tool=\"ci-error-extract\" />"},{"anchor":"custos","heading":"Costos","markdown":"Cada extracción es una lectura medida: cuenta para la [cuota diaria gratuita](/es/cobranca-e-limites#franquia-diaria) y, una vez agotada, debita el valor indicado en el endpoint. El cobro aplica solo a claves de API."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"extraer-el-error-de-un-log","heading":"Extraer el error de un log","markdown":"<Endpoint id=\"ci.extract\" example={{ body: `{ \"log\": \"npm ERR! code ELIFECYCLE\\\\nnpm ERR! errno 1\\\\nFAIL src/app.test.ts\", \"max_lines\": 40 }` }}>\n  `log` (obligatorio) es el texto del log. `max_lines` (opcional, por defecto 40, máximo 200) limita cuántas líneas del error se devuelven.\n</Endpoint>"},{"anchor":"errores","heading":"Errores","markdown":"<ErrorTable tool=\"ci-error-extract\" />"}]},{"slug":"ferramentas/memoria-de-aprendizados","url":"https://docs.respawntech.dev/es/ferramentas/memoria-de-aprendizados","title":"Memoria de aprendizajes","description":"Memoria de largo plazo para agentes de IA: guarda lo aprendido en cada proyecto y recupéralo antes de la próxima tarea.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"memoria-de-aprendizados\" />"},{"anchor":"como-funciona","heading":"Cómo funciona","markdown":"Un aprendizaje es un texto corto con un `kind`, ligado a un `project`:\n\n| `kind`       | Para guardar                                   |\n| ------------ | ---------------------------------------------- |\n| `pitfall`    | Trampas y errores que no deben repetirse.      |\n| `decision`   | Decisiones tomadas y el motivo.                |\n| `convention` | Convenciones del proyecto.                     |\n| `howto`      | Cómo hacer algo (ejecutar, publicar, depurar). |\n| `other`      | Lo que no cabe en los demás.                   |\n\nLos aprendizajes viven en una [base de conocimiento](/es/ferramentas/base-de-conhecimento) llamada `agent-learnings`, **creada automáticamente** en el primer guardado. `recall` hace una búsqueda semántica por la tarea y devuelve solo los ítems del `project` pedido, con el `score` de cada uno.\n\nTambién está disponible en el [MCP](/es/agentes-de-ia) como `learning_save`, `learning_recall` y `learning_forget` (toolset `learnings`).\n\n<EndpointList tool=\"memoria-de-aprendizados\" />"},{"anchor":"ingestao","heading":"Duplicados e ingesta asíncrona","markdown":"* **Deduplicación:** `save` compara por hash y por similitud semántica. Si ya existe un aprendizaje igual o parecido, no se guarda nada nuevo y la respuesta trae `duplicate_of` con el id del existente.\n* **Ingesta asíncrona:** el texto se indexa en segundo plano. Un `recall` justo después de un `save` puede no traer aún el ítem; inténtalo de nuevo en unos segundos."},{"anchor":"custos","heading":"Costos","markdown":"La facturación es la de la [base de conocimiento](/es/ferramentas/base-de-conhecimento): `save` como la ingesta (por fragmento), `recall` como la búsqueda (franquicia diaria + bloques) y el almacenamiento por día. Olvidar es gratis. La memoria sigue las mismas reglas de habilitación de la base de conocimiento (`rag.disabled` y los demás errores `rag.*`)."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"guardar-aprendizaje","heading":"Guardar aprendizaje","markdown":"<Endpoint id=\"learnings.save\" example={{ body: `{ \"project\": \"respawn-tech-docs\", \"kind\": \"pitfall\", \"text\": \"check:backend lee ../respawn-tech-backend; ejecútalo solo con el backend ya integrado.\", \"source\": \"conversación del 06/10\" }` }}>\n  `project`, `kind` y `text` son obligatorios; `source` es opcional e indica de dónde vino. La respuesta trae el `id` del aprendizaje o, si es repetido, `duplicate_of`.\n</Endpoint>"},{"anchor":"recuperar-aprendizajes","heading":"Recuperar aprendizajes","markdown":"<Endpoint id=\"learnings.recall\" example={{ body: `{ \"project\": \"respawn-tech-docs\", \"task\": \"ejecutar los checks antes de abrir el PR\", \"top_k\": 5 }` }}>\n  `project` y `task` son obligatorios; `top_k` limita cuántos ítems vuelven. Cada ítem trae `id`, `kind`, `text`, `source` y `score`. Recuerda que la ingesta es asíncrona: un ítem recién guardado puede no aparecer aún.\n</Endpoint>"},{"anchor":"olvidar-aprendizaje","heading":"Olvidar aprendizaje","markdown":"<Endpoint id=\"learnings.forget\" />"}]},{"slug":"ferramentas/notify","url":"https://docs.respawntech.dev/es/ferramentas/notify","title":"Notificaciones","description":"Un agente avisa a su humano por correo, webhook firmado o ntfy, con canales, prioridad e historial de envíos.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"notify\" />"},{"anchor":"como-funciona","heading":"Cómo funciona","markdown":"Una notificación tiene `title`, `message` y `priority` (`normal` o `high`), y opcionalmente una `url` para que el humano la abra. Sale por un **canal**:\n\n* **Canal elegido:** el `channel` de la solicitud, por el nombre del canal.\n* **Canal predeterminado:** el canal marcado con `is_default`, cuando la solicitud no trae `channel`.\n* **Correo de la cuenta:** sin ningún canal configurado, la notificación va al correo de la cuenta.\n\nTambién está disponible en el [MCP](/es/agentes-de-ia) como la tool `notify` (toolset `notify`): el agente avisa que terminó una tarea larga o que necesita una decisión.\n\n<EndpointList tool=\"notify\" />"},{"anchor":"canais","heading":"Canales","markdown":"Cada canal tiene un `name` único en la cuenta y un `kind`:\n\n| `kind`    | Campos            | Cómo entrega                                                                                |\n| --------- | ----------------- | ------------------------------------------------------------------------------------------- |\n| `email`   | ninguno           | Envía **solo al correo de la cuenta**. No se puede elegir otro destinatario.                |\n| `webhook` | `url`, `secret`   | Hace un `POST` a la URL, firmado como en el [Webhook Relay](/es/ferramentas/webhook-relay). |\n| `ntfy`    | `server`, `topic` | Publica en el tópico de [ntfy](https://ntfy.sh) del servidor indicado.                      |\n\n`is_default` marca el canal usado cuando la solicitud no indica uno, y `enabled: false` desactiva el canal sin borrarlo. El `secret` del webhook nunca vuelve en las respuestas: guárdalo al crear el canal.\n\n<Callout type=\"warn\" title=\"El tópico de ntfy es la contraseña\">\n  En servidores públicos de ntfy, quien conozca el tópico lee las notificaciones. Usa un nombre largo e impredecible, o un servidor propio.\n</Callout>"},{"anchor":"limites","heading":"Límites, retry e historial","markdown":"* **Rate limit:** hay un límite de notificaciones por período; por encima de él el envío falla con `notify.rate_limited`.\n* **Retry del webhook:** una entrega que falla se reintenta algunas veces antes de convertirse en `notify.delivery_failed`. El receptor debe tratar las repeticiones y verificar la firma.\n* **Historial:** `GET /messages` lista los envíos (canal, prioridad, estado y fecha), paginado con `page` y `page_size` (hasta 100). El historial **no guarda el título ni el cuerpo del mensaje**."},{"anchor":"custos","heading":"Costos","markdown":"Cada envío por la API se cobra según el valor indicado en el endpoint. Los canales y el historial son gratis, y una entrega fallida no se cobra."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"enviar-notificación","heading":"Enviar notificación","markdown":"<Endpoint id=\"notify.send\" example={{ body: `{ \"title\": \"Deploy terminado\", \"message\": \"La versión 2.4 está en producción.\", \"priority\": \"normal\" }` }}>\n  `title` y `message` son obligatorios; `priority` es `normal` o `high`; `url` y `channel` son opcionales. Sin `channel` usa el canal predeterminado y, sin ningún canal, el correo de la cuenta.\n</Endpoint>"},{"anchor":"historial-de-envíos","heading":"Historial de envíos","markdown":"<Endpoint id=\"notify.messages\" example={{ query: { page: \"1\", page_size: \"20\" } }}>\n  Paginado por `page` y `page_size` (hasta 100). No incluye el cuerpo de los mensajes.\n</Endpoint>"},{"anchor":"listar-canales","heading":"Listar canales","markdown":"<Endpoint id=\"notify.channels.list\" />"},{"anchor":"crear-canal","heading":"Crear canal","markdown":"<Endpoint id=\"notify.channels.create\" example={{ body: `{ \"name\": \"celular\", \"kind\": \"ntfy\", \"server\": \"https://ntfy.sh\", \"topic\": \"mi-topico-secreto\", \"is_default\": true }` }}>\n  `name` y `kind` son obligatorios. `url` y `secret` valen para `webhook`; `server` y `topic`, para `ntfy`. Los campos que no combinan con el `kind` responden `notify.invalid_channel`.\n</Endpoint>"},{"anchor":"consultar-canal","heading":"Consultar canal","markdown":"<Endpoint id=\"notify.channels.get\" />"},{"anchor":"modificar-canal","heading":"Modificar canal","markdown":"<Endpoint id=\"notify.channels.update\" example={{ body: `{ \"enabled\": false }` }}>\n  Envía solo los campos que cambian. Un canal desactivado responde `notify.channel_disabled` cuando se elige en un envío.\n</Endpoint>"},{"anchor":"eliminar-canal","heading":"Eliminar canal","markdown":"<Endpoint id=\"notify.channels.delete\" />"},{"anchor":"probar-canal","heading":"Probar canal","markdown":"<Endpoint id=\"notify.channels.test\">\n  Envía una notificación de prueba por el canal, para comprobar la configuración antes de depender de él.\n</Endpoint>"},{"anchor":"errores","heading":"Errores","markdown":"<ErrorTable tool=\"notify\" />"}]},{"slug":"ferramentas/pdf-convert","url":"https://docs.respawntech.dev/es/ferramentas/pdf-convert","title":"PDF Convert","description":"API de conversión de HTML a PDF, PNG o JPG — flujo asíncrono, batch, retención y costos.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"pdf-convert\" />"},{"anchor":"descripción-general","heading":"Descripción general","markdown":"PDF Convert es **asíncrono**:\n\n1. `POST /convert` o `POST /batch` → **202 Accepted** con el `conversion_id` (el `{id}` de las demás rutas).\n2. Un worker renderiza en segundo plano (Chromium headless).\n3. Sigue el progreso mediante el [callback `webhook_url`](/es/webhooks#pdf-callback) o consultando `GET /{id}`.\n4. Descarga con `GET /{id}/download` cuando `status` sea `completed`.\n\n<Callout type=\"warn\" title=\"Cobro al encolar\">\n  Los créditos se debitan cuando se acepta el job. Si el encolado de un batch falla después del débito, el monto se reembolsa automáticamente.\n</Callout>\n\n<EndpointList tool=\"pdf-convert\" />"},{"anchor":"custos","heading":"Costos","markdown":"<PricingTable tool=\"pdf-convert\" group=\"document\" />\n\n<PricingTable tool=\"pdf-convert\" group=\"addons\" />\n\n<PricingTable tool=\"pdf-convert\" group=\"batch-discounts\" />\n\nEl batch acepta hasta **<Limit name=\"pdfBatchMax\" /> ítems**. El reintento vuelve a cobrar el costo completo, incluidos los adicionales. Guardar plantillas y subir fuentes no cuesta créditos. Las consultas y descargas vía API siguen la [cuota diaria gratuita](/es/cobranca-e-limites#franquia-diaria)."},{"anchor":"conversión","heading":"Conversión","markdown":""},{"anchor":"convertir-html-a-pdfpngjpg","heading":"Convertir HTML a PDF/PNG/JPG","markdown":"<Endpoint\n  id=\"pdf.convert\"\n  example={{\n  headers: { 'X-Idempotency-Key': '550e8400-e29b-41d4-a716-446655440001' },\n  body: `{\n\"html\": \"<html><body><h1>Fatura</h1></body></html>\",\n\"css\": \"body { font-family: sans-serif; }\",\n\"format\": \"pdf\",\n\"page_size\": \"A4\",\n\"retention_days\": 7\n}`,\n}}\n>\n  Envía **`html` o `url`** (nunca ambos). Con `url`, Chromium abre la página pública y la renderiza; `css`, `google_fonts_url` y `data` se rechazan en ese modo (`pdf.invalid_option`).\n\n  | Campo                                                         | Límite / valores                                                                                                           |\n  | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |\n  | `html`                                                        | máx. <Limit name=\"pdfHtmlMax\" />                                                                                           |\n  | `url`                                                         | alternativa a `html`: dirección pública `http`/`https`, hasta 2048 caracteres (`pdf.invalid_url` si es privada o inválida) |\n  | `css`                                                         | máx. <Limit name=\"pdfCssMax\" />                                                                                            |\n  | `format`                                                      | `pdf` (predeterminado), `png`, `jpg` / `jpeg`                                                                              |\n  | `file_name`                                                   | nombre del archivo generado                                                                                                |\n  | `title`, `author`, `subject`, `keywords`                      | metadatos del documento, hasta 1000 caracteres cada uno                                                                    |\n  | `header_html` / `footer_html`                                 | encabezado y pie de página repetidos en cada página                                                                        |\n  | `google_fonts_url`                                            | URL de CSS de Google Fonts a cargar                                                                                        |\n  | `wait_selector` / `wait_delay`                                | espera a que aparezca un selector / un retraso en ms antes de renderizar                                                   |\n  | `enable_js` / `custom_js`                                     | ejecuta JavaScript en sandbox (SPAs, gráficos) / script adicional                                                          |\n  | `data`                                                        | objeto con variables para los placeholders `{{campo}}` en el HTML                                                          |\n  | `retention_days`                                              | `0` (efímero, \\~24 h), `1`–`30` o `-1` (permanente)                                                                        |\n  | `webhook_url`                                                 | recibe el [callback](/es/webhooks#pdf-callback) al terminar                                                                |\n  | `template_id`, `template_version`, `font_ids`, `post_process` | [plantillas guardadas](#templates), [fuentes](#fontes) y [posprocesamiento](#pos-processamento)                            |\n\n  Tamaño de página, márgenes, medio, numeración y calidad están en [Opciones de página y renderizado](#opcoes-de-pagina).\n\n  Guarda el `conversion_id`: es el `{id}` que se usa para consultar el estado y descargar el archivo.\n</Endpoint>"},{"anchor":"batch-con-plantilla","heading":"Batch con plantilla","markdown":"<Endpoint\n  id=\"pdf.batch\"\n  example={{\n  headers: { 'X-Idempotency-Key': '550e8400-e29b-41d4-a716-446655440002' },\n  body: `{\n\"template\": {\n  \"html\": \"<html><body><h1>{{title}}</h1></body></html>\",\n  \"css\": \"h1 { color: #333; }\"\n},\n\"config\": { \"page_size\": \"A4\", \"format\": \"pdf\", \"retention_days\": 7 },\n\"items\": [\n  { \"custom_id\": \"fatura-001\", \"file_name\": \"fatura-001.pdf\", \"data\": { \"title\": \"Fatura 001\" } },\n  { \"custom_id\": \"fatura-002\", \"file_name\": \"fatura-002.pdf\", \"data\": { \"title\": \"Fatura 002\" } }\n],\n\"delivery\": { \"method\": \"zip\", \"webhook_url\": \"https://example.com/hooks/pdf\" }\n}`,\n}}\n>\n  | Campo                  | Descripción                                                                                                                                                                                                                                                          |\n  | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n  | `template.html`        | **obligatorio** — HTML con placeholders `{{campo}}` (sintaxis de Go template: `{{if}}` y `{{range}}` también funcionan)                                                                                                                                              |\n  | `template.css`         | CSS compartido                                                                                                                                                                                                                                                       |\n  | `items[]`              | **obligatorio**, de 1 a <Limit name=\"pdfBatchMax\" /> ítems: `custom_id` (**obligatorio**, tu identificador), `file_name` y `data`                                                                                                                                    |\n  | `config`               | opciones aplicadas a todos los ítems: `page_size`, `orientation`, márgenes, `format`, `scale`, `retention_days`, `google_fonts_url`, `header_html`, `footer_html`, `wait_selector`, `wait_delay`, `title`, `author`, `enable_js`, `custom_js` y `file_name_template` |\n  | `delivery.method`      | `zip` o `list`: cómo se entrega el resultado al terminar                                                                                                                                                                                                             |\n  | `delivery.webhook_url` | recibe el [callback](/es/webhooks#pdf-callback) al terminar                                                                                                                                                                                                          |\n\n  El batch no acepta `url`, `template_id`, `font_ids` ni `post_process`: siempre usa el `template.html` del cuerpo. Los descuentos por volumen se aplican sobre la base, JavaScript y la retención. Sigue el progreso en [Detalle del batch](#pdf-batch-get).\n</Endpoint>"},{"anchor":"reprocesar-una-falla","heading":"Reprocesar una falla","markdown":"<Endpoint\n  id=\"pdf.retry\"\n  example={{\n  body: `{\n\"html\": \"<html><body><h1>Fatura corrigida</h1></body></html>\",\n\"format\": \"pdf\",\n\"page_size\": \"A4\"\n}`,\n}}\n>\n  Permitido solo cuando `status` es `failed`. El cuerpo acepta las mismas opciones que [Convertir](#pdf-convert), así que puedes corregir el HTML o las opciones antes de reintentar. El `webhook_url` del cuerpo se ignora: la conversión conserva el que se envió originalmente. Se vuelve a cobrar el costo completo.\n</Endpoint>"},{"anchor":"opcoes-de-pagina","heading":"Opciones de página y renderizado","markdown":"Valen para `POST /convert` y `POST /retry`. Los valores fuera de la lista devuelven `pdf.invalid_option` (o `pdf.invalid_page_size` para el tamaño del papel).\n\n| Campo                                                        | Valores                                                                                                                                              |\n| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `page_size`                                                  | `A3`, `A4` (predeterminado), `A5`, `Letter`, `Legal`, `Tabloid` o `Custom`                                                                           |\n| `paper_width`, `paper_height`                                | papel personalizado, en **pulgadas** (1 a 200). Solo se combinan con `page_size` vacío o `Custom`; con otro tamaño devuelven `pdf.invalid_page_size` |\n| `orientation`                                                | `portrait` (predeterminado) o `landscape`                                                                                                            |\n| `margin_top`, `margin_bottom`, `margin_left`, `margin_right` | márgenes de la página                                                                                                                                |\n| `scale`                                                      | escala del renderizado                                                                                                                               |\n| `print_background`                                           | imprime colores e imágenes de fondo (predeterminado `true`)                                                                                          |\n| `media_type`                                                 | `print` o `screen`: qué `@media` ve el CSS                                                                                                           |\n| `color_scheme`                                               | `light` o `dark`: valor de `prefers-color-scheme`                                                                                                    |\n| `page_numbers`                                               | activa el contador de páginas en la posición: `header-left`, `header-center`, `header-right`, `footer-left`, `footer-center` o `footer-right`        |\n| `page_number_format`                                         | texto del contador, con `{page}` y `{total}` (predeterminado `{page} / {total}`, hasta 200 caracteres). Requiere `page_numbers`                      |\n| `jpeg_quality`                                               | de 1 a 100 (predeterminado 90). Solo vale con `format: jpg`                                                                                          |\n\nEjemplo: informe en papel de 8 x 11 pulgadas, con contador en el pie:\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n  \"url\": \"https://example.com/relatorio\",\n  \"page_size\": \"Custom\",\n  \"paper_width\": 8,\n  \"paper_height\": 11,\n  \"media_type\": \"print\",\n  \"page_numbers\": \"footer-center\",\n  \"page_number_format\": \"Página {page} de {total}\"\n}'\n```"},{"anchor":"pos-processamento","heading":"Posprocesamiento","markdown":"`post_process` actúa sobre el PDF ya renderizado y **solo vale con `format: pdf`** (con `png` o `jpg`, `pdf.invalid_option`). Cada recurso se cobra aparte, consulta [Costos](#custos).\n\n| Campo                                   | Descripción                                                                                                              |\n| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |\n| `post_process.watermark.text`           | texto de la marca de agua, hasta 200 caracteres                                                                          |\n| `post_process.watermark.image`          | alternativa al texto: imagen PNG o JPEG en base64, hasta 1 MiB. Envía **exactamente uno** entre `text` e `image`         |\n| `watermark.font_size`                   | de 1 a 300                                                                                                               |\n| `watermark.color`                       | color en formato `#rrggbb`                                                                                               |\n| `watermark.opacity`, `watermark.scale`  | de 0 a 1                                                                                                                 |\n| `watermark.rotation`                    | de -180 a 180 grados                                                                                                     |\n| `watermark.position`                    | `center`, `top-left`, `top-center`, `top-right`, `left`, `right`, `bottom-left`, `bottom-center` o `bottom-right`        |\n| `watermark.on_top`                      | `true` dibuja la marca sobre el contenido                                                                                |\n| `post_process.pdfa`                     | `true` convierte a PDF/A-2b (archivado a largo plazo)                                                                    |\n| `post_process.protection.user_password` | **obligatoria** si hay `protection`, hasta 128 caracteres: contraseña para abrir el archivo                              |\n| `protection.owner_password`             | contraseña de propietario, hasta 128 caracteres                                                                          |\n| `protection.permissions`                | qué puede hacer el lector: `print`, `copy`, `modify`, `annotate`, `fill_forms`, `assemble`. Lista vacía = ningún permiso |\n\n`pdfa` y `protection` no se combinan (PDF/A prohíbe el cifrado): la API responde `pdf.invalid_option`. Las contraseñas se guardan cifradas solo para que el worker las aplique y no vuelven en las respuestas.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n  \"html\": \"<html><body><h1>Contrato</h1></body></html>\",\n  \"format\": \"pdf\",\n  \"post_process\": {\n    \"watermark\": { \"text\": \"CONFIDENCIAL\", \"color\": \"#cc0000\", \"opacity\": 0.2, \"rotation\": -45, \"position\": \"center\" },\n    \"protection\": { \"user_password\": \"contraseña-lector\", \"permissions\": [\"print\"] }\n  }\n}'\n```"},{"anchor":"templates","heading":"Plantillas guardadas","markdown":"Guarda en la cuenta el HTML de un documento recurrente y renderízalo solo con `template_id` y `data`. Cada cambio de contenido crea una **versión** nueva; las anteriores siguen disponibles.\n\n* En `POST /convert`, `template_id` no se combina con `html` ni `url` (`pdf.invalid_option`). `template_version` elige la versión; `0` u omitido usa la más reciente.\n* El `css`, el `header_html` y el `footer_html` de la plantilla valen como predeterminados; si los envías en la conversión, los tuyos prevalecen.\n* Plantilla o versión inexistente: `pdf.template_not_found`. Nombre de hasta 120 caracteres; el `html` sigue el límite de <Limit name=\"pdfHtmlMax\" /> y el `css` el de <Limit name=\"pdfCssMax\" />.\n* Las lecturas usan el scope `tools:pdf:read`; las escrituras, `tools:pdf:manage` (o `tools:manage`). Ninguna cobra créditos ni consume la cuota diaria gratuita.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n  \"template_id\": \"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a\",\n  \"template_version\": 0,\n  \"data\": { \"numero\": \"001\" }\n}'\n```"},{"anchor":"listar-plantillas-guardadas","heading":"Listar plantillas guardadas","markdown":"<Endpoint id=\"pdf.templates.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  `limit` (predeterminado y máximo 100) y `offset`. La respuesta trae `items` y `total`.\n</Endpoint>"},{"anchor":"guardar-plantilla","heading":"Guardar plantilla","markdown":"<Endpoint\n  id=\"pdf.templates.create\"\n  example={{\n  body: `{\n\"name\": \"Fatura padrão\",\n\"description\": \"Fatura mensal com logotipo\",\n\"html\": \"<html><body><h1>Fatura {{numero}}</h1></body></html>\",\n\"css\": \"h1 { color: #333; }\",\n\"header_html\": \"\",\n\"footer_html\": \"\"\n}`,\n}}\n>\n  `name` (hasta 120 caracteres) y `html` son obligatorios; `description`, `css`, `header_html` y `footer_html` son opcionales. Crea la versión 1.\n</Endpoint>"},{"anchor":"consultar-plantilla","heading":"Consultar plantilla","markdown":"<Endpoint id=\"pdf.templates.get\" example={{ query: { version: '2' } }}>\n  `version` opcional (predeterminado: la actual). Devuelve la `template` y el contenido de la `version`.\n</Endpoint>"},{"anchor":"listar-versiones-de-la-plantilla","heading":"Listar versiones de la plantilla","markdown":"<Endpoint id=\"pdf.templates.versions\" />"},{"anchor":"renombrar-plantilla-o-guardar-una-nueva-versión","heading":"Renombrar plantilla o guardar una nueva versión","markdown":"<Endpoint\n  id=\"pdf.templates.update\"\n  example={{\n  body: `{\n\"name\": \"Fatura padrão v2\",\n\"html\": \"<html><body><h1>Fatura {{numero}}</h1><p>{{cliente}}</p></body></html>\",\n\"css\": \"h1 { color: #333; }\"\n}`,\n}}\n>\n  Envía `name` y/o `description` para renombrar. Con `html`, se guarda una **nueva versión** con el `html`, `css`, `header_html` y `footer_html` del cuerpo, y `current_version` sube. Los campos de contenido omitidos quedan vacíos en la nueva versión: reenvía el contenido completo.\n</Endpoint>"},{"anchor":"eliminar-plantilla","heading":"Eliminar plantilla","markdown":"<Endpoint id=\"pdf.templates.delete\">\n  Elimina la plantilla y todas sus versiones. Las conversiones ya generadas no se ven afectadas.\n</Endpoint>"},{"anchor":"fontes","heading":"Fuentes","markdown":"Sube tus propias fuentes (`woff2` o `ttf`) y úsalas en el HTML con `font-family`: pasa los ids en `font_ids` (hasta 20 por conversión) y la plataforma inyecta el `@font-face` correspondiente. Fuente inexistente en `font_ids`: `pdf.font_not_found`. El adicional de fuentes se cobra una vez por documento que usa `font_ids`.\n\n* Hasta **50 fuentes por cuenta** (`pdf.font_limit`).\n* Una combinación de `family` + `weight` + `style` solo puede existir una vez (`pdf.font_exists`, 409).\n* El formato se detecta por el contenido del archivo, no por la extensión."},{"anchor":"listar-fuentes","heading":"Listar fuentes","markdown":"<Endpoint id=\"pdf.fonts.list\" />"},{"anchor":"subir-fuente","heading":"Subir fuente","markdown":"<Endpoint id=\"pdf.fonts.upload\">\n  Envío `multipart/form-data` con `file` (obligatorio, `woff2` o `ttf`, hasta 5 MiB), `family` (obligatorio, de 1 a 64 letras, números, espacios, `-` o `_`), `weight` (`100` a `900` en pasos de 100; predeterminado `400`) y `style` (`normal` o `italic`; predeterminado `normal`). Cualquier infracción devuelve `pdf.font_invalid`.\n\n  ```bash\n  curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/fonts' \\\n    -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n    -F 'file=@Inter-Regular.woff2' \\\n    -F 'family=Inter' \\\n    -F 'weight=400' \\\n    -F 'style=normal'\n  ```\n\n  Luego, convierte con la fuente:\n\n  ```bash\n  curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n    -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"html\": \"<html><body style=\\\"font-family: Inter\\\"><h1>Hola</h1></body></html>\",\n    \"font_ids\": [\"2b3c4d5e-6f70-4a8b-9c0d-1e2f3a4b5c6d\"]\n  }'\n  ```\n</Endpoint>"},{"anchor":"eliminar-fuente","heading":"Eliminar fuente","markdown":"<Endpoint id=\"pdf.fonts.delete\">\n  Elimina la fuente y el archivo almacenado.\n</Endpoint>"},{"anchor":"consulta-y-descarga","heading":"Consulta y descarga","markdown":""},{"anchor":"listar-conversiones","heading":"Listar conversiones","markdown":"<Endpoint id=\"pdf.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  Query: `limit` (predeterminado 20), `offset` y `exclude_batch=true` para ocultar los ítems de batch. En el listado, `file_url` y `error_message` no se completan — consulta el [estado](#pdf-get) de cada conversión.\n</Endpoint>"},{"anchor":"estado-de-la-conversión","heading":"Estado de la conversión","markdown":"<Endpoint id=\"pdf.get\">\n  `status`: `pending`, `processing`, `completed` o `failed`. En caso de falla, `error_code` y `error_message` explican el motivo. `data` incluye las variables enviadas y `file_url` aparece cuando el job termina — para descargar, usa siempre [Descargar archivo](#pdf-download).\n</Endpoint>"},{"anchor":"descargar-archivo","heading":"Descargar archivo","markdown":"<Endpoint id=\"pdf.download\" example={{ response: 'binary' }}>\n  Requiere `status: completed`. El archivo se descifra y se descomprime durante la descarga. `X-Dry-Run` no aplica a las descargas.\n\n  El `file_url` de la conversión (o el del detalle del batch) es un **enlace firmado** con `?sig=&exp=`: descarga el archivo sin el encabezado `X-API-Token` y sin consumir la cuota diaria gratuita, así que sirve para entregar el enlace a un cliente final. Es válido hasta la fecha `exp`; después, consulta la conversión de nuevo para obtener otro.\n</Endpoint>"},{"anchor":"batches","heading":"Batches","markdown":""},{"anchor":"listar-batches","heading":"Listar batches","markdown":"<Endpoint id=\"pdf.batches\" example={{ query: { limit: '20', offset: '0' } }}>\n  `status` del batch: `processing`, `delivering`, `completed`, `partially_failed` o `failed`.\n</Endpoint>"},{"anchor":"detalle-del-batch","heading":"Detalle del batch","markdown":"<Endpoint id=\"pdf.batch-get\" />"},{"anchor":"descarga-zip-del-batch","heading":"Descarga ZIP del batch","markdown":"<Endpoint id=\"pdf.batch-download\" example={{ response: 'binary' }}>\n  Disponible cuando el batch termina — incluso parcialmente, si algunos ítems fallan. También acepta el enlace firmado `?sig=&exp=`, sin token.\n</Endpoint>"},{"anchor":"errores","heading":"Errores","markdown":"<ErrorTable tool=\"pdf-convert\" />"},{"anchor":"particularidades","heading":"Particularidades","markdown":"* Un batch usa una única plantilla HTML; cada ítem de `items[].data` completa los placeholders. `custom_id` ayuda a relacionar los archivos generados con tus registros.\n* Para PDFs recurrentes, guarda una [plantilla](#templates) en lugar de reenviar el HTML en cada llamada.\n* `retention_days: 0` es efímero (\\~24 h) — ideal para vistas previas.\n* Prefiere `webhook_url` a consultar el estado en bucle: cada consulta vía API cuenta para la cuota diaria gratuita."}]},{"slug":"ferramentas/qr-dinamico","url":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico","title":"QR Dinámico","description":"API de códigos QR dinámicos — creación, gestión, analytics, costos y redirect público.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"qr-dinamico\" />"},{"anchor":"descripción-general","heading":"Descripción general","markdown":"El QR dinámico separa la **imagen impresa** (enlace corto estable) del **destino** (editable sin reimprimir). Flujo típico:\n\n1. `POST` con `X-Dry-Run: true` para validar el payload y ver el costo.\n2. `POST` definitivo con `X-Idempotency-Key` → recibe la imagen del QR, el `resource_id` y el `slug`.\n3. Usa la imagen devuelta (o genera la tuya apuntando a `full_url`, disponible en `GET /{id}`).\n4. Sigue los escaneos mediante analytics o con el webhook `qr.dynamic.scanned`. El webhook es de mejor esfuerzo (en picos de tráfico, algunos eventos pueden no enviarse); para conteos exactos, usa analytics.\n\n<Callout type=\"warn\" title=\"El redirect público cobra por escaneo\">\n  `GET /r/{short_id}` no usa autenticación, pero cada escaneo puede debitar créditos de la cuenta dueña del QR. Sin saldo, el QR puede desactivarse. Los escaneos repetidos del mismo visitante en el mismo QR dentro de 30 segundos se cobran una sola vez: siguen redirigiendo, pero no entran en analytics, no disparan el webhook ni consumen el `scan_limit`.\n</Callout>\n\n<EndpointList tool=\"qr-dinamico\" />"},{"anchor":"custos","heading":"Costos","markdown":"<PricingTable tool=\"qr-dinamico\" group=\"creation\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"domain\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"batch\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"scans\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"storage\" />\n\nLas lecturas vía API (listado, detalle, analytics, escaneos, verificación de slug) siguen la [cuota diaria gratuita](/es/cobranca-e-limites#franquia-diaria). `PATCH`, activar, desactivar y `DELETE` son gratuitos."},{"anchor":"creación-y-gestión","heading":"Creación y gestión","markdown":""},{"anchor":"crear-qr-dinámico","heading":"Crear QR dinámico","markdown":"<Endpoint\n  id=\"qr.create\"\n  example={{\n  headers: { 'X-Idempotency-Key': '550e8400-e29b-41d4-a716-446655440000' },\n  body: `{\n\"name\": \"Campanha verão\",\n\"target_url\": \"https://loja.example.com/promo\",\n\"custom_slug\": \"promo-verao\",\n\"foreground_color\": \"#000000\",\n\"background_color\": \"#ffffff\"\n}`,\n}}\n>\n  Campos principales: `name` y `target_url` (la `target_url` es opcional cuando hay [`content`](#conteudo-hospedado)). Los colores y el estilo van en el propio cuerpo (`foreground_color`, `background_color`, `module_style`, `eye_style`, `size`, `logo_data_url`…). Opcionales: `custom_slug`, `custom_domain_id` ([dominio propio](#dominio-proprio)), `expires_at`, `scan_limit`, `skip_image_generation`, `utm_forwarding`, `folder_id` y `tags` ([organización](#organizacao)), `frame` y `dpi` ([impresión](#impressao)) y las opciones de [comportamiento del escaneo](#comportamento-do-scan) — cada una puede sumar costo (consulta [Costos](#custos)). `response_types` elige los formatos de la imagen (`svg`, `png`, `pdf` y `eps`; predeterminado `svg`): `svg` y `eps` llegan como texto, `png` y `pdf` en base64.\n\n  La respuesta incluye las imágenes, el `resource_id` (id del QR) y el `slug`. Con `skip_image_generation: true`, llega un único ítem solo con `resource_id` y `slug`.\n</Endpoint>"},{"anchor":"listar-qrs","heading":"Listar QRs","markdown":"<Endpoint id=\"qr.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  Query: `limit` (predeterminado 20, máx. <Limit name=\"pageSizeMax\" />), `offset`, `search`, `folder_id` (un UUID, o `none` para los QR sin carpeta), `tags` (separadas por coma; el QR debe tenerlas todas) y `status` (`active` o `paused`).\n</Endpoint>"},{"anchor":"detalle","heading":"Detalle","markdown":"<Endpoint id=\"qr.get\" />"},{"anchor":"actualizar-destino-nombre-o-diseño","heading":"Actualizar destino, nombre o diseño","markdown":"<Endpoint\n  id=\"qr.update\"\n  example={{\n  body: `{\n\"name\": \"Campanha atualizada\",\n\"target_url\": \"https://loja.example.com/nova-promo\"\n}`,\n}}\n>\n  Cambiar el nombre, el destino y los colores es gratis. Activar una opción de pago que estaba desactivada — `utm_forwarding`, `scan_limit` (de 0 a un valor), degradado (`foreground_color_end`), ojos personalizados (`eye_style` o `eye_dot_style` `shield`/`flower`) o logo — cuesta lo mismo que en la [creación](#custos), una sola vez por actualización. Cambiar una opción ya activada o desactivarla es gratis, sin reembolso. Acepta `X-Dry-Run`, `X-Include-Cost` y `X-Idempotency-Key`; sin saldo la respuesta es `402` y nada cambia, una actualización idéntica ya cobrada responde `409`, y un color inválido se rechaza (`400`) antes de cobrar. La nueva `target_url` pasa por las verificaciones anti-malware y anti-loop. Al igual que al activar, desactivar y eliminar, la respuesta es solo una confirmación — consulta `GET /{id}` para ver el recurso actualizado.\n</Endpoint>"},{"anchor":"reactivar","heading":"Reactivar","markdown":"<Endpoint id=\"qr.activate\" />"},{"anchor":"desactivar","heading":"Desactivar","markdown":"<Endpoint id=\"qr.deactivate\">\n  Un QR desactivado muestra una página de QR inactivo en el redirect público y sigue ocupando un slot hasta que se elimine.\n</Endpoint>"},{"anchor":"eliminar","heading":"Eliminar","markdown":"<Endpoint id=\"qr.delete\">\n  Termina el cobro de almacenamiento del slot.\n</Endpoint>"},{"anchor":"verificar-disponibilidad-de-slug","heading":"Verificar disponibilidad de slug","markdown":"<Endpoint id=\"qr.check-slug\" example={{ pathParams: { slug: 'promo-verao' } }} />"},{"anchor":"analytics","heading":"Analytics","markdown":""},{"anchor":"resumen-de-analytics","heading":"Resumen de analytics","markdown":"<Endpoint id=\"qr.analytics\" example={{ query: { period: 'last_30d' } }}>\n  Agregaciones con caché. Query `period`: `last_7d`, `last_30d` (predeterminado), `last_90d` o `all`; los valores desconocidos cuentan como 30 días. `scan_history` incluye los 20 escaneos más recientes.\n</Endpoint>"},{"anchor":"escaneos-por-período","heading":"Escaneos por período","markdown":"<Endpoint id=\"qr.analytics-period\" example={{ query: { period: 'last_30d' } }}>\n  Devuelve los totales y `period_stats` (escaneos por día). Los campos `period`, `period_scans`, `period_cost` y `unique_scans` vienen en cero en esta ruta — usa el [resumen](#qr-analytics) si los necesitas.\n</Endpoint>"},{"anchor":"distribución-por-dispositivo","heading":"Distribución por dispositivo","markdown":"<Endpoint id=\"qr.analytics-device\" example={{ query: { period: 'last_30d' } }}>\n  `device` es `Mobile`, `Desktop` o `Tablet`. Al igual que en escaneos por período, los campos de período vienen en cero.\n</Endpoint>"},{"anchor":"historial-de-escaneos","heading":"Historial de escaneos","markdown":"<Endpoint id=\"qr.scans\" example={{ query: { limit: '50', offset: '0', period: 'last_30d' } }}>\n  Paginación: `limit` (predeterminado 50, máx. <Limit name=\"pageSizeMax\" />), `offset` y `period` (predeterminado: últimos 30 días). `location` llega como `Unknown` cuando la ciudad y el país son desconocidos.\n</Endpoint>"},{"anchor":"redirect-público","heading":"Redirect público","markdown":"<Endpoint id=\"qr.redirect\" example={false}>\n  * **Activo:** `307` a la `target_url` (con la query string reenviada, si el UTM forwarding está activo). En cuentas sin plan premium, el escaneo pasa antes por una página intermedia de la marca que redirige en 3 segundos.\n  * **Inexistente, inactivo, vencido, sin saldo, por encima del límite de escaneos o con destino inseguro:** redirige a la página de QR no disponible, con el motivo en `?reason=` (`unknown`, `inactive`, `expired`, `bankrupt`, `limit_reached`, `safety_risk`, `rate_limited`).\n  * **Demasiados accesos del mismo visitante:** por encima de 600 por minuto (sumando todos los QR), redirige a la página de QR no disponible con `?reason=rate_limited`. Por encima de 30 escaneos por minuto en el mismo QR, el redirect sigue funcionando, pero los escaneos extra no se cuentan ni se cobran.\n  * Las respuestas nunca se almacenan en caché (`Cache-Control: no-store`).\n\n  ```bash\n  curl -sS -I 'https://api.respawntech.dev/r/promo-verao'\n  ```\n</Endpoint>"},{"anchor":"dominio-proprio","heading":"Dominio propio","markdown":"En lugar de `api.respawntech.dev/r/promo-verao`, el QR puede vivir en `https://qr.suaempresa.com/promo-verao`. Apuntas un **subdominio tuyo** a Respawn Tech con un registro CNAME; el certificado HTTPS se emite automáticamente (Cloudflare for SaaS). El `hostname` debe ser un subdominio con al menos tres etiquetas (`qr.suaempresa.com`): un dominio raíz (apex) no admite CNAME, y se rechazan puerto, ruta, IP y comodín.\n\nEl scope es `tools:qr:domains:manage` (o `tools:qr:dynamic:manage` / `tools:manage`). El límite de dominios depende del plan (`qr_domain.plan_limit`) y un dominio activo se cobra cada 30 días (consulta [Costos](#custos)).\n\n<Steps>\n  <Step>\n    ### Registra el dominio\n\n    ```bash\n    curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains' \\\n      -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n      -H 'Content-Type: application/json' \\\n      -d '{\n        \"hostname\": \"qr.suaempresa.com\",\n        \"validation_method\": \"http\",\n        \"root_url\": \"https://www.suaempresa.com\"\n      }'\n    ```\n\n    `validation_method` es `http` (predeterminado) o `txt`: es la forma en que se valida el certificado. La respuesta (`201`) trae `status: \"pending\"` y la lista `records` con todo lo que necesitas publicar. Guarda el `id`.\n  </Step>\n\n  <Step>\n    ### Publica el CNAME\n\n    En el panel de DNS de tu dominio, crea el registro del ítem con `purpose: \"routing\"`. El destino (`value`) viene en la respuesta; no lo copies de ejemplos.\n\n    | Tipo    | Nombre                       | Valor                             |\n    | ------- | ---------------------------- | --------------------------------- |\n    | `CNAME` | `qr` (o `qr.suaempresa.com`) | el `value` del registro `routing` |\n\n    En **Cloudflare**, crea el CNAME en la zona `suaempresa.com` y deja el proxy **desactivado** (nube gris, \"solo DNS\") hasta que el dominio quede `active`; solo entonces, si quieres, activa el proxy. En otros proveedores, es un CNAME común para el subdominio.\n  </Step>\n\n  <Step>\n    ### Valida el certificado\n\n    Si elegiste `http`, el registro `purpose: \"certificate\"` con `type: \"HTTP\"` se valida solo apenas el CNAME se propague, sin nada que publicar. Si elegiste `txt`, publica también los registros `TXT` que lleguen en `records` (`ownership` y `certificate`), en el mismo panel de DNS.\n  </Step>\n\n  <Step>\n    ### Verifica hasta que quede activo\n\n    La plataforma consulta el dominio periódicamente; para no esperar, fuerza la verificación:\n\n    ```bash\n    curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains/$DOMAIN_ID/refresh' \\\n      -H \"X-API-Token: $RESPAWN_API_TOKEN\"\n    ```\n\n    El `status` pasa de `pending` a `active` (el cobro del período ocurre en ese momento). `failed` indica que el proveedor rechazó el hostname (consulta `last_error`); `suspended`, que no se pudo cobrar el período — los escaneos van a la página de fallback hasta el próximo cobro exitoso.\n  </Step>\n\n  <Step>\n    ### Crea QRs en el dominio\n\n    Envía el `id` del dominio en `custom_domain_id`. Un dominio aún `pending` ya se acepta, para que imprimas mientras el DNS se propaga, pero los escaneos solo redirigen después de que quede `active`.\n\n    ```bash\n    curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n      -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n      -H 'Content-Type: application/json' \\\n      -d '{\n        \"name\": \"Cartaz da loja\",\n        \"target_url\": \"https://loja.example.com/promo\",\n        \"custom_slug\": \"promo-verao\",\n        \"custom_domain_id\": \"'\"$DOMAIN_ID\"'\"\n      }'\n    ```\n\n    El `full_url` del QR pasa a ser `https://qr.suaempresa.com/promo-verao`. El slug solo necesita ser único dentro del dominio.\n  </Step>\n</Steps>\n\n`root_url` es adónde va la raíz del dominio (`https://qr.suaempresa.com/`) y `not_found_url`, adónde van los slugs desconocidos; sin ellos, rige la página de fallback de la plataforma. Ninguno de los dos puede apuntar de vuelta al propio dominio. Solo `root_url` y `not_found_url` cambian después del registro (`PATCH`); para cambiar el hostname, elimina y registra de nuevo.\n\n<Callout type=\"info\" title=\"Eliminar un dominio\">\n  Un dominio con QRs aún vinculados no se puede eliminar (`qr_domain.in_use`). Elimina primero los QRs; una vez eliminado el dominio, su `full_url` deja de funcionar.\n</Callout>"},{"anchor":"registra-el-dominio","heading":"Listar dominios propios","markdown":"<Endpoint id=\"qr.domains-list\" />"},{"anchor":"publica-el-cname","heading":"Detalle del dominio","markdown":"<Endpoint id=\"qr.domains-get\" />"},{"anchor":"valida-el-certificado","heading":"Registrar dominio propio","markdown":"<Endpoint\n  id=\"qr.domains-create\"\n  example={{\n  body: `{\n\"hostname\": \"qr.suaempresa.com\",\n\"validation_method\": \"http\",\n\"root_url\": \"https://www.suaempresa.com\"\n}`,\n}}\n/>"},{"anchor":"verifica-hasta-que-quede-activo","heading":"Editar páginas de fallback del dominio","markdown":"<Endpoint\n  id=\"qr.domains-update\"\n  example={{\n  body: `{\n\"root_url\": \"https://www.suaempresa.com\",\n\"not_found_url\": \"https://www.suaempresa.com/ops\"\n}`,\n}}\n>\n  Envía `\"\"` para limpiar uno de los campos.\n</Endpoint>"},{"anchor":"crea-qrs-en-el-dominio","heading":"Verificar el dominio ahora","markdown":"<Endpoint id=\"qr.domains-refresh\" />"},{"anchor":"listar-dominios-propios","heading":"Eliminar dominio","markdown":"<Endpoint id=\"qr.domains-delete\" />\n\n## Comportamiento del escaneo\n\nPor defecto el escaneo es un redirect a la `target_url`. Estos campos, aceptados en el `POST` y en el `PATCH`, cambian lo que ocurre en el escaneo. La evaluación sigue este orden: **contraseña** (el escaneo solo continúa después de ella), **reglas** (gana la primera que coincida), **prueba A/B** (para los escaneos que ninguna regla tomó) y, por último, la `target_url`. Cada opción se cobra una vez (consulta [Costos](#custos)).\n\nEn un `PATCH`, cada campo enviado **reemplaza** su parte: `[]` limpia las reglas o las variantes, `{}` limpia los píxeles y `\"\"` limpia una URL o la contraseña."},{"anchor":"detalle-del-dominio","heading":"Reglas de redirect","markdown":"`redirect_rules` (hasta 20) envía el escaneo a una URL propia cuando todas las condiciones de la regla coinciden. Una regla sin ninguna condición se rechaza.\n\n| Campo                    | Significado                                                                                            |\n| ------------------------ | ------------------------------------------------------------------------------------------------------ |\n| `id`                     | Nombre de la regla (`[a-zA-Z0-9_-]`, hasta 40); aparece en los escaneos y en `rule_stats` de analytics |\n| `target_url`             | Destino cuando la regla coincide (URL absoluta)                                                        |\n| `countries`              | Países ISO 3166-1 alfa-2 en mayúsculas (`[\"BR\", \"PT\"]`)                                                |\n| `devices`                | `mobile`, `tablet`, `desktop`                                                                          |\n| `os`                     | `ios`, `android`, `windows`, `macos`, `linux`, `other`                                                 |\n| `languages`              | Idioma preferido del visitante: `pt` coincide con `pt` y `pt-br`; `pt-br` solo con `pt-br`             |\n| `days`                   | Días de la semana, `0` (domingo) a `6` (sábado), en el `timezone`                                      |\n| `start_time`, `end_time` | Ventana del día (`HH:MM`); si el fin viene antes del inicio, cruza la medianoche. Los dos o ninguno    |\n| `timezone`               | Zona horaria IANA (`America/Sao_Paulo`); UTC si está vacío                                             |\n| `from`, `until`          | Período en que la regla vale (fecha y hora RFC 3339)                                                   |\n\nCada lista acepta hasta 50 valores. Dentro de una regla las condiciones se suman (Y); dentro de una lista, basta un valor (O)."},{"anchor":"registrar-dominio-propio","heading":"Prueba A/B","markdown":"`ab_variants` (de 2 a 10) divide, por peso, los escaneos que ninguna regla tomó. Cada variante tiene `id` único, `target_url` y `weight` de 1 a 1000; la porción de una variante es su peso dividido por la suma. Analytics devuelve `variant_stats` con los escaneos de cada variante."},{"anchor":"editar-páginas-de-fallback-del-dominio","heading":"Contraseña","markdown":"`password` (de 4 a 72 caracteres) oculta el QR detrás de una página de contraseña. La contraseña se guarda con hash; las respuestas solo traen `scan_behavior.has_password`. Tras escribir la contraseña correcta, el visitante sigue el flujo normal y esa página no se almacena en caché. Tras 5 contraseñas incorrectas, el visitante queda bloqueado en ese QR por 15 minutos."},{"anchor":"verificar-el-dominio-ahora","heading":"Píxeles","markdown":"`pixels` dispara las etiquetas en una página ligera antes del redirect: `ga4_measurement_id` (`G-XXXXXXX`), `meta_pixel_id` (solo dígitos) y `gtm_container_id` (`GTM-XXXXXX`)."},{"anchor":"eliminar-dominio","heading":"Redirect de vencido y agotado","markdown":"`expired_url` y `limit_reached_url` (URLs absolutas) reemplazan la página de QR no disponible cuando el QR venció (`expires_at`) o usó todo el `scan_limit`.\n\n### Ejemplo completo\n\nCopia, cambia las URLs y ejecuta:\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Campanha com regras\",\n    \"target_url\": \"https://loja.example.com/global\",\n    \"redirect_rules\": [\n      {\n        \"id\": \"br-mobile\",\n        \"target_url\": \"https://loja.example.com/br\",\n        \"countries\": [\"BR\"],\n        \"devices\": [\"mobile\"]\n      },\n      {\n        \"id\": \"horario-comercial\",\n        \"target_url\": \"https://loja.example.com/atendimento\",\n        \"days\": [1, 2, 3, 4, 5],\n        \"start_time\": \"09:00\",\n        \"end_time\": \"18:00\",\n        \"timezone\": \"America/Sao_Paulo\"\n      }\n    ],\n    \"ab_variants\": [\n      { \"id\": \"a\", \"target_url\": \"https://loja.example.com/lp-a\", \"weight\": 50 },\n      { \"id\": \"b\", \"target_url\": \"https://loja.example.com/lp-b\", \"weight\": 50 }\n    ],\n    \"pixels\": { \"ga4_measurement_id\": \"G-ABCD1234\" },\n    \"expired_url\": \"https://loja.example.com/encerrada\",\n    \"limit_reached_url\": \"https://loja.example.com/esgotado\"\n  }'\n```\n\nPara quitar todo después, sin recrear el QR (cambia `$QR_ID` por el `resource_id` de la creación):\n\n```bash\ncurl -sS -X PATCH \"https://api.respawntech.dev/v1/tools/image/qr-code/dynamic/$QR_ID\" \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{ \"redirect_rules\": [], \"ab_variants\": [], \"pixels\": {}, \"password\": \"\" }'\n```\n\n`GET /{id}` devuelve la configuración en `scan_behavior` (`redirect_rules`, `ab_variants`, `has_password`, `pixels`, `expired_url`, `limit_reached_url`, `content`)."},{"anchor":"comportamento-do-scan","heading":"Contenido alojado","markdown":"Con `content`, el propio `/r/{short_id}` sirve una página ligera (o un redirect calculado) en lugar de la `target_url`. El `type` elige el formato y solo se usa el campo del mismo nombre. Si una regla de redirect o una prueba A/B coincide, esa vale en lugar del contenido. Las páginas no ejecutan scripts y se almacenan en caché por 5 minutos (revalidadas por ETag). `{}` limpia el contenido en un `PATCH`.\n\n| `type`      | Campo                                                                                     | El escaneo muestra                                                                                             |\n| ----------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |\n| `vcard`     | `vcard` (`FirstName`, `LastName`, `Org`, `Title`, `Phone`, `Email`, `Website`, `Address`) | Página de contacto y descarga del `.vcf` (`?format=vcf`). Exige nombre u organización                          |\n| `file`      | `file` (`url`, `name`)                                                                    | Abre el archivo o PDF alojado (URL absoluta)                                                                   |\n| `links`     | `links` (`title`, `description`, `avatar_url`, `links[]` con `label` y `url`)             | Link en bio, de 1 a 50 enlaces                                                                                 |\n| `menu`      | `menu` (`title`, `currency`, `sections[]` → `items[]` con `name`, `description`, `price`) | Menú, hasta 30 secciones y 300 ítems                                                                           |\n| `wifi`      | `wifi` (`SSID`, `Password`, `Security`, `Hidden`)                                         | Nombre de la red y contraseña para copiar                                                                      |\n| `app_store` | `app_store` (`ios_url`, `android_url`, `fallback_url`)                                    | Redirect a la App Store o Google Play según el celular; `fallback_url` atiende desktop y sistemas desconocidos |\n\nLos campos de `vcard` y `wifi` usan esos nombres en PascalCase, exactamente como en la tabla; los demás usan `snake_case`. La `target_url` deja de ser obligatoria. Asume el enlace del archivo o de la tienda y, en los tipos de página, cae en la página de fallback de la plataforma.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Link na bio\",\n    \"content\": {\n      \"type\": \"links\",\n      \"links\": {\n        \"title\": \"Loja Exemplo\",\n        \"links\": [\n          { \"label\": \"Site\", \"url\": \"https://loja.example.com\" },\n          { \"label\": \"Instagram\", \"url\": \"https://instagram.com/lojaexemplo\" }\n        ]\n      }\n    }\n  }'\n```\n\nEjemplo de tarjeta de contacto:\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Cartão da Ana\",\n    \"content\": {\n      \"type\": \"vcard\",\n      \"vcard\": { \"FirstName\": \"Ana\", \"LastName\": \"Souza\", \"Org\": \"Loja Exemplo\", \"Phone\": \"+5511999990000\", \"Email\": \"ana@loja.example.com\" }\n    }\n  }'\n```\n\n## Marcos e impresión\n\n`frame` dibuja una llamada a la acción alrededor del código y `dpi` deja la imagen lista para imprimir. Ambos valen para cualquier `response_types`.\n\n* `frame`: `text` (hasta 32 caracteres; se reduce para caber), `color`, `text_color` (hex) y `position` (`top` o `bottom`).\n* `dpi`: de 72 a 1200. En el **PDF** la página queda del tamaño exacto del QR, en vector; en el **SVG** el ancho y el alto salen en mm; el **PNG** lleva la resolución de impresión en el archivo. Sin `dpi`, el PDF mantiene el layout A4.\n* `eps`: formato vectorial nuevo, con el marco y fuente Helvetica-Bold con acentos. Sale sin logo.\n\nEn el PDF y en el EPS, los ojos salen cuadrados y solo los estilos de módulo `dots` y cuadrado se vuelven vector; el degradado no se aplica.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Cartaz A5\",\n    \"target_url\": \"https://loja.example.com/promo\",\n    \"response_types\": [\"pdf\", \"eps\"],\n    \"frame\": { \"text\": \"Escaneie aqui\", \"color\": \"#111111\", \"text_color\": \"#FFFFFF\", \"position\": \"bottom\" },\n    \"dpi\": 300\n  }'\n```\n\n## Organización, edición masiva y lote"},{"anchor":"regras","heading":"Carpetas y tags","markdown":"`folder_id` guarda el QR en una carpeta y `tags` (hasta 20 por QR, 40 caracteres cada una, sin coma) lo etiqueta; ambos entran en el `POST` y en el `PATCH` (`folder_id: \"\"` vuelve a la raíz, `tags: []` limpia). El listado filtra por `folder_id`, `tags`, `status` y `search`."},{"anchor":"ab","heading":"Listar carpetas","markdown":"<Endpoint id=\"qr.folders-list\" />"},{"anchor":"senha","heading":"Crear carpeta","markdown":"<Endpoint id=\"qr.folders-create\" example={{ body: '{ \"name\": \"Campanha de verão\" }' }}>\n  El nombre tiene de 1 a 80 caracteres y es único en la cuenta (`dynamic_qr.folder_taken`).\n</Endpoint>"},{"anchor":"pixels","heading":"Renombrar carpeta","markdown":"<Endpoint id=\"qr.folders-rename\" example={{ body: '{ \"name\": \"Verão 2027\" }' }} />"},{"anchor":"fallback","heading":"Eliminar carpeta","markdown":"<Endpoint id=\"qr.folders-delete\" />"},{"anchor":"ejemplo-completo","heading":"Editar varios QRs a la vez","markdown":"<Endpoint\n  id=\"qr.bulk\"\n  example={{\n  body: `{\n\"ids\": [\"8f3a2b1c-4d5e-6f70-8192-a3b4c5d6e7f8\"],\n\"action\": \"add_tags\",\n\"tags\": [\"verao\"]\n}`,\n}}\n>\n  Una acción en hasta 500 QRs, gratis. `action` es `activate`, `deactivate`, `move` (con `folder_id`; `null` envía a la raíz), `set_target` (con `target_url`), `add_tags` o `remove_tags` (con `tags`). La respuesta trae el resultado de cada QR en `items`, así que un fallo no tumba a los demás. Acepta `X-Idempotency-Key`.\n</Endpoint>\n\n### Crear QRs en lote\n\n<Endpoint\n  id=\"qr.batches-create\"\n  example={{\n  headers: { 'X-Idempotency-Key': '7f1c2e84-5b3a-4d6c-9e01-a2b3c4d5e6f7' },\n  body: `{\n\"csv\": \"name,target_url,custom_slug,tags\\\\nLoja Centro,https://loja.example.com/centro,loja-centro,varejo|sp\\\\nLoja Norte,https://loja.example.com/norte,loja-norte,varejo\",\n\"defaults\": { \"utm_forwarding\": true }\n}`,\n}}\n>\n  Hasta 1.000 filas por lote, de una de estas formas:\n\n  * JSON con `rows` (una lista de objetos con los campos del QR) o `csv` (el texto del CSV), más `defaults` con los valores usados en las filas que no los traen (`folder_id`, `tags`, `scan_limit`, `expires_at`, `utm_forwarding`, colores, `custom_domain_id`);\n  * cuerpo `text/csv` directo, con `folder_id` y `tags` como parámetros de query.\n\n  El CSV tiene una fila de encabezado y las columnas `name`, `target_url` (obligatoria), `custom_slug`, `folder_id`, `tags` (separadas por `|`), `scan_limit`, `expires_at`, `utm_forwarding`, `foreground_color` y `background_color`:\n\n  ```csv\n  name,target_url,custom_slug,tags\n  Loja Centro,https://loja.example.com/centro,loja-centro,varejo|sp\n  Loja Norte,https://loja.example.com/norte,loja-norte,varejo\n  ```\n\n  ```bash\n  curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/batches?tags=campanha' \\\n    -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n    -H 'X-Idempotency-Key: 7f1c2e84-5b3a-4d6c-9e01-a2b3c4d5e6f7' \\\n    -H 'Content-Type: text/csv' \\\n    --data-binary @lojas.csv\n  ```\n\n  Las filas inválidas se reportan y **no** se cobran; las válidas se cobran de una vez, con descuento por volumen (a partir de 100, 500 y 1.000 filas), y se crean en segundo plano (`202`). Una fila que falle después se reembolsa. Haz seguimiento consultando el detalle del lote hasta que `pending` llegue a 0. Con el mismo `X-Idempotency-Key` y las mismas filas, el retry devuelve el mismo lote, cobrado una vez. Por encima del límite, la respuesta es `413` (`dynamic_qr.batch_too_large`).\n</Endpoint>\n\n### Listar lotes\n\n<Endpoint id=\"qr.batches-list\" />\n\n### Detalle del lote\n\n<Endpoint id=\"qr.batches-get\">\n  `items` trae cada fila (`row`, `status`, `qr_id`, `slug`, `full_url` o `error_code` y `error_message`). `charged_units`, `discount_units` y `refunded_units` están en las unidades internas de crédito (10.000 unidades = 1 crédito).\n</Endpoint>\n\n### Exportar escaneos en CSV\n\n<Endpoint id=\"qr.scans-export\" example={{ query: { period: 'last_30d' } }}>\n  Query: `period` (`last_7d`, `last_30d` predeterminado, `last_90d` o `all`) o `from` y `to` (`YYYY-MM-DD`, UTC, inclusive), que reemplazan el período. Se cobra como el [historial de escaneos](#qr-scans). Guarda con `curl -o scans.csv`.\n</Endpoint>\n\n### Borrar el historial de escaneos\n\n<Endpoint id=\"qr.scans-purge\" example={{ query: { before: '2026-01-01' } }}>\n  Borra las filas de escaneo (todas, o las anteriores a `before`, `YYYY-MM-DD` UTC). `total_scans` y `scan_limit` se mantienen. Es irreversible.\n</Endpoint>"},{"anchor":"conteudo-hospedado","heading":"Errores","markdown":"<ErrorTable tool=\"qr-dinamico\" />"},{"anchor":"impressao","heading":"Particularidades y límites","markdown":"* Las URLs maliciosas se bloquean automáticamente (`dynamic_qr.malicious_url`).\n* Protección anti-loop: el destino no puede apuntar al propio dominio de redirect.\n* El primer QR no ocupa un slot de pago; los demás ocupan slots en grupos de 3.\n* `skip_image_generation: true` reduce el costo cuando generas la imagen del QR por tu cuenta.\n* Slug: 3–50 caracteres `[a-zA-Z0-9-]`. Listados: hasta <Limit name=\"pageSizeMax\" /> ítems por página."}]},{"slug":"ferramentas/realtime","url":"https://docs.respawntech.dev/es/ferramentas/realtime","title":"Realtime","description":"Publica y recibe mensajes en tiempo real por WebSocket o SSE, con tokens emitidos en tu servidor, presencia, historial por canal, webhook de canales y reconexión sin perder mensajes.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"realtime\" />"},{"anchor":"como-funciona","heading":"Cómo funciona","markdown":"Realtime lleva mensajes de tu servidor, o de otros clientes, a los navegadores conectados, sin que mantengas servidores de WebSocket.\n\n* **Canal:** un nombre libre de hasta <Limit name=\"realtimeChannelNameMax\" /> caracteres, con letras, números y `: _ - .` (`chat:lobby`, `pedido:123`). Quien se suscribe al canal recibe lo que se publique en él. No hay nada que crear antes.\n* **Token:** credencial de corta duración que **tu servidor** emite con tu clave de API y entrega al cliente. Dice quién es el cliente (`client_id`, opcional) y en qué canales puede suscribirse, publicar y usar la presencia (`capabilities`).\n* **`connect_url`:** la dirección WebSocket a la que se conecta el cliente, devuelta en la misma respuesta del token y que ya incluye el `?token=`.\n* **Mensaje:** `data` JSON publicado en un canal, con `id`, `timestamp` (milisegundos Unix) y el `client_id` de quien lo publicó.\n* **Presencia:** la lista de quién está conectado y \"presente\" en un canal, con datos libres de cada miembro (un apodo, un estado).\n\nEl flujo es siempre el mismo:\n\n<Steps>\n  <Step>\n    El navegador pide un token a **tu servidor**, que ya sabe quién es el usuario.\n  </Step>\n\n  <Step>\n    Tu servidor [emite el token](#realtime-token) en la API con las capabilities de ese usuario y devuelve al navegador la respuesta completa: `token`, `expires_at` y `connect_url`.\n  </Step>\n\n  <Step>\n    El navegador se conecta a `connect_url`, se suscribe a canales y publica.\n  </Step>\n\n  <Step>\n    Si la conexión se cae, el navegador pide un token nuevo y [reconecta](#reconexao), recuperando los mensajes perdidos con `rewind`.\n  </Step>\n</Steps>\n\n<Callout type=\"warn\" title=\"Usa siempre el connect_url de la respuesta\">\n  Nunca guardes la dirección del WebSocket en el código ni en la configuración. El `connect_url` puede cambiar entre entornos (prueba y producción) y cuando la plataforma redistribuye las conexiones; un token solo vale en la dirección que vino con él.\n</Callout>\n\nTu clave de API (`X-API-Token`) se queda solo en el servidor. El navegador usa solo el token de conexión, que expira y solo ve los canales que liberaste.\n\n<EndpointList tool=\"realtime\" />"},{"anchor":"token-e-capabilities","heading":"Token y capabilities","markdown":""},{"anchor":"emitir-token-de-conexión","heading":"Emitir token de conexión","markdown":"<Endpoint id=\"realtime.token\" example={{ body: `{ \"client_id\": \"user-42\", \"capabilities\": { \"chat:*\": [\"subscribe\", \"publish\", \"presence\"], \"alerts\": [\"subscribe\"] }, \"ttl_seconds\": 3600 }` }}>\n  * `capabilities` (obligatorio): objeto de patrón de canal a la lista de operaciones permitidas, hasta <Limit name=\"realtimeTokenPatternsMax\" /> patrones. El patrón es un nombre exacto (`alerts`), un prefijo terminado en `*` (`chat:*`) o solo `*` (todos los canales). Las operaciones son `subscribe`, `publish`, `presence` (entrar en la presencia; leerla exige `subscribe`) o `*` (todas). Sin capability para el canal, la operación falla con `realtime.capability_denied`.\n  * `client_id` (opcional, hasta <Limit name=\"realtimeClientIdMax\" /> caracteres): identifica al cliente en los mensajes que publica y en la presencia. Usa el id de tu usuario; no aceptes este valor del navegador.\n  * `ttl_seconds` (opcional): validez del token, de <Limit name=\"realtimeTokenTtlMin\" /> hasta <Limit name=\"realtimeTokenTtlMax\" />, por defecto <Limit name=\"realtimeTokenTtlDefault\" />. El token solo se comprueba al conectar: una conexión abierta sigue valiendo después de que expire, pero la siguiente reconexión exige un token nuevo.\n\n  La respuesta trae `token`, `expires_at` y `connect_url` (que ya incluye `?token=`). Cuando la plataforma necesita encender un nodo antes de aceptar conexiones, la respuesta también trae `retry_after_ms`: el token es válido, pero **espera ese tiempo antes de conectar**.\n</Endpoint>\n\nDa a cada cliente solo lo que necesita: un chat con `{\"chat:lobby\": [\"subscribe\", \"publish\", \"presence\"]}`, un panel de solo lectura con `{\"metricas:*\": [\"subscribe\"]}`. El token se firma con HMAC (HS256) y solo vale para Realtime.\n\n<Callout title=\"Token del inspector\">\n  El dashboard usa la ruta `POST /v1/tools/realtime/inspector-token` para abrir el inspector en vivo (suscripción a todos los canales, sin publicar, 15 minutos). Solo acepta la sesión del dashboard; con una clave de API responde `realtime.session_required`.\n</Callout>"},{"anchor":"conexao-pelo-navegador","heading":"Conexión desde el navegador","markdown":"En tu servidor, expón una ruta que emita el token y reenvíe la respuesta. El ejemplo usa `RESPAWN_API_TOKEN` y una ruta `/realtime-token`; cambia el `client_id` por el usuario autenticado.\n\n<Tabs groupId=\"code-language\" persist items={['Node.js', 'Python']}>\n  <Tab value=\"Node.js\">\n    ```js\n    import { createServer } from 'node:http';\n\n    createServer(async (req, res) => {\n      if (req.url !== '/realtime-token') return res.writeHead(404).end();\n\n      const api = await fetch('https://api.respawntech.dev/v1/tools/realtime/tokens', {\n        method: 'POST',\n        headers: { 'X-API-Token': process.env.RESPAWN_API_TOKEN, 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          client_id: 'user-42', // the authenticated user\n          capabilities: { 'chat:*': ['subscribe', 'publish', 'presence'] },\n        }),\n      });\n      const { data } = await api.json();\n\n      // token, expires_at, connect_url and retry_after_ms go together to the browser\n      res.writeHead(api.status, { 'Content-Type': 'application/json' }).end(JSON.stringify(data));\n    }).listen(3000);\n    ```\n  </Tab>\n\n  <Tab value=\"Python\">\n    ```python\n    import os\n    import requests\n    from flask import Flask, jsonify\n\n    app = Flask(__name__)\n\n    @app.post(\"/realtime-token\")\n    def realtime_token():\n        api = requests.post(\n            \"https://api.respawntech.dev/v1/tools/realtime/tokens\",\n            headers={\"X-API-Token\": os.environ[\"RESPAWN_API_TOKEN\"]},\n            json={\n                \"client_id\": \"user-42\",  # the authenticated user\n                \"capabilities\": {\"chat:*\": [\"subscribe\", \"publish\", \"presence\"]},\n            },\n            timeout=30,\n        )\n        # token, expires_at, connect_url and retry_after_ms go together to the browser\n        return jsonify(api.json()[\"data\"]), api.status_code\n    ```\n  </Tab>\n</Tabs>\n\nEn el navegador, pide el token a tu servidor y conéctate al `connect_url` que vino con él. Este HTML funciona pegado en un archivo y servido por el mismo servidor de la ruta `/realtime-token` (en pruebas, usa una clave del entorno de prueba); ya reconecta solo. Si prefieres no escribir el protocolo a mano, usa el [SDK de JavaScript](#sdk).\n\n```html\n<!doctype html>\n<meta charset=\"utf-8\" />\n<title>Realtime</title>\n<ul id=\"log\"></ul>\n<input id=\"text\" placeholder=\"Message\" />\n<button id=\"send\">Send</button>\n\n<script>\n  const CHANNEL = 'chat:lobby';\n  const REWIND = 50; // messages recovered on each (re)connection\n  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n  const seen = new Set(); // ids already shown: rewind can repeat messages\n  let ws = null;\n  let attempt = 0;\n\n  function show(text) {\n    const li = document.createElement('li');\n    li.textContent = text;\n    document.getElementById('log').append(li);\n  }\n\n  async function connect() {\n    // A new token on every connection: covers expiry and close 4001\n    const res = await fetch('/realtime-token', { method: 'POST' });\n    if (!res.ok) return retry();\n    const { connect_url, retry_after_ms } = await res.json();\n    if (retry_after_ms) await sleep(retry_after_ms);\n\n    ws = new WebSocket(connect_url); // connect_url already carries ?token=\n\n    ws.onmessage = (event) => {\n      const frame = JSON.parse(event.data);\n      if (frame.action === 'connected') {\n        attempt = 0;\n        ws.send(JSON.stringify({ action: 'subscribe', channel: CHANNEL, rewind: REWIND }));\n      } else if (frame.action === 'message') {\n        if (seen.has(frame.id)) return;\n        seen.add(frame.id);\n        show(`${frame.client_id ?? '?'}: ${JSON.stringify(frame.data)}`);\n      } else if (frame.action === 'error') {\n        console.warn('realtime error', frame.code, frame.message);\n      }\n    };\n\n    ws.onclose = (event) => {\n      // 4003: Realtime disabled for the account, do not reconnect.\n      if (event.code === 4003) return;\n      // 1012: node restarting, reconnect now. 1013 and 4029: backoff.\n      // 4001: invalid token, the next connect() already issues another one.\n      retry(event.code === 1012 ? 0 : undefined);\n    };\n  }\n\n  async function retry(delay) {\n    attempt += 1;\n    // exponential backoff with jitter, capped at 30 s\n    const wait = delay ?? Math.random() * Math.min(30000, 500 * 2 ** attempt);\n    await sleep(wait);\n    connect();\n  }\n\n  document.getElementById('send').onclick = () => {\n    const input = document.getElementById('text');\n    if (ws?.readyState !== WebSocket.OPEN || !input.value) return;\n    ws.send(JSON.stringify({ action: 'publish', channel: CHANNEL, data: { text: input.value } }));\n    input.value = '';\n  };\n\n  connect();\n</script>\n```"},{"anchor":"protocolo","heading":"Protocolo","markdown":"Los mensajes de la conexión son frames de texto JSON, cada uno con un campo `action`. Tras conectar, espera el frame `connected` antes de suscribirte. El nombre del canal sigue la regla `^[A-Za-z0-9:_\\-.]{1,128}$`."},{"anchor":"del-cliente-al-servidor","heading":"Del cliente al servidor","markdown":"| `action`      | Campos                            | Qué hace                                                                                                                                                                                                            |\n| ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `subscribe`   | `channel`, `rewind?`              | Se suscribe al canal (exige `subscribe` en el token). Con `rewind` (número de mensajes, hasta <Limit name=\"realtimeRewindMax\" />), reenvía los últimos mensajes del canal antes de los nuevos. Responde `attached`. |\n| `unsubscribe` | `channel`                         | Deja de recibir el canal. Responde `detached`.                                                                                                                                                                      |\n| `publish`     | `channel`, `name?`, `data`, `id?` | Publica `data` (JSON) en el canal (exige `publish`). `name` clasifica el mensaje. Si envías `id`, el servidor responde `ack` con él.                                                                                |\n| `presence`    | `op`, `channel`, `data?`, `id?`   | Operaciones de [presencia](#presenca): `op` es `enter`, `update`, `leave` o `get`.                                                                                                                                  |"},{"anchor":"del-servidor-al-cliente","heading":"Del servidor al cliente","markdown":"| `action`    | Campos                                                      | Cuándo                                                                                                      |\n| ----------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |\n| `connected` | `connection_id`                                             | La conexión fue aceptada; ya puedes suscribirte.                                                            |\n| `attached`  | `channel`                                                   | La suscripción al canal está activa (tras el `rewind`, si se pidió).                                        |\n| `detached`  | `channel`                                                   | La suscripción terminó.                                                                                     |\n| `message`   | `channel`, `id`, `name?`, `data`, `timestamp`, `client_id?` | Un mensaje del canal, incluidos los que tú mismo publicaste. `timestamp` es un entero en milisegundos Unix. |\n| `ack`       | `id`, `channel?`                                            | La publicación (u operación de presencia) con ese `id` fue aceptada.                                        |\n| `presence`  | `op`, `channel`, `member` o `members`                       | Evento de presencia (`enter`, `update`, `leave`) o respuesta a `get` (`sync`).                              |\n| `error`     | `code`, `message`, `channel?`, `id?`                        | Una operación falló. Los `code` están en [Errores](#erros). La conexión sigue abierta.                      |\n\nEjemplo de intercambio:\n\n```json\n← { \"action\": \"connected\", \"connection_id\": \"c_8f3a1d\" }\n→ { \"action\": \"subscribe\", \"channel\": \"chat:lobby\", \"rewind\": 20 }\n← { \"action\": \"attached\", \"channel\": \"chat:lobby\" }\n→ { \"action\": \"publish\", \"channel\": \"chat:lobby\", \"id\": \"m-1\", \"data\": { \"text\": \"Hello!\" } }\n← { \"action\": \"message\", \"channel\": \"chat:lobby\", \"id\": \"1790338000000-0\", \"data\": { \"text\": \"Hello!\" }, \"timestamp\": 1790338000000, \"client_id\": \"user-42\" }\n← { \"action\": \"ack\", \"id\": \"m-1\" }\n```\n\nEl `id` del `message` lo genera el servidor y es el que usas para descartar repeticiones. El `id` del `publish` es solo tuyo, para casarlo con el `ack`."},{"anchor":"close-codes","heading":"Close codes","markdown":"| Código         | Significado                                                              | Qué hacer                                                          |\n| -------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------ |\n| `1012`         | El nodo se está reiniciando (despliegue o mantenimiento).                | Reconecta ya, con un pequeño jitter.                               |\n| `1013`         | El nodo está lleno.                                                      | Reconecta con backoff exponencial.                                 |\n| `4001`         | Token inválido o expirado (`realtime.token_invalid`).                    | Emite un token nuevo y reconecta; repetir el mismo token no sirve. |\n| `4003`         | Realtime desactivado para la cuenta (`realtime.disabled`).               | No reconectes.                                                     |\n| `4029`         | La cuenta alcanzó el límite de conexiones (`realtime.connection_limit`). | Reconecta con backoff exponencial.                                 |\n| Cualquier otro | Caída de red o error inesperado.                                         | Reconecta con backoff exponencial.                                 |"},{"anchor":"sse","heading":"SSE: lectura sin WebSocket","markdown":"Para clientes que no pueden abrir un WebSocket, el mismo token abre un flujo `text/event-stream` **de solo lectura**, en el mismo servidor del `connect_url`:\n\n```text\nGET https://<connect_url host>/v1/sse?token=<token>&channel=chat:lobby&channel=alerts&rewind=20\n```\n\n* Indica cada canal en un `channel=` (o varios en `channels=a,b`), hasta <Limit name=\"realtimeChannelsPerConnection\" />. El token necesita `subscribe` en todos.\n* Cada `data:` es un frame JSON igual al del WebSocket (`connected`, `attached`, `message`, `presence`, `error`); las líneas `: ping` mantienen viva la conexión.\n* El flujo cuenta en el límite de conexiones de la cuenta y termina cuando el token expira (antes llega un frame `error` con `realtime.token_invalid`) o cuando el nodo se reinicia.\n* Para publicar, usa la [API](#publicar) o un WebSocket.\n\n```js\nconst url = new URL(connectUrl); // the connect_url from the token response\nconst sse = new EventSource(\n  `https://${url.host}/v1/sse?token=${encodeURIComponent(token)}&channel=chat:lobby&rewind=20`,\n);\nsse.onmessage = (event) => {\n  const frame = JSON.parse(event.data);\n  if (frame.action === 'message') console.log(frame.data);\n};\n```"},{"anchor":"presenca","heading":"Presencia","markdown":"La presencia dice quién está en un canal ahora. Un cliente con la capability `presence` **entra** con datos libres (`{\"nick\": \"ana\"}`), puede **actualizar** esos datos y **sale** al dejar el canal o cuando la conexión se cae. Quien está suscrito al canal recibe los eventos, y `get` (o la API) devuelve la lista completa.\n\n| Frame del cliente                                                                | Exige       | Efecto                                         |\n| -------------------------------------------------------------------------------- | ----------- | ---------------------------------------------- |\n| `{\"action\":\"presence\",\"op\":\"enter\",\"channel\":\"chat:lobby\",\"data\":{…},\"id\":\"p1\"}` | `presence`  | Entra en la presencia; responde `ack`.         |\n| `{\"action\":\"presence\",\"op\":\"update\",\"channel\":\"chat:lobby\",\"data\":{…}}`          | `presence`  | Actualiza los datos del miembro.               |\n| `{\"action\":\"presence\",\"op\":\"leave\",\"channel\":\"chat:lobby\"}`                      | `presence`  | Sale de la presencia.                          |\n| `{\"action\":\"presence\",\"op\":\"get\",\"channel\":\"chat:lobby\",\"id\":\"p2\"}`              | `subscribe` | Responde `op: \"sync\"` con todos los `members`. |\n\nLos suscriptores reciben `{\"action\":\"presence\",\"op\":\"enter|update|leave\",\"channel\":\"chat:lobby\",\"member\":{\"connection_id\":\"…\",\"client_id\":\"user-42\",\"data\":{…},\"timestamp\":1790338000000}}`. Cada conexión es un miembro (el mismo usuario en dos pestañas cuenta dos veces). Un canal admite hasta <Limit name=\"realtimePresenceMembers\" /> miembros; por encima, `realtime.presence_limit`. Si un nodo cae sin avisar, sus miembros desaparecen de la lista en hasta 30 segundos."},{"anchor":"listar-la-presencia-del-canal","heading":"Listar la presencia del canal","markdown":"<Endpoint id=\"realtime.presence\" example={{ pathParams: { channel: 'chat:lobby' } }}>\n  Devuelve los miembros del canal en este instante. Sirve para mostrar \"quién está en línea\" desde tu servidor, sin abrir una conexión.\n</Endpoint>"},{"anchor":"reconexao","heading":"Reconexión","markdown":"Las conexiones se caen: el nodo se reinicia, la red oscila, el token expira. Toda aplicación debe reconectar sola. El ejemplo de la sección [Conexión desde el navegador](#conexao-pelo-navegador) ya lo hace (el [SDK](#sdk) también); las reglas son estas y los [close codes](#protocolo) dicen cuándo reconectar."},{"anchor":"backoff","heading":"Backoff","markdown":"Usa backoff exponencial **con jitter** (un valor aleatorio hasta el tope del intento) y tope de <Limit name=\"realtimeBackoffMax\" />: por ejemplo, 0,5 s, 1 s, 2 s, 4 s… Sin jitter, miles de clientes reconectan en el mismo instante tras una caída y vuelven a tumbar el nodo. Reinicia la cuenta cuando recibas `connected`."},{"anchor":"token-nuevo-y-retry_after_ms","heading":"Token nuevo y `retry_after_ms`","markdown":"Pide un token nuevo en cada reconexión: es barato (gratis) y evita conectar con un token vencido. Si la respuesta trae `retry_after_ms`, espera ese tiempo antes de conectar; vale en lugar del backoff de ese intento. Usa siempre el `connect_url` de la respuesta más reciente."},{"anchor":"recuperar-mensajes-con-rewind","heading":"Recuperar mensajes con `rewind`","markdown":"Una conexión nueva no sabe lo que pasó mientras estuvo fuera. Después de `connected`, vuelve a suscribirte a cada canal con `rewind`: el servidor reenvía los últimos mensajes antes de entregar los nuevos, y `attached` marca el fin de la recuperación. Como `rewind` puede repetir mensajes que ya mostraste, descarta los que tengan un `id` ya visto. Vuelve a entrar en la presencia de cada canal en que estabas.\n\nElige el `rewind` según el tiempo que la conexión suele estar fuera (hasta <Limit name=\"realtimeRewindMax\" /> mensajes). Si el canal tuvo más mensajes que eso durante la caída, la diferencia se pierde: el historial guarda solo los últimos."},{"anchor":"historico","heading":"Historial","markdown":"Cada canal guarda los últimos <Limit name=\"realtimeHistoryPageMax\" /> mensajes publicados, por hasta <Limit name=\"realtimeHistoryRetention\" />. El `rewind` usa ese historial; para consultarlo desde tu servidor, usa la API."},{"anchor":"consultar-historial-del-canal","heading":"Consultar historial del canal","markdown":"<Endpoint id=\"realtime.history\" example={{ pathParams: { channel: 'chat:lobby' }, query: { limit: '50' } }}>\n  Devuelve los mensajes **del más antiguo al más nuevo**, en `messages` (`id`, `name`, `data`, `timestamp`, `client_id`), junto con `channel` y el `limit` aplicado. No hay cursor ni páginas: `limit` vale como máximo <Limit name=\"realtimeHistoryPageMax\" /> (el valor por defecto es el máximo). Es una lectura medida: cuenta en la [franquicia diaria de lecturas](/es/cobranca-e-limites#franquia-diaria) de la cuenta.\n</Endpoint>"},{"anchor":"publicar","heading":"Publicar mensaje desde el servidor","markdown":"Publicar por la API sirve para eventos que nacen en tu backend (un pedido pagado, una alerta). El mensaje llega a los suscriptores como cualquier otro.\n\n<Endpoint id=\"realtime.publish\" example={{ pathParams: { channel: 'chat:lobby' }, body: `{ \"name\": \"message\", \"data\": { \"text\": \"Pedido aprobado\" }, \"client_id\": \"sistema\" }` }}>\n  * `data`: cualquier JSON, hasta <Limit name=\"realtimeMessageMax\" /> (sumado al `name`).\n  * `name` (opcional, hasta 128 caracteres): clasifica el mensaje.\n  * `client_id` (opcional): el remitente que ven los suscriptores.\n\n  La respuesta trae `id`, `timestamp` (milisegundos Unix) y `charged`: `true` cuando el mensaje superó la franquicia mensual y se cobró.\n\n  Con `X-Dry-Run: true`, la API valida el canal y el tamaño, no publica ni cobra, y responde `200` con la estimación:\n\n  ```json\n  { \"success\": true, \"data\": { \"cost_units\": 0, \"within_quota\": true, \"free_used\": 18250, \"free_monthly\": 200000, \"would_charge\": false } }\n  ```\n\n  `cost_units` usa la escala del backend (10.000 unidades = 1 crédito) y es `0` dentro de la franquicia.\n</Endpoint>"},{"anchor":"canais","heading":"Canales y estadísticas","markdown":""},{"anchor":"listar-canales-activos","heading":"Listar canales activos","markdown":"<Endpoint id=\"realtime.channels\" example={{ query: { limit: '50' } }}>\n  Lista los canales con suscriptores o con una publicación dentro de la ventana del historial, con `name`, `subscribers` (el conteo se retrasa hasta 10 segundos) y `last_publish_at`. Usa `limit` (1 a <Limit name=\"realtimeChannelListPageMax\" />, por defecto 50) y `offset`; la respuesta trae `total` y, cuando hay más páginas, `next_offset` para la llamada siguiente.\n</Endpoint>"},{"anchor":"consultar-uso-y-conexiones","heading":"Consultar uso y conexiones","markdown":"<Endpoint id=\"realtime.stats\">\n  Muestra las `connections` abiertas ahora (sumadas en todos los nodos), los `active_channels` (con al menos un suscriptor), el `month` (UTC) y los `messages_this_month`, junto con la franquicia: `free_monthly_messages` y `free_remaining`.\n</Endpoint>"},{"anchor":"webhooks-de-canais","heading":"Webhooks de canales","markdown":"Cada cuenta puede tener **un** webhook de canales, que avisa a tu servidor cuando un canal queda ocupado o vacío, cuando alguien entra o sale de la presencia y (opcionalmente) en cada mensaje. Es una configuración de Realtime, separada de los [webhooks de la cuenta](/es/webhooks)."},{"anchor":"consultar-el-webhook-de-canales","heading":"Consultar el webhook de canales","markdown":"<Endpoint id=\"realtime.webhook.get\">\n  Devuelve la configuración actual con el `secret` y la `standard_key` (el `secret` en formato `whsec_…`, para las bibliotecas Standard Webhooks). Sin webhook configurado, responde `404`.\n</Endpoint>"},{"anchor":"configurar-el-webhook-de-canales","heading":"Configurar el webhook de canales","markdown":"<Endpoint id=\"realtime.webhook.put\" example={{ body: `{ \"url\": \"https://example.com/hooks/realtime\", \"events\": [\"channel.occupied\", \"channel.vacated\", \"channel.message\"], \"channels\": [\"chat:*\"], \"enabled\": true }` }}>\n  * `url`: URL pública `http` o `https`; las direcciones de red interna o privada se rechazan.\n  * `events`: al menos uno de los eventos de la tabla de abajo.\n  * `channels`: patrones de canal (un nombre, un prefijo con `*` o `*`), hasta <Limit name=\"realtimeWebhookChannelsMax\" />. Vacío significa todos los canales, **excepto** `channel.message`: los mensajes son opt-in y exigen al menos un patrón.\n  * `enabled` (opcional, por defecto `true`): `false` pausa los envíos sin borrar la configuración.\n\n  La llamada crea el webhook o reemplaza el actual, y el `secret` se conserva. La configuración se lee de caché durante hasta 30 segundos.\n</Endpoint>"},{"anchor":"cambiar-el-secret-del-webhook","heading":"Cambiar el secret del webhook","markdown":"<Endpoint id=\"realtime.webhook.rotate\">\n  Genera un nuevo `secret` (y la `standard_key` correspondiente). El anterior deja de valer; actualiza tu receptor enseguida.\n</Endpoint>"},{"anchor":"eliminar-el-webhook-de-canales","heading":"Eliminar el webhook de canales","markdown":"<Endpoint id=\"realtime.webhook.delete\">\n  Borra la configuración; ya no se envía ningún evento.\n</Endpoint>"},{"anchor":"eventos","heading":"Eventos","markdown":"<WebhookEventsTable source=\"realtime\" />"},{"anchor":"entrega","heading":"Entrega","markdown":"La plataforma hace un `POST` JSON a tu URL:\n\n```json\n{\n  \"id\": \"evt_5b0c9a7e-3c1f-4a52-9d57-2a6f1f0e8a11\",\n  \"type\": \"presence.member_added\",\n  \"timestamp\": 1790338000000,\n  \"account_id\": \"7a1d4c8e-2f6b-4e9a-b3c5-0d8f1a6e4b27\",\n  \"data\": {\n    \"channel\": \"chat:lobby\",\n    \"member\": { \"connection_id\": \"c_8f3a1d\", \"client_id\": \"user-42\", \"data\": { \"nick\": \"ana\" }, \"timestamp\": 1790338000000 }\n  }\n}\n```\n\n`data.channel` está en todos los eventos; `data.member` viene en los eventos de presencia y `data.message` (`id`, `name`, `data`, `timestamp`, `client_id`) en `channel.message`. El `timestamp` está en milisegundos Unix.\n\n* **Firma:** Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature`), con la `standard_key` de la configuración; consulta [Validar la firma](/es/webhooks#verificar-assinatura).\n* **Intentos:** hasta 3 (esperas de 1 s y 5 s) ante error de red, `5xx` o `429`; otras respuestas terminan el envío. Cada intento espera hasta <Limit name=\"realtimeWebhookTimeout\" />.\n* **Procesamiento:** el envío es asíncrono, con 4 trabajadores; si la cola se llena, el evento se descarta. Trata el webhook como un aviso y usa el `id` para descartar duplicados."},{"anchor":"sdk","heading":"SDK de JavaScript","markdown":"El paquete `@respawn/realtime` implementa el protocolo de arriba, sin dependencias, para el navegador y Node 22 o más nuevo. Renueva el token 30 segundos antes de que expire, reconecta según los [close codes](#protocolo), retoma los canales con `rewind` (100 mensajes, sin repetir los ya entregados) y vuelve a entrar en la presencia.\n\n```bash\nnpm install @respawn/realtime\n```"},{"anchor":"en-el-servidor","heading":"En el servidor","markdown":"`RealtimeRest` usa la clave de API (nunca la envíes al navegador) para emitir tokens, publicar y leer el historial y la presencia. Pasa `baseUrl` con la dirección de la API:\n\n```ts\nimport { RealtimeRest } from '@respawn/realtime';\n\nconst rest = new RealtimeRest({\n  apiKey: process.env.RESPAWN_API_TOKEN!,\n  baseUrl: 'https://api.respawntech.dev',\n});\n\n// route the browser calls (Express): returns token, expires_at and connect_url\napp.post('/realtime/token', async (req, res) => {\n  res.json(\n    await rest.createToken({\n      client_id: req.user.id,\n      capabilities: { 'room:*': ['subscribe', 'publish', 'presence'] },\n      ttl_seconds: 600,\n    }),\n  );\n});\n\nawait rest.publish('room:1', 'order', { id: 42, status: 'paid' });\nconst history = await rest.history('room:1', { limit: 20 });\nconst members = await rest.presence('room:1');\n```"},{"anchor":"en-el-navegador","heading":"En el navegador","markdown":"`Realtime` pide el token a tu servidor por `authUrl` (un `POST` con cookies) o por `authCallback`:\n\n```ts\nimport { Realtime } from '@respawn/realtime';\n\nconst rt = new Realtime({ authUrl: '/realtime/token' });\nrt.on('state', (state, err) => console.log('connection:', state, err?.code));\n\nconst room = rt.channel('room:1');\n\nawait room.subscribe((m) => console.log(m.name, m.data), { rewind: 20 });\nawait room.publish('chat', { text: 'hi' });\n\nawait room.presence.watch((members) => render(members));\nawait room.presence.enter({ nick: 'ana' });\n```\n\nLos estados son `initialized`, `connecting`, `connected`, `disconnected`, `closed` (llamaste a `close()`) y `failed` (por ejemplo, close `4003`). Si el tiempo fuera de línea superó la ventana del historial, el canal emite el evento `gap`. Las publicaciones hechas sin conexión quedan en cola; una ya enviada cuando la conexión se cae falla con `sdk.connection_lost` (puede haberse publicado o no)."},{"anchor":"react","heading":"React","markdown":"Con `@respawn/realtime/react`:\n\n```tsx\nimport { useState } from 'react';\nimport { Realtime } from '@respawn/realtime';\nimport { RealtimeProvider, useChannel, usePresence } from '@respawn/realtime/react';\n\nconst rt = new Realtime({ authUrl: '/realtime/token' });\n\nexport function App() {\n  return (\n    <RealtimeProvider client={rt}>\n      <Room />\n    </RealtimeProvider>\n  );\n}\n\nfunction Room() {\n  const [msgs, setMsgs] = useState<{ id: string; data: unknown }[]>([]);\n  const { publish } = useChannel('room:1', (m) => setMsgs((all) => [...all, m]), { rewind: 20 });\n  const { members } = usePresence('room:1', { nick: 'ana' });\n\n  return (\n    <div>\n      <p>{members.length} online</p>\n      <ul>{msgs.map((m) => <li key={m.id}>{JSON.stringify(m.data)}</li>)}</ul>\n      <button onClick={() => publish('chat', { text: 'hi' })}>Send</button>\n    </div>\n  );\n}\n```"},{"anchor":"limites","heading":"Límites","markdown":"| Límite                                 | Valor                                                                                                                                      |\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |\n| Mensajes gratis por mes (por cuenta)   | <Limit name=\"realtimeFreeMessages\" />                                                                                                      |\n| Tamaño de un mensaje (`data`)          | <Limit name=\"realtimeMessageMax\" />                                                                                                        |\n| Largo del nombre del canal             | <Limit name=\"realtimeChannelNameMax\" /> caracteres                                                                                         |\n| Canales suscritos por conexión         | <Limit name=\"realtimeChannelsPerConnection\" />                                                                                             |\n| Publicaciones por segundo (por cuenta) | <Limit name=\"realtimePublishPerSecond\" />                                                                                                  |\n| Conexiones simultáneas (por cuenta)    | <Limit name=\"realtimeConnectionsPerAccount\" />                                                                                             |\n| Miembros en la presencia de un canal   | <Limit name=\"realtimePresenceMembers\" />                                                                                                   |\n| Validez del token                      | <Limit name=\"realtimeTokenTtlDefault\" /> (por defecto), de <Limit name=\"realtimeTokenTtlMin\" /> hasta <Limit name=\"realtimeTokenTtlMax\" /> |\n| Patrones de canal por token            | hasta <Limit name=\"realtimeTokenPatternsMax\" />                                                                                            |\n| Retención del historial                | <Limit name=\"realtimeHistoryRetention\" />                                                                                                  |\n| Mensajes guardados y por `rewind`      | hasta <Limit name=\"realtimeHistoryPageMax\" />                                                                                              |\n| Canales por página (`GET /channels`)   | hasta <Limit name=\"realtimeChannelListPageMax\" />                                                                                          |\n| Patrones de canal del webhook          | hasta <Limit name=\"realtimeWebhookChannelsMax\" />                                                                                          |\n\nLos valores son los predeterminados de la plataforma y esta puede ajustarlos. Las solicitudes a la API también siguen el [límite de solicitudes](/es/cobranca-e-limites) de la cuenta."},{"anchor":"erros","heading":"Errores","markdown":"Los errores de la API REST (`realtime.channel_invalid`, `realtime.message_too_large`…) llegan como respuesta HTTP, en el formato de [Errores](/es/erros). En la conexión WebSocket o SSE, los mismos códigos llegan por un frame `error` (la conexión sigue abierta) o por un close code (consulta [Protocolo](#protocolo)).\n\n<ErrorTable tool=\"realtime\" />\n\nLos errores de validación comunes (`validation.failed`, `not_found`) están en [Errores](/es/erros)."},{"anchor":"preco","heading":"Precio","markdown":"<PricingTable tool=\"realtime\" group=\"per-message\" />\n\n<PricingTable tool=\"realtime\" group=\"free-quota\" />\n\nEl cobro es por mensaje publicado, una vez, por los mensajes que superan la franquicia mensual; el número de suscriptores no cambia el costo. Emitir tokens, mantener conexiones abiertas, usar la presencia, listar canales y configurar el webhook son gratis. La consulta del historial es una lectura medida: cuenta en la franquicia diaria de lecturas y, después de ella, cuesta el valor de la [tabla de lecturas](/es/cobranca-e-limites#franquia-diaria). Sin saldo para un mensaje fuera de la franquicia, la publicación falla con `realtime.insufficient_credits`."}]},{"slug":"ferramentas/verificacao-de-url","url":"https://docs.respawntech.dev/es/ferramentas/verificacao-de-url","title":"Verificación de URL","description":"Verifica si una URL es maliciosa y obtén el favicon de un dominio mediante un proxy.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"verificacao-de-url\" />"},{"anchor":"descripción-general","heading":"Descripción general","markdown":"Úsala antes de mostrar enlaces generados por usuarios o de redirigir a alguien a un destino externo. Es la misma verificación que [QR Dinámico](/es/ferramentas/qr-dinamico) aplica a las URLs de destino.\n\n<EndpointList tool=\"verificacao-de-url\" />"},{"anchor":"custos","heading":"Costos","markdown":"Las dos operaciones son lecturas medidas: cuentan para la [cuota diaria gratuita](/es/cobranca-e-limites#franquia-diaria) y, una vez agotada, debitan el valor indicado en cada endpoint."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"verificar-url","heading":"Verificar URL","markdown":"<Endpoint id=\"safety.check\" example={{ query: { url: 'https://example.com' } }}>\n  Query: `url` (obligatorio, URL absoluta). Los resultados se guardan en caché durante 1 hora.\n\n  <Callout type=\"warn\" title=\"Respuesta fuera del envelope estándar\">\n    Esta ruta responde `{ \"is_safe\": … }` directamente, sin `success`/`data`. Si el servicio de verificación no está disponible, la respuesta es `is_safe: true` (*fail-open*), para no bloquear tu flujo. En dry-run, la respuesta vuelve al envelope estándar.\n  </Callout>\n</Endpoint>"},{"anchor":"favicon-de-un-dominio","heading":"Favicon de un dominio","markdown":"<Endpoint id=\"safety.favicon\" example={{ query: { domain: 'example.com' }, response: 'binary' }}>\n  Query: `domain` (obligatorio).\n</Endpoint>"},{"anchor":"errores","heading":"Errores","markdown":"<ErrorTable tool=\"verificacao-de-url\" />"}]},{"slug":"ferramentas/web-to-markdown","url":"https://docs.respawntech.dev/es/ferramentas/web-to-markdown","title":"Página web a Markdown","description":"Descargue una página web y reciba su contenido principal en Markdown, con opción de guardarlo en la base de conocimiento.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"web-to-markdown\" />"},{"anchor":"visión-general","heading":"Visión general","markdown":"Indique una URL y reciba el texto principal de la página en Markdown, con título, autor y conteo de palabras. Con `collection_id`, el contenido también se guarda en una [base de conocimiento](/es/ferramentas/base-de-conhecimento). También está disponible en el [MCP](/es/agentes-de-ia) como `web_to_markdown`.\n\n<EndpointList tool=\"web-to-markdown\" />"},{"anchor":"custos","heading":"Costos","markdown":"La conversión es una lectura medida: cuenta para la [cuota diaria gratuita](/es/cobranca-e-limites#franquia-diaria) y, una vez agotada, debita el valor indicado en el endpoint. Al guardar en una base, el guardado se cobra como una ingesta de la [base de conocimiento](/es/ferramentas/base-de-conhecimento)."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"convertir-una-página-a-markdown","heading":"Convertir una página a Markdown","markdown":"<Endpoint id=\"web.markdown\" example={{ body: `{ \"url\": \"https://example.com/blog/post\" }` }}>\n  `url` (obligatorio) es la dirección `http` o `https` de la página. `collection_id` (opcional) guarda el contenido en esa base y exige también el scope `tools:rag:manage`.\n\n  La respuesta trae `url`, `final_url` (tras las redirecciones), `title`, `byline`, `markdown` y `word_count`. Con `collection_id`, trae además `document` (`document_id`, `status` y `duplicate`) y, en dry-run, `estimate`.\n\n  <Callout type=\"warn\" title=\"Solo HTML estático\">\n    La página se lee tal como la entrega el servidor, sin ejecutar JavaScript: las páginas que arman su contenido en el navegador responden `tools.web.no_content`. Las direcciones internas y privadas se rechazan (`tools.web.blocked_url`).\n  </Callout>\n</Endpoint>"},{"anchor":"errores","heading":"Errores","markdown":"<ErrorTable tool=\"web-to-markdown\" />"}]},{"slug":"ferramentas/webhook-relay","url":"https://docs.respawntech.dev/es/ferramentas/webhook-relay","title":"Webhook Relay","description":"Envía webhooks firmados a tus clientes, con reintentos automáticos, registro de intentos y replay, y redistribuye webhooks de Stripe, GitHub y otros proveedores.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"webhook-relay\" />"},{"anchor":"visao-geral","heading":"Visión general","markdown":"Webhook Relay es la infraestructura de webhooks de tu aplicación. En lugar de mantener colas, reintentos y firmas, publicas un evento con una llamada y la plataforma lo entrega en cada endpoint suscrito, con el mismo motor que envía los [webhooks de la cuenta](/es/webhooks).\n\n* **Proyecto:** agrupa endpoints, fuentes, rutas, tipos de evento y mensajes, para separar configuraciones (una tienda, un cliente, un entorno). Toda cuenta tiene un proyecto por defecto; consulta [Proyectos](#projetos).\n* **Endpoint:** la URL que recibe los eventos (normalmente de tu cliente, o un canal de Slack, Discord o Teams), con los eventos a los que se suscribe y su propio `secret`.\n* **Mensaje:** un evento que publicas (`event_type` + `payload`).\n* **Entrega:** un mensaje camino de un endpoint. Un mensaje para 3 endpoints genera 3 entregas, cada una con sus reintentos.\n* **Intento:** cada llamada HTTP de una entrega, registrada con estado, latencia y el comienzo de la respuesta.\n* **Fuente y ruta:** para **recibir** webhooks de terceros (Stripe, GitHub…) en una URL de la plataforma y redistribuirlos a tus endpoints. Consulta [Recibir webhooks de terceros](#ingestao).\n\nTodas las solicitudes siguen el estándar [Standard Webhooks](https://www.standardwebhooks.com) (headers `webhook-id`, `webhook-timestamp` y `webhook-signature`). El formato, la verificación de la firma y el calendario de reintentos están en la guía [Webhooks](/es/webhooks)."},{"anchor":"primeiros-passos","heading":"Primeros pasos","markdown":"<Steps>\n  <Step>\n    **Crea un endpoint** con la URL de destino y los eventos a los que se suscribe. Guarda el `secret` de la respuesta (usa `standard_webhooks_secret` con las bibliotecas oficiales). Sin `project_id`, va al proyecto por defecto.\n  </Step>\n\n  <Step>\n    **Prueba la conexión** con el [ping de prueba](#webhooks-endpoints-test): un evento `webhook.test` gratis.\n  </Step>\n\n  <Step>\n    **Envía un mensaje** con `event_type` y `payload`. Usa antes `X-Dry-Run: true` para ver cuántas entregas genera y cuánto cuesta.\n  </Step>\n\n  <Step>\n    **Verifica la firma** en el receptor y responde `2xx` rápido. Consulta los ejemplos en [Validar la firma](/es/webhooks#verificar-assinatura).\n  </Step>\n\n  <Step>\n    **Sigue** las entregas en el [registro de mensajes](#mensagens) y reenvía lo que falló.\n  </Step>\n</Steps>\n\n<EndpointList tool=\"webhook-relay\" />"},{"anchor":"custos","heading":"Costos","markdown":"<PricingTable tool=\"webhook-relay\" group=\"per-delivery\" />\n\n<PricingTable tool=\"webhook-relay\" group=\"free-quota\" />\n\nEl cobro se hace en el envío, por mensaje, por las entregas que superan la cuota (`charged_units`, en unidades de 1/10.000 de crédito). Si el mensaje no se puede guardar, el importe se reembolsa. El `payload` se valida antes de cualquier cobro.\n\nLa cuota y el cobro son **por cuenta**: todos los proyectos comparten la misma cuota mensual, y una entrega cuesta lo mismo en cualquier proyecto.\n\n<Callout title=\"Lecturas del registro\">\n  Configurar proyectos, endpoints, tipos de evento, fuentes y rutas es gratis. Las rutas para [listar](#webhooks-messages-list) y [consultar](#webhooks-messages-get) mensajes son lecturas medidas: cuentan en la [cuota diaria de lecturas](/es/cobranca-e-limites#franquia-diaria) y, después de ella, cuestan el valor de la tabla de lecturas.\n</Callout>"},{"anchor":"consultar-el-uso-del-mes","heading":"Consultar el uso del mes","markdown":"<Endpoint id=\"webhooks.usage\">\n  Muestra el mes en curso (UTC, de `period_start` a `period_end`): la cuota (`free_deliveries`), las entregas ya usadas por la API y la ingesta (`used_deliveries`) y lo que queda (`free_left`). `delivery_units` y `read_units` son los precios actuales de una entrega y de una lectura del registro, en unidades; `blocked_retention_days` es cuánto tiempo espera una entrega por créditos (consulta [Sin créditos](#sem-creditos)).\n</Endpoint>"},{"anchor":"projetos","heading":"Proyectos","markdown":"Todo lo que configuras en Webhook Relay pertenece a un proyecto: endpoints, fuentes (y sus rutas), tipos de evento, mensajes y el layout del canvas en el dashboard. Usa proyectos para separar configuraciones, por ejemplo una por tienda o por cliente.\n\n* Toda cuenta tiene un **proyecto por defecto** (`is_default: true`, llamado `Default`), creado en el primer uso. No se puede eliminar.\n* `project_id` es **opcional** en toda la API: sin él, las creaciones y los envíos usan el proyecto por defecto. Las integraciones que no conocen los proyectos siguen funcionando.\n* Un mensaje solo llega a los endpoints de su proyecto y se valida con los tipos de evento de ese proyecto. Los nombres de tipo de evento son únicos por proyecto.\n* Los listados (`GET /endpoints`, `/sources`, `/event-types`, `/routes` y `/messages`) aceptan `?project_id=` como filtro; sin él, traen todos los proyectos.\n* Los eventos de la plataforma (p. ej., `qr.dynamic.scanned`) generan un mensaje en cada proyecto que tiene endpoints suscritos, siempre gratis. Los callbacks de jobs (`webhook_url`) no pertenecen a ningún proyecto (`project_id: null`).\n* Los límites y el cobro siguen siendo por cuenta: consulta [Límites](#limites). Por defecto no hay límite de proyectos."},{"anchor":"listar-proyectos","heading":"Listar proyectos","markdown":"<Endpoint id=\"webhooks.projects.list\" example={{ query: { q: 'tienda', page: '1', page_size: '20' } }}>\n  Paginado y con búsqueda: `q` busca en el nombre (sin distinguir mayúsculas), `page` empieza en 1 y `page_size` llega hasta <Limit name=\"webhookProjectsPageMax\" /> (por defecto <Limit name=\"webhookProjectsPageDefault\" />). Cada proyecto trae `stats`: fuentes, endpoints, mensajes de las últimas 24 horas, entregas del mes y `failing_endpoints` (endpoints que fallan o desactivados automáticamente; un endpoint que pausaste no cuenta).\n</Endpoint>"},{"anchor":"crear-proyecto","heading":"Crear proyecto","markdown":"<Endpoint id=\"webhooks.projects.create\" example={{ body: `{ \"name\": \"Tienda\", \"description\": \"Eventos de la tienda online\" }` }}>\n  `name` (obligatorio) tiene hasta <Limit name=\"webhookProjectNameMax\" /> caracteres; `description`, hasta <Limit name=\"webhookProjectDescriptionMax\" />.\n</Endpoint>"},{"anchor":"consultar-proyecto","heading":"Consultar proyecto","markdown":"<Endpoint id=\"webhooks.projects.get\" />"},{"anchor":"modificar-proyecto","heading":"Modificar proyecto","markdown":"<Endpoint id=\"webhooks.projects.update\" example={{ body: `{ \"name\": \"Tienda principal\" }` }}>\n  Envía solo los campos que quieres cambiar.\n</Endpoint>"},{"anchor":"eliminar-proyecto","heading":"Eliminar proyecto","markdown":"<Endpoint id=\"webhooks.projects.delete\">\n  Elimina los endpoints del proyecto (las entregas pendientes se cancelan), las fuentes, las rutas, los tipos de evento y el layout. Los mensajes siguen en el registro general, con `project_id: null`.\n</Endpoint>"},{"anchor":"duplicar-proyecto","heading":"Duplicar proyecto","markdown":"<Endpoint id=\"webhooks.projects.duplicate\" example={{ body: `{ \"name\": \"Tienda (copia)\" }` }}>\n  Crea un proyecto nuevo con la misma descripción y copias de todo, ya conectadas entre sí:\n\n  * **Endpoints** con **secrets nuevos** (misma URL, eventos, headers, límite de envío, formato y estado activo; el historial de fallos no se copia).\n  * **Fuentes** con **`ingest_url` nuevas** (misma configuración y el mismo secret del proveedor). Registra las URL nuevas en el proveedor.\n  * Rutas, tipos de evento y el layout del canvas.\n\n  Los mensajes, entregas y contadores no se copian. Los límites de la cuenta se comprueban antes: si la copia superaría alguno, la respuesta es `webhook.limit_reached` y no se crea nada.\n</Endpoint>"},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"crear-endpoint","heading":"Crear endpoint","markdown":"<Endpoint\n  id=\"webhooks.endpoints.create\"\n  example={{ body: `{\n\"url\": \"https://api.example.com/hooks/respawn\",\n\"description\": \"Pedidos de la tienda\",\n\"events\": [\"order.paid\", \"order.refunded\"],\n\"custom_headers\": { \"X-Tenant\": \"tienda-1\" }\n}` }}\n>\n  * `url` (obligatorio): URL pública `http` o `https`. Las direcciones privadas, locales o de metadatos de la nube se rechazan (`webhook.invalid_url`).\n  * `events`: tipos de evento que recibe el endpoint de los mensajes de la API y de la plataforma; `[\"*\"]` suscribe a todos. Sin eventos, el endpoint solo recibe lo que le envían las [rutas](#ingestao).\n  * `project_id`: proyecto del endpoint (por defecto: el proyecto por defecto). Un endpoint no cambia de proyecto.\n  * `format`: `raw` (por defecto), `slack`, `discord` o `teams`. Consulta [Slack, Discord y Teams](#formatos).\n  * `custom_headers`: hasta <Limit name=\"webhookCustomHeadersMax\" /> headers extra enviados en cada entrega (p. ej., un token de tu cliente). Los headers de firma, `Content-Type`, `User-Agent` y `Host` no se pueden sobrescribir.\n  * `rate_limit_per_sec`: tope de envíos por segundo para este endpoint (`0` = sin tope propio).\n  * El `secret` solo viene en esta respuesta; después, usa [Revelar secret](#webhooks-endpoints-secret). Hasta <Limit name=\"webhookEndpointsMax\" /> endpoints por cuenta, sumando todos los proyectos.\n</Endpoint>"},{"anchor":"listar-endpoints","heading":"Listar endpoints","markdown":"<Endpoint id=\"webhooks.endpoints.list\" example={{ query: { project_id: '3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53' } }}>\n  Filtro opcional por `project_id`. Un endpoint desactivado automáticamente trae `is_active: false` y `disabled_reason`; uno que está fallando trae `failing_since`.\n</Endpoint>"},{"anchor":"consultar-endpoint","heading":"Consultar endpoint","markdown":"<Endpoint id=\"webhooks.endpoints.get\" />"},{"anchor":"modificar-endpoint","heading":"Modificar endpoint","markdown":"<Endpoint id=\"webhooks.endpoints.update\" example={{ body: `{ \"events\": [\"*\"], \"is_active\": true }` }}>\n  Envía solo los campos que quieres cambiar (incluido `format`). `is_active: true` reactiva un endpoint desactivado y reinicia la racha de fallos.\n</Endpoint>"},{"anchor":"eliminar-endpoint","heading":"Eliminar endpoint","markdown":"<Endpoint id=\"webhooks.endpoints.delete\">\n  Las entregas pendientes para el endpoint se cancelan.\n</Endpoint>"},{"anchor":"revelar-secret","heading":"Revelar secret","markdown":"<Endpoint id=\"webhooks.endpoints.secret\">\n  `secret` es la clave en bruto (HMAC del header heredado `X-Webhook-Signature`); `standard_webhooks_secret` es la misma clave en el formato `whsec_…` aceptado por las bibliotecas Standard Webhooks.\n</Endpoint>"},{"anchor":"rotar-secret","heading":"Rotar secret","markdown":"<Endpoint id=\"webhooks.endpoints.rotate_secret\">\n  Durante <Limit name=\"webhookSecretOverlap\" /> las entregas llevan dos firmas en `webhook-signature` (secret nuevo y anterior), para que cambies el secret del receptor sin perder eventos.\n</Endpoint>"},{"anchor":"enviar-ping-de-prueba","heading":"Enviar ping de prueba","markdown":"<Endpoint id=\"webhooks.endpoints.test\">\n  Envía un evento `webhook.test` firmado, gratis y fuera de la cuota. Sigue el resultado en [Consultar mensaje](#webhooks-messages-get) con el `message_id` devuelto.\n</Endpoint>"},{"anchor":"reenviar-entregas-fallidas","heading":"Reenviar entregas fallidas","markdown":"<Endpoint id=\"webhooks.endpoints.recover\" example={{ query: { since: '2026-09-24T00:00:00Z' } }}>\n  Reenvía a este endpoint todas las entregas que terminaron en fallo (`failed`, `exhausted` o `canceled`) desde `since` (RFC 3339), hasta <Limit name=\"webhookRecoverMax\" /> por llamada. Úsalo después de corregir un receptor que estuvo caído. Cada entrega reenviada se cobra como nueva.\n</Endpoint>"},{"anchor":"formatos","heading":"Slack, Discord y Teams","markdown":"Con `format` en `slack`, `discord` o `teams`, registra como `url` el webhook de entrada del canal (Incoming Webhook de Slack o de Teams, webhook de Discord). La plataforma convierte cada entrega en un mensaje de texto al enviarla:\n\n* El cuerpo pasa a ser `{\"text\": \"…\"}` (Slack y Teams) o `{\"content\": \"…\"}` (Discord), con `Content-Type: application/json`.\n* El texto lleva el tipo del evento en negrita (y el nombre de la fuente, en los mensajes recibidos de un proveedor), seguido del cuerpo en un bloque de código: JSON formateado o el texto en bruto.\n* Los textos largos se cortan con `…` para caber en el límite de la plataforma: <Limit name=\"webhookSlackTextMax\" /> caracteres en Slack, <Limit name=\"webhookDiscordTextMax\" /> en Discord y <Limit name=\"webhookTeamsTextMax\" /> en Teams.\n* Los headers de firma se siguen enviando, calculados sobre ese cuerpo; los headers originales de una fuente con `forward_original_headers` no se reenvían.\n\nEl mensaje guardado en el registro no cambia: el registro, el reenvío y la recuperación funcionan igual que en los endpoints `raw`."},{"anchor":"mensagens","heading":"Mensajes","markdown":""},{"anchor":"enviar-mensaje","heading":"Enviar mensaje","markdown":"<Endpoint\n  id=\"webhooks.messages.send\"\n  example={{ headers: { 'X-Dry-Run': 'true' }, body: `{\n\"event_type\": \"order.paid\",\n\"payload\": { \"order_id\": \"ped_1042\", \"amount\": 199.9 },\n\"idempotency_key\": \"order-paid-ped_1042\"\n}` }}\n>\n  * `event_type` (obligatorio): `^[a-z0-9][a-z0-9._-]{0,127}$`, p. ej. `order.paid`. Va a los endpoints del proyecto suscritos a ese tipo (o a `*`).\n  * `payload` (obligatorio): cualquier JSON, hasta <Limit name=\"webhookPayloadMax\" />. Si el tipo de evento tiene [JSON Schema](#tipos-de-evento) en el proyecto, el payload debe cumplirlo (`webhook.payload_invalid`, sin cobro).\n  * `project_id` (opcional): proyecto del mensaje (por defecto: el proyecto por defecto).\n  * `endpoint_ids` (opcional): restringe el envío a estos endpoints, entre los del proyecto suscritos al evento.\n  * `idempotency_key` (o el header `X-Idempotency-Key`): repetir la clave devuelve el primer mensaje con `duplicate: true` y no cobra nada. Úsalo para reenviar con seguridad tras un timeout.\n  * Con `X-Dry-Run: true` no se envía ni se cobra nada: la respuesta muestra `deliveries`, `free_deliveries` y `charged_units`.\n\n  El receptor recibe el sobre `{ \"event_id\", \"type\", \"timestamp\", \"payload\" }` descrito en [Webhooks](/es/webhooks#corpo); `event_id` es el `message_id` y `webhook-id` es `msg_<message_id>`.\n</Endpoint>"},{"anchor":"enviar-mensajes-en-lote","heading":"Enviar mensajes en lote","markdown":"<Endpoint\n  id=\"webhooks.messages.batch\"\n  example={{ body: `{\n\"messages\": [\n  { \"event_type\": \"order.paid\", \"payload\": { \"order_id\": \"ped_1042\", \"amount\": 199.9 }, \"idempotency_key\": \"order-paid-ped_1042\" },\n  { \"event_type\": \"order.paid\", \"payload\": { \"order_id\": \"ped_1043\" }, \"idempotency_key\": \"order-paid-ped_1043\" }\n]\n}` }}\n>\n  Hasta <Limit name=\"webhookBatchMax\" /> mensajes, cada uno con los mismos campos del envío individual. El `project_id` va en el nivel superior del body y vale para todos los mensajes del lote. Cada mensaje se valida, deduplica y cobra por separado: un fallo aparece en `error` en su ítem y no impide los demás. Acepta `X-Dry-Run: true`.\n</Endpoint>"},{"anchor":"listar-mensajes","heading":"Listar mensajes","markdown":"<Endpoint id=\"webhooks.messages.list\" example={{ query: { event_type: 'order.paid', failed: 'true', limit: '20' } }}>\n  Los más recientes primero, con las entregas de cada mensaje (sin el cuerpo). Paginación: pasa el `next_before` recibido en `before`. `limit` de 1 a 100 (por defecto 20). Los mensajes quedan en el registro durante <Limit name=\"webhookLogRetention\" />.\n\n  * `project_id`: solo los mensajes de ese proyecto. Sin él, el registro trae todos los proyectos, además de los mensajes sin proyecto (`project_id: null`: callbacks de jobs y mensajes de proyectos eliminados).\n  * `event_type` y `endpoint_id`: filtros exactos.\n  * `failed=true`: solo mensajes con al menos una entrega `failed`, `exhausted` o `blocked_no_credits`.\n  * `q`: búsqueda, de 1 a <Limit name=\"webhookSearchQueryMax\" /> caracteres, combinable con los demás filtros. Un UUID encuentra el mensaje con ese id. Otro texto encuentra la clave de idempotencia o el tipo de evento iguales al texto, o el texto dentro del cuerpo (sin distinguir mayúsculas, salvo las letras acentuadas); la búsqueda en el cuerpo solo mira los mensajes de los últimos <Limit name=\"webhookSearchBodyWindow\" />.\n</Endpoint>"},{"anchor":"consultar-mensaje","heading":"Consultar mensaje","markdown":"<Endpoint id=\"webhooks.messages.get\">\n  Trae el cuerpo enviado y, por entrega, cada intento: `http_status`, `latency_ms`, el comienzo de la respuesta (`response_body`) o el error de red (`error`). `next_attempt_at` aparece mientras la entrega está pendiente.\n\n  | `status` de la entrega | Significado                                                                                                           |\n  | ---------------------- | --------------------------------------------------------------------------------------------------------------------- |\n  | `pending`              | Esperando el próximo intento (`next_attempt_at`).                                                                     |\n  | `sending`              | Intento en curso.                                                                                                     |\n  | `succeeded`            | El endpoint respondió `2xx`.                                                                                          |\n  | `exhausted`            | Todos los intentos fallaron. Se puede reenviar.                                                                       |\n  | `failed`               | Se detuvo del todo: el endpoint respondió `410 Gone`, fue desactivado o su URL dejó de ser válida. Se puede reenviar. |\n  | `canceled`             | Cancelada, por ejemplo porque el endpoint se eliminó con la entrega pendiente.                                        |\n  | `blocked_no_credits`   | Mensaje recibido de un proveedor sin saldo para la entrega (consulta [Sin créditos](#sem-creditos)).                  |\n</Endpoint>"},{"anchor":"reenviar-mensaje","heading":"Reenviar mensaje","markdown":"<Endpoint id=\"webhooks.messages.retry\" example={{ body: `{ \"endpoint_id\": \"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34\" }` }}>\n  Sin body, envía de nuevo el mensaje a los endpoints activos cuya entrega terminó en fallo (`failed`, `exhausted` o `canceled`).\n\n  Con `endpoint_id`, lo envía solo a ese endpoint, sea cual sea el resultado anterior (aunque la entrega haya salido bien). Debe ser un endpoint activo del proyecto del mensaje; si no, y para mensajes sin proyecto, la respuesta es `400`. Las entregas nuevas se cobran como un envío.\n</Endpoint>"},{"anchor":"tipos-de-evento","heading":"Tipos de evento","markdown":"Declarar tipos de evento es opcional: sin declaración, se acepta cualquier `event_type` válido. Decláralos para documentar los eventos de tu integración y, con `schema`, validar el `payload` antes de cobrar y enviar. Cada tipo pertenece a un proyecto y solo vale para los mensajes de ese proyecto."},{"anchor":"declarar-tipo-de-evento","heading":"Declarar tipo de evento","markdown":"<Endpoint\n  id=\"webhooks.event_types.create\"\n  example={{ body: `{\n\"name\": \"order.paid\",\n\"description\": \"Pedido pagado\",\n\"schema\": {\n  \"type\": \"object\",\n  \"required\": [\"order_id\", \"amount\"],\n  \"properties\": {\n    \"order_id\": { \"type\": \"string\" },\n    \"amount\": { \"type\": \"number\" }\n  }\n}\n}` }}\n>\n  `schema` es un JSON Schema aplicado al `payload` de cada envío de ese tipo; un payload que no lo cumple responde `webhook.payload_invalid` y no se cobra. `project_id` es opcional (por defecto: el proyecto por defecto); el nombre es único dentro del proyecto. Hasta <Limit name=\"webhookEventTypesMax\" /> tipos por cuenta, sumando todos los proyectos.\n</Endpoint>"},{"anchor":"listar-tipos-de-evento","heading":"Listar tipos de evento","markdown":"<Endpoint id=\"webhooks.event_types.list\">\n  Filtro opcional por `project_id`.\n</Endpoint>"},{"anchor":"eliminar-tipo-de-evento","heading":"Eliminar tipo de evento","markdown":"<Endpoint id=\"webhooks.event_types.delete\">\n  Los envíos de ese tipo se siguen aceptando, sin validación de schema.\n</Endpoint>"},{"anchor":"ingestao","heading":"Recibir webhooks de terceros","markdown":"Una **fuente** es una URL de la plataforma (`ingest_url`) que registras en el proveedor (Stripe, GitHub, Shopify…). Cada evento recibido tiene verificada la firma del proveedor, se guarda y se redistribuye por las **rutas** de la fuente a tus endpoints, con la misma firma, reintentos y registro de los mensajes de la API. Así, un único webhook del proveedor puede alimentar varios servicios, y el proveedor nunca espera a tu receptor.\n\n| `provider`          | Cómo se verifica la solicitud                                                                                                    |\n| ------------------- | -------------------------------------------------------------------------------------------------------------------------------- |\n| `stripe`            | Header `Stripe-Signature` con el signing secret de Stripe (`whsec_…`), tolerancia de <Limit name=\"webhookSignatureTolerance\" />. |\n| `github`            | Header `X-Hub-Signature-256` con el secret del webhook de GitHub.                                                                |\n| `shopify`           | Header `X-Shopify-Hmac-Sha256` con el secret de la app de Shopify.                                                               |\n| `hmac_generic`      | Header `X-Webhook-Signature: sha256=<hex>` (HMAC-SHA256 del cuerpo).                                                             |\n| `standard_webhooks` | Headers `webhook-id`, `webhook-timestamp` y `webhook-signature`.                                                                 |\n| `basic_auth`        | Header `Authorization: Basic …`; el secret es `usuario:contraseña`.                                                              |\n| `none`              | Sin verificación: cualquiera con la URL puede enviar. Evítalo en producción.                                                     |\n\n`hmac_generic`, `standard_webhooks` y `basic_auth` sirven para cualquier sistema que no esté en la lista: elige el que sepa enviar el sistema de origen."},{"anchor":"crear-fuente-de-ingesta","heading":"Crear fuente de ingesta","markdown":"<Endpoint\n  id=\"webhooks.sources.create\"\n  example={{ body: `{\n\"name\": \"Stripe producción\",\n\"provider\": \"stripe\",\n\"verification_secret\": \"whsec_...\",\n\"event_type_path\": \"type\",\n\"dedupe_key_path\": \"id\"\n}` }}\n>\n  * `name` y `provider` (obligatorios); `project_id` es opcional (por defecto: el proyecto por defecto).\n  * `verification_secret`: el secret con el que firma el proveedor. Nunca vuelve en las respuestas (`has_verification_secret`). Puede dejarse para después, porque varios proveedores solo muestran el secret una vez registrada la `ingest_url`: mientras no se defina en [Definir secret del proveedor](#webhooks-sources-verification_secret), la fuente responde `401` a toda solicitud (salvo con `provider: \"none\"`).\n  * `event_type_path`: ruta en el JSON recibido de donde sale el tipo del evento (p. ej., `type`, `data.object.status`). Sin ella, el tipo es `ingest`.\n  * `dedupe_key_path`: ruta del identificador del evento (p. ej., `id`). Un reenvío del proveedor con la misma clave responde `200` sin generar entregas nuevas.\n  * `forward_original_headers`: reenvía los headers del proveedor (p. ej., `Stripe-Signature`) junto con el cuerpo idéntico, además de nuestra firma. Útil cuando tu receptor ya valida la firma del proveedor. Las cookies, `Authorization` y los headers de proxy nunca se reenvían.\n  * Registra la `ingest_url` de la respuesta en el proveedor. Hasta <Limit name=\"webhookSourcesMax\" /> fuentes por cuenta, sumando todos los proyectos.\n</Endpoint>"},{"anchor":"listar-fuentes","heading":"Listar fuentes","markdown":"<Endpoint id=\"webhooks.sources.list\">\n  Filtro opcional por `project_id`.\n</Endpoint>"},{"anchor":"consultar-fuente","heading":"Consultar fuente","markdown":"<Endpoint id=\"webhooks.sources.get\" />"},{"anchor":"modificar-fuente","heading":"Modificar fuente","markdown":"<Endpoint id=\"webhooks.sources.update\" example={{ body: `{ \"provider\": \"hmac_generic\", \"forward_original_headers\": true }` }}>\n  Envía solo los campos que quieres cambiar, incluido `provider` (por ejemplo, para pasar una fuente `none` a `hmac_generic`). Al pasar a un proveedor con firma, envía `verification_secret` en la misma solicitud para que la fuente no quede sin secret (sin él responde `401`). Para cambiar solo el secret, usa [Definir secret del proveedor](#webhooks-sources-verification_secret); un secret en blanco se rechaza. Una fuente no cambia de proyecto.\n</Endpoint>"},{"anchor":"eliminar-fuente","heading":"Eliminar fuente","markdown":"<Endpoint id=\"webhooks.sources.delete\">\n  Las rutas de la fuente se eliminan con ella y la `ingest_url` deja de responder.\n</Endpoint>"},{"anchor":"rotar-url-de-ingesta","heading":"Rotar URL de ingesta","markdown":"<Endpoint id=\"webhooks.sources.rotate_token\">\n  Genera una nueva `ingest_url`; la anterior deja de funcionar al instante. Actualiza la URL en el proveedor enseguida.\n</Endpoint>"},{"anchor":"definir-secret-del-proveedor","heading":"Definir secret del proveedor","markdown":"<Endpoint id=\"webhooks.sources.verification_secret\" example={{ body: `{ \"secret\": \"whsec_...\" }` }}>\n  Un secret vacío solo se acepta con `provider: \"none\"`.\n</Endpoint>"},{"anchor":"consultar-el-último-evento-recibido","heading":"Consultar el último evento recibido","markdown":"<Endpoint id=\"webhooks.sources.sample\">\n  Devuelve el último cuerpo recibido por la fuente, con el tipo de evento leído y la hora. Útil para descubrir los campos del proveedor antes de configurar `event_type_path`, `dedupe_key_path` y los filtros de las rutas.\n</Endpoint>"},{"anchor":"enviar-un-evento-de-prueba-por-la-fuente","heading":"Enviar un evento de prueba por la fuente","markdown":"<Endpoint id=\"webhooks.sources.test\" example={{ body: `{ \"event_type\": \"invoice.paid\" }` }}>\n  Hace pasar un evento por la fuente como si lo hubiera enviado el proveedor: el tipo se lee en `event_type_path` y las rutas (con sus filtros) deciden adónde va. El cuerpo es el último recibido por la fuente o, si aún no ha recibido nada, un ejemplo del proveedor. `event_type` (opcional) se escribe en la ruta de `event_type_path`; si el cuerpo no tiene ese campo, el tipo usado es `test.event`.\n\n  Es gratis y queda fuera de la cuota mensual. No verifica la firma ni deduplica, pero cuenta en el límite de <Limit name=\"webhookIngestPerMinute\" /> solicitudes por minuto de la fuente.\n</Endpoint>"},{"anchor":"crear-ruta","heading":"Crear ruta","markdown":"<Endpoint\n  id=\"webhooks.routes.create\"\n  example={{ body: `{\n\"source_id\": \"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27\",\n\"endpoint_id\": \"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34\",\n\"event_types\": [\"checkout.session.completed\", \"invoice.paid\"]\n}` }}\n>\n  Conecta una fuente con un endpoint **del mismo proyecto** (una fuente y un endpoint de proyectos distintos responden `400`). `event_types` restringe la ruta a esos tipos, hasta <Limit name=\"webhookRouteEventTypesMax\" />; vacío, la ruta lleva todos los eventos de la fuente. Hasta <Limit name=\"webhookRoutesPerSourceMax\" /> rutas por fuente.\n</Endpoint>"},{"anchor":"listar-rutas","heading":"Listar rutas","markdown":"<Endpoint id=\"webhooks.routes.list\" example={{ query: { source_id: 'a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27' } }}>\n  Filtros opcionales por `source_id` y `project_id`.\n</Endpoint>"},{"anchor":"modificar-ruta","heading":"Modificar ruta","markdown":"<Endpoint id=\"webhooks.routes.update\" example={{ body: `{ \"event_types\": [\"invoice.paid\"], \"is_active\": false }` }}>\n  Cambia los tipos de evento de la ruta (lista vacía = todos) o la pausa con `is_active: false`. Envía solo los campos que quieres cambiar.\n</Endpoint>"},{"anchor":"eliminar-ruta","heading":"Eliminar ruta","markdown":"<Endpoint id=\"webhooks.routes.delete\" />"},{"anchor":"recibir-evento-de-un-proveedor","heading":"Recibir evento de un proveedor","markdown":"<Endpoint id=\"webhooks.ingest\" example={{ pathParams: { token: 'src_4f8a2c6e1b9d3f7a5c0e8b2d6f4a1c9e7b3d5f0a2c8e6b4d' }, headers: { 'Stripe-Signature': 't=1790338000,v1=5257a869…' }, body: `{ \"id\": \"evt_1Q2w3E\", \"type\": \"checkout.session.completed\", \"data\": { \"object\": { \"id\": \"cs_test_a1B2\" } } }` }}>\n  Es la URL que llama el proveedor; no necesitas llamarla tú. El cuerpo se guarda y se reenvía byte a byte, con el `Content-Type` original. Las respuestas están pensadas para que el proveedor decida si reintenta:\n\n  | Estado | Cuándo                                                                                |\n  | ------ | ------------------------------------------------------------------------------------- |\n  | `200`  | Aceptado, duplicado (reenvío del proveedor) o sin ruta para el tipo.                  |\n  | `401`  | Firma inválida, o secret del proveedor aún no definido; no se guarda nada.            |\n  | `404`  | Fuente desconocida o desactivada, o herramienta no habilitada en la cuenta.           |\n  | `429`  | Más de <Limit name=\"webhookIngestPerMinute\" /> solicitudes por minuto para la fuente. |\n  | `5xx`  | Fallo al guardar; el proveedor debe reintentar.                                       |\n</Endpoint>"},{"anchor":"exemplo-stripe","heading":"Ejemplo: Stripe","markdown":"<Steps>\n  <Step>\n    [Crea un endpoint](#webhooks-endpoints-create) para cada servicio tuyo que necesite los eventos de Stripe. Los eventos llegan por las rutas, así que `events` puede quedar vacío.\n  </Step>\n\n  <Step>\n    [Crea una fuente](#webhooks-sources-create) con `provider: \"stripe\"`, `event_type_path: \"type\"` y `dedupe_key_path: \"id\"`, todavía sin el secret.\n  </Step>\n\n  <Step>\n    En el panel de Stripe, añade un endpoint de webhook con la `ingest_url` y copia el **signing secret** (`whsec_…`) en [Definir secret del proveedor](#webhooks-sources-verification_secret).\n  </Step>\n\n  <Step>\n    [Crea una ruta](#webhooks-routes-create) de la fuente a cada endpoint, con `event_types` cuando un servicio solo necesita algunos eventos. Comprueba el recorrido con [Enviar un evento de prueba por la fuente](#webhooks-sources-test).\n  </Step>\n</Steps>\n\nTus servicios pasan a recibir los eventos firmados por la plataforma (valídalos con el secret de cada endpoint). Si prefieren validar la firma del propio Stripe, activa `forward_original_headers` en la fuente: `Stripe-Signature` y el cuerpo idéntico llegan también."},{"anchor":"sem-creditos","heading":"Sin créditos","markdown":"El proveedor nunca se rechaza por falta de saldo. Las entregas que se cobrarían quedan con estado `blocked_no_credits` (contadas en `blocked` en la respuesta al proveedor) y se cobran y liberan automáticamente en cuanto la cuenta tenga créditos. Si el saldo no vuelve en <Limit name=\"webhookBlockedRetention\" />, terminan como `failed`."},{"anchor":"canvas","heading":"Canvas del dashboard","markdown":"En el dashboard, cada proyecto aparece como un canvas con las fuentes, los endpoints y las rutas entre ellos. Estas rutas alimentan esa pantalla; en una integración por API, los listados anteriores suelen bastar. Las tres aceptan `?project_id=` (por defecto: el proyecto por defecto)."},{"anchor":"consultar-el-mapa-del-flujo","heading":"Consultar el mapa del flujo","markdown":"<Endpoint id=\"webhooks.graph\" example={{ query: { project_id: '3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53' } }}>\n  Fuentes, endpoints y rutas del proyecto con los contadores de las últimas 24 horas: por fuente, mensajes recibidos, el último y los tipos de evento recientes; por endpoint y por ruta, entregas, fallos y el estado de la última.\n</Endpoint>"},{"anchor":"consultar-el-layout-del-canvas","heading":"Consultar el layout del canvas","markdown":"<Endpoint id=\"webhooks.layout.get\" />"},{"anchor":"guardar-el-layout-del-canvas","heading":"Guardar el layout del canvas","markdown":"<Endpoint\n  id=\"webhooks.layout.save\"\n  example={{ body: `{\n\"positions\": {\n  \"source:a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27\": { \"x\": 0, \"y\": 160 },\n  \"endpoint:8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34\": { \"x\": 520, \"y\": 200 }\n}\n}` }}\n>\n  `positions` guarda las coordenadas de cada tarjeta: claves `platform`, `source:<id>` o `endpoint:<id>`. Las tarjetas sin posición las coloca automáticamente el dashboard.\n</Endpoint>"},{"anchor":"alertas","heading":"Alertas","markdown":"El titular de la cuenta recibe notificaciones en el dashboard y por e-mail. Cada tipo se puede desactivar en las preferencias de notificación (categoría webhooks).\n\n| Tipo                        | Cuándo                                                                                                                                           |\n| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `webhook_endpoint_failing`  | Un endpoint empieza a fallar (primer intento con error después de estar sano). No se repite hasta que una respuesta `2xx` cierre la racha.       |\n| `webhook_endpoint_disabled` | La desactivación automática apaga un endpoint que falla desde hace <Limit name=\"webhookDisableAfter\" /> (no cuando lo pausas ni por `410 Gone`). |\n| `webhook_quota`             | Las entregas del mes alcanzan el <Limit name=\"webhookQuotaAlerts\" /> de la cuota mensual, una vez por umbral y por mes.                          |\n\nLas alertas nunca retrasan ni bloquean un envío."},{"anchor":"retries","heading":"Entrega y reintentos","markdown":"El calendario de reintentos, qué cuenta como éxito, `Retry-After`, `410 Gone` y la desactivación automática de endpoints que fallan durante <Limit name=\"webhookDisableAfter\" /> seguidos están en la guía [Webhooks](/es/webhooks#retries). Los reintentos automáticos no se cobran de nuevo; solo [Reenviar mensaje](#webhooks-messages-retry) y [Reenviar entregas fallidas](#webhooks-endpoints-recover) generan entregas nuevas."},{"anchor":"api-legada","heading":"API heredada de la cuenta","markdown":"Las rutas antiguas de webhooks de la cuenta (`/v1/account/webhooks`) están **obsoletas**: responden con los headers `Deprecation: true` y `Link` apuntando a los proyectos de Webhook Relay. Siguen funcionando, y los endpoints creados con ellas quedan en el proyecto por defecto. Usa Webhook Relay en integraciones nuevas. La ruta del secret de los callbacks de jobs no es obsoleta (consulta [Secrets y rotación](/es/webhooks#secrets))."},{"anchor":"erros","heading":"Errores","markdown":"<ErrorTable tool=\"webhook-relay\" />\n\nLos errores de validación comunes (`validation.failed`, `not_found`) y de saldo (`billing.insufficient_credits`) están en [Errores](/es/erros)."},{"anchor":"limites","heading":"Límites","markdown":"| Límite                                           | Valor                                              |\n| ------------------------------------------------ | -------------------------------------------------- |\n| Entregas gratis por mes (por cuenta)             | <Limit name=\"webhookFreeDeliveries\" />             |\n| Tamaño del `payload` / cuerpo recibido           | <Limit name=\"webhookPayloadMax\" />                 |\n| Proyectos por cuenta                             | <Limit name=\"webhookProjectsMax\" />                |\n| Endpoints por cuenta                             | <Limit name=\"webhookEndpointsMax\" />               |\n| Headers personalizados por endpoint              | <Limit name=\"webhookCustomHeadersMax\" />           |\n| Tipos de evento por cuenta                       | <Limit name=\"webhookEventTypesMax\" />              |\n| Mensajes por lote                                | <Limit name=\"webhookBatchMax\" />                   |\n| Entregas por recuperación                        | <Limit name=\"webhookRecoverMax\" />                 |\n| Fuentes por cuenta                               | <Limit name=\"webhookSourcesMax\" />                 |\n| Rutas por fuente                                 | <Limit name=\"webhookRoutesPerSourceMax\" />         |\n| Tipos de evento por ruta                         | <Limit name=\"webhookRouteEventTypesMax\" />         |\n| Solicitudes recibidas por fuente                 | <Limit name=\"webhookIngestPerMinute\" /> por minuto |\n| Espera por créditos (`blocked_no_credits`)       | <Limit name=\"webhookBlockedRetention\" />           |\n| Retención del registro de mensajes               | <Limit name=\"webhookLogRetention\" />               |\n| Ventana de búsqueda en el cuerpo de los mensajes | <Limit name=\"webhookSearchBodyWindow\" />           |\n\nLos límites por cuenta suman todos los proyectos. Son los valores por defecto de la plataforma, que ella puede ajustar."}]}],"fr":[{"slug":"agentes-de-ia","url":"https://docs.respawntech.dev/fr/agentes-de-ia","title":"Agents IA (MCP)","description":"Connectez Claude, Cursor, VS Code et d'autres agents aux outils via le serveur MCP, avec un coût visible et une limite de dépense par clé.","sections":[{"anchor":"top","heading":"","markdown":"Le serveur **MCP** (Model Context Protocol) permet à un agent IA de découvrir et d'utiliser les outils de la plateforme, comme créer un QR, convertir un PDF ou interroger la base de connaissances, avec votre compte et vos crédits. Il applique les mêmes règles que l'API : mêmes prix, mêmes scopes et mêmes quotas gratuits.\n\n<Callout title=\"Déploiement progressif\">\n  Le serveur MCP est activé progressivement. Tant qu'il n'est pas actif dans votre environnement, `/mcp` répond `404` avec le code `mcp.disabled`.\n</Callout>"},{"anchor":"conexao","heading":"Connexion","markdown":"|                  |                                               |\n| ---------------- | --------------------------------------------- |\n| URL              | <McpUrl />                                    |\n| Transport        | Streamable HTTP, sans session, réponses JSON  |\n| Authentification | `Authorization: Bearer <token d'API>`         |\n| Scope requis     | `mcp:use`, en plus des scopes de chaque outil |\n\nNe mettez jamais le token dans l'URL (`?token=`, `?api_key=`…) : le serveur la refuse avec `400`. Les connecteurs de produits comme Claude.ai et ChatGPT, qui demandent une connexion au lieu d'un token, ne sont pas encore pris en charge."},{"anchor":"chave","heading":"Créer une clé pour l'agent","markdown":"Dans le [dashboard](https://dashboard.respawntech.dev), sous **APIs**, créez un token réservé à l'agent. Les modèles prêts à l'emploi remplissent scopes et contrôles :\n\n* **Agent de support** : recherche dans la base de connaissances et consultation du solde.\n* **Agent complet** : tous les outils.\n* **Lecture seule** : consultations uniquement.\n\nUne clé d'agent devrait avoir une **limite de dépense quotidienne** (suggestion : <Limit name=\"mcpSuggestedDailyLimit\" />) et une **date d'expiration**. Voir [Contrôles de la clé](/fr/autenticacao#controles-da-chave)."},{"anchor":"clientes","heading":"Configurer le client","markdown":"<McpClientSnippets />\n\nLes clients qui ne parlent que stdio passent par la passerelle `mcp-remote`, comme dans le dernier exemple."},{"anchor":"ferramentas","heading":"Outils","markdown":"L'agent ne voit que les outils autorisés par les scopes de la clé. Les descriptions qu'il reçoit sont en anglais par défaut ; utilisez `lang` (ci-dessous) pour une autre langue.\n\n<McpToolsTable />\n\nLes outils **destructifs** demandent une confirmation à l'utilisateur dans la plupart des clients. Ceux marqués dry-run acceptent `dry_run: true` et ne renvoient que l'estimation du coût."},{"anchor":"parametros","heading":"Paramètres de connexion","markdown":"Tous sont facultatifs et se placent dans la query string de l'URL. Ils ne font que **restreindre** ce que la clé autorise, jamais l'élargir. Un paramètre inconnu ou une valeur invalide renvoie `400`, pour qu'une faute de frappe ne se transforme pas en « aucun filtre ».\n\n<McpParamsTable />\n\nPar exemple, un agent de support qui ne fait que lire la base de connaissances, avec des réponses en portugais : <McpUrl query=\"toolsets=rag&access=read&lang=pt\" />\n\nLes paramètres répétés d'une connexion à l'autre peuvent devenir un **profil** dans le dashboard (`profile=<slug>`), et une clé peut avoir un profil par défaut."},{"anchor":"custos","heading":"Coûts et contrôle des dépenses","markdown":"* Chaque appel coûte autant que l'opération équivalente dans l'API, et les lectures mesurées entrent dans le même [quota gratuit quotidien](/fr/cobranca-e-limites#franquia-diaria).\n* Chaque réponse indique le coût dans `_meta[\"tech.respawn/cost\"]` : crédits facturés, estimation (en dry-run), solde après l'opération et reste de la limite du jour.\n* `simulate=true` dans l'URL fait que tous les outils payants ne renvoient que l'estimation. C'est la façon de tester un agent sans dépenser.\n* Dépasser la limite quotidienne de la clé renvoie `billing.spend_limit_reached` ; dépasser le plafond par appel (`max_call_credits`) renvoie `billing.call_limit_exceeded`.\n* Le **bouton panique** du dashboard coupe immédiatement l'accès de tous les agents du compte (`mcp.agents_disabled`).\n* Le relevé du dashboard ventile les dépenses par canal (API, MCP, dashboard) et par clé."},{"anchor":"idempotencia","heading":"Idempotence","markdown":"Les agents répètent souvent leurs appels. Les outils qui créent ou modifient quelque chose acceptent `idempotency_key` ; sans lui, un appel identique (même clé, même outil, mêmes arguments) dans un délai de <Limit name=\"mcpIdempotencyWindow\" /> renvoie le résultat précédent sans exécution ni nouvelle facturation (`_meta[\"tech.respawn/replayed\"]`)."},{"anchor":"limites","heading":"Limites","markdown":"| Limite                  | Valeur                                                                                   |\n| ----------------------- | ---------------------------------------------------------------------------------------- |\n| Requêtes                | <Limit name=\"rateLimitPerMinute\" /> par minute et par clé, cumulées avec celles de l'API |\n| Corps de la requête     | <Limit name=\"mcpRequestMax\" />                                                           |\n| Résultat d'un outil     | <Limit name=\"mcpResultMax\" />                                                            |\n| Appels à `tools_search` | <Limit name=\"mcpToolsSearchPerMinute\" /> par minute                                      |"},{"anchor":"erros","heading":"Erreurs","markdown":"Les erreurs métier reviennent comme résultat de l'outil, avec un message qui indique à l'agent quoi faire et le code dans `_meta[\"tech.respawn/error\"]`. Les erreurs de connexion reviennent en HTTP, au format `{\"error\": {\"code\", \"message\"}}`.\n\n<ErrorTable group=\"agents\" />\n\nLes erreurs des outils sont les mêmes que dans l'API ; voir [Codes d'erreur](/fr/erros)."}]},{"slug":"autenticacao","url":"https://docs.respawntech.dev/fr/autenticacao","title":"Authentification","description":"Comment s'authentifier auprès des outils avec un token d'API, les scopes et les prérequis du compte.","sections":[{"anchor":"token-dapi","heading":"Token d'API","markdown":"Toutes les routes `/v1/tools/*` exigent un **token d'API**, créé dans le [dashboard](https://dashboard.respawntech.dev) sous **APIs**.\n\n| Méthode                     | Exemple                              |\n| --------------------------- | ------------------------------------ |\n| En-tête dédié (recommandé)  | `X-API-Token: rsk_live_...`          |\n| Authorization               | `Authorization: ApiKey rsk_live_...` |\n| Authorization (alternative) | `Authorization: Token rsk_live_...`  |\n\n<Callout title=\"Bonnes pratiques\">\n  Stockez le token dans la variable d'environnement `RESPAWN_API_TOKEN`, créez un token par intégration avec les **scopes minimaux** nécessaires et révoquez ceux que vous n'utilisez plus.\n</Callout>"},{"anchor":"prérequis-du-compte","heading":"Prérequis du compte","markdown":"Avant tout appel aux outils, le compte doit avoir :\n\n* **Une adresse e-mail vérifiée** — sinon, `auth.email_not_verified` (403).\n* **Un solde non négatif** — un compte suspendu renvoie `billing.user_bankrupt` (402).\n* **Suffisamment de crédits** — les opérations payantes sans solde renvoient `billing.insufficient_credits` (402)."},{"anchor":"scopes","heading":"Scopes","markdown":"Chaque route accepte un ensemble de scopes ; il suffit que le token en possède **un**. En cas de permission manquante, l'API renvoie `scope.missing` (403) avec `details.required` et `details.provided`. Les scopes acceptés par chaque endpoint figurent sur la carte de l'endpoint.\n\n<ScopeTable />\n\n<Callout type=\"warn\" title=\"Le PDF est plus restrictif en lecture\">\n  Les routes de lecture PDF acceptent uniquement `tools:pdf:read` ou `tools:manage`. Le scope générique `tools:read` et `tools:pdf:manage` ne donnent **pas** accès à la lecture PDF.\n</Callout>"},{"anchor":"combinaisons-recommandées","heading":"Combinaisons recommandées","markdown":"| Cas d'usage                                      | Scopes suggérés                                                    |\n| ------------------------------------------------ | ------------------------------------------------------------------ |\n| Générer et gérer des QR codes                    | `tools:qr:dynamic:manage`                                          |\n| Tableau de bord en lecture seule des QR          | `tools:qr:dynamic:read`                                            |\n| Pipeline PDF                                     | `tools:pdf:manage` + `tools:pdf:read`                              |\n| Vérifier des URL avant de les afficher           | `tools:safety:read`                                                |\n| Assistant qui interroge la base de connaissances | `tools:rag:query`                                                  |\n| Envoyer et maintenir les documents de la base    | `tools:rag:manage`                                                 |\n| [Agent IA (MCP)](/fr/agentes-de-ia)              | `mcp:use` + les scopes des outils qu'il utilisera + `account:read` |\n| Intégration complète                             | `tools:manage` (plus simple, moins sûr)                            |"},{"anchor":"controles-da-chave","heading":"Contrôles de la clé","markdown":"En plus des scopes, chaque token peut avoir des contrôles configurés dans le dashboard. Ils s'appliquent sur tous les canaux, dans l'API comme dans le [MCP](/fr/agentes-de-ia) :\n\n* **Expiration** : passée la date, le token cesse de fonctionner (`auth.unauthorized`).\n* **Limite de dépense quotidienne** : un débit qui dépasserait la limite du jour est refusé avec `billing.spend_limit_reached` (402), avec `details.limit`, `details.spent_today` et `details.required`. Le compteur est remis à zéro à minuit UTC.\n* **Restriction par base de connaissances** : le token ne voit que les bases choisies ; les autres répondent `not_found`, et il ne peut pas créer de nouvelles bases (`forbidden`).\n* **Modèles prêts à l'emploi** : à la création de la clé, « Lecture seule », « Agent de support » et « Agent complet » remplissent scopes et contrôles d'un coup.\n\nL'expiration, la limite et les bases peuvent être modifiées ensuite ; les scopes, non. Pour changer de scopes, créez une autre clé."},{"anchor":"erreurs-dauthentification","heading":"Erreurs d'authentification","markdown":"<ErrorTable group=\"auth\" />"},{"anchor":"dashboard-vs-token-dapi","heading":"Dashboard vs token d'API","markdown":"* **Dashboard** — la navigation dans le tableau de bord ne consomme pas le quota gratuit quotidien et ne débite pas de lectures mesurées.\n* **Token d'API** — les lectures mesurées partagent le [quota gratuit quotidien](/fr/cobranca-e-limites#franquia-diaria) de <Limit name=\"dailyQuota\" /> requêtes (<Limit name=\"dailyQuotaPremium\" /> pour les comptes premium). Au-delà, chaque lecture débite des crédits."},{"anchor":"exemple","heading":"Exemple","markdown":"Créer un QR en dry-run pour valider le token, les scopes et le coût sans rien débiter :\n\n<ApiExamples\n  method=\"POST\"\n  path=\"/v1/tools/image/qr-code/dynamic\"\n  headers={{ 'X-Dry-Run': 'true', 'X-Include-Cost': 'true' }}\n  body={`{\n\"name\": \"Campanha\",\n\"target_url\": \"https://example.com\"\n}`}\n/>"}]},{"slug":"cobranca-e-limites","url":"https://docs.respawntech.dev/fr/cobranca-e-limites","title":"Facturation et limites","description":"Crédits, dry-run, idempotence, rate limit, quota gratuit quotidien de lectures et timeouts.","sections":[{"anchor":"modèle-de-crédits","heading":"Modèle de crédits","markdown":"* Le solde est affiché en **crédits** dans le dashboard, et tous les montants de cette documentation sont exprimés en crédits.\n* Les opérations payantes sont débitées à la confirmation — jamais lors du dry-run.\n* Les règles de tarification de chaque outil figurent sur la page de l'outil : [QR Dynamique](/fr/ferramentas/qr-dinamico#custos), [PDF Convert](/fr/ferramentas/pdf-convert#custos), [Base de connaissances](/fr/ferramentas/base-de-conhecimento#custos), [Audio](/fr/ferramentas/audio#custos).\n* Chaque token peut avoir une [limite de dépense quotidienne](/fr/autenticacao#controles-da-chave) : la dépasser renvoie `billing.spend_limit_reached`.\n\n<Callout title=\"Prix exact\">\n  Les coûts de création peuvent inclure des suppléments (slug personnalisé, design, rétention du PDF…). Effectuez un dry-run avec le payload réel pour connaître le total exact.\n</Callout>"},{"anchor":"dry-run","heading":"Dry-run et aperçu du coût","markdown":"| En-tête                | Effet                                               |\n| ---------------------- | --------------------------------------------------- |\n| `X-Dry-Run: true`      | Valide et calcule le coût sans persister ni débiter |\n| `X-Preview-Cost: true` | Alias de `X-Dry-Run`                                |\n| `X-Include-Cost: true` | Inclut `meta.cost` dans la réponse                  |\n\nLe dry-run est accepté sur les opérations payantes signalées sur la carte de chaque endpoint et sur les **lectures mesurées** (sauf les téléchargements). La réponse est `200` et rien n'est persisté ni débité.\n\nLes routes [Audio](/fr/ferramentas/audio#dry-run) demandent le dry-run avec `?dry_run=true` dans la query, pas avec l'en-tête. L'audio est facturé à la minute d'audio, par 1 000 caractères ou à la seconde générée, et le montant de la transcription, de la parole et des autres jobs est remboursé si le moteur échoue."},{"anchor":"devis-dune-opération-payante","heading":"Devis d'une opération payante","markdown":"Sur les `POST` de création et de conversion, `data` contient le devis ligne par ligne :\n\n```json\n{\n  \"success\": true,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"data\": {\n    \"total_cost_credits\": 15000,\n    \"total_cost_units\": 15000,\n    \"items\": [\n      { \"description\": { \"key\": \"billing.pdf.convert_base\" }, \"cost_credits\": 10000, \"type\": \"base\" },\n      { \"description\": { \"key\": \"billing.pdf.retention.7\" }, \"cost_credits\": 5000, \"type\": \"storage\" }\n    ],\n    \"is_monthly_recurring\": false,\n    \"currency\": \"CREDITS\"\n  }\n}\n```\n\n<Callout type=\"warn\" title=\"Montants en unités\">\n  Malgré leur nom, `total_cost_credits` et `cost_credits` du devis (ainsi que `cost_credits` des conversions PDF) sont exprimés en **unités internes** : divisez par 10 000 pour obtenir des crédits. Dans l'exemple, 15 000 unités = 1,5 credit.\n</Callout>\n\nPour le QR Dynamique, le devis inclut également `slot_usage` (utilisation des slots et indication si la création ouvrira un nouveau slot payant) et `recurring_cost`."},{"anchor":"aperçu-dune-lecture-mesurée","heading":"Aperçu d'une lecture mesurée","markdown":"Pour les lectures mesurées, le dry-run n'exécute pas la requête — il indique seulement la position dans le quota et si la lecture serait facturée :\n\n```json\n{\n  \"success\": true,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"data\": {\n    \"operation\": \"qr_dynamic.analytics\",\n    \"metering\": {\n      \"within_quota\": true,\n      \"quota_limit\": 2000,\n      \"quota_used\": 125,\n      \"quota_remaining\": 1875,\n      \"cost_units\": 300,\n      \"would_charge\": false\n    }\n  },\n  \"meta\": {\n    \"cost\": {\n      \"units\": 300,\n      \"credits\": 0.03,\n      \"charged\": false,\n      \"within_quota\": true,\n      \"quota_limit\": 2000,\n      \"quota_used\": 125,\n      \"quota_remaining\": 1875\n    }\n  }\n}\n```\n\nAvec `X-Include-Cost: true` sur une lecture réelle, le même objet `meta.cost` accompagne la réponse normale (`charged: true` lorsque la lecture a été facturée)."},{"anchor":"idempotence","heading":"Idempotence","markdown":"Envoyez `X-Idempotency-Key` (UUID recommandé) sur n'importe quelle route des outils :\n\n* la clé est valable **<Limit name=\"idempotencyTtl\" />** ;\n* même clé + même payload → la réponse d'origine est réutilisée, sans nouvelle facturation ;\n* même clé + payload différent → `idempotency.conflict` (409).\n\nUtilisez-la systématiquement sur les `POST` de création et de conversion pour que les retries réseau ne soient pas facturés deux fois.\n\nException : les routes de la [Base de connaissances](/fr/ferramentas/base-de-conhecimento#limites) ignorent cet en-tête, car elles sont déjà idempotentes (un contenu répété n'est ni réindexé ni facturé)."},{"anchor":"rate-limit","heading":"Rate limit","markdown":"Chaque token d'API peut effectuer **<Limit name=\"rateLimitPerMinute\" /> requêtes par minute** sur les routes `/v1/tools/*`. Chaque réponse indique la situation :\n\n```http\nX-RateLimit-Limit: 60\nX-RateLimit-Remaining: 42\nX-RateLimit-Reset: 1712345678\n```\n\nEn cas de dépassement, l'API renvoie `rate_limit.exceeded` (429). Attendez `X-RateLimit-Reset` avant de renvoyer la requête."},{"anchor":"retry","heading":"Réessayer avec attente (retry)","markdown":"Ne réessayez que ce qui peut réussir à la tentative suivante :\n\n* **`429`** : attendez l’heure indiquée par `X-RateLimit-Reset` (secondes Unix).\n* **`5xx` et erreurs réseau** : attendez un peu plus à chaque fois, avec une part d’aléatoire (*backoff* exponentiel avec *jitter*) : environ 1 s, 2 s, 4 s… jusqu’à 30 s, 5 tentatives au maximum.\n* **Autres `4xx`** (validation, scope, solde) : ne réessayez pas. La réponse ne changera pas tant que la requête n’est pas corrigée.\n\nSur un `POST` qui crée ou facture quelque chose, envoyez **la même** `X-Idempotency-Key` à chaque tentative : si la première est arrivée et que seule la réponse s’est perdue, la nouvelle tentative renvoie le résultat d’origine sans facturer à nouveau.\n\n<Tabs groupId=\"code-language\" persist items={['JavaScript', 'Python']}>\n  <Tab value=\"JavaScript\">\n    ```js\n    // Réessaie 429, 5xx et les erreurs réseau ; renvoie les autres erreurs tout de suite.\n    async function withRetry(send, attempts = 5) {\n      for (let attempt = 1; ; attempt++) {\n        let response;\n        try {\n          response = await send();\n        } catch (error) {\n          if (attempt === attempts) throw error; // erreur réseau\n        }\n        if (response && response.status !== 429 && response.status < 500) return response;\n        if (attempt === attempts) return response;\n\n        const reset = Number(response?.headers.get('X-RateLimit-Reset'));\n        const wait =\n          response?.status === 429 && reset\n            ? Math.max(0, reset * 1000 - Date.now())\n            : Math.min(2 ** (attempt - 1) * 1000, 30_000) * (0.5 + Math.random());\n        await new Promise((resolve) => setTimeout(resolve, wait));\n      }\n    }\n\n    const key = crypto.randomUUID(); // la même clé à chaque tentative\n    const response = await withRetry(() =>\n      fetch(url, {\n        method: 'POST',\n        headers: {\n          'X-API-Token': process.env.RESPAWN_API_TOKEN,\n          'X-Idempotency-Key': key,\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify(payload),\n      }),\n    );\n    ```\n  </Tab>\n\n  <Tab value=\"Python\">\n    ```python\n    import os, random, time, uuid\n    import requests\n\n    # Réessaie 429, 5xx et les erreurs réseau ; renvoie les autres erreurs tout de suite.\n    def with_retry(send, attempts=5):\n        for attempt in range(1, attempts + 1):\n            try:\n                response = send()\n            except (requests.ConnectionError, requests.Timeout):\n                if attempt == attempts:\n                    raise\n                response = None\n            if response is not None and response.status_code != 429 and response.status_code < 500:\n                return response\n            if attempt == attempts:\n                return response\n            reset = response.headers.get(\"X-RateLimit-Reset\") if response is not None else None\n            if response is not None and response.status_code == 429 and reset:\n                wait = max(0, int(reset) - time.time())\n            else:\n                wait = min(2 ** (attempt - 1), 30) * (0.5 + random.random())\n            time.sleep(wait)\n\n    key = str(uuid.uuid4())  # la même clé à chaque tentative\n    response = with_retry(lambda: requests.post(\n        url,\n        json=payload,\n        headers={\"X-API-Token\": os.environ[\"RESPAWN_API_TOKEN\"], \"X-Idempotency-Key\": key},\n        timeout=30,\n    ))\n    ```\n  </Tab>\n</Tabs>"},{"anchor":"franquia-diaria","heading":"Quota gratuit quotidien de lectures","markdown":"Les lectures effectuées avec un **token d'API** (listes, statuts, analytics, téléchargements…) sont décomptées d'un quota gratuit quotidien partagé entre tous les outils :\n\n| Élément                     | Valeur                                 |\n| --------------------------- | -------------------------------------- |\n| Lectures gratuites par jour | **<Limit name=\"dailyQuota\" />**        |\n| Comptes premium             | **<Limit name=\"dailyQuotaPremium\" />** |\n| Réinitialisation            | <Limit name=\"quotaReset\" />            |\n\nChaque lecture mesurée renvoie la position dans le quota :\n\n```http\nX-API-Quota-Limit: 2000\nX-API-Quota-Used: 847\nX-API-Quota-Remaining: 1153\n```\n\nLes requêtes effectuées depuis le [dashboard](https://dashboard.respawntech.dev) ne sont **pas** décomptées du quota ni facturées."},{"anchor":"lectures-mesurées","heading":"Lectures mesurées","markdown":"Une fois le quota épuisé, chaque lecture via l'API débite :\n\n<MeteredReadsTable />\n\nLes opérations d'écriture sans coût propre (`PATCH`, activer, désactiver, `DELETE`) et le dry-run de création sont toujours gratuits.\n\n<Callout type=\"idea\" title=\"Évitez le polling\">\n  Le quota couvre la plupart des intégrations. Pour suivre les changements d'état, préférez les [webhooks](/fr/webhooks) plutôt que d'interroger le statut en boucle.\n</Callout>"},{"anchor":"timeouts","heading":"Timeouts","markdown":"* La plupart des routes : **<Limit name=\"requestTimeout\" />**.\n* Téléchargements de fichiers : **<Limit name=\"downloadTimeout\" />**."}]},{"slug":"erros","url":"https://docs.respawntech.dev/fr/erros","title":"Codes d'erreur","description":"Enveloppe de réponse, format d'erreur et référence de tous les codes renvoyés par les outils.","sections":[{"anchor":"réponse-de-succès","heading":"Réponse de succès","markdown":"```json\n{\n  \"success\": true,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"data\": {},\n  \"meta\": {}\n}\n```\n\n| Champ      | Description                                               |\n| ---------- | --------------------------------------------------------- |\n| `success`  | `true` pour les réponses OK                               |\n| `trace_id` | ID de corrélation (également dans l'en-tête `X-Trace-Id`) |\n| `data`     | Ressource ou résultat                                     |\n| `meta`     | Facultatif — coût (`meta.cost`), pagination, etc.         |\n\nAvec `X-Include-Cost: true` ou en dry-run, `meta` contient le coût de l'opération — voir les formats dans [Dry-run](/fr/cobranca-e-limites#dry-run). Les listes renvoient la pagination dans `data` (`total` et, pour le PDF, `page` et `page_size`), et non dans `meta`."},{"anchor":"format-derreur","heading":"Format d'erreur","markdown":"```json\n{\n  \"success\": false,\n  \"trace_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"error\": {\n    \"code\": \"billing.insufficient_credits\",\n    \"message\": \"Créditos insuficientes para esta operação.\",\n    \"details\": { \"required\": 100, \"available\": 50 }\n  }\n}\n```\n\n| Champ           | Description                                                                                               |\n| --------------- | --------------------------------------------------------------------------------------------------------- |\n| `error.code`    | Identifiant stable — traitez les erreurs à partir de celui-ci, jamais à partir de `message`               |\n| `error.message` | Texte lisible ; peut changer d'une version à l'autre                                                      |\n| `error.details` | Contexte structuré facultatif ; pour `validation.failed`, `details.fields` contient les erreurs par champ |\n| `trace_id`      | À indiquer dans vos tickets de support                                                                    |"},{"anchor":"erreurs-communes-à-tous-les-outils","heading":"Erreurs communes à tous les outils","markdown":""},{"anchor":"authentification-et-permissions","heading":"Authentification et permissions","markdown":"<ErrorTable group=\"auth\" />"},{"anchor":"crédits-et-compte","heading":"Crédits et compte","markdown":"<ErrorTable group=\"billing\" />"},{"anchor":"requête-et-limites","heading":"Requête et limites","markdown":"<ErrorTable group=\"request\" />"},{"anchor":"agents-ia-mcp","heading":"Agents IA (MCP)","markdown":"<ErrorTable group=\"agents\" />"},{"anchor":"plateforme","heading":"Plateforme","markdown":"<ErrorTable group=\"platform\" />"},{"anchor":"vidéo","heading":"Vidéo","markdown":"<ErrorTable group=\"video\" />"},{"anchor":"erreurs-par-outil","heading":"Erreurs par outil","markdown":"<ToolErrorSections />\n\nLes jobs asynchrones (PDF) exposent également `error_code` sur la ressource elle-même lorsque `status` vaut `failed` ; les codes marqués **asynchrone** n'apparaissent qu'à cet endroit."},{"anchor":"bonnes-pratiques","heading":"Bonnes pratiques","markdown":"1. Traitez les `402` avec un parcours de rechargement dans votre système.\n2. En cas de `429`, respectez `X-RateLimit-Reset` avant de réessayer.\n3. Utilisez `X-Dry-Run` pour détecter `validation.failed` sans frais.\n4. Ne réessayez que les erreurs `5xx` et réseau, toujours avec la même `X-Idempotency-Key`.\n5. Enregistrez le `trace_id` de chaque réponse en erreur."}]},{"slug":"","url":"https://docs.respawntech.dev/fr","title":"Documentation de l'API","description":"Intégrez les outils Respawn Tech Dev grâce à une API REST prévisible — authentification par token, facturation en crédits et dry-run sur toutes les opérations payantes.","sections":[{"anchor":"outils","heading":"Outils","markdown":"<ToolCards />"},{"anchor":"premiers-pas","heading":"Premiers pas","markdown":"<Steps>\n  <Step>\n    ### Créez un token d'API\n\n    Dans le [dashboard](https://dashboard.respawntech.dev), ouvrez **APIs** et créez un token avec les scopes de l'outil que vous allez utiliser. Stockez-le dans la variable d'environnement `RESPAWN_API_TOKEN`. Voir [Authentification](/fr/autenticacao).\n  </Step>\n\n  <Step>\n    ### Estimez le coût avec le dry-run\n\n    Envoyez `X-Dry-Run: true` avant une opération payante pour valider le payload et connaître le coût sans débiter de crédits. Voir [Facturation et limites](/fr/cobranca-e-limites#dry-run).\n  </Step>\n\n  <Step>\n    ### Effectuez l'appel\n\n    Toutes les routes des outils utilisent le préfixe `https://api.respawntech.dev/v1/tools`. Chaque endpoint fournit des exemples prêts à l'emploi en cURL, JavaScript et Python.\n\n    <ApiExamples method=\"GET\" path=\"/v1/tools/image/qr-code/dynamic\" query={{ limit: '20' }} />\n  </Step>\n</Steps>\n\n## En-têtes courants\n\n| En-tête             | Valeur         | Effet                                                                          |\n| ------------------- | -------------- | ------------------------------------------------------------------------------ |\n| `X-API-Token`       | `rsk_live_...` | Authentification (recommandé)                                                  |\n| `X-Dry-Run`         | `true`         | Simule l'opération sans persister ni débiter                                   |\n| `X-Include-Cost`    | `true`         | Inclut `meta.cost` dans la réponse                                             |\n| `X-Idempotency-Key` | UUID           | Évite la double facturation lors des renvois (<Limit name=\"idempotencyTtl\" />) |\n\nLes réponses incluent `X-Trace-Id` (à utiliser dans vos tickets de support), `X-RateLimit-*` et, pour les lectures mesurées, `X-API-Quota-*`.\n\n## Guides\n\n<Cards>\n  <Card title=\"Authentification\" description=\"Token d'API, scopes et prérequis du compte.\" href=\"/fr/autenticacao\" />\n\n  <Card title=\"Facturation et limites\" description=\"Crédits, dry-run, idempotence, rate limit et quota gratuit quotidien.\" href=\"/fr/cobranca-e-limites\" />\n\n  <Card title=\"Webhooks\" description=\"Événements du compte, signature Standard Webhooks, relances et callbacks de jobs.\" href=\"/fr/webhooks\" />\n\n  <Card title=\"Codes d'erreur\" description=\"Enveloppe de réponse et tous les codes par outil.\" href=\"/fr/erros\" />\n\n  <Card title=\"Agents IA (MCP)\" description=\"Connectez Claude, Cursor et d'autres agents aux outils, avec une limite de dépense.\" href=\"/fr/agentes-de-ia\" />\n</Cards>"}]},{"slug":"usar-com-ia","url":"https://docs.respawntech.dev/fr/usar-com-ia","title":"Utiliser avec l’IA","description":"Donnez à votre assistant IA l’accès à cette documentation via le MCP de documentation (public, gratuit, sans jeton), llms.txt ou les pages Markdown.","sections":[{"anchor":"top","heading":"","markdown":"Les assistants IA écrivent de meilleures intégrations quand ils consultent la documentation au lieu de deviner. Il existe trois façons de fournir ces pages à un agent, de la plus complète à la plus simple : le **MCP de documentation**, **llms.txt** et les **pages Markdown**."},{"anchor":"dois-servidores","heading":"Deux serveurs MCP","markdown":"|                  | MCP de documentation                                    | [MCP du compte](/fr/agentes-de-ia)                                                            |\n| ---------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------- |\n| Rôle             | Répondre aux questions sur l’API pendant que vous codez | Utiliser les outils avec votre compte (créer un QR, convertir un PDF, transcrire de l’audio…) |\n| URL              | <McpUrl server=\"docs\" />                                | <McpUrl />                                                                                    |\n| Authentification | Aucune                                                  | Jeton d’API avec `mcp:use`                                                                    |\n| Coût             | Gratuit                                                 | Le même que l’API                                                                             |\n| Accès            | Lecture seule de la documentation publique              | Agit sur le compte, avec portées et limite de dépense                                         |\n\nLes deux peuvent être connectés en même temps : un agent consulte la documentation, puis utilise le MCP du compte pour agir.\n\n<Callout title=\"Ouverture progressive\">\n  Le MCP de documentation est ouvert progressivement. Tant qu’il n’est pas actif, <McpUrl server=\"docs\" /> répond `404` avec le code `mcp.disabled`. llms.txt et les pages Markdown fonctionnent déjà.\n</Callout>"},{"anchor":"conexao","heading":"Connexion","markdown":"|                  |                                                                            |\n| ---------------- | -------------------------------------------------------------------------- |\n| URL              | <McpUrl server=\"docs\" />                                                   |\n| Transport        | Streamable HTTP                                                            |\n| Authentification | Aucune (n’envoyez pas de jeton)                                            |\n| Coût             | Gratuit                                                                    |\n| Limite           | <Limit name=\"mcpDocsRateLimitPerMinute\" /> requêtes par minute par IP      |\n| Langue           | Paramètre `lang` facultatif sur chaque outil : `pt-br`, `en`, `es` ou `fr` |\n\nLes réponses proviennent du même contenu que ce site, y compris les prix en vigueur."},{"anchor":"clientes","heading":"Configurer le client","markdown":"<McpDocsClientSnippets />\n\nDans **Claude.ai** et **ChatGPT**, ajoutez un connecteur personnalisé (custom connector) en indiquant seulement l’URL <McpUrl server=\"docs\" />. Aucune connexion ni jeton n’est nécessaire. Les clients qui ne parlent que stdio utilisent le pont `mcp-remote`, comme dans le dernier exemple."},{"anchor":"ferramentas","heading":"Outils","markdown":"<McpDocsToolsTable />\n\nUn parcours typique : `docs_search` avec la question, `docs_get_page` sur la section trouvée, puis `docs_get_endpoint` ou `docs_get_pricing` pour les détails exacts avant d’écrire le code."},{"anchor":"llms-txt","heading":"Sans MCP : llms.txt et Markdown","markdown":"Pour les agents qui ne lisent que le web :\n\n* [`/llms.txt`](/llms.txt) : index au format [llmstxt.org](https://llmstxt.org), avec chaque page dans chaque langue et le lien vers sa version Markdown.\n* [`/llms-full.txt`](/llms-full.txt) : toutes les pages dans un seul fichier texte.\n* Pages Markdown : `/md/<page>.md` en portugais et `/md/<langue>/<page>.md` dans les autres langues, par exemple [`/md/webhooks.md`](/md/webhooks.md) ou [`/md/fr/webhooks.md`](/md/fr/webhooks.md). La page d’accueil est `/md/index.md`.\n* [`/ai/bundle.json`](/ai/bundle.json) : toute la documentation en JSON structuré (sections, outils, endpoints, prix, erreurs, portées et limites). C’est la base du MCP de documentation."}]},{"slug":"webhooks","url":"https://docs.respawntech.dev/fr/webhooks","title":"Webhooks","description":"Comment la plateforme envoie des webhooks — événements du compte, callbacks de jobs et Webhook Relay — et comment valider la signature Standard Webhooks.","sections":[{"anchor":"top","heading":"","markdown":"La plateforme fait un `POST` vers votre URL quand quelque chose se produit. Tous les envois passent par le même moteur de livraison : le message est enregistré, chaque destination reçoit une livraison avec ses propres relances et chaque appel HTTP est journalisé. Il existe trois origines :\n\n| Origine                                               | Où la configurer                                                          | Coût                                           |\n| ----------------------------------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------- |\n| **Événements du compte** (ex. : `qr.dynamic.scanned`) | **Webhooks** dans le [tableau de bord](https://dashboard.respawntech.dev) | Gratuit                                        |\n| **Callbacks de jobs** (ex. : conversion PDF)          | `webhook_url` dans chaque requête                                         | Gratuit                                        |\n| **Webhook Relay** (vos propres événements)            | API [Webhook Relay](/fr/ferramentas/webhook-relay)                        | Par livraison au-delà du quota mensuel gratuit |"},{"anchor":"eventos","heading":"Événements du compte","markdown":"Enregistrez un endpoint dans **Webhooks** du [tableau de bord](https://dashboard.respawntech.dev) et choisissez les événements. Chaque endpoint a son propre `secret`, affiché à la création puis via l’option de révélation du secret.\n\nCes endpoints appartiennent à [Webhook Relay](/fr/ferramentas/webhook-relay#projetos) : un endpoint de n’importe quel projet abonné à un événement du compte le reçoit, gratuitement. L’ancienne API des webhooks du compte est obsolète ; voir [API historique du compte](/fr/ferramentas/webhook-relay#api-legada).\n\n<WebhookEventsTable />"},{"anchor":"requisicao","heading":"Requête envoyée","markdown":"```http\nPOST <url de l'endpoint>\nContent-Type: application/json\nUser-Agent: RespawnTech-Webhook/2.0\nwebhook-id: msg_2Vf8tK1pQ9zX\nwebhook-timestamp: 1790338000\nwebhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=\nX-Webhook-Signature: sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd\n```\n\n| En-tête               | Contenu                                                                                                            |\n| --------------------- | ------------------------------------------------------------------------------------------------------------------ |\n| `webhook-id`          | Identifiant du message. Il est **identique à chaque relance** : utilisez-le pour écarter les doublons.             |\n| `webhook-timestamp`   | Moment de cette tentative, en secondes Unix.                                                                       |\n| `webhook-signature`   | `v1,<base64>`, une signature par secret actif, séparées par une espace (deux pendant un renouvellement de secret). |\n| `X-Webhook-Signature` | Signature historique `sha256=<hex>`, sans horodatage. Conservée pour les anciens récepteurs.                       |\n\nLes en-têtes suivent le standard [Standard Webhooks](https://www.standardwebhooks.com) : toute bibliothèque de ce standard valide la requête. Les endpoints Webhook Relay peuvent porter des en-têtes supplémentaires (`custom_headers`)."},{"anchor":"corpo","heading":"Corps","markdown":"Les événements du compte et les messages Webhook Relay utilisent la même enveloppe :\n\n```json\n{\n  \"event_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"type\": \"qr.dynamic.scanned\",\n  \"timestamp\": 1790338000,\n  \"payload\": {\n    \"resource_id\": \"…\",\n    \"slug\": \"promo-verao\",\n    \"ip\": \"hash_anonyme\",\n    \"user_agent\": \"Mozilla/5.0 …\",\n    \"referer\": \"https://…\"\n  }\n}\n```\n\nLe format de `payload` dépend du `type`. Les callbacks de jobs gardent leur propre corps (voir [Callback de jobs](#pdf-callback)), et les messages reçus de fournisseurs via Webhook Relay arrivent avec le corps d’origine du fournisseur, inchangé."},{"anchor":"verificar-assinatura","heading":"Valider la signature","markdown":"Le contenu signé est `webhook-id + \".\" + webhook-timestamp + \".\" + corps brut`, en HMAC-SHA256 encodé en base64. Pour valider :\n\n1. Lisez le **corps brut**, avant tout parsing JSON (reformater le JSON change la signature).\n2. Refusez les horodatages éloignés de plus de <Limit name=\"webhookSignatureTolerance\" /> de votre horloge (protection contre le rejeu).\n3. Calculez la signature et comparez-la, **en temps constant**, à chaque élément de `webhook-signature`. Une seule correspondance suffit.\n\nAvec une bibliothèque Standard Webhooks, utilisez le `standard_webhooks_secret` (format `whsec_…`). Sans bibliothèque, la clé HMAC est constituée des octets du `secret` brut.\n\n<Tabs groupId=\"code-language\" persist items={['JavaScript', 'Python', 'Go']}>\n  <Tab value=\"JavaScript\">\n    ```js\n    // npm i standardwebhooks\n    import express from 'express';\n    import { Webhook } from 'standardwebhooks';\n\n    const wh = new Webhook(process.env.WEBHOOK_SECRET); // whsec_…\n    const app = express();\n\n    app.post('/hook', express.raw({ type: '*/*' }), (req, res) => {\n      let event;\n      try {\n        event = wh.verify(req.body, req.headers); // lève une erreur si invalide\n      } catch {\n        return res.sendStatus(401);\n      }\n      // traitez `event` de façon asynchrone et répondez vite\n      res.sendStatus(204);\n    });\n    ```\n  </Tab>\n\n  <Tab value=\"Python\">\n    ```python\n    # pip install standardwebhooks\n    import os\n    from flask import Flask, request\n    from standardwebhooks.webhooks import Webhook\n\n    wh = Webhook(os.environ[\"WEBHOOK_SECRET\"])  # whsec_…\n    app = Flask(__name__)\n\n    @app.post(\"/hook\")\n    def hook():\n        try:\n            event = wh.verify(request.get_data(), dict(request.headers))\n        except Exception:\n            return \"\", 401\n        # traitez `event` de façon asynchrone et répondez vite\n        return \"\", 204\n    ```\n  </Tab>\n\n  <Tab value=\"Go\">\n    ```go\n    // Sans bibliothèque : secret est la valeur brute (pas le whsec_).\n    func verify(secret string, r *http.Request, body []byte) bool {\n    \tid, ts := r.Header.Get(\"webhook-id\"), r.Header.Get(\"webhook-timestamp\")\n    \tt, err := strconv.ParseInt(ts, 10, 64)\n    \tif err != nil || math.Abs(time.Since(time.Unix(t, 0)).Seconds()) > 300 {\n    \t\treturn false\n    \t}\n    \tmac := hmac.New(sha256.New, []byte(secret))\n    \tmac.Write([]byte(id + \".\" + ts + \".\"))\n    \tmac.Write(body)\n    \twant := \"v1,\" + base64.StdEncoding.EncodeToString(mac.Sum(nil))\n    \tfor _, sig := range strings.Fields(r.Header.Get(\"webhook-signature\")) {\n    \t\tif hmac.Equal([]byte(sig), []byte(want)) {\n    \t\t\treturn true\n    \t\t}\n    \t}\n    \treturn false\n    }\n    ```\n  </Tab>\n</Tabs>\n\n<Callout title=\"Anciens récepteurs\">\n  Les récepteurs qui valident déjà `X-Webhook-Signature` continuent de fonctionner : c’est `sha256=` + le HMAC-SHA256 hexadécimal du corps brut, avec le `secret` brut comme clé. Cet en-tête n’a pas d’horodatage ; préférez `webhook-signature` pour les nouvelles intégrations.\n</Callout>"},{"anchor":"secrets","heading":"Secrets et renouvellement","markdown":"| Destination                      | Où obtenir le secret                                                                                                                                             |\n| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Endpoint d’événements du compte  | À la création, ou via l’option de révélation du secret dans **Webhooks** du tableau de bord.                                                                     |\n| Endpoint Webhook Relay           | À la création, ou via [Révéler le secret](/fr/ferramentas/webhook-relay#webhooks-endpoints-secret).                                                              |\n| Callback de jobs (`webhook_url`) | Secret de signature du compte, unique pour tous les callbacks : `GET /v1/account/webhooks/signing-secret` (route du compte, avec la session du tableau de bord). |\n\nChaque secret existe sous deux formes : `secret` (brut) et `standard_webhooks_secret` (`whsec_` + base64 des mêmes octets). Les deux valident les mêmes signatures.\n\nQuand vous [renouvelez le secret](/fr/ferramentas/webhook-relay#webhooks-endpoints-rotate_secret) d’un endpoint, l’ancien continue de signer pendant <Limit name=\"webhookSecretOverlap\" /> : pendant cette période `webhook-signature` porte les deux signatures, et vous mettez à jour le récepteur sans perdre d’événements."},{"anchor":"retries","heading":"Livraison et relances","markdown":"Seule une réponse **2xx** compte comme un succès. Les redirections (3xx) ne sont pas suivies, et les 3xx, 4xx, 5xx, timeouts et erreurs réseau sont relancés selon ce calendrier :\n\n| Tentative | Attente avant |\n| --------- | ------------- |\n| 1         | —             |\n| 2         | 5 secondes    |\n| 3         | 5 minutes     |\n| 4         | 30 minutes    |\n| 5         | 2 heures      |\n| 6         | 5 heures      |\n| 7 et 8    | 10 heures     |\n\nEnviron 27 heures au total, avec une variation de ±10 %. Cas particuliers :\n\n* **`429` et `503`** avec `Retry-After` : la tentative suivante respecte l’en-tête quand il demande plus de temps (jusqu’à 10 heures).\n* **`410 Gone`** : la livraison s’arrête et l’endpoint est désactivé.\n* Après la dernière tentative, la livraison passe en `exhausted`. Avec Webhook Relay, vous pouvez [renvoyer le message](/fr/ferramentas/webhook-relay#webhooks-messages-retry) ou [récupérer les échecs d’un endpoint](/fr/ferramentas/webhook-relay#webhooks-endpoints-recover).\n\nBonnes pratiques côté récepteur :\n\n* Répondez **2xx** le plus vite possible et traitez l’événement de façon asynchrone.\n* **Écartez les doublons par `webhook-id`** : une livraison peut arriver plusieurs fois (ex. : votre serveur l’a traitée mais la réponse s’est perdue).\n* Ne dépendez pas de l’ordre d’arrivée ; utilisez le `timestamp` du corps quand l’ordre compte."},{"anchor":"desativacao","heading":"Désactivation automatique","markdown":"Un endpoint qui échoue sans interruption pendant <Limit name=\"webhookDisableAfter\" /> est désactivé automatiquement (`is_active: false`, avec `disabled_reason`), tout comme un endpoint qui répond `410`. Après avoir réparé le récepteur, réactivez l’endpoint (dans le tableau de bord ou avec `is_active: true` dans Webhook Relay) ; cela remet à zéro la série d’échecs.\n\nL’historique des messages, livraisons et tentatives est conservé pendant <Limit name=\"webhookLogRetention\" />."},{"anchor":"pdf-callback","heading":"Callback de jobs","markdown":"Dans `POST /convert` ou `POST /batch` de [PDF Convert](/fr/ferramentas/pdf-convert), envoyez `webhook_url` dans le body. Quand le job se termine, l’API fait un `POST` vers cette URL :\n\n```json\n{\n  \"job_id\": \"job_abc123\",\n  \"success\": true,\n  \"result_url\": \"https://…\",\n  \"metadata\": { \"conversion_id\": \"…\" }\n}\n```\n\nEn cas d’échec, `success` vaut `false` et le champ `error` contient le message. Le callback a les mêmes en-têtes, signature et relances que les autres webhooks, signé avec le **secret de signature du compte** (voir [Secrets et renouvellement](#secrets)). La `webhook_url` doit être une URL publique `http` ou `https` ; sinon la requête est refusée avec `webhook.invalid_url`."},{"anchor":"webhook-relay","heading":"Vos propres événements","markdown":"Pour envoyer des webhooks de **votre** application à vos clients, avec la même signature, les mêmes relances et le même journal, ou pour recevoir les webhooks de Stripe, GitHub et d’autres et les redistribuer, utilisez [Webhook Relay](/fr/ferramentas/webhook-relay)."}]},{"slug":"ferramentas/audio","url":"https://docs.respawntech.dev/fr/ferramentas/audio","title":"Audio","description":"Transcription, voix synthétique, effets et musique, analyse, conversion, résumé, traduction et revoice d'audio, par REST ou MCP, avec des jobs asynchrones et un prix à la minute.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"audio\" />"},{"anchor":"visao-geral","heading":"Vue d'ensemble","markdown":"Toute opération audio est **asynchrone** et facturée tout de suite :\n\n1. `POST` sur l'opération → **202 Accepted** avec `job_id`, `status: pending` et `poll_after_seconds`.\n2. Un worker traite en arrière-plan.\n3. Interrogez `GET /jobs/{id}` jusqu'à ce que `status` vaille `completed` (ou `failed`, et la facturation est remboursée). Une fois terminé, la réponse contient le résultat et un **lien signé** qui expire.\n\nLes mêmes cas d'usage sont dans le [MCP](/fr/agentes-de-ia) sous forme de tools `audio_*` (toolset `audio`) ; REST et MCP partagent formats, prix et limites.\n\n<Callout type=\"warn\" title=\"Facturé à l'acceptation du job\">\n  Les crédits sont débités à l'acceptation du job. Les échecs du moteur, les étapes devenues inutiles et les prix surestimés sont **remboursés** automatiquement ; rien n'est facturé si la requête est refusée en amont (`audio.too_long`, `audio.price_above_max`…).\n</Callout>\n\n<EndpointList tool=\"audio\" />"},{"anchor":"entrada","heading":"Entrée audio","markdown":"Les opérations qui prennent un audio exigent **exactement un** de ces champs :\n\n| Champ          | De quoi il s'agit                                                                                                                                                                     |\n| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `audio_url`    | URL publique `http(s)` du fichier. Les adresses de réseau interne ou privé sont refusées (`audio.input_blocked`) et le téléchargement est limité à <Limit name=\"audioFetchTimeout\" /> |\n| `audio_base64` | L'audio en ligne, en base64, jusqu'à <Limit name=\"audioInlineMax\" /> décodés                                                                                                          |\n| `file_id`      | Id d'un fichier audio déjà envoyé au compte                                                                                                                                           |\n\nFormats acceptés : mp3, wav, ogg, m4a, flac et webm. La limite est de <Limit name=\"audioMaxDuration\" /> d'audio et de <Limit name=\"audioInputMax\" /> par fichier et par appel. Pour `summarize` et `translate`, vous pouvez envoyer, à la place de l'audio, le `job_id` d'une transcription déjà prête : la transcription est réutilisée et seule la nouvelle opération est facturée.\n\nToutes les routes d'opération acceptent `max_credits` (en unités ; 10 000 unités = 1 crédit) : si le prix le dépasse, la requête est refusée avec `audio.price_above_max` et rien n'est facturé. L'en-tête `Idempotency-Key` sécurise les nouvelles tentatives : la même clé renvoie le premier job au lieu de facturer à nouveau."},{"anchor":"custos","heading":"Coûts","markdown":"<PricingTable tool=\"audio\" group=\"speech-to-text\" />\n\n<PricingTable tool=\"audio\" group=\"text-to-speech\" />\n\n<PricingTable tool=\"audio\" group=\"processing\" />\n\n<PricingTable tool=\"audio\" group=\"revoice\" />\n\nLa « minute commencée » est arrondie à l'entier supérieur. Un audio dont la durée ne se lit pas dans l'en-tête est facturé au plafond de <Limit name=\"audioMaxDuration\" /> et la différence est remboursée à la fin du job. Les lectures de jobs suivent le [quota quotidien](/fr/cobranca-e-limites#franquia-diaria). Le prix exact d'un appel vient du [dry-run](#dry-run)."},{"anchor":"dry-run","heading":"Dry-run","markdown":"Avec `?dry_run=true` dans la query (sur les routes audio, c'est la query et non l'en-tête `X-Dry-Run`), les opérations payantes répondent `200` avec le prix, sans facturer ni créer de job :\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"service_code\": \"audio_transcribe_minute\",\n    \"units\": 3,\n    \"rate\": 15000,\n    \"total_units\": 45000,\n    \"duration_ms\": 125000,\n    \"duration_known\": true,\n    \"parts\": [{ \"service_code\": \"audio_transcribe_minute\", \"units\": 3, \"rate\": 15000, \"total\": 45000 }]\n  }\n}\n```\n\n`rate` et `total_units` sont en unités (10 000 unités = 1 crédit) ; `parts` détaille chaque service facturé. Pour `speak`, le devis contient `chars`, `voice`, `language` et `format`."},{"anchor":"opérations","heading":"Opérations","markdown":""},{"anchor":"transcrire-un-audio","heading":"Transcrire un audio","markdown":"<Endpoint id=\"audio.transcribe\" example={{ body: `{ \"audio_url\": \"https://example.com/episode-12.mp3\", \"format\": \"text\", \"language\": \"fr\" }` }}>\n  `format` : `text` (par défaut), `srt` ou `vtt`. `language` est facultatif (code ISO 639-1 ; sans lui, la langue est détectée). Avec `diarize: true` (ou `speakers`, le nombre connu de locuteurs), chaque passage indique qui parle (`Speaker 1`, `Speaker 2`…), moyennant un supplément à la minute.\n</Endpoint>"},{"anchor":"générer-de-la-parole-à-partir-dun-texte","heading":"Générer de la parole à partir d'un texte","markdown":"<Endpoint id=\"audio.speak\" example={{ body: `{ \"text\": \"Bonjour ! Votre commande est en cours de livraison.\", \"voice\": \"pt-BR-dora\", \"format\": \"mp3\", \"speed\": 1 }` }}>\n  `text` accepte jusqu'à <Limit name=\"audioTtsCharsMax\" /> caractères (au-delà, la requête est refusée avant facturation). `voice` est un id du [catalogue](#vozes-do-catalogo) ou une voix clonée du compte (`vc_…`, [MCP uniquement](#vozes-clonadas)) ; sans `voice`, la voix par défaut de `language` s'applique (`pt-BR`, `en-US`, `en-GB`, `es-ES`, `fr-FR` ou `it-IT`). `format` : `mp3` (par défaut) ou `wav` ; `speed` : 0,5 à 2.\n</Endpoint>"},{"anchor":"vozes-do-catalogo","heading":"Lister les voix","markdown":"<Endpoint id=\"audio.voices\" example={{ query: { language: 'pt-BR' } }}>\n  Catalogue fermé de voix pour `speak` et `revoice`. `language` filtre par langue. Gratuit.\n</Endpoint>"},{"anchor":"générer-un-effet-sonore-ou-une-musique","heading":"Générer un effet sonore ou une musique","markdown":"<Endpoint id=\"audio.generate\" example={{ body: `{ \"kind\": \"effect\", \"prompt\": \"porte en bois qui grince lentement\", \"duration_seconds\": 4, \"format\": \"mp3\" }` }}>\n  `kind` : `effect` (1 à <Limit name=\"audioEffectMax\" />, 5 s par défaut) ou `music` (instrumentale, 1 à <Limit name=\"audioMusicMax\" />, 30 s par défaut). L'audio sort avec exactement `duration_seconds`. Il n'y a ni parole ni chant, et les descriptions qui imitent des artistes, des voix ou des œuvres existantes sont refusées avant facturation (`audio.input_blocked`). `seed` répète un son : la même requête avec la `seed` signalée par un job donne le même audio. La génération prend quelques minutes.\n</Endpoint>"},{"anchor":"analyser-un-audio","heading":"Analyser un audio","markdown":"<Endpoint id=\"audio.analyze\" example={{ body: `{ \"audio_url\": \"https://example.com/episode-12.mp3\" }` }}>\n  Sans IA de transcription : renvoie dans `analysis` la durée, le format et le codec, le débit, la fréquence d'échantillonnage, les canaux, le loudness intégré (LUFS), le pic réel, la plage de loudness, les passages de silence (`start_ms`/`end_ms`) et la langue parlée. Prix fixe par appel.\n</Endpoint>"},{"anchor":"convertir-ou-normaliser-un-audio","heading":"Convertir ou normaliser un audio","markdown":"<Endpoint id=\"audio.convert\" example={{ body: `{ \"audio_url\": \"https://example.com/episode-12.wav\", \"format\": \"mp3\", \"bitrate_kbps\": 128, \"normalize\": true }` }}>\n  `format` (obligatoire) : `mp3`, `wav`, `ogg`, `m4a` ou `flac`. `bitrate_kbps` (32 à 320) ne s'applique qu'aux formats avec perte. `normalize: true` ramène le loudness à -14 LUFS (standard du streaming, -1,5 dBTP). Les balises de métadonnées sont supprimées.\n</Endpoint>"},{"anchor":"résumer-un-audio","heading":"Résumer un audio","markdown":"<Endpoint id=\"audio.summarize\" example={{ body: `{ \"job_id\": \"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64\", \"output_language\": \"fr\" }` }}>\n  Envoyez le `job_id` d'une transcription prête (l'audio n'est pas retraité) ou un nouvel audio (transcrit d'abord et facturé avec). Le résultat arrive dans `summary` : le résumé et les sujets dans l'ordre (`title`, `start_ms`, `points`), plus un fichier Markdown à télécharger. Le texte de la transcription (jamais l'audio) est envoyé au modèle de langage de la plateforme.\n</Endpoint>"},{"anchor":"traduire-un-audio","heading":"Traduire un audio","markdown":"<Endpoint id=\"audio.translate\" example={{ body: `{ \"job_id\": \"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64\", \"target_language\": \"en\", \"format\": \"srt\" }` }}>\n  `target_language` est obligatoire. La traduction sort en texte, SRT ou VTT avec les minutages d'origine (et les locuteurs, si la transcription utilisait `diarize`) ; `source_language` indique la langue d'origine. Aucun audio doublé n'est produit : pour cela, utilisez [revoice](#revoice).\n</Endpoint>"},{"anchor":"revoice","heading":"Reprononcer l'enregistrement avec une autre voix","markdown":"<Endpoint id=\"audio.revoice\" example={{ body: `{ \"audio_url\": \"https://example.com/annonce.mp3\", \"voice_id\": \"pt-BR-alex\", \"format\": \"mp3\" }` }}>\n  En un seul job : transcrit l'enregistrement, traduit le texte quand la voix parle une autre langue et synthétise le résultat. Indiquez `voice_id` (du catalogue) ou `language` (utilise sa voix par défaut). Le nouvel audio ne garde ni le minutage ni le rythme de l'original (pas de synchronisation labiale), et seules les voix du catalogue ou clonées de votre propre compte sont acceptées : on n'imite pas une personne réelle. Le résultat contient le texte prononcé (`transcript` et `text_download_url`) et ce que chaque étape a coûté dans `revoice.stages`.\n</Endpoint>"},{"anchor":"lister-les-jobs","heading":"Lister les jobs","markdown":"<Endpoint id=\"audio.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  Les jobs les plus récents d'abord, avec `limit` (20 par défaut, jusqu'à <Limit name=\"pageSizeMax\" />) et `offset`. La liste ne contient que le statut ; le résultat et le lien se trouvent dans `GET /jobs/{id}`. Les jobs et fichiers sont supprimés après la rétention (<Limit name=\"audioRetention\" />).\n</Endpoint>"},{"anchor":"resultado","heading":"Statut, résultat et téléchargement","markdown":"<Endpoint id=\"audio.get\">\n  Il n'y a pas de route de téléchargement séparée : le résultat et le fichier viennent de cet endpoint. Avec `status: completed`, la réponse contient le contenu selon le type de job — `transcript` (aussi en ligne jusqu'à 32 Ko ; `truncated: true` signale la coupe) pour la transcription et la traduction, `summary`, `analysis`, `conversion`, `generation` ou `revoice` — et `download_url` avec `download_expires_at`. Le lien est signé et expire : interrogez de nouveau pour en obtenir un neuf. En `failed`, `error_code` explique la cause et la facturation est remboursée. La lecture est mesurée dans le [quota quotidien](/fr/cobranca-e-limites#franquia-diaria).\n\n  `status` : `pending`, `processing`, `completed` ou `failed`.\n</Endpoint>"},{"anchor":"vozes-clonadas","heading":"Voix clonées et consentement (MCP uniquement)","markdown":"Tant qu'il n'existe pas de route REST, le clonage de voix est **réservé aux tools MCP** `voice_create`, `voice_list` et `voice_delete` (scope `tools:audio:speak`). Une voix clonée ne sert qu'au compte qui l'a créée, dans `audio_speak` (`voice: \"vc_…\"`, tarif de voix clonée) et dans `audio_revoice` (`voice_id`).\n\n* **`voice_create`** clone la voix **du titulaire du compte** à partir d'un échantillon de <Limit name=\"audioVoiceSample\" />. Le consentement est obligatoire : appelez d'abord avec `dry_run` pour lire `consent_terms`, `consent_version` et `consent_phrase` ; enregistrez l'échantillon **seul, en lisant la phrase à voix haute** ; puis appelez avec `consent.accepted: true` et la `consent_version`. Les échantillons sans la phrase, avec plus d'une voix ou hors de la durée sont refusés et la facturation est remboursée (`audio.voice_sample_rejected`). Sans l'acceptation, la réponse est `audio.consent_required`. C'est asynchrone : suivez avec `voice_list` jusqu'à ce que la voix soit `completed`.\n* **`voice_list`** liste les voix du compte (`voice_id`, nom, langue, `status`, expiration). Gratuit.\n* **`voice_delete`** supprime vraiment la voix : l'échantillon chiffré et le registre de consentement sont détruits et la voix cesse de fonctionner immédiatement (les jobs en file avec elle échouent et sont remboursés). Gratuit."},{"anchor":"uso-responsavel","heading":"Usage responsable","markdown":"* **Seulement votre voix, avec votre consentement.** Cloner la voix d'une autre personne, même avec son enregistrement, n'est pas autorisé ; la vérification de la phrase prononcée et d'un seul locuteur sert à l'empêcher.\n* **Aucune imitation.** Ni la parole, ni le `revoice`, ni la génération de sons n'acceptent d'imiter des artistes, des voix ou des œuvres existantes.\n* **Limites :** jusqu'à <Limit name=\"audioVoicesMax\" /> voix clonées par compte, <Limit name=\"audioVoiceCreatesPerDay\" /> créations par jour et <Limit name=\"audioVoiceUsesPerHour\" /> usages par heure (`audio.voice_limit`, `audio.voice_rate_limited`).\n* **Données conservées :** l'échantillon reste chiffré jusqu'à <Limit name=\"audioVoiceSampleRetention\" /> ou jusqu'à `voice_delete` ; chaque création, refus, usage et suppression est audité, sans contenu vocal. L'audio d'entrée et de sortie des autres jobs expire en <Limit name=\"audioRetention\" />.\n* **Prévenez vos auditeurs :** si vous publiez un audio synthétique, indiquez que la voix est générée, surtout s'il s'agit d'une voix clonée."},{"anchor":"scopes","heading":"Scopes","markdown":"| Scope                    | Opérations                                                                                 |\n| ------------------------ | ------------------------------------------------------------------------------------------ |\n| `tools:audio:transcribe` | `transcribe`, `summarize`, `translate`                                                     |\n| `tools:audio:speak`      | `speak`, `generate`, `revoice` (en MCP, aussi `voice_create` et `voice_delete`)            |\n| `tools:audio:process`    | `analyze`, `convert`                                                                       |\n| `tools:audio:read`       | `voices`, `jobs`, `jobs/{id}` (les trois scopes ci-dessus lisent aussi leurs propres jobs) |\n\n`tools:manage` couvre tout. La liste complète est dans [Authentification](/fr/autenticacao)."},{"anchor":"erros","heading":"Erreurs","markdown":"`audio.*` dans la réponse ou, pour les jobs qui échouent après acceptation, dans le `error_code` du job.\n\n<ErrorTable tool=\"audio\" />"}]},{"slug":"ferramentas/base-de-conhecimento","url":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento","title":"Base de connaissances","description":"Recherche hybride (sémantique + mots-clés) dans vos documents, avec rerank et feedback de pertinence, pour le RAG et les assistants IA.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"base-de-conhecimento\" />"},{"anchor":"vue-densemble","heading":"Vue d'ensemble","markdown":"Une **base** (collection) stocke des documents texte ou Markdown. Chaque document est découpé en **sections** (d'après les titres) et en **fragments** (morceaux d'environ 96 tokens), indexés de deux façons : par le sens (embeddings) et par mots-clés. La recherche combine les deux et renvoie les fragments les plus pertinents, prêts à être transmis à un modèle de langage (RAG) ou affichés à l'utilisateur.\n\n<Steps>\n  <Step>\n    **Créez une base** avec la langue de vos documents (cela améliore la recherche par mots-clés).\n  </Step>\n\n  <Step>\n    **Alimentez la base.** Envoyez du texte ou du Markdown, importez des fichiers (PDF, Office, images) ou connectez une source qui se synchronise toute seule (site, sitemap, dépôt GitHub, dossier Google Drive). L'indexation est asynchrone : suivez le `status` jusqu'à `ready`. Renvoyer un document avec le même `external_id` remplace la version précédente sans rendre la base indisponible.\n  </Step>\n\n  <Step>\n    **Recherchez** avec la question de l'utilisateur et utilisez les fragments renvoyés comme contexte de la réponse.\n  </Step>\n\n  <Step>\n    **Répondez avec des citations** (facultatif) : `answer` renvoie un texte dont chaque affirmation pointe vers le fragment d'origine, ou indique qu'il ne sait pas. Pour votre site, intégrez le [widget](#widget).\n  </Step>\n\n  <Step>\n    **Enregistrez du feedback** (facultatif) : les fragments confirmés remontent dans les recherches similaires suivantes et les fragments rejetés disparaissent.\n  </Step>\n</Steps>\n\n<EndpointList tool=\"base-de-conhecimento\" />"},{"anchor":"custos","heading":"Coûts","markdown":"<PricingTable tool=\"base-de-conhecimento\" group=\"per-use\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"free-quota\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"blocks\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"volume\" />\n\n<PricingTable tool=\"base-de-conhecimento\" group=\"storage\" />\n\n<Callout title=\"Fonctionnement des blocs\">\n  Les recherches et le rerank ont un quota gratuit par jour. Au-delà, l'appel qui ouvre un bloc débite d'un coup le bloc entier (10 appels pour le premier, puis 20, 50 et 100 selon l'usage payant de la journée), et les appels suivants du même bloc ne sont pas refacturés. Les compteurs sont remis à zéro à minuit UTC, et les appels non utilisés d'un bloc expirent sans remboursement. L'outil MCP `rag_get_usage` et le `dry_run` des recherches indiquent ce que coûtera le prochain appel. La consultation des bases, des documents et du feedback est gratuite et n'entre pas dans le [quota gratuit quotidien de lectures](/fr/cobranca-e-limites#franquia-diaria).\n</Callout>\n\nL'ingestion est facturée **à la fin du traitement**, selon le nombre réel de fragments ; l'`estimate` de la réponse est une approximation. Un contenu identique à celui déjà indexé n'est pas refacturé. Utilisez `X-Dry-Run: true` lors de l'envoi d'un document pour voir l'estimation sans rien envoyer."},{"anchor":"bases","heading":"Bases","markdown":""},{"anchor":"créer-une-base","heading":"Créer une base","markdown":"<Endpoint\n  id=\"rag.collections.create\"\n  example={{ body: `{\n\"name\": \"Centre d'aide\",\n\"description\": \"Articles d'assistance publics\",\n\"language\": \"french\",\n\"default_mode\": \"fast\"\n}` }}\n>\n  * `name` (obligatoire, jusqu'à 120 caractères) et `description` (jusqu'à 1 000).\n  * `language` : `simple` (par défaut), `portuguese`, `english`, `spanish` ou `french`. Définit le dictionnaire de la recherche par mots-clés.\n  * `default_mode` : `fast` (par défaut) ou `accurate` (rerank supplémentaire, plus précis et un peu plus cher).\n  * Réglages avancés : `chunk_size_tokens` (16–480, 96 par défaut), `chunk_overlap_tokens` (16 par défaut), `section_max_tokens` (768 par défaut), `max_per_document` (0–20, 2 par défaut), `dense_weight` et `lexical_weight` (0–10, 1 par défaut ; ils ne peuvent pas valoir 0 tous les deux) et `feedback_enabled` (`true` par défaut).\n</Endpoint>"},{"anchor":"lister-les-bases","heading":"Lister les bases","markdown":"<Endpoint id=\"rag.collections.list\">\n  Renvoie aussi l'utilisation du compte par rapport aux limites (`usage`).\n</Endpoint>"},{"anchor":"consulter-une-base","heading":"Consulter une base","markdown":"<Endpoint id=\"rag.collections.get\" />"},{"anchor":"filtres-disponibles","heading":"Filtres disponibles","markdown":"<Endpoint id=\"rag.collections.facets\">\n  Liste les clés de métadonnées des documents de la base et les valeurs les plus courantes de chacune, pour savoir ce que vous pouvez utiliser dans `filters` lors des recherches. `documents` est le nombre de documents prêts avec métadonnées analysés et `keys` le total de clés filtrables. Si `sampled` vaut `true`, la base compte plus de 5000 documents et le décompte ne porte que sur les 5000 plus récents. Dans `facets`, chaque élément contient la `key`, les `documents` qui la possèdent, `distinct` (total de valeurs distinctes) et `values[]` (les valeurs les plus courantes, chacune avec `value` et `documents`). `array: true` indique que la clé contient une liste ; filtrez avec `{\"clé\": [\"valeur\"]}`.\n</Endpoint>"},{"anchor":"modifier-une-base","heading":"Modifier une base","markdown":"<Endpoint id=\"rag.collections.update\" example={{ body: `{ \"default_mode\": \"accurate\" }` }}>\n  N'envoyez que les champs à modifier. Le modèle d'embeddings ne peut pas être changé après la création. Les changements de taille des fragments ne s'appliquent qu'aux documents envoyés ensuite ; pour les appliquer à toute la base, utilisez la réindexation.\n</Endpoint>"},{"anchor":"réindexer-une-base","heading":"Réindexer une base","markdown":"<Endpoint id=\"rag.collections.reindex\">\n  Retraite tous les documents avec la configuration actuelle de la base. Chaque fragment est facturé comme lors d'une nouvelle ingestion.\n</Endpoint>"},{"anchor":"tableau-de-santé-de-la-base","heading":"Tableau de santé de la base","markdown":"<Endpoint id=\"rag.collections.overview\" example={{ query: { rows: 'categoria', days: '30' } }}>\n  Résumé pour garder la base en ordre, sans frais : nombre de documents par `status`, combien sont périmés (`stale`), n'ont jamais été consultés ou n'ont pas de métadonnées ; une matrice de documents par métadonnée (`rows` et `cols` sont des clés de métadonnées, jusqu'à 20 valeurs par axe) ; l'activité des `days` derniers jours (1 à 180, 30 par défaut) ; et `attention`, avec jusqu'à 5 exemples de documents en échec, périmés, en double ou contradictoires, et avec un feedback négatif. La réponse est mise en cache pendant 60 s.\n</Endpoint>"},{"anchor":"index-des-documents","heading":"Index des documents","markdown":"<Endpoint id=\"rag.collections.index\" example={{ query: { fields: 'categoria', order: 'title' } }}>\n  Liste compacte des documents `ready`, sans contenu, pour que l'agent choisisse ce qu'il doit lire. `filters` (JSON de métadonnées), `fields` (jusqu'à 20 clés de métadonnées à copier), `order` (`updated_at_desc`, par défaut, ou `title`) et `max_bytes` (4096 par défaut, 32768 au maximum). `omitted` compte les documents coupés par la limite.\n</Endpoint>"},{"anchor":"trouver-les-doublons-et-les-contradictions","heading":"Trouver les doublons et les contradictions","markdown":"<Endpoint id=\"rag.collections.lint\" example={{ body: `{ \"limit\": 20 }` }}>\n  Compare les 500 documents les plus récents (éventuellement filtrés par `filters`) avec les embeddings déjà existants ; n'appelle aucun modèle et n'écrit rien. `duplicate` apparaît avec une similarité à partir de 0,75 ; `possible_contradiction`, entre 0,4 et 0,75 lorsque les deux documents ont la même métadonnée `tipo`. Chaque résultat indique quel document conserver (`keep`, le plus récent) et quoi faire (`action`). `limit` va de 1 à 100 (20 par défaut) et `include_content: true` renvoie le début de chaque document.\n</Endpoint>"},{"anchor":"changer-le-modèle-dembeddings","heading":"Changer le modèle d'embeddings","markdown":"<Endpoint id=\"rag.collections.model\" example={{ body: `{ \"embedding_model\": \"BAAI/bge-m3\" }` }}>\n  Bascule entre `intfloat/multilingual-e5-small` (par défaut) et `BAAI/bge-m3` (haute précision) en réindexant la base **en arrière-plan** ; vous n'avez pas besoin d'appeler la réindexation. Le changement coûte 50 % du prix normal d'ingestion, et revenir au modèle précédent est gratuit pendant qu'il s'exécute et pendant 24 h ensuite, une seule fois. Utilisez `X-Dry-Run: true` pour voir l'estimation. Pendant le changement, la recherche continue de fonctionner (la requête est encodée avec les deux modèles) et les nouveaux documents sont refusés avec `rag.model_switch_in_progress`. Suivez-le via le champ `model_switch` de la base (`running`, `paused` ou `done`) ; si le solde s'épuise, il se met en pause avec `paused_reason: insufficient_credits` et reprend quand vous rechargez.\n</Endpoint>"},{"anchor":"marquer-des-documents-comme-périmés","heading":"Marquer des documents comme périmés","markdown":"<Endpoint id=\"rag.collections.changes\" example={{ body: `{ \"paths\": [\"docs/api/\", \"src/auth.ts\"], \"commit\": \"a1b2c3d\" }` }}>\n  Pour les bases liées à du code ou à des fichiers : indiquez les `paths` modifiés (fichier exact ou préfixe `dir/`, jusqu'à 1000) et les documents qui ont déclaré ces `paths` lors de leur envoi passent à `stale: true`. Rien n'est réindexé ni facturé ; renvoyer le document efface la marque. Utilisez `exclude_stale` dans la recherche pour les ignorer. `commit` (jusqu'à 64 caractères) évite de marquer ce qui a déjà été vérifié à ce commit.\n</Endpoint>"},{"anchor":"supprimer-une-base","heading":"Supprimer une base","markdown":"<Endpoint id=\"rag.collections.delete\">\n  La suppression est asynchrone et efface documents, fragments et feedback.\n</Endpoint>"},{"anchor":"documents","heading":"Documents","markdown":""},{"anchor":"ajouter-ou-remplacer-un-document","heading":"Ajouter ou remplacer un document","markdown":"<Endpoint\n  id=\"rag.documents.add\"\n  example={{ body: `{\n\"external_id\": \"article-42\",\n\"title\": \"Changer son mot de passe\",\n\"content\": \"# Compte\\\\n\\\\n## Changer son mot de passe\\\\n\\\\nAllez dans Paramètres > Sécurité…\",\n\"source_uri\": \"https://aide.example.com/articles/42\",\n\"metadata\": { \"categorie\": \"compte\" }\n}` }}\n>\n  * `content` (obligatoire) : texte brut ou Markdown en UTF-8, jusqu'à <Limit name=\"ragDocumentMax\" />. Pour les PDF, Office et images, utilisez [Importer un fichier](#rag-documents-file).\n  * `external_id` (facultatif, jusqu'à 200 octets) : votre propre identifiant. Renvoyer la même valeur **remplace** le document ; l'ancienne version continue de répondre aux recherches (`processing: true`) jusqu'à ce que la nouvelle soit prête.\n  * `paths` (jusqu'à 50) et `commit` : les fichiers dont provient le document, pour [le marquer comme périmé](#rag-collections-changes) lorsqu'ils changent.\n  * Les secrets reconnus dans le texte (clés d'API, tokens) sont remplacés par `[REDACTED:type]` avant l'indexation ; `redactions` en indique le nombre.\n  * `metadata` : objet de 50 clés au maximum (4 KB, 3 niveaux au maximum), utilisé par les filtres de recherche. Il n'est pas chiffré ; n'y mettez pas de données sensibles.\n  * Suivez le traitement avec [Consulter un document](#rag-documents-get) jusqu'à `status: ready` (ou `failed`, avec un `error_code`). Avant d'accepter le document, l'API vérifie que le solde couvre l'estimation.\n</Endpoint>"},{"anchor":"importer-un-fichier-pdf-office-image","heading":"Importer un fichier (PDF, Office, image)","markdown":"<Endpoint id=\"rag.documents.file\">\n  Envoi `multipart/form-data` avec `file` (obligatoire, jusqu'à 50 MB) et, facultativement, `title`, `external_id`, `source_uri`, `metadata` (objet JSON sous forme de texte) et `ocr` (`true` par défaut ; désactivez-le pour les PDF qui contiennent déjà du texte). Formats : `.pdf`, `.docx`, `.pptx`, `.xlsx`, `.html`, `.md`, `.csv`, `.asciidoc` et images (`.png`, `.jpg`, `.tiff`, `.bmp`, `.webp`). Le fichier est converti en Markdown (tableaux préservés, images uniquement via le texte de l'OCR) puis indexé comme n'importe quel document, avec la même limite de <Limit name=\"ragDocumentMax\" /> pour le texte obtenu. La réponse est `202` avec l'`id` de l'importation ; consultez-la jusqu'à `completed` (elle fournit le `document_id`) ou `failed`. Sans conversion disponible, la réponse est `503` (`rag.service_unavailable`).\n</Endpoint>"},{"anchor":"consulter-limportation-dun-fichier","heading":"Consulter l'importation d'un fichier","markdown":"<Endpoint id=\"rag.imports.get\" />"},{"anchor":"lister-les-documents","heading":"Lister les documents","markdown":"<Endpoint id=\"rag.documents.list\" example={{ query: { status: 'ready', limit: '20' } }}>\n  Filtre facultatif par `status` (`pending`, `processing`, `ready`, `failed`, `deleting`). Pagination par curseur : passez le `next_cursor` reçu dans `after`. `limit` vaut 20 par défaut, 100 au maximum.\n</Endpoint>"},{"anchor":"consulter-un-document","heading":"Consulter un document","markdown":"<Endpoint id=\"rag.documents.get\" />"},{"anchor":"modifier-le-titre-ou-les-métadonnées","heading":"Modifier le titre ou les métadonnées","markdown":"<Endpoint id=\"rag.documents.update\" example={{ body: `{ \"metadata\": { \"categorie\": \"securite\" } }` }}>\n  Ne retraite pas le contenu et n'est pas facturé.\n</Endpoint>"},{"anchor":"voir-les-sections-indexées","heading":"Voir les sections indexées","markdown":"<Endpoint id=\"rag.documents.sections\">\n  Montre comment le document a été découpé ; utile pour ajuster les titres et la taille des fragments.\n</Endpoint>"},{"anchor":"relancer-un-document-en-échec","heading":"Relancer un document en échec","markdown":"<Endpoint id=\"rag.documents.retry\">\n  Uniquement pour les documents au `status: failed` ; les autres répondent `rag.document_not_failed`.\n</Endpoint>"},{"anchor":"supprimer-un-document","heading":"Supprimer un document","markdown":"<Endpoint id=\"rag.documents.delete\" />"},{"anchor":"sources-synchronisées","heading":"Sources synchronisées","markdown":"Une **source** importe des pages externes et les garde à jour : à chaque synchronisation, les pages nouvelles ou modifiées sont réindexées, celles qui n'ont pas changé (même contenu) ne sont pas refacturées, et celles qui ont disparu de l'origine sont retirées de la base. Chaque page devient un document ordinaire (`external_id` `src:<id de la source>:…`, avec `source_id` et `source_url` dans les métadonnées), facturé par fragment comme lors d'une ingestion manuelle ; s'il manque du solde ou du quota, la synchronisation échoue et en indique la raison.\n\n| `kind`    | `start_url`                             | Ce qui est importé                                                                                                                                                                |\n| --------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `site`    | page d'accueil                          | Parcourt les liens du même hôte jusqu'à `max_depth`, en respectant le `robots.txt` ; le HTML devient du Markdown et les fichiers PDF/Office liés sont convertis.                  |\n| `sitemap` | URL du sitemap (ou de l'index)          | Importe les pages listées, jusqu'à `max_pages`. `max_depth` ne s'applique pas.                                                                                                    |\n| `github`  | `https://github.com/proprietaire/depot` | Dépôt **public** ; `config.ref` (`HEAD` par défaut) et `config.paths` (préfixes) filtrent. Lit `.md`, `.mdx`, `.txt`, `.rst` et convertit les formats de fichiers pris en charge. |\n| `gdrive`  | URL d'un dossier `/folders/<id>`        | Dossier partagé en « toute personne disposant du lien » ; les Docs deviennent du Markdown. N'existe que si le serveur a l'intégration activée.                                    |"},{"anchor":"lister-les-sources-synchronisées","heading":"Lister les sources synchronisées","markdown":"<Endpoint id=\"rag.sources.list\" />"},{"anchor":"créer-une-source-synchronisée","heading":"Créer une source synchronisée","markdown":"<Endpoint\n  id=\"rag.sources.create\"\n  example={{ body: `{\n\"kind\": \"site\",\n\"name\": \"Centre d'aide\",\n\"start_url\": \"https://aide.example.com\",\n\"max_pages\": 100,\n\"max_depth\": 3,\n\"frequency_hours\": 24\n}` }}\n>\n  * `name` (jusqu'à 200) et `start_url` (http ou https) sont obligatoires ; `kind` vaut `site` par défaut.\n  * `max_pages` : 1 à 1000 (100 par défaut). `max_depth` : 1 à 10 (3 par défaut).\n  * `frequency_hours` : `0` (manuel uniquement), `6`, `24` (par défaut) ou `168`.\n  * La première synchronisation démarre aussitôt. Pages et fichiers de 20 MB maximum chacun ; un fichier qui ne se convertit pas est ignoré sans faire échouer la synchronisation.\n</Endpoint>"},{"anchor":"modifier-une-source","heading":"Modifier une source","markdown":"<Endpoint id=\"rag.sources.update\" example={{ body: `{ \"frequency_hours\": 168, \"enabled\": true }` }}>\n  Tous les champs sont facultatifs. Avec `enabled: false` ou `frequency_hours: 0`, `next_sync_at` devient nul.\n</Endpoint>"},{"anchor":"synchroniser-maintenant","heading":"Synchroniser maintenant","markdown":"<Endpoint id=\"rag.sources.sync\">\n  Répond `409` si la source est déjà en cours de synchronisation. `pages_total`, `pages_new`, `pages_changed` et `pages_removed` résument l'exécution.\n</Endpoint>"},{"anchor":"historique-des-synchronisations","heading":"Historique des synchronisations","markdown":"<Endpoint id=\"rag.sources.syncs\" />"},{"anchor":"supprimer-une-source","heading":"Supprimer une source","markdown":"<Endpoint id=\"rag.sources.delete\" />"},{"anchor":"recherche","heading":"Recherche","markdown":""},{"anchor":"modes-de-recherche-et-reranker","heading":"Modes de recherche et reranker","markdown":"* **`fast`** (par défaut) : combine la recherche par le sens et celle par mots-clés. Le `score` est une fusion de rangs (petites valeurs, proches de 0,03), utile pour trier, pas pour comparer à un seuil.\n* **`accurate`** : après la fusion, un **reranker** relit les 20 meilleurs candidats avec la question et renvoie un `score` calibré de 0 à 1 ; `min_score` n'a d'effet que sur lui. Le supplément de rerank est facturé. Si le rerank est indisponible, la recherche se replie sur `fast` sans le supplément (`mode_used`).\n* **Reranker** : `rerank_provider` de la base (et `provider` dans le rerank autonome et dans `decide`) choisit entre `bge` (par défaut, cross-encoder) et `laya`, le juge de pertinence de Respawn, qui demande si le fragment répond à la requête et renvoie la probabilité. Laya sépare généralement mieux les fragments qui ne font que ressembler à la question, mais il est plus lent et, s'il est hors service, `accurate` se replie sur `fast` et le rerank autonome répond `503` (`rag.service_unavailable`). Le prix est le même que pour `bge`.\n* Si le service d'embeddings échoue, la recherche répond quand même uniquement par mots-clés (`mode_used: lexical`), facturée comme un cache."},{"anchor":"rechercher-dans-une-base","heading":"Rechercher dans une base","markdown":"<Endpoint\n  id=\"rag.query\"\n  example={{ body: `{\n\"query\": \"comment changer mon mot de passe ?\",\n\"top_k\": 5,\n\"filters\": { \"categorie\": \"compte\" }\n}` }}\n>\n  * `query` (obligatoire, jusqu'à <Limit name=\"ragQueryCharsMax\" /> caractères).\n  * `top_k` : nombre de fragments à renvoyer (5 par défaut, <Limit name=\"ragTopKMax\" /> au maximum).\n  * `mode` : `fast` ou `accurate` ; à défaut, le mode par défaut de la base s'applique. Si le rerank du mode `accurate` est indisponible, la recherche se replie sur `fast` sans facturer le supplément ; `mode_used` indique le mode utilisé.\n  * `filters` : uniquement les fragments des documents dont les métadonnées contiennent ces valeurs.\n  * `min_score` (uniquement en `accurate`) et `max_per_document` (0–20) affinent le résultat.\n  * `updated_after` et `updated_before` (RFC 3339) filtrent selon la date de mise à jour du document, et `exclude_stale: true` ignore ceux marqués comme périmés. Chaque résultat contient `updated_at`, `stale`, `paths`, `commit`, `source_uri` et `also_in` (autres documents au texte identique, regroupés en un seul résultat).\n  * `highlights` repère, en positions de caractères Unicode, les termes trouvés dans `content`. Les requêtes répétées dans un délai de <Limit name=\"ragQueryCacheTtl\" /> sont servies depuis le cache (`cached: true`) et coûtent moins cher.\n\n  <Callout type=\"warn\" title=\"Contenu utilisateur\">\n    Les fragments renvoyés proviennent de vos documents. Lorsque vous les transmettez à un modèle de langage, traitez-les comme des données, pas comme des instructions.\n  </Callout>\n</Endpoint>"},{"anchor":"feedback-de-pertinence","heading":"Feedback de pertinence","markdown":""},{"anchor":"enregistrer-un-feedback-de-pertinence","heading":"Enregistrer un feedback de pertinence","markdown":"<Endpoint\n  id=\"rag.feedback.add\"\n  example={{ body: `{\n\"query\": \"comment changer mon mot de passe ?\",\n\"section_id\": \"b2c4d6e8-0a1b-4c3d-8e5f-6a7b8c9d0e1f\",\n\"label\": \"positive\",\n\"end_user_ref\": \"utilisateur-123\"\n}` }}\n>\n  * `label` : `positive` (le fragment répond à la question) ou `negative`.\n  * `end_user_ref` (facultatif) : identifie l'auteur de l'évaluation, quand le vote vient de votre utilisateur final. Seul un hash est conservé.\n  * Jusqu'à <Limit name=\"ragFeedbackPerMinute\" /> votes par minute. Les questions similaires sont regroupées, et le vote s'applique à tout le groupe.\n</Endpoint>"},{"anchor":"lister-le-feedback","heading":"Lister le feedback","markdown":"<Endpoint id=\"rag.feedback.list\" />"},{"anchor":"supprimer-un-groupe-de-feedback","heading":"Supprimer un groupe de feedback","markdown":"<Endpoint id=\"rag.feedback.delete\" />"},{"anchor":"rerank-autonome","heading":"Rerank autonome","markdown":""},{"anchor":"réordonner-des-textes-rerank","heading":"Réordonner des textes (rerank)","markdown":"<Endpoint\n  id=\"rag.rerank\"\n  example={{ body: `{\n\"query\": \"délai de remboursement\",\n\"documents\": [\"Livraison sous 5 jours ouvrés.\", \"Livraison gratuite dès 40 € d'achat.\", \"Remboursement sous 7 jours après le retour.\"],\n\"top_k\": 2\n}` }}\n>\n  Trie **votre propre** liste de textes selon leur pertinence pour la question, sans aucune base. Jusqu'à <Limit name=\"ragRerankDocumentsMax\" /> textes par appel (chacun est tronqué à 2 000 caractères). `index` renvoie à la position dans le tableau envoyé.\n</Endpoint>"},{"anchor":"réponse-avec-citations","heading":"Réponse avec citations","markdown":""},{"anchor":"répondre-avec-des-citations","heading":"Répondre avec des citations","markdown":"<Endpoint\n  id=\"rag.answer\"\n  example={{ body: `{\n\"query\": \"comment changer mon mot de passe ?\",\n\"top_k\": 5,\n\"min_score\": 0.35,\n\"generate\": false\n}` }}\n>\n  Effectue une recherche `accurate` et répond **uniquement** avec des fragments dont le `score` est égal ou supérieur à `min_score` (0 à 1, 0,35 par défaut, un point de départ à calibrer sur votre base).\n\n  * Avec une réponse, `answered: true` et chaque `[n]` du texte renvoie à un élément de `citations[]` (`section_id`, `document_id`, `title`, `heading_path`, `source_uri`, `score`, `excerpt`). Il n'y a jamais de réponse sans citation.\n  * Sans fragment au-dessus du seuil, `answered: false`, `reason: no_passage_above_threshold`, aucune citation, et le webhook `rag.question.unanswered` est déclenché. Si le rerank n'a pas pu vérifier la pertinence, `reason: relevance_unverified` (rien n'est affirmé et le webhook n'est pas déclenché).\n  * `generate: false` (par défaut) compose la réponse en citant les 3 meilleurs fragments. Avec `generate: true`, le fournisseur d'IA par défaut de la plateforme rédige le texte à partir des fragments ; sans marqueur de citation valide ou sans fournisseur, on revient au mode extractif (`generated: false`). Avec un texte généré, les fragments de la base sont transmis au fournisseur d'IA de la plateforme.\n  * Avec `Accept: text/event-stream`, la réponse arrive en SSE : `citations`, plusieurs `delta` (morceaux du texte) et `done` (le résultat complet). Le texte est d'abord généré en entier, le SSE ne fait que le découper.\n  * Facturation d'une recherche `accurate` ; la génération n'est pas facturée à part. Utilisez le `query_id` dans le [feedback](#rag-feedback-add) pour alimenter le CTR de l'analytics.\n</Endpoint>"},{"anchor":"widget","heading":"Widget de chat intégrable","markdown":"Le widget est un bouton de chat en JavaScript pur (sans CDN, en shadow DOM, il n'écrit que du texte) qui répond avec des citations à partir de votre base. Il utilise une **clé publique** (`rpk_…`) qui peut seulement poser des questions sur une base : elle n'en lit aucune autre et n'écrit rien.\n\n```html\n<script src=\"https://api.respawntech.dev/widget/rag.js\" data-key=\"rpk_...\" async></script>\n```\n\nAttributs : `data-key` (obligatoire), `data-title`, `data-placeholder` et `data-dont-know` (texte lorsqu'il n'y a pas de réponse). Les questions posées via le widget sont facturées au propriétaire de la base, comme `answer`."},{"anchor":"créer-une-clé-publique-widget","heading":"Créer une clé publique (widget)","markdown":"<Endpoint\n  id=\"rag.publickeys.create\"\n  example={{ body: `{\n\"name\": \"Site institutionnel\",\n\"allowed_origins\": [\"https://www.example.com\", \"https://*.example.com\"],\n\"rate_per_minute\": 30,\n\"generate\": false\n}` }}\n>\n  * `allowed_origins` (obligatoire, 1 à 20) : `https://site.com` ou `https://*.site.com`, sans chemin. Le serveur refuse les requêtes dont l'`Origin` n'est pas dans la liste.\n  * `rate_per_minute` : 1 à 600 (30 par défaut), par clé. `generate` active le texte généré par IA dans les réponses de cette clé.\n  * Jusqu'à 20 clés par base. La `key` n'apparaît qu'**une seule fois** ; nous ne conservons que le hash.\n\n  <Callout type=\"warn\" title=\"Origin n'est pas une authentification\">\n    Seul le navigateur respecte l'`Origin` ; un client extérieur peut le falsifier. Ce qui limite l'abus est la limite par minute de la clé, et le coût retombe sur vous. Révoquez la clé si vous constatez un usage suspect.\n  </Callout>\n</Endpoint>"},{"anchor":"lister-les-clés-publiques","heading":"Lister les clés publiques","markdown":"<Endpoint id=\"rag.publickeys.list\" />"},{"anchor":"révoquer-une-clé-publique","heading":"Révoquer une clé publique","markdown":"<Endpoint id=\"rag.publickeys.revoke\" />"},{"anchor":"répondre-via-le-widget-route-publique","heading":"Répondre via le widget (route publique)","markdown":"<Endpoint id=\"rag.public.answer\" example={{ body: `{ \"query\": \"comment changer mon mot de passe ?\" }` }}>\n  Sans token d'API : envoyez la clé dans l'en-tête `X-Respawn-Public-Key` et le navigateur envoie l'`Origin`. La base est déterminée par la clé, et `top_k`, `min_score` et `generate` ne sont pas acceptés dans la requête. Répond comme `answer` (JSON ou SSE), avec CORS ouvert pour l'origine autorisée. Erreurs : `401` (clé absente, inconnue ou révoquée), `403` (origine non autorisée) et `429` (limite par minute).\n</Endpoint>"},{"anchor":"script-du-widget","heading":"Script du widget","markdown":"<Endpoint id=\"rag.widget\" />"},{"anchor":"analytics-des-questions","heading":"Analytics des questions","markdown":""},{"anchor":"analytics-des-questions-1","heading":"Analytics des questions","markdown":"<Endpoint id=\"rag.analytics\" example={{ query: { days: '30' } }}>\n  Montre ce que les utilisateurs demandent et ce que la base ne sait pas répondre, à partir des appels à `answer` et du widget. `days` va de 1 à 90 (30 par défaut).\n\n  * `answer_rate` : questions répondues ÷ total des questions.\n  * `feedback_ctr` : réponses ayant reçu un vote (via le `query_id`) ÷ réponses.\n  * `positive_rate` : votes positifs ÷ votes.\n  * `top_questions` et `unanswered_questions` : jusqu'à 20 questions regroupées chacune, pour découvrir quoi écrire dans la base.\n\n  Les questions sont chiffrées avec la clé de la base. Le widget n'a pas de vote propre : le CTR ne compte que les votes envoyés par l'API.\n</Endpoint>"},{"anchor":"routage","heading":"Routage","markdown":""},{"anchor":"décider-entre-des-options-routage","heading":"Décider entre des options (routage)","markdown":"<Endpoint\n  id=\"rag.decide\"\n  example={{ body: `{\n\"text\": \"je veux résilier mon abonnement\",\n\"options\": [\n  { \"label\": \"facturation\", \"description\": \"paiements, factures et résiliations\" },\n  { \"label\": \"support\", \"description\": \"problèmes techniques\" }\n],\n\"threshold\": 0.5,\n\"min_margin\": 0.1\n}` }}\n>\n  Classe un texte parmi 2 à 50 options sans utiliser de LLM, en évaluant chaque `label: description` avec le reranker (`provider` : `bge` ou `laya`). Sert à router des demandes. Si la meilleure option ne dépasse pas `threshold` (0,5 par défaut) ou n'a pas `min_margin` (0,1 par défaut) d'avance sur la deuxième, `decided` vaut `false` : traitez-le comme « je ne sais pas ». Le `ranking` complet est toujours renvoyé. Prix et quota du rerank autonome.\n</Endpoint>"},{"anchor":"prix-et-limites-du-compte","heading":"Prix et limites du compte","markdown":""},{"anchor":"consulter-les-prix","heading":"Consulter les prix","markdown":"<Endpoint id=\"rag.pricing\" />"},{"anchor":"consulter-les-limites","heading":"Consulter les limites","markdown":"<Endpoint id=\"rag.limits\" />\n\nValeurs en vigueur pour votre compte (bonus de l'offre inclus), utiles pour valider un envoi avant de le faire."},{"anchor":"événements-webhooks","heading":"Événements (webhooks)","markdown":"Enregistrez un endpoint dans le dashboard pour recevoir :\n\n* `rag.document.ready` et `rag.document.failed` : `collection_id`, `document_id`, `external_id` et `version` ; celui d'échec contient aussi `error_code`. Valables pour les documents envoyés, importés depuis un fichier et issus de sources.\n* `rag.question.unanswered` : `collection_id`, `question`, `query_id` et `source` (`api` ou `widget`), lorsqu'aucun fragment n'a dépassé `min_score`. Idéal pour alimenter une file « quoi écrire ».\n\nL'envoi se fait au mieux, comme pour les autres événements. Voir la [liste des événements](/fr/webhooks)."},{"anchor":"utiliser-via-mcp","heading":"Utiliser via MCP","markdown":"Les mêmes opérations sont disponibles comme outils MCP du groupe `rag` ([liste complète](/fr/agentes-de-ia)) : `rag_answer`, `rag_decide`, `rag_index`, `rag_lint`, `rag_mark_changed` et `rag_retry_document` sont les nouveaux. Les outils de lecture (`rag_index`, `rag_lint`) sont gratuits ; utilisez `rag_access=query` dans la connexion pour masquer ceux qui modifient la base."},{"anchor":"erreurs","heading":"Erreurs","markdown":"<ErrorTable tool=\"base-de-conhecimento\" />\n\nLes codes sans statut HTTP apparaissent dans le champ `error_code` d'un document au `status: failed`."},{"anchor":"limites","heading":"Limites","markdown":"| Limite                                         | Valeur                                       |\n| ---------------------------------------------- | -------------------------------------------- |\n| Taille d'un document                           | <Limit name=\"ragDocumentMax\" />              |\n| Bases par compte                               | <Limit name=\"ragCollectionsMax\" />           |\n| Fragments indexés par compte                   | <Limit name=\"ragChunksMax\" />                |\n| Documents en cours de traitement simultanément | <Limit name=\"ragPendingMax\" />               |\n| Longueur de la question                        | <Limit name=\"ragQueryCharsMax\" /> caractères |\n| Fragments par recherche (`top_k`)              | <Limit name=\"ragTopKMax\" />                  |\n| Textes par rerank autonome                     | <Limit name=\"ragRerankDocumentsMax\" />       |\n| Votes de feedback par minute                   | <Limit name=\"ragFeedbackPerMinute\" />        |\n| Fichier importé                                | 50 MB                                        |\n| Pages par source                               | 1 000 (100 par défaut)                       |\n| Clés publiques par base                        | 20                                           |\n\nLes routes de la base de connaissances ignorent l'en-tête `X-Idempotency-Key` : elles sont déjà idempotentes (un contenu répété n'est ni réindexé ni facturé). Une clé restreinte à certaines bases ne voit que ces bases et ne peut pas en créer de nouvelles (`forbidden`)."}]},{"slug":"ferramentas/ci-error-extract","url":"https://docs.respawntech.dev/fr/ferramentas/ci-error-extract","title":"Extracteur d'erreur de CI","description":"Extrayez d'un log de CI uniquement les lignes qui expliquent l'échec.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"ci-error-extract\" />"},{"anchor":"vue-densemble","heading":"Vue d'ensemble","markdown":"Envoyez le log d'un job de CI et recevez uniquement les lignes de l'erreur. Utile pour les agents et les bots qui doivent comprendre pourquoi un build a échoué sans charger des milliers de lignes dans leur contexte. Également disponible dans le [MCP](/fr/agentes-de-ia) sous le nom `ci_error_extract`.\n\n<EndpointList tool=\"ci-error-extract\" />"},{"anchor":"custos","heading":"Coûts","markdown":"Chaque extraction est une lecture mesurée : elle est décomptée du [quota gratuit quotidien](/fr/cobranca-e-limites#franquia-diaria) puis, au-delà, débite le montant indiqué sur l'endpoint. La facturation ne concerne que les clés d'API."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"extraire-lerreur-dun-log","heading":"Extraire l'erreur d'un log","markdown":"<Endpoint id=\"ci.extract\" example={{ body: `{ \"log\": \"npm ERR! code ELIFECYCLE\\\\nnpm ERR! errno 1\\\\nFAIL src/app.test.ts\", \"max_lines\": 40 }` }}>\n  `log` (obligatoire) est le texte du log. `max_lines` (optionnel, 40 par défaut, 200 au maximum) limite le nombre de lignes d'erreur renvoyées.\n</Endpoint>"},{"anchor":"erreurs","heading":"Erreurs","markdown":"<ErrorTable tool=\"ci-error-extract\" />"}]},{"slug":"ferramentas/memoria-de-aprendizados","url":"https://docs.respawntech.dev/fr/ferramentas/memoria-de-aprendizados","title":"Mémoire d'apprentissages","description":"Mémoire à long terme pour les agents IA : enregistrez ce qui a été appris dans chaque projet et retrouvez-le avant la prochaine tâche.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"memoria-de-aprendizados\" />"},{"anchor":"como-funciona","heading":"Fonctionnement","markdown":"Un apprentissage est un texte court avec un `kind`, lié à un `project` :\n\n| `kind`       | À conserver                                              |\n| ------------ | -------------------------------------------------------- |\n| `pitfall`    | Pièges et erreurs à ne pas répéter.                      |\n| `decision`   | Décisions prises et leur raison.                         |\n| `convention` | Conventions du projet.                                   |\n| `howto`      | Comment faire quelque chose (lancer, publier, déboguer). |\n| `other`      | Ce qui ne rentre pas dans les autres.                    |\n\nLes apprentissages vivent dans une [base de connaissances](/fr/ferramentas/base-de-conhecimento) appelée `agent-learnings`, **créée automatiquement** au premier enregistrement. `recall` fait une recherche sémantique pour la tâche et ne renvoie que les éléments du `project` demandé, avec le `score` de chacun.\n\nElle est aussi disponible dans le [MCP](/fr/agentes-de-ia) comme `learning_save`, `learning_recall` et `learning_forget` (toolset `learnings`).\n\n<EndpointList tool=\"memoria-de-aprendizados\" />"},{"anchor":"ingestao","heading":"Doublons et ingestion asynchrone","markdown":"* **Déduplication :** `save` compare par hash et par similarité sémantique. Si un apprentissage identique ou proche existe déjà, rien de nouveau n'est enregistré et la réponse contient `duplicate_of` avec l'id de l'existant.\n* **Ingestion asynchrone :** le texte est indexé en arrière-plan. Un `recall` juste après un `save` peut ne pas encore renvoyer l'élément ; réessayez dans quelques secondes."},{"anchor":"custos","heading":"Coûts","markdown":"La facturation est celle de la [base de connaissances](/fr/ferramentas/base-de-conhecimento) : `save` comme l'ingestion (par fragment), `recall` comme la recherche (franchise quotidienne + blocs) et le stockage par jour. Oublier est gratuit. La mémoire suit les mêmes règles d'activation que la base de connaissances (`rag.disabled` et les autres erreurs `rag.*`)."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"enregistrer-un-apprentissage","heading":"Enregistrer un apprentissage","markdown":"<Endpoint id=\"learnings.save\" example={{ body: `{ \"project\": \"respawn-tech-docs\", \"kind\": \"pitfall\", \"text\": \"check:backend lit ../respawn-tech-backend ; ne le lancez qu'une fois le backend fusionné.\", \"source\": \"conversation du 06/10\" }` }}>\n  `project`, `kind` et `text` sont obligatoires ; `source` est facultatif et indique l'origine. La réponse contient l'`id` de l'apprentissage ou, en cas de doublon, `duplicate_of`.\n</Endpoint>"},{"anchor":"retrouver-des-apprentissages","heading":"Retrouver des apprentissages","markdown":"<Endpoint id=\"learnings.recall\" example={{ body: `{ \"project\": \"respawn-tech-docs\", \"task\": \"lancer les checks avant d'ouvrir la PR\", \"top_k\": 5 }` }}>\n  `project` et `task` sont obligatoires ; `top_k` limite le nombre d'éléments renvoyés. Chaque élément contient `id`, `kind`, `text`, `source` et `score`. Rappel : l'ingestion est asynchrone, un élément tout juste enregistré peut ne pas encore apparaître.\n</Endpoint>"},{"anchor":"oublier-un-apprentissage","heading":"Oublier un apprentissage","markdown":"<Endpoint id=\"learnings.forget\" />"}]},{"slug":"ferramentas/notify","url":"https://docs.respawntech.dev/fr/ferramentas/notify","title":"Notifications","description":"Un agent prévient son humain par e-mail, webhook signé ou ntfy, avec canaux, priorité et historique des envois.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"notify\" />"},{"anchor":"como-funciona","heading":"Fonctionnement","markdown":"Une notification a un `title`, un `message` et une `priority` (`normal` ou `high`), et éventuellement une `url` que l'humain peut ouvrir. Elle part par un **canal** :\n\n* **Canal choisi :** le `channel` de la requête, par nom de canal.\n* **Canal par défaut :** le canal marqué `is_default`, quand la requête n'a pas de `channel`.\n* **E-mail du compte :** sans aucun canal configuré, la notification va à l'e-mail du compte.\n\nElle est aussi disponible dans le [MCP](/fr/agentes-de-ia) comme l'outil `notify` (toolset `notify`) : l'agent signale qu'il a terminé une longue tâche ou qu'il a besoin d'une décision.\n\n<EndpointList tool=\"notify\" />"},{"anchor":"canais","heading":"Canaux","markdown":"Chaque canal a un `name` unique dans le compte et un `kind` :\n\n| `kind`    | Champs            | Mode de livraison                                                                             |\n| --------- | ----------------- | --------------------------------------------------------------------------------------------- |\n| `email`   | aucun             | Envoie **uniquement à l'e-mail du compte**. Impossible de choisir un autre destinataire.      |\n| `webhook` | `url`, `secret`   | Fait un `POST` sur l'URL, signé comme dans le [Webhook Relay](/fr/ferramentas/webhook-relay). |\n| `ntfy`    | `server`, `topic` | Publie sur le topic [ntfy](https://ntfy.sh) du serveur indiqué.                               |\n\n`is_default` marque le canal utilisé quand la requête n'en indique pas, et `enabled: false` désactive un canal sans le supprimer. Le `secret` du webhook n'est jamais renvoyé dans les réponses : conservez-le à la création du canal.\n\n<Callout type=\"warn\" title=\"Le topic ntfy est le mot de passe\">\n  Sur les serveurs ntfy publics, quiconque connaît le topic peut lire les notifications. Utilisez un nom long et imprévisible, ou votre propre serveur.\n</Callout>"},{"anchor":"limites","heading":"Limites, retry et historique","markdown":"* **Rate limit :** il existe une limite de notifications par période ; au-delà, l'envoi échoue avec `notify.rate_limited`.\n* **Retry du webhook :** une livraison qui échoue est retentée plusieurs fois avant de devenir `notify.delivery_failed`. Le récepteur doit gérer les répétitions et vérifier la signature.\n* **Historique :** `GET /messages` liste les envois (canal, priorité, statut et date), paginé avec `page` et `page_size` (jusqu'à 100). L'historique **ne conserve ni le titre ni le corps du message**."},{"anchor":"custos","heading":"Coûts","markdown":"Chaque envoi par l'API est facturé au montant indiqué sur l'endpoint. Les canaux et l'historique sont gratuits, et une livraison échouée n'est pas facturée."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"envoyer-une-notification","heading":"Envoyer une notification","markdown":"<Endpoint id=\"notify.send\" example={{ body: `{ \"title\": \"Déploiement terminé\", \"message\": \"La version 2.4 est en ligne.\", \"priority\": \"normal\" }` }}>\n  `title` et `message` sont obligatoires ; `priority` vaut `normal` ou `high` ; `url` et `channel` sont facultatifs. Sans `channel`, il utilise le canal par défaut et, sans aucun canal, l'e-mail du compte.\n</Endpoint>"},{"anchor":"historique-des-envois","heading":"Historique des envois","markdown":"<Endpoint id=\"notify.messages\" example={{ query: { page: \"1\", page_size: \"20\" } }}>\n  Paginé par `page` et `page_size` (jusqu'à 100). Il n'inclut pas le corps des messages.\n</Endpoint>"},{"anchor":"lister-les-canaux","heading":"Lister les canaux","markdown":"<Endpoint id=\"notify.channels.list\" />"},{"anchor":"créer-un-canal","heading":"Créer un canal","markdown":"<Endpoint id=\"notify.channels.create\" example={{ body: `{ \"name\": \"telephone\", \"kind\": \"ntfy\", \"server\": \"https://ntfy.sh\", \"topic\": \"mon-topic-secret\", \"is_default\": true }` }}>\n  `name` et `kind` sont obligatoires. `url` et `secret` valent pour `webhook` ; `server` et `topic`, pour `ntfy`. Les champs qui ne correspondent pas au `kind` renvoient `notify.invalid_channel`.\n</Endpoint>"},{"anchor":"consulter-un-canal","heading":"Consulter un canal","markdown":"<Endpoint id=\"notify.channels.get\" />"},{"anchor":"modifier-un-canal","heading":"Modifier un canal","markdown":"<Endpoint id=\"notify.channels.update\" example={{ body: `{ \"enabled\": false }` }}>\n  Envoyez uniquement les champs qui changent. Un canal désactivé renvoie `notify.channel_disabled` quand il est choisi dans un envoi.\n</Endpoint>"},{"anchor":"supprimer-un-canal","heading":"Supprimer un canal","markdown":"<Endpoint id=\"notify.channels.delete\" />"},{"anchor":"tester-un-canal","heading":"Tester un canal","markdown":"<Endpoint id=\"notify.channels.test\">\n  Envoie une notification de test par le canal, pour vérifier la configuration avant de vous y fier.\n</Endpoint>"},{"anchor":"erreurs","heading":"Erreurs","markdown":"<ErrorTable tool=\"notify\" />"}]},{"slug":"ferramentas/pdf-convert","url":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert","title":"PDF Convert","description":"API de conversion HTML en PDF, PNG ou JPG — flux asynchrone, batch, rétention et coûts.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"pdf-convert\" />"},{"anchor":"vue-densemble","heading":"Vue d'ensemble","markdown":"PDF Convert est **asynchrone** :\n\n1. `POST /convert` ou `POST /batch` → **202 Accepted** avec le `conversion_id` (le `{id}` des autres routes).\n2. Un worker effectue le rendu en arrière-plan (Chromium headless).\n3. Suivez l'avancement via le [callback `webhook_url`](/fr/webhooks#pdf-callback) ou en appelant `GET /{id}`.\n4. Téléchargez avec `GET /{id}/download` lorsque `status` vaut `completed`.\n\n<Callout type=\"warn\" title=\"Facturation à la mise en file\">\n  Les crédits sont débités lorsque le job est accepté. Si la mise en file d'un batch échoue après le débit, le montant est remboursé automatiquement.\n</Callout>\n\n<EndpointList tool=\"pdf-convert\" />"},{"anchor":"custos","heading":"Coûts","markdown":"<PricingTable tool=\"pdf-convert\" group=\"document\" />\n\n<PricingTable tool=\"pdf-convert\" group=\"addons\" />\n\n<PricingTable tool=\"pdf-convert\" group=\"batch-discounts\" />\n\nLe batch accepte jusqu'à **<Limit name=\"pdfBatchMax\" /> éléments**. Le retry facture à nouveau le coût intégral, options additionnelles comprises. Enregistrer des templates et envoyer des polices ne coûte aucun crédit. Les consultations et téléchargements via l'API sont soumis au [quota gratuit quotidien](/fr/cobranca-e-limites#franquia-diaria)."},{"anchor":"conversion","heading":"Conversion","markdown":""},{"anchor":"convertir-du-html-en-pdfpngjpg","heading":"Convertir du HTML en PDF/PNG/JPG","markdown":"<Endpoint\n  id=\"pdf.convert\"\n  example={{\n  headers: { 'X-Idempotency-Key': '550e8400-e29b-41d4-a716-446655440001' },\n  body: `{\n\"html\": \"<html><body><h1>Fatura</h1></body></html>\",\n\"css\": \"body { font-family: sans-serif; }\",\n\"format\": \"pdf\",\n\"page_size\": \"A4\",\n\"retention_days\": 7\n}`,\n}}\n>\n  Envoyez **`html` ou `url`** (jamais les deux). Avec `url`, Chromium ouvre la page publique et en fait le rendu ; `css`, `google_fonts_url` et `data` sont refusés dans ce mode (`pdf.invalid_option`).\n\n  | Champ                                                         | Limite / valeurs                                                                                                          |\n  | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |\n  | `html`                                                        | max. <Limit name=\"pdfHtmlMax\" />                                                                                          |\n  | `url`                                                         | alternative à `html` : adresse publique `http`/`https`, jusqu'à 2048 caractères (`pdf.invalid_url` si privée ou invalide) |\n  | `css`                                                         | max. <Limit name=\"pdfCssMax\" />                                                                                           |\n  | `format`                                                      | `pdf` (par défaut), `png`, `jpg` / `jpeg`                                                                                 |\n  | `file_name`                                                   | nom du fichier généré                                                                                                     |\n  | `title`, `author`, `subject`, `keywords`                      | métadonnées du document, 1000 caractères maximum chacune                                                                  |\n  | `header_html` / `footer_html`                                 | en-tête et pied de page répétés sur chaque page                                                                           |\n  | `google_fonts_url`                                            | URL CSS Google Fonts à charger                                                                                            |\n  | `wait_selector` / `wait_delay`                                | attend l'apparition d'un sélecteur / un délai en ms avant le rendu                                                        |\n  | `enable_js` / `custom_js`                                     | exécute du JavaScript en sandbox (SPA, graphiques) / script supplémentaire                                                |\n  | `data`                                                        | objet contenant les variables des placeholders `{{campo}}` dans le HTML                                                   |\n  | `retention_days`                                              | `0` (éphémère, \\~24 h), `1`–`30` ou `-1` (permanent)                                                                      |\n  | `webhook_url`                                                 | reçoit le [callback](/fr/webhooks#pdf-callback) à la fin                                                                  |\n  | `template_id`, `template_version`, `font_ids`, `post_process` | [templates enregistrés](#templates), [polices](#fontes) et [post-traitement](#pos-processamento)                          |\n\n  La taille de page, les marges, le média, la numérotation et la qualité sont décrits dans [Options de page et de rendu](#opcoes-de-pagina).\n\n  Conservez le `conversion_id` : c'est le `{id}` utilisé pour consulter le statut et télécharger le fichier.\n</Endpoint>"},{"anchor":"batch-avec-template","heading":"Batch avec template","markdown":"<Endpoint\n  id=\"pdf.batch\"\n  example={{\n  headers: { 'X-Idempotency-Key': '550e8400-e29b-41d4-a716-446655440002' },\n  body: `{\n\"template\": {\n  \"html\": \"<html><body><h1>{{title}}</h1></body></html>\",\n  \"css\": \"h1 { color: #333; }\"\n},\n\"config\": { \"page_size\": \"A4\", \"format\": \"pdf\", \"retention_days\": 7 },\n\"items\": [\n  { \"custom_id\": \"fatura-001\", \"file_name\": \"fatura-001.pdf\", \"data\": { \"title\": \"Fatura 001\" } },\n  { \"custom_id\": \"fatura-002\", \"file_name\": \"fatura-002.pdf\", \"data\": { \"title\": \"Fatura 002\" } }\n],\n\"delivery\": { \"method\": \"zip\", \"webhook_url\": \"https://example.com/hooks/pdf\" }\n}`,\n}}\n>\n  | Champ                  | Description                                                                                                                                                                                                                                                         |\n  | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n  | `template.html`        | **obligatoire** — HTML avec des placeholders `{{campo}}` (syntaxe Go template : `{{if}}` et `{{range}}` fonctionnent aussi)                                                                                                                                         |\n  | `template.css`         | CSS partagé                                                                                                                                                                                                                                                         |\n  | `items[]`              | **obligatoire**, 1 à <Limit name=\"pdfBatchMax\" /> éléments : `custom_id` (**obligatoire**, votre identifiant), `file_name` et `data`                                                                                                                                |\n  | `config`               | options appliquées à chaque élément : `page_size`, `orientation`, marges, `format`, `scale`, `retention_days`, `google_fonts_url`, `header_html`, `footer_html`, `wait_selector`, `wait_delay`, `title`, `author`, `enable_js`, `custom_js` et `file_name_template` |\n  | `delivery.method`      | `zip` ou `list` : comment le résultat est livré à la fin                                                                                                                                                                                                            |\n  | `delivery.webhook_url` | reçoit le [callback](/fr/webhooks#pdf-callback) à la fin                                                                                                                                                                                                            |\n\n  Le batch n'accepte pas `url`, `template_id`, `font_ids` ni `post_process` : il utilise toujours le `template.html` du corps. Les remises sur volume s'appliquent à la base, au JavaScript et à la rétention. Suivez la progression dans [Détail du batch](#pdf-batch-get).\n</Endpoint>"},{"anchor":"relancer-un-échec","heading":"Relancer un échec","markdown":"<Endpoint\n  id=\"pdf.retry\"\n  example={{\n  body: `{\n\"html\": \"<html><body><h1>Fatura corrigida</h1></body></html>\",\n\"format\": \"pdf\",\n\"page_size\": \"A4\"\n}`,\n}}\n>\n  Autorisé uniquement lorsque `status` vaut `failed`. Le corps accepte les mêmes options que [Convertir](#pdf-convert) : vous pouvez donc corriger le HTML ou les options avant de réessayer. Le `webhook_url` du corps est ignoré : la conversion garde celui envoyé à l'origine. Le coût intégral est facturé à nouveau.\n</Endpoint>"},{"anchor":"opcoes-de-pagina","heading":"Options de page et de rendu","markdown":"Elles valent pour `POST /convert` et `POST /retry`. Les valeurs hors liste renvoient `pdf.invalid_option` (ou `pdf.invalid_page_size` pour la taille du papier).\n\n| Champ                                                        | Valeurs                                                                                                                                                                  |\n| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `page_size`                                                  | `A3`, `A4` (par défaut), `A5`, `Letter`, `Legal`, `Tabloid` ou `Custom`                                                                                                  |\n| `paper_width`, `paper_height`                                | papier personnalisé, en **pouces** (1 à 200). Ils ne se combinent qu'avec un `page_size` vide ou `Custom` ; avec une autre taille, ils renvoient `pdf.invalid_page_size` |\n| `orientation`                                                | `portrait` (par défaut) ou `landscape`                                                                                                                                   |\n| `margin_top`, `margin_bottom`, `margin_left`, `margin_right` | marges de la page                                                                                                                                                        |\n| `scale`                                                      | échelle du rendu                                                                                                                                                         |\n| `print_background`                                           | imprime les couleurs et images d'arrière-plan (par défaut `true`)                                                                                                        |\n| `media_type`                                                 | `print` ou `screen` : quel `@media` le CSS voit                                                                                                                          |\n| `color_scheme`                                               | `light` ou `dark` : valeur de `prefers-color-scheme`                                                                                                                     |\n| `page_numbers`                                               | active le compteur de pages à la position : `header-left`, `header-center`, `header-right`, `footer-left`, `footer-center` ou `footer-right`                             |\n| `page_number_format`                                         | texte du compteur, avec `{page}` et `{total}` (par défaut `{page} / {total}`, 200 caractères maximum). Exige `page_numbers`                                              |\n| `jpeg_quality`                                               | de 1 à 100 (par défaut 90). Ne s'applique qu'avec `format: jpg`                                                                                                          |\n\nExemple : un rapport sur papier de 8 x 11 pouces, avec compteur en pied de page :\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n  \"url\": \"https://example.com/relatorio\",\n  \"page_size\": \"Custom\",\n  \"paper_width\": 8,\n  \"paper_height\": 11,\n  \"media_type\": \"print\",\n  \"page_numbers\": \"footer-center\",\n  \"page_number_format\": \"Page {page} sur {total}\"\n}'\n```"},{"anchor":"pos-processamento","heading":"Post-traitement","markdown":"`post_process` agit sur le PDF déjà rendu et **ne s'applique qu'avec `format: pdf`** (avec `png` ou `jpg`, `pdf.invalid_option`). Chaque fonction est facturée séparément, voir [Coûts](#custos).\n\n| Champ                                   | Description                                                                                                                    |\n| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |\n| `post_process.watermark.text`           | texte du filigrane, 200 caractères maximum                                                                                     |\n| `post_process.watermark.image`          | alternative au texte : image PNG ou JPEG en base64, 1 MiB maximum. Envoyez **exactement un** parmi `text` et `image`           |\n| `watermark.font_size`                   | de 1 à 300                                                                                                                     |\n| `watermark.color`                       | couleur au format `#rrggbb`                                                                                                    |\n| `watermark.opacity`, `watermark.scale`  | de 0 à 1                                                                                                                       |\n| `watermark.rotation`                    | de -180 à 180 degrés                                                                                                           |\n| `watermark.position`                    | `center`, `top-left`, `top-center`, `top-right`, `left`, `right`, `bottom-left`, `bottom-center` ou `bottom-right`             |\n| `watermark.on_top`                      | `true` dessine le filigrane au-dessus du contenu                                                                               |\n| `post_process.pdfa`                     | `true` convertit en PDF/A-2b (archivage à long terme)                                                                          |\n| `post_process.protection.user_password` | **obligatoire** s'il y a une `protection`, 128 caractères maximum : mot de passe pour ouvrir le fichier                        |\n| `protection.owner_password`             | mot de passe propriétaire, 128 caractères maximum                                                                              |\n| `protection.permissions`                | ce que le lecteur peut faire : `print`, `copy`, `modify`, `annotate`, `fill_forms`, `assemble`. Liste vide = aucune permission |\n\n`pdfa` et `protection` ne se combinent pas (le PDF/A interdit le chiffrement) : l'API répond `pdf.invalid_option`. Les mots de passe sont stockés chiffrés uniquement pour que le worker les applique et ne reviennent jamais dans les réponses.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n  \"html\": \"<html><body><h1>Contrato</h1></body></html>\",\n  \"format\": \"pdf\",\n  \"post_process\": {\n    \"watermark\": { \"text\": \"CONFIDENTIEL\", \"color\": \"#cc0000\", \"opacity\": 0.2, \"rotation\": -45, \"position\": \"center\" },\n    \"protection\": { \"user_password\": \"mot-de-passe-lecteur\", \"permissions\": [\"print\"] }\n  }\n}'\n```"},{"anchor":"templates","heading":"Templates enregistrés","markdown":"Enregistrez dans le compte le HTML d'un document récurrent et faites-en le rendu avec seulement `template_id` et `data`. Chaque modification du contenu crée une nouvelle **version** ; les précédentes restent consultables.\n\n* Dans `POST /convert`, `template_id` ne se combine pas avec `html` ni `url` (`pdf.invalid_option`). `template_version` choisit la version ; `0` ou omis utilise la plus récente.\n* Le `css`, le `header_html` et le `footer_html` du template servent de valeurs par défaut ; si vous les envoyez dans la conversion, les vôtres l'emportent.\n* Template ou version inexistant : `pdf.template_not_found`. Nom de 120 caractères maximum ; le `html` suit la limite de <Limit name=\"pdfHtmlMax\" /> et le `css` celle de <Limit name=\"pdfCssMax\" />.\n* Les lectures utilisent le scope `tools:pdf:read` ; les écritures, `tools:pdf:manage` (ou `tools:manage`). Aucune ne facture de crédits ni ne consomme le quota gratuit quotidien.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n  \"template_id\": \"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a\",\n  \"template_version\": 0,\n  \"data\": { \"numero\": \"001\" }\n}'\n```"},{"anchor":"lister-les-templates-enregistrés","heading":"Lister les templates enregistrés","markdown":"<Endpoint id=\"pdf.templates.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  `limit` (par défaut et maximum 100) et `offset`. La réponse contient `items` et `total`.\n</Endpoint>"},{"anchor":"enregistrer-un-template","heading":"Enregistrer un template","markdown":"<Endpoint\n  id=\"pdf.templates.create\"\n  example={{\n  body: `{\n\"name\": \"Fatura padrão\",\n\"description\": \"Fatura mensal com logotipo\",\n\"html\": \"<html><body><h1>Fatura {{numero}}</h1></body></html>\",\n\"css\": \"h1 { color: #333; }\",\n\"header_html\": \"\",\n\"footer_html\": \"\"\n}`,\n}}\n>\n  `name` (120 caractères maximum) et `html` sont obligatoires ; `description`, `css`, `header_html` et `footer_html` sont facultatifs. Crée la version 1.\n</Endpoint>"},{"anchor":"consulter-un-template","heading":"Consulter un template","markdown":"<Endpoint id=\"pdf.templates.get\" example={{ query: { version: '2' } }}>\n  `version` facultative (par défaut : la version actuelle). Renvoie le `template` et le contenu de la `version`.\n</Endpoint>"},{"anchor":"lister-les-versions-du-template","heading":"Lister les versions du template","markdown":"<Endpoint id=\"pdf.templates.versions\" />"},{"anchor":"renommer-le-template-ou-enregistrer-une-nouvelle-version","heading":"Renommer le template ou enregistrer une nouvelle version","markdown":"<Endpoint\n  id=\"pdf.templates.update\"\n  example={{\n  body: `{\n\"name\": \"Fatura padrão v2\",\n\"html\": \"<html><body><h1>Fatura {{numero}}</h1><p>{{cliente}}</p></body></html>\",\n\"css\": \"h1 { color: #333; }\"\n}`,\n}}\n>\n  Envoyez `name` et/ou `description` pour renommer. Avec `html`, une **nouvelle version** est enregistrée avec le `html`, `css`, `header_html` et `footer_html` du corps, et `current_version` augmente. Les champs de contenu omis sont vides dans la nouvelle version : renvoyez le contenu complet.\n</Endpoint>"},{"anchor":"supprimer-un-template","heading":"Supprimer un template","markdown":"<Endpoint id=\"pdf.templates.delete\">\n  Supprime le template et toutes ses versions. Les conversions déjà générées ne sont pas affectées.\n</Endpoint>"},{"anchor":"fontes","heading":"Polices","markdown":"Envoyez vos propres polices (`woff2` ou `ttf`) et utilisez-les dans le HTML avec `font-family` : passez les ids dans `font_ids` (jusqu'à 20 par conversion) et la plateforme injecte le `@font-face` correspondant. Police inexistante dans `font_ids` : `pdf.font_not_found`. Le supplément polices est facturé une fois par document qui utilise `font_ids`.\n\n* Jusqu'à **50 polices par compte** (`pdf.font_limit`).\n* Une combinaison `family` + `weight` + `style` ne peut exister qu'une fois (`pdf.font_exists`, 409).\n* Le format est détecté d'après le contenu du fichier, pas l'extension."},{"anchor":"lister-les-polices","heading":"Lister les polices","markdown":"<Endpoint id=\"pdf.fonts.list\" />"},{"anchor":"envoyer-une-police","heading":"Envoyer une police","markdown":"<Endpoint id=\"pdf.fonts.upload\">\n  Envoi en `multipart/form-data` avec `file` (obligatoire, `woff2` ou `ttf`, 5 MiB maximum), `family` (obligatoire, 1 à 64 lettres, chiffres, espaces, `-` ou `_`), `weight` (`100` à `900` par pas de 100 ; par défaut `400`) et `style` (`normal` ou `italic` ; par défaut `normal`). Toute violation renvoie `pdf.font_invalid`.\n\n  ```bash\n  curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/fonts' \\\n    -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n    -F 'file=@Inter-Regular.woff2' \\\n    -F 'family=Inter' \\\n    -F 'weight=400' \\\n    -F 'style=normal'\n  ```\n\n  Puis convertissez avec la police :\n\n  ```bash\n  curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \\\n    -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"html\": \"<html><body style=\\\"font-family: Inter\\\"><h1>Bonjour</h1></body></html>\",\n    \"font_ids\": [\"2b3c4d5e-6f70-4a8b-9c0d-1e2f3a4b5c6d\"]\n  }'\n  ```\n</Endpoint>"},{"anchor":"supprimer-une-police","heading":"Supprimer une police","markdown":"<Endpoint id=\"pdf.fonts.delete\">\n  Supprime la police et le fichier stocké.\n</Endpoint>"},{"anchor":"consultation-et-téléchargement","heading":"Consultation et téléchargement","markdown":""},{"anchor":"lister-les-conversions","heading":"Lister les conversions","markdown":"<Endpoint id=\"pdf.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  Query : `limit` (20 par défaut), `offset` et `exclude_batch=true` pour masquer les éléments de batch. Dans la liste, `file_url` et `error_message` ne sont pas renseignés — consultez le [statut](#pdf-get) de chaque conversion.\n</Endpoint>"},{"anchor":"statut-de-la-conversion","heading":"Statut de la conversion","markdown":"<Endpoint id=\"pdf.get\">\n  `status` : `pending`, `processing`, `completed` ou `failed`. En cas d'échec, `error_code` et `error_message` en expliquent la raison. `data` contient les variables envoyées et `file_url` apparaît à la fin du job — pour télécharger, utilisez toujours [Télécharger le fichier](#pdf-download).\n</Endpoint>"},{"anchor":"télécharger-le-fichier","heading":"Télécharger le fichier","markdown":"<Endpoint id=\"pdf.download\" example={{ response: 'binary' }}>\n  Requiert `status: completed`. Le fichier est déchiffré et décompressé pendant le téléchargement. `X-Dry-Run` ne s'applique pas aux téléchargements.\n\n  Le `file_url` de la conversion (ou celui du détail du batch) est un **lien signé** avec `?sig=&exp=` : il télécharge le fichier sans l'en-tête `X-API-Token` et sans consommer le quota gratuit quotidien, ce qui permet de remettre le lien à un client final. Il est valable jusqu'à la date `exp` ; ensuite, consultez à nouveau la conversion pour en obtenir un autre.\n</Endpoint>"},{"anchor":"batches","heading":"Batches","markdown":""},{"anchor":"lister-les-batches","heading":"Lister les batches","markdown":"<Endpoint id=\"pdf.batches\" example={{ query: { limit: '20', offset: '0' } }}>\n  `status` du batch : `processing`, `delivering`, `completed`, `partially_failed` ou `failed`.\n</Endpoint>"},{"anchor":"détail-du-batch","heading":"Détail du batch","markdown":"<Endpoint id=\"pdf.batch-get\" />"},{"anchor":"téléchargement-zip-du-batch","heading":"Téléchargement ZIP du batch","markdown":"<Endpoint id=\"pdf.batch-download\" example={{ response: 'binary' }}>\n  Disponible lorsque le batch est terminé — y compris partiellement, si certains éléments ont échoué. Il accepte aussi le lien signé `?sig=&exp=`, sans jeton.\n</Endpoint>"},{"anchor":"erreurs","heading":"Erreurs","markdown":"<ErrorTable tool=\"pdf-convert\" />"},{"anchor":"particularités","heading":"Particularités","markdown":"* Un batch utilise un seul template HTML ; chaque élément de `items[].data` remplit les placeholders. `custom_id` aide à relier les fichiers générés à vos propres enregistrements.\n* Pour les PDF récurrents, enregistrez un [template](#templates) au lieu de renvoyer le HTML à chaque appel.\n* `retention_days: 0` est éphémère (\\~24 h) — idéal pour les aperçus.\n* Préférez `webhook_url` plutôt que d'interroger le statut en boucle : chaque consultation via l'API est décomptée du quota gratuit quotidien."}]},{"slug":"ferramentas/qr-dinamico","url":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico","title":"QR Dynamique","description":"API de QR codes dynamiques — création, gestion, analytics, coûts et redirection publique.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"qr-dinamico\" />"},{"anchor":"vue-densemble","heading":"Vue d'ensemble","markdown":"Le QR dynamique sépare l'**image imprimée** (lien court stable) de la **destination** (modifiable sans réimprimer). Flux typique :\n\n1. `POST` avec `X-Dry-Run: true` pour valider le payload et connaître le coût.\n2. `POST` définitif avec `X-Idempotency-Key` → renvoie l'image du QR, le `resource_id` et le `slug`.\n3. Utilisez l'image renvoyée (ou générez la vôtre pointant vers `full_url`, disponible via `GET /{id}`).\n4. Suivez les scans via les analytics ou le webhook `qr.dynamic.scanned`. Le webhook est « best effort » (lors de pics de trafic, certains événements peuvent ne pas être envoyés) ; utilisez les analytics pour des comptes exacts.\n\n<Callout type=\"warn\" title=\"La redirection publique facture chaque scan\">\n  `GET /r/{short_id}` ne requiert pas d'authentification, mais chaque scan peut débiter des crédits du compte propriétaire du QR. Sans solde, le QR peut être désactivé. Les scans répétés d'un même visiteur sur le même QR dans un délai de 30 secondes ne sont facturés qu'une fois : ils redirigent toujours, mais n'entrent pas dans les analytics, ne déclenchent pas le webhook et ne consomment pas le `scan_limit`.\n</Callout>\n\n<EndpointList tool=\"qr-dinamico\" />"},{"anchor":"custos","heading":"Coûts","markdown":"<PricingTable tool=\"qr-dinamico\" group=\"creation\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"domain\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"batch\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"scans\" />\n\n<PricingTable tool=\"qr-dinamico\" group=\"storage\" />\n\nLes lectures via l'API (liste, détail, analytics, scans, vérification de slug) sont soumises au [quota gratuit quotidien](/fr/cobranca-e-limites#franquia-diaria). `PATCH`, activer, désactiver et `DELETE` sont gratuits."},{"anchor":"création-et-gestion","heading":"Création et gestion","markdown":""},{"anchor":"créer-un-qr-dynamique","heading":"Créer un QR dynamique","markdown":"<Endpoint\n  id=\"qr.create\"\n  example={{\n  headers: { 'X-Idempotency-Key': '550e8400-e29b-41d4-a716-446655440000' },\n  body: `{\n\"name\": \"Campanha verão\",\n\"target_url\": \"https://loja.example.com/promo\",\n\"custom_slug\": \"promo-verao\",\n\"foreground_color\": \"#000000\",\n\"background_color\": \"#ffffff\"\n}`,\n}}\n>\n  Champs principaux : `name` et `target_url` (le `target_url` est facultatif lorsqu'un [`content`](#conteudo-hospedado) est fourni). Les couleurs et le style se placent directement dans le corps (`foreground_color`, `background_color`, `module_style`, `eye_style`, `size`, `logo_data_url`…). Facultatifs : `custom_slug`, `custom_domain_id` ([domaine personnalisé](#dominio-proprio)), `expires_at`, `scan_limit`, `skip_image_generation`, `utm_forwarding`, `folder_id` et `tags` ([organisation](#organizacao)), `frame` et `dpi` ([impression](#impressao)) ainsi que les options de [comportement du scan](#comportamento-do-scan) — chacune peut ajouter un coût (voir [Coûts](#custos)). `response_types` choisit les formats de l'image (`svg`, `png`, `pdf` et `eps` ; `svg` par défaut) : `svg` et `eps` sont renvoyés en texte, `png` et `pdf` en base64.\n\n  La réponse contient les images, le `resource_id` (id du QR) et le `slug`. Avec `skip_image_generation: true`, un seul élément est renvoyé, avec uniquement `resource_id` et `slug`.\n</Endpoint>"},{"anchor":"lister-les-qr","heading":"Lister les QR","markdown":"<Endpoint id=\"qr.list\" example={{ query: { limit: '20', offset: '0' } }}>\n  Query : `limit` (20 par défaut, max. <Limit name=\"pageSizeMax\" />), `offset`, `search`, `folder_id` (un UUID, ou `none` pour les QR sans dossier), `tags` (séparés par des virgules ; le QR doit toutes les avoir) et `status` (`active` ou `paused`).\n</Endpoint>"},{"anchor":"détail","heading":"Détail","markdown":"<Endpoint id=\"qr.get\" />"},{"anchor":"modifier-la-destination-le-nom-ou-le-design","heading":"Modifier la destination, le nom ou le design","markdown":"<Endpoint\n  id=\"qr.update\"\n  example={{\n  body: `{\n\"name\": \"Campanha atualizada\",\n\"target_url\": \"https://loja.example.com/nova-promo\"\n}`,\n}}\n>\n  Modifier le nom, la destination et les couleurs est gratuit. Activer une option payante désactivée — `utm_forwarding`, `scan_limit` (de 0 à une valeur), dégradé (`foreground_color_end`), yeux personnalisés (`eye_style` ou `eye_dot_style` `shield`/`flower`) ou logo — coûte le même prix qu'à la [création](#custos), une seule fois par mise à jour. Modifier une option déjà active ou la désactiver est gratuit, sans remboursement. Accepte `X-Dry-Run`, `X-Include-Cost` et `X-Idempotency-Key` ; sans solde la réponse est `402` et rien ne change, une mise à jour identique déjà facturée répond `409`, et une couleur invalide est refusée (`400`) avant la facturation. La nouvelle `target_url` passe par les vérifications anti-malware et anti-boucle. Comme pour activer, désactiver et supprimer, la réponse n'est qu'une confirmation — appelez `GET /{id}` pour voir la ressource mise à jour.\n</Endpoint>"},{"anchor":"réactiver","heading":"Réactiver","markdown":"<Endpoint id=\"qr.activate\" />"},{"anchor":"désactiver","heading":"Désactiver","markdown":"<Endpoint id=\"qr.deactivate\">\n  Un QR désactivé affiche une page de QR inactif sur la redirection publique et continue d'occuper un slot jusqu'à sa suppression.\n</Endpoint>"},{"anchor":"supprimer","heading":"Supprimer","markdown":"<Endpoint id=\"qr.delete\">\n  Met fin à la facturation du stockage du slot.\n</Endpoint>"},{"anchor":"vérifier-la-disponibilité-dun-slug","heading":"Vérifier la disponibilité d'un slug","markdown":"<Endpoint id=\"qr.check-slug\" example={{ pathParams: { slug: 'promo-verao' } }} />"},{"anchor":"analytics","heading":"Analytics","markdown":""},{"anchor":"vue-densemble-des-analytics","heading":"Vue d'ensemble des analytics","markdown":"<Endpoint id=\"qr.analytics\" example={{ query: { period: 'last_30d' } }}>\n  Agrégations mises en cache. Query `period` : `last_7d`, `last_30d` (par défaut), `last_90d` ou `all` ; les valeurs inconnues comptent comme 30 jours. `scan_history` contient les 20 scans les plus récents.\n</Endpoint>"},{"anchor":"scans-par-période","heading":"Scans par période","markdown":"<Endpoint id=\"qr.analytics-period\" example={{ query: { period: 'last_30d' } }}>\n  Renvoie les totaux et `period_stats` (scans par jour). Les champs `period`, `period_scans`, `period_cost` et `unique_scans` sont à zéro sur cette route — utilisez la [vue d'ensemble](#qr-analytics) si vous en avez besoin.\n</Endpoint>"},{"anchor":"répartition-par-appareil","heading":"Répartition par appareil","markdown":"<Endpoint id=\"qr.analytics-device\" example={{ query: { period: 'last_30d' } }}>\n  `device` vaut `Mobile`, `Desktop` ou `Tablet`. Comme pour les scans par période, les champs de période sont à zéro.\n</Endpoint>"},{"anchor":"historique-des-scans","heading":"Historique des scans","markdown":"<Endpoint id=\"qr.scans\" example={{ query: { limit: '50', offset: '0', period: 'last_30d' } }}>\n  Pagination : `limit` (50 par défaut, max. <Limit name=\"pageSizeMax\" />), `offset` et `period` (par défaut : 30 derniers jours). `location` vaut `Unknown` lorsque la ville et le pays sont inconnus.\n</Endpoint>"},{"anchor":"redirection-publique","heading":"Redirection publique","markdown":"<Endpoint id=\"qr.redirect\" example={false}>\n  * **Actif :** `307` vers la `target_url` (avec la query string transmise, si l'UTM forwarding est activé). Pour les comptes sans offre premium, le scan passe d'abord par une page intermédiaire de la marque qui redirige au bout de 3 secondes.\n  * **Inexistant, inactif, expiré, sans solde, au-delà de la limite de scans ou avec une destination dangereuse :** redirige vers la page de QR indisponible, avec le motif dans `?reason=` (`unknown`, `inactive`, `expired`, `bankrupt`, `limit_reached`, `safety_risk`, `rate_limited`).\n  * **Trop d'accès d'un même visiteur :** au-delà de 600 par minute (tous QR confondus), redirige vers la page de QR indisponible avec `?reason=rate_limited`. Au-delà de 30 scans par minute sur le même QR, la redirection fonctionne toujours, mais les scans supplémentaires ne sont ni comptés ni facturés.\n  * Les réponses ne sont jamais mises en cache (`Cache-Control: no-store`).\n\n  ```bash\n  curl -sS -I 'https://api.respawntech.dev/r/promo-verao'\n  ```\n</Endpoint>"},{"anchor":"dominio-proprio","heading":"Domaine personnalisé","markdown":"Au lieu de `api.respawntech.dev/r/promo-verao`, le QR peut vivre sur `https://qr.suaempresa.com/promo-verao`. Vous faites pointer un **sous-domaine à vous** vers Respawn Tech avec un enregistrement CNAME ; le certificat HTTPS est émis automatiquement (Cloudflare for SaaS). Le `hostname` doit être un sous-domaine d'au moins trois libellés (`qr.suaempresa.com`) : un domaine racine (apex) n'accepte pas de CNAME, et le port, le chemin, l'IP et le joker sont refusés.\n\nLe scope est `tools:qr:domains:manage` (ou `tools:qr:dynamic:manage` / `tools:manage`). La limite de domaines dépend de l'offre (`qr_domain.plan_limit`) et un domaine actif est facturé tous les 30 jours (voir [Coûts](#custos)).\n\n<Steps>\n  <Step>\n    ### Enregistrez le domaine\n\n    ```bash\n    curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains' \\\n      -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n      -H 'Content-Type: application/json' \\\n      -d '{\n        \"hostname\": \"qr.suaempresa.com\",\n        \"validation_method\": \"http\",\n        \"root_url\": \"https://www.suaempresa.com\"\n      }'\n    ```\n\n    `validation_method` vaut `http` (par défaut) ou `txt` : c'est la manière dont le certificat est validé. La réponse (`201`) contient `status: \"pending\"` et la liste `records` avec tout ce que vous devez publier. Conservez l'`id`.\n  </Step>\n\n  <Step>\n    ### Publiez le CNAME\n\n    Dans l'interface DNS de votre domaine, créez l'enregistrement de l'élément avec `purpose: \"routing\"`. La cible (`value`) est fournie dans la réponse ; ne la copiez pas depuis des exemples.\n\n    | Type    | Nom                           | Valeur                                   |\n    | ------- | ----------------------------- | ---------------------------------------- |\n    | `CNAME` | `qr` (ou `qr.suaempresa.com`) | le `value` de l'enregistrement `routing` |\n\n    Sur **Cloudflare**, créez le CNAME dans la zone `suaempresa.com` et laissez le proxy **désactivé** (nuage gris, « DNS uniquement ») jusqu'à ce que le domaine soit `active` ; ensuite seulement, si vous le souhaitez, activez le proxy. Chez les autres fournisseurs, il s'agit d'un CNAME ordinaire pour le sous-domaine.\n  </Step>\n\n  <Step>\n    ### Validez le certificat\n\n    Si vous avez choisi `http`, l'enregistrement `purpose: \"certificate\"` avec `type: \"HTTP\"` est validé automatiquement dès que le CNAME se propage, sans rien à publier. Si vous avez choisi `txt`, publiez aussi les enregistrements `TXT` fournis dans `records` (`ownership` et `certificate`), dans la même interface DNS.\n  </Step>\n\n  <Step>\n    ### Vérifiez jusqu'à ce qu'il soit actif\n\n    La plateforme interroge le domaine périodiquement ; pour ne pas attendre, forcez la vérification :\n\n    ```bash\n    curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains/$DOMAIN_ID/refresh' \\\n      -H \"X-API-Token: $RESPAWN_API_TOKEN\"\n    ```\n\n    Le `status` passe de `pending` à `active` (la facturation de la période a lieu à ce moment). `failed` indique que le fournisseur a refusé le hostname (voir `last_error`) ; `suspended`, que la période n'a pas pu être facturée — les scans vont vers la page de repli jusqu'à la prochaine facturation réussie.\n  </Step>\n\n  <Step>\n    ### Créez des QR sur le domaine\n\n    Envoyez l'`id` du domaine dans `custom_domain_id`. Un domaine encore `pending` est déjà accepté, pour que vous puissiez imprimer pendant la propagation du DNS, mais les scans ne redirigent qu'une fois le domaine `active`.\n\n    ```bash\n    curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n      -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n      -H 'Content-Type: application/json' \\\n      -d '{\n        \"name\": \"Cartaz da loja\",\n        \"target_url\": \"https://loja.example.com/promo\",\n        \"custom_slug\": \"promo-verao\",\n        \"custom_domain_id\": \"'\"$DOMAIN_ID\"'\"\n      }'\n    ```\n\n    Le `full_url` du QR devient `https://qr.suaempresa.com/promo-verao`. Le slug doit seulement être unique au sein du domaine.\n  </Step>\n</Steps>\n\n`root_url` est la destination de la racine du domaine (`https://qr.suaempresa.com/`) et `not_found_url`, celle des slugs inconnus ; sans eux, la page de repli de la plateforme s'applique. Aucun des deux ne peut pointer vers le domaine lui-même. Seuls `root_url` et `not_found_url` changent après l'enregistrement (`PATCH`) ; pour changer le hostname, supprimez puis enregistrez à nouveau.\n\n<Callout type=\"info\" title=\"Supprimer un domaine\">\n  Un domaine auquel des QR sont encore liés ne peut pas être supprimé (`qr_domain.in_use`). Supprimez d'abord les QR ; une fois le domaine supprimé, leur `full_url` cesse de fonctionner.\n</Callout>"},{"anchor":"enregistrez-le-domaine","heading":"Lister les domaines personnalisés","markdown":"<Endpoint id=\"qr.domains-list\" />"},{"anchor":"publiez-le-cname","heading":"Détail du domaine","markdown":"<Endpoint id=\"qr.domains-get\" />"},{"anchor":"validez-le-certificat","heading":"Enregistrer un domaine personnalisé","markdown":"<Endpoint\n  id=\"qr.domains-create\"\n  example={{\n  body: `{\n\"hostname\": \"qr.suaempresa.com\",\n\"validation_method\": \"http\",\n\"root_url\": \"https://www.suaempresa.com\"\n}`,\n}}\n/>"},{"anchor":"vérifiez-jusquà-ce-quil-soit-actif","heading":"Modifier les pages de repli du domaine","markdown":"<Endpoint\n  id=\"qr.domains-update\"\n  example={{\n  body: `{\n\"root_url\": \"https://www.suaempresa.com\",\n\"not_found_url\": \"https://www.suaempresa.com/ops\"\n}`,\n}}\n>\n  Envoyez `\"\"` pour vider l'un des champs.\n</Endpoint>"},{"anchor":"créez-des-qr-sur-le-domaine","heading":"Vérifier le domaine maintenant","markdown":"<Endpoint id=\"qr.domains-refresh\" />"},{"anchor":"lister-les-domaines-personnalisés","heading":"Supprimer le domaine","markdown":"<Endpoint id=\"qr.domains-delete\" />\n\n## Comportement du scan\n\nPar défaut, le scan est une redirection vers la `target_url`. Ces champs, acceptés dans le `POST` et le `PATCH`, modifient ce qui se passe au scan. L'évaluation suit cet ordre : **mot de passe** (le scan ne continue qu'après celui-ci), **règles** (la première qui correspond l'emporte), **test A/B** (pour les scans qu'aucune règle n'a captés) et enfin la `target_url`. Chaque option est facturée une fois (voir [Coûts](#custos)).\n\nDans un `PATCH`, chaque champ envoyé **remplace** sa partie : `[]` vide les règles ou les variantes, `{}` vide les pixels et `\"\"` vide une URL ou le mot de passe."},{"anchor":"détail-du-domaine","heading":"Règles de redirection","markdown":"`redirect_rules` (jusqu'à 20) envoie le scan vers une URL propre lorsque toutes les conditions de la règle sont remplies. Une règle sans aucune condition est refusée.\n\n| Champ                    | Signification                                                                                              |\n| ------------------------ | ---------------------------------------------------------------------------------------------------------- |\n| `id`                     | Nom de la règle (`[a-zA-Z0-9_-]`, jusqu'à 40) ; apparaît dans les scans et dans `rule_stats` des analytics |\n| `target_url`             | Destination lorsque la règle correspond (URL absolue)                                                      |\n| `countries`              | Pays ISO 3166-1 alpha-2 en majuscules (`[\"BR\", \"PT\"]`)                                                     |\n| `devices`                | `mobile`, `tablet`, `desktop`                                                                              |\n| `os`                     | `ios`, `android`, `windows`, `macos`, `linux`, `other`                                                     |\n| `languages`              | Langue préférée du visiteur : `pt` correspond à `pt` et `pt-br` ; `pt-br` seulement à `pt-br`              |\n| `days`                   | Jours de la semaine, `0` (dimanche) à `6` (samedi), dans le `timezone`                                     |\n| `start_time`, `end_time` | Plage horaire de la journée (`HH:MM`) ; si la fin précède le début, elle passe minuit. Les deux ou aucun   |\n| `timezone`               | Fuseau IANA (`America/Sao_Paulo`) ; UTC si vide                                                            |\n| `from`, `until`          | Période pendant laquelle la règle s'applique (date et heure RFC 3339)                                      |\n\nChaque liste accepte jusqu'à 50 valeurs. Au sein d'une règle, les conditions se cumulent (ET) ; au sein d'une liste, une seule valeur suffit (OU)."},{"anchor":"enregistrer-un-domaine-personnalisé","heading":"Test A/B","markdown":"`ab_variants` (de 2 à 10) répartit, selon le poids, les scans qu'aucune règle n'a captés. Chaque variante a un `id` unique, une `target_url` et un `weight` de 1 à 1000 ; la part d'une variante est son poids divisé par la somme. Les analytics renvoient `variant_stats` avec les scans de chaque variante."},{"anchor":"modifier-les-pages-de-repli-du-domaine","heading":"Mot de passe","markdown":"`password` (de 4 à 72 caractères) cache le QR derrière une page de mot de passe. Le mot de passe est stocké sous forme de hash ; les réponses contiennent uniquement `scan_behavior.has_password`. Après avoir saisi le bon mot de passe, le visiteur suit le flux normal et cette page n'est pas mise en cache. Après 5 mots de passe erronés, le visiteur est bloqué sur ce QR pendant 15 minutes."},{"anchor":"vérifier-le-domaine-maintenant","heading":"Pixels","markdown":"`pixels` déclenche les balises sur une page légère avant la redirection : `ga4_measurement_id` (`G-XXXXXXX`), `meta_pixel_id` (chiffres uniquement) et `gtm_container_id` (`GTM-XXXXXX`)."},{"anchor":"supprimer-le-domaine","heading":"Redirection d'expiré et d'épuisé","markdown":"`expired_url` et `limit_reached_url` (URL absolues) remplacent la page de QR indisponible lorsque le QR a expiré (`expires_at`) ou a utilisé tout son `scan_limit`.\n\n### Exemple complet\n\nCopiez, changez les URL et exécutez :\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Campanha com regras\",\n    \"target_url\": \"https://loja.example.com/global\",\n    \"redirect_rules\": [\n      {\n        \"id\": \"br-mobile\",\n        \"target_url\": \"https://loja.example.com/br\",\n        \"countries\": [\"BR\"],\n        \"devices\": [\"mobile\"]\n      },\n      {\n        \"id\": \"horario-comercial\",\n        \"target_url\": \"https://loja.example.com/atendimento\",\n        \"days\": [1, 2, 3, 4, 5],\n        \"start_time\": \"09:00\",\n        \"end_time\": \"18:00\",\n        \"timezone\": \"America/Sao_Paulo\"\n      }\n    ],\n    \"ab_variants\": [\n      { \"id\": \"a\", \"target_url\": \"https://loja.example.com/lp-a\", \"weight\": 50 },\n      { \"id\": \"b\", \"target_url\": \"https://loja.example.com/lp-b\", \"weight\": 50 }\n    ],\n    \"pixels\": { \"ga4_measurement_id\": \"G-ABCD1234\" },\n    \"expired_url\": \"https://loja.example.com/encerrada\",\n    \"limit_reached_url\": \"https://loja.example.com/esgotado\"\n  }'\n```\n\nPour tout retirer ensuite, sans recréer le QR (remplacez `$QR_ID` par le `resource_id` de la création) :\n\n```bash\ncurl -sS -X PATCH \"https://api.respawntech.dev/v1/tools/image/qr-code/dynamic/$QR_ID\" \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{ \"redirect_rules\": [], \"ab_variants\": [], \"pixels\": {}, \"password\": \"\" }'\n```\n\n`GET /{id}` renvoie la configuration dans `scan_behavior` (`redirect_rules`, `ab_variants`, `has_password`, `pixels`, `expired_url`, `limit_reached_url`, `content`)."},{"anchor":"comportamento-do-scan","heading":"Contenu hébergé","markdown":"Avec `content`, le `/r/{short_id}` lui-même sert une page légère (ou une redirection calculée) à la place de la `target_url`. Le `type` choisit le format et seul le champ du même nom est utilisé. Si une règle de redirection ou un test A/B correspond, il prévaut sur le contenu. Les pages n'exécutent pas de scripts et sont mises en cache 5 minutes (revalidées par ETag). `{}` vide le contenu dans un `PATCH`.\n\n| `type`      | Champ                                                                                      | Le scan affiche                                                                                                          |\n| ----------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |\n| `vcard`     | `vcard` (`FirstName`, `LastName`, `Org`, `Title`, `Phone`, `Email`, `Website`, `Address`)  | Page de contact et téléchargement du `.vcf` (`?format=vcf`). Exige un nom ou une organisation                            |\n| `file`      | `file` (`url`, `name`)                                                                     | Ouvre le fichier ou PDF hébergé (URL absolue)                                                                            |\n| `links`     | `links` (`title`, `description`, `avatar_url`, `links[]` avec `label` et `url`)            | Lien en bio, de 1 à 50 liens                                                                                             |\n| `menu`      | `menu` (`title`, `currency`, `sections[]` → `items[]` avec `name`, `description`, `price`) | Menu, jusqu'à 30 sections et 300 éléments                                                                                |\n| `wifi`      | `wifi` (`SSID`, `Password`, `Security`, `Hidden`)                                          | Nom du réseau et mot de passe à copier                                                                                   |\n| `app_store` | `app_store` (`ios_url`, `android_url`, `fallback_url`)                                     | Redirection vers l'App Store ou Google Play selon le téléphone ; `fallback_url` sert le desktop et les systèmes inconnus |\n\nLes champs de `vcard` et `wifi` utilisent ces noms en PascalCase, exactement comme dans le tableau ; les autres utilisent le `snake_case`. Le `target_url` n'est plus obligatoire. Il reprend le lien du fichier ou du store et, pour les types de page, retombe sur la page de repli de la plateforme.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Link na bio\",\n    \"content\": {\n      \"type\": \"links\",\n      \"links\": {\n        \"title\": \"Loja Exemplo\",\n        \"links\": [\n          { \"label\": \"Site\", \"url\": \"https://loja.example.com\" },\n          { \"label\": \"Instagram\", \"url\": \"https://instagram.com/lojaexemplo\" }\n        ]\n      }\n    }\n  }'\n```\n\nExemple de carte de contact :\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Cartão da Ana\",\n    \"content\": {\n      \"type\": \"vcard\",\n      \"vcard\": { \"FirstName\": \"Ana\", \"LastName\": \"Souza\", \"Org\": \"Loja Exemplo\", \"Phone\": \"+5511999990000\", \"Email\": \"ana@loja.example.com\" }\n    }\n  }'\n```\n\n## Cadres et impression\n\n`frame` dessine un appel à l'action autour du code et `dpi` prépare l'image pour l'impression. Les deux valent pour tout `response_types`.\n\n* `frame` : `text` (jusqu'à 32 caractères ; rétrécit pour tenir), `color`, `text_color` (hex) et `position` (`top` ou `bottom`).\n* `dpi` : de 72 à 1200. Dans le **PDF**, la page a exactement la taille du QR, en vectoriel ; dans le **SVG**, la largeur et la hauteur sont exprimées en mm ; le **PNG** embarque la résolution d'impression dans le fichier. Sans `dpi`, le PDF conserve la mise en page A4.\n* `eps` : nouveau format vectoriel, avec le cadre et la police Helvetica-Bold avec accents. Sort sans logo.\n\nDans le PDF et l'EPS, les yeux sont carrés et seuls les styles de module `dots` et carré deviennent vectoriels ; le dégradé n'est pas appliqué.\n\n```bash\ncurl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \\\n  -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"name\": \"Cartaz A5\",\n    \"target_url\": \"https://loja.example.com/promo\",\n    \"response_types\": [\"pdf\", \"eps\"],\n    \"frame\": { \"text\": \"Escaneie aqui\", \"color\": \"#111111\", \"text_color\": \"#FFFFFF\", \"position\": \"bottom\" },\n    \"dpi\": 300\n  }'\n```\n\n## Organisation, modification en masse et lots"},{"anchor":"regras","heading":"Dossiers et tags","markdown":"`folder_id` range le QR dans un dossier et `tags` (jusqu'à 20 par QR, 40 caractères chacun, sans virgule) l'étiquette ; les deux entrent dans le `POST` et le `PATCH` (`folder_id: \"\"` renvoie à la racine, `tags: []` vide). La liste filtre par `folder_id`, `tags`, `status` et `search`."},{"anchor":"ab","heading":"Lister les dossiers","markdown":"<Endpoint id=\"qr.folders-list\" />"},{"anchor":"senha","heading":"Créer un dossier","markdown":"<Endpoint id=\"qr.folders-create\" example={{ body: '{ \"name\": \"Campanha de verão\" }' }}>\n  Le nom compte de 1 à 80 caractères et est unique dans le compte (`dynamic_qr.folder_taken`).\n</Endpoint>"},{"anchor":"pixels","heading":"Renommer un dossier","markdown":"<Endpoint id=\"qr.folders-rename\" example={{ body: '{ \"name\": \"Verão 2027\" }' }} />"},{"anchor":"fallback","heading":"Supprimer un dossier","markdown":"<Endpoint id=\"qr.folders-delete\" />"},{"anchor":"exemple-complet","heading":"Modifier plusieurs QR à la fois","markdown":"<Endpoint\n  id=\"qr.bulk\"\n  example={{\n  body: `{\n\"ids\": [\"8f3a2b1c-4d5e-6f70-8192-a3b4c5d6e7f8\"],\n\"action\": \"add_tags\",\n\"tags\": [\"verao\"]\n}`,\n}}\n>\n  Une action sur jusqu'à 500 QR, gratuitement. `action` vaut `activate`, `deactivate`, `move` (avec `folder_id` ; `null` renvoie à la racine), `set_target` (avec `target_url`), `add_tags` ou `remove_tags` (avec `tags`). La réponse contient le résultat de chaque QR dans `items`, donc un échec n'entraîne pas les autres. Accepte `X-Idempotency-Key`.\n</Endpoint>\n\n### Créer des QR en lot\n\n<Endpoint\n  id=\"qr.batches-create\"\n  example={{\n  headers: { 'X-Idempotency-Key': '7f1c2e84-5b3a-4d6c-9e01-a2b3c4d5e6f7' },\n  body: `{\n\"csv\": \"name,target_url,custom_slug,tags\\\\nLoja Centro,https://loja.example.com/centro,loja-centro,varejo|sp\\\\nLoja Norte,https://loja.example.com/norte,loja-norte,varejo\",\n\"defaults\": { \"utm_forwarding\": true }\n}`,\n}}\n>\n  Jusqu'à 1 000 lignes par lot, de l'une de ces façons :\n\n  * JSON avec `rows` (une liste d'objets avec les champs du QR) ou `csv` (le texte du CSV), plus `defaults` avec les valeurs utilisées dans les lignes qui ne les fournissent pas (`folder_id`, `tags`, `scan_limit`, `expires_at`, `utm_forwarding`, couleurs, `custom_domain_id`) ;\n  * corps `text/csv` direct, avec `folder_id` et `tags` en paramètres de query.\n\n  Le CSV comporte une ligne d'en-tête et les colonnes `name`, `target_url` (obligatoire), `custom_slug`, `folder_id`, `tags` (séparés par `|`), `scan_limit`, `expires_at`, `utm_forwarding`, `foreground_color` et `background_color` :\n\n  ```csv\n  name,target_url,custom_slug,tags\n  Loja Centro,https://loja.example.com/centro,loja-centro,varejo|sp\n  Loja Norte,https://loja.example.com/norte,loja-norte,varejo\n  ```\n\n  ```bash\n  curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/batches?tags=campanha' \\\n    -H \"X-API-Token: $RESPAWN_API_TOKEN\" \\\n    -H 'X-Idempotency-Key: 7f1c2e84-5b3a-4d6c-9e01-a2b3c4d5e6f7' \\\n    -H 'Content-Type: text/csv' \\\n    --data-binary @lojas.csv\n  ```\n\n  Les lignes invalides sont signalées et **ne sont pas** facturées ; les lignes valides sont facturées en une fois, avec une remise de volume (à partir de 100, 500 et 1 000 lignes), et créées en arrière-plan (`202`). Une ligne qui échoue ensuite est remboursée. Suivez en consultant le détail du lot jusqu'à ce que `pending` atteigne 0. Avec le même `X-Idempotency-Key` et les mêmes lignes, le retry renvoie le même lot, facturé une seule fois. Au-delà de la limite, la réponse est `413` (`dynamic_qr.batch_too_large`).\n</Endpoint>\n\n### Lister les lots\n\n<Endpoint id=\"qr.batches-list\" />\n\n### Détail du lot\n\n<Endpoint id=\"qr.batches-get\">\n  `items` contient chaque ligne (`row`, `status`, `qr_id`, `slug`, `full_url` ou `error_code` et `error_message`). `charged_units`, `discount_units` et `refunded_units` sont exprimés en unités internes de crédit (10 000 unités = 1 crédit).\n</Endpoint>\n\n### Exporter les scans en CSV\n\n<Endpoint id=\"qr.scans-export\" example={{ query: { period: 'last_30d' } }}>\n  Query : `period` (`last_7d`, `last_30d` par défaut, `last_90d` ou `all`) ou `from` et `to` (`YYYY-MM-DD`, UTC, inclus), qui remplacent la période. Facturée comme l'[historique des scans](#qr-scans). Enregistrez avec `curl -o scans.csv`.\n</Endpoint>\n\n### Effacer l'historique des scans\n\n<Endpoint id=\"qr.scans-purge\" example={{ query: { before: '2026-01-01' } }}>\n  Efface les lignes de scan (toutes, ou celles antérieures à `before`, `YYYY-MM-DD` UTC). `total_scans` et `scan_limit` sont conservés. C'est irréversible.\n</Endpoint>"},{"anchor":"conteudo-hospedado","heading":"Erreurs","markdown":"<ErrorTable tool=\"qr-dinamico\" />"},{"anchor":"impressao","heading":"Particularités et limites","markdown":"* Les URL malveillantes sont bloquées automatiquement (`dynamic_qr.malicious_url`).\n* Protection anti-boucle : la destination ne peut pas pointer vers le domaine de redirection lui-même.\n* Le premier QR n'occupe pas de slot payant ; les suivants occupent des slots par groupes de 3.\n* `skip_image_generation: true` réduit le coût lorsque vous générez vous-même l'image du QR.\n* Slug : 3 à 50 caractères `[a-zA-Z0-9-]`. Listes : jusqu'à <Limit name=\"pageSizeMax\" /> éléments par page."}]},{"slug":"ferramentas/realtime","url":"https://docs.respawntech.dev/fr/ferramentas/realtime","title":"Realtime","description":"Publiez et recevez des messages en temps réel par WebSocket ou SSE, avec des tokens émis sur votre serveur, la présence, l'historique par canal, un webhook de canaux et une reconnexion sans perte de messages.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"realtime\" />"},{"anchor":"como-funciona","heading":"Fonctionnement","markdown":"Realtime achemine les messages de votre serveur, ou d'autres clients, vers les navigateurs connectés, sans que vous ayez à maintenir des serveurs WebSocket.\n\n* **Canal :** un nom libre de <Limit name=\"realtimeChannelNameMax\" /> caractères au plus, avec des lettres, des chiffres et `: _ - .` (`chat:lobby`, `commande:123`). Quiconque s'abonne au canal reçoit ce qui y est publié. Il n'y a rien à créer au préalable.\n* **Token :** identifiant de courte durée que **votre serveur** émet avec votre clé d'API et remet au client. Il indique qui est le client (`client_id`, facultatif) et sur quels canaux il peut s'abonner, publier et utiliser la présence (`capabilities`).\n* **`connect_url` :** l'adresse WebSocket à laquelle le client se connecte, renvoyée dans la même réponse que le token et contenant déjà le `?token=`.\n* **Message :** `data` JSON publié sur un canal, avec un `id`, un `timestamp` (millisecondes Unix) et le `client_id` de son auteur.\n* **Présence :** la liste de ceux qui sont connectés et « présents » sur un canal, avec des données libres pour chaque membre (un pseudo, un statut).\n\nLe flux est toujours le même :\n\n<Steps>\n  <Step>\n    Le navigateur demande un token à **votre serveur**, qui connaît déjà l'utilisateur.\n  </Step>\n\n  <Step>\n    Votre serveur [émet le token](#realtime-token) via l'API avec les capabilities de cet utilisateur et renvoie au navigateur la réponse complète : `token`, `expires_at` et `connect_url`.\n  </Step>\n\n  <Step>\n    Le navigateur se connecte à `connect_url`, s'abonne aux canaux et publie.\n  </Step>\n\n  <Step>\n    Si la connexion tombe, le navigateur demande un nouveau token et [se reconnecte](#reconexao), en récupérant les messages manqués avec `rewind`.\n  </Step>\n</Steps>\n\n<Callout type=\"warn\" title=\"Utilisez toujours le connect_url de la réponse\">\n  N'écrivez jamais l'adresse du WebSocket dans le code ou la configuration. Le `connect_url` peut changer d'un environnement à l'autre (test et production) et quand la plateforme redistribue les connexions ; un token n'est valable qu'à l'adresse fournie avec lui.\n</Callout>\n\nVotre clé d'API (`X-API-Token`) reste uniquement sur le serveur. Le navigateur n'utilise que le token de connexion, qui expire et ne voit que les canaux que vous avez autorisés.\n\n<EndpointList tool=\"realtime\" />"},{"anchor":"token-e-capabilities","heading":"Token et capabilities","markdown":""},{"anchor":"émettre-un-token-de-connexion","heading":"Émettre un token de connexion","markdown":"<Endpoint id=\"realtime.token\" example={{ body: `{ \"client_id\": \"user-42\", \"capabilities\": { \"chat:*\": [\"subscribe\", \"publish\", \"presence\"], \"alerts\": [\"subscribe\"] }, \"ttl_seconds\": 3600 }` }}>\n  * `capabilities` (obligatoire) : objet associant un motif de canal à la liste des opérations autorisées, jusqu'à <Limit name=\"realtimeTokenPatternsMax\" /> motifs. Le motif est un nom exact (`alerts`), un préfixe terminé par `*` (`chat:*`) ou simplement `*` (tous les canaux). Les opérations sont `subscribe`, `publish`, `presence` (entrer dans la présence ; la lire exige `subscribe`) ou `*` (toutes). Sans capability pour le canal, l'opération échoue avec `realtime.capability_denied`.\n  * `client_id` (facultatif, jusqu'à <Limit name=\"realtimeClientIdMax\" /> caractères) : identifie le client dans les messages qu'il publie et dans la présence. Utilisez l'id de votre utilisateur ; n'acceptez pas cette valeur venant du navigateur.\n  * `ttl_seconds` (facultatif) : durée de validité du token, de <Limit name=\"realtimeTokenTtlMin\" /> à <Limit name=\"realtimeTokenTtlMax\" />, <Limit name=\"realtimeTokenTtlDefault\" /> par défaut. Le token n'est vérifié qu'à la connexion : une connexion ouverte reste valable après son expiration, mais la reconnexion suivante exige un nouveau token.\n\n  La réponse contient `token`, `expires_at` et `connect_url` (qui inclut déjà `?token=`). Quand la plateforme doit démarrer un nœud avant d'accepter des connexions, la réponse contient aussi `retry_after_ms` : le token est valide, mais **attendez ce délai avant de vous connecter**.\n</Endpoint>\n\nDonnez à chaque client seulement ce dont il a besoin : un chat avec `{\"chat:lobby\": [\"subscribe\", \"publish\", \"presence\"]}`, un tableau de bord en lecture seule avec `{\"metriques:*\": [\"subscribe\"]}`. Le token est signé en HMAC (HS256) et n'est valable que pour Realtime.\n\n<Callout title=\"Token de l'inspecteur\">\n  Le dashboard utilise la route `POST /v1/tools/realtime/inspector-token` pour ouvrir l'inspecteur en direct (abonnement à tous les canaux, sans publication, 15 minutes). Elle n'accepte que la session du dashboard ; avec une clé d'API, elle répond `realtime.session_required`.\n</Callout>"},{"anchor":"conexao-pelo-navegador","heading":"Connexion depuis le navigateur","markdown":"Sur votre serveur, exposez une route qui émet le token et relaie la réponse. L'exemple ci-dessous utilise `RESPAWN_API_TOKEN` et une route `/realtime-token` ; remplacez le `client_id` par l'utilisateur authentifié.\n\n<Tabs groupId=\"code-language\" persist items={['Node.js', 'Python']}>\n  <Tab value=\"Node.js\">\n    ```js\n    import { createServer } from 'node:http';\n\n    createServer(async (req, res) => {\n      if (req.url !== '/realtime-token') return res.writeHead(404).end();\n\n      const api = await fetch('https://api.respawntech.dev/v1/tools/realtime/tokens', {\n        method: 'POST',\n        headers: { 'X-API-Token': process.env.RESPAWN_API_TOKEN, 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          client_id: 'user-42', // the authenticated user\n          capabilities: { 'chat:*': ['subscribe', 'publish', 'presence'] },\n        }),\n      });\n      const { data } = await api.json();\n\n      // token, expires_at, connect_url and retry_after_ms go together to the browser\n      res.writeHead(api.status, { 'Content-Type': 'application/json' }).end(JSON.stringify(data));\n    }).listen(3000);\n    ```\n  </Tab>\n\n  <Tab value=\"Python\">\n    ```python\n    import os\n    import requests\n    from flask import Flask, jsonify\n\n    app = Flask(__name__)\n\n    @app.post(\"/realtime-token\")\n    def realtime_token():\n        api = requests.post(\n            \"https://api.respawntech.dev/v1/tools/realtime/tokens\",\n            headers={\"X-API-Token\": os.environ[\"RESPAWN_API_TOKEN\"]},\n            json={\n                \"client_id\": \"user-42\",  # the authenticated user\n                \"capabilities\": {\"chat:*\": [\"subscribe\", \"publish\", \"presence\"]},\n            },\n            timeout=30,\n        )\n        # token, expires_at, connect_url and retry_after_ms go together to the browser\n        return jsonify(api.json()[\"data\"]), api.status_code\n    ```\n  </Tab>\n</Tabs>\n\nDans le navigateur, demandez le token à votre serveur et connectez-vous au `connect_url` fourni avec lui. Ce HTML fonctionne collé dans un fichier et servi par le même serveur que la route `/realtime-token` (en test, utilisez une clé de l'environnement de test) ; il se reconnecte déjà tout seul. Si vous préférez ne pas écrire le protocole à la main, utilisez le [SDK JavaScript](#sdk).\n\n```html\n<!doctype html>\n<meta charset=\"utf-8\" />\n<title>Realtime</title>\n<ul id=\"log\"></ul>\n<input id=\"text\" placeholder=\"Message\" />\n<button id=\"send\">Send</button>\n\n<script>\n  const CHANNEL = 'chat:lobby';\n  const REWIND = 50; // messages recovered on each (re)connection\n  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n  const seen = new Set(); // ids already shown: rewind can repeat messages\n  let ws = null;\n  let attempt = 0;\n\n  function show(text) {\n    const li = document.createElement('li');\n    li.textContent = text;\n    document.getElementById('log').append(li);\n  }\n\n  async function connect() {\n    // A new token on every connection: covers expiry and close 4001\n    const res = await fetch('/realtime-token', { method: 'POST' });\n    if (!res.ok) return retry();\n    const { connect_url, retry_after_ms } = await res.json();\n    if (retry_after_ms) await sleep(retry_after_ms);\n\n    ws = new WebSocket(connect_url); // connect_url already carries ?token=\n\n    ws.onmessage = (event) => {\n      const frame = JSON.parse(event.data);\n      if (frame.action === 'connected') {\n        attempt = 0;\n        ws.send(JSON.stringify({ action: 'subscribe', channel: CHANNEL, rewind: REWIND }));\n      } else if (frame.action === 'message') {\n        if (seen.has(frame.id)) return;\n        seen.add(frame.id);\n        show(`${frame.client_id ?? '?'}: ${JSON.stringify(frame.data)}`);\n      } else if (frame.action === 'error') {\n        console.warn('realtime error', frame.code, frame.message);\n      }\n    };\n\n    ws.onclose = (event) => {\n      // 4003: Realtime disabled for the account, do not reconnect.\n      if (event.code === 4003) return;\n      // 1012: node restarting, reconnect now. 1013 and 4029: backoff.\n      // 4001: invalid token, the next connect() already issues another one.\n      retry(event.code === 1012 ? 0 : undefined);\n    };\n  }\n\n  async function retry(delay) {\n    attempt += 1;\n    // exponential backoff with jitter, capped at 30 s\n    const wait = delay ?? Math.random() * Math.min(30000, 500 * 2 ** attempt);\n    await sleep(wait);\n    connect();\n  }\n\n  document.getElementById('send').onclick = () => {\n    const input = document.getElementById('text');\n    if (ws?.readyState !== WebSocket.OPEN || !input.value) return;\n    ws.send(JSON.stringify({ action: 'publish', channel: CHANNEL, data: { text: input.value } }));\n    input.value = '';\n  };\n\n  connect();\n</script>\n```"},{"anchor":"protocolo","heading":"Protocole","markdown":"Les messages de la connexion sont des frames texte JSON, chacun avec un champ `action`. Après la connexion, attendez le frame `connected` avant de vous abonner. Le nom du canal suit la règle `^[A-Za-z0-9:_\\-.]{1,128}$`."},{"anchor":"du-client-vers-le-serveur","heading":"Du client vers le serveur","markdown":"| `action`      | Champs                            | Effet                                                                                                                                                                                                              |\n| ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `subscribe`   | `channel`, `rewind?`              | S'abonne au canal (exige `subscribe` dans le token). Avec `rewind` (nombre de messages, jusqu'à <Limit name=\"realtimeRewindMax\" />), renvoie les derniers messages du canal avant les nouveaux. Répond `attached`. |\n| `unsubscribe` | `channel`                         | Cesse de recevoir le canal. Répond `detached`.                                                                                                                                                                     |\n| `publish`     | `channel`, `name?`, `data`, `id?` | Publie `data` (JSON) sur le canal (exige `publish`). `name` classe le message. Si vous envoyez un `id`, le serveur répond `ack` avec lui.                                                                          |\n| `presence`    | `op`, `channel`, `data?`, `id?`   | Opérations de [présence](#presenca) : `op` vaut `enter`, `update`, `leave` ou `get`.                                                                                                                               |"},{"anchor":"du-serveur-vers-le-client","heading":"Du serveur vers le client","markdown":"| `action`    | Champs                                                      | Quand                                                                                                                 |\n| ----------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |\n| `connected` | `connection_id`                                             | La connexion est acceptée ; vous pouvez vous abonner.                                                                 |\n| `attached`  | `channel`                                                   | L'abonnement au canal est actif (après le `rewind`, s'il a été demandé).                                              |\n| `detached`  | `channel`                                                   | L'abonnement est terminé.                                                                                             |\n| `message`   | `channel`, `id`, `name?`, `data`, `timestamp`, `client_id?` | Un message du canal, y compris ceux que vous avez publiés vous-même. `timestamp` est un entier en millisecondes Unix. |\n| `ack`       | `id`, `channel?`                                            | La publication (ou l'opération de présence) portant cet `id` a été acceptée.                                          |\n| `presence`  | `op`, `channel`, `member` ou `members`                      | Événement de présence (`enter`, `update`, `leave`) ou réponse à `get` (`sync`).                                       |\n| `error`     | `code`, `message`, `channel?`, `id?`                        | Une opération a échoué. Les `code` sont dans [Erreurs](#erros). La connexion reste ouverte.                           |\n\nExemple d'échange :\n\n```json\n← { \"action\": \"connected\", \"connection_id\": \"c_8f3a1d\" }\n→ { \"action\": \"subscribe\", \"channel\": \"chat:lobby\", \"rewind\": 20 }\n← { \"action\": \"attached\", \"channel\": \"chat:lobby\" }\n→ { \"action\": \"publish\", \"channel\": \"chat:lobby\", \"id\": \"m-1\", \"data\": { \"text\": \"Hello!\" } }\n← { \"action\": \"message\", \"channel\": \"chat:lobby\", \"id\": \"1790338000000-0\", \"data\": { \"text\": \"Hello!\" }, \"timestamp\": 1790338000000, \"client_id\": \"user-42\" }\n← { \"action\": \"ack\", \"id\": \"m-1\" }\n```\n\nL'`id` du `message` est généré par le serveur et sert à écarter les doublons. L'`id` du `publish` n'appartient qu'à vous, pour l'associer à l'`ack`."},{"anchor":"close-codes","heading":"Close codes","markdown":"| Code       | Signification                                                              | Que faire                                                                            |\n| ---------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| `1012`     | Le nœud redémarre (déploiement ou maintenance).                            | Reconnectez-vous tout de suite, avec un petit jitter.                                |\n| `1013`     | Le nœud est plein.                                                         | Reconnectez-vous avec un backoff exponentiel.                                        |\n| `4001`     | Token invalide ou expiré (`realtime.token_invalid`).                       | Émettez un nouveau token et reconnectez-vous ; répéter le même token ne sert à rien. |\n| `4003`     | Realtime désactivé pour le compte (`realtime.disabled`).                   | Ne vous reconnectez pas.                                                             |\n| `4029`     | Le compte a atteint sa limite de connexions (`realtime.connection_limit`). | Reconnectez-vous avec un backoff exponentiel.                                        |\n| Tout autre | Coupure réseau ou erreur inattendue.                                       | Reconnectez-vous avec un backoff exponentiel.                                        |"},{"anchor":"sse","heading":"SSE : lecture sans WebSocket","markdown":"Pour les clients qui ne peuvent pas ouvrir de WebSocket, le même token ouvre un flux `text/event-stream` **en lecture seule**, sur le même serveur que le `connect_url` :\n\n```text\nGET https://<connect_url host>/v1/sse?token=<token>&channel=chat:lobby&channel=alerts&rewind=20\n```\n\n* Indiquez chaque canal dans un `channel=` (ou plusieurs dans `channels=a,b`), jusqu'à <Limit name=\"realtimeChannelsPerConnection\" />. Le token doit avoir `subscribe` sur tous.\n* Chaque `data:` est un frame JSON identique à celui du WebSocket (`connected`, `attached`, `message`, `presence`, `error`) ; des lignes `: ping` maintiennent la connexion.\n* Le flux compte dans la limite de connexions du compte et se termine à l'expiration du token (un frame `error` avec `realtime.token_invalid` le précède) ou au redémarrage du nœud.\n* Pour publier, utilisez l'[API](#publicar) ou un WebSocket.\n\n```js\nconst url = new URL(connectUrl); // the connect_url from the token response\nconst sse = new EventSource(\n  `https://${url.host}/v1/sse?token=${encodeURIComponent(token)}&channel=chat:lobby&rewind=20`,\n);\nsse.onmessage = (event) => {\n  const frame = JSON.parse(event.data);\n  if (frame.action === 'message') console.log(frame.data);\n};\n```"},{"anchor":"presenca","heading":"Présence","markdown":"La présence indique qui est sur un canal à l'instant. Un client ayant la capability `presence` **entre** avec des données libres (`{\"nick\": \"ana\"}`), peut **mettre à jour** ces données et **sort** en quittant le canal ou quand la connexion tombe. Les abonnés du canal reçoivent les événements, et `get` (ou l'API) renvoie la liste complète.\n\n| Frame du client                                                                  | Exige       | Effet                                        |\n| -------------------------------------------------------------------------------- | ----------- | -------------------------------------------- |\n| `{\"action\":\"presence\",\"op\":\"enter\",\"channel\":\"chat:lobby\",\"data\":{…},\"id\":\"p1\"}` | `presence`  | Entre dans la présence ; répond `ack`.       |\n| `{\"action\":\"presence\",\"op\":\"update\",\"channel\":\"chat:lobby\",\"data\":{…}}`          | `presence`  | Met à jour les données du membre.            |\n| `{\"action\":\"presence\",\"op\":\"leave\",\"channel\":\"chat:lobby\"}`                      | `presence`  | Quitte la présence.                          |\n| `{\"action\":\"presence\",\"op\":\"get\",\"channel\":\"chat:lobby\",\"id\":\"p2\"}`              | `subscribe` | Répond `op: \"sync\"` avec tous les `members`. |\n\nLes abonnés reçoivent `{\"action\":\"presence\",\"op\":\"enter|update|leave\",\"channel\":\"chat:lobby\",\"member\":{\"connection_id\":\"…\",\"client_id\":\"user-42\",\"data\":{…},\"timestamp\":1790338000000}}`. Chaque connexion est un membre (le même utilisateur dans deux onglets compte deux fois). Un canal accepte jusqu'à <Limit name=\"realtimePresenceMembers\" /> membres ; au-delà, `realtime.presence_limit`. Si un nœud tombe sans prévenir, ses membres disparaissent de la liste en 30 secondes au plus."},{"anchor":"lister-la-présence-du-canal","heading":"Lister la présence du canal","markdown":"<Endpoint id=\"realtime.presence\" example={{ pathParams: { channel: 'chat:lobby' } }}>\n  Renvoie les membres du canal à cet instant. Utile pour afficher « qui est en ligne » depuis votre serveur, sans ouvrir de connexion.\n</Endpoint>"},{"anchor":"reconexao","heading":"Reconnexion","markdown":"Les connexions tombent : le nœud redémarre, le réseau vacille, le token expire. Toute application doit se reconnecter seule. L'exemple de la section [Connexion depuis le navigateur](#conexao-pelo-navegador) le fait déjà (le [SDK](#sdk) aussi) ; les règles sont celles-ci, et les [close codes](#protocolo) indiquent quand se reconnecter."},{"anchor":"backoff","heading":"Backoff","markdown":"Utilisez un backoff exponentiel **avec jitter** (une valeur aléatoire jusqu'au plafond de la tentative) et un plafond de <Limit name=\"realtimeBackoffMax\" /> : par exemple 0,5 s, 1 s, 2 s, 4 s… Sans jitter, des milliers de clients se reconnectent au même instant après une panne et font retomber le nœud. Remettez le compteur à zéro à la réception de `connected`."},{"anchor":"nouveau-token-et-retry_after_ms","heading":"Nouveau token et `retry_after_ms`","markdown":"Demandez un nouveau token à chaque reconnexion : il est peu coûteux (gratuit) et évite de se connecter avec un token périmé. Si la réponse contient `retry_after_ms`, attendez ce délai avant de vous connecter ; il remplace le backoff de cette tentative. Utilisez toujours le `connect_url` de la réponse la plus récente."},{"anchor":"récupérer-les-messages-avec-rewind","heading":"Récupérer les messages avec `rewind`","markdown":"Une nouvelle connexion ignore ce qui s'est passé pendant son absence. Après `connected`, abonnez-vous de nouveau à chaque canal avec `rewind` : le serveur renvoie les derniers messages avant de livrer les nouveaux, et `attached` marque la fin de la récupération. Comme `rewind` peut répéter des messages déjà affichés, écartez ceux dont l'`id` a déjà été vu. Entrez de nouveau dans la présence de chaque canal où vous étiez.\n\nChoisissez le `rewind` selon le temps que la connexion reste habituellement absente (jusqu'à <Limit name=\"realtimeRewindMax\" /> messages). Si le canal a eu plus de messages que cela pendant la coupure, la différence est perdue : l'historique ne garde que les derniers."},{"anchor":"historico","heading":"Historique","markdown":"Chaque canal conserve les <Limit name=\"realtimeHistoryPageMax\" /> derniers messages publiés, pendant <Limit name=\"realtimeHistoryRetention\" /> au plus. Le `rewind` utilise cet historique ; pour le consulter depuis votre serveur, utilisez l'API."},{"anchor":"consulter-lhistorique-du-canal","heading":"Consulter l'historique du canal","markdown":"<Endpoint id=\"realtime.history\" example={{ pathParams: { channel: 'chat:lobby' }, query: { limit: '50' } }}>\n  Renvoie les messages **du plus ancien au plus récent**, dans `messages` (`id`, `name`, `data`, `timestamp`, `client_id`), avec `channel` et le `limit` appliqué. Il n'y a ni curseur ni pagination : `limit` vaut au plus <Limit name=\"realtimeHistoryPageMax\" /> (la valeur par défaut est le maximum). C'est une lecture mesurée : elle compte dans le [quota quotidien de lectures](/fr/cobranca-e-limites#franquia-diaria) du compte.\n</Endpoint>"},{"anchor":"publicar","heading":"Publier un message depuis le serveur","markdown":"Publier par l'API est utile pour les événements qui naissent dans votre backend (une commande payée, une alerte). Le message parvient aux abonnés comme n'importe quel autre.\n\n<Endpoint id=\"realtime.publish\" example={{ pathParams: { channel: 'chat:lobby' }, body: `{ \"name\": \"message\", \"data\": { \"text\": \"Commande approuvée\" }, \"client_id\": \"systeme\" }` }}>\n  * `data` : n'importe quel JSON, jusqu'à <Limit name=\"realtimeMessageMax\" /> (ajouté au `name`).\n  * `name` (facultatif, 128 caractères au plus) : classe le message.\n  * `client_id` (facultatif) : l'expéditeur que voient les abonnés.\n\n  La réponse contient `id`, `timestamp` (millisecondes Unix) et `charged` : `true` quand le message a dépassé le quota mensuel et a été facturé.\n\n  Avec `X-Dry-Run: true`, l'API valide le canal et la taille, ne publie ni ne facture, et répond `200` avec l'estimation :\n\n  ```json\n  { \"success\": true, \"data\": { \"cost_units\": 0, \"within_quota\": true, \"free_used\": 18250, \"free_monthly\": 200000, \"would_charge\": false } }\n  ```\n\n  `cost_units` utilise l'échelle du backend (10 000 unités = 1 crédit) et vaut `0` dans le quota.\n</Endpoint>"},{"anchor":"canais","heading":"Canaux et statistiques","markdown":""},{"anchor":"lister-les-canaux-actifs","heading":"Lister les canaux actifs","markdown":"<Endpoint id=\"realtime.channels\" example={{ query: { limit: '50' } }}>\n  Liste les canaux ayant des abonnés ou une publication dans la fenêtre de l'historique, avec `name`, `subscribers` (le compte a jusqu'à 10 secondes de retard) et `last_publish_at`. Utilisez `limit` (1 à <Limit name=\"realtimeChannelListPageMax\" />, 50 par défaut) et `offset` ; la réponse contient `total` et, s'il y a d'autres pages, `next_offset` pour l'appel suivant.\n</Endpoint>"},{"anchor":"consulter-lusage-et-les-connexions","heading":"Consulter l'usage et les connexions","markdown":"<Endpoint id=\"realtime.stats\">\n  Affiche les `connections` ouvertes en ce moment (additionnées sur tous les nœuds), les `active_channels` (avec au moins un abonné), le `month` (UTC) et les `messages_this_month`, avec le quota : `free_monthly_messages` et `free_remaining`.\n</Endpoint>"},{"anchor":"webhooks-de-canais","heading":"Webhooks de canaux","markdown":"Chaque compte peut avoir **un** webhook de canaux, qui prévient votre serveur quand un canal devient occupé ou vide, quand quelqu'un entre ou sort de la présence et (facultativement) à chaque message. C'est un réglage de Realtime, distinct des [webhooks du compte](/fr/webhooks)."},{"anchor":"consulter-le-webhook-de-canaux","heading":"Consulter le webhook de canaux","markdown":"<Endpoint id=\"realtime.webhook.get\">\n  Renvoie la configuration actuelle avec le `secret` et la `standard_key` (le `secret` au format `whsec_…`, pour les bibliothèques Standard Webhooks). Sans webhook configuré, répond `404`.\n</Endpoint>"},{"anchor":"configurer-le-webhook-de-canaux","heading":"Configurer le webhook de canaux","markdown":"<Endpoint id=\"realtime.webhook.put\" example={{ body: `{ \"url\": \"https://example.com/hooks/realtime\", \"events\": [\"channel.occupied\", \"channel.vacated\", \"channel.message\"], \"channels\": [\"chat:*\"], \"enabled\": true }` }}>\n  * `url` : URL publique `http` ou `https` ; les adresses de réseau interne ou privé sont refusées.\n  * `events` : au moins un des événements du tableau ci-dessous.\n  * `channels` : motifs de canal (un nom, un préfixe avec `*` ou `*`), jusqu'à <Limit name=\"realtimeWebhookChannelsMax\" />. Vide signifie tous les canaux, **sauf** `channel.message` : les messages sont en opt-in et exigent au moins un motif.\n  * `enabled` (facultatif, `true` par défaut) : `false` suspend les envois sans supprimer la configuration.\n\n  L'appel crée le webhook ou remplace l'actuel, et le `secret` est conservé. La configuration est lue en cache pendant 30 secondes au plus.\n</Endpoint>"},{"anchor":"changer-le-secret-du-webhook","heading":"Changer le secret du webhook","markdown":"<Endpoint id=\"realtime.webhook.rotate\">\n  Génère un nouveau `secret` (et la `standard_key` correspondante). L'ancien cesse d'être valable ; mettez à jour votre récepteur aussitôt.\n</Endpoint>"},{"anchor":"supprimer-le-webhook-de-canaux","heading":"Supprimer le webhook de canaux","markdown":"<Endpoint id=\"realtime.webhook.delete\">\n  Supprime la configuration ; plus aucun événement n'est envoyé.\n</Endpoint>"},{"anchor":"événements","heading":"Événements","markdown":"<WebhookEventsTable source=\"realtime\" />"},{"anchor":"livraison","heading":"Livraison","markdown":"La plateforme envoie un `POST` JSON à votre URL :\n\n```json\n{\n  \"id\": \"evt_5b0c9a7e-3c1f-4a52-9d57-2a6f1f0e8a11\",\n  \"type\": \"presence.member_added\",\n  \"timestamp\": 1790338000000,\n  \"account_id\": \"7a1d4c8e-2f6b-4e9a-b3c5-0d8f1a6e4b27\",\n  \"data\": {\n    \"channel\": \"chat:lobby\",\n    \"member\": { \"connection_id\": \"c_8f3a1d\", \"client_id\": \"user-42\", \"data\": { \"nick\": \"ana\" }, \"timestamp\": 1790338000000 }\n  }\n}\n```\n\n`data.channel` est présent dans tous les événements ; `data.member` accompagne les événements de présence et `data.message` (`id`, `name`, `data`, `timestamp`, `client_id`) accompagne `channel.message`. Le `timestamp` est en millisecondes Unix.\n\n* **Signature :** Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature`), avec la `standard_key` de la configuration ; voir [Valider la signature](/fr/webhooks#verificar-assinatura).\n* **Tentatives :** jusqu'à 3 (attentes de 1 s et 5 s) en cas d'erreur réseau, de `5xx` ou de `429` ; les autres réponses mettent fin à la livraison. Chaque tentative attend <Limit name=\"realtimeWebhookTimeout\" /> au plus.\n* **Traitement :** la livraison est asynchrone, par 4 workers ; si la file se remplit, l'événement est abandonné. Traitez le webhook comme une notification et utilisez l'`id` pour écarter les doublons."},{"anchor":"sdk","heading":"SDK JavaScript","markdown":"Le paquet `@respawn/realtime` implémente le protocole ci-dessus, sans dépendances, pour le navigateur et Node 22 ou plus récent. Il renouvelle le token 30 secondes avant son expiration, se reconnecte selon les [close codes](#protocolo), reprend les canaux avec `rewind` (100 messages, sans répéter ceux déjà livrés) et réintègre la présence.\n\n```bash\nnpm install @respawn/realtime\n```"},{"anchor":"côté-serveur","heading":"Côté serveur","markdown":"`RealtimeRest` utilise la clé d'API (ne l'envoyez jamais au navigateur) pour émettre des tokens, publier et lire l'historique et la présence. Passez `baseUrl` avec l'adresse de l'API :\n\n```ts\nimport { RealtimeRest } from '@respawn/realtime';\n\nconst rest = new RealtimeRest({\n  apiKey: process.env.RESPAWN_API_TOKEN!,\n  baseUrl: 'https://api.respawntech.dev',\n});\n\n// route the browser calls (Express): returns token, expires_at and connect_url\napp.post('/realtime/token', async (req, res) => {\n  res.json(\n    await rest.createToken({\n      client_id: req.user.id,\n      capabilities: { 'room:*': ['subscribe', 'publish', 'presence'] },\n      ttl_seconds: 600,\n    }),\n  );\n});\n\nawait rest.publish('room:1', 'order', { id: 42, status: 'paid' });\nconst history = await rest.history('room:1', { limit: 20 });\nconst members = await rest.presence('room:1');\n```"},{"anchor":"dans-le-navigateur","heading":"Dans le navigateur","markdown":"`Realtime` demande le token à votre serveur par `authUrl` (un `POST` avec cookies) ou par `authCallback` :\n\n```ts\nimport { Realtime } from '@respawn/realtime';\n\nconst rt = new Realtime({ authUrl: '/realtime/token' });\nrt.on('state', (state, err) => console.log('connection:', state, err?.code));\n\nconst room = rt.channel('room:1');\n\nawait room.subscribe((m) => console.log(m.name, m.data), { rewind: 20 });\nawait room.publish('chat', { text: 'hi' });\n\nawait room.presence.watch((members) => render(members));\nawait room.presence.enter({ nick: 'ana' });\n```\n\nLes états sont `initialized`, `connecting`, `connected`, `disconnected`, `closed` (vous avez appelé `close()`) et `failed` (par exemple, close `4003`). Si le temps hors ligne a dépassé la fenêtre de l'historique, le canal émet l'événement `gap`. Les publications faites hors connexion sont mises en file ; une publication déjà envoyée quand la connexion tombe échoue avec `sdk.connection_lost` (elle a pu être publiée ou non)."},{"anchor":"react","heading":"React","markdown":"Avec `@respawn/realtime/react` :\n\n```tsx\nimport { useState } from 'react';\nimport { Realtime } from '@respawn/realtime';\nimport { RealtimeProvider, useChannel, usePresence } from '@respawn/realtime/react';\n\nconst rt = new Realtime({ authUrl: '/realtime/token' });\n\nexport function App() {\n  return (\n    <RealtimeProvider client={rt}>\n      <Room />\n    </RealtimeProvider>\n  );\n}\n\nfunction Room() {\n  const [msgs, setMsgs] = useState<{ id: string; data: unknown }[]>([]);\n  const { publish } = useChannel('room:1', (m) => setMsgs((all) => [...all, m]), { rewind: 20 });\n  const { members } = usePresence('room:1', { nick: 'ana' });\n\n  return (\n    <div>\n      <p>{members.length} online</p>\n      <ul>{msgs.map((m) => <li key={m.id}>{JSON.stringify(m.data)}</li>)}</ul>\n      <button onClick={() => publish('chat', { text: 'hi' })}>Send</button>\n    </div>\n  );\n}\n```"},{"anchor":"limites","heading":"Limites","markdown":"| Limite                                  | Valeur                                                                                                                                |\n| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |\n| Messages gratuits par mois (par compte) | <Limit name=\"realtimeFreeMessages\" />                                                                                                 |\n| Taille d'un message (`data`)            | <Limit name=\"realtimeMessageMax\" />                                                                                                   |\n| Longueur du nom du canal                | <Limit name=\"realtimeChannelNameMax\" /> caractères                                                                                    |\n| Canaux souscrits par connexion          | <Limit name=\"realtimeChannelsPerConnection\" />                                                                                        |\n| Publications par seconde (par compte)   | <Limit name=\"realtimePublishPerSecond\" />                                                                                             |\n| Connexions simultanées (par compte)     | <Limit name=\"realtimeConnectionsPerAccount\" />                                                                                        |\n| Membres dans la présence d'un canal     | <Limit name=\"realtimePresenceMembers\" />                                                                                              |\n| Validité du token                       | <Limit name=\"realtimeTokenTtlDefault\" /> (par défaut), de <Limit name=\"realtimeTokenTtlMin\" /> à <Limit name=\"realtimeTokenTtlMax\" /> |\n| Motifs de canal par token               | jusqu'à <Limit name=\"realtimeTokenPatternsMax\" />                                                                                     |\n| Rétention de l'historique               | <Limit name=\"realtimeHistoryRetention\" />                                                                                             |\n| Messages conservés et par `rewind`      | jusqu'à <Limit name=\"realtimeHistoryPageMax\" />                                                                                       |\n| Canaux par page (`GET /channels`)       | jusqu'à <Limit name=\"realtimeChannelListPageMax\" />                                                                                   |\n| Motifs de canal du webhook              | jusqu'à <Limit name=\"realtimeWebhookChannelsMax\" />                                                                                   |\n\nLes valeurs sont les valeurs par défaut de la plateforme, qui peut les ajuster. Les requêtes à l'API suivent aussi la [limite de requêtes](/fr/cobranca-e-limites) du compte."},{"anchor":"erros","heading":"Erreurs","markdown":"Les erreurs de l'API REST (`realtime.channel_invalid`, `realtime.message_too_large`…) arrivent sous forme de réponse HTTP, au format décrit dans [Erreurs](/fr/erros). Sur la connexion WebSocket ou SSE, les mêmes codes arrivent dans un frame `error` (la connexion reste ouverte) ou par un close code (voir [Protocole](#protocolo)).\n\n<ErrorTable tool=\"realtime\" />\n\nLes erreurs de validation courantes (`validation.failed`, `not_found`) sont dans [Erreurs](/fr/erros)."},{"anchor":"preco","heading":"Tarifs","markdown":"<PricingTable tool=\"realtime\" group=\"per-message\" />\n\n<PricingTable tool=\"realtime\" group=\"free-quota\" />\n\nLa facturation se fait par message publié, une seule fois, pour les messages qui dépassent le quota mensuel ; le nombre d'abonnés ne change pas le coût. Émettre des tokens, garder des connexions ouvertes, utiliser la présence, lister les canaux et configurer le webhook sont gratuits. La consultation de l'historique est une lecture mesurée : elle compte dans le quota quotidien de lectures puis, au-delà, coûte la valeur du [tableau des lectures](/fr/cobranca-e-limites#franquia-diaria). Sans solde pour un message hors quota, la publication échoue avec `realtime.insufficient_credits`."}]},{"slug":"ferramentas/verificacao-de-url","url":"https://docs.respawntech.dev/fr/ferramentas/verificacao-de-url","title":"Vérification d'URL","description":"Vérifiez si une URL est malveillante et récupérez le favicon d'un domaine via un proxy.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"verificacao-de-url\" />"},{"anchor":"vue-densemble","heading":"Vue d'ensemble","markdown":"À utiliser avant d'afficher des liens générés par des utilisateurs ou de rediriger quelqu'un vers une destination externe. Il s'agit de la même vérification que celle appliquée par le [QR Dynamique](/fr/ferramentas/qr-dinamico) aux URL de destination.\n\n<EndpointList tool=\"verificacao-de-url\" />"},{"anchor":"custos","heading":"Coûts","markdown":"Les deux opérations sont des lectures mesurées : elles sont décomptées du [quota gratuit quotidien](/fr/cobranca-e-limites#franquia-diaria) puis, au-delà, débitent le montant indiqué sur chaque endpoint."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"vérifier-une-url","heading":"Vérifier une URL","markdown":"<Endpoint id=\"safety.check\" example={{ query: { url: 'https://example.com' } }}>\n  Query : `url` (obligatoire, URL absolue). Les résultats sont mis en cache pendant 1 heure.\n\n  <Callout type=\"warn\" title=\"Réponse hors de l'enveloppe standard\">\n    Cette route renvoie directement `{ \"is_safe\": … }`, sans `success`/`data`. Si le service de vérification est indisponible, la réponse est `is_safe: true` (*fail-open*), afin de ne pas bloquer votre flux. En dry-run, la réponse revient à l'enveloppe standard.\n  </Callout>\n</Endpoint>"},{"anchor":"favicon-dun-domaine","heading":"Favicon d'un domaine","markdown":"<Endpoint id=\"safety.favicon\" example={{ query: { domain: 'example.com' }, response: 'binary' }}>\n  Query : `domain` (obligatoire).\n</Endpoint>"},{"anchor":"erreurs","heading":"Erreurs","markdown":"<ErrorTable tool=\"verificacao-de-url\" />"}]},{"slug":"ferramentas/web-to-markdown","url":"https://docs.respawntech.dev/fr/ferramentas/web-to-markdown","title":"Page web vers Markdown","description":"Téléchargez une page web et recevez son contenu principal en Markdown, avec la possibilité de l'enregistrer dans la base de connaissances.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"web-to-markdown\" />"},{"anchor":"vue-densemble","heading":"Vue d'ensemble","markdown":"Indiquez une URL et recevez le texte principal de la page en Markdown, avec titre, auteur et nombre de mots. Avec `collection_id`, le contenu est aussi enregistré dans une [base de connaissances](/fr/ferramentas/base-de-conhecimento). Également disponible dans le [MCP](/fr/agentes-de-ia) sous le nom `web_to_markdown`.\n\n<EndpointList tool=\"web-to-markdown\" />"},{"anchor":"custos","heading":"Coûts","markdown":"La conversion est une lecture mesurée : elle est décomptée du [quota gratuit quotidien](/fr/cobranca-e-limites#franquia-diaria) puis, au-delà, débite le montant indiqué sur l'endpoint. Lors de l'enregistrement dans une base, l'écriture est facturée comme une ingestion de la [base de connaissances](/fr/ferramentas/base-de-conhecimento)."},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"convertir-une-page-en-markdown","heading":"Convertir une page en Markdown","markdown":"<Endpoint id=\"web.markdown\" example={{ body: `{ \"url\": \"https://example.com/blog/post\" }` }}>\n  `url` (obligatoire) est l'adresse `http` ou `https` de la page. `collection_id` (optionnel) enregistre le contenu dans cette base et exige aussi le scope `tools:rag:manage`.\n\n  La réponse contient `url`, `final_url` (après redirections), `title`, `byline`, `markdown` et `word_count`. Avec `collection_id`, elle contient aussi `document` (`document_id`, `status` et `duplicate`) et, en dry-run, `estimate`.\n\n  <Callout type=\"warn\" title=\"HTML statique uniquement\">\n    La page est lue telle que le serveur la livre, sans exécuter JavaScript : les pages qui construisent leur contenu dans le navigateur répondent `tools.web.no_content`. Les adresses internes et privées sont refusées (`tools.web.blocked_url`).\n  </Callout>\n</Endpoint>"},{"anchor":"erreurs","heading":"Erreurs","markdown":"<ErrorTable tool=\"web-to-markdown\" />"}]},{"slug":"ferramentas/webhook-relay","url":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay","title":"Webhook Relay","description":"Envoyez des webhooks signés à vos clients, avec relances automatiques, journal des tentatives et replay, et redistribuez les webhooks de Stripe, GitHub et d’autres fournisseurs.","sections":[{"anchor":"top","heading":"","markdown":"<ToolHero tool=\"webhook-relay\" />"},{"anchor":"visao-geral","heading":"Vue d'ensemble","markdown":"Webhook Relay est l’infrastructure de webhooks de votre application. Au lieu de gérer files d’attente, relances et signatures, vous publiez un événement en un appel et la plateforme le livre à chaque endpoint abonné, avec le même moteur que les [webhooks du compte](/fr/webhooks).\n\n* **Projet :** regroupe endpoints, sources, routes, types d’événement et messages, pour séparer les configurations (une boutique, un client, un environnement). Chaque compte a un projet par défaut ; voir [Projets](#projetos).\n* **Endpoint :** l’URL qui reçoit les événements (généralement celle de votre client, ou un canal Slack, Discord ou Teams), avec les événements auxquels elle est abonnée et son propre `secret`.\n* **Message :** un événement que vous publiez (`event_type` + `payload`).\n* **Livraison :** un message en route vers un endpoint. Un message vers 3 endpoints crée 3 livraisons, chacune avec ses relances.\n* **Tentative :** chaque appel HTTP d’une livraison, journalisé avec le statut, la latence et le début de la réponse.\n* **Source et route :** pour **recevoir** les webhooks de tiers (Stripe, GitHub…) sur une URL de la plateforme et les redistribuer vers vos endpoints. Voir [Recevoir des webhooks de tiers](#ingestao).\n\nToutes les requêtes suivent le standard [Standard Webhooks](https://www.standardwebhooks.com) (en-têtes `webhook-id`, `webhook-timestamp` et `webhook-signature`). Le format, la vérification de la signature et le calendrier des relances sont décrits dans le guide [Webhooks](/fr/webhooks)."},{"anchor":"primeiros-passos","heading":"Premiers pas","markdown":"<Steps>\n  <Step>\n    **Créez un endpoint** avec l’URL de destination et les événements auxquels il s’abonne. Conservez le `secret` de la réponse (utilisez `standard_webhooks_secret` avec les bibliothèques officielles). Sans `project_id`, il va dans le projet par défaut.\n  </Step>\n\n  <Step>\n    **Testez la connexion** avec le [ping de test](#webhooks-endpoints-test) : un événement `webhook.test` gratuit.\n  </Step>\n\n  <Step>\n    **Envoyez un message** avec `event_type` et `payload`. Utilisez d’abord `X-Dry-Run: true` pour voir combien de livraisons il crée et ce qu’il coûte.\n  </Step>\n\n  <Step>\n    **Vérifiez la signature** côté récepteur et répondez `2xx` rapidement. Voir les exemples dans [Valider la signature](/fr/webhooks#verificar-assinatura).\n  </Step>\n\n  <Step>\n    **Suivez** les livraisons dans le [journal des messages](#mensagens) et renvoyez ce qui a échoué.\n  </Step>\n</Steps>\n\n<EndpointList tool=\"webhook-relay\" />"},{"anchor":"custos","heading":"Coûts","markdown":"<PricingTable tool=\"webhook-relay\" group=\"per-delivery\" />\n\n<PricingTable tool=\"webhook-relay\" group=\"free-quota\" />\n\nLa facturation a lieu à l’envoi, par message, pour les livraisons au-delà du quota (`charged_units`, en unités de 1/10 000 de crédit). Si le message ne peut pas être enregistré, le montant est remboursé. Le `payload` est validé avant toute facturation.\n\nLe quota gratuit et la facturation sont **par compte** : tous les projets partagent le même quota mensuel, et une livraison coûte le même prix dans n’importe quel projet.\n\n<Callout title=\"Lectures du journal\">\n  Configurer projets, endpoints, types d’événement, sources et routes est gratuit. Les routes pour [lister](#webhooks-messages-list) et [consulter](#webhooks-messages-get) les messages sont des lectures mesurées : elles comptent dans le [quota quotidien de lectures](/fr/cobranca-e-limites#franquia-diaria) et, au-delà, coûtent la valeur du tableau des lectures.\n</Callout>"},{"anchor":"consulter-lusage-du-mois","heading":"Consulter l’usage du mois","markdown":"<Endpoint id=\"webhooks.usage\">\n  Indique le mois en cours (UTC, de `period_start` à `period_end`) : le quota gratuit (`free_deliveries`), les livraisons déjà utilisées par l’API et l’ingestion (`used_deliveries`) et ce qui reste (`free_left`). `delivery_units` et `read_units` sont les prix actuels d’une livraison et d’une lecture du journal, en unités ; `blocked_retention_days` est la durée pendant laquelle une livraison attend des crédits (voir [Sans crédits](#sem-creditos)).\n</Endpoint>"},{"anchor":"projetos","heading":"Projets","markdown":"Tout ce que vous configurez dans Webhook Relay appartient à un projet : endpoints, sources (et leurs routes), types d’événement, messages et la disposition du canvas dans le tableau de bord. Utilisez les projets pour séparer les configurations, par exemple une par boutique ou par client.\n\n* Chaque compte a un **projet par défaut** (`is_default: true`, nommé `Default`), créé à la première utilisation. Il ne peut pas être supprimé.\n* `project_id` est **facultatif** dans toute l’API : sans lui, les créations et les envois utilisent le projet par défaut. Les intégrations qui ignorent les projets continuent de fonctionner.\n* Un message n’atteint que les endpoints de son projet et est validé avec les types d’événement de ce projet. Les noms de type d’événement sont uniques par projet.\n* Les listes (`GET /endpoints`, `/sources`, `/event-types`, `/routes` et `/messages`) acceptent `?project_id=` comme filtre ; sans lui, elles renvoient tous les projets.\n* Les événements de la plateforme (ex. : `qr.dynamic.scanned`) créent un message dans chaque projet qui a des endpoints abonnés, toujours gratuitement. Les callbacks de jobs (`webhook_url`) n’appartiennent à aucun projet (`project_id: null`).\n* Les limites et la facturation restent par compte : voir [Limites](#limites). Par défaut, le nombre de projets n’est pas limité."},{"anchor":"lister-les-projets","heading":"Lister les projets","markdown":"<Endpoint id=\"webhooks.projects.list\" example={{ query: { q: 'boutique', page: '1', page_size: '20' } }}>\n  Paginé et avec recherche : `q` cherche dans le nom (sans tenir compte de la casse), `page` commence à 1 et `page_size` va jusqu’à <Limit name=\"webhookProjectsPageMax\" /> (<Limit name=\"webhookProjectsPageDefault\" /> par défaut). Chaque projet porte `stats` : sources, endpoints, messages des dernières 24 heures, livraisons du mois et `failing_endpoints` (endpoints en échec ou désactivés automatiquement ; un endpoint que vous avez mis en pause ne compte pas).\n</Endpoint>"},{"anchor":"créer-un-projet","heading":"Créer un projet","markdown":"<Endpoint id=\"webhooks.projects.create\" example={{ body: `{ \"name\": \"Boutique\", \"description\": \"Événements de la boutique en ligne\" }` }}>\n  `name` (obligatoire) fait jusqu’à <Limit name=\"webhookProjectNameMax\" /> caractères ; `description`, jusqu’à <Limit name=\"webhookProjectDescriptionMax\" />.\n</Endpoint>"},{"anchor":"consulter-un-projet","heading":"Consulter un projet","markdown":"<Endpoint id=\"webhooks.projects.get\" />"},{"anchor":"modifier-un-projet","heading":"Modifier un projet","markdown":"<Endpoint id=\"webhooks.projects.update\" example={{ body: `{ \"name\": \"Boutique principale\" }` }}>\n  Envoyez uniquement les champs à modifier.\n</Endpoint>"},{"anchor":"supprimer-un-projet","heading":"Supprimer un projet","markdown":"<Endpoint id=\"webhooks.projects.delete\">\n  Supprime les endpoints du projet (les livraisons en attente sont annulées), les sources, les routes, les types d’événement et la disposition. Les messages restent dans le journal global, avec `project_id: null`.\n</Endpoint>"},{"anchor":"dupliquer-un-projet","heading":"Dupliquer un projet","markdown":"<Endpoint id=\"webhooks.projects.duplicate\" example={{ body: `{ \"name\": \"Boutique (copie)\" }` }}>\n  Crée un nouveau projet avec la même description et des copies de tout, déjà reliées entre elles :\n\n  * **Endpoints** avec de **nouveaux secrets** (même URL, événements, en-têtes, débit d’envoi, format et état actif ; l’historique d’échecs n’est pas copié).\n  * **Sources** avec de **nouvelles `ingest_url`** (mêmes réglages et même secret du fournisseur). Enregistrez les nouvelles URL chez le fournisseur.\n  * Routes, types d’événement et disposition du canvas.\n\n  Les messages, livraisons et compteurs ne sont pas copiés. Les limites du compte sont vérifiées d’abord : si la copie en dépasserait une, la réponse est `webhook.limit_reached` et rien n’est créé.\n</Endpoint>"},{"anchor":"endpoints","heading":"Endpoints","markdown":""},{"anchor":"créer-un-endpoint","heading":"Créer un endpoint","markdown":"<Endpoint\n  id=\"webhooks.endpoints.create\"\n  example={{ body: `{\n\"url\": \"https://api.example.com/hooks/respawn\",\n\"description\": \"Commandes de la boutique\",\n\"events\": [\"order.paid\", \"order.refunded\"],\n\"custom_headers\": { \"X-Tenant\": \"boutique-1\" }\n}` }}\n>\n  * `url` (obligatoire) : URL publique `http` ou `https`. Les adresses privées, locales ou de métadonnées cloud sont refusées (`webhook.invalid_url`).\n  * `events` : types d’événement reçus par l’endpoint depuis les messages de l’API et de la plateforme ; `[\"*\"]` abonne à tous. Sans événements, l’endpoint ne reçoit que ce que les [routes](#ingestao) lui envoient.\n  * `project_id` : projet de l’endpoint (par défaut : le projet par défaut). Un endpoint ne change pas de projet.\n  * `format` : `raw` (par défaut), `slack`, `discord` ou `teams`. Voir [Slack, Discord et Teams](#formatos).\n  * `custom_headers` : jusqu’à <Limit name=\"webhookCustomHeadersMax\" /> en-têtes supplémentaires envoyés à chaque livraison (ex. : un jeton de votre client). Les en-têtes de signature, `Content-Type`, `User-Agent` et `Host` ne peuvent pas être remplacés.\n  * `rate_limit_per_sec` : plafond d’envois par seconde pour cet endpoint (`0` = pas de plafond propre).\n  * Le `secret` n’est renvoyé que dans cette réponse ; ensuite, utilisez [Révéler le secret](#webhooks-endpoints-secret). Jusqu’à <Limit name=\"webhookEndpointsMax\" /> endpoints par compte, tous projets confondus.\n</Endpoint>"},{"anchor":"lister-les-endpoints","heading":"Lister les endpoints","markdown":"<Endpoint id=\"webhooks.endpoints.list\" example={{ query: { project_id: '3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53' } }}>\n  Filtre facultatif par `project_id`. Un endpoint désactivé automatiquement porte `is_active: false` et `disabled_reason` ; un endpoint en échec porte `failing_since`.\n</Endpoint>"},{"anchor":"consulter-un-endpoint","heading":"Consulter un endpoint","markdown":"<Endpoint id=\"webhooks.endpoints.get\" />"},{"anchor":"modifier-un-endpoint","heading":"Modifier un endpoint","markdown":"<Endpoint id=\"webhooks.endpoints.update\" example={{ body: `{ \"events\": [\"*\"], \"is_active\": true }` }}>\n  Envoyez uniquement les champs à modifier (y compris `format`). `is_active: true` réactive un endpoint désactivé et remet à zéro sa série d’échecs.\n</Endpoint>"},{"anchor":"supprimer-un-endpoint","heading":"Supprimer un endpoint","markdown":"<Endpoint id=\"webhooks.endpoints.delete\">\n  Les livraisons en attente vers l’endpoint sont annulées.\n</Endpoint>"},{"anchor":"révéler-le-secret","heading":"Révéler le secret","markdown":"<Endpoint id=\"webhooks.endpoints.secret\">\n  `secret` est la clé brute (HMAC de l’en-tête historique `X-Webhook-Signature`) ; `standard_webhooks_secret` est la même clé au format `whsec_…` accepté par les bibliothèques Standard Webhooks.\n</Endpoint>"},{"anchor":"renouveler-le-secret","heading":"Renouveler le secret","markdown":"<Endpoint id=\"webhooks.endpoints.rotate_secret\">\n  Pendant <Limit name=\"webhookSecretOverlap\" />, les livraisons portent deux signatures dans `webhook-signature` (nouveau et ancien secret), pour changer le secret du récepteur sans perdre d’événements.\n</Endpoint>"},{"anchor":"envoyer-un-ping-de-test","heading":"Envoyer un ping de test","markdown":"<Endpoint id=\"webhooks.endpoints.test\">\n  Envoie un événement `webhook.test` signé, gratuit et hors quota. Suivez le résultat dans [Consulter un message](#webhooks-messages-get) avec le `message_id` renvoyé.\n</Endpoint>"},{"anchor":"rejouer-les-livraisons-en-échec","heading":"Rejouer les livraisons en échec","markdown":"<Endpoint id=\"webhooks.endpoints.recover\" example={{ query: { since: '2026-09-24T00:00:00Z' } }}>\n  Renvoie à cet endpoint toutes les livraisons terminées en échec (`failed`, `exhausted` ou `canceled`) depuis `since` (RFC 3339), jusqu’à <Limit name=\"webhookRecoverMax\" /> par appel. À utiliser après avoir réparé un récepteur resté hors ligne. Chaque livraison rejouée est facturée comme une nouvelle.\n</Endpoint>"},{"anchor":"formatos","heading":"Slack, Discord et Teams","markdown":"Avec `format` à `slack`, `discord` ou `teams`, enregistrez comme `url` le webhook entrant du canal (Incoming Webhook Slack ou Teams, webhook Discord). La plateforme transforme chaque livraison en message texte au moment de l’envoi :\n\n* Le corps devient `{\"text\": \"…\"}` (Slack et Teams) ou `{\"content\": \"…\"}` (Discord), avec `Content-Type: application/json`.\n* Le texte contient le type d’événement en gras (et le nom de la source, pour les messages reçus d’un fournisseur), suivi du corps dans un bloc de code : JSON mis en forme ou texte brut.\n* Les textes longs sont coupés avec `…` pour tenir dans la limite de la plateforme : <Limit name=\"webhookSlackTextMax\" /> caractères sur Slack, <Limit name=\"webhookDiscordTextMax\" /> sur Discord et <Limit name=\"webhookTeamsTextMax\" /> sur Teams.\n* Les en-têtes de signature sont toujours envoyés, calculés sur ce corps ; les en-têtes d’origine d’une source avec `forward_original_headers` ne sont pas transmis.\n\nLe message enregistré dans le journal ne change pas : le journal, le renvoi et la récupération fonctionnent comme pour les endpoints `raw`."},{"anchor":"mensagens","heading":"Messages","markdown":""},{"anchor":"envoyer-un-message","heading":"Envoyer un message","markdown":"<Endpoint\n  id=\"webhooks.messages.send\"\n  example={{ headers: { 'X-Dry-Run': 'true' }, body: `{\n\"event_type\": \"order.paid\",\n\"payload\": { \"order_id\": \"cmd_1042\", \"amount\": 199.9 },\n\"idempotency_key\": \"order-paid-cmd_1042\"\n}` }}\n>\n  * `event_type` (obligatoire) : `^[a-z0-9][a-z0-9._-]{0,127}$`, ex. `order.paid`. Il part vers les endpoints du projet abonnés à ce type (ou à `*`).\n  * `payload` (obligatoire) : n’importe quel JSON, jusqu’à <Limit name=\"webhookPayloadMax\" />. Si le type d’événement a un [JSON Schema](#tipos-de-evento) dans le projet, le payload doit le respecter (`webhook.payload_invalid`, sans facturation).\n  * `project_id` (facultatif) : projet du message (par défaut : le projet par défaut).\n  * `endpoint_ids` (facultatif) : limite l’envoi à ces endpoints, parmi ceux du projet abonnés à l’événement.\n  * `idempotency_key` (ou l’en-tête `X-Idempotency-Key`) : répéter la clé renvoie le premier message avec `duplicate: true` et ne facture rien. Utile pour renvoyer sans risque après un timeout.\n  * Avec `X-Dry-Run: true`, rien n’est envoyé ni facturé : la réponse indique `deliveries`, `free_deliveries` et `charged_units`.\n\n  Le récepteur reçoit l’enveloppe `{ \"event_id\", \"type\", \"timestamp\", \"payload\" }` décrite dans [Webhooks](/fr/webhooks#corpo) ; `event_id` est le `message_id` et `webhook-id` vaut `msg_<message_id>`.\n</Endpoint>"},{"anchor":"envoyer-des-messages-par-lot","heading":"Envoyer des messages par lot","markdown":"<Endpoint\n  id=\"webhooks.messages.batch\"\n  example={{ body: `{\n\"messages\": [\n  { \"event_type\": \"order.paid\", \"payload\": { \"order_id\": \"cmd_1042\", \"amount\": 199.9 }, \"idempotency_key\": \"order-paid-cmd_1042\" },\n  { \"event_type\": \"order.paid\", \"payload\": { \"order_id\": \"cmd_1043\" }, \"idempotency_key\": \"order-paid-cmd_1043\" }\n]\n}` }}\n>\n  Jusqu’à <Limit name=\"webhookBatchMax\" /> messages, chacun avec les mêmes champs qu’un envoi unitaire. Le `project_id` se place au premier niveau du body et vaut pour tous les messages du lot. Chaque message est validé, dédupliqué et facturé séparément : un échec apparaît dans `error` sur son élément sans bloquer les autres. Accepte `X-Dry-Run: true`.\n</Endpoint>"},{"anchor":"lister-les-messages","heading":"Lister les messages","markdown":"<Endpoint id=\"webhooks.messages.list\" example={{ query: { event_type: 'order.paid', failed: 'true', limit: '20' } }}>\n  Les plus récents d’abord, avec les livraisons de chaque message (sans le corps). Pagination : passez le `next_before` reçu dans `before`. `limit` de 1 à 100 (20 par défaut). Les messages restent dans le journal pendant <Limit name=\"webhookLogRetention\" />.\n\n  * `project_id` : seulement les messages de ce projet. Sans lui, le journal renvoie tous les projets, plus les messages sans projet (`project_id: null` : callbacks de jobs et messages de projets supprimés).\n  * `event_type` et `endpoint_id` : filtres exacts.\n  * `failed=true` : seulement les messages ayant au moins une livraison `failed`, `exhausted` ou `blocked_no_credits`.\n  * `q` : recherche, de 1 à <Limit name=\"webhookSearchQueryMax\" /> caractères, combinable avec les autres filtres. Un UUID trouve le message ayant cet id. Un autre texte trouve la clé d’idempotence ou le type d’événement égaux au texte, ou le texte dans le corps (sans tenir compte de la casse, sauf pour les lettres accentuées) ; la recherche dans le corps ne porte que sur les messages des derniers <Limit name=\"webhookSearchBodyWindow\" />.\n</Endpoint>"},{"anchor":"consulter-un-message","heading":"Consulter un message","markdown":"<Endpoint id=\"webhooks.messages.get\">\n  Renvoie le corps envoyé et, par livraison, chaque tentative : `http_status`, `latency_ms`, le début de la réponse (`response_body`) ou l’erreur réseau (`error`). `next_attempt_at` apparaît tant que la livraison est en attente.\n\n  | `status` de la livraison | Signification                                                                                                           |\n  | ------------------------ | ----------------------------------------------------------------------------------------------------------------------- |\n  | `pending`                | En attente de la prochaine tentative (`next_attempt_at`).                                                               |\n  | `sending`                | Tentative en cours.                                                                                                     |\n  | `succeeded`              | L’endpoint a répondu `2xx`.                                                                                             |\n  | `exhausted`              | Toutes les tentatives ont échoué. Peut être renvoyée.                                                                   |\n  | `failed`                 | Arrêt définitif : l’endpoint a répondu `410 Gone`, a été désactivé ou son URL est devenue invalide. Peut être renvoyée. |\n  | `canceled`               | Annulée, par exemple parce que l’endpoint a été supprimé avec la livraison en attente.                                  |\n  | `blocked_no_credits`     | Message reçu d’un fournisseur sans solde pour la livraison (voir [Sans crédits](#sem-creditos)).                        |\n</Endpoint>"},{"anchor":"renvoyer-un-message","heading":"Renvoyer un message","markdown":"<Endpoint id=\"webhooks.messages.retry\" example={{ body: `{ \"endpoint_id\": \"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34\" }` }}>\n  Sans body, renvoie le message aux endpoints actifs dont la livraison s’est terminée en échec (`failed`, `exhausted` ou `canceled`).\n\n  Avec `endpoint_id`, l’envoie uniquement à cet endpoint, quel que soit le résultat précédent (même si la livraison a réussi). Il doit s’agir d’un endpoint actif du projet du message ; sinon, et pour les messages sans projet, la réponse est `400`. Les nouvelles livraisons sont facturées comme un envoi.\n</Endpoint>"},{"anchor":"tipos-de-evento","heading":"Types d’événement","markdown":"Déclarer des types d’événement est facultatif : sans déclaration, tout `event_type` valide est accepté. Déclarez-les pour documenter les événements de votre intégration et, avec `schema`, valider le `payload` avant de facturer et d’envoyer. Chaque type appartient à un projet et ne vaut que pour les messages de ce projet."},{"anchor":"déclarer-un-type-dévénement","heading":"Déclarer un type d’événement","markdown":"<Endpoint\n  id=\"webhooks.event_types.create\"\n  example={{ body: `{\n\"name\": \"order.paid\",\n\"description\": \"Commande payée\",\n\"schema\": {\n  \"type\": \"object\",\n  \"required\": [\"order_id\", \"amount\"],\n  \"properties\": {\n    \"order_id\": { \"type\": \"string\" },\n    \"amount\": { \"type\": \"number\" }\n  }\n}\n}` }}\n>\n  `schema` est un JSON Schema appliqué au `payload` de chaque envoi de ce type ; un payload non conforme répond `webhook.payload_invalid` et n’est pas facturé. `project_id` est facultatif (par défaut : le projet par défaut) ; le nom est unique dans le projet. Jusqu’à <Limit name=\"webhookEventTypesMax\" /> types par compte, tous projets confondus.\n</Endpoint>"},{"anchor":"lister-les-types-dévénement","heading":"Lister les types d’événement","markdown":"<Endpoint id=\"webhooks.event_types.list\">\n  Filtre facultatif par `project_id`.\n</Endpoint>"},{"anchor":"supprimer-un-type-dévénement","heading":"Supprimer un type d’événement","markdown":"<Endpoint id=\"webhooks.event_types.delete\">\n  Les envois de ce type restent acceptés, sans validation de schéma.\n</Endpoint>"},{"anchor":"ingestao","heading":"Recevoir des webhooks de tiers","markdown":"Une **source** est une URL de la plateforme (`ingest_url`) que vous enregistrez chez le fournisseur (Stripe, GitHub, Shopify…). Chaque événement reçu voit sa signature fournisseur vérifiée, est enregistré puis redistribué par les **routes** de la source vers vos endpoints, avec la même signature, les mêmes relances et le même journal que les messages de l’API. Un seul webhook fournisseur peut ainsi alimenter plusieurs services, et le fournisseur n’attend jamais votre récepteur.\n\n| `provider`          | Vérification de la requête                                                                                                     |\n| ------------------- | ------------------------------------------------------------------------------------------------------------------------------ |\n| `stripe`            | En-tête `Stripe-Signature` avec le signing secret Stripe (`whsec_…`), tolérance de <Limit name=\"webhookSignatureTolerance\" />. |\n| `github`            | En-tête `X-Hub-Signature-256` avec le secret du webhook GitHub.                                                                |\n| `shopify`           | En-tête `X-Shopify-Hmac-Sha256` avec le secret de l’app Shopify.                                                               |\n| `hmac_generic`      | En-tête `X-Webhook-Signature: sha256=<hex>` (HMAC-SHA256 du corps).                                                            |\n| `standard_webhooks` | En-têtes `webhook-id`, `webhook-timestamp` et `webhook-signature`.                                                             |\n| `basic_auth`        | En-tête `Authorization: Basic …` ; le secret est `utilisateur:motdepasse`.                                                     |\n| `none`              | Aucune vérification : quiconque possède l’URL peut envoyer. À éviter en production.                                            |\n\n`hmac_generic`, `standard_webhooks` et `basic_auth` conviennent à tout système absent de la liste : choisissez celui que le système d’origine sait envoyer."},{"anchor":"créer-une-source-dingestion","heading":"Créer une source d’ingestion","markdown":"<Endpoint\n  id=\"webhooks.sources.create\"\n  example={{ body: `{\n\"name\": \"Stripe production\",\n\"provider\": \"stripe\",\n\"verification_secret\": \"whsec_...\",\n\"event_type_path\": \"type\",\n\"dedupe_key_path\": \"id\"\n}` }}\n>\n  * `name` et `provider` (obligatoires) ; `project_id` est facultatif (par défaut : le projet par défaut).\n  * `verification_secret` : le secret avec lequel signe le fournisseur. Il n’est jamais renvoyé (`has_verification_secret`). Il peut être défini plus tard, car plusieurs fournisseurs n’affichent le secret qu’une fois l’`ingest_url` enregistrée : tant qu’il n’est pas défini via [Définir le secret du fournisseur](#webhooks-sources-verification_secret), la source répond `401` à toute requête (sauf avec `provider: \"none\"`).\n  * `event_type_path` : chemin dans le JSON reçu qui contient le type d’événement (ex. : `type`, `data.object.status`). Sans lui, le type est `ingest`.\n  * `dedupe_key_path` : chemin de l’identifiant de l’événement (ex. : `id`). Un renvoi du fournisseur avec la même clé répond `200` sans créer de nouvelles livraisons.\n  * `forward_original_headers` : transmet les en-têtes du fournisseur (ex. : `Stripe-Signature`) avec le corps identique, en plus de notre signature. Utile quand votre récepteur valide déjà la signature du fournisseur. Les cookies, `Authorization` et les en-têtes de proxy ne sont jamais transmis.\n  * Enregistrez l’`ingest_url` de la réponse chez le fournisseur. Jusqu’à <Limit name=\"webhookSourcesMax\" /> sources par compte, tous projets confondus.\n</Endpoint>"},{"anchor":"lister-les-sources","heading":"Lister les sources","markdown":"<Endpoint id=\"webhooks.sources.list\">\n  Filtre facultatif par `project_id`.\n</Endpoint>"},{"anchor":"consulter-une-source","heading":"Consulter une source","markdown":"<Endpoint id=\"webhooks.sources.get\" />"},{"anchor":"modifier-une-source","heading":"Modifier une source","markdown":"<Endpoint id=\"webhooks.sources.update\" example={{ body: `{ \"provider\": \"hmac_generic\", \"forward_original_headers\": true }` }}>\n  Envoyez uniquement les champs à modifier, y compris `provider` (par exemple pour passer une source `none` à `hmac_generic`). En passant à un fournisseur signé, envoyez `verification_secret` dans la même requête pour que la source ne reste jamais sans secret (sans lui, elle répond `401`). Pour changer seulement le secret, utilisez [Définir le secret du fournisseur](#webhooks-sources-verification_secret) ; un secret vide est refusé. Une source ne change pas de projet.\n</Endpoint>"},{"anchor":"supprimer-une-source","heading":"Supprimer une source","markdown":"<Endpoint id=\"webhooks.sources.delete\">\n  Les routes de la source sont supprimées avec elle et l’`ingest_url` cesse de répondre.\n</Endpoint>"},{"anchor":"renouveler-lurl-dingestion","heading":"Renouveler l’URL d’ingestion","markdown":"<Endpoint id=\"webhooks.sources.rotate_token\">\n  Génère une nouvelle `ingest_url` ; l’ancienne cesse de fonctionner immédiatement. Mettez à jour l’URL chez le fournisseur sans attendre.\n</Endpoint>"},{"anchor":"définir-le-secret-du-fournisseur","heading":"Définir le secret du fournisseur","markdown":"<Endpoint id=\"webhooks.sources.verification_secret\" example={{ body: `{ \"secret\": \"whsec_...\" }` }}>\n  Un secret vide n’est accepté qu’avec `provider: \"none\"`.\n</Endpoint>"},{"anchor":"consulter-le-dernier-événement-reçu","heading":"Consulter le dernier événement reçu","markdown":"<Endpoint id=\"webhooks.sources.sample\">\n  Renvoie le dernier corps reçu par la source, avec le type d’événement lu et l’heure. Utile pour découvrir les champs du fournisseur avant de régler `event_type_path`, `dedupe_key_path` et les filtres des routes.\n</Endpoint>"},{"anchor":"envoyer-un-événement-de-test-par-la-source","heading":"Envoyer un événement de test par la source","markdown":"<Endpoint id=\"webhooks.sources.test\" example={{ body: `{ \"event_type\": \"invoice.paid\" }` }}>\n  Fait passer un événement par la source comme si le fournisseur l’avait envoyé : le type est lu à `event_type_path` et les routes (avec leurs filtres) décident où il va. Le corps est le dernier reçu par la source ou, si elle n’a encore rien reçu, un exemple du fournisseur. `event_type` (facultatif) est écrit à l’emplacement de `event_type_path` ; si le corps n’a pas ce champ, le type utilisé est `test.event`.\n\n  C’est gratuit et hors du quota mensuel. Ni la signature ni la déduplication ne sont vérifiées, mais l’appel compte dans la limite de <Limit name=\"webhookIngestPerMinute\" /> requêtes par minute de la source.\n</Endpoint>"},{"anchor":"créer-une-route","heading":"Créer une route","markdown":"<Endpoint\n  id=\"webhooks.routes.create\"\n  example={{ body: `{\n\"source_id\": \"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27\",\n\"endpoint_id\": \"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34\",\n\"event_types\": [\"checkout.session.completed\", \"invoice.paid\"]\n}` }}\n>\n  Relie une source à un endpoint **du même projet** (une source et un endpoint de projets différents répondent `400`). `event_types` limite la route à ces types, jusqu’à <Limit name=\"webhookRouteEventTypesMax\" /> ; vide, la route prend tous les événements de la source. Jusqu’à <Limit name=\"webhookRoutesPerSourceMax\" /> routes par source.\n</Endpoint>"},{"anchor":"lister-les-routes","heading":"Lister les routes","markdown":"<Endpoint id=\"webhooks.routes.list\" example={{ query: { source_id: 'a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27' } }}>\n  Filtres facultatifs par `source_id` et `project_id`.\n</Endpoint>"},{"anchor":"modifier-une-route","heading":"Modifier une route","markdown":"<Endpoint id=\"webhooks.routes.update\" example={{ body: `{ \"event_types\": [\"invoice.paid\"], \"is_active\": false }` }}>\n  Change les types d’événement de la route (liste vide = tous) ou la met en pause avec `is_active: false`. Envoyez uniquement les champs à modifier.\n</Endpoint>"},{"anchor":"supprimer-une-route","heading":"Supprimer une route","markdown":"<Endpoint id=\"webhooks.routes.delete\" />"},{"anchor":"recevoir-un-événement-dun-fournisseur","heading":"Recevoir un événement d’un fournisseur","markdown":"<Endpoint id=\"webhooks.ingest\" example={{ pathParams: { token: 'src_4f8a2c6e1b9d3f7a5c0e8b2d6f4a1c9e7b3d5f0a2c8e6b4d' }, headers: { 'Stripe-Signature': 't=1790338000,v1=5257a869…' }, body: `{ \"id\": \"evt_1Q2w3E\", \"type\": \"checkout.session.completed\", \"data\": { \"object\": { \"id\": \"cs_test_a1B2\" } } }` }}>\n  C’est l’URL appelée par le fournisseur ; vous n’avez pas à l’appeler. Le corps est enregistré et transmis octet par octet, avec le `Content-Type` d’origine. Les réponses sont conçues pour que le fournisseur décide s’il doit relancer :\n\n  | Statut | Quand                                                                                   |\n  | ------ | --------------------------------------------------------------------------------------- |\n  | `200`  | Accepté, doublon (renvoi du fournisseur) ou aucune route pour le type.                  |\n  | `401`  | Signature invalide, ou secret du fournisseur pas encore défini ; rien n’est enregistré. |\n  | `404`  | Source inconnue ou désactivée, ou outil non activé pour le compte.                      |\n  | `429`  | Plus de <Limit name=\"webhookIngestPerMinute\" /> requêtes par minute pour la source.     |\n  | `5xx`  | Échec de l’enregistrement ; le fournisseur doit relancer.                               |\n</Endpoint>"},{"anchor":"exemplo-stripe","heading":"Exemple : Stripe","markdown":"<Steps>\n  <Step>\n    [Créez un endpoint](#webhooks-endpoints-create) pour chacun de vos services qui a besoin des événements Stripe. Les événements arrivent par les routes, donc `events` peut rester vide.\n  </Step>\n\n  <Step>\n    [Créez une source](#webhooks-sources-create) avec `provider: \"stripe\"`, `event_type_path: \"type\"` et `dedupe_key_path: \"id\"`, encore sans le secret.\n  </Step>\n\n  <Step>\n    Dans le tableau de bord Stripe, ajoutez un endpoint de webhook avec l’`ingest_url` et copiez le **signing secret** (`whsec_…`) dans [Définir le secret du fournisseur](#webhooks-sources-verification_secret).\n  </Step>\n\n  <Step>\n    [Créez une route](#webhooks-routes-create) de la source vers chaque endpoint, avec `event_types` quand un service n’a besoin que de certains événements. Vérifiez le parcours avec [Envoyer un événement de test par la source](#webhooks-sources-test).\n  </Step>\n</Steps>\n\nVos services reçoivent désormais les événements signés par la plateforme (validez-les avec le secret de chaque endpoint). S’ils préfèrent valider la signature de Stripe lui-même, activez `forward_original_headers` sur la source : `Stripe-Signature` et le corps identique arrivent aussi."},{"anchor":"sem-creditos","heading":"Sans crédits","markdown":"Le fournisseur n’est jamais refusé faute de solde. Les livraisons qui seraient facturées passent au statut `blocked_no_credits` (comptées dans `blocked` dans la réponse au fournisseur) et sont facturées et libérées automatiquement dès que le compte a des crédits. Si le solde ne revient pas sous <Limit name=\"webhookBlockedRetention\" />, elles se terminent en `failed`."},{"anchor":"canvas","heading":"Canvas du tableau de bord","markdown":"Dans le tableau de bord, chaque projet s’affiche comme un canvas avec ses sources, ses endpoints et les routes qui les relient. Ces routes alimentent cet écran ; pour une intégration par API, les listes ci-dessus suffisent généralement. Les trois acceptent `?project_id=` (par défaut : le projet par défaut)."},{"anchor":"consulter-la-carte-du-flux","heading":"Consulter la carte du flux","markdown":"<Endpoint id=\"webhooks.graph\" example={{ query: { project_id: '3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53' } }}>\n  Les sources, endpoints et routes du projet avec les compteurs des dernières 24 heures : par source, messages reçus, le dernier et les types d’événement récents ; par endpoint et par route, livraisons, échecs et statut de la dernière.\n</Endpoint>"},{"anchor":"consulter-la-disposition-du-canvas","heading":"Consulter la disposition du canvas","markdown":"<Endpoint id=\"webhooks.layout.get\" />"},{"anchor":"enregistrer-la-disposition-du-canvas","heading":"Enregistrer la disposition du canvas","markdown":"<Endpoint\n  id=\"webhooks.layout.save\"\n  example={{ body: `{\n\"positions\": {\n  \"source:a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27\": { \"x\": 0, \"y\": 160 },\n  \"endpoint:8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34\": { \"x\": 520, \"y\": 200 }\n}\n}` }}\n>\n  `positions` stocke les coordonnées de chaque carte : clés `platform`, `source:<id>` ou `endpoint:<id>`. Les cartes sans position sont placées automatiquement par le tableau de bord.\n</Endpoint>"},{"anchor":"alertas","heading":"Alertes","markdown":"Le titulaire du compte est notifié dans le tableau de bord et par e-mail. Chaque type peut être désactivé dans les préférences de notification (catégorie webhooks).\n\n| Type                        | Quand                                                                                                                                                       |\n| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `webhook_endpoint_failing`  | Un endpoint commence à échouer (première tentative en erreur après une période saine). Pas renvoyée tant qu’une réponse `2xx` n’a pas mis fin à la série.   |\n| `webhook_endpoint_disabled` | La désactivation automatique coupe un endpoint en échec depuis <Limit name=\"webhookDisableAfter\" /> (pas quand vous le mettez en pause, ni sur `410 Gone`). |\n| `webhook_quota`             | Les livraisons du mois atteignent <Limit name=\"webhookQuotaAlerts\" /> du quota mensuel gratuit, une fois par seuil et par mois.                             |\n\nLes alertes ne retardent ni ne bloquent jamais un envoi."},{"anchor":"retries","heading":"Livraison et relances","markdown":"Le calendrier des relances, ce qui compte comme un succès, `Retry-After`, `410 Gone` et la désactivation automatique des endpoints en échec pendant <Limit name=\"webhookDisableAfter\" /> d’affilée sont décrits dans le guide [Webhooks](/fr/webhooks#retries). Les relances automatiques ne sont pas refacturées ; seuls [Renvoyer un message](#webhooks-messages-retry) et [Rejouer les livraisons en échec](#webhooks-endpoints-recover) créent de nouvelles livraisons."},{"anchor":"api-legada","heading":"API historique du compte","markdown":"Les anciennes routes de webhooks du compte (`/v1/account/webhooks`) sont **obsolètes** : elles répondent avec les en-têtes `Deprecation: true` et `Link` pointant vers les projets de Webhook Relay. Elles continuent de fonctionner, et les endpoints qu’elles créent vont dans le projet par défaut. Utilisez Webhook Relay pour les nouvelles intégrations. La route du secret des callbacks de jobs n’est pas obsolète (voir [Secrets et renouvellement](/fr/webhooks#secrets))."},{"anchor":"erros","heading":"Erreurs","markdown":"<ErrorTable tool=\"webhook-relay\" />\n\nLes erreurs de validation courantes (`validation.failed`, `not_found`) et de solde (`billing.insufficient_credits`) sont dans [Erreurs](/fr/erros)."},{"anchor":"limites","heading":"Limites","markdown":"| Limite                                          | Valeur                                             |\n| ----------------------------------------------- | -------------------------------------------------- |\n| Livraisons gratuites par mois (par compte)      | <Limit name=\"webhookFreeDeliveries\" />             |\n| Taille du `payload` / corps reçu                | <Limit name=\"webhookPayloadMax\" />                 |\n| Projets par compte                              | <Limit name=\"webhookProjectsMax\" />                |\n| Endpoints par compte                            | <Limit name=\"webhookEndpointsMax\" />               |\n| En-têtes personnalisés par endpoint             | <Limit name=\"webhookCustomHeadersMax\" />           |\n| Types d’événement par compte                    | <Limit name=\"webhookEventTypesMax\" />              |\n| Messages par lot                                | <Limit name=\"webhookBatchMax\" />                   |\n| Livraisons par récupération                     | <Limit name=\"webhookRecoverMax\" />                 |\n| Sources par compte                              | <Limit name=\"webhookSourcesMax\" />                 |\n| Routes par source                               | <Limit name=\"webhookRoutesPerSourceMax\" />         |\n| Types d’événement par route                     | <Limit name=\"webhookRouteEventTypesMax\" />         |\n| Requêtes reçues par source                      | <Limit name=\"webhookIngestPerMinute\" /> par minute |\n| Attente de crédits (`blocked_no_credits`)       | <Limit name=\"webhookBlockedRetention\" />           |\n| Conservation du journal des messages            | <Limit name=\"webhookLogRetention\" />               |\n| Fenêtre de recherche dans le corps des messages | <Limit name=\"webhookSearchBodyWindow\" />           |\n\nLes limites par compte comptent tous les projets. Ce sont les valeurs par défaut de la plateforme, qu’elle peut ajuster."}]}]},"tools":[{"slug":"qr-dinamico","category":"marketing","code":"qr_dynamic","base_path":"/v1/tools/image/qr-code/dynamic","status":"published","name":{"pt-br":"QR Dinâmico","en":"Dynamic QR","es":"QR dinámico","fr":"QR dynamique"},"summary":{"pt-br":"QR codes com destino editável, design customizável e analytics de scans.","en":"QR codes with an editable destination, customizable design and scan analytics.","es":"Códigos QR con destino editable, diseño personalizable y analíticas de escaneos.","fr":"QR codes à destination modifiable, design personnalisable et analytics des scans."},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico"},"endpoints":[{"id":"qr.create","method":"POST","path":"/v1/tools/image/qr-code/dynamic","title":{"pt-br":"Criar QR dinâmico","en":"Create dynamic QR","es":"Crear QR dinámico","fr":"Créer un QR dynamique"},"scopes":["tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Variável (ver custos)","en":"Variable (see costs)","es":"Variable (ver costos)","fr":"Variable (voir les coûts)"}},"dry_run":true,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":[{"body":"<svg xmlns=\"http://www.w3.org/2000/svg\" …>…</svg>","content_type":"image/svg+xml","format":"svg","resource_id":"8f3a2b1c-4d5e-6f70-8192-a3b4c5d6e7f8","slug":"promo-verao"},{"body":"iVBORw0KGgoAAAANSUhEUgAA…","content_type":"image/png","format":"png","resource_id":"8f3a2b1c-4d5e-6f70-8192-a3b4c5d6e7f8","slug":"promo-verao"}]},"note":{"pt-br":"um item por formato pedido em `response_types` (padrão `[\"svg\"]`)","en":"one item per format requested in `response_types` (default `[\"svg\"]`)","es":"un elemento por cada formato solicitado en `response_types` (predeterminado `[\"svg\"]`)","fr":"un élément par format demandé dans `response_types` (par défaut `[\"svg\"]`)"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-create","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-create","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-create","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-create"}},{"id":"qr.list","method":"GET","path":"/v1/tools/image/qr-code/dynamic","title":{"pt-br":"Listar QRs","en":"List QRs","es":"Listar QR","fr":"Lister les QR"},"scopes":["tools:qr:dynamic:read","tools:qr:dynamic:manage","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"qr_dynamic.list"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"items":[{"id":"8f3a2b1c-4d5e-6f70-8192-a3b4c5d6e7f8","name":"Campanha verão","target_url":"https://loja.example.com/promo","full_url":"https://api.respawntech.dev/r/promo-verao","utm_forwarding":false,"scan_limit":1000,"total_scans":42,"updated_at":"2026-09-20T14:03:11Z","design":{"size":512,"foreground_color":"#000000","background_color":"#FFFFFF","transparent":false,"recovery_level":"M","module_style":"dots","eye_style":"rounded"},"resource":{"slug":"promo-verao","status":"active","created_at":"2026-09-01T10:00:00Z","expires_at":"2026-12-31T23:59:59Z"}}],"total":1}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-list","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-list","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-list","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-list"}},{"id":"qr.get","method":"GET","path":"/v1/tools/image/qr-code/dynamic/{id}","title":{"pt-br":"Detalhe","en":"Retrieve","es":"Detalle","fr":"Détail"},"scopes":["tools:qr:dynamic:read","tools:qr:dynamic:manage","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"qr_dynamic.get"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"8f3a2b1c-4d5e-6f70-8192-a3b4c5d6e7f8","name":"Campanha verão","target_url":"https://loja.example.com/promo","full_url":"https://api.respawntech.dev/r/promo-verao","utm_forwarding":false,"scan_limit":1000,"total_scans":42,"updated_at":"2026-09-20T14:03:11Z","design":{"size":512,"foreground_color":"#000000","background_color":"#FFFFFF","transparent":false,"recovery_level":"M","module_style":"dots","eye_style":"rounded"},"resource":{"slug":"promo-verao","status":"active","created_at":"2026-09-01T10:00:00Z","expires_at":"2026-12-31T23:59:59Z"}}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-get","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-get","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-get","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-get"}},{"id":"qr.update","method":"PATCH","path":"/v1/tools/image/qr-code/dynamic/{id}","title":{"pt-br":"Atualizar destino, nome ou design","en":"Update destination, name or design","es":"Actualizar destino, nombre o diseño","fr":"Modifier la destination, le nom ou le design"},"scopes":["tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Grátis, exceto ao ligar opções pagas (ver custos)","en":"Free, except when turning on paid options (see costs)","es":"Gratis, excepto al activar opciones de pago (ver costos)","fr":"Gratuit, sauf à l'activation d'options payantes (voir les coûts)"}},"dry_run":true,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"available":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-update","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-update","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-update","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-update"}},{"id":"qr.activate","method":"PUT","path":"/v1/tools/image/qr-code/dynamic/{id}/activate","title":{"pt-br":"Reativar","en":"Reactivate","es":"Reactivar","fr":"Réactiver"},"scopes":["tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"available":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-activate","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-activate","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-activate","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-activate"}},{"id":"qr.deactivate","method":"PUT","path":"/v1/tools/image/qr-code/dynamic/{id}/deactivate","title":{"pt-br":"Desativar","en":"Deactivate","es":"Desactivar","fr":"Désactiver"},"scopes":["tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"available":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-deactivate","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-deactivate","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-deactivate","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-deactivate"}},{"id":"qr.delete","method":"DELETE","path":"/v1/tools/image/qr-code/dynamic/{id}","title":{"pt-br":"Excluir","en":"Delete","es":"Eliminar","fr":"Supprimer"},"scopes":["tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"available":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-delete","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-delete","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-delete"}},{"id":"qr.check-slug","method":"GET","path":"/v1/tools/image/qr-code/dynamic/check-slug/{slug}","title":{"pt-br":"Verificar disponibilidade de slug","en":"Check slug availability","es":"Verificar disponibilidad del slug","fr":"Vérifier la disponibilité d'un slug"},"scopes":["tools:qr:dynamic:read","tools:qr:dynamic:manage","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"qr_dynamic.slug_check"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"available":false}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-check-slug","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-check-slug","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-check-slug","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-check-slug"}},{"id":"qr.analytics","method":"GET","path":"/v1/tools/image/qr-code/dynamic/{id}/analytics","title":{"pt-br":"Overview de analytics","en":"Analytics overview","es":"Resumen de analíticas","fr":"Vue d'ensemble des analytics"},"scopes":["tools:qr:dynamic:read","tools:qr:dynamic:manage","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"qr_dynamic.analytics"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"period":"last_30d","total_scans":42,"total_cost":21000,"period_scans":30,"period_cost":15000,"unique_scans":18,"period_stats":[{"date":"2026-09-20","count":7}],"device_stats":[{"device":"Mobile","count":25},{"device":"Desktop","count":5}],"country_stats":[{"location":"BR","count":20,"iso_code":"BR"}],"city_stats":[{"location":"São Paulo","count":12}],"os_stats":[{"location":"Android","count":15}],"browser_stats":[{"location":"Chrome","count":19}],"referrer_stats":[{"location":"instagram.com","count":4}],"heatmap_stats":[{"day":1,"hour":14,"count":3}],"scan_history":[{"id":"c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f","country":"BR","city":"São Paulo","os":"Android","browser":"Chrome","user_agent":"Mozilla/5.0 (Linux; Android 14) …","referer":"https://instagram.com/","scanned_at":"2026-09-20T18:22:04Z","location":"São Paulo, BR","device":"Mobile"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-analytics","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-analytics","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-analytics","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-analytics"}},{"id":"qr.analytics-period","method":"GET","path":"/v1/tools/image/qr-code/dynamic/{id}/analytics/period","title":{"pt-br":"Scans por período","en":"Scans by period","es":"Escaneos por período","fr":"Scans par période"},"scopes":["tools:qr:dynamic:read","tools:qr:dynamic:manage","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"qr_dynamic.analytics_period"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"period":"","total_scans":42,"total_cost":21000,"period_scans":0,"period_cost":0,"unique_scans":0,"period_stats":[{"date":"2026-09-19","count":5},{"date":"2026-09-20","count":7}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-analytics-period","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-analytics-period","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-analytics-period","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-analytics-period"}},{"id":"qr.analytics-device","method":"GET","path":"/v1/tools/image/qr-code/dynamic/{id}/analytics/device","title":{"pt-br":"Distribuição por dispositivo","en":"Device breakdown","es":"Distribución por dispositivo","fr":"Répartition par appareil"},"scopes":["tools:qr:dynamic:read","tools:qr:dynamic:manage","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"qr_dynamic.analytics_device"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"period":"","total_scans":42,"total_cost":21000,"period_scans":0,"period_cost":0,"unique_scans":0,"device_stats":[{"device":"Mobile","count":30},{"device":"Desktop","count":10},{"device":"Tablet","count":2}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-analytics-device","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-analytics-device","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-analytics-device","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-analytics-device"}},{"id":"qr.scans","method":"GET","path":"/v1/tools/image/qr-code/dynamic/{id}/scans","title":{"pt-br":"Histórico de scans","en":"Scan history","es":"Historial de escaneos","fr":"Historique des scans"},"scopes":["tools:qr:dynamic:read","tools:qr:dynamic:manage","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"qr_dynamic.scans"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"items":[{"id":"c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f","country":"BR","city":"São Paulo","os":"Android","browser":"Chrome","user_agent":"Mozilla/5.0 (Linux; Android 14) …","referer":"https://instagram.com/","scanned_at":"2026-09-20T18:22:04Z","location":"São Paulo, BR","device":"Mobile"}],"total":42}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-scans","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-scans","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-scans","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-scans"}},{"id":"qr.scans-export","method":"GET","path":"/v1/tools/image/qr-code/dynamic/{id}/scans/export","title":{"pt-br":"Exportar scans em CSV","en":"Export scans as CSV","es":"Exportar escaneos en CSV","fr":"Exporter les scans en CSV"},"scopes":["tools:qr:dynamic:read","tools:qr:dynamic:manage","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"qr_dynamic.scans"},"dry_run":false,"response":{"status":200,"note":{"pt-br":"`text/csv`, com `Content-Disposition: attachment`. Colunas: `scanned_at`, `country`, `city`, `os`, `browser`, `device`, `referer`, `user_agent`, `rule_id`, `variant` (mais antigos primeiro)","en":"`text/csv`, with `Content-Disposition: attachment`. Columns: `scanned_at`, `country`, `city`, `os`, `browser`, `device`, `referer`, `user_agent`, `rule_id`, `variant` (oldest first)","es":"`text/csv`, con `Content-Disposition: attachment`. Columnas: `scanned_at`, `country`, `city`, `os`, `browser`, `device`, `referer`, `user_agent`, `rule_id`, `variant` (los más antiguos primero)","fr":"`text/csv`, avec `Content-Disposition: attachment`. Colonnes : `scanned_at`, `country`, `city`, `os`, `browser`, `device`, `referer`, `user_agent`, `rule_id`, `variant` (les plus anciens d'abord)"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-scans-export","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-scans-export","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-scans-export","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-scans-export"}},{"id":"qr.scans-purge","method":"DELETE","path":"/v1/tools/image/qr-code/dynamic/{id}/scans","title":{"pt-br":"Apagar o histórico de scans","en":"Purge scan history","es":"Borrar el historial de escaneos","fr":"Effacer l'historique des scans"},"scopes":["tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"deleted":42}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-scans-purge","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-scans-purge","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-scans-purge","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-scans-purge"}},{"id":"qr.bulk","method":"POST","path":"/v1/tools/image/qr-code/dynamic/bulk","title":{"pt-br":"Editar vários QRs de uma vez","en":"Edit several QR codes at once","es":"Editar varios QRs a la vez","fr":"Modifier plusieurs QR à la fois"},"scopes":["tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"succeeded":1,"failed":1,"items":[{"id":"8f3a2b1c-4d5e-6f70-8192-a3b4c5d6e7f8","ok":true},{"id":"0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d","ok":false,"error_code":"dynamic_qr.not_found","error":"QR code not found"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-bulk","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-bulk","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-bulk","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-bulk"}},{"id":"qr.folders-list","method":"GET","path":"/v1/tools/image/qr-code/folders","title":{"pt-br":"Listar pastas","en":"List folders","es":"Listar carpetas","fr":"Lister les dossiers"},"scopes":["tools:qr:dynamic:read","tools:qr:dynamic:manage","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"qr_dynamic.list"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":[{"id":"3b9f6c1e-7a52-4d0b-9c41-5e2d8a7f1b30","name":"Campanha de verão","qr_count":12,"created_at":"2026-09-01T10:00:00Z","updated_at":"2026-09-01T10:00:00Z"}]}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-folders-list","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-folders-list","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-folders-list","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-folders-list"}},{"id":"qr.folders-create","method":"POST","path":"/v1/tools/image/qr-code/folders","title":{"pt-br":"Criar pasta","en":"Create a folder","es":"Crear carpeta","fr":"Créer un dossier"},"scopes":["tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"3b9f6c1e-7a52-4d0b-9c41-5e2d8a7f1b30","name":"Campanha de verão","qr_count":12,"created_at":"2026-09-01T10:00:00Z","updated_at":"2026-09-01T10:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-folders-create","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-folders-create","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-folders-create","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-folders-create"}},{"id":"qr.folders-rename","method":"PATCH","path":"/v1/tools/image/qr-code/folders/{id}","title":{"pt-br":"Renomear pasta","en":"Rename a folder","es":"Renombrar carpeta","fr":"Renommer un dossier"},"scopes":["tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"3b9f6c1e-7a52-4d0b-9c41-5e2d8a7f1b30","name":"Verão 2027","qr_count":12,"created_at":"2026-09-01T10:00:00Z","updated_at":"2026-09-01T10:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-folders-rename","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-folders-rename","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-folders-rename","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-folders-rename"}},{"id":"qr.folders-delete","method":"DELETE","path":"/v1/tools/image/qr-code/folders/{id}","title":{"pt-br":"Excluir pasta","en":"Delete a folder","es":"Eliminar carpeta","fr":"Supprimer un dossier"},"scopes":["tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":204,"note":{"pt-br":"sem corpo; os QRs da pasta voltam para a raiz","en":"no body; the folder's QR codes go back to the root","es":"sin cuerpo; los QRs de la carpeta vuelven a la raíz","fr":"sans corps ; les QR du dossier reviennent à la racine"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-folders-delete","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-folders-delete","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-folders-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-folders-delete"}},{"id":"qr.batches-create","method":"POST","path":"/v1/tools/image/qr-code/batches","title":{"pt-br":"Criar QRs em lote","en":"Create QR codes in batches","es":"Crear QRs en lote","fr":"Créer des QR en lot"},"scopes":["tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Criação de cada linha válida, com desconto por volume","en":"Creation of each valid row, with a volume discount","es":"Creación de cada fila válida, con descuento por volumen","fr":"Création de chaque ligne valide, avec remise de volume"}},"dry_run":false,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"7d1e4a90-2c3b-4f58-a6e7-9b0c1d2e3f40","status":"processing","total_items":2,"created":0,"failed":0,"pending":2,"charged_units":20000,"discount_units":0,"refunded_units":0,"created_at":"2026-09-20T14:00:00Z","completed_at":null}},"note":{"pt-br":"o lote é criado em segundo plano","en":"the batch is created in the background","es":"el lote se crea en segundo plano","fr":"le lot est créé en arrière-plan"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-batches-create","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-batches-create","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-batches-create","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-batches-create"}},{"id":"qr.batches-list","method":"GET","path":"/v1/tools/image/qr-code/batches","title":{"pt-br":"Listar lotes","en":"List batches","es":"Listar lotes","fr":"Lister les lots"},"scopes":["tools:qr:dynamic:read","tools:qr:dynamic:manage","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"qr_dynamic.list"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":[{"id":"7d1e4a90-2c3b-4f58-a6e7-9b0c1d2e3f40","status":"completed","total_items":2,"created":2,"failed":0,"pending":0,"charged_units":20000,"discount_units":0,"refunded_units":0,"created_at":"2026-09-20T14:00:00Z","completed_at":"2026-09-20T14:00:07Z"}]}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-batches-list","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-batches-list","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-batches-list","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-batches-list"}},{"id":"qr.batches-get","method":"GET","path":"/v1/tools/image/qr-code/batches/{id}","title":{"pt-br":"Detalhe do lote","en":"Batch details","es":"Detalle del lote","fr":"Détail du lot"},"scopes":["tools:qr:dynamic:read","tools:qr:dynamic:manage","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"qr_dynamic.get"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"7d1e4a90-2c3b-4f58-a6e7-9b0c1d2e3f40","status":"completed","total_items":2,"created":2,"failed":0,"pending":0,"charged_units":20000,"discount_units":0,"refunded_units":0,"created_at":"2026-09-20T14:00:00Z","completed_at":"2026-09-20T14:00:07Z","items":[{"row":1,"status":"created","name":"Loja Centro","target_url":"https://loja.example.com/centro","cost_units":10000,"qr_id":"8f3a2b1c-4d5e-6f70-8192-a3b4c5d6e7f8","slug":"loja-centro","full_url":"https://api.respawntech.dev/r/loja-centro"},{"row":2,"status":"created","name":"Loja Norte","target_url":"https://loja.example.com/norte","cost_units":10000,"qr_id":"9e8d7c6b-5a49-4382-b1a0-f9e8d7c6b5a4","slug":"loja-norte","full_url":"https://api.respawntech.dev/r/loja-norte"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-batches-get","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-batches-get","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-batches-get","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-batches-get"}},{"id":"qr.domains-list","method":"GET","path":"/v1/tools/image/qr-code/domains","title":{"pt-br":"Listar domínios próprios","en":"List custom domains","es":"Listar dominios propios","fr":"Lister les domaines personnalisés"},"scopes":["tools:qr:domains:manage","tools:qr:dynamic:read","tools:qr:dynamic:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":[{"id":"5c2a8e71-0b4d-4e96-8f13-a7d6c9b0e214","hostname":"qr.suaempresa.com","status":"pending","ssl_status":"pending_validation","validation_method":"http","records":[{"purpose":"routing","type":"CNAME","name":"qr.suaempresa.com","value":"domains.respawntech.dev"},{"purpose":"certificate","type":"HTTP","name":"http://qr.suaempresa.com/.well-known/pki-validation/4d2f.txt","value":"4d2f9a1c-6b7e-4f0a-8c3d-2e1b5a9d7c60"}],"root_url":"https://www.suaempresa.com","created_at":"2026-09-20T14:00:00Z"}]}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-domains-list","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-domains-list","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-domains-list","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-domains-list"}},{"id":"qr.domains-get","method":"GET","path":"/v1/tools/image/qr-code/domains/{id}","title":{"pt-br":"Detalhe do domínio","en":"Domain details","es":"Detalle del dominio","fr":"Détail du domaine"},"scopes":["tools:qr:domains:manage","tools:qr:dynamic:read","tools:qr:dynamic:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"5c2a8e71-0b4d-4e96-8f13-a7d6c9b0e214","hostname":"qr.suaempresa.com","status":"pending","ssl_status":"pending_validation","validation_method":"http","records":[{"purpose":"routing","type":"CNAME","name":"qr.suaempresa.com","value":"domains.respawntech.dev"},{"purpose":"certificate","type":"HTTP","name":"http://qr.suaempresa.com/.well-known/pki-validation/4d2f.txt","value":"4d2f9a1c-6b7e-4f0a-8c3d-2e1b5a9d7c60"}],"root_url":"https://www.suaempresa.com","created_at":"2026-09-20T14:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-domains-get","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-domains-get","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-domains-get","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-domains-get"}},{"id":"qr.domains-create","method":"POST","path":"/v1/tools/image/qr-code/domains","title":{"pt-br":"Cadastrar domínio próprio","en":"Register a custom domain","es":"Registrar dominio propio","fr":"Enregistrer un domaine personnalisé"},"scopes":["tools:qr:domains:manage","tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por 30 dias, enquanto o domínio estiver ativo (ver custos)","en":"For 30 days, while the domain is active (see costs)","es":"Por 30 días, mientras el dominio esté activo (ver costos)","fr":"Pour 30 jours, tant que le domaine est actif (voir coûts)"}},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"5c2a8e71-0b4d-4e96-8f13-a7d6c9b0e214","hostname":"qr.suaempresa.com","status":"pending","ssl_status":"pending_validation","validation_method":"http","records":[{"purpose":"routing","type":"CNAME","name":"qr.suaempresa.com","value":"domains.respawntech.dev"},{"purpose":"certificate","type":"HTTP","name":"http://qr.suaempresa.com/.well-known/pki-validation/4d2f.txt","value":"4d2f9a1c-6b7e-4f0a-8c3d-2e1b5a9d7c60"}],"root_url":"https://www.suaempresa.com","created_at":"2026-09-20T14:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-domains-create","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-domains-create","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-domains-create","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-domains-create"}},{"id":"qr.domains-update","method":"PATCH","path":"/v1/tools/image/qr-code/domains/{id}","title":{"pt-br":"Editar páginas de fallback do domínio","en":"Edit the domain's fallback pages","es":"Editar páginas de fallback del dominio","fr":"Modifier les pages de repli du domaine"},"scopes":["tools:qr:domains:manage","tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"5c2a8e71-0b4d-4e96-8f13-a7d6c9b0e214","hostname":"qr.suaempresa.com","status":"pending","ssl_status":"pending_validation","validation_method":"http","records":[{"purpose":"routing","type":"CNAME","name":"qr.suaempresa.com","value":"domains.respawntech.dev"},{"purpose":"certificate","type":"HTTP","name":"http://qr.suaempresa.com/.well-known/pki-validation/4d2f.txt","value":"4d2f9a1c-6b7e-4f0a-8c3d-2e1b5a9d7c60"}],"root_url":"https://www.suaempresa.com","created_at":"2026-09-20T14:00:00Z","not_found_url":"https://www.suaempresa.com/ops"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-domains-update","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-domains-update","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-domains-update","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-domains-update"}},{"id":"qr.domains-refresh","method":"POST","path":"/v1/tools/image/qr-code/domains/{id}/refresh","title":{"pt-br":"Verificar o domínio agora","en":"Verify the domain now","es":"Verificar el dominio ahora","fr":"Vérifier le domaine maintenant"},"scopes":["tools:qr:domains:manage","tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"5c2a8e71-0b4d-4e96-8f13-a7d6c9b0e214","hostname":"qr.suaempresa.com","status":"active","ssl_status":"active","validation_method":"http","records":[{"purpose":"routing","type":"CNAME","name":"qr.suaempresa.com","value":"domains.respawntech.dev"},{"purpose":"certificate","type":"HTTP","name":"http://qr.suaempresa.com/.well-known/pki-validation/4d2f.txt","value":"4d2f9a1c-6b7e-4f0a-8c3d-2e1b5a9d7c60"}],"root_url":"https://www.suaempresa.com","created_at":"2026-09-20T14:00:00Z","verified_at":"2026-09-20T14:20:00Z","billed_until":"2026-10-20T14:20:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-domains-refresh","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-domains-refresh","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-domains-refresh","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-domains-refresh"}},{"id":"qr.domains-delete","method":"DELETE","path":"/v1/tools/image/qr-code/domains/{id}","title":{"pt-br":"Remover domínio","en":"Remove a domain","es":"Eliminar dominio","fr":"Supprimer le domaine"},"scopes":["tools:qr:domains:manage","tools:qr:dynamic:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":204,"note":{"pt-br":"sem corpo; recusado com `qr_domain.in_use` se ainda houver QRs nele","en":"no body; rejected with `qr_domain.in_use` if it still has QR codes","es":"sin cuerpo; se rechaza con `qr_domain.in_use` si aún hay QRs en él","fr":"sans corps ; refusé avec `qr_domain.in_use` s'il reste des QR dessus"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-domains-delete","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-domains-delete","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-domains-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-domains-delete"}},{"id":"qr.redirect","method":"GET","path":"/r/{short_id}","title":{"pt-br":"Redirect público","en":"Public redirect","es":"Redirección pública","fr":"Redirection publique"},"scopes":[],"billing":{"kind":"paid","summary":{"pt-br":"Por scan (ver custos)","en":"Per scan (see costs)","es":"Por escaneo (ver costos)","fr":"Par scan (voir les coûts)"}},"dry_run":false,"auth":"public","response":{"status":307,"note":{"pt-br":"`Location: <target_url>`; contas sem plano premium recebem `200` com uma página HTML intermediária que redireciona em 3 s","en":"`Location: <target_url>`; accounts without a premium plan receive `200` with an interstitial HTML page that redirects after 3 s","es":"`Location: <target_url>`; las cuentas sin plan premium reciben `200` con una página HTML intermedia que redirige en 3 s","fr":"`Location: <target_url>` ; les comptes sans offre premium reçoivent `200` avec une page HTML intermédiaire qui redirige en 3 s"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/qr-dinamico#qr-redirect","en":"https://docs.respawntech.dev/en/ferramentas/qr-dinamico#qr-redirect","es":"https://docs.respawntech.dev/es/ferramentas/qr-dinamico#qr-redirect","fr":"https://docs.respawntech.dev/fr/ferramentas/qr-dinamico#qr-redirect"}}],"pricing":[{"id":"creation","title":{"pt-br":"Criação — cobrada uma vez no POST (opções ligadas depois num PATCH custam o mesmo)","en":"Creation — charged once on POST (options turned on later in a PATCH cost the same)","es":"Creación — cobrada una vez en el POST (las opciones activadas después en un PATCH cuestan lo mismo)","fr":"Création — facturée une fois au POST (les options activées ensuite par un PATCH coûtent le même prix)"},"items":[{"label":{"pt-br":"Base (com imagem)","en":"Base (with image)","es":"Base (con imagen)","fr":"Base (avec image)"},"service":"qr_dynamic_create","credits":1},{"label":{"pt-br":"Base sem gerar imagem","en":"Base without image generation","es":"Base sin generar imagen","fr":"Base sans génération d'image"},"credits":0.5,"note":{"pt-br":"`skip_image_generation: true`","en":"`skip_image_generation: true`","es":"`skip_image_generation: true`","fr":"`skip_image_generation: true`"}},{"label":{"pt-br":"Slug customizado","en":"Custom slug","es":"Slug personalizado","fr":"Slug personnalisé"},"service":"branding_custom_slug","credits":35,"additive":true,"note":{"pt-br":"3–50 caracteres `[a-zA-Z0-9-]`","en":"3–50 characters `[a-zA-Z0-9-]`","es":"3–50 caracteres `[a-zA-Z0-9-]`","fr":"3–50 caractères `[a-zA-Z0-9-]`"}},{"label":{"pt-br":"Gradiente no design","en":"Gradient in design","es":"Degradado en el diseño","fr":"Dégradé dans le design"},"service":"design_gradient","credits":5,"additive":true},{"label":{"pt-br":"Olhos customizados","en":"Custom eyes","es":"Ojos personalizados","fr":"Yeux personnalisés"},"service":"design_custom_eyes","credits":2,"additive":true,"note":{"pt-br":"`eye_style` ou `eye_dot_style` `shield`/`flower`","en":"`eye_style` or `eye_dot_style` `shield`/`flower`","es":"`eye_style` o `eye_dot_style` `shield`/`flower`","fr":"`eye_style` ou `eye_dot_style` `shield`/`flower`"}},{"label":{"pt-br":"Controle de expiração","en":"Expiration control","es":"Control de expiración","fr":"Contrôle d'expiration"},"service":"control_expiration","credits":2,"additive":true,"note":{"pt-br":"`expires_at`","en":"`expires_at`","es":"`expires_at`","fr":"`expires_at`"}},{"label":{"pt-br":"Limite de scans","en":"Scan limit","es":"Límite de escaneos","fr":"Limite de scans"},"service":"control_scan_limit","credits":2,"additive":true,"note":{"pt-br":"`scan_limit`","en":"`scan_limit`","es":"`scan_limit`","fr":"`scan_limit`"}},{"label":{"pt-br":"Processamento de logo","en":"Logo processing","es":"Procesamiento de logo","fr":"Traitement du logo"},"service":"content_logo_process","credits":4,"additive":true},{"label":{"pt-br":"Encaminhamento UTM","en":"UTM forwarding","es":"Reenvío de UTM","fr":"Transfert des UTM"},"service":"marketing_utm_forwarding","credits":59,"additive":true,"note":{"pt-br":"Repassa a query string ao destino","en":"Forwards the query string to the destination","es":"Reenvía la query string al destino","fr":"Transmet la query string à la destination"}},{"label":{"pt-br":"Regras de redirect","en":"Redirect rules","es":"Reglas de redirect","fr":"Règles de redirection"},"service":"control_redirect_rules","credits":99,"additive":true,"note":{"pt-br":"`redirect_rules`","en":"`redirect_rules`","es":"`redirect_rules`","fr":"`redirect_rules`"}},{"label":{"pt-br":"Teste A/B","en":"A/B test","es":"Prueba A/B","fr":"Test A/B"},"service":"marketing_ab_test","credits":79,"additive":true,"note":{"pt-br":"`ab_variants`","en":"`ab_variants`","es":"`ab_variants`","fr":"`ab_variants`"}},{"label":{"pt-br":"Senha","en":"Password","es":"Contraseña","fr":"Mot de passe"},"service":"control_password","credits":49,"additive":true,"note":{"pt-br":"`password`","en":"`password`","es":"`password`","fr":"`password`"}},{"label":{"pt-br":"Pixels de rastreamento","en":"Tracking pixels","es":"Píxeles de seguimiento","fr":"Pixels de suivi"},"service":"marketing_pixels","credits":59,"additive":true,"note":{"pt-br":"`pixels` (GA4, Meta, GTM)","en":"`pixels` (GA4, Meta, GTM)","es":"`pixels` (GA4, Meta, GTM)","fr":"`pixels` (GA4, Meta, GTM)"}},{"label":{"pt-br":"Redirect de expirado/esgotado","en":"Expired/exhausted redirect","es":"Redirect de vencido/agotado","fr":"Redirection d'expiré/épuisé"},"service":"control_fallback_redirect","credits":29,"additive":true,"note":{"pt-br":"`expired_url` ou `limit_reached_url`","en":"`expired_url` or `limit_reached_url`","es":"`expired_url` o `limit_reached_url`","fr":"`expired_url` ou `limit_reached_url`"}},{"label":{"pt-br":"Página hospedada","en":"Hosted page","es":"Página alojada","fr":"Page hébergée"},"service":"content_hosted_page","credits":49,"additive":true,"note":{"pt-br":"`content` (vCard, arquivo, links, cardápio, Wi-Fi, loja de apps)","en":"`content` (vCard, file, links, menu, Wi-Fi, app store)","es":"`content` (vCard, archivo, enlaces, menú, Wi-Fi, tienda de apps)","fr":"`content` (vCard, fichier, liens, menu, Wi-Fi, store d'apps)"}},{"label":{"pt-br":"Render PNG extra","en":"Extra PNG render","es":"Render PNG adicional","fr":"Rendu PNG supplémentaire"},"service":"png_render","credits":1.5,"additive":true,"note":{"pt-br":"formato `png` ou `pdf` (cobrado uma vez)","en":"`png` or `pdf` format (charged once)","es":"formato `png` o `pdf` (cobrado una vez)","fr":"format `png` ou `pdf` (facturé une fois)"}}]},{"id":"domain","title":{"pt-br":"Domínio próprio","en":"Custom domain","es":"Dominio propio","fr":"Domaine personnalisé"},"items":[{"label":{"pt-br":"Domínio ativo (por 30 dias)","en":"Active domain (per 30 days)","es":"Dominio activo (por 30 días)","fr":"Domaine actif (pour 30 jours)"},"service":"branding_custom_domain","credits":99,"note":{"pt-br":"Cobrado ao ativar e a cada 30 dias; domínio pendente, com falha ou suspenso não é cobrado","en":"Charged on activation and every 30 days; a pending, failed or suspended domain is not charged","es":"Se cobra al activar y cada 30 días; un dominio pendiente, con fallo o suspendido no se cobra","fr":"Facturé à l'activation puis tous les 30 jours ; un domaine en attente, en échec ou suspendu n'est pas facturé"}}]},{"id":"batch","title":{"pt-br":"Lote","en":"Batch","es":"Lote","fr":"Lot"},"items":[{"label":{"pt-br":"Linha válida do lote","en":"Valid batch row","es":"Fila válida del lote","fr":"Ligne valide du lot"},"service":"qr_dynamic_create","credits":1,"note":{"pt-br":"Desconto por volume a partir de 100, 500 e 1.000 linhas; linha que falha depois é estornada","en":"Volume discount from 100, 500 and 1,000 rows; a row that fails later is refunded","es":"Descuento por volumen desde 100, 500 y 1.000 filas; una fila que falla después se reembolsa","fr":"Remise de volume à partir de 100, 500 et 1 000 lignes ; une ligne qui échoue ensuite est remboursée"}}]},{"id":"scans","title":{"pt-br":"Por scan — cobrado de forma assíncrona no redirect público","en":"Per scan — charged asynchronously on the public redirect","es":"Por escaneo: se cobra de forma asíncrona en la redirección pública","fr":"Par scan — facturé de manière asynchrone lors de la redirection publique"},"description":{"pt-br":"O valor por scan depende da quantidade de slots de armazenamento pagos na conta. Scans repetidos do mesmo visitante no mesmo QR em até 30 s são cobrados uma única vez.","en":"The per-scan price depends on the number of paid storage slots on the account. Repeat scans of the same QR code by the same visitor within 30 s are charged only once.","es":"El valor por escaneo depende de la cantidad de slots de almacenamiento pagados en la cuenta. Los escaneos repetidos del mismo visitante en el mismo QR dentro de 30 s se cobran una sola vez.","fr":"Le montant par scan dépend du nombre de slots de stockage payants sur le compte. Les scans répétés d'un même visiteur sur le même QR dans un délai de 30 s ne sont facturés qu'une fois."},"items":[{"label":{"pt-br":"0–1 slots pagos","en":"0–1 paid slots","es":"0–1 slots pagados","fr":"0–1 slots payants"},"credits":0.05},{"label":{"pt-br":"2–20 slots pagos","en":"2–20 paid slots","es":"2–20 slots pagados","fr":"2–20 slots payants"},"credits":0.045},{"label":{"pt-br":"21–40 slots pagos","en":"21–40 paid slots","es":"21–40 slots pagados","fr":"21–40 slots payants"},"credits":0.04},{"label":{"pt-br":"41–80 slots pagos","en":"41–80 paid slots","es":"41–80 slots pagados","fr":"41–80 slots payants"},"credits":0.035},{"label":{"pt-br":"81+ slots pagos","en":"81+ paid slots","es":"81+ slots pagados","fr":"81+ slots payants"},"credits":0.025}]},{"id":"storage","title":{"pt-br":"Armazenamento","en":"Storage","es":"Almacenamiento","fr":"Stockage"},"items":[{"label":{"pt-br":"Primeiro QR dinâmico","en":"First dynamic QR","es":"Primer QR dinámico","fr":"Premier QR dynamique"},"credits":0,"note":{"pt-br":"Não ocupa slot pago","en":"Does not use a paid slot","es":"No ocupa un slot pagado","fr":"N'occupe pas de slot payant"}},{"label":{"pt-br":"Slot (grupo de 3 QRs)","en":"Slot (group of 3 QRs)","es":"Slot (grupo de 3 QR)","fr":"Slot (groupe de 3 QR)"},"display":{"pt-br":"3,0 / dia","en":"3.0 / day","es":"3,0 / día","fr":"3,0 / jour"},"note":{"pt-br":"QRs desativados contam até serem excluídos","en":"Deactivated QRs count until they are deleted","es":"Los QR desactivados cuentan hasta que se eliminan","fr":"Les QR désactivés sont comptés jusqu'à leur suppression"}}]}]},{"slug":"pdf-convert","category":"documents","code":"pdf_convert","base_path":"/v1/tools/document/pdf","status":"published","name":{"pt-br":"PDF Convert","en":"PDF Convert","es":"PDF Convert","fr":"PDF Convert"},"summary":{"pt-br":"Conversão assíncrona de HTML/CSS em PDF, PNG ou JPG, unitária ou em batch.","en":"Asynchronous HTML/CSS conversion to PDF, PNG or JPG, single or in batch.","es":"Conversión asíncrona de HTML/CSS a PDF, PNG o JPG, individual o en batch.","fr":"Conversion asynchrone de HTML/CSS en PDF, PNG ou JPG, à l'unité ou en batch."},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert"},"endpoints":[{"id":"pdf.convert","method":"POST","path":"/v1/tools/document/pdf/convert","title":{"pt-br":"Converter HTML em PDF/PNG/JPG","en":"Convert HTML to PDF/PNG/JPG","es":"Convertir HTML a PDF/PNG/JPG","fr":"Convertir du HTML en PDF/PNG/JPG"},"scopes":["tools:pdf:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Variável (ver custos)","en":"Variable (see costs)","es":"Variable (ver costos)","fr":"Variable (voir les coûts)"}},"dry_run":true,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"conversion_id":"3e4f5a6b-7c8d-4e9f-a0b1-c2d3e4f5a6b7","job_id":"pdf_9b8c7d6e5f","status":"pending"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-convert","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-convert","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-convert","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-convert"}},{"id":"pdf.batch","method":"POST","path":"/v1/tools/document/pdf/batch","title":{"pt-br":"Batch com template","en":"Templated batch","es":"Batch con plantilla","fr":"Batch avec template"},"scopes":["tools:pdf:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Soma dos itens com desconto","en":"Sum of items with discount","es":"Suma de los elementos con descuento","fr":"Somme des éléments avec remise"}},"dry_run":true,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"conversion_id":"3e4f5a6b-7c8d-4e9f-a0b1-c2d3e4f5a6b7","job_id":"batch_1f2e3d4c","status":"processing"}},"note":{"pt-br":"`conversion_id` é o id do batch","en":"`conversion_id` is the batch id","es":"`conversion_id` es el id del batch","fr":"`conversion_id` est l'id du batch"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-batch","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-batch","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-batch","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-batch"}},{"id":"pdf.retry","method":"POST","path":"/v1/tools/document/pdf/{id}/retry","title":{"pt-br":"Reprocessar falha","en":"Retry failed conversion","es":"Reprocesar fallo","fr":"Relancer un échec"},"scopes":["tools:pdf:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Custo completo novamente","en":"Full cost charged again","es":"Costo completo nuevamente","fr":"Coût complet à nouveau"}},"dry_run":false,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"conversion_id":"3e4f5a6b-7c8d-4e9f-a0b1-c2d3e4f5a6b7","job_id":"pdf_4c3b2a19","status":"pending"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-retry","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-retry","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-retry","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-retry"}},{"id":"pdf.list","method":"GET","path":"/v1/tools/document/pdf","title":{"pt-br":"Listar conversões","en":"List conversions","es":"Listar conversiones","fr":"Lister les conversions"},"scopes":["tools:pdf:read","tools:manage"],"billing":{"kind":"metered","operation":"pdf.list"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"items":[{"id":"3e4f5a6b-7c8d-4e9f-a0b1-c2d3e4f5a6b7","job_id":"pdf_9b8c7d6e5f","status":"completed","file_name":"fatura-001.pdf","page_size":"A4","orientation":"portrait","margin_top":10,"margin_bottom":10,"margin_left":10,"margin_right":10,"scale":1,"format":"pdf","enable_js":false,"cost_credits":1.5,"cost_units":15000,"retention_days":7,"expires_at":"2026-10-01T12:00:00Z","is_deleted_from_storage":false,"html":"<html>…</html>","css":"body { font-family: sans-serif; }","created_at":"2026-09-24T12:00:00Z","updated_at":"2026-09-24T12:00:09Z"}],"total":57,"page":1,"page_size":20}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-list","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-list","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-list","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-list"}},{"id":"pdf.get","method":"GET","path":"/v1/tools/document/pdf/{id}","title":{"pt-br":"Status da conversão","en":"Conversion status","es":"Estado de la conversión","fr":"Statut de la conversion"},"scopes":["tools:pdf:read","tools:manage"],"billing":{"kind":"metered","operation":"pdf.status"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"3e4f5a6b-7c8d-4e9f-a0b1-c2d3e4f5a6b7","job_id":"pdf_9b8c7d6e5f","status":"failed","file_name":"fatura-001.pdf","page_size":"A4","orientation":"portrait","margin_top":10,"margin_bottom":10,"margin_left":10,"margin_right":10,"scale":1,"format":"pdf","enable_js":false,"cost_credits":1.5,"cost_units":15000,"retention_days":7,"expires_at":"2026-10-01T12:00:00Z","is_deleted_from_storage":false,"html":"<html>…</html>","css":"body { font-family: sans-serif; }","created_at":"2026-09-24T12:00:00Z","updated_at":"2026-09-24T12:00:09Z","error_code":"pdf.render_timeout","error_tag":"pdf.render_timeout","error_message":"O tempo limite de renderização foi excedido","webhook_url":"https://example.com/hooks/pdf","data":"{\"cliente\":\"Ana\"}"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-get","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-get","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-get","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-get"}},{"id":"pdf.download","method":"GET","path":"/v1/tools/document/pdf/{id}/download","title":{"pt-br":"Baixar arquivo","en":"Download file","es":"Descargar archivo","fr":"Télécharger le fichier"},"scopes":["tools:pdf:read","tools:manage"],"billing":{"kind":"metered","operation":"pdf.download"},"dry_run":false,"timeout":"2 minutos","response":{"status":200,"note":{"pt-br":"corpo binário com `Content-Type` do formato (`application/pdf`, `image/png` ou `image/jpeg`) e `Content-Disposition: attachment; filename=\"…\"`; suporta `If-None-Match` (304)","en":"binary body with the format's `Content-Type` (`application/pdf`, `image/png` or `image/jpeg`) and `Content-Disposition: attachment; filename=\"…\"`; supports `If-None-Match` (304)","es":"cuerpo binario con `Content-Type` del formato (`application/pdf`, `image/png` o `image/jpeg`) y `Content-Disposition: attachment; filename=\"…\"`; admite `If-None-Match` (304)","fr":"corps binaire avec le `Content-Type` du format (`application/pdf`, `image/png` ou `image/jpeg`) et `Content-Disposition: attachment; filename=\"…\"` ; prend en charge `If-None-Match` (304)"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-download","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-download","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-download","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-download"}},{"id":"pdf.batches","method":"GET","path":"/v1/tools/document/pdf/batches","title":{"pt-br":"Listar batches","en":"List batches","es":"Listar batches","fr":"Lister les batches"},"scopes":["tools:pdf:read","tools:manage"],"billing":{"kind":"metered","operation":"pdf.batch_list"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"items":[{"id":"7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d","status":"partially_failed","total_items":25,"completed_items":24,"failed_items":1,"created_at":"2026-09-24T12:00:00Z"}],"total":3,"page":1,"page_size":20}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-batches","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-batches","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-batches","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-batches"}},{"id":"pdf.batch-get","method":"GET","path":"/v1/tools/document/pdf/batches/{id}","title":{"pt-br":"Detalhe do batch","en":"Retrieve batch","es":"Detalle del batch","fr":"Détail du batch"},"scopes":["tools:pdf:read","tools:manage"],"billing":{"kind":"metered","operation":"pdf.batch_status"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"batch":{"id":"7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d","status":"completed","total_items":1,"completed_items":1,"failed_items":0,"created_at":"2026-09-24T12:00:00Z"},"conversions":[{"id":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d","job_id":"pdf_9b8c7d6e5f","status":"completed","file_name":"fatura-ana.pdf","page_size":"A4","orientation":"portrait","margin_top":10,"margin_bottom":10,"margin_left":10,"margin_right":10,"scale":1,"format":"pdf","enable_js":false,"cost_credits":1.5,"cost_units":15000,"retention_days":7,"expires_at":"2026-10-01T12:00:00Z","is_deleted_from_storage":false,"html":"<html>…</html>","css":"body { font-family: sans-serif; }","created_at":"2026-09-24T12:00:00Z","updated_at":"2026-09-24T12:00:09Z","batch_id":"7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d"}],"template":{"html":"<html><body><h1>{{title}}</h1></body></html>","css":""}}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-batch-get","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-batch-get","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-batch-get","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-batch-get"}},{"id":"pdf.batch-download","method":"GET","path":"/v1/tools/document/pdf/batches/{id}/download","title":{"pt-br":"Download ZIP do batch","en":"Download batch ZIP","es":"Descargar ZIP del batch","fr":"Téléchargement ZIP du batch"},"scopes":["tools:pdf:read","tools:manage"],"billing":{"kind":"metered","operation":"pdf.batch_download"},"dry_run":false,"timeout":"2 minutos","response":{"status":200,"note":{"pt-br":"`application/zip` com os itens concluídos (`batch-<id>.zip`); itens com falha ficam de fora","en":"`application/zip` with the completed items (`batch-<id>.zip`); failed items are left out","es":"`application/zip` con los elementos completados (`batch-<id>.zip`); los elementos con fallo quedan fuera","fr":"`application/zip` avec les éléments terminés (`batch-<id>.zip`) ; les éléments en échec sont exclus"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-batch-download","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-batch-download","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-batch-download","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-batch-download"}},{"id":"pdf.templates.list","method":"GET","path":"/v1/tools/document/pdf/templates","title":{"pt-br":"Listar templates salvos","en":"List saved templates","es":"Listar plantillas guardadas","fr":"Lister les templates enregistrés"},"scopes":["tools:pdf:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"items":[{"id":"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a","name":"Fatura padrão","description":"Fatura mensal com logotipo","current_version":2,"created_at":"2026-10-01T12:00:00Z","updated_at":"2026-10-10T09:30:00Z"}],"total":1}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-templates-list","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-templates-list","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-templates-list","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-templates-list"}},{"id":"pdf.templates.create","method":"POST","path":"/v1/tools/document/pdf/templates","title":{"pt-br":"Salvar template","en":"Save template","es":"Guardar plantilla","fr":"Enregistrer un template"},"scopes":["tools:pdf:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a","name":"Fatura padrão","description":"Fatura mensal com logotipo","current_version":1,"created_at":"2026-10-01T12:00:00Z","updated_at":"2026-10-10T09:30:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-templates-create","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-templates-create","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-templates-create","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-templates-create"}},{"id":"pdf.templates.get","method":"GET","path":"/v1/tools/document/pdf/templates/{id}","title":{"pt-br":"Consultar template","en":"Get template","es":"Consultar plantilla","fr":"Consulter un template"},"scopes":["tools:pdf:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"template":{"id":"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a","name":"Fatura padrão","description":"Fatura mensal com logotipo","current_version":2,"created_at":"2026-10-01T12:00:00Z","updated_at":"2026-10-10T09:30:00Z"},"version":{"template_id":"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a","version":2,"html":"<html><body><h1>Fatura {{numero}}</h1></body></html>","css":"h1 { color: #333; }","header_html":"","footer_html":"","created_at":"2026-10-10T09:30:00Z"}}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-templates-get","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-templates-get","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-templates-get","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-templates-get"}},{"id":"pdf.templates.versions","method":"GET","path":"/v1/tools/document/pdf/templates/{id}/versions","title":{"pt-br":"Listar versões do template","en":"List template versions","es":"Listar versiones de la plantilla","fr":"Lister les versions du template"},"scopes":["tools:pdf:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"items":[{"template_id":"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a","version":2,"html":"<html><body><h1>Fatura {{numero}}</h1></body></html>","css":"h1 { color: #333; }","header_html":"","footer_html":"","created_at":"2026-10-10T09:30:00Z"},{"template_id":"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a","version":1,"html":"<html><body><h1>Fatura</h1></body></html>","css":"h1 { color: #333; }","header_html":"","footer_html":"","created_at":"2026-10-10T09:30:00Z"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-templates-versions","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-templates-versions","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-templates-versions","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-templates-versions"}},{"id":"pdf.templates.update","method":"PATCH","path":"/v1/tools/document/pdf/templates/{id}","title":{"pt-br":"Renomear template ou salvar nova versão","en":"Rename template or save a new version","es":"Renombrar plantilla o guardar una nueva versión","fr":"Renommer le template ou enregistrer une nouvelle version"},"scopes":["tools:pdf:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a","name":"Fatura padrão","description":"Fatura mensal com logotipo","current_version":3,"created_at":"2026-10-01T12:00:00Z","updated_at":"2026-10-10T09:30:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-templates-update","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-templates-update","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-templates-update","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-templates-update"}},{"id":"pdf.templates.delete","method":"DELETE","path":"/v1/tools/document/pdf/templates/{id}","title":{"pt-br":"Excluir template","en":"Delete template","es":"Eliminar plantilla","fr":"Supprimer un template"},"scopes":["tools:pdf:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":204,"note":{"pt-br":"sem corpo; apaga o template e todas as versões","en":"no body; deletes the template and all its versions","es":"sin cuerpo; elimina la plantilla y todas sus versiones","fr":"sans corps ; supprime le template et toutes ses versions"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-templates-delete","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-templates-delete","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-templates-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-templates-delete"}},{"id":"pdf.fonts.list","method":"GET","path":"/v1/tools/document/pdf/fonts","title":{"pt-br":"Listar fontes","en":"List sources","es":"Listar fuentes","fr":"Lister les sources"},"scopes":["tools:pdf:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"items":[{"id":"2b3c4d5e-6f70-4a8b-9c0d-1e2f3a4b5c6d","family":"Inter","weight":400,"style":"normal","format":"woff2","size_bytes":108544,"created_at":"2026-10-05T14:00:00Z"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-fonts-list","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-fonts-list","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-fonts-list","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-fonts-list"}},{"id":"pdf.fonts.upload","method":"POST","path":"/v1/tools/document/pdf/fonts","title":{"pt-br":"Enviar fonte","en":"Upload font","es":"Subir fuente","fr":"Envoyer une police"},"scopes":["tools:pdf:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"2b3c4d5e-6f70-4a8b-9c0d-1e2f3a4b5c6d","family":"Inter","weight":400,"style":"normal","format":"woff2","size_bytes":108544,"created_at":"2026-10-05T14:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-fonts-upload","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-fonts-upload","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-fonts-upload","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-fonts-upload"}},{"id":"pdf.fonts.delete","method":"DELETE","path":"/v1/tools/document/pdf/fonts/{id}","title":{"pt-br":"Excluir fonte","en":"Delete source","es":"Eliminar fuente","fr":"Supprimer une source"},"scopes":["tools:pdf:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":204,"note":{"pt-br":"sem corpo; apaga a fonte e o arquivo armazenado","en":"no body; deletes the font and its stored file","es":"sin cuerpo; elimina la fuente y el archivo almacenado","fr":"sans corps ; supprime la police et le fichier stocké"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/pdf-convert#pdf-fonts-delete","en":"https://docs.respawntech.dev/en/ferramentas/pdf-convert#pdf-fonts-delete","es":"https://docs.respawntech.dev/es/ferramentas/pdf-convert#pdf-fonts-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/pdf-convert#pdf-fonts-delete"}}],"pricing":[{"id":"document","title":{"pt-br":"Por documento","en":"Per document","es":"Por documento","fr":"Par document"},"items":[{"label":{"pt-br":"Conversão base","en":"Base conversion","es":"Conversión base","fr":"Conversion de base"},"service":"pdf_convert","credits":1},{"label":{"pt-br":"Retenção efêmera ou até 3 dias","en":"Ephemeral retention or up to 3 days","es":"Retención efímera o hasta 3 días","fr":"Rétention éphémère ou jusqu'à 3 jours"},"credits":0,"additive":true,"note":{"pt-br":"`retention_days` 0 (~24h) a 3","en":"`retention_days` 0 (~24h) to 3","es":"`retention_days` 0 (~24h) a 3","fr":"`retention_days` 0 (~24 h) à 3"}},{"label":{"pt-br":"Retenção 4–7 dias","en":"Retention 4–7 days","es":"Retención 4–7 días","fr":"Rétention 4–7 jours"},"service":"pdf_retention_7","credits":0.5,"additive":true},{"label":{"pt-br":"Retenção 8–30 dias","en":"Retention 8–30 days","es":"Retención 8–30 días","fr":"Rétention 8–30 jours"},"service":"pdf_retention_30","credits":2.5,"additive":true},{"label":{"pt-br":"Retenção permanente","en":"Permanent retention","es":"Retención permanente","fr":"Rétention permanente"},"service":"pdf_retention_forever","credits":15,"additive":true,"note":{"pt-br":"`retention_days: -1`","en":"`retention_days: -1`","es":"`retention_days: -1`","fr":"`retention_days: -1`"}},{"label":{"pt-br":"Sandbox JavaScript","en":"JavaScript sandbox","es":"Sandbox JavaScript","fr":"Sandbox JavaScript"},"service":"pdf_js_sandbox","credits":0.25,"additive":true,"note":{"pt-br":"`enable_js: true`","en":"`enable_js: true`","es":"`enable_js: true`","fr":"`enable_js: true`"}}]},{"id":"addons","title":{"pt-br":"Pós-processamento e fontes","en":"Post-processing and fonts","es":"Posprocesamiento y fuentes","fr":"Post-traitement et polices"},"description":{"pt-br":"Somados ao custo do documento. Só `pdf` aceita pós-processamento.","en":"Added to the document cost. Only `pdf` accepts post-processing.","es":"Se suman al costo del documento. Solo `pdf` admite posprocesamiento.","fr":"Ajoutés au coût du document. Seul `pdf` accepte le post-traitement."},"items":[{"label":{"pt-br":"Marca d'água","en":"Watermark","es":"Marca de agua","fr":"Filigrane"},"service":"pdf_watermark","credits":0.1,"additive":true,"note":{"pt-br":"`post_process.watermark`","en":"`post_process.watermark`","es":"`post_process.watermark`","fr":"`post_process.watermark`"}},{"label":{"pt-br":"Proteção por senha","en":"Password protection","es":"Protección con contraseña","fr":"Protection par mot de passe"},"service":"pdf_password","credits":0.1,"additive":true,"note":{"pt-br":"`post_process.protection`","en":"`post_process.protection`","es":"`post_process.protection`","fr":"`post_process.protection`"}},{"label":{"pt-br":"PDF/A-2b","en":"PDF/A-2b","es":"PDF/A-2b","fr":"PDF/A-2b"},"service":"pdf_pdfa","credits":0.25,"additive":true,"note":{"pt-br":"`post_process.pdfa`","en":"`post_process.pdfa`","es":"`post_process.pdfa`","fr":"`post_process.pdfa`"}},{"label":{"pt-br":"Fontes personalizadas","en":"Custom fonts","es":"Fuentes personalizadas","fr":"Polices personnalisées"},"service":"pdf_custom_fonts","credits":0.05,"additive":true,"note":{"pt-br":"`font_ids` (uma vez por documento, não por fonte)","en":"`font_ids` (once per document, not per font)","es":"`font_ids` (una vez por documento, no por fuente)","fr":"`font_ids` (une fois par document, pas par police)"}}]},{"id":"batch-discounts","title":{"pt-br":"Desconto de volume no batch","en":"Batch volume discount","es":"Descuento por volumen en el batch","fr":"Remise sur volume du batch"},"description":{"pt-br":"Aplicado sobre base, JavaScript e retenção de cada item.","en":"Applied to each item's base, JavaScript and retention cost.","es":"Se aplica sobre la base, JavaScript y retención de cada elemento.","fr":"Appliquée à la base, au JavaScript et à la rétention de chaque élément."},"items":[{"label":{"pt-br":"10+ itens","en":"10+ items","es":"10+ elementos","fr":"10+ éléments"},"display":{"pt-br":"5%","en":"5%","es":"5%","fr":"5 %"}},{"label":{"pt-br":"20+ itens","en":"20+ items","es":"20+ elementos","fr":"20+ éléments"},"display":{"pt-br":"10%","en":"10%","es":"10%","fr":"10 %"}},{"label":{"pt-br":"30+ itens","en":"30+ items","es":"30+ elementos","fr":"30+ éléments"},"display":{"pt-br":"15%","en":"15%","es":"15%","fr":"15 %"}},{"label":{"pt-br":"40+ itens","en":"40+ items","es":"40+ elementos","fr":"40+ éléments"},"display":{"pt-br":"20%","en":"20%","es":"20%","fr":"20 %"}}]}]},{"slug":"verificacao-de-url","category":"security","code":"safety","base_path":"/v1/tools/safety","status":"published","name":{"pt-br":"Verificação de URL","en":"URL Safety Check","es":"Verificación de URL","fr":"Vérification d'URL"},"summary":{"pt-br":"Checagem de URLs maliciosas e proxy de favicon por domínio.","en":"Malicious URL checks and a per-domain favicon proxy.","es":"Comprobación de URL maliciosas y proxy de favicon por dominio.","fr":"Détection d'URL malveillantes et proxy de favicon par domaine."},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/verificacao-de-url","en":"https://docs.respawntech.dev/en/ferramentas/verificacao-de-url","es":"https://docs.respawntech.dev/es/ferramentas/verificacao-de-url","fr":"https://docs.respawntech.dev/fr/ferramentas/verificacao-de-url"},"endpoints":[{"id":"safety.check","method":"GET","path":"/v1/tools/safety/check","title":{"pt-br":"Verificar URL","en":"Check URL","es":"Verificar URL","fr":"Vérifier une URL"},"scopes":["tools:safety:read","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"safety.check"},"dry_run":false,"response":{"status":200,"body":{"is_safe":true},"note":{"pt-br":"fora do envelope padrão","en":"outside the standard envelope","es":"fuera del envelope estándar","fr":"hors de l'enveloppe standard"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/verificacao-de-url#safety-check","en":"https://docs.respawntech.dev/en/ferramentas/verificacao-de-url#safety-check","es":"https://docs.respawntech.dev/es/ferramentas/verificacao-de-url#safety-check","fr":"https://docs.respawntech.dev/fr/ferramentas/verificacao-de-url#safety-check"}},{"id":"safety.favicon","method":"GET","path":"/v1/tools/safety/favicon","title":{"pt-br":"Favicon de um domínio","en":"Domain favicon","es":"Favicon de un dominio","fr":"Favicon d'un domaine"},"scopes":["tools:safety:read","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"safety.favicon"},"dry_run":false,"response":{"status":200,"note":{"pt-br":"imagem binária (64×64) com o `Content-Type` original e `Cache-Control: public, max-age=86400`","en":"binary image (64×64) with the original `Content-Type` and `Cache-Control: public, max-age=86400`","es":"imagen binaria (64×64) con el `Content-Type` original y `Cache-Control: public, max-age=86400`","fr":"image binaire (64×64) avec le `Content-Type` d'origine et `Cache-Control: public, max-age=86400`"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/verificacao-de-url#safety-favicon","en":"https://docs.respawntech.dev/en/ferramentas/verificacao-de-url#safety-favicon","es":"https://docs.respawntech.dev/es/ferramentas/verificacao-de-url#safety-favicon","fr":"https://docs.respawntech.dev/fr/ferramentas/verificacao-de-url#safety-favicon"}}],"pricing":[]},{"slug":"base-de-conhecimento","category":"ai","code":"rag","base_path":"/v1/tools/rag","status":"beta","name":{"pt-br":"Base de conhecimento","en":"Knowledge base","es":"Base de conocimiento","fr":"Base de connaissances"},"summary":{"pt-br":"Busca semântica e híbrida nos seus documentos, para RAG e assistentes.","en":"Semantic and hybrid search over your documents, for RAG and assistants.","es":"Búsqueda semántica e híbrida en tus documentos, para RAG y asistentes.","fr":"Recherche sémantique et hybride dans vos documents, pour le RAG et les assistants."},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento"},"endpoints":[{"id":"rag.collections.create","method":"POST","path":"/v1/tools/rag/collections","title":{"pt-br":"Criar base","en":"Create knowledge base","es":"Crear base","fr":"Créer une base"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"3c9e7a52-1b4d-4f0e-9a6c-2d8e5f7a1b3c","name":"Central de ajuda","description":"Artigos públicos de suporte","embedding_model":"intfloat/multilingual-e5-small","language":"portuguese","chunk_size_tokens":96,"chunk_overlap_tokens":16,"section_max_tokens":768,"default_mode":"fast","max_per_document":2,"dense_weight":1,"lexical_weight":1,"feedback_enabled":true,"document_count":0,"chunk_count":0,"created_at":"2026-09-20T12:00:00Z","updated_at":"2026-09-24T09:30:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-collections-create","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-collections-create","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-collections-create","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-collections-create"}},{"id":"rag.collections.list","method":"GET","path":"/v1/tools/rag/collections","title":{"pt-br":"Listar bases","en":"List knowledge bases","es":"Listar bases","fr":"Lister les bases"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"collections":[{"id":"3c9e7a52-1b4d-4f0e-9a6c-2d8e5f7a1b3c","name":"Central de ajuda","description":"Artigos públicos de suporte","embedding_model":"intfloat/multilingual-e5-small","language":"portuguese","chunk_size_tokens":96,"chunk_overlap_tokens":16,"section_max_tokens":768,"default_mode":"fast","max_per_document":2,"dense_weight":1,"lexical_weight":1,"feedback_enabled":true,"document_count":128,"chunk_count":4210,"created_at":"2026-09-20T12:00:00Z","updated_at":"2026-09-24T09:30:00Z"}],"usage":{"collections":1,"max_collections":50,"chunks":4210,"max_chunks":200000,"pending_documents":0,"max_pending_documents":20}}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-collections-list","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-collections-list","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-collections-list","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-collections-list"}},{"id":"rag.collections.get","method":"GET","path":"/v1/tools/rag/collections/{collection_id}","title":{"pt-br":"Consultar base","en":"Retrieve knowledge base","es":"Consultar base","fr":"Consulter une base"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"3c9e7a52-1b4d-4f0e-9a6c-2d8e5f7a1b3c","name":"Central de ajuda","description":"Artigos públicos de suporte","embedding_model":"intfloat/multilingual-e5-small","language":"portuguese","chunk_size_tokens":96,"chunk_overlap_tokens":16,"section_max_tokens":768,"default_mode":"fast","max_per_document":2,"dense_weight":1,"lexical_weight":1,"feedback_enabled":true,"document_count":128,"chunk_count":4210,"created_at":"2026-09-20T12:00:00Z","updated_at":"2026-09-24T09:30:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-collections-get","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-collections-get","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-collections-get","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-collections-get"}},{"id":"rag.collections.facets","method":"GET","path":"/v1/tools/rag/collections/{collection_id}/facets","title":{"pt-br":"Filtros disponíveis","en":"Available filters","es":"Filtros disponibles","fr":"Filtres disponibles"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"documents":128,"sampled":false,"keys":2,"facets":[{"key":"categoria","documents":120,"distinct":3,"values":[{"value":"conta","documents":64},{"value":"cobranca","documents":41},{"value":"api","documents":15}]},{"key":"tags","documents":37,"array":true,"distinct":2,"values":[{"value":"faq","documents":30},{"value":"legado","documents":7}]}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-collections-facets","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-collections-facets","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-collections-facets","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-collections-facets"}},{"id":"rag.collections.update","method":"PATCH","path":"/v1/tools/rag/collections/{collection_id}","title":{"pt-br":"Alterar base","en":"Update knowledge base","es":"Modificar base","fr":"Modifier une base"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"3c9e7a52-1b4d-4f0e-9a6c-2d8e5f7a1b3c","name":"Central de ajuda","description":"Artigos públicos de suporte","embedding_model":"intfloat/multilingual-e5-small","language":"portuguese","chunk_size_tokens":96,"chunk_overlap_tokens":16,"section_max_tokens":768,"default_mode":"fast","max_per_document":2,"dense_weight":1,"lexical_weight":1,"feedback_enabled":true,"document_count":128,"chunk_count":4210,"created_at":"2026-09-20T12:00:00Z","updated_at":"2026-09-24T09:30:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-collections-update","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-collections-update","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-collections-update","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-collections-update"}},{"id":"rag.collections.delete","method":"DELETE","path":"/v1/tools/rag/collections/{collection_id}","title":{"pt-br":"Excluir base","en":"Delete knowledge base","es":"Eliminar base","fr":"Supprimer une base"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"3c9e7a52-1b4d-4f0e-9a6c-2d8e5f7a1b3c","status":"deleting"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-collections-delete","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-collections-delete","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-collections-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-collections-delete"}},{"id":"rag.collections.reindex","method":"POST","path":"/v1/tools/rag/collections/{collection_id}/reindex","title":{"pt-br":"Reindexar base","en":"Reindex knowledge base","es":"Reindexar base","fr":"Réindexer une base"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Reprocessa todos os documentos; cada trecho é cobrado como na ingestão","en":"Reprocesses every document; each chunk is billed as in ingestion","es":"Reprocesa todos los documentos; cada fragmento se cobra como en la ingesta","fr":"Retraite tous les documents ; chaque fragment est facturé comme à l'ingestion"}},"dry_run":false,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"documents_enqueued":128}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-collections-reindex","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-collections-reindex","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-collections-reindex","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-collections-reindex"}},{"id":"rag.documents.add","method":"POST","path":"/v1/tools/rag/collections/{collection_id}/documents","title":{"pt-br":"Adicionar ou substituir documento","en":"Add or replace document","es":"Añadir o reemplazar documento","fr":"Ajouter ou remplacer un document"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por trecho indexado, cobrado quando a ingestão termina; conteúdo idêntico não é cobrado de novo","en":"Per indexed chunk, billed when ingestion finishes; identical content is not billed again","es":"Por fragmento indexado, cobrado cuando termina la ingesta; el contenido idéntico no se vuelve a cobrar","fr":"Par fragment indexé, facturé à la fin de l'ingestion ; un contenu identique n'est pas refacturé"}},"dry_run":true,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"document_id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","status":"pending","estimate":{"chunks":18,"credit_units":1800},"document":{"id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","collection_id":"3c9e7a52-1b4d-4f0e-9a6c-2d8e5f7a1b3c","external_id":"artigo-42","title":"Como trocar a senha","source_uri":"https://ajuda.example.com/artigos/42","status":"pending","processing":false,"byte_size":5120,"section_count":0,"chunk_count":0,"metadata":{"categoria":"conta"},"created_at":"2026-09-24T09:30:00Z","updated_at":"2026-09-24T09:30:05Z"}}},"note":{"pt-br":"ingestão assíncrona; `200` em dry-run (só `estimate`) e quando o conteúdo é idêntico ao atual (`duplicate: true`)","en":"asynchronous ingestion; `200` on dry-run (only `estimate`) and when the content is identical to the current one (`duplicate: true`)","es":"ingesta asíncrona; `200` en dry-run (solo `estimate`) y cuando el contenido es idéntico al actual (`duplicate: true`)","fr":"ingestion asynchrone ; `200` en dry-run (uniquement `estimate`) et quand le contenu est identique à l'actuel (`duplicate: true`)"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-documents-add","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-documents-add","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-documents-add","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-documents-add"}},{"id":"rag.documents.list","method":"GET","path":"/v1/tools/rag/collections/{collection_id}/documents","title":{"pt-br":"Listar documentos","en":"List documents","es":"Listar documentos","fr":"Lister les documents"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"documents":[{"id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","collection_id":"3c9e7a52-1b4d-4f0e-9a6c-2d8e5f7a1b3c","external_id":"artigo-42","title":"Como trocar a senha","source_uri":"https://ajuda.example.com/artigos/42","status":"ready","processing":false,"byte_size":5120,"section_count":4,"chunk_count":18,"metadata":{"categoria":"conta"},"created_at":"2026-09-24T09:30:00Z","updated_at":"2026-09-24T09:30:05Z"}],"next_cursor":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-documents-list","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-documents-list","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-documents-list","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-documents-list"}},{"id":"rag.documents.get","method":"GET","path":"/v1/tools/rag/collections/{collection_id}/documents/{document_id}","title":{"pt-br":"Consultar documento","en":"Retrieve document","es":"Consultar documento","fr":"Consulter un document"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","collection_id":"3c9e7a52-1b4d-4f0e-9a6c-2d8e5f7a1b3c","external_id":"artigo-42","title":"Como trocar a senha","source_uri":"https://ajuda.example.com/artigos/42","status":"ready","processing":false,"byte_size":5120,"section_count":4,"chunk_count":18,"metadata":{"categoria":"conta"},"created_at":"2026-09-24T09:30:00Z","updated_at":"2026-09-24T09:30:05Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-documents-get","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-documents-get","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-documents-get","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-documents-get"}},{"id":"rag.documents.update","method":"PATCH","path":"/v1/tools/rag/collections/{collection_id}/documents/{document_id}","title":{"pt-br":"Alterar título ou metadados","en":"Update title or metadata","es":"Modificar título o metadatos","fr":"Modifier le titre ou les métadonnées"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","collection_id":"3c9e7a52-1b4d-4f0e-9a6c-2d8e5f7a1b3c","external_id":"artigo-42","title":"Como trocar a senha","source_uri":"https://ajuda.example.com/artigos/42","status":"ready","processing":false,"byte_size":5120,"section_count":4,"chunk_count":18,"metadata":{"categoria":"conta"},"created_at":"2026-09-24T09:30:00Z","updated_at":"2026-09-24T09:30:05Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-documents-update","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-documents-update","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-documents-update","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-documents-update"}},{"id":"rag.documents.delete","method":"DELETE","path":"/v1/tools/rag/collections/{collection_id}/documents/{document_id}","title":{"pt-br":"Excluir documento","en":"Delete document","es":"Eliminar documento","fr":"Supprimer un document"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","status":"deleting"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-documents-delete","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-documents-delete","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-documents-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-documents-delete"}},{"id":"rag.documents.retry","method":"POST","path":"/v1/tools/rag/collections/{collection_id}/documents/{document_id}/retry","title":{"pt-br":"Reprocessar documento com falha","en":"Retry failed document","es":"Reprocesar documento fallido","fr":"Relancer un document en échec"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por trecho indexado, como na ingestão","en":"Per indexed chunk, as in ingestion","es":"Por fragmento indexado, como en la ingesta","fr":"Par fragment indexé, comme à l'ingestion"}},"dry_run":false,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","collection_id":"3c9e7a52-1b4d-4f0e-9a6c-2d8e5f7a1b3c","external_id":"artigo-42","title":"Como trocar a senha","source_uri":"https://ajuda.example.com/artigos/42","status":"pending","processing":false,"byte_size":5120,"section_count":4,"chunk_count":18,"metadata":{"categoria":"conta"},"created_at":"2026-09-24T09:30:00Z","updated_at":"2026-09-24T09:30:05Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-documents-retry","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-documents-retry","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-documents-retry","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-documents-retry"}},{"id":"rag.documents.sections","method":"GET","path":"/v1/tools/rag/collections/{collection_id}/documents/{document_id}/sections","title":{"pt-br":"Ver seções indexadas","en":"View indexed sections","es":"Ver secciones indexadas","fr":"Voir les sections indexées"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"sections":[{"id":"b2c4d6e8-0a1b-4c3d-8e5f-6a7b8c9d0e1f","index":0,"heading_path":["Conta","Como trocar a senha"],"content":"Acesse Configurações > Segurança e clique em Trocar senha…","token_count":84}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-documents-sections","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-documents-sections","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-documents-sections","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-documents-sections"}},{"id":"rag.query","method":"POST","path":"/v1/tools/rag/collections/{collection_id}/query","title":{"pt-br":"Buscar na base","en":"Query knowledge base","es":"Buscar en la base","fr":"Rechercher dans une base"},"scopes":["tools:rag:query","tools:rag:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por consulta, com franquia diária e blocos pré-pagos (veja Custos)","en":"Per query, with a daily free quota and prepaid blocks (see Costs)","es":"Por consulta, con cuota diaria gratuita y bloques prepagos (ver Costos)","fr":"Par requête, avec quota gratuit quotidien et blocs prépayés (voir Coûts)"}},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"query_id":"f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f","mode_used":"fast","cached":false,"results":[{"section_id":"b2c4d6e8-0a1b-4c3d-8e5f-6a7b8c9d0e1f","document_id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","title":"Como trocar a senha","heading_path":["Conta","Como trocar a senha"],"section_index":3,"content":"Acesse Configurações > Segurança e clique em Trocar senha…","highlights":[{"start":38,"end":49}],"score":0.87,"signals":{"dense_rank":1,"lexical_rank":2},"metadata":{"categoria":"conta"}}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-query","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-query","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-query","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-query"}},{"id":"rag.feedback.add","method":"POST","path":"/v1/tools/rag/collections/{collection_id}/feedback","title":{"pt-br":"Registrar feedback de relevância","en":"Record relevance feedback","es":"Registrar feedback de relevancia","fr":"Enregistrer un feedback de pertinence"},"scopes":["tools:rag:query","tools:rag:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"recorded":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-feedback-add","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-feedback-add","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-feedback-add","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-feedback-add"}},{"id":"rag.feedback.list","method":"GET","path":"/v1/tools/rag/collections/{collection_id}/feedback","title":{"pt-br":"Listar feedback","en":"List feedback","es":"Listar feedback","fr":"Lister le feedback"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"groups":[{"id":"a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d","sample_query":"como mudo minha senha","query_count":12,"updated_at":"2026-09-24T18:00:00Z","votes":[{"document_id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","section_hash":"5f2c…","label":"positive","source":"end_user","confirmations":7,"updated_at":"2026-09-24T18:00:00Z","answer":{"section_id":"b2c4d6e8-0a1b-4c3d-8e5f-6a7b8c9d0e1f","section_index":3,"document_title":"Como trocar a senha","heading_path":"Conta › Como trocar a senha","preview":"Acesse Configurações > Segurança e clique em Trocar senha…"}}]}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-feedback-list","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-feedback-list","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-feedback-list","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-feedback-list"}},{"id":"rag.feedback.delete","method":"DELETE","path":"/v1/tools/rag/collections/{collection_id}/feedback/{cluster_id}","title":{"pt-br":"Apagar um grupo de feedback","en":"Delete a feedback group","es":"Eliminar un grupo de feedback","fr":"Supprimer un groupe de feedback"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"deleted":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-feedback-delete","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-feedback-delete","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-feedback-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-feedback-delete"}},{"id":"rag.rerank","method":"POST","path":"/v1/tools/rag/rerank","title":{"pt-br":"Reordenar textos (rerank)","en":"Rerank texts","es":"Reordenar textos (rerank)","fr":"Réordonner des textes (rerank)"},"scopes":["tools:rag:query","tools:rag:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por chamada, com franquia diária e blocos pré-pagos (veja Custos)","en":"Per call, with a daily free quota and prepaid blocks (see Costs)","es":"Por llamada, con cuota diaria gratuita y bloques prepagos (ver Costos)","fr":"Par appel, avec quota gratuit quotidien et blocs prépayés (voir Coûts)"}},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"results":[{"index":2,"score":0.93},{"index":0,"score":0.41}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-rerank","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-rerank","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-rerank","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-rerank"}},{"id":"rag.decide","method":"POST","path":"/v1/tools/rag/decide","title":{"pt-br":"Decidir entre opções (roteamento)","en":"Decide between options (routing)","es":"Decidir entre opciones (enrutamiento)","fr":"Décider entre des options (routage)"},"scopes":["tools:rag:query","tools:rag:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por chamada, como o rerank avulso (veja Custos)","en":"Per call, like standalone rerank (see Costs)","es":"Por llamada, como el rerank independiente (consulta Costos)","fr":"Par appel, comme le rerank autonome (voir Coûts)"}},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"decided":true,"label":"cobranca","score":0.92,"margin":0.61,"ranking":[{"label":"cobranca","score":0.92},{"label":"suporte","score":0.31}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-decide","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-decide","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-decide","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-decide"}},{"id":"rag.pricing","method":"GET","path":"/v1/tools/rag/pricing","title":{"pt-br":"Consultar preços","en":"Retrieve pricing","es":"Consultar precios","fr":"Consulter les prix"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"collection_free":true,"feedback_free":true,"ingest":{"unit_cost":200,"volume_discounts":[{"min_quantity":1000,"discount_percentage":20}]},"calls":[{"service_code":"rag_query_search","unit_cost":2500,"free_per_day":20,"first_block_size":10,"block_size":20}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-pricing","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-pricing","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-pricing","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-pricing"}},{"id":"rag.limits","method":"GET","path":"/v1/tools/rag/limits","title":{"pt-br":"Consultar limites","en":"Retrieve limits","es":"Consultar límites","fr":"Consulter les limites"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"max_collections":50,"max_chunks":200000,"max_pending_documents":20,"max_document_bytes":5242880,"max_query_chars":1000,"max_top_k":20}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-limits","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-limits","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-limits","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-limits"}},{"id":"rag.collections.overview","method":"GET","path":"/v1/tools/rag/collections/{collection_id}/overview","title":{"pt-br":"Painel de saúde da base","en":"Knowledge base health dashboard","es":"Panel de salud de la base","fr":"Tableau de santé de la base"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"counts":{"total":128,"by_status":{"pending":0,"processing":0,"ready":126,"failed":2},"stale":4,"never_accessed":31,"without_metadata":8,"total_bytes":912000,"total_sections":540},"matrix":{"rows_key":"categoria","cols_key":"","rows":["conta","api"],"cols":[],"cells":[{"row":"conta","col":"","count":64}]},"activity":{"key":"","days":30,"values":[],"series":[{"date":"2026-10-20","total":12,"counts":{}}]},"attention":{"failed":{"count":2,"items":[{"id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","title":"Como trocar a senha"}]},"stale":{"count":4,"items":[]},"lint":{"count":1,"items":[]},"negative_feedback":{"count":0,"items":[]}},"generated_at":"2026-10-20T12:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-collections-overview","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-collections-overview","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-collections-overview","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-collections-overview"}},{"id":"rag.collections.index","method":"GET","path":"/v1/tools/rag/collections/{collection_id}/index","title":{"pt-br":"Índice dos documentos","en":"Document index","es":"Índice de los documentos","fr":"Index des documents"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"total":126,"omitted":0,"documents":[{"document_id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","external_id":"artigo-42","title":"Como trocar a senha","updated_at":"2026-09-24T09:30:05Z","metadata":{"categoria":"conta"}}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-collections-index","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-collections-index","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-collections-index","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-collections-index"}},{"id":"rag.collections.lint","method":"POST","path":"/v1/tools/rag/collections/{collection_id}/lint","title":{"pt-br":"Achar duplicados e contradições","en":"Find duplicates and contradictions","es":"Encontrar duplicados y contradicciones","fr":"Trouver les doublons et les contradictions"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"documents":126,"total":1,"truncated":false,"found":1,"findings":[{"kind":"duplicate","similarity":0.93,"newer":{"document_id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","external_id":"artigo-42","title":"Como trocar a senha","updated_at":"2026-09-24T09:30:05Z"},"older":{"document_id":"9d8c7b6a-5f4e-4d3c-8b2a-1f0e9d8c7b6a","external_id":"artigo-7","title":"Trocar senha","updated_at":"2026-08-02T10:00:00Z"},"keep":"artigo-42","action":"rewrite `artigo-42` merging the unique content of `artigo-7`, then delete `artigo-7`"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-collections-lint","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-collections-lint","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-collections-lint","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-collections-lint"}},{"id":"rag.collections.model","method":"POST","path":"/v1/tools/rag/collections/{collection_id}/model","title":{"pt-br":"Trocar o modelo de embeddings","en":"Switch the embeddings model","es":"Cambiar el modelo de embeddings","fr":"Changer le modèle d'embeddings"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Reindexa a base com desconto de 50%; voltar ao modelo anterior é grátis por 24 h","en":"Reindexes the knowledge base at a 50% discount; going back to the previous model is free for 24 h","es":"Reindexa la base con un 50% de descuento; volver al modelo anterior es gratis durante 24 h","fr":"Réindexe la base avec 50 % de remise ; revenir au modèle précédent est gratuit pendant 24 h"}},"dry_run":true,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"embedding_model":"BAAI/bge-m3","documents_enqueued":0,"documents_remaining":128,"estimate":{"documents":128,"chunks":4210,"credit_units":421000}}},"note":{"pt-br":"`200` com só `estimate` em dry-run","en":"`200` with only `estimate` in dry-run","es":"`200` con solo `estimate` en dry-run","fr":"`200` avec uniquement `estimate` en dry-run"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-collections-model","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-collections-model","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-collections-model","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-collections-model"}},{"id":"rag.collections.changes","method":"POST","path":"/v1/tools/rag/collections/{collection_id}/changes","title":{"pt-br":"Marcar documentos como desatualizados","en":"Mark documents as outdated","es":"Marcar documentos como desactualizados","fr":"Marquer des documents comme périmés"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"marked":1,"document_ids":["7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e"]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-collections-changes","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-collections-changes","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-collections-changes","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-collections-changes"}},{"id":"rag.answer","method":"POST","path":"/v1/tools/rag/collections/{collection_id}/answer","title":{"pt-br":"Responder com citações","en":"Answer with citations","es":"Responder con citas","fr":"Répondre avec des citations"},"scopes":["tools:rag:query","tools:rag:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Como uma busca no modo `accurate` (veja Custos)","en":"Like a search in `accurate` mode (see Costs)","es":"Como una búsqueda en modo `accurate` (consulta Costos)","fr":"Comme une recherche en mode `accurate` (voir Coûts)"}},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"query_id":"f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f","answered":true,"answer":"Acesse Configurações > Segurança e clique em Trocar senha… [1]","generated":false,"citations":[{"marker":1,"section_id":"b2c4d6e8-0a1b-4c3d-8e5f-6a7b8c9d0e1f","document_id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","title":"Como trocar a senha","heading_path":"Conta › Como trocar a senha","source_uri":"https://ajuda.example.com/artigos/42","score":0.91,"excerpt":"Acesse Configurações > Segurança e clique em Trocar senha…"}]}},"note":{"pt-br":"JSON, ou `text/event-stream` com `Accept: text/event-stream` (eventos `citations`, `delta` e `done`)","en":"JSON, or `text/event-stream` with `Accept: text/event-stream` (`citations`, `delta` and `done` events)","es":"JSON, o `text/event-stream` con `Accept: text/event-stream` (eventos `citations`, `delta` y `done`)","fr":"JSON, ou `text/event-stream` avec `Accept: text/event-stream` (événements `citations`, `delta` et `done`)"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-answer","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-answer","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-answer","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-answer"}},{"id":"rag.analytics","method":"GET","path":"/v1/tools/rag/collections/{collection_id}/analytics","title":{"pt-br":"Analytics das perguntas","en":"Question analytics","es":"Analytics de las preguntas","fr":"Analytics des questions"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"days":30,"questions":340,"unanswered":41,"answer_rate":0.88,"feedback_ctr":0.12,"positive_rate":0.9,"top_questions":[{"question":"como mudo minha senha","count":52,"unanswered":0,"last_asked":"2026-10-20T11:40:00Z"}],"unanswered_questions":[{"question":"tem integração com zapier","count":9,"unanswered":9,"last_asked":"2026-10-20T10:02:00Z"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-analytics","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-analytics","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-analytics","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-analytics"}},{"id":"rag.publickeys.create","method":"POST","path":"/v1/tools/rag/collections/{collection_id}/public-keys","title":{"pt-br":"Criar chave pública (widget)","en":"Create public key (widget)","es":"Crear clave pública (widget)","fr":"Créer une clé publique (widget)"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"d1e2f3a4-b5c6-4d7e-8f90-a1b2c3d4e5f6","name":"Site institucional","key_prefix":"rpk_Ab3dEf","allowed_origins":["https://www.example.com","https://*.example.com"],"rate_per_minute":30,"generate":false,"created_at":"2026-10-20T12:00:00Z","key":"rpk_Ab3dEf…"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-publickeys-create","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-publickeys-create","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-publickeys-create","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-publickeys-create"}},{"id":"rag.publickeys.list","method":"GET","path":"/v1/tools/rag/collections/{collection_id}/public-keys","title":{"pt-br":"Listar chaves públicas","en":"List public keys","es":"Listar claves públicas","fr":"Lister les clés publiques"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"public_keys":[{"id":"d1e2f3a4-b5c6-4d7e-8f90-a1b2c3d4e5f6","name":"Site institucional","key_prefix":"rpk_Ab3dEf","allowed_origins":["https://www.example.com","https://*.example.com"],"rate_per_minute":30,"generate":false,"created_at":"2026-10-20T12:00:00Z"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-publickeys-list","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-publickeys-list","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-publickeys-list","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-publickeys-list"}},{"id":"rag.publickeys.revoke","method":"DELETE","path":"/v1/tools/rag/collections/{collection_id}/public-keys/{key_id}","title":{"pt-br":"Revogar chave pública","en":"Revoke public key","es":"Revocar clave pública","fr":"Révoquer une clé publique"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"revoked":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-publickeys-revoke","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-publickeys-revoke","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-publickeys-revoke","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-publickeys-revoke"}},{"id":"rag.public.answer","method":"POST","path":"/v1/public/rag/answer","title":{"pt-br":"Responder pelo widget (rota pública)","en":"Answer through the widget (public route)","es":"Responder desde el widget (ruta pública)","fr":"Répondre via le widget (route publique)"},"scopes":[],"billing":{"kind":"paid","summary":{"pt-br":"Cobrada do dono da base, como o `answer` (veja Custos)","en":"Charged to the knowledge base owner, like `answer` (see Costs)","es":"Se cobra al dueño de la base, como `answer` (consulta Costos)","fr":"Facturée au propriétaire de la base, comme `answer` (voir Coûts)"}},"dry_run":false,"auth":"public","response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"query_id":"f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f","answered":true,"answer":"Acesse Configurações > Segurança e clique em Trocar senha… [1]","generated":false,"citations":[{"marker":1,"section_id":"b2c4d6e8-0a1b-4c3d-8e5f-6a7b8c9d0e1f","document_id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","title":"Como trocar a senha","heading_path":"Conta › Como trocar a senha","source_uri":"https://ajuda.example.com/artigos/42","score":0.91,"excerpt":"Acesse Configurações > Segurança e clique em Trocar senha…"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-public-answer","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-public-answer","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-public-answer","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-public-answer"}},{"id":"rag.widget","method":"GET","path":"/widget/rag.js","title":{"pt-br":"Script do widget","en":"Widget script","es":"Script del widget","fr":"Script du widget"},"scopes":[],"billing":{"kind":"free"},"dry_run":false,"auth":"public","response":{"status":200,"note":{"pt-br":"`application/javascript`, com cache de 5 minutos","en":"`application/javascript`, cached for 5 minutes","es":"`application/javascript`, con caché de 5 minutos","fr":"`application/javascript`, avec cache de 5 minutes"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-widget","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-widget","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-widget","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-widget"}},{"id":"rag.sources.list","method":"GET","path":"/v1/tools/rag/collections/{collection_id}/sources","title":{"pt-br":"Listar fontes sincronizadas","en":"List synced sources","es":"Listar fuentes sincronizadas","fr":"Lister les sources synchronisées"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"sources":[{"id":"c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f","collection_id":"3c9e7a52-1b4d-4f0e-9a6c-2d8e5f7a1b3c","kind":"site","name":"Central de ajuda","start_url":"https://ajuda.example.com","config":{},"max_pages":100,"max_depth":3,"frequency_hours":24,"enabled":true,"status":"ok","next_sync_at":"2026-10-21T12:00:00Z","last_sync":{"id":"e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a0b","status":"completed","started_at":"2026-10-20T12:00:00Z","finished_at":"2026-10-20T12:03:10Z","pages_total":42,"pages_new":3,"pages_changed":1,"pages_removed":0,"error":null},"created_at":"2026-10-15T09:00:00Z"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-sources-list","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-sources-list","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-sources-list","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-sources-list"}},{"id":"rag.sources.create","method":"POST","path":"/v1/tools/rag/collections/{collection_id}/sources","title":{"pt-br":"Criar fonte sincronizada","en":"Create synced source","es":"Crear fuente sincronizada","fr":"Créer une source synchronisée"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Cada página nova ou alterada é cobrada como uma ingestão","en":"Each new or changed page is charged as an ingestion","es":"Cada página nueva o modificada se cobra como una ingesta","fr":"Chaque page nouvelle ou modifiée est facturée comme une ingestion"}},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f","collection_id":"3c9e7a52-1b4d-4f0e-9a6c-2d8e5f7a1b3c","kind":"site","name":"Central de ajuda","start_url":"https://ajuda.example.com","config":{},"max_pages":100,"max_depth":3,"frequency_hours":24,"enabled":true,"status":"syncing","next_sync_at":"2026-10-21T12:00:00Z","last_sync":{"id":"e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a0b","status":"completed","started_at":"2026-10-20T12:00:00Z","finished_at":"2026-10-20T12:03:10Z","pages_total":42,"pages_new":3,"pages_changed":1,"pages_removed":0,"error":null},"created_at":"2026-10-15T09:00:00Z"}},"note":{"pt-br":"a primeira sincronização começa na hora (`status: syncing`)","en":"the first sync starts right away (`status: syncing`)","es":"la primera sincronización empieza al instante (`status: syncing`)","fr":"la première synchronisation démarre aussitôt (`status: syncing`)"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-sources-create","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-sources-create","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-sources-create","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-sources-create"}},{"id":"rag.sources.update","method":"PATCH","path":"/v1/tools/rag/collections/{collection_id}/sources/{source_id}","title":{"pt-br":"Alterar fonte","en":"Update source","es":"Modificar fuente","fr":"Modifier une source"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f","collection_id":"3c9e7a52-1b4d-4f0e-9a6c-2d8e5f7a1b3c","kind":"site","name":"Central de ajuda","start_url":"https://ajuda.example.com","config":{},"max_pages":100,"max_depth":3,"frequency_hours":24,"enabled":true,"status":"ok","next_sync_at":"2026-10-21T12:00:00Z","last_sync":{"id":"e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a0b","status":"completed","started_at":"2026-10-20T12:00:00Z","finished_at":"2026-10-20T12:03:10Z","pages_total":42,"pages_new":3,"pages_changed":1,"pages_removed":0,"error":null},"created_at":"2026-10-15T09:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-sources-update","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-sources-update","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-sources-update","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-sources-update"}},{"id":"rag.sources.delete","method":"DELETE","path":"/v1/tools/rag/collections/{collection_id}/sources/{source_id}","title":{"pt-br":"Excluir fonte","en":"Delete source","es":"Eliminar fuente","fr":"Supprimer une source"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"deleted":true}},"note":{"pt-br":"os documentos já importados continuam na base","en":"documents already imported stay in the knowledge base","es":"los documentos ya importados permanecen en la base","fr":"les documents déjà importés restent dans la base"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-sources-delete","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-sources-delete","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-sources-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-sources-delete"}},{"id":"rag.sources.sync","method":"POST","path":"/v1/tools/rag/collections/{collection_id}/sources/{source_id}/sync","title":{"pt-br":"Sincronizar agora","en":"Sync now","es":"Sincronizar ahora","fr":"Synchroniser maintenant"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Cada página nova ou alterada é cobrada como uma ingestão","en":"Each new or changed page is charged as an ingestion","es":"Cada página nueva o modificada se cobra como una ingesta","fr":"Chaque page nouvelle ou modifiée est facturée comme une ingestion"}},"dry_run":false,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a0b","status":"completed","started_at":"2026-10-20T12:00:00Z","finished_at":"2026-10-20T12:03:10Z","pages_total":42,"pages_new":3,"pages_changed":1,"pages_removed":0,"error":null}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-sources-sync","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-sources-sync","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-sources-sync","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-sources-sync"}},{"id":"rag.sources.syncs","method":"GET","path":"/v1/tools/rag/collections/{collection_id}/sources/{source_id}/syncs","title":{"pt-br":"Histórico de sincronizações","en":"Sync history","es":"Historial de sincronizaciones","fr":"Historique des synchronisations"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"syncs":[{"id":"e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a0b","status":"completed","started_at":"2026-10-20T12:00:00Z","finished_at":"2026-10-20T12:03:10Z","pages_total":42,"pages_new":3,"pages_changed":1,"pages_removed":0,"error":null}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-sources-syncs","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-sources-syncs","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-sources-syncs","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-sources-syncs"}},{"id":"rag.documents.file","method":"POST","path":"/v1/tools/rag/collections/{collection_id}/documents/file","title":{"pt-br":"Importar arquivo (PDF, Office, imagem)","en":"Import file (PDF, Office, image)","es":"Importar archivo (PDF, Office, imagen)","fr":"Importer un fichier (PDF, Office, image)"},"scopes":["tools:rag:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"A conversão não cobra; o texto resultante é cobrado por trecho, como na ingestão","en":"Conversion is free; the resulting text is charged per chunk, as in ingestion","es":"La conversión no se cobra; el texto resultante se cobra por fragmento, como en la ingesta","fr":"La conversion n'est pas facturée ; le texte obtenu est facturé par fragment, comme pour l'ingestion"}},"dry_run":false,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"import_id":"5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e","status":"queued"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-documents-file","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-documents-file","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-documents-file","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-documents-file"}},{"id":"rag.imports.get","method":"GET","path":"/v1/tools/rag/collections/{collection_id}/imports/{import_id}","title":{"pt-br":"Consultar importação de arquivo","en":"Retrieve file import","es":"Consultar importación de archivo","fr":"Consulter l'importation d'un fichier"},"scopes":["tools:rag:read","tools:rag:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e","filename":"manual.pdf","status":"completed","document_id":"7a1f2e9c-5b3d-4c8a-9e0f-1d2c3b4a5f6e","error":null,"created_at":"2026-10-20T12:00:00Z","updated_at":"2026-10-20T12:00:40Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/base-de-conhecimento#rag-imports-get","en":"https://docs.respawntech.dev/en/ferramentas/base-de-conhecimento#rag-imports-get","es":"https://docs.respawntech.dev/es/ferramentas/base-de-conhecimento#rag-imports-get","fr":"https://docs.respawntech.dev/fr/ferramentas/base-de-conhecimento#rag-imports-get"}}],"pricing":[{"id":"per-use","title":{"pt-br":"Por uso","en":"Per use","es":"Por uso","fr":"À l'usage"},"description":{"pt-br":"Valores por unidade. Buscas e rerank têm franquia diária e são cobrados em blocos pré-pagos (tabelas seguintes). A ingestão tem desconto no documento: −20% a partir de 1.000 trechos e −40% a partir de 10.000.","en":"Per-unit prices. Searches and rerank have a daily free quota and are charged in prepaid blocks (next tables). Ingestion gets a per-document discount: −20% from 1,000 chunks and −40% from 10,000.","es":"Valores por unidad. Las búsquedas y el rerank tienen cuota diaria y se cobran en bloques prepagados (tablas siguientes). La ingesta tiene descuento por documento: −20% desde 1.000 fragmentos y −40% desde 10.000.","fr":"Prix unitaires. Les recherches et le rerank ont un quota quotidien et sont facturés par blocs prépayés (tableaux suivants). L'ingestion bénéficie d'une remise par document : −20 % à partir de 1 000 fragments et −40 % à partir de 10 000."},"items":[{"label":{"pt-br":"Ingestão, por trecho indexado","en":"Ingestion, per indexed chunk","es":"Ingesta, por fragmento indexado","fr":"Ingestion, par fragment indexé"},"service":"rag_embed_ingest","credits":0.02,"note":{"pt-br":"modelo padrão; trecho padrão ≈ 96 tokens","en":"default model; default chunk ≈ 96 tokens","es":"modelo estándar; fragmento estándar ≈ 96 tokens","fr":"modèle standard ; fragment standard ≈ 96 tokens"}},{"label":{"pt-br":"Ingestão Alta precisão, por trecho","en":"High-precision ingestion, per chunk","es":"Ingesta Alta precisión, por fragmento","fr":"Ingestion Haute précision, par fragment"},"service":"rag_hq_embed_ingest","credits":0.06,"note":{"pt-br":"bases com o modelo Alta precisão","en":"knowledge bases using the High-precision model","es":"bases con el modelo Alta precisión","fr":"bases utilisant le modèle Haute précision"}},{"label":{"pt-br":"Busca (modo `fast`)","en":"Search (`fast` mode)","es":"Búsqueda (modo `fast`)","fr":"Recherche (mode `fast`)"},"service":"rag_query_search","credits":0.25},{"label":{"pt-br":"Busca Alta precisão","en":"High-precision search","es":"Búsqueda Alta precisión","fr":"Recherche Haute précision"},"service":"rag_hq_query_search","credits":0.4,"note":{"pt-br":"substitui a busca padrão nas bases Alta precisão","en":"replaces the standard search in High-precision bases","es":"reemplaza la búsqueda estándar en las bases Alta precisión","fr":"remplace la recherche standard dans les bases Haute précision"}},{"label":{"pt-br":"Busca respondida do cache","en":"Search served from cache","es":"Búsqueda respondida desde la caché","fr":"Recherche servie depuis le cache"},"service":"rag_query_search_cached","credits":0.02,"note":{"pt-br":"mesma consulta nos últimos 10 min","en":"same query in the last 10 min","es":"misma consulta en los últimos 10 min","fr":"même requête dans les 10 dernières min"}},{"label":{"pt-br":"Rerank no modo `accurate`","en":"Rerank in `accurate` mode","es":"Rerank en modo `accurate`","fr":"Rerank en mode `accurate`"},"service":"rag_query_accurate","credits":0.08,"additive":true,"note":{"pt-br":"sem cobrança se cair para `fast`","en":"not billed if it falls back to `fast`","es":"sin cobro si pasa a `fast`","fr":"non facturé en cas de repli sur `fast`"}},{"label":{"pt-br":"Rerank avulso, por chamada","en":"Standalone rerank, per call","es":"Rerank independiente, por llamada","fr":"Rerank autonome, par appel"},"service":"rag_rerank","credits":0.08}]},{"id":"free-quota","title":{"pt-br":"Franquia diária","en":"Daily free quota","es":"Cuota diaria gratuita","fr":"Quota quotidien gratuit"},"description":{"pt-br":"Chamadas grátis por conta e por dia (UTC). Depois delas, a cobrança é em blocos pré-pagos.","en":"Free calls per account per day (UTC). After them, calls are charged in prepaid blocks.","es":"Llamadas gratis por cuenta y por día (UTC). Después, el cobro es en bloques prepagados.","fr":"Appels gratuits par compte et par jour (UTC). Au-delà, la facturation se fait par blocs prépayés."},"items":[{"label":{"pt-br":"Buscas","en":"Searches","es":"Búsquedas","fr":"Recherches"},"display":{"pt-br":"20","en":"20","es":"20","fr":"20"}},{"label":{"pt-br":"Buscas Alta precisão","en":"High-precision searches","es":"Búsquedas Alta precisión","fr":"Recherches Haute précision"},"display":{"pt-br":"5","en":"5","es":"5","fr":"5"}},{"label":{"pt-br":"Buscas respondidas do cache","en":"Searches served from cache","es":"Búsquedas respondidas desde la caché","fr":"Recherches servies depuis le cache"},"display":{"pt-br":"50","en":"50","es":"50","fr":"50"}},{"label":{"pt-br":"Rerank no modo `accurate`","en":"Rerank in `accurate` mode","es":"Rerank en modo `accurate`","fr":"Rerank en mode `accurate`"},"display":{"pt-br":"5","en":"5","es":"5","fr":"5"}},{"label":{"pt-br":"Rerank avulso","en":"Standalone rerank","es":"Rerank independiente","fr":"Rerank autonome"},"display":{"pt-br":"5","en":"5","es":"5","fr":"5"}}]},{"id":"blocks","title":{"pt-br":"Blocos pré-pagos","en":"Prepaid blocks","es":"Bloques prepagados","fr":"Blocs prépayés"},"description":{"pt-br":"Depois da franquia, a chamada que abre um bloco cobra o bloco inteiro de uma vez; as seguintes do bloco não cobram de novo. Os blocos crescem com o uso pago do dia, igual em todos os serviços. As chamadas não usadas de um bloco expiram à meia-noite UTC, sem estorno.","en":"After the free quota, the call that opens a block charges the whole block at once; the next calls in that block are not charged again. Blocks grow with the day's paid usage, the same for every service. Unused calls in a block expire at midnight UTC, without a refund.","es":"Tras la cuota gratuita, la llamada que abre un bloque cobra el bloque entero de una vez; las siguientes del bloque no se vuelven a cobrar. Los bloques crecen con el uso pagado del día, igual en todos los servicios. Las llamadas no usadas de un bloque vencen a medianoche UTC, sin reembolso.","fr":"Après le quota gratuit, l'appel qui ouvre un bloc facture le bloc entier d'un coup ; les appels suivants du bloc ne sont pas refacturés. Les blocs grandissent avec l'usage payant de la journée, de la même façon pour tous les services. Les appels non utilisés d'un bloc expirent à minuit UTC, sans remboursement."},"items":[{"label":{"pt-br":"Primeiro bloco pago do dia","en":"First paid block of the day","es":"Primer bloque pagado del día","fr":"Premier bloc payant de la journée"},"display":{"pt-br":"10","en":"10","es":"10","fr":"10"},"note":{"pt-br":"ex.: 10 buscas = 2,5 créditos","en":"e.g. 10 searches = 2.5 credits","es":"ej.: 10 búsquedas = 2,5 créditos","fr":"ex. : 10 recherches = 2,5 crédits"}},{"label":{"pt-br":"Até 200 chamadas pagas no dia","en":"Up to 200 paid calls in the day","es":"Hasta 200 llamadas pagadas en el día","fr":"Jusqu'à 200 appels payants dans la journée"},"display":{"pt-br":"20","en":"20","es":"20","fr":"20"}},{"label":{"pt-br":"A partir de 200 chamadas pagas","en":"From 200 paid calls","es":"Desde 200 llamadas pagadas","fr":"À partir de 200 appels payants"},"display":{"pt-br":"50","en":"50","es":"50","fr":"50"}},{"label":{"pt-br":"A partir de 1.000 chamadas pagas","en":"From 1,000 paid calls","es":"Desde 1.000 llamadas pagadas","fr":"À partir de 1 000 appels payants"},"display":{"pt-br":"100","en":"100","es":"100","fr":"100"},"note":{"pt-br":"com −40% (desconto por volume)","en":"with −40% (volume discount)","es":"con −40% (descuento por volumen)","fr":"avec −40 % (remise sur volume)"}}]},{"id":"volume","title":{"pt-br":"Desconto por volume diário","en":"Daily volume discount","es":"Descuento por volumen diario","fr":"Remise quotidienne sur volume"},"description":{"pt-br":"Por serviço, conforme as chamadas pagas que os blocos anteriores do dia já somam. Soma-se ao desconto dos pacotes de crédito.","en":"Per service, based on the paid calls the day's earlier blocks already add up to. Stacks with the credit package discount.","es":"Por servicio, según las llamadas pagadas que ya suman los bloques anteriores del día. Se suma al descuento de los paquetes de créditos.","fr":"Par service, selon les appels payants déjà cumulés par les blocs précédents de la journée. S'ajoute à la remise des packs de crédits."},"items":[{"label":{"pt-br":"A partir de 1.000 chamadas pagas no dia","en":"From 1,000 paid calls in the day","es":"Desde 1.000 llamadas pagadas en el día","fr":"À partir de 1 000 appels payants dans la journée"},"display":{"pt-br":"−40%","en":"−40%","es":"−40%","fr":"−40 %"}},{"label":{"pt-br":"A partir de 10.000 chamadas pagas no dia","en":"From 10,000 paid calls in the day","es":"Desde 10.000 llamadas pagadas en el día","fr":"À partir de 10 000 appels payants dans la journée"},"display":{"pt-br":"−70%","en":"−70%","es":"−70%","fr":"−70 %"}}]},{"id":"storage","title":{"pt-br":"Armazenamento","en":"Storage","es":"Almacenamiento","fr":"Stockage"},"description":{"pt-br":"Cobrado uma vez por dia sobre o total de trechos guardados na conta.","en":"Billed once a day on the total number of chunks stored in the account.","es":"Se cobra una vez al día sobre el total de fragmentos guardados en la cuenta.","fr":"Facturé une fois par jour sur le total des fragments stockés dans le compte."},"items":[{"label":{"pt-br":"Primeiros 2.000 trechos","en":"First 2,000 chunks","es":"Primeros 2.000 fragmentos","fr":"2 000 premiers fragments"},"credits":0},{"label":{"pt-br":"Cada bloco iniciado de 10.000 trechos","en":"Each started block of 10,000 chunks","es":"Cada bloque iniciado de 10.000 fragmentos","fr":"Chaque bloc entamé de 10 000 fragments"},"display":{"pt-br":"0,1 / dia","en":"0.1 / day","es":"0,1 / día","fr":"0,1 / jour"}}]}]},{"slug":"webhook-relay","category":"integrations","code":"webhook_relay","base_path":"/v1/tools/webhooks","status":"beta","name":{"pt-br":"Webhook Relay","en":"Webhook Relay","es":"Webhook Relay","fr":"Webhook Relay"},"summary":{"pt-br":"Envie webhooks assinados para os seus clientes, com retry, log de tentativas e replay.","en":"Send signed webhooks to your customers, with retries, an attempt log and replay.","es":"Envía webhooks firmados a tus clientes, con reintentos, registro de intentos y replay.","fr":"Envoyez des webhooks signés à vos clients, avec relances, journal des tentatives et rejeu."},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay"},"endpoints":[{"id":"webhooks.projects.list","method":"GET","path":"/v1/tools/webhooks/projects","title":{"pt-br":"Listar projetos","en":"List projects","es":"Listar proyectos","fr":"Lister les projets"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"projects":[{"id":"7a1d4c8e-2f6b-4e9a-b3c5-0d8f1a6e4b27","name":"Default","description":"","is_default":true,"created_at":"2026-09-25T11:40:00Z","updated_at":"2026-09-25T11:40:00Z","stats":{"sources":0,"endpoints":1,"messages_24h":3,"deliveries_month":57,"failing_endpoints":0}},{"id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","name":"Loja","description":"Eventos da loja virtual","is_default":false,"created_at":"2026-09-25T11:40:00Z","updated_at":"2026-09-25T11:40:00Z","stats":{"sources":1,"endpoints":2,"messages_24h":12,"deliveries_month":340,"failing_endpoints":0}}],"total":2,"page":1,"page_size":50}},"note":{"pt-br":"o projeto padrão vem primeiro; `total` conta os projetos que atendem à busca","en":"the default project comes first; `total` counts the projects matching the search","es":"el proyecto por defecto va primero; `total` cuenta los proyectos que coinciden con la búsqueda","fr":"le projet par défaut vient en premier ; `total` compte les projets correspondant à la recherche"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-projects-list","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-projects-list","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-projects-list","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-projects-list"}},{"id":"webhooks.projects.create","method":"POST","path":"/v1/tools/webhooks/projects","title":{"pt-br":"Criar projeto","en":"Create project","es":"Crear proyecto","fr":"Créer un projet"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","name":"Loja","description":"Eventos da loja virtual","is_default":false,"created_at":"2026-09-25T11:40:00Z","updated_at":"2026-09-25T11:40:00Z","stats":{"sources":0,"endpoints":0,"messages_24h":0,"deliveries_month":0,"failing_endpoints":0}}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-projects-create","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-projects-create","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-projects-create","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-projects-create"}},{"id":"webhooks.projects.get","method":"GET","path":"/v1/tools/webhooks/projects/{id}","title":{"pt-br":"Consultar projeto","en":"Retrieve project","es":"Consultar proyecto","fr":"Consulter un projet"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","name":"Loja","description":"Eventos da loja virtual","is_default":false,"created_at":"2026-09-25T11:40:00Z","updated_at":"2026-09-25T11:40:00Z","stats":{"sources":1,"endpoints":2,"messages_24h":12,"deliveries_month":340,"failing_endpoints":0}}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-projects-get","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-projects-get","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-projects-get","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-projects-get"}},{"id":"webhooks.projects.update","method":"PATCH","path":"/v1/tools/webhooks/projects/{id}","title":{"pt-br":"Alterar projeto","en":"Update project","es":"Modificar proyecto","fr":"Modifier un projet"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","name":"Loja principal","description":"Eventos da loja virtual","is_default":false,"created_at":"2026-09-25T11:40:00Z","updated_at":"2026-09-25T13:00:00Z","stats":{"sources":1,"endpoints":2,"messages_24h":12,"deliveries_month":340,"failing_endpoints":0}}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-projects-update","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-projects-update","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-projects-update","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-projects-update"}},{"id":"webhooks.projects.delete","method":"DELETE","path":"/v1/tools/webhooks/projects/{id}","title":{"pt-br":"Excluir projeto","en":"Delete project","es":"Eliminar proyecto","fr":"Supprimer un projet"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"deleted":true}},"note":{"pt-br":"o projeto padrão não pode ser excluído (`400`)","en":"the default project cannot be deleted (`400`)","es":"el proyecto por defecto no se puede eliminar (`400`)","fr":"le projet par défaut ne peut pas être supprimé (`400`)"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-projects-delete","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-projects-delete","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-projects-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-projects-delete"}},{"id":"webhooks.projects.duplicate","method":"POST","path":"/v1/tools/webhooks/projects/{id}/duplicate","title":{"pt-br":"Duplicar projeto","en":"Duplicate project","es":"Duplicar proyecto","fr":"Dupliquer un projet"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"b8e2f5a1-4c7d-4a3e-9f6b-2d1c8e5a7f40","name":"Loja (cópia)","description":"Eventos da loja virtual","is_default":false,"created_at":"2026-09-25T14:00:00Z","updated_at":"2026-09-25T14:00:00Z","stats":{"sources":1,"endpoints":2,"messages_24h":0,"deliveries_month":0,"failing_endpoints":0}}},"note":{"pt-br":"endpoints com secrets novos e fontes com `ingest_url` novas; mensagens e contadores não são copiados","en":"endpoints get new secrets and sources new `ingest_url`s; messages and counters are not copied","es":"endpoints con secrets nuevos y fuentes con `ingest_url` nuevas; los mensajes y contadores no se copian","fr":"endpoints avec de nouveaux secrets et sources avec de nouvelles `ingest_url` ; les messages et compteurs ne sont pas copiés"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-projects-duplicate","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-projects-duplicate","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-projects-duplicate","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-projects-duplicate"}},{"id":"webhooks.usage","method":"GET","path":"/v1/tools/webhooks/usage","title":{"pt-br":"Consultar uso do mês","en":"Retrieve this month's usage","es":"Consultar el uso del mes","fr":"Consulter l’usage du mois"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"period_start":"2026-09-01T00:00:00Z","period_end":"2026-10-01T00:00:00Z","free_deliveries":1000,"used_deliveries":340,"free_left":660,"delivery_units":20,"read_units":20,"blocked_retention_days":7}},"note":{"pt-br":"valores da conta inteira, somando todos os projetos; preços em unidades (10.000 = 1 crédito)","en":"whole-account values, across every project; prices in units (10,000 = 1 credit)","es":"valores de toda la cuenta, sumando todos los proyectos; precios en unidades (10.000 = 1 crédito)","fr":"valeurs du compte entier, tous projets confondus ; prix en unités (10 000 = 1 crédit)"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-usage","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-usage","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-usage","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-usage"}},{"id":"webhooks.endpoints.create","method":"POST","path":"/v1/tools/webhooks/endpoints","title":{"pt-br":"Criar endpoint","en":"Create endpoint","es":"Crear endpoint","fr":"Créer un endpoint"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"endpoint":{"id":"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","url":"https://api.example.com/hooks/respawn","description":"Pedidos da loja","events":["order.paid","order.refunded"],"custom_headers":{"X-Tenant":"loja-1"},"is_active":true,"rate_limit_per_sec":0,"format":"raw","created_at":"2026-09-25T12:00:00Z","updated_at":"2026-09-25T12:00:00Z"},"secret":{"secret":"9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c2e4b6d8f0a1c3e5b7d9f1a3c5e7b9d0f","standard_webhooks_secret":"whsec_OWYyYzRlNmE4YjBkMWYzZTVhN2M5ZTFiM2Q1ZjdhOWMyZTRiNmQ4ZjBhMWMzZTViN2Q5ZjFhM2M1ZTdiOWQwZg=="}}},"note":{"pt-br":"o `secret` só aparece aqui e na rota de revelar o secret","en":"the `secret` is only shown here and by the reveal-secret route","es":"el `secret` solo aparece aquí y en la ruta para revelar el secret","fr":"le `secret` n’apparaît qu’ici et via la route de révélation du secret"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-endpoints-create","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-endpoints-create","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-endpoints-create","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-endpoints-create"}},{"id":"webhooks.endpoints.list","method":"GET","path":"/v1/tools/webhooks/endpoints","title":{"pt-br":"Listar endpoints","en":"List endpoints","es":"Listar endpoints","fr":"Lister les endpoints"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"endpoints":[{"id":"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","url":"https://api.example.com/hooks/respawn","description":"Pedidos da loja","events":["order.paid","order.refunded"],"custom_headers":{"X-Tenant":"loja-1"},"is_active":true,"rate_limit_per_sec":0,"format":"raw","created_at":"2026-09-25T12:00:00Z","updated_at":"2026-09-25T12:00:00Z"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-endpoints-list","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-endpoints-list","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-endpoints-list","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-endpoints-list"}},{"id":"webhooks.endpoints.get","method":"GET","path":"/v1/tools/webhooks/endpoints/{id}","title":{"pt-br":"Consultar endpoint","en":"Retrieve endpoint","es":"Consultar endpoint","fr":"Consulter un endpoint"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","url":"https://api.example.com/hooks/respawn","description":"Pedidos da loja","events":["order.paid","order.refunded"],"custom_headers":{"X-Tenant":"loja-1"},"is_active":true,"rate_limit_per_sec":0,"format":"raw","created_at":"2026-09-25T12:00:00Z","updated_at":"2026-09-25T12:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-endpoints-get","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-endpoints-get","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-endpoints-get","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-endpoints-get"}},{"id":"webhooks.endpoints.update","method":"PATCH","path":"/v1/tools/webhooks/endpoints/{id}","title":{"pt-br":"Alterar endpoint","en":"Update endpoint","es":"Modificar endpoint","fr":"Modifier un endpoint"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","url":"https://api.example.com/hooks/respawn","description":"Pedidos da loja","events":["*"],"custom_headers":{"X-Tenant":"loja-1"},"is_active":true,"rate_limit_per_sec":0,"format":"raw","created_at":"2026-09-25T12:00:00Z","updated_at":"2026-09-25T13:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-endpoints-update","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-endpoints-update","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-endpoints-update","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-endpoints-update"}},{"id":"webhooks.endpoints.delete","method":"DELETE","path":"/v1/tools/webhooks/endpoints/{id}","title":{"pt-br":"Excluir endpoint","en":"Delete endpoint","es":"Eliminar endpoint","fr":"Supprimer un endpoint"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"deleted":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-endpoints-delete","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-endpoints-delete","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-endpoints-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-endpoints-delete"}},{"id":"webhooks.endpoints.secret","method":"GET","path":"/v1/tools/webhooks/endpoints/{id}/secret","title":{"pt-br":"Revelar secret","en":"Reveal secret","es":"Revelar secret","fr":"Révéler le secret"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"secret":"9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c2e4b6d8f0a1c3e5b7d9f1a3c5e7b9d0f","standard_webhooks_secret":"whsec_OWYyYzRlNmE4YjBkMWYzZTVhN2M5ZTFiM2Q1ZjdhOWMyZTRiNmQ4ZjBhMWMzZTViN2Q5ZjFhM2M1ZTdiOWQwZg=="}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-endpoints-secret","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-endpoints-secret","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-endpoints-secret","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-endpoints-secret"}},{"id":"webhooks.endpoints.rotate_secret","method":"POST","path":"/v1/tools/webhooks/endpoints/{id}/rotate-secret","title":{"pt-br":"Trocar secret","en":"Rotate secret","es":"Rotar secret","fr":"Renouveler le secret"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"secret":"4a6c8e0b2d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a6c","standard_webhooks_secret":"whsec_NGE2YzhlMGIyZDRmNmE4YzBlMmI0ZDZmOGEwYzJlNGI2ZDhmMGEyYzRlNmI4ZDBmMmE0YzZlOGIwZDJmNGE2Yw=="}},"note":{"pt-br":"o secret anterior continua assinando por 24 horas","en":"the previous secret keeps signing for 24 hours","es":"el secret anterior sigue firmando durante 24 horas","fr":"l’ancien secret continue de signer pendant 24 heures"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-endpoints-rotate-secret","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-endpoints-rotate-secret","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-endpoints-rotate-secret","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-endpoints-rotate-secret"}},{"id":"webhooks.endpoints.test","method":"POST","path":"/v1/tools/webhooks/endpoints/{id}/test","title":{"pt-br":"Enviar ping de teste","en":"Send test ping","es":"Enviar ping de prueba","fr":"Envoyer un ping de test"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"message_id":"c1a7e5d3-9b2f-4a6e-8d4c-0f3b7e1a5c92"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-endpoints-test","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-endpoints-test","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-endpoints-test","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-endpoints-test"}},{"id":"webhooks.endpoints.recover","method":"POST","path":"/v1/tools/webhooks/endpoints/{id}/recover","title":{"pt-br":"Reenviar entregas com falha","en":"Replay failed deliveries","es":"Reenviar entregas fallidas","fr":"Rejouer les livraisons en échec"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Cada entrega reenviada é cobrada como uma nova (conta na franquia mensal)","en":"Each replayed delivery is charged as a new one (counts toward the monthly free quota)","es":"Cada entrega reenviada se cobra como una nueva (cuenta en la cuota mensual gratuita)","fr":"Chaque livraison rejouée est facturée comme une nouvelle (compte dans le quota mensuel gratuit)"}},"dry_run":false,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"message_id":"c1a7e5d3-9b2f-4a6e-8d4c-0f3b7e1a5c92","duplicate":false,"deliveries":12,"free_deliveries":12,"charged_units":0}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-endpoints-recover","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-endpoints-recover","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-endpoints-recover","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-endpoints-recover"}},{"id":"webhooks.messages.send","method":"POST","path":"/v1/tools/webhooks/messages","title":{"pt-br":"Enviar mensagem","en":"Send message","es":"Enviar mensaje","fr":"Envoyer un message"},"scopes":["tools:webhooks:send","tools:webhooks:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por entrega (mensagem × endpoint) além da franquia mensal","en":"Per delivery (message × endpoint) beyond the monthly free quota","es":"Por entrega (mensaje × endpoint) más allá de la cuota mensual gratuita","fr":"Par livraison (message × endpoint) au-delà du quota mensuel gratuit"}},"dry_run":true,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"message_id":"c1a7e5d3-9b2f-4a6e-8d4c-0f3b7e1a5c92","duplicate":false,"deliveries":2,"free_deliveries":0,"charged_units":40}},"note":{"pt-br":"`200` em dry-run (`dry_run: true`, nada é enviado) e quando a chave de idempotência já foi usada (`duplicate: true`, sem cobrança)","en":"`200` in dry-run (`dry_run: true`, nothing is sent) and when the idempotency key was already used (`duplicate: true`, no charge)","es":"`200` en dry-run (`dry_run: true`, no se envía nada) y cuando la clave de idempotencia ya se usó (`duplicate: true`, sin cobro)","fr":"`200` en dry-run (`dry_run: true`, rien n’est envoyé) et quand la clé d’idempotence a déjà servi (`duplicate: true`, sans facturation)"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-messages-send","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-messages-send","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-messages-send","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-messages-send"}},{"id":"webhooks.messages.batch","method":"POST","path":"/v1/tools/webhooks/messages/batch","title":{"pt-br":"Enviar mensagens em lote","en":"Send messages in batch","es":"Enviar mensajes en lote","fr":"Envoyer des messages par lot"},"scopes":["tools:webhooks:send","tools:webhooks:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por entrega, cada mensagem cobrada separadamente","en":"Per delivery, each message charged separately","es":"Por entrega, cada mensaje se cobra por separado","fr":"Par livraison, chaque message facturé séparément"}},"dry_run":true,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"results":[{"message_id":"c1a7e5d3-9b2f-4a6e-8d4c-0f3b7e1a5c92","duplicate":false,"deliveries":2,"free_deliveries":0,"charged_units":40},{"message_id":"00000000-0000-0000-0000-000000000000","duplicate":false,"deliveries":0,"free_deliveries":0,"charged_units":0,"error":{"code":"webhook.payload_invalid","message":"payload does not match the event type schema: missing property \"amount\""}}]}},"note":{"pt-br":"um item por mensagem, na ordem enviada; um item com erro traz `error` e não impede os demais","en":"one item per message, in the order sent; a failed item carries `error` and does not stop the others","es":"un ítem por mensaje, en el orden enviado; un ítem con error trae `error` y no impide los demás","fr":"un élément par message, dans l’ordre d’envoi ; un élément en erreur porte `error` sans bloquer les autres"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-messages-batch","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-messages-batch","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-messages-batch","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-messages-batch"}},{"id":"webhooks.messages.list","method":"GET","path":"/v1/tools/webhooks/messages","title":{"pt-br":"Listar mensagens","en":"List messages","es":"Listar mensajes","fr":"Lister les messages"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"webhook_relay.messages"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"messages":[{"id":"c1a7e5d3-9b2f-4a6e-8d4c-0f3b7e1a5c92","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","event_type":"order.paid","origin":"api","created_at":"2026-09-25T12:05:00Z","deliveries":[{"id":"e4b9c2a7-1d6f-4e3b-a8c5-2f7d0b9e6a13","endpoint_id":"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34","target_url":"https://api.example.com/hooks/respawn","status":"succeeded","attempt_count":2,"last_status_code":200,"charged":true,"created_at":"2026-09-25T12:05:00Z"}]}],"next_before":"2026-09-25T12:05:00.123456Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-messages-list","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-messages-list","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-messages-list","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-messages-list"}},{"id":"webhooks.messages.get","method":"GET","path":"/v1/tools/webhooks/messages/{id}","title":{"pt-br":"Consultar mensagem","en":"Retrieve message","es":"Consultar mensaje","fr":"Consulter un message"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"webhook_relay.messages"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"c1a7e5d3-9b2f-4a6e-8d4c-0f3b7e1a5c92","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","event_type":"order.paid","origin":"api","body":{"event_id":"c1a7e5d3-9b2f-4a6e-8d4c-0f3b7e1a5c92","type":"order.paid","timestamp":1790338000,"payload":{"order_id":"ped_1042","amount":199.9}},"created_at":"2026-09-25T12:05:00Z","deliveries":[{"id":"e4b9c2a7-1d6f-4e3b-a8c5-2f7d0b9e6a13","endpoint_id":"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34","target_url":"https://api.example.com/hooks/respawn","status":"succeeded","attempt_count":2,"last_status_code":200,"charged":true,"created_at":"2026-09-25T12:05:00Z","attempts":[{"attempt":1,"http_status":503,"latency_ms":412,"response_body":"Service Unavailable","created_at":"2026-09-25T12:05:00Z"},{"attempt":2,"http_status":200,"latency_ms":88,"created_at":"2026-09-25T12:05:06Z"}]}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-messages-get","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-messages-get","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-messages-get","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-messages-get"}},{"id":"webhooks.messages.retry","method":"POST","path":"/v1/tools/webhooks/messages/{id}/retry","title":{"pt-br":"Reenviar mensagem","en":"Retry message","es":"Reenviar mensaje","fr":"Renvoyer un message"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Cada entrega reenviada é cobrada como uma nova (conta na franquia mensal)","en":"Each replayed delivery is charged as a new one (counts toward the monthly free quota)","es":"Cada entrega reenviada se cobra como una nueva (cuenta en la cuota mensual gratuita)","fr":"Chaque livraison rejouée est facturée comme une nouvelle (compte dans le quota mensuel gratuit)"}},"dry_run":false,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"message_id":"c1a7e5d3-9b2f-4a6e-8d4c-0f3b7e1a5c92","duplicate":false,"deliveries":1,"free_deliveries":0,"charged_units":20}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-messages-retry","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-messages-retry","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-messages-retry","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-messages-retry"}},{"id":"webhooks.event_types.create","method":"POST","path":"/v1/tools/webhooks/event-types","title":{"pt-br":"Declarar tipo de evento","en":"Declare event type","es":"Declarar tipo de evento","fr":"Déclarer un type d’événement"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"5b3e8f1a-7c2d-4a9e-b6f0-1d4c8e2a7b59","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","name":"order.paid","description":"Pedido pago","schema":{"type":"object","required":["order_id","amount"],"properties":{"order_id":{"type":"string"},"amount":{"type":"number"}}},"created_at":"2026-09-25T11:50:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-event-types-create","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-event-types-create","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-event-types-create","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-event-types-create"}},{"id":"webhooks.event_types.list","method":"GET","path":"/v1/tools/webhooks/event-types","title":{"pt-br":"Listar tipos de evento","en":"List event types","es":"Listar tipos de evento","fr":"Lister les types d’événement"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"event_types":[{"id":"5b3e8f1a-7c2d-4a9e-b6f0-1d4c8e2a7b59","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","name":"order.paid","description":"Pedido pago","schema":{"type":"object","required":["order_id","amount"],"properties":{"order_id":{"type":"string"},"amount":{"type":"number"}}},"created_at":"2026-09-25T11:50:00Z"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-event-types-list","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-event-types-list","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-event-types-list","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-event-types-list"}},{"id":"webhooks.event_types.delete","method":"DELETE","path":"/v1/tools/webhooks/event-types/{id}","title":{"pt-br":"Excluir tipo de evento","en":"Delete event type","es":"Eliminar tipo de evento","fr":"Supprimer un type d’événement"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"deleted":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-event-types-delete","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-event-types-delete","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-event-types-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-event-types-delete"}},{"id":"webhooks.sources.create","method":"POST","path":"/v1/tools/webhooks/sources","title":{"pt-br":"Criar fonte de ingestão","en":"Create ingestion source","es":"Crear fuente de ingesta","fr":"Créer une source d’ingestion"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","name":"Stripe produção","provider":"stripe","ingest_url":"https://api.respawntech.dev/in/src_4f8a2c6e1b9d3f7a5c0e8b2d6f4a1c9e7b3d5f0a2c8e6b4d","has_verification_secret":true,"event_type_path":"type","dedupe_key_path":"id","forward_original_headers":false,"is_active":true,"created_at":"2026-09-25T12:10:00Z","updated_at":"2026-09-25T12:10:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-sources-create","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-sources-create","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-sources-create","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-sources-create"}},{"id":"webhooks.sources.list","method":"GET","path":"/v1/tools/webhooks/sources","title":{"pt-br":"Listar fontes","en":"List sources","es":"Listar fuentes","fr":"Lister les sources"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"sources":[{"id":"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","name":"Stripe produção","provider":"stripe","ingest_url":"https://api.respawntech.dev/in/src_4f8a2c6e1b9d3f7a5c0e8b2d6f4a1c9e7b3d5f0a2c8e6b4d","has_verification_secret":true,"event_type_path":"type","dedupe_key_path":"id","forward_original_headers":false,"is_active":true,"created_at":"2026-09-25T12:10:00Z","updated_at":"2026-09-25T12:10:00Z"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-sources-list","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-sources-list","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-sources-list","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-sources-list"}},{"id":"webhooks.sources.get","method":"GET","path":"/v1/tools/webhooks/sources/{id}","title":{"pt-br":"Consultar fonte","en":"Retrieve source","es":"Consultar fuente","fr":"Consulter une source"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","name":"Stripe produção","provider":"stripe","ingest_url":"https://api.respawntech.dev/in/src_4f8a2c6e1b9d3f7a5c0e8b2d6f4a1c9e7b3d5f0a2c8e6b4d","has_verification_secret":true,"event_type_path":"type","dedupe_key_path":"id","forward_original_headers":false,"is_active":true,"created_at":"2026-09-25T12:10:00Z","updated_at":"2026-09-25T12:10:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-sources-get","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-sources-get","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-sources-get","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-sources-get"}},{"id":"webhooks.sources.update","method":"PATCH","path":"/v1/tools/webhooks/sources/{id}","title":{"pt-br":"Alterar fonte","en":"Update source","es":"Modificar fuente","fr":"Modifier une source"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","name":"Stripe produção","provider":"stripe","ingest_url":"https://api.respawntech.dev/in/src_4f8a2c6e1b9d3f7a5c0e8b2d6f4a1c9e7b3d5f0a2c8e6b4d","has_verification_secret":true,"event_type_path":"type","dedupe_key_path":"id","forward_original_headers":true,"is_active":true,"created_at":"2026-09-25T12:10:00Z","updated_at":"2026-09-25T13:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-sources-update","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-sources-update","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-sources-update","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-sources-update"}},{"id":"webhooks.sources.delete","method":"DELETE","path":"/v1/tools/webhooks/sources/{id}","title":{"pt-br":"Excluir fonte","en":"Delete source","es":"Eliminar fuente","fr":"Supprimer une source"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"deleted":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-sources-delete","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-sources-delete","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-sources-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-sources-delete"}},{"id":"webhooks.sources.rotate_token","method":"POST","path":"/v1/tools/webhooks/sources/{id}/rotate-token","title":{"pt-br":"Trocar URL de ingestão","en":"Rotate ingestion URL","es":"Rotar URL de ingesta","fr":"Renouveler l’URL d’ingestion"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","name":"Stripe produção","provider":"stripe","ingest_url":"https://api.respawntech.dev/in/src_9c1e7a3f5b2d8e4a6c0f9b1d3e7a5c2f8b4d6e0a1c3f5b7d","has_verification_secret":true,"event_type_path":"type","dedupe_key_path":"id","forward_original_headers":false,"is_active":true,"created_at":"2026-09-25T12:10:00Z","updated_at":"2026-09-25T13:00:00Z"}},"note":{"pt-br":"a `ingest_url` anterior para de funcionar na hora","en":"the previous `ingest_url` stops working immediately","es":"la `ingest_url` anterior deja de funcionar al instante","fr":"l’ancienne `ingest_url` cesse de fonctionner immédiatement"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-sources-rotate-token","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-sources-rotate-token","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-sources-rotate-token","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-sources-rotate-token"}},{"id":"webhooks.sources.verification_secret","method":"PUT","path":"/v1/tools/webhooks/sources/{id}/verification-secret","title":{"pt-br":"Definir secret do provedor","en":"Set provider secret","es":"Definir secret del proveedor","fr":"Définir le secret du fournisseur"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","name":"Stripe produção","provider":"stripe","ingest_url":"https://api.respawntech.dev/in/src_4f8a2c6e1b9d3f7a5c0e8b2d6f4a1c9e7b3d5f0a2c8e6b4d","has_verification_secret":true,"event_type_path":"type","dedupe_key_path":"id","forward_original_headers":false,"is_active":true,"created_at":"2026-09-25T12:10:00Z","updated_at":"2026-09-25T13:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-sources-verification-secret","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-sources-verification-secret","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-sources-verification-secret","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-sources-verification-secret"}},{"id":"webhooks.sources.sample","method":"GET","path":"/v1/tools/webhooks/sources/{id}/sample","title":{"pt-br":"Consultar último evento recebido","en":"Retrieve last received event","es":"Consultar el último evento recibido","fr":"Consulter le dernier événement reçu"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"event_type":"checkout.session.completed","body":{"id":"evt_1Q2w3E","type":"checkout.session.completed","data":{"object":{"id":"cs_test_a1B2"}}},"received_at":"2026-09-25T12:20:00Z"}},"note":{"pt-br":"`data` é `null` enquanto a fonte não recebeu nada; um corpo que não é JSON vem como texto","en":"`data` is `null` until the source receives something; a non-JSON body comes back as text","es":"`data` es `null` mientras la fuente no haya recibido nada; un cuerpo que no es JSON vuelve como texto","fr":"`data` vaut `null` tant que la source n’a rien reçu ; un corps qui n’est pas du JSON revient sous forme de texte"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-sources-sample","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-sources-sample","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-sources-sample","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-sources-sample"}},{"id":"webhooks.sources.test","method":"POST","path":"/v1/tools/webhooks/sources/{id}/test","title":{"pt-br":"Enviar evento de teste pela fonte","en":"Send a test event through the source","es":"Enviar un evento de prueba por la fuente","fr":"Envoyer un événement de test par la source"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"message_id":"c1a7e5d3-9b2f-4a6e-8d4c-0f3b7e1a5c92","event_type":"invoice.paid","deliveries":1}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-sources-test","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-sources-test","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-sources-test","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-sources-test"}},{"id":"webhooks.routes.create","method":"POST","path":"/v1/tools/webhooks/routes","title":{"pt-br":"Criar rota","en":"Create route","es":"Crear ruta","fr":"Créer une route"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"f2c8a5e1-6b3d-4f9a-8e7c-1a4d9b2e5f60","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","source_id":"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27","endpoint_id":"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34","event_types":["checkout.session.completed","invoice.paid"],"is_active":true,"created_at":"2026-09-25T12:12:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-routes-create","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-routes-create","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-routes-create","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-routes-create"}},{"id":"webhooks.routes.list","method":"GET","path":"/v1/tools/webhooks/routes","title":{"pt-br":"Listar rotas","en":"List routes","es":"Listar rutas","fr":"Lister les routes"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"routes":[{"id":"f2c8a5e1-6b3d-4f9a-8e7c-1a4d9b2e5f60","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","source_id":"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27","endpoint_id":"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34","event_types":["checkout.session.completed","invoice.paid"],"is_active":true,"created_at":"2026-09-25T12:12:00Z"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-routes-list","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-routes-list","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-routes-list","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-routes-list"}},{"id":"webhooks.routes.update","method":"PATCH","path":"/v1/tools/webhooks/routes/{id}","title":{"pt-br":"Alterar rota","en":"Update route","es":"Modificar ruta","fr":"Modifier une route"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"f2c8a5e1-6b3d-4f9a-8e7c-1a4d9b2e5f60","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","source_id":"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27","endpoint_id":"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34","event_types":["invoice.paid"],"is_active":false,"created_at":"2026-09-25T12:12:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-routes-update","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-routes-update","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-routes-update","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-routes-update"}},{"id":"webhooks.routes.delete","method":"DELETE","path":"/v1/tools/webhooks/routes/{id}","title":{"pt-br":"Excluir rota","en":"Delete route","es":"Eliminar ruta","fr":"Supprimer une route"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"deleted":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-routes-delete","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-routes-delete","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-routes-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-routes-delete"}},{"id":"webhooks.graph","method":"GET","path":"/v1/tools/webhooks/graph","title":{"pt-br":"Consultar mapa do fluxo","en":"Retrieve flow map","es":"Consultar el mapa del flujo","fr":"Consulter la carte du flux"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"sources":[{"id":"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","name":"Stripe produção","provider":"stripe","ingest_url":"https://api.respawntech.dev/in/src_4f8a2c6e1b9d3f7a5c0e8b2d6f4a1c9e7b3d5f0a2c8e6b4d","has_verification_secret":true,"event_type_path":"type","dedupe_key_path":"id","forward_original_headers":false,"is_active":true,"created_at":"2026-09-25T12:10:00Z","updated_at":"2026-09-25T12:10:00Z","messages_24h":20,"last_message_at":"2026-09-25T12:20:00Z","recent_event_types":["checkout.session.completed","invoice.paid"]}],"endpoints":[{"id":"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","url":"https://api.example.com/hooks/respawn","description":"Pedidos da loja","events":["order.paid","order.refunded"],"custom_headers":{"X-Tenant":"loja-1"},"is_active":true,"rate_limit_per_sec":0,"format":"raw","created_at":"2026-09-25T12:00:00Z","updated_at":"2026-09-25T12:00:00Z","stats":{"deliveries_24h":48,"failed_24h":1,"last_delivery_at":"2026-09-25T12:05:06Z","last_status":"succeeded"}}],"routes":[{"id":"f2c8a5e1-6b3d-4f9a-8e7c-1a4d9b2e5f60","project_id":"3e7c1a9f-5b2d-4c8e-a6f0-9d1b4e7a2c53","source_id":"a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27","endpoint_id":"8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34","event_types":["checkout.session.completed","invoice.paid"],"is_active":true,"created_at":"2026-09-25T12:12:00Z","stats":{"deliveries_24h":48,"failed_24h":1,"last_delivery_at":"2026-09-25T12:05:06Z","last_status":"succeeded"}}]}},"note":{"pt-br":"contadores das últimas 24 horas","en":"counters of the last 24 hours","es":"contadores de las últimas 24 horas","fr":"compteurs des dernières 24 heures"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-graph","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-graph","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-graph","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-graph"}},{"id":"webhooks.layout.get","method":"GET","path":"/v1/tools/webhooks/layout","title":{"pt-br":"Consultar layout do canvas","en":"Retrieve canvas layout","es":"Consultar el layout del canvas","fr":"Consulter la disposition du canvas"},"scopes":["tools:webhooks:read","tools:webhooks:manage","tools:read","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"positions":{"platform":{"x":0,"y":0},"source:a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27":{"x":0,"y":160},"endpoint:8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34":{"x":480,"y":160}}}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-layout-get","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-layout-get","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-layout-get","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-layout-get"}},{"id":"webhooks.layout.save","method":"PUT","path":"/v1/tools/webhooks/layout","title":{"pt-br":"Salvar layout do canvas","en":"Save canvas layout","es":"Guardar el layout del canvas","fr":"Enregistrer la disposition du canvas"},"scopes":["tools:webhooks:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"positions":{"source:a6d1f8c3-2e9b-4c7a-9f5d-3b0e6a1c8d27":{"x":0,"y":160},"endpoint:8d2f4a61-3c7b-4e9a-b1d5-6f0e2a9c7b34":{"x":520,"y":200}}}},"note":{"pt-br":"substitui o layout inteiro do projeto","en":"replaces the project's whole layout","es":"reemplaza todo el layout del proyecto","fr":"remplace toute la disposition du projet"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-layout-save","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-layout-save","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-layout-save","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-layout-save"}},{"id":"webhooks.ingest","method":"POST","path":"/in/{token}","title":{"pt-br":"Receber evento de um provedor","en":"Receive a provider event","es":"Recibir evento de un proveedor","fr":"Recevoir un événement d’un fournisseur"},"scopes":[],"billing":{"kind":"paid","summary":{"pt-br":"Por entrega redistribuída, além da franquia mensal","en":"Per fanned-out delivery, beyond the monthly free quota","es":"Por entrega redistribuida, más allá de la cuota mensual gratuita","fr":"Par livraison redistribuée, au-delà du quota mensuel gratuit"}},"dry_run":false,"auth":"public","response":{"status":200,"body":{"message_id":"c1a7e5d3-9b2f-4a6e-8d4c-0f3b7e1a5c92","event_type":"checkout.session.completed","deliveries":1},"note":{"pt-br":"resposta sem o envelope da API; `200` também para duplicatas e eventos sem rota. `blocked` conta as entregas esperando créditos","en":"response without the API envelope; `200` also for duplicates and unrouted events. `blocked` counts deliveries waiting for credits","es":"respuesta sin el sobre de la API; `200` también para duplicados y eventos sin ruta. `blocked` cuenta las entregas que esperan créditos","fr":"réponse sans l’enveloppe de l’API ; `200` aussi pour les doublons et les événements sans route. `blocked` compte les livraisons en attente de crédits"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/webhook-relay#webhooks-ingest","en":"https://docs.respawntech.dev/en/ferramentas/webhook-relay#webhooks-ingest","es":"https://docs.respawntech.dev/es/ferramentas/webhook-relay#webhooks-ingest","fr":"https://docs.respawntech.dev/fr/ferramentas/webhook-relay#webhooks-ingest"}}],"pricing":[{"id":"per-delivery","title":{"pt-br":"Por entrega","en":"Per delivery","es":"Por entrega","fr":"Par livraison"},"description":{"pt-br":"Uma entrega é uma mensagem enviada a um endpoint: uma mensagem para 3 endpoints são 3 entregas. Retries automáticos da mesma entrega não cobram de novo.","en":"A delivery is one message sent to one endpoint: one message to 3 endpoints is 3 deliveries. Automatic retries of the same delivery are not charged again.","es":"Una entrega es un mensaje enviado a un endpoint: un mensaje a 3 endpoints son 3 entregas. Los reintentos automáticos de la misma entrega no se cobran de nuevo.","fr":"Une livraison est un message envoyé à un endpoint : un message vers 3 endpoints fait 3 livraisons. Les relances automatiques d’une même livraison ne sont pas refacturées."},"items":[{"label":{"pt-br":"Entrega além da franquia mensal","en":"Delivery beyond the monthly free quota","es":"Entrega más allá de la cuota mensual gratuita","fr":"Livraison au-delà du quota mensuel gratuit"},"service":"webhook_delivery","credits":0.002,"note":{"pt-br":"mensagens da API, ingestão, retry manual e recuperação","en":"API messages, ingestion, manual retry and recovery","es":"mensajes de la API, ingesta, reenvío manual y recuperación","fr":"messages de l’API, ingestion, renvoi manuel et récupération"}},{"label":{"pt-br":"Eventos da plataforma e callbacks de jobs","en":"Platform events and job callbacks","es":"Eventos de la plataforma y callbacks de jobs","fr":"Événements de la plateforme et callbacks de jobs"},"credits":0,"note":{"pt-br":"ex.: `qr.dynamic.scanned`, `webhook_url` do PDF, ping `webhook.test`","en":"e.g. `qr.dynamic.scanned`, PDF `webhook_url`, `webhook.test` ping","es":"p. ej.: `qr.dynamic.scanned`, `webhook_url` del PDF, ping `webhook.test`","fr":"ex. : `qr.dynamic.scanned`, `webhook_url` du PDF, ping `webhook.test`"}}]},{"id":"free-quota","title":{"pt-br":"Franquia mensal","en":"Monthly free quota","es":"Cuota mensual gratuita","fr":"Quota mensuel gratuit"},"description":{"pt-br":"Por conta e por mês (UTC), somando API e ingestão de todos os projetos. O valor padrão pode ser ajustado pela plataforma; a resposta do envio mostra quantas entregas saíram da franquia (`free_deliveries`) e o uso do mês aparece em `GET /usage`.","en":"Per account and per calendar month (UTC), API and ingestion of every project combined. The platform may adjust the default; the send response shows how many deliveries came out of the free quota (`free_deliveries`) and the month's usage is in `GET /usage`.","es":"Por cuenta y por mes (UTC), sumando API e ingesta de todos los proyectos. La plataforma puede ajustar el valor por defecto; la respuesta del envío muestra cuántas entregas salieron de la franquicia (`free_deliveries`) y el uso del mes aparece en `GET /usage`.","fr":"Par compte et par mois (UTC), API et ingestion de tous les projets confondues. La plateforme peut ajuster la valeur par défaut ; la réponse d’envoi indique combien de livraisons sont sorties du quota gratuit (`free_deliveries`) et l’usage du mois figure dans `GET /usage`."},"items":[{"label":{"pt-br":"Entregas","en":"Deliveries","es":"Entregas","fr":"Livraisons"},"display":{"pt-br":"1.000","en":"1,000","es":"1.000","fr":"1 000"}}]}]},{"slug":"realtime","category":"integrations","code":"realtime","base_path":"/v1/tools/realtime","status":"published","name":{"pt-br":"Realtime","en":"Realtime","es":"Realtime","fr":"Realtime"},"summary":{"pt-br":"Publique e receba mensagens em tempo real por WebSocket ou SSE, com presença, histórico e reconexão sem perder mensagens.","en":"Publish and receive real-time messages over WebSocket or SSE, with presence, history and reconnection without losing messages.","es":"Publica y recibe mensajes en tiempo real por WebSocket o SSE, con presencia, historial y reconexión sin perder mensajes.","fr":"Publiez et recevez des messages en temps réel par WebSocket ou SSE, avec présence, historique et reconnexion sans perte de messages."},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/realtime","en":"https://docs.respawntech.dev/en/ferramentas/realtime","es":"https://docs.respawntech.dev/es/ferramentas/realtime","fr":"https://docs.respawntech.dev/fr/ferramentas/realtime"},"endpoints":[{"id":"realtime.token","method":"POST","path":"/v1/tools/realtime/tokens","title":{"pt-br":"Emitir token de conexão","en":"Issue connection token","es":"Emitir token de conexión","fr":"Émettre un token de connexion"},"scopes":["tools:realtime:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJyZWFsdGltZSIsImNpZCI6InVzZXItNDIifQ.q3Zk1xYpL0","expires_at":"2026-10-06T13:00:00Z","connect_url":"wss://rt.respawntech.dev/v1/connect?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJyZWFsdGltZSIsImNpZCI6InVzZXItNDIifQ.q3Zk1xYpL0"}},"note":{"pt-br":"conecte direto em `connect_url`, que já traz o `?token=`; se vier `retry_after_ms`, espere esse tempo antes de conectar","en":"connect straight to `connect_url`, which already carries `?token=`; if `retry_after_ms` comes back, wait that long before connecting","es":"conéctate directo a `connect_url`, que ya trae el `?token=`; si viene `retry_after_ms`, espera ese tiempo antes de conectar","fr":"connectez-vous directement à `connect_url`, qui contient déjà le `?token=` ; si `retry_after_ms` est présent, attendez ce délai avant de vous connecter"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/realtime#realtime-token","en":"https://docs.respawntech.dev/en/ferramentas/realtime#realtime-token","es":"https://docs.respawntech.dev/es/ferramentas/realtime#realtime-token","fr":"https://docs.respawntech.dev/fr/ferramentas/realtime#realtime-token"}},{"id":"realtime.publish","method":"POST","path":"/v1/tools/realtime/channels/{channel}/messages","title":{"pt-br":"Publicar mensagem pelo servidor","en":"Publish a message from the server","es":"Publicar mensaje desde el servidor","fr":"Publier un message depuis le serveur"},"scopes":["tools:realtime:publish","tools:realtime:manage","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por mensagem publicada, além da franquia mensal","en":"Per published message, beyond the monthly free quota","es":"Por mensaje publicado, además de la cuota mensual","fr":"Par message publié, au-delà du quota gratuit mensuel"}},"dry_run":true,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"1790338000000-0","timestamp":1790338000000,"charged":false}},"note":{"pt-br":"`timestamp` em milissegundos Unix; `charged` é `true` quando a mensagem passou da franquia mensal e foi cobrada","en":"`timestamp` in Unix milliseconds; `charged` is `true` when the message went past the monthly allowance and was billed","es":"`timestamp` en milisegundos Unix; `charged` es `true` cuando el mensaje superó la franquicia mensual y se cobró","fr":"`timestamp` en millisecondes Unix ; `charged` vaut `true` quand le message a dépassé le quota mensuel et a été facturé"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/realtime#realtime-publish","en":"https://docs.respawntech.dev/en/ferramentas/realtime#realtime-publish","es":"https://docs.respawntech.dev/es/ferramentas/realtime#realtime-publish","fr":"https://docs.respawntech.dev/fr/ferramentas/realtime#realtime-publish"}},{"id":"realtime.history","method":"GET","path":"/v1/tools/realtime/channels/{channel}/history","title":{"pt-br":"Consultar histórico do canal","en":"Get channel history","es":"Consultar historial del canal","fr":"Consulter l'historique du canal"},"scopes":["tools:realtime:subscribe","tools:realtime:manage","tools:manage"],"billing":{"kind":"metered","operation":"realtime.history"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"channel":"chat:lobby","limit":100,"messages":[{"id":"1790338000000-0","name":"message","data":{"text":"Olá!"},"timestamp":1790338000000,"client_id":"user-42"}]}},"note":{"pt-br":"da mais antiga para a mais nova, sem paginação; `timestamp` em milissegundos Unix","en":"oldest to newest, no paging; `timestamp` in Unix milliseconds","es":"del más antiguo al más nuevo, sin paginación; `timestamp` en milisegundos Unix","fr":"du plus ancien au plus récent, sans pagination ; `timestamp` en millisecondes Unix"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/realtime#realtime-history","en":"https://docs.respawntech.dev/en/ferramentas/realtime#realtime-history","es":"https://docs.respawntech.dev/es/ferramentas/realtime#realtime-history","fr":"https://docs.respawntech.dev/fr/ferramentas/realtime#realtime-history"}},{"id":"realtime.presence","method":"GET","path":"/v1/tools/realtime/channels/{channel}/presence","title":{"pt-br":"Listar a presença do canal","en":"List channel presence","es":"Listar la presencia del canal","fr":"Lister la présence du canal"},"scopes":["tools:realtime:subscribe","tools:realtime:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"channel":"chat:lobby","members":[{"connection_id":"c_8f3a1d","client_id":"user-42","data":{"nick":"ana"},"timestamp":1790338000000}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/realtime#realtime-presence","en":"https://docs.respawntech.dev/en/ferramentas/realtime#realtime-presence","es":"https://docs.respawntech.dev/es/ferramentas/realtime#realtime-presence","fr":"https://docs.respawntech.dev/fr/ferramentas/realtime#realtime-presence"}},{"id":"realtime.channels","method":"GET","path":"/v1/tools/realtime/channels","title":{"pt-br":"Listar canais ativos","en":"List active channels","es":"Listar canales activos","fr":"Lister les canaux actifs"},"scopes":["tools:realtime:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"channels":[{"name":"chat:lobby","subscribers":12,"last_publish_at":"2026-10-06T12:00:00Z"}],"total":1}},"note":{"pt-br":"canais com assinantes ou com uma publicação dentro da janela do histórico; `next_offset` só vem quando há mais páginas","en":"channels with subscribers or a publish within the history window; `next_offset` only appears when there are more pages","es":"canales con suscriptores o con una publicación dentro de la ventana del historial; `next_offset` solo viene cuando hay más páginas","fr":"canaux ayant des abonnés ou une publication dans la fenêtre de l'historique ; `next_offset` n'apparaît que s'il y a d'autres pages"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/realtime#realtime-channels","en":"https://docs.respawntech.dev/en/ferramentas/realtime#realtime-channels","es":"https://docs.respawntech.dev/es/ferramentas/realtime#realtime-channels","fr":"https://docs.respawntech.dev/fr/ferramentas/realtime#realtime-channels"}},{"id":"realtime.stats","method":"GET","path":"/v1/tools/realtime/stats","title":{"pt-br":"Consultar uso e conexões","en":"Get usage and connections","es":"Consultar uso y conexiones","fr":"Consulter l'usage et les connexions"},"scopes":["tools:realtime:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"connections":42,"active_channels":7,"month":"2026-10","messages_this_month":18250,"free_monthly_messages":200000,"free_remaining":181750}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/realtime#realtime-stats","en":"https://docs.respawntech.dev/en/ferramentas/realtime#realtime-stats","es":"https://docs.respawntech.dev/es/ferramentas/realtime#realtime-stats","fr":"https://docs.respawntech.dev/fr/ferramentas/realtime#realtime-stats"}},{"id":"realtime.webhook.get","method":"GET","path":"/v1/tools/realtime/webhook","title":{"pt-br":"Consultar o webhook de canais","en":"Get the channel webhook","es":"Consultar el webhook de canales","fr":"Consulter le webhook de canaux"},"scopes":["tools:realtime:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"url":"https://example.com/hooks/realtime","events":["channel.occupied","channel.vacated","channel.message","presence.member_added"],"channels":["chat:*"],"enabled":true,"secret":"3f9c1b7a5e2d4c8f90a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b","standard_key":"whsec_M2Y5YzFiN2E1ZTJkNGM4ZjkwYTFiMmMzZDRlNWY2MDcxODI5M2E0YjVjNmQ3ZThmOWEwYjFjMmQzZTRmNWE2Yg==","updated_at":"2026-10-06T12:00:00Z"}},"note":{"pt-br":"sem webhook configurado, responde `404` (`not_found`)","en":"with no webhook configured, answers `404` (`not_found`)","es":"sin webhook configurado, responde `404` (`not_found`)","fr":"sans webhook configuré, répond `404` (`not_found`)"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/realtime#realtime-webhook-get","en":"https://docs.respawntech.dev/en/ferramentas/realtime#realtime-webhook-get","es":"https://docs.respawntech.dev/es/ferramentas/realtime#realtime-webhook-get","fr":"https://docs.respawntech.dev/fr/ferramentas/realtime#realtime-webhook-get"}},{"id":"realtime.webhook.put","method":"PUT","path":"/v1/tools/realtime/webhook","title":{"pt-br":"Configurar o webhook de canais","en":"Configure the channel webhook","es":"Configurar el webhook de canales","fr":"Configurer le webhook de canaux"},"scopes":["tools:realtime:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"url":"https://example.com/hooks/realtime","events":["channel.occupied","channel.vacated","channel.message","presence.member_added"],"channels":["chat:*"],"enabled":true,"secret":"3f9c1b7a5e2d4c8f90a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b","standard_key":"whsec_M2Y5YzFiN2E1ZTJkNGM4ZjkwYTFiMmMzZDRlNWY2MDcxODI5M2E0YjVjNmQ3ZThmOWEwYjFjMmQzZTRmNWE2Yg==","updated_at":"2026-10-06T12:00:00Z"}},"note":{"pt-br":"cria o webhook ou substitui o atual, mantendo o mesmo secret","en":"creates the webhook or replaces the current one, keeping the same secret","es":"crea el webhook o reemplaza el actual, conservando el mismo secret","fr":"crée le webhook ou remplace l'actuel, en conservant le même secret"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/realtime#realtime-webhook-put","en":"https://docs.respawntech.dev/en/ferramentas/realtime#realtime-webhook-put","es":"https://docs.respawntech.dev/es/ferramentas/realtime#realtime-webhook-put","fr":"https://docs.respawntech.dev/fr/ferramentas/realtime#realtime-webhook-put"}},{"id":"realtime.webhook.rotate","method":"POST","path":"/v1/tools/realtime/webhook/rotate-secret","title":{"pt-br":"Trocar o secret do webhook","en":"Rotate the webhook secret","es":"Cambiar el secret del webhook","fr":"Changer le secret du webhook"},"scopes":["tools:realtime:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"url":"https://example.com/hooks/realtime","events":["channel.occupied","channel.vacated","channel.message","presence.member_added"],"channels":["chat:*"],"enabled":true,"secret":"3f9c1b7a5e2d4c8f90a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b","standard_key":"whsec_M2Y5YzFiN2E1ZTJkNGM4ZjkwYTFiMmMzZDRlNWY2MDcxODI5M2E0YjVjNmQ3ZThmOWEwYjFjMmQzZTRmNWE2Yg==","updated_at":"2026-10-06T12:00:00Z"}},"note":{"pt-br":"o secret anterior deixa de valer; a configuração é lida em cache por até 30 segundos","en":"the previous secret stops being valid; the configuration is cached for up to 30 seconds","es":"el secret anterior deja de valer; la configuración se lee de caché durante hasta 30 segundos","fr":"l'ancien secret cesse d'être valable ; la configuration est lue en cache pendant 30 secondes au plus"}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/realtime#realtime-webhook-rotate","en":"https://docs.respawntech.dev/en/ferramentas/realtime#realtime-webhook-rotate","es":"https://docs.respawntech.dev/es/ferramentas/realtime#realtime-webhook-rotate","fr":"https://docs.respawntech.dev/fr/ferramentas/realtime#realtime-webhook-rotate"}},{"id":"realtime.webhook.delete","method":"DELETE","path":"/v1/tools/realtime/webhook","title":{"pt-br":"Remover o webhook de canais","en":"Remove the channel webhook","es":"Eliminar el webhook de canales","fr":"Supprimer le webhook de canaux"},"scopes":["tools:realtime:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"deleted":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/realtime#realtime-webhook-delete","en":"https://docs.respawntech.dev/en/ferramentas/realtime#realtime-webhook-delete","es":"https://docs.respawntech.dev/es/ferramentas/realtime#realtime-webhook-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/realtime#realtime-webhook-delete"}}],"pricing":[{"id":"per-message","title":{"pt-br":"Por mensagem","en":"Per message","es":"Por mensaje","fr":"Par message"},"description":{"pt-br":"Conta cada mensagem publicada, pela API ou por uma conexão WebSocket, uma única vez, não importa quantos clientes a recebam. Emitir tokens, conectar, assinar e usar a presença são grátis.","en":"Counts each published message, through the API or a WebSocket connection, once, no matter how many clients receive it. Issuing tokens, connecting, subscribing and using presence are free.","es":"Cuenta cada mensaje publicado, por la API o por una conexión WebSocket, una sola vez, sin importar cuántos clientes lo reciban. Emitir tokens, conectar, suscribirse y usar la presencia son gratis.","fr":"Compte chaque message publié, par l'API ou par une connexion WebSocket, une seule fois, quel que soit le nombre de clients qui le reçoivent. Émettre des tokens, se connecter, s'abonner et utiliser la présence sont gratuits."},"items":[{"label":{"pt-br":"Mensagem além da franquia mensal","en":"Message beyond the monthly free quota","es":"Mensaje por encima de la cuota mensual","fr":"Message au-delà du quota gratuit mensuel"},"service":"realtime_publish","credits":0.001}]},{"id":"free-quota","title":{"pt-br":"Franquia mensal","en":"Monthly free quota","es":"Cuota mensual gratuita","fr":"Quota mensuel gratuit"},"description":{"pt-br":"Por conta e por mês (UTC). O valor padrão pode ser ajustado pela plataforma; a resposta da publicação mostra se a mensagem foi cobrada (`charged`).","en":"Per account and per month (UTC). The default value can be adjusted by the platform; the publish response shows whether the message was billed (`charged`).","es":"Por cuenta y por mes (UTC). El valor predeterminado puede ajustarlo la plataforma; la respuesta de la publicación indica si el mensaje se cobró (`charged`).","fr":"Par compte et par mois (UTC). La valeur par défaut peut être ajustée par la plateforme ; la réponse de la publication indique si le message a été facturé (`charged`)."},"items":[{"label":{"pt-br":"Mensagens","en":"Messages","es":"Mensajes","fr":"Messages"},"display":{"pt-br":"200.000","en":"200,000","es":"200.000","fr":"200 000"}}]}]},{"slug":"ci-error-extract","category":"integrations","code":"ci","base_path":"/v1/tools/ci","status":"published","name":{"pt-br":"Extrator de erro de CI","en":"CI Error Extractor","es":"Extractor de errores de CI","fr":"Extracteur d'erreur de CI"},"summary":{"pt-br":"Extrai de um log de CI só as linhas que explicam a falha.","en":"Extracts from a CI log only the lines that explain the failure.","es":"Extrae de un log de CI solo las líneas que explican el fallo.","fr":"Extrait d'un log de CI uniquement les lignes qui expliquent l'échec."},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/ci-error-extract","en":"https://docs.respawntech.dev/en/ferramentas/ci-error-extract","es":"https://docs.respawntech.dev/es/ferramentas/ci-error-extract","fr":"https://docs.respawntech.dev/fr/ferramentas/ci-error-extract"},"endpoints":[{"id":"ci.extract","method":"POST","path":"/v1/tools/ci/extract","title":{"pt-br":"Extrair o erro de um log","en":"Extract the error from a log","es":"Extraer el error de un log","fr":"Extraire l'erreur d'un log"},"scopes":["tools:ci:read","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"ci.error_extract"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"lines":["npm ERR! code ELIFECYCLE","npm ERR! errno 1","FAIL src/app.test.ts"]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/ci-error-extract#ci-extract","en":"https://docs.respawntech.dev/en/ferramentas/ci-error-extract#ci-extract","es":"https://docs.respawntech.dev/es/ferramentas/ci-error-extract#ci-extract","fr":"https://docs.respawntech.dev/fr/ferramentas/ci-error-extract#ci-extract"}}],"pricing":[]},{"slug":"web-to-markdown","category":"integrations","code":"web","base_path":"/v1/tools/web","status":"published","name":{"pt-br":"Página web para Markdown","en":"Web Page to Markdown","es":"Página web a Markdown","fr":"Page web vers Markdown"},"summary":{"pt-br":"Baixa uma página web e devolve o conteúdo principal em Markdown, com opção de gravar na base de conhecimento.","en":"Fetches a web page and returns its main content as Markdown, optionally saving it to the knowledge base.","es":"Descarga una página web y devuelve su contenido principal en Markdown, con opción de guardarlo en la base de conocimiento.","fr":"Télécharge une page web et renvoie son contenu principal en Markdown, avec la possibilité de l'enregistrer dans la base de connaissances."},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/web-to-markdown","en":"https://docs.respawntech.dev/en/ferramentas/web-to-markdown","es":"https://docs.respawntech.dev/es/ferramentas/web-to-markdown","fr":"https://docs.respawntech.dev/fr/ferramentas/web-to-markdown"},"endpoints":[{"id":"web.markdown","method":"POST","path":"/v1/tools/web/markdown","title":{"pt-br":"Converter página em Markdown","en":"Convert a page to Markdown","es":"Convertir una página a Markdown","fr":"Convertir une page en Markdown"},"scopes":["tools:web:read","tools:read","tools:manage"],"billing":{"kind":"metered","operation":"web.markdown"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"url":"https://example.com/blog/post","final_url":"https://example.com/blog/post","title":"Como funciona o nosso blog","byline":"Equipe Exemplo","markdown":"# Como funciona o nosso blog\n\nTexto da página…","word_count":412}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/web-to-markdown#web-markdown","en":"https://docs.respawntech.dev/en/ferramentas/web-to-markdown#web-markdown","es":"https://docs.respawntech.dev/es/ferramentas/web-to-markdown#web-markdown","fr":"https://docs.respawntech.dev/fr/ferramentas/web-to-markdown#web-markdown"}}],"pricing":[]},{"slug":"notify","category":"integrations","code":"notify","base_path":"/v1/tools/notify","status":"published","name":{"pt-br":"Notificações","en":"Notifications","es":"Notificaciones","fr":"Notifications"},"summary":{"pt-br":"Um agente avisa o seu humano por e-mail, webhook ou ntfy.","en":"An agent alerts its human by email, webhook or ntfy.","es":"Un agente avisa a su humano por correo, webhook o ntfy.","fr":"Un agent prévient son humain par e-mail, webhook ou ntfy."},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/notify","en":"https://docs.respawntech.dev/en/ferramentas/notify","es":"https://docs.respawntech.dev/es/ferramentas/notify","fr":"https://docs.respawntech.dev/fr/ferramentas/notify"},"endpoints":[{"id":"notify.send","method":"POST","path":"/v1/tools/notify","title":{"pt-br":"Enviar notificação","en":"Send notification","es":"Enviar notificación","fr":"Envoyer une notification"},"scopes":["tools:notify:send","tools:manage"],"billing":{"kind":"metered","operation":"notify.send"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"550e8400-e29b-41d4-a716-446655440000","channel":"celular","kind":"ntfy","status":"delivered"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/notify#notify-send","en":"https://docs.respawntech.dev/en/ferramentas/notify#notify-send","es":"https://docs.respawntech.dev/es/ferramentas/notify#notify-send","fr":"https://docs.respawntech.dev/fr/ferramentas/notify#notify-send"}},{"id":"notify.messages","method":"GET","path":"/v1/tools/notify/messages","title":{"pt-br":"Histórico de envios","en":"Delivery history","es":"Historial de envíos","fr":"Historique des envois"},"scopes":["tools:notify:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"items":[{"id":"550e8400-e29b-41d4-a716-446655440000","channel":"celular","kind":"ntfy","priority":"high","status":"delivered","created_at":"2026-10-06T12:00:00Z"}],"page":1,"page_size":20,"total":1}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/notify#notify-messages","en":"https://docs.respawntech.dev/en/ferramentas/notify#notify-messages","es":"https://docs.respawntech.dev/es/ferramentas/notify#notify-messages","fr":"https://docs.respawntech.dev/fr/ferramentas/notify#notify-messages"}},{"id":"notify.channels.list","method":"GET","path":"/v1/tools/notify/channels","title":{"pt-br":"Listar canais","en":"List channels","es":"Listar canales","fr":"Lister les canaux"},"scopes":["tools:notify:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"items":[{"id":"550e8400-e29b-41d4-a716-446655440000","name":"celular","kind":"ntfy","server":"https://ntfy.sh","topic":"meu-topico-secreto","is_default":true,"enabled":true,"created_at":"2026-10-06T12:00:00Z"}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/notify#notify-channels-list","en":"https://docs.respawntech.dev/en/ferramentas/notify#notify-channels-list","es":"https://docs.respawntech.dev/es/ferramentas/notify#notify-channels-list","fr":"https://docs.respawntech.dev/fr/ferramentas/notify#notify-channels-list"}},{"id":"notify.channels.create","method":"POST","path":"/v1/tools/notify/channels","title":{"pt-br":"Criar canal","en":"Create channel","es":"Crear canal","fr":"Créer un canal"},"scopes":["tools:notify:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"550e8400-e29b-41d4-a716-446655440000","name":"celular","kind":"ntfy","server":"https://ntfy.sh","topic":"meu-topico-secreto","is_default":true,"enabled":true,"created_at":"2026-10-06T12:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/notify#notify-channels-create","en":"https://docs.respawntech.dev/en/ferramentas/notify#notify-channels-create","es":"https://docs.respawntech.dev/es/ferramentas/notify#notify-channels-create","fr":"https://docs.respawntech.dev/fr/ferramentas/notify#notify-channels-create"}},{"id":"notify.channels.get","method":"GET","path":"/v1/tools/notify/channels/{id}","title":{"pt-br":"Consultar canal","en":"Get channel","es":"Consultar canal","fr":"Consulter un canal"},"scopes":["tools:notify:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"550e8400-e29b-41d4-a716-446655440000","name":"celular","kind":"ntfy","server":"https://ntfy.sh","topic":"meu-topico-secreto","is_default":true,"enabled":true,"created_at":"2026-10-06T12:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/notify#notify-channels-get","en":"https://docs.respawntech.dev/en/ferramentas/notify#notify-channels-get","es":"https://docs.respawntech.dev/es/ferramentas/notify#notify-channels-get","fr":"https://docs.respawntech.dev/fr/ferramentas/notify#notify-channels-get"}},{"id":"notify.channels.update","method":"PATCH","path":"/v1/tools/notify/channels/{id}","title":{"pt-br":"Alterar canal","en":"Update channel","es":"Modificar canal","fr":"Modifier un canal"},"scopes":["tools:notify:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"550e8400-e29b-41d4-a716-446655440000","name":"celular","kind":"ntfy","server":"https://ntfy.sh","topic":"meu-topico-secreto","is_default":true,"enabled":true,"created_at":"2026-10-06T12:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/notify#notify-channels-update","en":"https://docs.respawntech.dev/en/ferramentas/notify#notify-channels-update","es":"https://docs.respawntech.dev/es/ferramentas/notify#notify-channels-update","fr":"https://docs.respawntech.dev/fr/ferramentas/notify#notify-channels-update"}},{"id":"notify.channels.delete","method":"DELETE","path":"/v1/tools/notify/channels/{id}","title":{"pt-br":"Excluir canal","en":"Delete channel","es":"Eliminar canal","fr":"Supprimer un canal"},"scopes":["tools:notify:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"deleted":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/notify#notify-channels-delete","en":"https://docs.respawntech.dev/en/ferramentas/notify#notify-channels-delete","es":"https://docs.respawntech.dev/es/ferramentas/notify#notify-channels-delete","fr":"https://docs.respawntech.dev/fr/ferramentas/notify#notify-channels-delete"}},{"id":"notify.channels.test","method":"POST","path":"/v1/tools/notify/channels/{id}/test","title":{"pt-br":"Testar canal","en":"Test channel","es":"Probar canal","fr":"Tester un canal"},"scopes":["tools:notify:manage","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"status":"delivered"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/notify#notify-channels-test","en":"https://docs.respawntech.dev/en/ferramentas/notify#notify-channels-test","es":"https://docs.respawntech.dev/es/ferramentas/notify#notify-channels-test","fr":"https://docs.respawntech.dev/fr/ferramentas/notify#notify-channels-test"}}],"pricing":[]},{"slug":"memoria-de-aprendizados","category":"ai","code":"learnings","base_path":"/v1/tools/learnings","status":"published","name":{"pt-br":"Memória de aprendizados","en":"Learnings memory","es":"Memoria de aprendizajes","fr":"Mémoire d'apprentissages"},"summary":{"pt-br":"Memória de longo prazo para agentes: salve o que aprendeu num projeto e recupere antes da próxima tarefa.","en":"Long-term memory for agents: save what was learned in a project and recall it before the next task.","es":"Memoria de largo plazo para agentes: guarda lo aprendido en un proyecto y recupéralo antes de la próxima tarea.","fr":"Mémoire à long terme pour les agents : enregistrez ce qui a été appris dans un projet et retrouvez-le avant la prochaine tâche."},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/memoria-de-aprendizados","en":"https://docs.respawntech.dev/en/ferramentas/memoria-de-aprendizados","es":"https://docs.respawntech.dev/es/ferramentas/memoria-de-aprendizados","fr":"https://docs.respawntech.dev/fr/ferramentas/memoria-de-aprendizados"},"endpoints":[{"id":"learnings.save","method":"POST","path":"/v1/tools/learnings","title":{"pt-br":"Salvar aprendizado","en":"Save learning","es":"Guardar aprendizaje","fr":"Enregistrer un apprentissage"},"scopes":["tools:learnings:write","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Cobrado como a ingestão da base de conhecimento (por trecho)","en":"Billed like knowledge base ingestion (per chunk)","es":"Se cobra como la ingesta de la base de conocimiento (por fragmento)","fr":"Facturé comme l'ingestion de la base de connaissances (par fragment)"}},"dry_run":false,"response":{"status":201,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"id":"550e8400-e29b-41d4-a716-446655440000"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/memoria-de-aprendizados#learnings-save","en":"https://docs.respawntech.dev/en/ferramentas/memoria-de-aprendizados#learnings-save","es":"https://docs.respawntech.dev/es/ferramentas/memoria-de-aprendizados#learnings-save","fr":"https://docs.respawntech.dev/fr/ferramentas/memoria-de-aprendizados#learnings-save"}},{"id":"learnings.recall","method":"POST","path":"/v1/tools/learnings/recall","title":{"pt-br":"Recuperar aprendizados","en":"Recall learnings","es":"Recuperar aprendizajes","fr":"Retrouver des apprentissages"},"scopes":["tools:learnings:read","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Cobrado como a busca da base de conhecimento (franquia diária + blocos)","en":"Billed like knowledge base search (daily free quota + blocks)","es":"Se cobra como la búsqueda de la base de conocimiento (franquicia diaria + bloques)","fr":"Facturé comme la recherche de la base de connaissances (franchise quotidienne + blocs)"}},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"items":[{"id":"550e8400-e29b-41d4-a716-446655440000","kind":"pitfall","text":"O check:backend lê ../respawn-tech-backend; rode só com o backend já junto.","source":"conversa de 06/10","score":0.82}]}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/memoria-de-aprendizados#learnings-recall","en":"https://docs.respawntech.dev/en/ferramentas/memoria-de-aprendizados#learnings-recall","es":"https://docs.respawntech.dev/es/ferramentas/memoria-de-aprendizados#learnings-recall","fr":"https://docs.respawntech.dev/fr/ferramentas/memoria-de-aprendizados#learnings-recall"}},{"id":"learnings.forget","method":"DELETE","path":"/v1/tools/learnings/{id}","title":{"pt-br":"Esquecer aprendizado","en":"Forget learning","es":"Olvidar aprendizaje","fr":"Oublier un apprentissage"},"scopes":["tools:learnings:write","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"deleted":true}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/memoria-de-aprendizados#learnings-forget","en":"https://docs.respawntech.dev/en/ferramentas/memoria-de-aprendizados#learnings-forget","es":"https://docs.respawntech.dev/es/ferramentas/memoria-de-aprendizados#learnings-forget","fr":"https://docs.respawntech.dev/fr/ferramentas/memoria-de-aprendizados#learnings-forget"}}],"pricing":[]},{"slug":"audio","category":"ai","code":"audio_transcribe","base_path":"/v1/tools/audio","status":"published","name":{"pt-br":"Áudio","en":"Audio","es":"Audio","fr":"Audio"},"summary":{"pt-br":"Transcrição, voz sintética, sons gerados, análise e conversão de áudio, com jobs assíncronos.","en":"Transcription, synthetic voice, generated sounds, audio analysis and conversion, with asynchronous jobs.","es":"Transcripción, voz sintética, sonidos generados, análisis y conversión de audio, con jobs asíncronos.","fr":"Transcription, voix synthétique, sons générés, analyse et conversion d'audio, avec des jobs asynchrones."},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/audio","en":"https://docs.respawntech.dev/en/ferramentas/audio","es":"https://docs.respawntech.dev/es/ferramentas/audio","fr":"https://docs.respawntech.dev/fr/ferramentas/audio"},"endpoints":[{"id":"audio.transcribe","method":"POST","path":"/v1/tools/audio/transcribe","title":{"pt-br":"Transcrever áudio","en":"Transcribe audio","es":"Transcribir audio","fr":"Transcrire un audio"},"scopes":["tools:audio:transcribe","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por minuto iniciado de áudio","en":"Per started minute of audio","es":"Por minuto iniciado de audio","fr":"Par minute d'audio commencée"}},"dry_run":true,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"job_id":"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64","kind":"transcribe","status":"pending","poll_after_seconds":10,"created_at":"2026-10-12T12:00:00Z","expires_at":"2026-10-13T12:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/audio#audio-transcribe","en":"https://docs.respawntech.dev/en/ferramentas/audio#audio-transcribe","es":"https://docs.respawntech.dev/es/ferramentas/audio#audio-transcribe","fr":"https://docs.respawntech.dev/fr/ferramentas/audio#audio-transcribe"}},{"id":"audio.speak","method":"POST","path":"/v1/tools/audio/speak","title":{"pt-br":"Gerar fala a partir de texto","en":"Generate speech from text","es":"Generar voz a partir de texto","fr":"Générer de la parole à partir d'un texte"},"scopes":["tools:audio:speak","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por 1.000 caracteres iniciados","en":"Per started 1,000 characters","es":"Por 1.000 caracteres iniciados","fr":"Par 1 000 caractères commencés"}},"dry_run":true,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"job_id":"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64","kind":"speak","status":"pending","poll_after_seconds":10,"created_at":"2026-10-12T12:00:00Z","expires_at":"2026-10-13T12:00:00Z","voice":"pt-BR-dora","language":"pt-BR","format":"mp3"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/audio#audio-speak","en":"https://docs.respawntech.dev/en/ferramentas/audio#audio-speak","es":"https://docs.respawntech.dev/es/ferramentas/audio#audio-speak","fr":"https://docs.respawntech.dev/fr/ferramentas/audio#audio-speak"}},{"id":"audio.voices","method":"GET","path":"/v1/tools/audio/voices","title":{"pt-br":"Listar as vozes","en":"List the voices","es":"Listar las voces","fr":"Lister les voix"},"scopes":["tools:audio:read","tools:audio:transcribe","tools:audio:speak","tools:audio:process","tools:manage"],"billing":{"kind":"free"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"voices":[{"id":"pt-BR-dora","name":"Dora","language":"pt-BR","gender":"female"},{"id":"pt-BR-alex","name":"Alex","language":"pt-BR","gender":"male"}],"default_voice":"pt-BR-dora"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/audio#audio-voices","en":"https://docs.respawntech.dev/en/ferramentas/audio#audio-voices","es":"https://docs.respawntech.dev/es/ferramentas/audio#audio-voices","fr":"https://docs.respawntech.dev/fr/ferramentas/audio#audio-voices"}},{"id":"audio.generate","method":"POST","path":"/v1/tools/audio/generate","title":{"pt-br":"Gerar efeito sonoro ou música","en":"Generate a sound effect or music","es":"Generar efecto de sonido o música","fr":"Générer un effet sonore ou une musique"},"scopes":["tools:audio:speak","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por segundo gerado","en":"Per generated second","es":"Por segundo generado","fr":"Par seconde générée"}},"dry_run":true,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"job_id":"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64","kind":"generate","status":"pending","poll_after_seconds":10,"created_at":"2026-10-12T12:00:00Z","expires_at":"2026-10-13T12:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/audio#audio-generate","en":"https://docs.respawntech.dev/en/ferramentas/audio#audio-generate","es":"https://docs.respawntech.dev/es/ferramentas/audio#audio-generate","fr":"https://docs.respawntech.dev/fr/ferramentas/audio#audio-generate"}},{"id":"audio.analyze","method":"POST","path":"/v1/tools/audio/analyze","title":{"pt-br":"Analisar áudio","en":"Analyze audio","es":"Analizar audio","fr":"Analyser un audio"},"scopes":["tools:audio:process","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Preço fixo por chamada","en":"Flat price per call","es":"Precio fijo por llamada","fr":"Prix fixe par appel"}},"dry_run":true,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"job_id":"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64","kind":"analyze","status":"pending","poll_after_seconds":10,"created_at":"2026-10-12T12:00:00Z","expires_at":"2026-10-13T12:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/audio#audio-analyze","en":"https://docs.respawntech.dev/en/ferramentas/audio#audio-analyze","es":"https://docs.respawntech.dev/es/ferramentas/audio#audio-analyze","fr":"https://docs.respawntech.dev/fr/ferramentas/audio#audio-analyze"}},{"id":"audio.convert","method":"POST","path":"/v1/tools/audio/convert","title":{"pt-br":"Converter ou normalizar áudio","en":"Convert or normalize audio","es":"Convertir o normalizar audio","fr":"Convertir ou normaliser un audio"},"scopes":["tools:audio:process","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por minuto iniciado de áudio","en":"Per started minute of audio","es":"Por minuto iniciado de audio","fr":"Par minute d'audio commencée"}},"dry_run":true,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"job_id":"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64","kind":"convert","status":"pending","poll_after_seconds":10,"created_at":"2026-10-12T12:00:00Z","expires_at":"2026-10-13T12:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/audio#audio-convert","en":"https://docs.respawntech.dev/en/ferramentas/audio#audio-convert","es":"https://docs.respawntech.dev/es/ferramentas/audio#audio-convert","fr":"https://docs.respawntech.dev/fr/ferramentas/audio#audio-convert"}},{"id":"audio.summarize","method":"POST","path":"/v1/tools/audio/summarize","title":{"pt-br":"Resumir áudio","en":"Summarize audio","es":"Resumir audio","fr":"Résumer un audio"},"scopes":["tools:audio:transcribe","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por minuto iniciado (mais a transcrição de um áudio novo)","en":"Per started minute (plus the transcription of a new audio)","es":"Por minuto iniciado (más la transcripción de un audio nuevo)","fr":"Par minute commencée (plus la transcription d'un nouvel audio)"}},"dry_run":true,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"job_id":"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64","kind":"summarize","status":"pending","poll_after_seconds":10,"created_at":"2026-10-12T12:00:00Z","expires_at":"2026-10-13T12:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/audio#audio-summarize","en":"https://docs.respawntech.dev/en/ferramentas/audio#audio-summarize","es":"https://docs.respawntech.dev/es/ferramentas/audio#audio-summarize","fr":"https://docs.respawntech.dev/fr/ferramentas/audio#audio-summarize"}},{"id":"audio.translate","method":"POST","path":"/v1/tools/audio/translate","title":{"pt-br":"Traduzir áudio","en":"Translate audio","es":"Traducir audio","fr":"Traduire un audio"},"scopes":["tools:audio:transcribe","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Por minuto iniciado (mais a transcrição de um áudio novo)","en":"Per started minute (plus the transcription of a new audio)","es":"Por minuto iniciado (más la transcripción de un audio nuevo)","fr":"Par minute commencée (plus la transcription d'un nouvel audio)"}},"dry_run":true,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"job_id":"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64","kind":"translate","status":"pending","poll_after_seconds":10,"created_at":"2026-10-12T12:00:00Z","expires_at":"2026-10-13T12:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/audio#audio-translate","en":"https://docs.respawntech.dev/en/ferramentas/audio#audio-translate","es":"https://docs.respawntech.dev/es/ferramentas/audio#audio-translate","fr":"https://docs.respawntech.dev/fr/ferramentas/audio#audio-translate"}},{"id":"audio.revoice","method":"POST","path":"/v1/tools/audio/revoice","title":{"pt-br":"Falar a gravação de novo com outra voz","en":"Speak the recording again in another voice","es":"Volver a hablar la grabación con otra voz","fr":"Reprononcer l'enregistrement avec une autre voix"},"scopes":["tools:audio:speak","tools:manage"],"billing":{"kind":"paid","summary":{"pt-br":"Soma das etapas (transcrição, tradução e fala)","en":"Sum of the stages (transcription, translation and speech)","es":"Suma de las etapas (transcripción, traducción y habla)","fr":"Somme des étapes (transcription, traduction et parole)"}},"dry_run":true,"response":{"status":202,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"job_id":"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64","kind":"revoice","status":"pending","poll_after_seconds":10,"created_at":"2026-10-12T12:00:00Z","expires_at":"2026-10-13T12:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/audio#audio-revoice","en":"https://docs.respawntech.dev/en/ferramentas/audio#audio-revoice","es":"https://docs.respawntech.dev/es/ferramentas/audio#audio-revoice","fr":"https://docs.respawntech.dev/fr/ferramentas/audio#audio-revoice"}},{"id":"audio.list","method":"GET","path":"/v1/tools/audio/jobs","title":{"pt-br":"Listar jobs de áudio","en":"List audio jobs","es":"Listar jobs de audio","fr":"Lister les jobs audio"},"scopes":["tools:audio:read","tools:audio:transcribe","tools:audio:speak","tools:audio:process","tools:manage"],"billing":{"kind":"metered","operation":"audio.list"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"jobs":[{"job_id":"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64","kind":"transcribe","status":"completed","duration_ms":125000,"cost_units":30000,"created_at":"2026-10-12T12:00:00Z","expires_at":"2026-10-13T12:00:00Z"}],"total":1}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/audio#audio-list","en":"https://docs.respawntech.dev/en/ferramentas/audio#audio-list","es":"https://docs.respawntech.dev/es/ferramentas/audio#audio-list","fr":"https://docs.respawntech.dev/fr/ferramentas/audio#audio-list"}},{"id":"audio.get","method":"GET","path":"/v1/tools/audio/jobs/{id}","title":{"pt-br":"Status e resultado do job","en":"Job status and result","es":"Estado y resultado del job","fr":"Statut et résultat du job"},"scopes":["tools:audio:read","tools:audio:transcribe","tools:audio:speak","tools:audio:process","tools:manage"],"billing":{"kind":"metered","operation":"audio.status"},"dry_run":false,"response":{"status":200,"body":{"success":true,"trace_id":"550e8400-e29b-41d4-a716-446655440000","data":{"job_id":"5b1d8c2e-6f4a-4e7b-9c3d-2a8e1f0b7d64","kind":"transcribe","status":"completed","duration_ms":125000,"cost_units":30000,"language":"pt","format":"text","transcript":"Olá, pessoal, bem-vindos ao episódio de hoje. Vamos falar de como automatizar relatórios.","segments":2,"download_url":"https://files.respawntech.dev/audio/5b1d8c2e.txt?signature=…","download_expires_at":"2026-10-12T12:15:00Z","created_at":"2026-10-12T12:00:00Z","expires_at":"2026-10-13T12:00:00Z"}}},"docs_url":{"pt-br":"https://docs.respawntech.dev/ferramentas/audio#audio-get","en":"https://docs.respawntech.dev/en/ferramentas/audio#audio-get","es":"https://docs.respawntech.dev/es/ferramentas/audio#audio-get","fr":"https://docs.respawntech.dev/fr/ferramentas/audio#audio-get"}}],"pricing":[{"id":"speech-to-text","title":{"pt-br":"Transcrição e texto","en":"Transcription and text","es":"Transcripción y texto","fr":"Transcription et texte"},"description":{"pt-br":"Por minuto iniciado de áudio. Um áudio cuja duração não dá para ler do cabeçalho é cobrado pelo teto (60 minutos) e a diferença é devolvida no fim.","en":"Per started minute of audio. An audio whose length cannot be read from its header is charged at the cap (60 minutes) and the difference is refunded at the end.","es":"Por minuto iniciado de audio. Un audio cuya duración no se puede leer del encabezado se cobra por el tope (60 minutos) y la diferencia se devuelve al final.","fr":"Par minute d'audio commencée. Un audio dont la durée ne se lit pas dans l'en-tête est facturé au plafond (60 minutes) et la différence est remboursée à la fin."},"items":[{"label":{"pt-br":"Transcrição (por minuto)","en":"Transcription (per minute)","es":"Transcripción (por minuto)","fr":"Transcription (par minute)"},"service":"audio_transcribe_minute","credits":1.5},{"label":{"pt-br":"Quem falou, `diarize` (por minuto)","en":"Who spoke, `diarize` (per minute)","es":"Quién habló, `diarize` (por minuto)","fr":"Qui parle, `diarize` (par minute)"},"service":"audio_diarize_minute","credits":0.5,"additive":true},{"label":{"pt-br":"Resumo (por minuto)","en":"Summary (per minute)","es":"Resumen (por minuto)","fr":"Résumé (par minute)"},"service":"audio_summarize_minute","credits":0.5,"note":{"pt-br":"Mais a transcrição quando parte de um áudio novo; com `job_id` de uma transcrição pronta, só o resumo","en":"Plus the transcription when starting from a new audio; with the `job_id` of a finished transcription, only the summary","es":"Más la transcripción cuando parte de un audio nuevo; con el `job_id` de una transcripción lista, solo el resumen","fr":"Plus la transcription quand on part d'un nouvel audio ; avec le `job_id` d'une transcription prête, seulement le résumé"}},{"label":{"pt-br":"Tradução (por minuto)","en":"Translation (per minute)","es":"Traducción (por minuto)","fr":"Traduction (par minute)"},"service":"audio_translate_minute","credits":1,"note":{"pt-br":"Mais a transcrição quando parte de um áudio novo; com `job_id` de uma transcrição pronta, só a tradução","en":"Plus the transcription when starting from a new audio; with the `job_id` of a finished transcription, only the translation","es":"Más la transcripción cuando parte de un audio nuevo; con el `job_id` de una transcripción lista, solo la traducción","fr":"Plus la transcription quand on part d'un nouvel audio ; avec le `job_id` d'une transcription prête, seulement la traduction"}}]},{"id":"text-to-speech","title":{"pt-br":"Voz e sons","en":"Voice and sounds","es":"Voz y sonidos","fr":"Voix et sons"},"items":[{"label":{"pt-br":"Fala com voz do catálogo (por 1.000 caracteres iniciados)","en":"Speech with a catalog voice (per started 1,000 characters)","es":"Habla con voz del catálogo (por 1.000 caracteres iniciados)","fr":"Parole avec une voix du catalogue (par 1 000 caractères commencés)"},"service":"audio_speak_1k_chars","credits":3},{"label":{"pt-br":"Fala com voz clonada (por 1.000 caracteres iniciados)","en":"Speech with a cloned voice (per started 1,000 characters)","es":"Habla con voz clonada (por 1.000 caracteres iniciados)","fr":"Parole avec une voix clonée (par 1 000 caractères commencés)"},"service":"audio_speak_clone_1k_chars","credits":9},{"label":{"pt-br":"Efeito sonoro ou música (por segundo gerado)","en":"Sound effect or music (per generated second)","es":"Efecto de sonido o música (por segundo generado)","fr":"Effet sonore ou musique (par seconde générée)"},"service":"audio_generate_second","credits":0.2},{"label":{"pt-br":"Criar uma voz clonada (`voice_create`, só no MCP)","en":"Create a cloned voice (`voice_create`, MCP only)","es":"Crear una voz clonada (`voice_create`, solo en MCP)","fr":"Créer une voix clonée (`voice_create`, MCP uniquement)"},"service":"audio_voice_create","credits":5,"note":{"pt-br":"Devolvido se a amostra for recusada","en":"Refunded if the sample is rejected","es":"Se devuelve si la muestra es rechazada","fr":"Remboursé si l'échantillon est refusé"}}]},{"id":"processing","title":{"pt-br":"Análise e conversão","en":"Analysis and conversion","es":"Análisis y conversión","fr":"Analyse et conversion"},"items":[{"label":{"pt-br":"Análise (por chamada)","en":"Analysis (per call)","es":"Análisis (por llamada)","fr":"Analyse (par appel)"},"service":"audio_analyze","credits":0.1},{"label":{"pt-br":"Conversão ou normalização (por minuto iniciado)","en":"Conversion or normalization (per started minute)","es":"Conversión o normalización (por minuto iniciado)","fr":"Conversion ou normalisation (par minute commencée)"},"service":"audio_convert_minute","credits":0.1}]},{"id":"revoice","title":{"pt-br":"Falar de novo (revoice)","en":"Speak again (revoice)","es":"Hablar de nuevo (revoice)","fr":"Reparler (revoice)"},"description":{"pt-br":"Cada etapa é cobrada pelo preço do serviço dela e conferida no fim: a tradução que não foi necessária e a etapa que falhou são devolvidas. O `dry_run` traz o preço de cada etapa.","en":"Each stage is charged at its own service price and reconciled at the end: a translation that was not needed and a stage that failed are refunded. The `dry_run` shows the price of each stage.","es":"Cada etapa se cobra por el precio de su servicio y se concilia al final: la traducción que no hizo falta y la etapa que falló se devuelven. El `dry_run` muestra el precio de cada etapa.","fr":"Chaque étape est facturée au prix de son service et rapprochée à la fin : la traduction devenue inutile et l'étape en échec sont remboursées. Le `dry_run` donne le prix de chaque étape."},"items":[{"label":{"pt-br":"Transcrição + (tradução) + fala","en":"Transcription + (translation) + speech","es":"Transcripción + (traducción) + habla","fr":"Transcription + (traduction) + parole"},"display":{"pt-br":"Soma das três etapas","en":"Sum of the three stages","es":"Suma de las tres etapas","fr":"Somme des trois étapes"},"note":{"pt-br":"A fala é estimada em 1.200 caracteres por minuto e cobrada sobre o texto real","en":"Speech is estimated at 1,200 characters per minute and charged on the real text","es":"El habla se estima en 1.200 caracteres por minuto y se cobra sobre el texto real","fr":"La parole est estimée à 1 200 caractères par minute et facturée sur le texte réel"}}]}]}],"metered_reads":{"qr_dynamic.list":{"service":"api_read_qr_list","label":{"pt-br":"Listagem de QRs","en":"QR listing","es":"Listado de QR","fr":"Liste des QR"},"credits":0.005},"qr_dynamic.slug_check":{"service":"api_read_qr_list","label":{"pt-br":"Verificar slug","en":"Check slug","es":"Verificar slug","fr":"Vérifier un slug"},"credits":0.005},"qr_dynamic.get":{"service":"api_read_qr_status","label":{"pt-br":"Detalhe de QR","en":"QR detail","es":"Detalle de QR","fr":"Détail d'un QR"},"credits":0.01},"qr_dynamic.analytics":{"service":"api_read_qr_analytics","label":{"pt-br":"Analytics (overview)","en":"Analytics (overview)","es":"Analíticas (resumen)","fr":"Analytics (vue d'ensemble)"},"credits":0.03},"qr_dynamic.analytics_period":{"service":"api_read_qr_analytics","label":{"pt-br":"Analytics por período","en":"Analytics by period","es":"Analíticas por período","fr":"Analytics par période"},"credits":0.03},"qr_dynamic.analytics_device":{"service":"api_read_qr_analytics","label":{"pt-br":"Analytics por dispositivo","en":"Analytics by device","es":"Analíticas por dispositivo","fr":"Analytics par appareil"},"credits":0.03},"qr_dynamic.scans":{"service":"api_read_qr_scans","label":{"pt-br":"Histórico de scans","en":"Scan history","es":"Historial de escaneos","fr":"Historique des scans"},"credits":0.01},"pdf.list":{"service":"api_read_pdf_list","label":{"pt-br":"Listagem de conversões","en":"Conversion listing","es":"Listado de conversiones","fr":"Liste des conversions"},"credits":0.005},"pdf.status":{"service":"api_read_pdf_status","label":{"pt-br":"Status da conversão","en":"Conversion status","es":"Estado de la conversión","fr":"Statut de la conversion"},"credits":0.01},"pdf.download":{"service":"api_read_pdf_download","label":{"pt-br":"Download de arquivo","en":"File download","es":"Descarga de archivo","fr":"Téléchargement de fichier"},"credits":0.03},"pdf.batch_list":{"service":"api_read_pdf_list","label":{"pt-br":"Listagem de batches","en":"Batch listing","es":"Listado de batches","fr":"Liste des batches"},"credits":0.005},"pdf.batch_status":{"service":"api_read_pdf_status","label":{"pt-br":"Detalhe do batch","en":"Retrieve batch","es":"Detalle del batch","fr":"Détail du batch"},"credits":0.01},"pdf.batch_download":{"service":"api_read_pdf_download","label":{"pt-br":"Download ZIP do batch","en":"Download batch ZIP","es":"Descargar ZIP del batch","fr":"Téléchargement ZIP du batch"},"credits":0.03},"safety.check":{"service":"api_read_safety_check","label":{"pt-br":"Verificar URL","en":"Check URL","es":"Verificar URL","fr":"Vérifier une URL"},"credits":0.01},"safety.favicon":{"service":"api_read_safety_favicon","label":{"pt-br":"Proxy de favicon","en":"Favicon proxy","es":"Proxy de favicon","fr":"Proxy de favicon"},"credits":0.002},"ci.error_extract":{"service":"api_read_ci_error_extract","label":{"pt-br":"Extrair erro de log de CI","en":"Extract error from CI log","es":"Extraer error de log de CI","fr":"Extraire l'erreur d'un log de CI"},"credits":0.01},"web.markdown":{"service":"api_read_web_markdown","label":{"pt-br":"Página web para Markdown","en":"Web Page to Markdown","es":"Página web a Markdown","fr":"Page web vers Markdown"},"credits":0.01},"notify.send":{"service":"api_notify_send","label":{"pt-br":"Enviar notificação","en":"Send notification","es":"Enviar notificación","fr":"Envoyer une notification"},"credits":0.03},"video.list":{"service":"api_read_video_list","label":{"pt-br":"Listagem de renders de vídeo","en":"Video render listing","es":"Listado de renders de video","fr":"Liste des rendus vidéo"},"credits":0.005},"video.status":{"service":"api_read_video_status","label":{"pt-br":"Status do render de vídeo","en":"Video render status","es":"Estado del render de video","fr":"Statut du rendu vidéo"},"credits":0.01},"video.download":{"service":"api_read_video_download","label":{"pt-br":"Download do vídeo","en":"Video download","es":"Descarga del video","fr":"Téléchargement de la vidéo"},"credits":0.03},"audio.list":{"service":"api_read_audio_list","label":{"pt-br":"Listagem de jobs de áudio","en":"Audio job listing","es":"Listado de jobs de audio","fr":"Liste des jobs audio"},"credits":0.005},"audio.status":{"service":"api_read_audio_status","label":{"pt-br":"Status do job de áudio","en":"Audio job status","es":"Estado del job de audio","fr":"Statut du job audio"},"credits":0.01},"audio.download":{"service":"api_read_audio_download","label":{"pt-br":"Download do resultado de áudio","en":"Audio result download","es":"Descarga del resultado de audio","fr":"Téléchargement du résultat audio"},"credits":0.03},"webhook_relay.messages":{"service":"api_read_webhook_messages","label":{"pt-br":"Log de mensagens de webhook","en":"Webhook message log","es":"Registro de mensajes de webhook","fr":"Journal des messages de webhook"},"credits":0.002},"realtime.history":{"service":"api_read_realtime_history","label":{"pt-br":"Histórico de canal do Realtime","en":"Realtime channel history","es":"Historial de canal de Realtime","fr":"Historique de canal Realtime"},"credits":0.002}},"daily_free_reads":2000,"errors":[{"code":"auth.missing_token","http":401,"group":"auth","when":{"pt-br":"Header de autenticação não enviado.","en":"Authentication header not sent.","es":"No se envió el header de autenticación.","fr":"Header d'authentification non envoyé."}},{"code":"auth.invalid_token","http":401,"group":"auth","when":{"pt-br":"Token malformado.","en":"Malformed token.","es":"Token con formato inválido.","fr":"Token mal formé."}},{"code":"auth.unauthorized","http":401,"group":"auth","when":{"pt-br":"Token inválido, expirado ou revogado.","en":"Invalid, expired or revoked token.","es":"Token inválido, expirado o revocado.","fr":"Token invalide, expiré ou révoqué."}},{"code":"auth.email_not_verified","http":403,"group":"auth","when":{"pt-br":"E-mail da conta ainda não verificado.","en":"Account email not verified yet.","es":"El correo electrónico de la cuenta aún no está verificado.","fr":"E-mail du compte pas encore vérifié."}},{"code":"scope.missing","http":403,"group":"auth","when":{"pt-br":"Token sem o escopo exigido pela rota (`details.required`, `details.provided`).","en":"Token lacks the scope required by the route (`details.required`, `details.provided`).","es":"El token no tiene el scope requerido por la ruta (`details.required`, `details.provided`).","fr":"Le token n'a pas le scope exigé par la route (`details.required`, `details.provided`)."}},{"code":"tool.disabled","http":403,"group":"auth","when":{"pt-br":"A ferramenta ainda não está liberada para a conta: está desligada ou em testes com contas selecionadas. Ferramentas com código próprio (como `rag.disabled`) respondem com ele.","en":"The tool is not enabled for the account yet: it is switched off or being tested with selected accounts. Tools with their own code (such as `rag.disabled`) return that one instead.","es":"La herramienta aún no está habilitada para la cuenta: está desactivada o en pruebas con cuentas seleccionadas. Las herramientas con código propio (como `rag.disabled`) responden con ese.","fr":"L’outil n’est pas encore activé pour le compte : il est désactivé ou en test avec des comptes sélectionnés. Les outils qui ont leur propre code (comme `rag.disabled`) renvoient celui-ci."}},{"code":"forbidden","http":403,"group":"auth","when":{"pt-br":"A chave não pode fazer esta ação (ex.: chave restrita a bases de conhecimento específicas tentando criar uma base).","en":"The key is not allowed to perform this action (e.g. a key restricted to specific knowledge bases trying to create one).","es":"La clave no puede realizar esta acción (p. ej., una clave restringida a bases de conocimiento específicas intentando crear una base).","fr":"La clé ne peut pas effectuer cette action (ex. : clé restreinte à certaines bases de connaissances qui tente de créer une base)."}},{"code":"billing.insufficient_credits","http":402,"group":"billing","when":{"pt-br":"Saldo insuficiente para a operação.","en":"Insufficient balance for the operation.","es":"Saldo insuficiente para la operación.","fr":"Solde insuffisant pour l'opération."}},{"code":"billing.user_bankrupt","http":402,"group":"billing","when":{"pt-br":"Conta suspensa por saldo negativo.","en":"Account suspended due to negative balance.","es":"Cuenta suspendida por saldo negativo.","fr":"Compte suspendu pour solde négatif."}},{"code":"billing.spend_limit_reached","http":402,"group":"billing","when":{"pt-br":"A cobrança passaria do limite diário de gasto da chave (`details.limit`, `details.spent_today`, `details.required`).","en":"The charge would exceed the key's daily spend limit (`details.limit`, `details.spent_today`, `details.required`).","es":"El cobro superaría el límite diario de gasto de la clave (`details.limit`, `details.spent_today`, `details.required`).","fr":"Le débit dépasserait la limite de dépense quotidienne de la clé (`details.limit`, `details.spent_today`, `details.required`)."}},{"code":"billing.call_limit_exceeded","http":402,"group":"billing","when":{"pt-br":"Uma única cobrança passaria do teto por chamada da conexão MCP (`max_call_credits`).","en":"A single charge would exceed the MCP connection's per-call cap (`max_call_credits`).","es":"Un solo cobro superaría el tope por llamada de la conexión MCP (`max_call_credits`).","fr":"Un seul débit dépasserait le plafond par appel de la connexion MCP (`max_call_credits`)."}},{"code":"validation.failed","http":400,"group":"request","when":{"pt-br":"Payload inválido; veja `details.fields`.","en":"Invalid payload; see `details.fields`.","es":"Payload inválido; consulta `details.fields`.","fr":"Payload invalide ; voir `details.fields`."}},{"code":"validation.invalid_id","http":400,"group":"request","when":{"pt-br":"UUID ou ID malformado.","en":"Malformed UUID or ID.","es":"UUID o ID con formato inválido.","fr":"UUID ou ID mal formé."}},{"code":"idempotency.conflict","http":409,"group":"request","when":{"pt-br":"Chave de idempotência reutilizada com payload diferente.","en":"Idempotency key reused with a different payload.","es":"Clave de idempotencia reutilizada con un payload diferente.","fr":"Clé d'idempotence réutilisée avec un payload différent."}},{"code":"not_found","http":404,"group":"request","when":{"pt-br":"Recurso inexistente, de outra conta ou fora das restrições da chave.","en":"Resource doesn't exist, belongs to another account or is outside the key's restrictions.","es":"Recurso inexistente, de otra cuenta o fuera de las restricciones de la clave.","fr":"Ressource inexistante, appartenant à un autre compte ou hors des restrictions de la clé."}},{"code":"conflict","http":409,"group":"request","when":{"pt-br":"O recurso está num estado que não permite a operação (ex.: documento sendo excluído).","en":"The resource is in a state that does not allow the operation (e.g. a document being deleted).","es":"El recurso está en un estado que no permite la operación (p. ej., un documento en proceso de eliminación).","fr":"La ressource est dans un état qui ne permet pas l'opération (ex. : document en cours de suppression)."}},{"code":"rate_limit.exceeded","http":429,"group":"request","when":{"pt-br":"Limite de requisições excedido; aguarde `X-RateLimit-Reset`.","en":"Rate limit exceeded; wait until `X-RateLimit-Reset`.","es":"Límite de solicitudes excedido; espera hasta `X-RateLimit-Reset`.","fr":"Limite de requêtes dépassée ; attendez `X-RateLimit-Reset`."}},{"code":"mcp.disabled","http":404,"group":"agents","when":{"pt-br":"Servidor MCP desligado.","en":"MCP server disabled.","es":"Servidor MCP desactivado.","fr":"Serveur MCP désactivé."}},{"code":"mcp.agents_disabled","http":403,"group":"agents","when":{"pt-br":"Acesso de agentes de IA desligado na conta (botão de pânico no dashboard).","en":"AI agent access is disabled for the account (panic button in the dashboard).","es":"Acceso de agentes de IA desactivado en la cuenta (botón de pánico en el dashboard).","fr":"Accès des agents IA désactivé sur le compte (bouton panique du dashboard)."}},{"code":"mcp.tool_disabled","http":403,"group":"agents","when":{"pt-br":"Ferramenta desligada temporariamente para agentes.","en":"Tool temporarily disabled for agents.","es":"Herramienta desactivada temporalmente para agentes.","fr":"Outil temporairement désactivé pour les agents."}},{"code":"mcp.timeout","http":504,"group":"agents","when":{"pt-br":"A ferramenta passou do tempo limite; tente de novo.","en":"The tool timed out; try again.","es":"La herramienta superó el tiempo límite; inténtalo de nuevo.","fr":"L'outil a dépassé le délai ; réessayez."}},{"code":"mcp.result_too_large","http":400,"group":"agents","when":{"pt-br":"Resultado acima de 32 KB; peça menos itens ou `response_format=concise`.","en":"Result over 32 KB; request fewer items or `response_format=concise`.","es":"Resultado mayor de 32 KB; pide menos elementos o `response_format=concise`.","fr":"Résultat supérieur à 32 KB ; demandez moins d'éléments ou `response_format=concise`."}},{"code":"mcp.tools_search_rate_limited","http":429,"group":"agents","when":{"pt-br":"Mais de 60 chamadas de `tools_search` por minuto.","en":"More than 60 `tools_search` calls per minute.","es":"Más de 60 llamadas a `tools_search` por minuto.","fr":"Plus de 60 appels à `tools_search` par minute."}},{"code":"internal.unexpected","http":500,"group":"platform","when":{"pt-br":"Erro interno inesperado. Informe o `trace_id` ao suporte.","en":"Unexpected internal error. Share the `trace_id` with support.","es":"Error interno inesperado. Proporciona el `trace_id` al soporte.","fr":"Erreur interne inattendue. Communiquez le `trace_id` au support."}},{"code":"gateway.timeout","http":504,"group":"platform","when":{"pt-br":"Um serviço interno demorou demais para responder; tente de novo.","en":"An internal service took too long to respond; try again.","es":"Un servicio interno tardó demasiado en responder; inténtalo de nuevo.","fr":"Un service interne a mis trop de temps à répondre ; réessayez."}},{"code":"dynamic_qr.name_required","http":400,"group":"qr-dinamico","when":{"pt-br":"Campo `name` obrigatório.","en":"`name` field is required.","es":"El campo `name` es obligatorio.","fr":"Champ `name` obligatoire."}},{"code":"dynamic_qr.invalid_url","http":400,"group":"qr-dinamico","when":{"pt-br":"URL de destino inválida.","en":"Invalid destination URL.","es":"URL de destino inválida.","fr":"URL de destination invalide."}},{"code":"dynamic_qr.invalid_slug","http":400,"group":"qr-dinamico","when":{"pt-br":"Slug fora do padrão `[a-zA-Z0-9-]{3,50}`.","en":"Slug does not match `[a-zA-Z0-9-]{3,50}`.","es":"Slug fuera del patrón `[a-zA-Z0-9-]{3,50}`.","fr":"Slug non conforme au motif `[a-zA-Z0-9-]{3,50}`."}},{"code":"dynamic_qr.malicious_url","http":403,"group":"qr-dinamico","when":{"pt-br":"URL bloqueada pela verificação de segurança.","en":"URL blocked by the safety check.","es":"URL bloqueada por la verificación de seguridad.","fr":"URL bloquée par la vérification de sécurité."}},{"code":"dynamic_qr.not_found","http":404,"group":"qr-dinamico","when":{"pt-br":"QR inexistente ou de outra conta.","en":"QR does not exist or belongs to another account.","es":"El QR no existe o pertenece a otra cuenta.","fr":"QR inexistant ou appartenant à un autre compte."}},{"code":"dynamic_qr.slug_taken","http":409,"group":"qr-dinamico","when":{"pt-br":"Slug já em uso.","en":"Slug already in use.","es":"El slug ya está en uso.","fr":"Slug déjà utilisé."}},{"code":"dynamic_qr.invalid_rules","http":400,"group":"qr-dinamico","when":{"pt-br":"Regra de redirect inválida: sem condição, `id` repetido, valor de condição desconhecido ou mais de 20 regras.","en":"Invalid redirect rule: no condition, duplicate `id`, unknown condition value or more than 20 rules.","es":"Regla de redirect inválida: sin condición, `id` repetido, valor de condición desconocido o más de 20 reglas.","fr":"Règle de redirection invalide : sans condition, `id` en double, valeur de condition inconnue ou plus de 20 règles."}},{"code":"dynamic_qr.invalid_variants","http":400,"group":"qr-dinamico","when":{"pt-br":"Teste A/B inválido: de 2 a 10 variantes, `id` único, `weight` de 1 a 1000 e `target_url` absoluta.","en":"Invalid A/B test: 2 to 10 variants, unique `id`, `weight` from 1 to 1000 and an absolute `target_url`.","es":"Prueba A/B inválida: de 2 a 10 variantes, `id` único, `weight` de 1 a 1000 y `target_url` absoluta.","fr":"Test A/B invalide : de 2 à 10 variantes, `id` unique, `weight` de 1 à 1000 et `target_url` absolue."}},{"code":"dynamic_qr.invalid_pixels","http":400,"group":"qr-dinamico","when":{"pt-br":"ID de pixel fora do formato (GA4 `G-XXXX`, Meta só dígitos, GTM `GTM-XXXX`).","en":"Pixel ID in the wrong format (GA4 `G-XXXX`, Meta digits only, GTM `GTM-XXXX`).","es":"ID de píxel con formato inválido (GA4 `G-XXXX`, Meta solo dígitos, GTM `GTM-XXXX`).","fr":"ID de pixel au mauvais format (GA4 `G-XXXX`, Meta chiffres uniquement, GTM `GTM-XXXX`)."}},{"code":"dynamic_qr.invalid_fallback_urls","http":400,"group":"qr-dinamico","when":{"pt-br":"`expired_url` e `limit_reached_url` precisam ser URLs absolutas.","en":"`expired_url` and `limit_reached_url` must be absolute URLs.","es":"`expired_url` y `limit_reached_url` deben ser URLs absolutas.","fr":"`expired_url` et `limit_reached_url` doivent être des URL absolues."}},{"code":"dynamic_qr.invalid_content","http":400,"group":"qr-dinamico","when":{"pt-br":"Conteúdo hospedado inválido para o `type` informado.","en":"Invalid hosted content for the given `type`.","es":"Contenido alojado inválido para el `type` indicado.","fr":"Contenu hébergé invalide pour le `type` indiqué."}},{"code":"dynamic_qr.folder_not_found","http":404,"group":"qr-dinamico","when":{"pt-br":"Pasta inexistente ou de outra conta.","en":"Folder does not exist or belongs to another account.","es":"Carpeta inexistente o de otra cuenta.","fr":"Dossier inexistant ou appartenant à un autre compte."}},{"code":"dynamic_qr.folder_taken","http":409,"group":"qr-dinamico","when":{"pt-br":"Já existe uma pasta com esse nome.","en":"A folder with that name already exists.","es":"Ya existe una carpeta con ese nombre.","fr":"Un dossier portant ce nom existe déjà."}},{"code":"dynamic_qr.folder_invalid","http":400,"group":"qr-dinamico","when":{"pt-br":"O nome da pasta tem de 1 a 80 caracteres.","en":"The folder name must be 1 to 80 characters.","es":"El nombre de la carpeta tiene de 1 a 80 caracteres.","fr":"Le nom du dossier compte de 1 à 80 caractères."}},{"code":"dynamic_qr.tags_invalid","http":400,"group":"qr-dinamico","when":{"pt-br":"Tags inválidas: até 20 por QR, até 40 caracteres cada e sem vírgula ou ponto e vírgula.","en":"Invalid tags: up to 20 per QR code, up to 40 characters each, and no commas or semicolons.","es":"Tags inválidas: hasta 20 por QR, hasta 40 caracteres cada una y sin coma ni punto y coma.","fr":"Tags invalides : jusqu'à 20 par QR, 40 caractères chacun, sans virgule ni point-virgule."}},{"code":"dynamic_qr.bulk_invalid","http":400,"group":"qr-dinamico","when":{"pt-br":"Edição em massa inválida: `action` desconhecida, de 1 a 500 `ids` ou campo exigido pela ação ausente.","en":"Invalid bulk edit: unknown `action`, not 1 to 500 `ids`, or a field required by the action is missing.","es":"Edición masiva inválida: `action` desconocida, de 1 a 500 `ids` o falta un campo exigido por la acción.","fr":"Modification en masse invalide : `action` inconnue, de 1 à 500 `ids` ou champ requis par l'action manquant."}},{"code":"dynamic_qr.batch_not_found","http":404,"group":"qr-dinamico","when":{"pt-br":"Lote inexistente ou de outra conta.","en":"Batch does not exist or belongs to another account.","es":"Lote inexistente o de otra cuenta.","fr":"Lot inexistant ou appartenant à un autre compte."}},{"code":"dynamic_qr.batch_invalid","http":400,"group":"qr-dinamico","when":{"pt-br":"Lote sem linhas ou CSV malformado (sem cabeçalho, coluna desconhecida ou sem `target_url`).","en":"Batch has no rows or the CSV is malformed (no header, unknown column or no `target_url`).","es":"Lote sin filas o CSV mal formado (sin encabezado, columna desconocida o sin `target_url`).","fr":"Lot sans lignes ou CSV mal formé (sans en-tête, colonne inconnue ou sans `target_url`)."}},{"code":"dynamic_qr.batch_too_large","http":413,"group":"qr-dinamico","when":{"pt-br":"Mais de 1.000 linhas no lote.","en":"More than 1,000 rows in the batch.","es":"Más de 1.000 filas en el lote.","fr":"Plus de 1 000 lignes dans le lot."}},{"code":"dynamic_qr.scan_range_invalid","http":400,"group":"qr-dinamico","when":{"pt-br":"`from`/`to`/`before` fora do formato `YYYY-MM-DD`.","en":"`from`/`to`/`before` not in `YYYY-MM-DD` format.","es":"`from`/`to`/`before` fuera del formato `YYYY-MM-DD`.","fr":"`from`/`to`/`before` au mauvais format `YYYY-MM-DD`."}},{"code":"qr_domain.invalid_hostname","http":400,"group":"qr-dinamico","when":{"pt-br":"O `hostname` precisa ser um subdomínio público, como `qr.suaempresa.com` (sem porta, caminho, IP ou curinga).","en":"The `hostname` must be a public subdomain, such as `qr.suaempresa.com` (no port, path, IP or wildcard).","es":"El `hostname` debe ser un subdominio público, como `qr.suaempresa.com` (sin puerto, ruta, IP ni comodín).","fr":"Le `hostname` doit être un sous-domaine public, comme `qr.suaempresa.com` (sans port, chemin, IP ni joker)."}},{"code":"qr_domain.not_found","http":404,"group":"qr-dinamico","when":{"pt-br":"Domínio inexistente ou de outra conta.","en":"Domain does not exist or belongs to another account.","es":"Dominio inexistente o de otra cuenta.","fr":"Domaine inexistant ou appartenant à un autre compte."}},{"code":"qr_domain.taken","http":409,"group":"qr-dinamico","when":{"pt-br":"Esse hostname já está cadastrado.","en":"This hostname is already registered.","es":"Ese hostname ya está registrado.","fr":"Ce hostname est déjà enregistré."}},{"code":"qr_domain.in_use","http":409,"group":"qr-dinamico","when":{"pt-br":"Há QRs usando o domínio; exclua-os ou mova-os antes de remover.","en":"QR codes are using the domain; delete or move them before removing it.","es":"Hay QRs usando el dominio; elimínalos o muévelos antes de eliminarlo.","fr":"Des QR utilisent le domaine ; supprimez-les ou déplacez-les avant de le supprimer."}},{"code":"qr_domain.not_usable","http":409,"group":"qr-dinamico","when":{"pt-br":"O domínio falhou ou está suspenso e não aceita novos QRs.","en":"The domain failed or is suspended and does not accept new QR codes.","es":"El dominio falló o está suspendido y no acepta nuevos QRs.","fr":"Le domaine a échoué ou est suspendu et n'accepte pas de nouveaux QR."}},{"code":"qr_domain.plan_limit","http":403,"group":"qr-dinamico","when":{"pt-br":"O plano não permite mais domínios próprios.","en":"The plan does not allow more custom domains.","es":"El plan no permite más dominios propios.","fr":"L'offre ne permet plus de domaines personnalisés."}},{"code":"qr_domain.unavailable","http":503,"group":"qr-dinamico","when":{"pt-br":"Domínios próprios indisponíveis no momento.","en":"Custom domains are currently unavailable.","es":"Dominios propios no disponibles por el momento.","fr":"Les domaines personnalisés sont indisponibles pour le moment."}},{"code":"qr_domain.provider_failed","http":502,"group":"qr-dinamico","when":{"pt-br":"O provedor de certificados recusou ou não respondeu; tente de novo.","en":"The certificate provider rejected the request or did not respond; try again.","es":"El proveedor de certificados rechazó la solicitud o no respondió; inténtalo de nuevo.","fr":"Le fournisseur de certificats a refusé ou n'a pas répondu ; réessayez."}},{"code":"dynamic_qr.generation_failed","http":502,"group":"qr-dinamico","when":{"pt-br":"Falha ao gerar o QR.","en":"Failed to generate the QR.","es":"Error al generar el QR.","fr":"Échec de la génération du QR."}},{"code":"dynamic_qr.image_generation_failed","http":502,"group":"qr-dinamico","when":{"pt-br":"Falha ao renderizar a imagem do QR.","en":"Failed to render the QR image.","es":"Error al renderizar la imagen del QR.","fr":"Échec du rendu de l'image du QR."}},{"code":"pdf.invalid_request","http":400,"group":"pdf-convert","when":{"pt-br":"Corpo da requisição inválido.","en":"Invalid request body.","es":"Cuerpo de la solicitud inválido.","fr":"Corps de la requête invalide."}},{"code":"pdf.html_required","http":400,"group":"pdf-convert","when":{"pt-br":"`html` e `url` ausentes: envie um dos dois.","en":"Both `html` and `url` are missing: send one of them.","es":"Faltan `html` y `url`: envíe uno de los dos.","fr":"`html` et `url` absents : envoyez l'un des deux."}},{"code":"pdf.html_too_large","http":400,"group":"pdf-convert","when":{"pt-br":"HTML acima de 10 MB.","en":"HTML larger than 10 MB.","es":"HTML de más de 10 MB.","fr":"HTML supérieur à 10 MB."}},{"code":"pdf.css_too_large","http":400,"group":"pdf-convert","when":{"pt-br":"CSS acima de 1 MB.","en":"CSS larger than 1 MB.","es":"CSS de más de 1 MB.","fr":"CSS supérieur à 1 MB."}},{"code":"pdf.invalid_page_size","http":400,"group":"pdf-convert","when":{"pt-br":"Tamanho de página inválido.","en":"Invalid page size.","es":"Tamaño de página inválido.","fr":"Taille de page invalide."}},{"code":"pdf.invalid_format","http":400,"group":"pdf-convert","when":{"pt-br":"Formato diferente de `pdf`, `png` ou `jpg`.","en":"Format is not `pdf`, `png` or `jpg`.","es":"Formato distinto de `pdf`, `png` o `jpg`.","fr":"Format autre que `pdf`, `png` ou `jpg`."}},{"code":"pdf.invalid_url","http":400,"group":"pdf-convert","when":{"pt-br":"`url` (ou `google_fonts_url`) inválida: precisa ser um endereço público `http`/`https` de até 2048 caracteres.","en":"Invalid `url` (or `google_fonts_url`): it must be a public `http`/`https` address of up to 2048 characters.","es":"`url` (o `google_fonts_url`) no válida: debe ser una dirección pública `http`/`https` de hasta 2048 caracteres.","fr":"`url` (ou `google_fonts_url`) invalide : doit être une adresse publique `http`/`https` de 2048 caractères maximum."}},{"code":"pdf.invalid_option","http":400,"group":"pdf-convert","when":{"pt-br":"Opção inválida ou combinação não permitida (`html` com `url`, `post_process` fora do formato `pdf`, `pdfa` com `protection`, `template_id` com `html`…).","en":"Invalid option or disallowed combination (`html` with `url`, `post_process` outside the `pdf` format, `pdfa` with `protection`, `template_id` with `html`…).","es":"Opción no válida o combinación no permitida (`html` con `url`, `post_process` fuera del formato `pdf`, `pdfa` con `protection`, `template_id` con `html`…).","fr":"Option invalide ou combinaison interdite (`html` avec `url`, `post_process` hors du format `pdf`, `pdfa` avec `protection`, `template_id` avec `html`…)."}},{"code":"pdf.template_not_found","http":404,"group":"pdf-convert","when":{"pt-br":"Template (ou versão) não encontrado na conta.","en":"Template (or version) not found in the account.","es":"Plantilla (o versión) no encontrada en la cuenta.","fr":"Template (ou version) introuvable dans le compte."}},{"code":"pdf.font_not_found","http":404,"group":"pdf-convert","when":{"pt-br":"Fonte não encontrada na conta.","en":"Font not found in the account.","es":"Fuente no encontrada en la cuenta.","fr":"Police introuvable dans le compte."}},{"code":"pdf.font_invalid","http":400,"group":"pdf-convert","when":{"pt-br":"Fonte inválida: arquivo ausente, acima de 5 MiB, que não seja woff2/ttf, ou `family`, `weight` ou `style` fora do permitido.","en":"Invalid font: missing file, over 5 MiB, not woff2/ttf, or `family`, `weight` or `style` out of the allowed range.","es":"Fuente no válida: archivo ausente, de más de 5 MiB, que no sea woff2/ttf, o `family`, `weight` o `style` fuera de lo permitido.","fr":"Police invalide : fichier absent, supérieur à 5 MiB, ni woff2 ni ttf, ou `family`, `weight` ou `style` hors des valeurs permises."}},{"code":"pdf.font_exists","http":409,"group":"pdf-convert","when":{"pt-br":"Já existe uma fonte com a mesma família, peso e estilo.","en":"A font with the same family, weight and style already exists.","es":"Ya existe una fuente con la misma familia, peso y estilo.","fr":"Une police avec la même famille, graisse et style existe déjà."}},{"code":"pdf.font_limit","http":400,"group":"pdf-convert","when":{"pt-br":"Limite de 50 fontes por conta atingido.","en":"Limit of 50 fonts per account reached.","es":"Límite de 50 fuentes por cuenta alcanzado.","fr":"Limite de 50 polices par compte atteinte."}},{"code":"pdf.retention_invalid","http":400,"group":"pdf-convert","when":{"pt-br":"`retention_days` inválido.","en":"Invalid `retention_days`.","es":"`retention_days` inválido.","fr":"`retention_days` invalide."}},{"code":"pdf.batch_empty","http":400,"group":"pdf-convert","when":{"pt-br":"Batch sem itens.","en":"Batch has no items.","es":"Batch sin elementos.","fr":"Batch sans éléments."}},{"code":"pdf.batch_too_large","http":400,"group":"pdf-convert","when":{"pt-br":"Mais de 500 itens no batch.","en":"More than 500 items in the batch.","es":"Más de 500 elementos en el batch.","fr":"Plus de 500 éléments dans le batch."}},{"code":"pdf.template_required","http":400,"group":"pdf-convert","when":{"pt-br":"Template HTML obrigatório no batch.","en":"HTML template is required in a batch.","es":"La plantilla HTML es obligatoria en el batch.","fr":"Template HTML obligatoire dans le batch."}},{"code":"pdf.not_completed","http":400,"group":"pdf-convert","when":{"pt-br":"Download solicitado antes de a conversão terminar.","en":"Download requested before the conversion finished.","es":"Descarga solicitada antes de que termine la conversión.","fr":"Téléchargement demandé avant la fin de la conversion."}},{"code":"pdf.no_file","http":400,"group":"pdf-convert","when":{"pt-br":"Arquivo não disponível (expirado ou não gerado).","en":"File not available (expired or not generated).","es":"Archivo no disponible (expirado o no generado).","fr":"Fichier non disponible (expiré ou non généré)."}},{"code":"pdf.download_failed","http":400,"group":"pdf-convert","when":{"pt-br":"Falha ao ler ou descomprimir o arquivo.","en":"Failed to read or decompress the file.","es":"Error al leer o descomprimir el archivo.","fr":"Échec de la lecture ou de la décompression du fichier."}},{"code":"pdf.decrypt_failed","http":400,"group":"pdf-convert","when":{"pt-br":"Falha ao preparar o arquivo para download.","en":"Failed to prepare the file for download.","es":"Error al preparar el archivo para la descarga.","fr":"Échec de la préparation du fichier pour le téléchargement."}},{"code":"pdf.render_failed","http":502,"group":"pdf-convert","when":{"pt-br":"Falha no render.","en":"Render failed.","es":"Error en el render.","fr":"Échec du rendu."}},{"code":"pdf.storage_failed","http":502,"group":"pdf-convert","when":{"pt-br":"Falha ao armazenar o arquivo.","en":"Failed to store the file.","es":"Error al almacenar el archivo.","fr":"Échec du stockage du fichier."}},{"code":"pdf.enqueue_failed","http":502,"group":"pdf-convert","when":{"pt-br":"Falha ao enfileirar o job.","en":"Failed to enqueue the job.","es":"Error al encolar el job.","fr":"Échec de la mise en file d'attente du job."}},{"code":"pdf.render_timeout","http":504,"group":"pdf-convert","when":{"pt-br":"Tempo esgotado no render.","en":"Render timed out.","es":"Tiempo de render agotado.","fr":"Délai de rendu dépassé."}},{"code":"pdf.internal","http":500,"group":"pdf-convert","when":{"pt-br":"Erro interno ao processar a conversão.","en":"Internal error while processing the conversion.","es":"Error interno al procesar la conversión.","fr":"Erreur interne lors du traitement de la conversion."}},{"code":"pdf.template_failed","http":null,"group":"pdf-convert","when":{"pt-br":"Falha ao aplicar os dados do item no template (batch).","en":"Failed to apply the item's data to the template (batch).","es":"Error al aplicar los datos del elemento en la plantilla (batch).","fr":"Échec de l'application des données de l'élément au template (batch)."}},{"code":"tools.safety.invalid_url","http":400,"group":"verificacao-de-url","when":{"pt-br":"Parâmetro `url` ausente ou inválido.","en":"Missing or invalid `url` parameter.","es":"Parámetro `url` ausente o inválido.","fr":"Paramètre `url` manquant ou invalide."}},{"code":"tools.safety.domain_required","http":400,"group":"verificacao-de-url","when":{"pt-br":"Parâmetro `domain` ausente.","en":"Missing `domain` parameter.","es":"Falta el parámetro `domain`.","fr":"Paramètre `domain` manquant."}},{"code":"tools.safety.favicon_not_found","http":404,"group":"verificacao-de-url","when":{"pt-br":"Favicon não encontrado para o domínio.","en":"Favicon not found for the domain.","es":"No se encontró favicon para el dominio.","fr":"Favicon introuvable pour le domaine."}},{"code":"tools.ci.log_required","http":400,"group":"ci-error-extract","when":{"pt-br":"Campo `log` ausente ou vazio.","en":"Missing or empty `log` field.","es":"Campo `log` ausente o vacío.","fr":"Champ `log` absent ou vide."}},{"code":"tools.ci.log_too_large","http":413,"group":"ci-error-extract","when":{"pt-br":"Log acima do tamanho máximo aceito.","en":"Log over the maximum accepted size.","es":"Log por encima del tamaño máximo aceptado.","fr":"Log au-delà de la taille maximale acceptée."}},{"code":"tools.web.invalid_url","http":400,"group":"web-to-markdown","when":{"pt-br":"Campo `url` ausente ou inválido (só `http` e `https`).","en":"Missing or invalid `url` field (`http` and `https` only).","es":"Campo `url` ausente o inválido (solo `http` y `https`).","fr":"Champ `url` absent ou invalide (`http` et `https` uniquement)."}},{"code":"tools.web.blocked_url","http":400,"group":"web-to-markdown","when":{"pt-br":"URL recusada: aponta para um endereço interno ou privado.","en":"URL refused: it points to an internal or private address.","es":"URL rechazada: apunta a una dirección interna o privada.","fr":"URL refusée : elle pointe vers une adresse interne ou privée."}},{"code":"tools.web.fetch_failed","http":502,"group":"web-to-markdown","when":{"pt-br":"Não foi possível baixar a página.","en":"The page could not be downloaded.","es":"No se pudo descargar la página.","fr":"Impossible de télécharger la page."}},{"code":"tools.web.not_html","http":422,"group":"web-to-markdown","when":{"pt-br":"O endereço não devolveu uma página HTML.","en":"The address did not return an HTML page.","es":"La dirección no devolvió una página HTML.","fr":"L'adresse n'a pas renvoyé de page HTML."}},{"code":"tools.web.too_large","http":413,"group":"web-to-markdown","when":{"pt-br":"Página acima do tamanho máximo aceito.","en":"Page over the maximum accepted size.","es":"Página por encima del tamaño máximo aceptado.","fr":"Page au-delà de la taille maximale acceptée."}},{"code":"tools.web.no_content","http":422,"group":"web-to-markdown","when":{"pt-br":"Nenhum conteúdo de texto encontrado na página (por exemplo, página que depende de JavaScript).","en":"No text content found on the page (for example, a page that depends on JavaScript).","es":"No se encontró contenido de texto en la página (por ejemplo, una página que depende de JavaScript).","fr":"Aucun contenu texte trouvé sur la page (par exemple, une page qui dépend de JavaScript)."}},{"code":"notify.invalid_channel","http":400,"group":"notify","when":{"pt-br":"Canal inválido: tipo desconhecido ou campos que não combinam com o tipo (URL do webhook, servidor e tópico do ntfy).","en":"Invalid channel: unknown kind or fields that don't match the kind (webhook URL, ntfy server and topic).","es":"Canal inválido: tipo desconocido o campos que no combinan con el tipo (URL del webhook, servidor y tópico de ntfy).","fr":"Canal invalide : type inconnu ou champs qui ne correspondent pas au type (URL du webhook, serveur et topic ntfy)."}},{"code":"notify.channel_not_found","http":404,"group":"notify","when":{"pt-br":"Canal inexistente ou de outra conta.","en":"Channel doesn't exist or belongs to another account.","es":"Canal inexistente o de otra cuenta.","fr":"Canal inexistant ou d'un autre compte."}},{"code":"notify.channel_name_taken","http":409,"group":"notify","when":{"pt-br":"Já existe um canal com esse nome na conta.","en":"A channel with this name already exists in the account.","es":"Ya existe un canal con ese nombre en la cuenta.","fr":"Un canal portant ce nom existe déjà dans le compte."}},{"code":"notify.channel_disabled","http":409,"group":"notify","when":{"pt-br":"O canal escolhido está desativado.","en":"The chosen channel is disabled.","es":"El canal elegido está desactivado.","fr":"Le canal choisi est désactivé."}},{"code":"notify.rate_limited","http":429,"group":"notify","when":{"pt-br":"Limite de notificações por período excedido; tente de novo mais tarde.","en":"Notification limit per period exceeded; try again later.","es":"Límite de notificaciones por período excedido; inténtalo de nuevo más tarde.","fr":"Limite de notifications par période dépassée ; réessayez plus tard."}},{"code":"notify.delivery_failed","http":502,"group":"notify","when":{"pt-br":"Não foi possível entregar a notificação no canal (nada é cobrado).","en":"The notification could not be delivered on the channel (nothing is billed).","es":"No fue posible entregar la notificación en el canal (no se cobra nada).","fr":"Impossible de livrer la notification sur le canal (rien n'est facturé)."}},{"code":"notify.unavailable","http":503,"group":"notify","when":{"pt-br":"O envio de notificações está indisponível no momento; tente de novo em alguns segundos.","en":"Sending notifications is unavailable right now; try again in a few seconds.","es":"El envío de notificaciones no está disponible en este momento; inténtalo de nuevo en unos segundos.","fr":"L'envoi de notifications est indisponible pour le moment ; réessayez dans quelques secondes."}},{"code":"rag.disabled","http":403,"group":"base-de-conhecimento","when":{"pt-br":"Base de conhecimento ainda não liberada para a conta.","en":"Knowledge base not yet enabled for the account.","es":"Base de conocimiento aún no habilitada para la cuenta.","fr":"Base de connaissances pas encore activée pour le compte."}},{"code":"rag.document_too_large","http":413,"group":"base-de-conhecimento","when":{"pt-br":"Conteúdo acima de 5 MB.","en":"Content over 5 MB.","es":"Contenido mayor de 5 MB.","fr":"Contenu supérieur à 5 MB."}},{"code":"rag.chunk_quota_exceeded","http":403,"group":"base-de-conhecimento","when":{"pt-br":"A conta passaria de 200.000 trechos indexados.","en":"The account would exceed 200,000 indexed chunks.","es":"La cuenta superaría los 200.000 fragmentos indexados.","fr":"Le compte dépasserait 200 000 fragments indexés."}},{"code":"rag.collection_limit_reached","http":403,"group":"base-de-conhecimento","when":{"pt-br":"Limite de 50 bases por conta.","en":"Limit of 50 knowledge bases per account.","es":"Límite de 50 bases por cuenta.","fr":"Limite de 50 bases par compte."}},{"code":"rag.too_many_pending_documents","http":429,"group":"base-de-conhecimento","when":{"pt-br":"Já há 20 documentos em processamento; aguarde alguns terminarem.","en":"There are already 20 documents being processed; wait for some to finish.","es":"Ya hay 20 documentos en procesamiento; espera a que terminen algunos.","fr":"Il y a déjà 20 documents en cours de traitement ; attendez que certains se terminent."}},{"code":"rag.capacity_exhausted","http":429,"group":"base-de-conhecimento","when":{"pt-br":"Serviço de busca sem capacidade no momento; tente de novo em alguns segundos.","en":"Search service is at capacity right now; try again in a few seconds.","es":"El servicio de búsqueda no tiene capacidad en este momento; inténtalo de nuevo en unos segundos.","fr":"Service de recherche saturé pour le moment ; réessayez dans quelques secondes."}},{"code":"rag.embedding_failed","http":502,"group":"base-de-conhecimento","when":{"pt-br":"Falha no serviço de embeddings.","en":"Embeddings service failure.","es":"Fallo en el servicio de embeddings.","fr":"Échec du service d'embeddings."}},{"code":"rag.feedback_rate_limited","http":429,"group":"base-de-conhecimento","when":{"pt-br":"Mais de 30 votos de feedback por minuto.","en":"More than 30 feedback votes per minute.","es":"Más de 30 votos de feedback por minuto.","fr":"Plus de 30 votes de feedback par minute."}},{"code":"rag.document_not_failed","http":409,"group":"base-de-conhecimento","when":{"pt-br":"Só documentos com status `failed` podem ser reprocessados.","en":"Only documents with status `failed` can be retried.","es":"Solo los documentos con estado `failed` se pueden reprocesar.","fr":"Seuls les documents au statut `failed` peuvent être relancés."}},{"code":"rag.collection_create_restricted","http":403,"group":"base-de-conhecimento","when":{"pt-br":"A conexão MCP está limitada a bases específicas e não pode criar bases novas.","en":"The MCP connection is limited to specific knowledge bases and cannot create new ones.","es":"La conexión MCP está limitada a bases específicas y no puede crear bases nuevas.","fr":"La connexion MCP est limitée à des bases précises et ne peut pas en créer de nouvelles."}},{"code":"rag.model_switch_in_progress","http":409,"group":"base-de-conhecimento","when":{"pt-br":"A base está trocando de modelo de busca; envie documentos quando a troca terminar ou cancele-a.","en":"The knowledge base is switching its search model; upload documents when the switch finishes, or cancel it.","es":"La base está cambiando de modelo de búsqueda; envía documentos cuando termine el cambio o cancélalo.","fr":"La base change de modèle de recherche ; envoyez des documents une fois le changement terminé, ou annulez-le."}},{"code":"rag.service_unavailable","http":503,"group":"base-de-conhecimento","when":{"pt-br":"Serviço de rerank (inclusive o Laya), conversão de arquivo, analytics ou chaves públicas indisponível no momento.","en":"Rerank service (including Laya), file conversion, analytics or public keys currently unavailable.","es":"Servicio de rerank (incluido Laya), conversión de archivo, analytics o claves públicas no disponible por el momento.","fr":"Service de rerank (y compris Laya), conversion de fichier, analytics ou clés publiques indisponible pour le moment."}},{"code":"rag.decrypt_failed","http":500,"group":"base-de-conhecimento","when":{"pt-br":"Falha ao ler o conteúdo guardado. Informe o `trace_id` ao suporte.","en":"Failed to read the stored content. Send the `trace_id` to support.","es":"Fallo al leer el contenido guardado. Indica el `trace_id` al soporte.","fr":"Échec de la lecture du contenu stocké. Communiquez le `trace_id` au support."}},{"code":"embedding_failed","http":null,"group":"base-de-conhecimento","when":{"pt-br":"A indexação do documento falhou; use o reprocessamento.","en":"Document indexing failed; use retry.","es":"La indexación del documento falló; usa el reprocesamiento.","fr":"L'indexation du document a échoué ; relancez-le."}},{"code":"insufficient_credits","http":null,"group":"base-de-conhecimento","when":{"pt-br":"Saldo ou limite diário insuficiente para cobrar a ingestão ao terminar.","en":"Insufficient balance or daily limit to charge the ingestion when it finished.","es":"Saldo o límite diario insuficiente para cobrar la ingesta al terminar.","fr":"Solde ou limite quotidienne insuffisant pour facturer l'ingestion à la fin."}},{"code":"empty_document","http":null,"group":"base-de-conhecimento","when":{"pt-br":"O documento não tem texto indexável.","en":"The document has no indexable text.","es":"El documento no tiene texto indexable.","fr":"Le document n'a pas de texte indexable."}},{"code":"chunk_quota_exceeded","http":null,"group":"base-de-conhecimento","when":{"pt-br":"A ingestão passaria do limite de trechos da conta.","en":"The ingestion would exceed the account's chunk limit.","es":"La ingesta superaría el límite de fragmentos de la cuenta.","fr":"L'ingestion dépasserait la limite de fragments du compte."}},{"code":"persist_failed","http":null,"group":"base-de-conhecimento","when":{"pt-br":"Falha ao gravar o resultado; a cobrança é estornada.","en":"Failed to save the result; the charge is refunded.","es":"Fallo al guardar el resultado; el cobro se reembolsa.","fr":"Échec de l'enregistrement du résultat ; le débit est remboursé."}},{"code":"enqueue_failed","http":null,"group":"base-de-conhecimento","when":{"pt-br":"Falha ao enfileirar a ingestão.","en":"Failed to enqueue the ingestion.","es":"Fallo al encolar la ingesta.","fr":"Échec de la mise en file d'attente de l'ingestion."}},{"code":"webhook.relay_disabled","http":403,"group":"webhook-relay","when":{"pt-br":"Webhook Relay ainda não liberado para a conta.","en":"Webhook Relay not yet enabled for the account.","es":"Webhook Relay aún no habilitado para la cuenta.","fr":"Webhook Relay pas encore activé pour le compte."}},{"code":"webhook.limit_reached","http":403,"group":"webhook-relay","when":{"pt-br":"Limite da conta atingido: projetos, endpoints, tipos de evento, fontes ou rotas por fonte. Os limites valem para a conta inteira, somando todos os projetos; ao duplicar um projeto, nada é criado.","en":"Account limit reached: projects, endpoints, event types, sources or routes per source. Limits apply to the whole account, across every project; when duplicating a project, nothing is created.","es":"Límite de la cuenta alcanzado: proyectos, endpoints, tipos de evento, fuentes o rutas por fuente. Los límites valen para toda la cuenta, sumando todos los proyectos; al duplicar un proyecto, no se crea nada.","fr":"Limite du compte atteinte : projets, endpoints, types d’événement, sources ou routes par source. Les limites s’appliquent au compte entier, tous projets confondus ; lors de la duplication d’un projet, rien n’est créé."}},{"code":"webhook.payload_invalid","http":422,"group":"webhook-relay","when":{"pt-br":"`payload` não é JSON válido ou não segue o JSON Schema do tipo de evento. Nada é cobrado.","en":"`payload` is not valid JSON or does not match the event type's JSON Schema. Nothing is charged.","es":"`payload` no es JSON válido o no cumple el JSON Schema del tipo de evento. No se cobra nada.","fr":"`payload` n’est pas un JSON valide ou ne respecte pas le JSON Schema du type d’événement. Rien n’est facturé."}},{"code":"webhook.invalid_url","http":400,"group":"webhook-relay","when":{"pt-br":"URL de destino inválida ou restrita: só URLs públicas `http`/`https` (também vale para a `webhook_url` do PDF).","en":"Invalid or restricted destination URL: public `http`/`https` URLs only (also applies to the PDF `webhook_url`).","es":"URL de destino inválida o restringida: solo URLs públicas `http`/`https` (también vale para la `webhook_url` del PDF).","fr":"URL de destination invalide ou restreinte : URLs publiques `http`/`https` uniquement (vaut aussi pour la `webhook_url` du PDF)."}},{"code":"webhook.invalid_event","http":400,"group":"webhook-relay","when":{"pt-br":"Evento desconhecido na API legada de webhooks da conta (`/v1/account/webhooks`, obsoleta). No Webhook Relay qualquer nome de evento válido é aceito.","en":"Unknown event in the legacy account webhooks API (`/v1/account/webhooks`, deprecated). Webhook Relay accepts any valid event name.","es":"Evento desconocido en la API heredada de webhooks de la cuenta (`/v1/account/webhooks`, obsoleta). En Webhook Relay se acepta cualquier nombre de evento válido.","fr":"Événement inconnu dans l’API héritée des webhooks du compte (`/v1/account/webhooks`, obsolète). Webhook Relay accepte tout nom d’événement valide."}},{"code":"realtime.disabled","http":403,"group":"realtime","when":{"pt-br":"Realtime ainda não liberado para a conta (na conexão WebSocket, close `4003`).","en":"Realtime not yet enabled for the account (on the WebSocket connection, close `4003`).","es":"Realtime aún no habilitado para la cuenta (en la conexión WebSocket, close `4003`).","fr":"Realtime pas encore activé pour le compte (sur la connexion WebSocket, close `4003`)."}},{"code":"realtime.token_invalid","http":401,"group":"realtime","when":{"pt-br":"Token de conexão inválido ou expirado; na conexão WebSocket, close `4001`. Emita um token novo.","en":"Invalid or expired connection token; on the WebSocket connection, close `4001`. Issue a new token.","es":"Token de conexión inválido o expirado; en la conexión WebSocket, close `4001`. Emite un token nuevo.","fr":"Token de connexion invalide ou expiré ; sur la connexion WebSocket, close `4001`. Émettez un nouveau token."}},{"code":"realtime.capability_denied","http":403,"group":"realtime","when":{"pt-br":"O token não permite a operação (`subscribe`, `publish` ou `presence`) no canal; no WebSocket, frame `error` com `channel` e a conexão continua aberta.","en":"The token does not allow the operation (`subscribe`, `publish` or `presence`) on the channel; on the WebSocket, an `error` frame with `channel` and the connection stays open.","es":"El token no permite la operación (`subscribe`, `publish` o `presence`) en el canal; en el WebSocket, frame `error` con `channel` y la conexión sigue abierta.","fr":"Le token n'autorise pas l'opération (`subscribe`, `publish` ou `presence`) sur le canal ; sur le WebSocket, frame `error` avec `channel` et la connexion reste ouverte."}},{"code":"realtime.channel_invalid","http":400,"group":"realtime","when":{"pt-br":"Nome de canal ou padrão de canal inválido: vazio, com mais de 128 caracteres ou com caracteres fora de `A-Z a-z 0-9 : _ - .`.","en":"Invalid channel name or channel pattern: empty, longer than 128 characters or with characters outside `A-Z a-z 0-9 : _ - .`.","es":"Nombre o patrón de canal inválido: vacío, con más de 128 caracteres o con caracteres fuera de `A-Z a-z 0-9 : _ - .`.","fr":"Nom ou motif de canal invalide : vide, de plus de 128 caractères ou avec des caractères hors de `A-Z a-z 0-9 : _ - .`."}},{"code":"realtime.message_too_large","http":413,"group":"realtime","when":{"pt-br":"Mensagem (`data`) acima de 64 KB.","en":"Message (`data`) above 64 KB.","es":"Mensaje (`data`) por encima de 64 KB.","fr":"Message (`data`) de plus de 64 Ko."}},{"code":"realtime.rate_limited","http":429,"group":"realtime","when":{"pt-br":"A conta passou do limite de publicações por segundo; a mensagem é descartada e, no WebSocket, a conexão continua aberta.","en":"The account exceeded the publishes-per-second limit; the message is dropped and, on the WebSocket, the connection stays open.","es":"La cuenta superó el límite de publicaciones por segundo; el mensaje se descarta y, en el WebSocket, la conexión sigue abierta.","fr":"Le compte a dépassé la limite de publications par seconde ; le message est abandonné et, sur le WebSocket, la connexion reste ouverte."}},{"code":"realtime.connection_limit","http":429,"group":"realtime","when":{"pt-br":"A conta atingiu o limite de conexões simultâneas (WebSocket: close `4029`). Reconecte com backoff.","en":"The account reached its simultaneous connection limit (WebSocket: close `4029`). Reconnect with backoff.","es":"La cuenta alcanzó el límite de conexiones simultáneas (WebSocket: close `4029`). Reconecta con backoff.","fr":"Le compte a atteint sa limite de connexions simultanées (WebSocket : close `4029`). Reconnectez-vous avec un backoff."}},{"code":"realtime.presence_limit","http":429,"group":"realtime","when":{"pt-br":"O canal já tem o máximo de membros na presença (200).","en":"The channel already has the maximum number of presence members (200).","es":"El canal ya tiene el máximo de miembros en la presencia (200).","fr":"Le canal a déjà le nombre maximal de membres dans la présence (200)."}},{"code":"realtime.insufficient_credits","http":402,"group":"realtime","when":{"pt-br":"Saldo insuficiente para publicar uma mensagem além da franquia mensal.","en":"Insufficient balance to publish a message beyond the monthly allowance.","es":"Saldo insuficiente para publicar un mensaje fuera de la franquicia mensual.","fr":"Solde insuffisant pour publier un message au-delà du quota mensuel."}},{"code":"realtime.session_required","http":403,"group":"realtime","when":{"pt-br":"A rota só aceita a sessão do dashboard (`POST /inspector-token`), não uma chave de API.","en":"The route only accepts the dashboard session (`POST /inspector-token`), not an API key.","es":"La ruta solo acepta la sesión del dashboard (`POST /inspector-token`), no una clave de API.","fr":"La route n'accepte que la session du dashboard (`POST /inspector-token`), pas une clé d'API."}},{"code":"audio.disabled","http":403,"group":"audio","when":{"pt-br":"Áudio ainda não liberado para a conta.","en":"Audio not yet enabled for the account.","es":"Audio aún no habilitado para la cuenta.","fr":"Audio pas encore activé pour le compte."}},{"code":"audio.input_invalid","http":400,"group":"audio","when":{"pt-br":"Corpo inválido, ou áudio informado de forma errada (mande exatamente um entre `audio_url`, `audio_base64` e `file_id`).","en":"Invalid body, or the audio was given incorrectly (send exactly one of `audio_url`, `audio_base64` and `file_id`).","es":"Cuerpo inválido, o audio informado de forma incorrecta (envía exactamente uno entre `audio_url`, `audio_base64` y `file_id`).","fr":"Corps invalide, ou audio mal renseigné (envoyez exactement un champ parmi `audio_url`, `audio_base64` et `file_id`)."}},{"code":"audio.input_blocked","http":400,"group":"audio","when":{"pt-br":"A URL do áudio aponta para um endereço não permitido (rede interna ou privada), ou a descrição de `audio_generate` pede imitação de artistas, vozes ou obras. Nada é cobrado.","en":"The audio URL points to a disallowed address (internal or private network), or the `audio_generate` description asks to imitate artists, voices or works. Nothing is charged.","es":"La URL del audio apunta a una dirección no permitida (red interna o privada), o la descripción de `audio_generate` pide imitar artistas, voces u obras. No se cobra nada.","fr":"L'URL de l'audio pointe vers une adresse interdite (réseau interne ou privé), ou la description d'`audio_generate` demande d'imiter des artistes, des voix ou des œuvres. Rien n'est facturé."}},{"code":"audio.too_long","http":400,"group":"audio","when":{"pt-br":"Áudio acima de 60 minutos, ou texto acima de 5.000 caracteres: recusado antes de cobrar.","en":"Audio over 60 minutes, or text over 5,000 characters: refused before charging.","es":"Audio de más de 60 minutos, o texto de más de 5.000 caracteres: rechazado antes de cobrar.","fr":"Audio de plus de 60 minutes, ou texte de plus de 5 000 caractères : refusé avant facturation."}},{"code":"audio.input_too_large","http":413,"group":"audio","when":{"pt-br":"Arquivo de áudio acima de 25 MB (ou 10 MB em `audio_base64`).","en":"Audio file over 25 MB (or 10 MB in `audio_base64`).","es":"Archivo de audio de más de 25 MB (o 10 MB en `audio_base64`).","fr":"Fichier audio de plus de 25 Mo (ou 10 Mo en `audio_base64`)."}},{"code":"audio.format_unsupported","http":415,"group":"audio","when":{"pt-br":"Formato de áudio não suportado; use mp3, wav, ogg, m4a, flac ou webm.","en":"Unsupported audio format; use mp3, wav, ogg, m4a, flac or webm.","es":"Formato de audio no soportado; usa mp3, wav, ogg, m4a, flac o webm.","fr":"Format audio non pris en charge ; utilisez mp3, wav, ogg, m4a, flac ou webm."}},{"code":"audio.input_fetch_failed","http":502,"group":"audio","when":{"pt-br":"Não foi possível baixar o áudio da `audio_url`.","en":"The audio could not be downloaded from `audio_url`.","es":"No fue posible descargar el audio de `audio_url`.","fr":"Impossible de télécharger l'audio depuis `audio_url`."}},{"code":"audio.provider_failed","http":502,"group":"audio","when":{"pt-br":"O motor de áudio falhou; a cobrança é devolvida.","en":"The audio engine failed; the charge is refunded.","es":"El motor de audio falló; el cobro se devuelve.","fr":"Le moteur audio a échoué ; la facturation est remboursée."}},{"code":"audio.input_timeout","http":504,"group":"audio","when":{"pt-br":"O download do áudio passou de 30 segundos.","en":"The audio download took longer than 30 seconds.","es":"La descarga del audio superó los 30 segundos.","fr":"Le téléchargement de l'audio a dépassé 30 secondes."}},{"code":"audio.price_above_max","http":409,"group":"audio","when":{"pt-br":"O preço passa de `max_credits`; nada é cobrado.","en":"The price exceeds `max_credits`; nothing is charged.","es":"El precio supera `max_credits`; no se cobra nada.","fr":"Le prix dépasse `max_credits` ; rien n'est facturé."}},{"code":"audio.pricing_unavailable","http":503,"group":"audio","when":{"pt-br":"Preço do serviço de áudio indisponível no momento; tente de novo.","en":"Audio service price unavailable at the moment; try again.","es":"Precio del servicio de audio no disponible por ahora; inténtalo de nuevo.","fr":"Prix du service audio indisponible pour le moment ; réessayez."}},{"code":"audio.not_found","http":404,"group":"audio","when":{"pt-br":"Job de áudio inexistente, de outra conta ou já expirado.","en":"Audio job not found, from another account or already expired.","es":"Job de audio inexistente, de otra cuenta o ya caducado.","fr":"Job audio introuvable, d'un autre compte ou déjà expiré."}},{"code":"audio.voice_unknown","http":400,"group":"audio","when":{"pt-br":"Voz inexistente ou apagada: use um id de `GET /audio/voices` ou uma voz clonada da conta que já esteja pronta.","en":"Unknown or deleted voice: use an id from `GET /audio/voices` or a cloned voice of the account that is already ready.","es":"Voz inexistente o borrada: usa un id de `GET /audio/voices` o una voz clonada de la cuenta que ya esté lista.","fr":"Voix inconnue ou supprimée : utilisez un id de `GET /audio/voices` ou une voix clonée du compte déjà prête."}},{"code":"audio.consent_required","http":400,"group":"audio","when":{"pt-br":"`voice_create` sem o consentimento aceito, ou com versão dos termos desatualizada (leia os termos com `dry_run`).","en":"`voice_create` without the consent accepted, or with an outdated terms version (read the terms with `dry_run`).","es":"`voice_create` sin el consentimiento aceptado, o con una versión de los términos desactualizada (lee los términos con `dry_run`).","fr":"`voice_create` sans le consentement accepté, ou avec une version des conditions périmée (lisez les conditions avec `dry_run`)."}},{"code":"audio.voice_sample_rejected","http":422,"group":"audio","when":{"pt-br":"Amostra recusada: sem a frase de consentimento lida em voz alta, com mais de uma voz ou fora de 10 a 60 segundos. O valor é devolvido.","en":"Sample rejected: without the consent phrase read aloud, with more than one voice or outside 10 to 60 seconds. The amount is refunded.","es":"Muestra rechazada: sin la frase de consentimiento leída en voz alta, con más de una voz o fuera de 10 a 60 segundos. El valor se devuelve.","fr":"Échantillon refusé : sans la phrase de consentement lue à voix haute, avec plus d'une voix ou hors de 10 à 60 secondes. Le montant est remboursé."}},{"code":"audio.voice_limit","http":409,"group":"audio","when":{"pt-br":"A conta já tem o máximo de 3 vozes clonadas; apague uma com `voice_delete`.","en":"The account already has the maximum of 3 cloned voices; delete one with `voice_delete`.","es":"La cuenta ya tiene el máximo de 3 voces clonadas; borra una con `voice_delete`.","fr":"Le compte a déjà le maximum de 3 voix clonées ; supprimez-en une avec `voice_delete`."}},{"code":"audio.voice_rate_limited","http":429,"group":"audio","when":{"pt-br":"Limite de criações de voz por dia (5) ou de usos por hora (60) atingido.","en":"Limit of voice creations per day (5) or uses per hour (60) reached.","es":"Se alcanzó el límite de creaciones de voz por día (5) o de usos por hora (60).","fr":"Limite de créations de voix par jour (5) ou d'usages par heure (60) atteinte."}},{"code":"video.spec_invalid","http":400,"group":"video","when":{"pt-br":"VideoSpec fora do schema ou dos limites (o erro traz o caminho do campo).","en":"VideoSpec outside the schema or the limits (the error carries the field path).","es":"VideoSpec fuera del schema o de los límites (el error trae la ruta del campo).","fr":"VideoSpec hors du schéma ou des limites (l'erreur indique le chemin du champ)."}},{"code":"video.asset_invalid","http":400,"group":"video","when":{"pt-br":"Um asset do vídeo não pôde ser usado.","en":"A video asset could not be used.","es":"Un asset del video no pudo usarse.","fr":"Un asset de la vidéo n'a pas pu être utilisé."}},{"code":"video.retention_invalid","http":400,"group":"video","when":{"pt-br":"Retenção do render inválida.","en":"Invalid render retention.","es":"Retención del render inválida.","fr":"Rétention du rendu invalide."}},{"code":"video.price_above_max","http":409,"group":"video","when":{"pt-br":"O preço do render passa de `max_credits`; nada é cobrado.","en":"The render price exceeds `max_credits`; nothing is charged.","es":"El precio del render supera `max_credits`; no se cobra nada.","fr":"Le prix du rendu dépasse `max_credits` ; rien n'est facturé."}},{"code":"video.too_many_in_flight","http":429,"group":"video","when":{"pt-br":"Renders demais em andamento na conta; espere algum terminar.","en":"Too many renders in progress on the account; wait for one to finish.","es":"Demasiados renders en curso en la cuenta; espera a que termine alguno.","fr":"Trop de rendus en cours sur le compte ; attendez qu'un d'eux se termine."}},{"code":"video.not_ready","http":409,"group":"video","when":{"pt-br":"O render ainda não terminou.","en":"The render has not finished yet.","es":"El render aún no terminó.","fr":"Le rendu n'est pas encore terminé."}},{"code":"video.not_cancelable","http":409,"group":"video","when":{"pt-br":"O render já terminou e não pode ser cancelado.","en":"The render has already finished and cannot be canceled.","es":"El render ya terminó y no se puede cancelar.","fr":"Le rendu est déjà terminé et ne peut pas être annulé."}},{"code":"video.not_found","http":404,"group":"video","when":{"pt-br":"Render inexistente ou de outra conta.","en":"Render not found or from another account.","es":"Render inexistente o de otra cuenta.","fr":"Rendu introuvable ou d'un autre compte."}},{"code":"video.unavailable","http":503,"group":"video","when":{"pt-br":"Renderização de vídeo indisponível no momento.","en":"Video rendering unavailable at the moment.","es":"Renderizado de video no disponible por ahora.","fr":"Rendu vidéo indisponible pour le moment."}},{"code":"video.enqueue_failed","http":500,"group":"video","when":{"pt-br":"Falha ao enfileirar o render.","en":"Failed to enqueue the render.","es":"Falló al encolar el render.","fr":"Échec de la mise en file du rendu."}},{"code":"video.internal","http":500,"group":"video","when":{"pt-br":"Erro interno ao processar o vídeo.","en":"Internal error while processing the video.","es":"Error interno al procesar el video.","fr":"Erreur interne lors du traitement de la vidéo."}},{"code":"video.preview_time_invalid","http":400,"group":"video","when":{"pt-br":"O instante `t` do quadro está fora da duração do vídeo.","en":"The frame time `t` is outside the video duration.","es":"El instante `t` del fotograma está fuera de la duración del video.","fr":"L'instant `t` de l'image est hors de la durée de la vidéo."}},{"code":"video.preview_busy","http":503,"group":"video","when":{"pt-br":"Há prévias demais em andamento; tente de novo em instantes.","en":"Too many previews in progress; try again shortly.","es":"Hay demasiadas vistas previas en curso; inténtalo de nuevo en un momento.","fr":"Trop d'aperçus en cours ; réessayez dans un instant."}},{"code":"video.preview_failed","http":422,"group":"video","when":{"pt-br":"Não foi possível renderizar o quadro de prévia.","en":"The preview frame could not be rendered.","es":"No fue posible renderizar el fotograma de vista previa.","fr":"Impossible de rendre l'image d'aperçu."}}],"scopes":[{"id":"tools:manage","allows":{"pt-br":"Acesso total a todas as ferramentas (inclui leitura e escrita).","en":"Full access to all tools (includes read and write).","es":"Acceso total a todas las herramientas (incluye lectura y escritura).","fr":"Accès complet à tous les outils (lecture et écriture incluses)."}},{"id":"tools:read","allows":{"pt-br":"Leitura genérica de QR Dinâmico, Verificação de URL, Base de conhecimento, Webhook Relay, Extrator de erro de CI e Página web para Markdown. Não cobre PDF.","en":"Generic read access to Dynamic QR, URL Safety Check, Knowledge base, Webhook Relay, CI Error Extractor and Web Page to Markdown. Does not cover PDF.","es":"Lectura genérica de QR dinámico, Verificación de URL, Base de conocimiento, Webhook Relay, Extractor de errores de CI y Página web a Markdown. No cubre PDF.","fr":"Lecture générique de QR dynamique, Vérification d'URL, Base de connaissances, Webhook Relay, Extracteur d'erreur de CI et Page web vers Markdown. Ne couvre pas PDF."}},{"id":"tools:qr:dynamic:read","allows":{"pt-br":"Listar, consultar e ver analytics de QR Dinâmico.","en":"List, retrieve and view analytics for Dynamic QR.","es":"Listar, consultar y ver analíticas de QR dinámico.","fr":"Lister, consulter et voir les analytics de QR dynamique."}},{"id":"tools:qr:dynamic:manage","allows":{"pt-br":"Criar, editar, ativar/desativar e excluir QR Dinâmico (também permite leitura).","en":"Create, edit, activate/deactivate and delete Dynamic QR (also allows read).","es":"Crear, editar, activar/desactivar y eliminar QR dinámico (también permite lectura).","fr":"Créer, modifier, activer/désactiver et supprimer des QR dynamiques (permet aussi la lecture)."}},{"id":"tools:qr:domains:manage","allows":{"pt-br":"Cadastrar, editar, verificar e remover os domínios próprios do QR Dinâmico (domínios ativos são cobrados).","en":"Register, edit, verify and remove Dynamic QR custom domains (active domains are charged).","es":"Registrar, editar, verificar y eliminar los dominios propios del QR Dinámico (los dominios activos se cobran).","fr":"Enregistrer, modifier, vérifier et supprimer les domaines personnalisés du QR Dynamique (les domaines actifs sont facturés)."}},{"id":"tools:pdf:read","allows":{"pt-br":"Listar, consultar status e baixar conversões de PDF.","en":"List, check status and download PDF conversions.","es":"Listar, consultar el estado y descargar conversiones de PDF.","fr":"Lister, consulter le statut et télécharger les conversions PDF."}},{"id":"tools:pdf:manage","allows":{"pt-br":"Converter, criar batch e reprocessar falhas de PDF.","en":"Convert, create batches and retry failed PDF conversions.","es":"Convertir, crear batches y reprocesar fallos de PDF.","fr":"Convertir, créer des batches et relancer les échecs PDF."}},{"id":"tools:safety:read","allows":{"pt-br":"Consultar a Verificação de URL e o proxy de favicon.","en":"Query URL Safety Check and the favicon proxy.","es":"Consultar la Verificación de URL y el proxy de favicon.","fr":"Consulter la Vérification d'URL et le proxy de favicon."}},{"id":"tools:ci:read","allows":{"pt-br":"Extrair as linhas de erro de um log de CI.","en":"Extract the error lines from a CI log.","es":"Extraer las líneas de error de un log de CI.","fr":"Extraire les lignes d'erreur d'un log de CI."}},{"id":"tools:web:read","allows":{"pt-br":"Converter páginas web em Markdown (para gravar o resultado numa base, exige também `tools:rag:manage`).","en":"Convert web pages to Markdown (saving the result to a base also requires `tools:rag:manage`).","es":"Convertir páginas web a Markdown (para guardar el resultado en una base, exige también `tools:rag:manage`).","fr":"Convertir des pages web en Markdown (pour enregistrer le résultat dans une base, `tools:rag:manage` est aussi requis)."}},{"id":"tools:rag:read","allows":{"pt-br":"Ler bases de conhecimento: listar bases, documentos, seções e feedback.","en":"Read knowledge bases: list knowledge bases, documents, sections and feedback.","es":"Leer bases de conocimiento: listar bases, documentos, secciones y feedback.","fr":"Lire les bases de connaissances : lister les bases, documents, sections et feedback."}},{"id":"tools:rag:query","allows":{"pt-br":"Buscar nas bases de conhecimento: fazer buscas, rerank e registrar feedback de relevância.","en":"Query knowledge bases: run searches, rerank and record relevance feedback.","es":"Buscar en las bases de conocimiento: hacer búsquedas, rerank y registrar feedback de relevancia.","fr":"Interroger les bases de connaissances : lancer des recherches, du rerank et enregistrer du feedback de pertinence."}},{"id":"tools:rag:manage","allows":{"pt-br":"Gerenciar bases de conhecimento: criar, alterar e excluir bases e documentos (também permite ler e buscar).","en":"Manage knowledge bases: create, update and delete knowledge bases and documents (also allows reading and querying).","es":"Gestionar bases de conocimiento: crear, modificar y eliminar bases y documentos (también permite leer y buscar).","fr":"Gérer les bases de connaissances : créer, modifier et supprimer des bases et des documents (permet aussi la lecture et la recherche)."}},{"id":"tools:webhooks:read","allows":{"pt-br":"Ler a configuração do Webhook Relay (projetos, endpoints, tipos de evento, fontes, rotas e canvas), o uso do mês e o log de mensagens.","en":"Read the Webhook Relay configuration (projects, endpoints, event types, sources, routes and canvas), the month's usage and the message log.","es":"Leer la configuración de Webhook Relay (proyectos, endpoints, tipos de evento, fuentes, rutas y canvas), el uso del mes y el registro de mensajes.","fr":"Lire la configuration de Webhook Relay (projets, endpoints, types d’événement, sources, routes et canvas), l’usage du mois et le journal des messages."}},{"id":"tools:webhooks:send","allows":{"pt-br":"Enviar mensagens pelo Webhook Relay (envio único e em lote). Não permite ler nem alterar a configuração.","en":"Send messages through Webhook Relay (single and batch). Cannot read or change the configuration.","es":"Enviar mensajes por Webhook Relay (individual y en lote). No permite leer ni modificar la configuración.","fr":"Envoyer des messages via Webhook Relay (unitaire et par lot). Ne permet ni de lire ni de modifier la configuration."}},{"id":"tools:webhooks:manage","allows":{"pt-br":"Gerenciar o Webhook Relay: projetos, endpoints, secrets, tipos de evento, fontes, rotas, layout do canvas, eventos de teste, reenvios e recuperação (também permite ler e enviar).","en":"Manage Webhook Relay: projects, endpoints, secrets, event types, sources, routes, canvas layout, test events, retries and recovery (also allows reading and sending).","es":"Gestionar Webhook Relay: proyectos, endpoints, secrets, tipos de evento, fuentes, rutas, layout del canvas, eventos de prueba, reenvíos y recuperación (también permite leer y enviar).","fr":"Gérer Webhook Relay : projets, endpoints, secrets, types d’événement, sources, routes, disposition du canvas, événements de test, renvois et récupération (permet aussi de lire et d’envoyer)."}},{"id":"tools:realtime:publish","allows":{"pt-br":"Publicar mensagens em canais do Realtime pela API.","en":"Publish messages to Realtime channels through the API.","es":"Publicar mensajes en canales de Realtime por la API.","fr":"Publier des messages sur les canaux Realtime via l'API."}},{"id":"tools:realtime:subscribe","allows":{"pt-br":"Ler o histórico e a presença dos canais do Realtime.","en":"Read Realtime channel history and presence.","es":"Leer el historial y la presencia de los canales de Realtime.","fr":"Lire l'historique et la présence des canaux Realtime."}},{"id":"tools:realtime:manage","allows":{"pt-br":"Gerenciar o Realtime: emitir tokens, consultar canais e estatísticas, configurar o webhook de canais, publicar e ler o histórico e a presença.","en":"Manage Realtime: issue tokens, query channels and statistics, configure the channel webhook, publish and read history and presence.","es":"Gestionar Realtime: emitir tokens, consultar canales y estadísticas, configurar el webhook de canales, publicar y leer el historial y la presencia.","fr":"Gérer Realtime : émettre des tokens, consulter les canaux et les statistiques, configurer le webhook de canaux, publier et lire l'historique et la présence."}},{"id":"tools:notify:send","allows":{"pt-br":"Enviar notificações pelo canal padrão ou por um canal escolhido.","en":"Send notifications through the default channel or a chosen channel.","es":"Enviar notificaciones por el canal predeterminado o por un canal elegido.","fr":"Envoyer des notifications par le canal par défaut ou par un canal choisi."}},{"id":"tools:notify:manage","allows":{"pt-br":"Gerenciar os canais de notificação (criar, alterar, testar e excluir) e ler o histórico de envios.","en":"Manage notification channels (create, update, test and delete) and read the delivery history.","es":"Gestionar los canales de notificación (crear, modificar, probar y eliminar) y leer el historial de envíos.","fr":"Gérer les canaux de notification (créer, modifier, tester et supprimer) et lire l'historique des envois."}},{"id":"tools:learnings:read","allows":{"pt-br":"Recuperar aprendizados salvos na Memória de aprendizados.","en":"Recall learnings saved in the Learnings memory.","es":"Recuperar aprendizajes guardados en la Memoria de aprendizajes.","fr":"Retrouver les apprentissages enregistrés dans la Mémoire d'apprentissages."}},{"id":"tools:learnings:write","allows":{"pt-br":"Salvar e esquecer aprendizados na Memória de aprendizados.","en":"Save and forget learnings in the Learnings memory.","es":"Guardar y olvidar aprendizajes en la Memoria de aprendizajes.","fr":"Enregistrer et oublier des apprentissages dans la Mémoire d'apprentissages."}},{"id":"tools:video:render","allows":{"pt-br":"Renderizar vídeos (cobrado por segundo renderizado) e validar o roteiro (`validate_video`, `preview_video_frame`).","en":"Render videos (charged per rendered second) and validate the script (`validate_video`, `preview_video_frame`).","es":"Renderizar videos (se cobra por segundo renderizado) y validar el guion (`validate_video`, `preview_video_frame`).","fr":"Rendre des vidéos (facturé à la seconde rendue) et valider le script (`validate_video`, `preview_video_frame`)."}},{"id":"tools:video:read","allows":{"pt-br":"Listar renders de vídeo, consultar o status e obter o link do MP4.","en":"List video renders, check the status and get the MP4 link.","es":"Listar renders de video, consultar el estado y obtener el enlace del MP4.","fr":"Lister les rendus vidéo, consulter le statut et obtenir le lien du MP4."}},{"id":"tools:audio:transcribe","allows":{"pt-br":"Transcrever áudio (cobrado por minuto), resumir e traduzir transcrições.","en":"Transcribe audio (charged per minute), summarize and translate transcripts.","es":"Transcribir audio (se cobra por minuto), resumir y traducir transcripciones.","fr":"Transcrire un audio (facturé à la minute), résumer et traduire des transcriptions."}},{"id":"tools:audio:read","allows":{"pt-br":"Listar os jobs de áudio, consultar o status e obter o link do resultado, além do catálogo de vozes.","en":"List audio jobs, check the status and get the result link, plus the voice catalog.","es":"Listar los jobs de audio, consultar el estado y obtener el enlace del resultado, además del catálogo de voces.","fr":"Lister les jobs audio, consulter le statut et obtenir le lien du résultat, ainsi que le catalogue de voix."}},{"id":"tools:audio:speak","allows":{"pt-br":"Gerar fala a partir de texto (por 1.000 caracteres), sons e músicas (por segundo) e falar uma gravação de novo com outra voz; no MCP, também criar e apagar vozes clonadas.","en":"Generate speech from text (per 1,000 characters), sounds and music (per second) and speak a recording again in another voice; in MCP, also create and delete cloned voices.","es":"Generar voz a partir de texto (por 1.000 caracteres), sonidos y música (por segundo) y volver a hablar una grabación con otra voz; en MCP, también crear y borrar voces clonadas.","fr":"Générer de la parole à partir d'un texte (par 1 000 caractères), des sons et de la musique (à la seconde) et faire reprononcer un enregistrement avec une autre voix ; en MCP, aussi créer et supprimer des voix clonées."}},{"id":"tools:audio:process","allows":{"pt-br":"Analisar e converter áudio (cobrado por chamada e por minuto).","en":"Analyze and convert audio (charged per call and per minute).","es":"Analizar y convertir audio (se cobra por llamada y por minuto).","fr":"Analyser et convertir un audio (facturé par appel et par minute)."}},{"id":"mcp:use","allows":{"pt-br":"Uso por agentes de IA: permite usar a chave no servidor MCP.","en":"Use by AI agents: allows the key to be used on the MCP server.","es":"Uso por agentes de IA: permite usar la clave en el servidor MCP.","fr":"Utilisation par des agents IA : permet d'utiliser la clé sur le serveur MCP."}},{"id":"account:read","allows":{"pt-br":"Ler saldo e limites: consultar o saldo de créditos e o limite diário de gasto.","en":"Read balance and limits: check the credit balance and the daily spend limit.","es":"Leer saldo y límites: consultar el saldo de créditos y el límite diario de gasto.","fr":"Lire le solde et les limites : consulter le solde de crédits et la limite de dépense quotidienne."}}],"limits":[{"name":"rateLimitPerMinute","value":{"pt-br":"60","en":"60","es":"60","fr":"60"}},{"name":"dailyQuota","value":{"pt-br":"2000","en":"2000","es":"2000","fr":"2000"}},{"name":"dailyQuotaPremium","value":{"pt-br":"4000","en":"4000","es":"4000","fr":"4000"}},{"name":"quotaReset","value":{"pt-br":"meia-noite UTC","en":"midnight UTC","es":"medianoche UTC","fr":"minuit UTC"}},{"name":"idempotencyTtl","value":{"pt-br":"24 horas","en":"24 hours","es":"24 horas","fr":"24 heures"}},{"name":"requestTimeout","value":{"pt-br":"30 segundos","en":"30 seconds","es":"30 segundos","fr":"30 secondes"}},{"name":"downloadTimeout","value":{"pt-br":"2 minutos","en":"2 minutes","es":"2 minutos","fr":"2 minutes"}},{"name":"pageSizeMax","value":{"pt-br":"100","en":"100","es":"100","fr":"100"}},{"name":"pdfHtmlMax","value":{"pt-br":"10 MB","en":"10 MB","es":"10 MB","fr":"10 MB"}},{"name":"pdfCssMax","value":{"pt-br":"1 MB","en":"1 MB","es":"1 MB","fr":"1 MB"}},{"name":"pdfBatchMax","value":{"pt-br":"500","en":"500","es":"500","fr":"500"}},{"name":"ragDocumentMax","value":{"pt-br":"5 MB","en":"5 MB","es":"5 MB","fr":"5 MB"}},{"name":"ragCollectionsMax","value":{"pt-br":"50","en":"50","es":"50","fr":"50"}},{"name":"ragChunksMax","value":{"pt-br":"200000","en":"200000","es":"200000","fr":"200000"}},{"name":"ragPendingMax","value":{"pt-br":"20","en":"20","es":"20","fr":"20"}},{"name":"ragQueryCharsMax","value":{"pt-br":"1000","en":"1000","es":"1000","fr":"1000"}},{"name":"ragTopKMax","value":{"pt-br":"20","en":"20","es":"20","fr":"20"}},{"name":"ragRerankDocumentsMax","value":{"pt-br":"100","en":"100","es":"100","fr":"100"}},{"name":"ragFeedbackPerMinute","value":{"pt-br":"30","en":"30","es":"30","fr":"30"}},{"name":"ragQueryCacheTtl","value":{"pt-br":"10 minutos","en":"10 minutes","es":"10 minutos","fr":"10 minutes"}},{"name":"audioMaxDuration","value":{"pt-br":"60 minutos","en":"60 minutes","es":"60 minutos","fr":"60 minutes"}},{"name":"audioInputMax","value":{"pt-br":"25 MB","en":"25 MB","es":"25 MB","fr":"25 Mo"}},{"name":"audioInlineMax","value":{"pt-br":"10 MB","en":"10 MB","es":"10 MB","fr":"10 MB"}},{"name":"audioFetchTimeout","value":{"pt-br":"30 segundos","en":"30 seconds","es":"30 segundos","fr":"30 secondes"}},{"name":"audioTtsCharsMax","value":{"pt-br":"5000","en":"5000","es":"5000","fr":"5000"}},{"name":"audioEffectMax","value":{"pt-br":"10 segundos","en":"10 seconds","es":"10 segundos","fr":"10 secondes"}},{"name":"audioMusicMax","value":{"pt-br":"45 segundos","en":"45 seconds","es":"45 segundos","fr":"45 secondes"}},{"name":"audioRetention","value":{"pt-br":"24 horas","en":"24 hours","es":"24 horas","fr":"24 heures"}},{"name":"audioVoiceSample","value":{"pt-br":"10 a 60 segundos","en":"10 to 60 seconds","es":"10 a 60 segundos","fr":"10 à 60 secondes"}},{"name":"audioVoiceSampleRetention","value":{"pt-br":"30 dias","en":"30 days","es":"30 días","fr":"30 jours"}},{"name":"audioVoicesMax","value":{"pt-br":"3","en":"3","es":"3","fr":"3"}},{"name":"audioVoiceCreatesPerDay","value":{"pt-br":"5","en":"5","es":"5","fr":"5"}},{"name":"audioVoiceUsesPerHour","value":{"pt-br":"60","en":"60","es":"60","fr":"60"}},{"name":"mcpResultMax","value":{"pt-br":"32 KB","en":"32 KB","es":"32 KB","fr":"32 KB"}},{"name":"mcpRequestMax","value":{"pt-br":"1 MB","en":"1 MB","es":"1 MB","fr":"1 MB"}},{"name":"mcpIdempotencyWindow","value":{"pt-br":"60 segundos","en":"60 seconds","es":"60 segundos","fr":"60 secondes"}},{"name":"mcpToolsSearchPerMinute","value":{"pt-br":"60","en":"60","es":"60","fr":"60"}},{"name":"mcpSuggestedDailyLimit","value":{"pt-br":"10 créditos","en":"10 credits","es":"10 créditos","fr":"10 crédits"}},{"name":"mcpDocsRateLimitPerMinute","value":{"pt-br":"60","en":"60","es":"60","fr":"60"}},{"name":"webhookFreeDeliveries","value":{"pt-br":"1000","en":"1000","es":"1000","fr":"1000"}},{"name":"webhookPayloadMax","value":{"pt-br":"256 KB","en":"256 KB","es":"256 KB","fr":"256 Ko"}},{"name":"webhookEndpointsMax","value":{"pt-br":"200","en":"200","es":"200","fr":"200"}},{"name":"webhookEventTypesMax","value":{"pt-br":"100","en":"100","es":"100","fr":"100"}},{"name":"webhookBatchMax","value":{"pt-br":"100","en":"100","es":"100","fr":"100"}},{"name":"webhookRecoverMax","value":{"pt-br":"1000","en":"1000","es":"1000","fr":"1000"}},{"name":"webhookCustomHeadersMax","value":{"pt-br":"20","en":"20","es":"20","fr":"20"}},{"name":"webhookSourcesMax","value":{"pt-br":"200","en":"200","es":"200","fr":"200"}},{"name":"webhookRoutesPerSourceMax","value":{"pt-br":"50","en":"50","es":"50","fr":"50"}},{"name":"webhookRouteEventTypesMax","value":{"pt-br":"100","en":"100","es":"100","fr":"100"}},{"name":"webhookProjectsMax","value":{"pt-br":"sem limite","en":"no limit","es":"sin límite","fr":"illimité"}},{"name":"webhookProjectNameMax","value":{"pt-br":"80","en":"80","es":"80","fr":"80"}},{"name":"webhookProjectDescriptionMax","value":{"pt-br":"500","en":"500","es":"500","fr":"500"}},{"name":"webhookProjectsPageDefault","value":{"pt-br":"50","en":"50","es":"50","fr":"50"}},{"name":"webhookProjectsPageMax","value":{"pt-br":"100","en":"100","es":"100","fr":"100"}},{"name":"webhookSearchQueryMax","value":{"pt-br":"200","en":"200","es":"200","fr":"200"}},{"name":"webhookSearchBodyWindow","value":{"pt-br":"30 dias","en":"30 days","es":"30 días","fr":"30 jours"}},{"name":"webhookSlackTextMax","value":{"pt-br":"3000","en":"3000","es":"3000","fr":"3000"}},{"name":"webhookDiscordTextMax","value":{"pt-br":"2000","en":"2000","es":"2000","fr":"2000"}},{"name":"webhookTeamsTextMax","value":{"pt-br":"20000","en":"20000","es":"20000","fr":"20000"}},{"name":"webhookQuotaAlerts","value":{"pt-br":"80% e 100%","en":"80% and 100%","es":"80% y 100%","fr":"80 % et 100 %"}},{"name":"webhookIngestPerMinute","value":{"pt-br":"600","en":"600","es":"600","fr":"600"}},{"name":"webhookBlockedRetention","value":{"pt-br":"7 dias","en":"7 days","es":"7 días","fr":"7 jours"}},{"name":"webhookSecretOverlap","value":{"pt-br":"24 horas","en":"24 hours","es":"24 horas","fr":"24 heures"}},{"name":"webhookSignatureTolerance","value":{"pt-br":"5 minutos","en":"5 minutes","es":"5 minutos","fr":"5 minutes"}},{"name":"webhookDisableAfter","value":{"pt-br":"5 dias","en":"5 days","es":"5 días","fr":"5 jours"}},{"name":"webhookLogRetention","value":{"pt-br":"30 dias","en":"30 days","es":"30 días","fr":"30 jours"}},{"name":"realtimeFreeMessages","value":{"pt-br":"200000","en":"200000","es":"200000","fr":"200000"}},{"name":"realtimeMessageMax","value":{"pt-br":"64 KB","en":"64 KB","es":"64 KB","fr":"64 Ko"}},{"name":"realtimeChannelNameMax","value":{"pt-br":"128","en":"128","es":"128","fr":"128"}},{"name":"realtimeChannelsPerConnection","value":{"pt-br":"50","en":"50","es":"50","fr":"50"}},{"name":"realtimePublishPerSecond","value":{"pt-br":"200","en":"200","es":"200","fr":"200"}},{"name":"realtimeConnectionsPerAccount","value":{"pt-br":"200","en":"200","es":"200","fr":"200"}},{"name":"realtimePresenceMembers","value":{"pt-br":"200","en":"200","es":"200","fr":"200"}},{"name":"realtimeClientIdMax","value":{"pt-br":"128","en":"128","es":"128","fr":"128"}},{"name":"realtimeTokenPatternsMax","value":{"pt-br":"100","en":"100","es":"100","fr":"100"}},{"name":"realtimeChannelListPageMax","value":{"pt-br":"200","en":"200","es":"200","fr":"200"}},{"name":"realtimeWebhookChannelsMax","value":{"pt-br":"50","en":"50","es":"50","fr":"50"}},{"name":"realtimeWebhookTimeout","value":{"pt-br":"5 segundos","en":"5 seconds","es":"5 segundos","fr":"5 secondes"}},{"name":"realtimeTokenTtlMin","value":{"pt-br":"1 minuto","en":"1 minute","es":"1 minuto","fr":"1 minute"}},{"name":"realtimeTokenTtlDefault","value":{"pt-br":"1 hora","en":"1 hour","es":"1 hora","fr":"1 heure"}},{"name":"realtimeTokenTtlMax","value":{"pt-br":"24 horas","en":"24 hours","es":"24 horas","fr":"24 heures"}},{"name":"realtimeHistoryRetention","value":{"pt-br":"24 horas","en":"24 hours","es":"24 horas","fr":"24 heures"}},{"name":"realtimeHistoryPageMax","value":{"pt-br":"100","en":"100","es":"100","fr":"100"}},{"name":"realtimeRewindMax","value":{"pt-br":"100","en":"100","es":"100","fr":"100"}},{"name":"realtimeBackoffMax","value":{"pt-br":"30 segundos","en":"30 seconds","es":"30 segundos","fr":"30 secondes"}}],"webhooks":[{"event":"qr.dynamic.scanned","tool":"qr-dinamico","fires_when":{"pt-br":"Alguém escaneou um QR dinâmico.","en":"Someone scanned a dynamic QR.","es":"Alguien escaneó un QR dinámico.","fr":"Quelqu'un a scanné un QR dynamique."}},{"event":"qr.batch.completed","tool":"qr-dinamico","fires_when":{"pt-br":"Um batch de geração de QR foi concluído.","en":"A QR generation batch has completed.","es":"Se completó un batch de generación de QR.","fr":"Un batch de génération de QR est terminé."}},{"event":"rag.document.ready","tool":"base-de-conhecimento","fires_when":{"pt-br":"Um documento da base de conhecimento terminou de ser indexado e já responde às buscas.","en":"A knowledge base document finished indexing and now answers searches.","es":"Un documento de la base de conocimiento terminó de indexarse y ya responde a las búsquedas.","fr":"Un document de la base de connaissances a fini d'être indexé et répond désormais aux recherches."}},{"event":"rag.document.failed","tool":"base-de-conhecimento","fires_when":{"pt-br":"A indexação de um documento falhou; o payload traz o `error_code`.","en":"Indexing of a document failed; the payload carries the `error_code`.","es":"La indexación de un documento falló; el payload trae el `error_code`.","fr":"L'indexation d'un document a échoué ; le payload contient l'`error_code`."}},{"event":"rag.question.unanswered","tool":"base-de-conhecimento","fires_when":{"pt-br":"Uma pergunta ficou sem resposta porque nenhum trecho da base passou do `min_score`.","en":"A question went unanswered because no knowledge base chunk passed `min_score`.","es":"Una pregunta quedó sin respuesta porque ningún fragmento de la base superó el `min_score`.","fr":"Une question est restée sans réponse car aucun fragment de la base n'a dépassé le `min_score`."}},{"event":"billing.recharge.success","fires_when":{"pt-br":"Uma recarga de créditos foi confirmada.","en":"A credit top-up was confirmed.","es":"Se confirmó una recarga de créditos.","fr":"Une recharge de crédits a été confirmée."}},{"event":"billing.bankruptcy.warning","fires_when":{"pt-br":"O saldo da conta ficou negativo; as ferramentas podem ser bloqueadas.","en":"The account balance went negative; tools may be blocked.","es":"El saldo de la cuenta quedó negativo; las herramientas pueden bloquearse.","fr":"Le solde du compte est devenu négatif ; les outils peuvent être bloqués."}},{"event":"webhook.test","fires_when":{"pt-br":"Ping de teste pedido na rota de teste de um endpoint. Grátis e assinado como os demais eventos.","en":"Test ping requested through an endpoint's test route. Free and signed like every other event.","es":"Ping de prueba solicitado en la ruta de prueba de un endpoint. Gratis y firmado como los demás eventos.","fr":"Ping de test demandé via la route de test d’un endpoint. Gratuit et signé comme les autres événements."}}]}