# Agentes de IA (MCP) 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. 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`. ## Conexão | | | | ------------------ | ---------------------------------------------- | | URL | | | Transporte | Streamable HTTP, sem sessão, respostas JSON | | Autenticação | `Authorization: Bearer ` | | Escopo obrigatório | `mcp:use`, além dos escopos de cada ferramenta | Nunca 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. ## Crie uma chave para o agente No [dashboard](https://dashboard.respawntech.dev), em **APIs**, crie um token só para o agente. Os modelos prontos preenchem escopos e controles: * **Agente de suporte**: busca na base de conhecimento e consulta de saldo. * **Agente completo**: todas as ferramentas. * **Somente leitura**: só consultas. Uma chave de agente deve ter um **limite diário de gasto** (sugestão: ) e uma **data de expiração**. Veja [Controles da chave](/autenticacao#controles-da-chave). ## Configure o cliente Clientes que só falam stdio usam a ponte `mcp-remote`, como no último exemplo. ## Ferramentas 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. Ferramentas **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. ## Parâmetros da conexão 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". Por exemplo, um agente de suporte que só lê a base de conhecimento, com respostas em português: Parâmetros repetidos em várias conexões podem virar um **perfil** no dashboard (`profile=`), e uma chave pode ter um perfil padrão. ## Custos e controle de gasto * 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). * 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. * `simulate=true` na URL faz todas as ferramentas que cobram devolverem só a estimativa. É o jeito de testar um agente sem gastar. * 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`. * O **botão de pânico** no dashboard desliga o acesso de todos os agentes da conta na hora (`mcp.agents_disabled`). * O extrato do dashboard separa o gasto por canal (API, MCP, dashboard) e por chave. ## Idempotência 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é devolve o resultado anterior sem executar nem cobrar de novo (`_meta["tech.respawn/replayed"]`). ## Limites | Limite | Valor | | --------------------------- | --------------------------------------------------------------------------- | | Requisições | por minuto por chave, somadas às da API | | Corpo da requisição | | | Resultado de uma ferramenta | | | Chamadas de `tools_search` | por minuto | ## Erros 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"}}`. Os erros das ferramentas são os mesmos da API; veja [Códigos de erro](/erros). # Autenticação ## Token de API Todas as rotas em `/v1/tools/*` exigem um **token de API**, criado no [dashboard](https://dashboard.respawntech.dev) em **APIs**. | Forma | Exemplo | | ----------------------------- | ------------------------------------ | | Header dedicado (recomendado) | `X-API-Token: rsk_live_...` | | Authorization | `Authorization: ApiKey rsk_live_...` | | Authorization (alternativo) | `Authorization: Token rsk_live_...` | 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. ## Requisitos da conta Antes de qualquer chamada às ferramentas, a conta precisa ter: * **E-mail verificado** — caso contrário, `auth.email_not_verified` (403). * **Saldo não negativo** — conta suspensa retorna `billing.user_bankrupt` (402). * **Créditos suficientes** — operações pagas sem saldo retornam `billing.insufficient_credits` (402). ## Escopos 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. 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. ### Combinações recomendadas | Caso de uso | Escopos sugeridos | | -------------------------------------------- | ------------------------------------------------------------------------ | | Gerar e gerenciar QR codes | `tools:qr:dynamic:manage` | | Painel somente leitura de QRs | `tools:qr:dynamic:read` | | Pipeline de PDF | `tools:pdf:manage` + `tools:pdf:read` | | Verificar URLs antes de exibir | `tools:safety:read` | | Assistente que busca na base de conhecimento | `tools:rag:query` | | Enviar e manter documentos da base | `tools:rag:manage` | | [Agente de IA (MCP)](/agentes-de-ia) | `mcp:use` + os escopos das ferramentas que ele vai usar + `account:read` | | Integração completa | `tools:manage` (mais simples, menos seguro) | ## Controles da chave 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): * **Expiração**: depois da data, o token para de funcionar (`auth.unauthorized`). * **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. * **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`). * **Modelos prontos**: ao criar a chave, "Somente leitura", "Agente de suporte" e "Agente completo" preenchem escopos e controles de uma vez. Expiração, limite e bases podem ser alterados depois; os escopos, não. Para mudar escopos, crie outra chave. ## Erros de autenticação ## Dashboard vs. token de API * **Dashboard** — a navegação no painel não consome a franquia diária nem debita leituras medidas. * **Token de API** — leituras medidas compartilham a [franquia diária](/cobranca-e-limites#franquia-diaria) de requisições ( em contas premium). Depois disso, cada leitura debita créditos. ## Exemplo Criar um QR com dry-run para validar o token, os escopos e o custo sem debitar nada: # Cobrança e limites ## Modelo de créditos * O saldo é exibido em **créditos** no dashboard, e todos os valores desta documentação estão em créditos. * Operações pagas debitam na confirmação — nunca no dry-run. * 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). * Cada token pode ter um [limite diário de gasto](/autenticacao#controles-da-chave): passar dele responde `billing.spend_limit_reached`. 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. ## Dry-run e preview de custo | Header | Efeito | | ---------------------- | -------------------------------------------------- | | `X-Dry-Run: true` | Valida e calcula o custo sem persistir nem debitar | | `X-Preview-Cost: true` | Alias de `X-Dry-Run` | | `X-Include-Cost: true` | Inclui `meta.cost` na resposta | Dry-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. As 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. ### Orçamento de uma operação paga Em `POST` de criação e conversão, `data` traz o orçamento item a item: ```json { "success": true, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "data": { "total_cost_credits": 15000, "total_cost_units": 15000, "items": [ { "description": { "key": "billing.pdf.convert_base" }, "cost_credits": 10000, "type": "base" }, { "description": { "key": "billing.pdf.retention.7" }, "cost_credits": 5000, "type": "storage" } ], "is_monthly_recurring": false, "currency": "CREDITS" } } ``` 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. No 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`. ### Preview de uma leitura medida Em leituras medidas, o dry-run não executa a consulta — mostra apenas a posição na franquia e se a leitura seria cobrada: ```json { "success": true, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "data": { "operation": "qr_dynamic.analytics", "metering": { "within_quota": true, "quota_limit": 2000, "quota_used": 125, "quota_remaining": 1875, "cost_units": 300, "would_charge": false } }, "meta": { "cost": { "units": 300, "credits": 0.03, "charged": false, "within_quota": true, "quota_limit": 2000, "quota_used": 125, "quota_remaining": 1875 } } } ``` Com `X-Include-Cost: true` numa leitura real, o mesmo objeto `meta.cost` acompanha a resposta normal (`charged: true` quando a leitura foi cobrada). ## Idempotência Envie `X-Idempotency-Key` (UUID recomendado) em qualquer rota de ferramentas: * a chave vale por ****; * mesma chave + mesmo payload → a resposta original é reaproveitada, sem nova cobrança; * mesma chave + payload diferente → `idempotency.conflict` (409). Use sempre em `POST` de criação e conversão para que retries de rede não cobrem duas vezes. Exceçã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). ## Rate limit Cada token de API pode fazer ** requisições por minuto** nas rotas `/v1/tools/*`. Toda resposta informa a situação: ```http X-RateLimit-Limit: 60 X-RateLimit-Remaining: 42 X-RateLimit-Reset: 1712345678 ``` Ao exceder, a API retorna `rate_limit.exceeded` (429). Aguarde até `X-RateLimit-Reset` antes de reenviar. ## Repetir com espera (retry) Repita só o que pode dar certo na tentativa seguinte: * **`429`**: espere até o horário de `X-RateLimit-Reset` (segundos Unix). * **`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. * **Demais `4xx`** (validação, escopo, saldo): não repita. A resposta não muda até você corrigir a requisição. Em `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. ```js // Repete 429, 5xx e falhas de rede; devolve na hora os demais erros. async function withRetry(send, attempts = 5) { for (let attempt = 1; ; attempt++) { let response; try { response = await send(); } catch (error) { if (attempt === attempts) throw error; // falha de rede } if (response && response.status !== 429 && response.status < 500) return response; if (attempt === attempts) return response; const reset = Number(response?.headers.get('X-RateLimit-Reset')); const wait = response?.status === 429 && reset ? Math.max(0, reset * 1000 - Date.now()) : Math.min(2 ** (attempt - 1) * 1000, 30_000) * (0.5 + Math.random()); await new Promise((resolve) => setTimeout(resolve, wait)); } } const key = crypto.randomUUID(); // a mesma chave em todas as tentativas const response = await withRetry(() => fetch(url, { method: 'POST', headers: { 'X-API-Token': process.env.RESPAWN_API_TOKEN, 'X-Idempotency-Key': key, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }), ); ``` ```python import os, random, time, uuid import requests # Repete 429, 5xx e falhas de rede; devolve na hora os demais erros. def with_retry(send, attempts=5): for attempt in range(1, attempts + 1): try: response = send() except (requests.ConnectionError, requests.Timeout): if attempt == attempts: raise response = None if response is not None and response.status_code != 429 and response.status_code < 500: return response if attempt == attempts: return response reset = response.headers.get("X-RateLimit-Reset") if response is not None else None if response is not None and response.status_code == 429 and reset: wait = max(0, int(reset) - time.time()) else: wait = min(2 ** (attempt - 1), 30) * (0.5 + random.random()) time.sleep(wait) key = str(uuid.uuid4()) # a mesma chave em todas as tentativas response = with_retry(lambda: requests.post( url, json=payload, headers={"X-API-Token": os.environ["RESPAWN_API_TOKEN"], "X-Idempotency-Key": key}, timeout=30, )) ``` ## Franquia diária de leituras Leituras feitas com **token de API** (listagens, status, analytics, downloads…) entram em uma franquia diária compartilhada entre todas as ferramentas: | Item | Valor | | ----------------------- | -------------------------------------- | | Leituras grátis por dia | **** | | Contas premium | **** | | Reset | | Toda leitura medida devolve a posição na franquia: ```http X-API-Quota-Limit: 2000 X-API-Quota-Used: 847 X-API-Quota-Remaining: 1153 ``` Requisições feitas pelo [dashboard](https://dashboard.respawntech.dev) **não** entram na franquia nem são cobradas. ### Leituras medidas Depois de esgotar a franquia, cada leitura via API debita: Operações de escrita sem custo próprio (`PATCH`, ativar, desativar, `DELETE`) e o dry-run de criação são sempre gratuitos. A franquia cobre a maioria das integrações. Para acompanhar mudanças de estado, prefira [webhooks](/webhooks) em vez de consultar o status repetidamente. ## Timeouts * Maioria das rotas: ****. * Downloads de arquivo: ****. # Códigos de erro ## Resposta de sucesso ```json { "success": true, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "data": {}, "meta": {} } ``` | Campo | Descrição | | ---------- | ------------------------------------------------ | | `success` | `true` em respostas OK | | `trace_id` | ID de correlação (também no header `X-Trace-Id`) | | `data` | Recurso ou resultado | | `meta` | Opcional — custo (`meta.cost`), paginação etc. | Com `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`. ## Formato de erro ```json { "success": false, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "error": { "code": "billing.insufficient_credits", "message": "Créditos insuficientes para esta operação.", "details": { "required": 100, "available": 50 } } } ``` | Campo | Descrição | | --------------- | ----------------------------------------------------------------------------------------------- | | `error.code` | Identificador estável — trate erros por ele, nunca pela `message` | | `error.message` | Texto legível; pode mudar entre versões | | `error.details` | Contexto estruturado opcional; em `validation.failed`, `details.fields` traz os erros por campo | | `trace_id` | Informe em tickets de suporte | ## Erros comuns a todas as ferramentas ### Autenticação e permissões ### Créditos e conta ### Requisição e limites ### Agentes de IA (MCP) ### Plataforma ### Vídeo ## Erros por ferramenta Jobs 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. ## Boas práticas 1. Trate `402` com um fluxo de recarga no seu sistema. 2. Em `429`, respeite `X-RateLimit-Reset` antes de tentar de novo. 3. Use `X-Dry-Run` para capturar `validation.failed` sem custo. 4. Repita apenas erros `5xx` e de rede, sempre com a mesma `X-Idempotency-Key`. 5. Registre o `trace_id` de toda resposta com erro. # Documentação da API ## Ferramentas ## Primeiros passos ### Crie um token de API 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). ### Estime o custo com dry-run 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). ### Faça a chamada Todas as rotas de ferramentas usam o prefixo `https://api.respawntech.dev/v1/tools`. Cada endpoint traz exemplos prontos em cURL, JavaScript e Python. ## Headers comuns | Header | Valor | Efeito | | ------------------- | -------------- | ---------------------------------------------------------------------- | | `X-API-Token` | `rsk_live_...` | Autenticação (recomendado) | | `X-Dry-Run` | `true` | Simula a operação sem persistir nem debitar | | `X-Include-Cost` | `true` | Inclui `meta.cost` na resposta | | `X-Idempotency-Key` | UUID | Evita cobrança duplicada em reenvios () | Respostas trazem `X-Trace-Id` (use em tickets de suporte), `X-RateLimit-*` e, em leituras medidas, `X-API-Quota-*`. ## Guias # Usar com IA 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**. ## Dois servidores MCP | | MCP de documentação | [MCP da conta](/agentes-de-ia) | | ------------ | ------------------------------------------------------ | --------------------------------------------------------------------------------- | | Para quê | Responder perguntas sobre a API enquanto você programa | Usar as ferramentas com a sua conta (criar QR, converter PDF, transcrever áudio…) | | URL | | | | Autenticação | Nenhuma | Token de API com `mcp:use` | | Custo | Grátis | O mesmo da API | | Acesso | Só leitura da documentação pública | Age na conta, com escopos e limite de gasto | Os dois podem ficar conectados ao mesmo tempo: um agente consulta a documentação e depois usa o MCP da conta para executar. O MCP de documentação está sendo liberado aos poucos. Enquanto ele não estiver ativo, responde `404` com o código `mcp.disabled`. O llms.txt e as páginas em Markdown já funcionam. ## Conexão | | | | ------------ | ------------------------------------------------------------------------- | | URL | | | Transporte | Streamable HTTP | | Autenticação | Nenhuma (não envie token) | | Custo | Grátis | | Limite | requisições por minuto por IP | | Idioma | Parâmetro `lang` opcional em cada ferramenta: `pt-br`, `en`, `es` ou `fr` | As respostas vêm do mesmo conteúdo deste site, incluindo os preços em vigor. ## Configure o cliente No **Claude.ai** e no **ChatGPT**, adicione um conector personalizado (custom connector) e informe só a URL . Não é preciso login nem token. Clientes que só falam stdio usam a ponte `mcp-remote`, como no último exemplo. ## Ferramentas Um 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. ## Sem MCP: llms.txt e Markdown Para agentes que só leem a web: * [`/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. * [`/llms-full.txt`](/llms-full.txt): todas as páginas em um único arquivo de texto. * Páginas em Markdown: `/md/.md` em português e `/md//.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`. * [`/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. # Webhooks 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: | Origem | Onde se configura | Custo | | ------------------------------------------------ | -------------------------------------------------------------- | ----------------------------------- | | **Eventos da conta** (ex.: `qr.dynamic.scanned`) | **Webhooks** no [dashboard](https://dashboard.respawntech.dev) | Grátis | | **Callbacks de jobs** (ex.: conversão de PDF) | `webhook_url` em cada requisição | Grátis | | **Webhook Relay** (seus próprios eventos) | API do [Webhook Relay](/ferramentas/webhook-relay) | Por entrega além da franquia mensal | ## Eventos da conta 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. Esses 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). ## Requisição enviada ```http POST Content-Type: application/json User-Agent: RespawnTech-Webhook/2.0 webhook-id: msg_2Vf8tK1pQ9zX webhook-timestamp: 1790338000 webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= X-Webhook-Signature: sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd ``` | Header | Conteúdo | | --------------------- | -------------------------------------------------------------------------------------------------------- | | `webhook-id` | Identificador da mensagem. É o **mesmo em todos os retries**: use para descartar duplicatas. | | `webhook-timestamp` | Momento do envio desta tentativa, em segundos Unix. | | `webhook-signature` | `v1,`, uma assinatura por secret ativo, separadas por espaço (duas durante uma troca de secret). | | `X-Webhook-Signature` | Assinatura legada `sha256=`, sem timestamp. Mantida para receptores antigos. | Os 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`). ### Corpo Eventos da conta e mensagens do Webhook Relay usam o mesmo envelope: ```json { "event_id": "550e8400-e29b-41d4-a716-446655440000", "type": "qr.dynamic.scanned", "timestamp": 1790338000, "payload": { "resource_id": "…", "slug": "promo-verao", "ip": "hash_anônimo", "user_agent": "Mozilla/5.0 …", "referer": "https://…" } } ``` O 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. ## Validar a assinatura O conteúdo assinado é `webhook-id + "." + webhook-timestamp + "." + corpo bruto`, com HMAC-SHA256 e o resultado em base64. Para validar: 1. Leia o **corpo bruto**, antes de qualquer parse de JSON (reformatar o JSON muda a assinatura). 2. Recuse timestamps com mais de de diferença do seu relógio (proteção contra replay). 3. Calcule a assinatura e compare, **em tempo constante**, com cada item de `webhook-signature`. Basta um bater. Com uma biblioteca Standard Webhooks, use o `standard_webhooks_secret` (formato `whsec_…`). Sem biblioteca, a chave do HMAC são os bytes do `secret` bruto. ```js // npm i standardwebhooks import express from 'express'; import { Webhook } from 'standardwebhooks'; const wh = new Webhook(process.env.WEBHOOK_SECRET); // whsec_… const app = express(); app.post('/hook', express.raw({ type: '*/*' }), (req, res) => { let event; try { event = wh.verify(req.body, req.headers); // lança erro se inválido } catch { return res.sendStatus(401); } // processe `event` de forma assíncrona e responda rápido res.sendStatus(204); }); ``` ```python # pip install standardwebhooks import os from flask import Flask, request from standardwebhooks.webhooks import Webhook wh = Webhook(os.environ["WEBHOOK_SECRET"]) # whsec_… app = Flask(__name__) @app.post("/hook") def hook(): try: event = wh.verify(request.get_data(), dict(request.headers)) except Exception: return "", 401 # processe `event` de forma assíncrona e responda rápido return "", 204 ``` ```go // Sem biblioteca: secret é o valor bruto (não o whsec_). func verify(secret string, r *http.Request, body []byte) bool { id, ts := r.Header.Get("webhook-id"), r.Header.Get("webhook-timestamp") t, err := strconv.ParseInt(ts, 10, 64) if err != nil || math.Abs(time.Since(time.Unix(t, 0)).Seconds()) > 300 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(id + "." + ts + ".")) mac.Write(body) want := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil)) for _, sig := range strings.Fields(r.Header.Get("webhook-signature")) { if hmac.Equal([]byte(sig), []byte(want)) { return true } } return false } ``` 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. ## Secrets e rotação | Destino | Onde obter o secret | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Endpoint de eventos da conta | Na criação, ou pela opção de revelar o secret em **Webhooks** no dashboard. | | Endpoint do Webhook Relay | Na criação, ou em [Revelar secret](/ferramentas/webhook-relay#webhooks-endpoints-secret). | | 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). | Todo secret aparece em duas formas: `secret` (bruto) e `standard_webhooks_secret` (`whsec_` + base64 dos mesmos bytes). As duas validam as mesmas assinaturas. Ao [trocar o secret](/ferramentas/webhook-relay#webhooks-endpoints-rotate_secret) de um endpoint, o antigo continua assinando por : nesse período `webhook-signature` traz as duas assinaturas, e você atualiza o receptor sem perder eventos. ## Entrega e retries 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: | Tentativa | Espera antes dela | | --------- | ----------------- | | 1 | — | | 2 | 5 segundos | | 3 | 5 minutos | | 4 | 30 minutos | | 5 | 2 horas | | 6 | 5 horas | | 7 e 8 | 10 horas | São cerca de 27 horas no total, com variação de ±10%. Casos especiais: * **`429` e `503`** com `Retry-After`: a próxima tentativa respeita o header quando ele pede mais tempo (até 10 horas). * **`410 Gone`**: a entrega para e o endpoint é desativado. * 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). Boas práticas no receptor: * Responda **2xx** o mais rápido possível e processe o evento de forma assíncrona. * **Descarte duplicatas pelo `webhook-id`**: uma entrega pode chegar mais de uma vez (ex.: seu servidor processou, mas a resposta se perdeu). * Não dependa da ordem de chegada; use o `timestamp` do corpo quando a ordem importar. ### Desativação automática Um endpoint que falha sem parar por é 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. O histórico de mensagens, entregas e tentativas fica guardado por . ## Callback de jobs 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: ```json { "job_id": "job_abc123", "success": true, "result_url": "https://…", "metadata": { "conversion_id": "…" } } ``` Em 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`. ## Seus próprios eventos 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). # Áudio ## Visão geral Toda operação de áudio é **assíncrona** e cobrada na hora: 1. `POST` na operação → **202 Accepted** com `job_id`, `status: pending` e `poll_after_seconds`. 2. Um worker processa em background. 3. 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. Os 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. 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`…). ## Entrada do áudio As operações que recebem um áudio pedem **exatamente um** destes campos: | Campo | O que é | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `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 | | `audio_base64` | O áudio inline em base64, até decodificado | | `file_id` | Id de um arquivo de áudio já enviado para a conta | Formatos aceitos: mp3, wav, ogg, m4a, flac e webm. O limite é de de áudio e 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. Todas 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. ## Custos O "minuto iniciado" arredonda para cima. Um áudio cuja duração não dá para ler do cabeçalho é cobrado pelo teto de 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). ### Dry-run 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: ```json { "success": true, "data": { "service_code": "audio_transcribe_minute", "units": 3, "rate": 15000, "total_units": 45000, "duration_ms": 125000, "duration_known": true, "parts": [{ "service_code": "audio_transcribe_minute", "units": 3, "rate": 15000, "total": 45000 }] } } ``` `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`. ## Operações ### Transcrever áudio `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. ### Gerar fala a partir de texto `text` aceita até 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. ### Listar as vozes Catálogo fechado de vozes do `speak` e do `revoice`. `language` filtra por idioma. Não é cobrado. ### Gerar efeito sonoro ou música `kind`: `effect` (1 a , padrão 5 s) ou `music` (instrumental, 1 a , 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. ### Analisar áudio 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. ### Converter ou normalizar áudio `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. ### Resumir áudio 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. ### Traduzir áudio `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). ### Falar a gravação de novo com outra voz 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`. ### Listar jobs Os jobs mais recentes primeiro, com `limit` (padrão 20, até ) 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 (). ### Status, resultado e download 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). `status`: `pending`, `processing`, `completed` ou `failed`. ## Vozes clonadas e consentimento (só MCP) 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`). * **`voice_create`** clona a voz **do titular da conta** a partir de uma amostra de . 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`. * **`voice_list`** lista as vozes da conta (`voice_id`, nome, idioma, `status`, validade). Grátis. * **`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. ### Uso responsável * **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. * **Nada de imitação.** Nem a fala, nem o `revoice`, nem a geração de sons aceitam imitar artistas, vozes ou obras existentes. * **Limites:** até vozes clonadas por conta, criações por dia e usos por hora (`audio.voice_limit`, `audio.voice_rate_limited`). * **Dados guardados:** a amostra fica criptografada por até 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 . * **Aviso ao ouvinte:** se você publica áudio sintético, avise que a voz é gerada, principalmente quando for uma voz clonada. ## Scopes | Scope | Operações | | ------------------------ | --------------------------------------------------------------------------------- | | `tools:audio:transcribe` | `transcribe`, `summarize`, `translate` | | `tools:audio:speak` | `speak`, `generate`, `revoice` (no MCP, também `voice_create` e `voice_delete`) | | `tools:audio:process` | `analyze`, `convert` | | `tools:audio:read` | `voices`, `jobs`, `jobs/{id}` (os três scopes acima também leem os próprios jobs) | `tools:manage` cobre tudo. A lista completa fica em [Autenticação](/autenticacao). ## Erros `audio.*` na resposta ou, em jobs que falham depois de aceitos, no `error_code` do job. # Base de conhecimento ## Visão geral 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. **Crie uma base** com o idioma dos documentos (melhora a busca por palavras-chave). **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. **Busque** com a pergunta do usuário e use os trechos devolvidos como contexto da resposta. **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). **Registre feedback** (opcional): trechos confirmados sobem nas próximas buscas parecidas e os rejeitados somem. ## Custos 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). A 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. ## Bases ### Criar base * `name` (obrigatório, até 120 caracteres) e `description` (até 1.000). * `language`: `simple` (padrão), `portuguese`, `english`, `spanish` ou `french`. Define o dicionário da busca por palavras-chave. * `default_mode`: `fast` (padrão) ou `accurate` (rerank extra, mais preciso e um pouco mais caro). * 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`). ### Listar bases Devolve também o uso da conta em relação aos limites (`usage`). ### Consultar base ### Filtros disponíveis 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"]}`. ### Alterar base 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. ### Reindexar base Reprocessa todos os documentos com as configurações atuais da base. Cada trecho é cobrado como numa ingestão nova. ### Painel de saúde da base 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. ### Índice dos documentos 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. ### Achar duplicados e contradições 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. ### Trocar o modelo de embeddings 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. ### Marcar documentos como desatualizados 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. ### Excluir base A exclusão é assíncrona e apaga documentos, trechos e feedback. ## Documentos ### Adicionar ou substituir documento Segurança…", "source_uri": "https://ajuda.example.com/artigos/42", "metadata": { "categoria": "conta" } }` }} > * `content` (obrigatório): texto puro ou Markdown em UTF-8, até . Para PDF, Office e imagens, use [Importar arquivo](#rag-documents-file). * `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. * `paths` (até 50) e `commit`: os arquivos de onde o documento vem, para [marcá-lo como desatualizado](#rag-collections-changes) quando mudarem. * Segredos reconhecidos no texto (chaves de API, tokens) são trocados por `[REDACTED:tipo]` antes de indexar; `redactions` conta quantos. * `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. * 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. ### Importar arquivo (PDF, Office, imagem) 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 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`). ### Consultar importação de arquivo ### Listar documentos 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. ### Consultar documento ### Alterar título ou metadados Não reprocessa o conteúdo e não cobra nada. ### Ver seções indexadas Mostra como o documento foi dividido, útil para ajustar títulos e tamanho dos trechos. ### Reprocessar documento com falha Só para documentos com `status: failed`; os demais respondem `rag.document_not_failed`. ### Excluir documento ## Fontes sincronizadas 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::…`, 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. | `kind` | `start_url` | O que importa | | --------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `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. | | `sitemap` | URL do sitemap (ou índice) | Importa as páginas listadas, até `max_pages`. `max_depth` não se aplica. | | `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. | | `gdrive` | URL de uma pasta `/folders/` | Pasta compartilhada como "qualquer pessoa com o link"; Docs viram Markdown. Só existe se o servidor tiver a integração ligada. | ### Listar fontes sincronizadas ### Criar fonte sincronizada * `name` (até 200) e `start_url` (http ou https) são obrigatórios; `kind` padrão `site`. * `max_pages`: 1 a 1000 (padrão 100). `max_depth`: 1 a 10 (padrão 3). * `frequency_hours`: `0` (só manual), `6`, `24` (padrão) ou `168`. * 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. ### Alterar fonte Todos os campos são opcionais. Com `enabled: false` ou `frequency_hours: 0`, `next_sync_at` fica nulo. ### Sincronizar agora Responde `409` se a fonte já estiver sincronizando. `pages_total`, `pages_new`, `pages_changed` e `pages_removed` resumem a execução. ### Histórico de sincronizações ### Excluir fonte ## Busca ### Modos de busca e reranker * **`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. * **`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`). * **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`. * Se o serviço de embeddings falhar, a busca ainda responde só por palavras-chave (`mode_used: lexical`), cobrada como cache. ### Buscar na base * `query` (obrigatório, até caracteres). * `top_k`: quantos trechos devolver (padrão 5, máximo ). * `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. * `filters`: só trechos de documentos cujos metadados contêm estes valores. * `min_score` (só no `accurate`) e `max_per_document` (0–20) refinam o resultado. * `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). * `highlights` marca, em posições de caractere Unicode, os termos encontrados em `content`. Consultas repetidas em até vêm do cache (`cached: true`) e custam menos. 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. ## Feedback de relevância ### Registrar feedback de relevância * `label`: `positive` (o trecho responde à pergunta) ou `negative`. * `end_user_ref` (opcional): identifica quem avaliou, quando o voto vem do seu usuário final. É guardado só como hash. * Até votos por minuto. Perguntas parecidas são agrupadas, e o voto passa a valer para todo o grupo. ### Listar feedback ### Apagar um grupo de feedback ## Rerank avulso ### Reordenar textos (rerank) Ordena **a sua** lista de textos pela relevância à pergunta, sem base envolvida. Até textos por chamada (cada um é cortado em 2.000 caracteres). `index` aponta para a posição no array enviado. ## Resposta com citações ### Responder com citações 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). * 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. * 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). * `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. * 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. * 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. ## Widget de chat embutível 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. ```html ``` Atributos: `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`. ### Criar chave pública (widget) * `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. * `rate_per_minute`: 1 a 600 (padrão 30), por chave. `generate` liga o texto gerado por IA nas respostas dessa chave. * Até 20 chaves por base. A `key` aparece **uma única vez**; guardamos só o hash. 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. ### Listar chaves públicas ### Revogar chave pública ### Responder pelo widget (rota pública) 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). ### Script do widget ## Analytics de perguntas ### Analytics das perguntas 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). * `answer_rate`: respondidas ÷ total de perguntas. * `feedback_ctr`: respondidas que receberam voto (pelo `query_id`) ÷ respondidas. * `positive_rate`: votos positivos ÷ votos. * `top_questions` e `unanswered_questions`: até 20 perguntas agrupadas cada, para você descobrir o que escrever na base. 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. ## Roteamento ### Decidir entre opções (roteamento) 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. ## Preços e limites da conta ### Consultar preços ### Consultar limites Valores vigentes da sua conta (já com o bônus do plano), úteis para validar um envio antes de fazê-lo. ## Eventos (webhooks) Cadastre um endpoint no dashboard para receber: * `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. * `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". O envio é de melhor esforço, como nos demais eventos. Veja a [lista de eventos](/webhooks). ## Usar pelo MCP 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. ## Erros Os códigos sem status HTTP aparecem no campo `error_code` de um documento com `status: failed`. ## Limites | Limite | Valor | | ------------------------------------------ | -------------------------------------------- | | Tamanho de um documento | | | Bases por conta | | | Trechos indexados por conta | | | Documentos em processamento ao mesmo tempo | | | Tamanho da pergunta | caracteres | | Trechos por busca (`top_k`) | | | Textos por rerank avulso | | | Votos de feedback por minuto | | | Arquivo importado | 50 MB | | Páginas por fonte | 1.000 (padrão 100) | | Chaves públicas por base | 20 | As 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`). # Extrator de erro de CI ## Visão geral 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`. ## Custos 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. ## Endpoints ### Extrair o erro de um log `log` (obrigatório) é o texto do log. `max_lines` (opcional, padrão 40, máximo 200) limita quantas linhas do erro voltam. ## Erros # Memória de aprendizados ## Como funciona Um aprendizado é um texto curto com um `kind`, ligado a um `project`: | `kind` | Para guardar | | ------------ | -------------------------------------------- | | `pitfall` | Armadilhas e erros que não devem se repetir. | | `decision` | Decisões tomadas e o motivo. | | `convention` | Convenções do projeto. | | `howto` | Como fazer algo (rodar, publicar, depurar). | | `other` | O que não cabe nos outros. | Os 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. Também está disponível no [MCP](/agentes-de-ia) como `learning_save`, `learning_recall` e `learning_forget` (toolset `learnings`). ## Duplicados e ingestão assíncrona * **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. * **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. ## Custos 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.*`). ## Endpoints ### Salvar aprendizado `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`. ### Recuperar aprendizados `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. ### Esquecer aprendizado # Notificações ## Como funciona Uma notificação tem `title`, `message` e `priority` (`normal` ou `high`), e opcionalmente uma `url` para o humano abrir. Ela sai por um **canal**: * **Canal escolhido:** o `channel` do pedido, pelo nome do canal. * **Canal padrão:** o canal marcado com `is_default`, quando o pedido não traz `channel`. * **E-mail da conta:** sem canal nenhum configurado, a notificação vai para o e-mail da conta. També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. ## Canais Cada canal tem um `name` único na conta e um `kind`: | `kind` | Campos | Como entrega | | --------- | ----------------- | ----------------------------------------------------------------------------------- | | `email` | nenhum | Envia **só para o e-mail da conta**. Não é possível escolher outro destinatário. | | `webhook` | `url`, `secret` | Faz um `POST` na URL, assinado como no [Webhook Relay](/ferramentas/webhook-relay). | | `ntfy` | `server`, `topic` | Publica no tópico do [ntfy](https://ntfy.sh), no servidor informado. | `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. 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. ## Limites, retry e histórico * **Rate limit:** há um limite de notificações por período; acima dele o envio falha com `notify.rate_limited`. * **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. * **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**. ## Custos 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. ## Endpoints ### Enviar notificação `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. ### Histórico de envios Paginado por `page` e `page_size` (até 100). Não traz o corpo das mensagens. ### Listar canais ### Criar canal `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`. ### Consultar canal ### Alterar canal Envie só os campos que mudam. Um canal desativado responde `notify.channel_disabled` quando escolhido num envio. ### Excluir canal ### Testar canal Envia uma notificação de teste pelo canal, para conferir a configuração antes de depender dele. ## Erros # PDF Convert ## Visão geral O PDF Convert é **assíncrono**: 1. `POST /convert` ou `POST /batch` → **202 Accepted** com o `conversion_id` (o `{id}` das demais rotas). 2. Um worker renderiza em background (Chromium headless). 3. Acompanhe pelo [callback `webhook_url`](/webhooks#pdf-callback) ou consultando `GET /{id}`. 4. Baixe com `GET /{id}/download` quando `status` for `completed`. 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. ## Custos O batch aceita até ** 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). ## Conversão ### Converter HTML em PDF/PNG/JPG

Fatura

", "css": "body { font-family: sans-serif; }", "format": "pdf", "page_size": "A4", "retention_days": 7 }`, }} > 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`). | Campo | Limite / valores | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `html` | máx. | | `url` | alternativa a `html`: endereço público `http`/`https`, até 2048 caracteres (`pdf.invalid_url` se for privado ou inválido) | | `css` | máx. | | `format` | `pdf` (padrão), `png`, `jpg` / `jpeg` | | `file_name` | nome do arquivo gerado | | `title`, `author`, `subject`, `keywords` | metadados do documento, até 1000 caracteres cada | | `header_html` / `footer_html` | cabeçalho e rodapé repetidos em cada página | | `google_fonts_url` | URL de CSS do Google Fonts a carregar | | `wait_selector` / `wait_delay` | espera um seletor aparecer / um atraso em ms antes de renderizar | | `enable_js` / `custom_js` | executa JavaScript em sandbox (SPAs, gráficos) / script extra | | `data` | objeto com variáveis para placeholders `{{campo}}` no HTML | | `retention_days` | `0` (efêmero, \~24h), `1`–`30` ou `-1` (permanente) | | `webhook_url` | recebe o [callback](/webhooks#pdf-callback) ao terminar | | `template_id`, `template_version`, `font_ids`, `post_process` | [templates salvos](#templates), [fontes](#fontes) e [pós-processamento](#pos-processamento) | Tamanho de página, margens, mídia, numeração e qualidade estão em [Opções de página e render](#opcoes-de-pagina). Guarde o `conversion_id`: é o `{id}` usado para consultar status e baixar o arquivo.
### Batch com template

{{title}}

", "css": "h1 { color: #333; }" }, "config": { "page_size": "A4", "format": "pdf", "retention_days": 7 }, "items": [ { "custom_id": "fatura-001", "file_name": "fatura-001.pdf", "data": { "title": "Fatura 001" } }, { "custom_id": "fatura-002", "file_name": "fatura-002.pdf", "data": { "title": "Fatura 002" } } ], "delivery": { "method": "zip", "webhook_url": "https://example.com/hooks/pdf" } }`, }} > | Campo | Descrição | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `template.html` | **obrigatório** — HTML com placeholders `{{campo}}` (sintaxe de Go template: `{{if}}`, `{{range}}` também funcionam) | | `template.css` | CSS compartilhado | | `items[]` | **obrigatório**, 1 a itens: `custom_id` (**obrigatório**, seu identificador), `file_name` e `data` | | `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` | | `delivery.method` | `zip` ou `list`: como o resultado é entregue ao terminar | | `delivery.webhook_url` | recebe o [callback](/webhooks#pdf-callback) ao terminar | 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).
### Reprocessar falha

Fatura corrigida

", "format": "pdf", "page_size": "A4" }`, }} > 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.
## Opções de página e render 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). | Campo | Valores | | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `page_size` | `A3`, `A4` (padrão), `A5`, `Letter`, `Legal`, `Tabloid` ou `Custom` | | `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` | | `orientation` | `portrait` (padrão) ou `landscape` | | `margin_top`, `margin_bottom`, `margin_left`, `margin_right` | margens da página | | `scale` | escala do render | | `print_background` | imprime cores e imagens de fundo (padrão `true`) | | `media_type` | `print` ou `screen`: qual `@media` o CSS enxerga | | `color_scheme` | `light` ou `dark`: valor de `prefers-color-scheme` | | `page_numbers` | liga o contador de páginas na posição: `header-left`, `header-center`, `header-right`, `footer-left`, `footer-center` ou `footer-right` | | `page_number_format` | texto do contador, com `{page}` e `{total}` (padrão `{page} / {total}`, até 200 caracteres). Exige `page_numbers` | | `jpeg_quality` | de 1 a 100 (padrão 90). Só vale com `format: jpg` | Exemplo: fatura em papel de 8 x 11 polegadas, com contador no rodapé: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "url": "https://example.com/relatorio", "page_size": "Custom", "paper_width": 8, "paper_height": 11, "media_type": "print", "page_numbers": "footer-center", "page_number_format": "Página {page} de {total}" }' ``` ## Pós-processamento `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). | Campo | Descrição | | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `post_process.watermark.text` | texto da marca d'água, até 200 caracteres | | `post_process.watermark.image` | alternativa ao texto: imagem PNG ou JPEG em base64, até 1 MiB. Envie **exatamente um** entre `text` e `image` | | `watermark.font_size` | de 1 a 300 | | `watermark.color` | cor no formato `#rrggbb` | | `watermark.opacity`, `watermark.scale` | de 0 a 1 | | `watermark.rotation` | de -180 a 180 graus | | `watermark.position` | `center`, `top-left`, `top-center`, `top-right`, `left`, `right`, `bottom-left`, `bottom-center` ou `bottom-right` | | `watermark.on_top` | `true` desenha a marca por cima do conteúdo | | `post_process.pdfa` | `true` converte para PDF/A-2b (arquivamento de longo prazo) | | `post_process.protection.user_password` | **obrigatória** se houver `protection`, até 128 caracteres: senha para abrir o arquivo | | `protection.owner_password` | senha de proprietário, até 128 caracteres | | `protection.permissions` | o que o leitor pode fazer: `print`, `copy`, `modify`, `annotate`, `fill_forms`, `assemble`. Lista vazia = nenhuma permissão | `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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "html": "

Contrato

", "format": "pdf", "post_process": { "watermark": { "text": "CONFIDENCIAL", "color": "#cc0000", "opacity": 0.2, "rotation": -45, "position": "center" }, "protection": { "user_password": "senha-do-leitor", "permissions": ["print"] } } }' ``` ## Templates salvos 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. * 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. * 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. * Template ou versão inexistente: `pdf.template_not_found`. Nome de até 120 caracteres; o `html` segue os limites de e o `css` de . * 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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "template_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a", "template_version": 0, "data": { "numero": "001" } }' ``` ### Listar templates salvos `limit` (padrão e máximo 100) e `offset`. A resposta traz `items` e `total`. ### Salvar template

Fatura {{numero}}

", "css": "h1 { color: #333; }", "header_html": "", "footer_html": "" }`, }} > `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.
### Consultar template `version` opcional (padrão: a versão atual). Devolve o `template` e o conteúdo da `version`. ### Listar versões do template ### Renomear template ou salvar nova versão

Fatura {{numero}}

{{cliente}}

", "css": "h1 { color: #333; }" }`, }} > 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.
### Excluir template Apaga o template e todas as versões. As conversões já geradas não são afetadas. ## Fontes 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`. * Até **50 fontes por conta** (`pdf.font_limit`). * Uma combinação de `family` + `weight` + `style` só pode existir uma vez (`pdf.font_exists`, 409). * O formato é detectado pelo conteúdo do arquivo, não pela extensão. ### Listar fontes ### Enviar fonte 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`. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/fonts' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -F 'file=@Inter-Regular.woff2' \ -F 'family=Inter' \ -F 'weight=400' \ -F 'style=normal' ``` Depois, converta com a fonte: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "html": "

Olá

", "font_ids": ["2b3c4d5e-6f70-4a8b-9c0d-1e2f3a4b5c6d"] }' ```
### Excluir fonte Remove a fonte e o arquivo armazenado. ## Consulta e download ### Listar conversões 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. ### Status da conversão `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). ### Baixar arquivo Requer `status: completed`. O arquivo é descriptografado e descomprimido durante o download. `X-Dry-Run` não se aplica a downloads. 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. ## Batches ### Listar batches `status` do batch: `processing`, `delivering`, `completed`, `partially_failed` ou `failed`. ### Detalhe do batch ### Download ZIP do batch Disponível quando o batch termina, inclusive parcialmente se alguns itens falharem. Também aceita o link assinado `?sig=&exp=`, sem token. ## Erros ## Peculiaridades * 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. * Para PDFs recorrentes, salve um [template](#templates) em vez de reenviar o HTML a cada chamada. * `retention_days: 0` é efêmero (\~24h) — ideal para previews. * Prefira `webhook_url` a consultar o status em loop: cada consulta via API conta na franquia diária. # QR Dinâmico ## Visão geral O QR dinâmico separa a **imagem impressa** (link curto estável) do **destino** (editável sem reimprimir). Fluxo típico: 1. `POST` com `X-Dry-Run: true` para validar o payload e ver o custo. 2. `POST` definitivo com `X-Idempotency-Key` → recebe a imagem do QR, o `resource_id` e o `slug`. 3. Use a imagem retornada (ou gere a sua apontando para `full_url`, disponível em `GET /{id}`). 4. 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. `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`. ## Custos Leituras 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. ## Criação e gestão ### Criar QR dinâmico 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. 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`. ### Listar QRs Query: `limit` (padrão 20, máx. ), `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`). ### Detalhe ### Atualizar destino, nome ou design 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. ### Reativar ### Desativar Um QR desativado mostra uma página de QR inativo no redirect público e continua ocupando slot até ser excluído. ### Excluir Encerra a cobrança de armazenamento do slot. ### Verificar disponibilidade de slug ## Analytics ### Overview de analytics 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. ### Scans por período 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. ### Distribuição por dispositivo `device` é `Mobile`, `Desktop` ou `Tablet`. Assim como em scans por período, os campos de período vêm zerados. ### Histórico de scans Paginação: `limit` (padrão 50, máx. ), `offset` e `period` (padrão: últimos 30 dias). `location` vem como `Unknown` quando cidade e país são desconhecidos. ## Redirect público * **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. * **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`). * **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. * As respostas nunca são cacheadas (`Cache-Control: no-store`). ```bash curl -sS -I 'https://api.respawntech.dev/r/promo-verao' ``` ## Domínio próprio 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. O 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)). ### Cadastre o domínio ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "hostname": "qr.suaempresa.com", "validation_method": "http", "root_url": "https://www.suaempresa.com" }' ``` `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`. ### Publique o CNAME 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. | Tipo | Nome | Valor | | ------- | ----------------------------- | ------------------------------- | | `CNAME` | `qr` (ou `qr.suaempresa.com`) | o `value` do registro `routing` | 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. ### Valide o certificado 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. ### Verifique até ficar ativo A plataforma consulta o domínio periodicamente; para não esperar, force a verificação: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains/$DOMAIN_ID/refresh' \ -H "X-API-Token: $RESPAWN_API_TOKEN" ``` 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. ### Crie QRs no domínio 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`. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Cartaz da loja", "target_url": "https://loja.example.com/promo", "custom_slug": "promo-verao", "custom_domain_id": "'"$DOMAIN_ID"'" }' ``` O `full_url` do QR passa a ser `https://qr.suaempresa.com/promo-verao`. O slug só precisa ser único dentro do domínio. `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. 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. ### Listar domínios próprios ### Detalhe do domínio ### Cadastrar domínio próprio ### Editar páginas de fallback do domínio Mande `""` para limpar um dos campos. ### Verificar o domínio agora ### Remover domínio ## Comportamento do scan Por 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)). Num `PATCH`, cada campo enviado **substitui** a sua parte: `[]` limpa as regras ou as variantes, `{}` limpa os pixels e `""` limpa uma URL ou a senha. ### Regras de redirect `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. | Campo | Significado | | ------------------------ | ------------------------------------------------------------------------------------------------- | | `id` | Nome da regra (`[a-zA-Z0-9_-]`, até 40); aparece nos scans e em `rule_stats` do analytics | | `target_url` | Destino quando a regra casa (URL absoluta) | | `countries` | Países ISO 3166-1 alfa-2 em maiúsculas (`["BR", "PT"]`) | | `devices` | `mobile`, `tablet`, `desktop` | | `os` | `ios`, `android`, `windows`, `macos`, `linux`, `other` | | `languages` | Idioma preferido do visitante: `pt` casa `pt` e `pt-br`; `pt-br` só `pt-br` | | `days` | Dias da semana, `0` (domingo) a `6` (sábado), no `timezone` | | `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 | | `timezone` | Fuso IANA (`America/Sao_Paulo`); UTC se vazio | | `from`, `until` | Período em que a regra vale (data e hora RFC 3339) | Cada lista aceita até 50 valores. Dentro de uma regra as condições se somam (E); dentro de uma lista, basta um valor (OU). ### Teste A/B `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. ### Senha `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. ### Pixels `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`). ### Redirect de expirado e esgotado `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`. ### Exemplo completo Copie, troque as URLs e rode: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Campanha com regras", "target_url": "https://loja.example.com/global", "redirect_rules": [ { "id": "br-mobile", "target_url": "https://loja.example.com/br", "countries": ["BR"], "devices": ["mobile"] }, { "id": "horario-comercial", "target_url": "https://loja.example.com/atendimento", "days": [1, 2, 3, 4, 5], "start_time": "09:00", "end_time": "18:00", "timezone": "America/Sao_Paulo" } ], "ab_variants": [ { "id": "a", "target_url": "https://loja.example.com/lp-a", "weight": 50 }, { "id": "b", "target_url": "https://loja.example.com/lp-b", "weight": 50 } ], "pixels": { "ga4_measurement_id": "G-ABCD1234" }, "expired_url": "https://loja.example.com/encerrada", "limit_reached_url": "https://loja.example.com/esgotado" }' ``` Para tirar tudo depois, sem recriar o QR (troque `$QR_ID` pelo `resource_id` da criação): ```bash curl -sS -X PATCH "https://api.respawntech.dev/v1/tools/image/qr-code/dynamic/$QR_ID" \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "redirect_rules": [], "ab_variants": [], "pixels": {}, "password": "" }' ``` `GET /{id}` devolve a configuração em `scan_behavior` (`redirect_rules`, `ab_variants`, `has_password`, `pixels`, `expired_url`, `limit_reached_url`, `content`). ## Conteúdo hospedado 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`. | `type` | Campo | O scan mostra | | ----------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `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 | | `file` | `file` (`url`, `name`) | Abre o arquivo ou PDF hospedado (URL absoluta) | | `links` | `links` (`title`, `description`, `avatar_url`, `links[]` com `label` e `url`) | Link na bio, de 1 a 50 links | | `menu` | `menu` (`title`, `currency`, `sections[]` → `items[]` com `name`, `description`, `price`) | Cardápio, até 30 seções e 300 itens | | `wifi` | `wifi` (`SSID`, `Password`, `Security`, `Hidden`) | Nome da rede e senha para copiar | | `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 | Os 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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Link na bio", "content": { "type": "links", "links": { "title": "Loja Exemplo", "links": [ { "label": "Site", "url": "https://loja.example.com" }, { "label": "Instagram", "url": "https://instagram.com/lojaexemplo" } ] } } }' ``` Exemplo de cartão de contato: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Cartão da Ana", "content": { "type": "vcard", "vcard": { "FirstName": "Ana", "LastName": "Souza", "Org": "Loja Exemplo", "Phone": "+5511999990000", "Email": "ana@loja.example.com" } } }' ``` ## Molduras e impressão `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`. * `frame`: `text` (até 32 caracteres; encolhe para caber), `color`, `text_color` (hex) e `position` (`top` ou `bottom`). * `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. * `eps`: formato vetorial novo, com a moldura e fonte Helvetica-Bold com acentos. Sai sem logo. No 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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Cartaz A5", "target_url": "https://loja.example.com/promo", "response_types": ["pdf", "eps"], "frame": { "text": "Escaneie aqui", "color": "#111111", "text_color": "#FFFFFF", "position": "bottom" }, "dpi": 300 }' ``` ## Organização, edição em massa e lote ### Pastas e tags `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`. ### Listar pastas ### Criar pasta O nome tem de 1 a 80 caracteres e é único na conta (`dynamic_qr.folder_taken`). ### Renomear pasta ### Excluir pasta ### Editar vários QRs de uma vez 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`. ### Criar QRs em lote Até 1.000 linhas por lote, de um destes jeitos: * 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`); * corpo `text/csv` direto, com `folder_id` e `tags` como parâmetros de query. 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`: ```csv name,target_url,custom_slug,tags Loja Centro,https://loja.example.com/centro,loja-centro,varejo|sp Loja Norte,https://loja.example.com/norte,loja-norte,varejo ``` ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/batches?tags=campanha' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'X-Idempotency-Key: 7f1c2e84-5b3a-4d6c-9e01-a2b3c4d5e6f7' \ -H 'Content-Type: text/csv' \ --data-binary @lojas.csv ``` 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`). ### Listar lotes ### Detalhe do lote `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). ### Exportar scans em CSV 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`. ### Apagar o histórico de scans 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. ## Erros ## Peculiaridades e limites * URLs maliciosas são bloqueadas automaticamente (`dynamic_qr.malicious_url`). * Proteção anti-loop: o destino não pode apontar para o próprio domínio de redirect. * O primeiro QR não ocupa slot pago; os demais ocupam slots em grupos de 3. * `skip_image_generation: true` reduz o custo quando você gera a imagem do QR por conta própria. * Slug: 3–50 caracteres `[a-zA-Z0-9-]`. Listagens: até itens por página. # Realtime ## Como funciona O Realtime leva mensagens do seu servidor, ou de outros clientes, para os navegadores conectados, sem você manter servidores de WebSocket. * **Canal:** um nome livre de até 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. * **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`). * **`connect_url`:** o endereço WebSocket para onde o cliente conecta, devolvido na mesma resposta do token e já com o `?token=`. * **Mensagem:** `data` JSON publicada num canal, com `id`, `timestamp` (milissegundos Unix) e o `client_id` de quem publicou. * **Presença:** a lista de quem está conectado e "presente" num canal, com dados livres de cada membro (um apelido, um status). O fluxo é sempre o mesmo: O navegador pede um token ao **seu servidor**, que já sabe quem é o usuário. 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`. O navegador conecta em `connect_url`, assina canais e publica. Se a conexão cair, o navegador pede um token novo e [reconecta](#reconexao), recuperando as mensagens perdidas com `rewind`. 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. A 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. ## Token e capabilities ### Emitir token de conexão * `capabilities` (obrigatório): objeto de padrão de canal para a lista de operações permitidas, até 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`. * `client_id` (opcional, até 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. * `ttl_seconds` (opcional): validade do token, de até , padrão . 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. 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**. Dê 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. 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`. ## Conexão pelo navegador 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. ```js import { createServer } from 'node:http'; createServer(async (req, res) => { if (req.url !== '/realtime-token') return res.writeHead(404).end(); const api = await fetch('https://api.respawntech.dev/v1/tools/realtime/tokens', { method: 'POST', headers: { 'X-API-Token': process.env.RESPAWN_API_TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: 'user-42', // o usuário autenticado capabilities: { 'chat:*': ['subscribe', 'publish', 'presence'] }, }), }); const { data } = await api.json(); // token, expires_at, connect_url e retry_after_ms seguem juntos para o navegador res.writeHead(api.status, { 'Content-Type': 'application/json' }).end(JSON.stringify(data)); }).listen(3000); ``` ```python import os import requests from flask import Flask, jsonify app = Flask(__name__) @app.post("/realtime-token") def realtime_token(): api = requests.post( "https://api.respawntech.dev/v1/tools/realtime/tokens", headers={"X-API-Token": os.environ["RESPAWN_API_TOKEN"]}, json={ "client_id": "user-42", # o usuário autenticado "capabilities": {"chat:*": ["subscribe", "publish", "presence"]}, }, timeout=30, ) # token, expires_at, connect_url e retry_after_ms seguem juntos para o navegador return jsonify(api.json()["data"]), api.status_code ``` No 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). ```html Realtime
    ``` ## Protocolo 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}$`. ### Do cliente para o servidor | `action` | Campos | O que faz | | ------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `subscribe` | `channel`, `rewind?` | Assina o canal (exige `subscribe` no token). Com `rewind` (número de mensagens, até ), reenvia as últimas mensagens do canal antes das novas. Responde `attached`. | | `unsubscribe` | `channel` | Deixa de receber o canal. Responde `detached`. | | `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. | | `presence` | `op`, `channel`, `data?`, `id?` | Operações de [presença](#presenca): `op` é `enter`, `update`, `leave` ou `get`. | ### Do servidor para o cliente | `action` | Campos | Quando | | ----------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `connected` | `connection_id` | A conexão foi aceita; já pode assinar. | | `attached` | `channel` | A assinatura do canal está ativa (depois do `rewind`, se pedido). | | `detached` | `channel` | A assinatura foi encerrada. | | `message` | `channel`, `id`, `name?`, `data`, `timestamp`, `client_id?` | Uma mensagem do canal, inclusive as que você mesmo publicou. `timestamp` é um inteiro em milissegundos Unix. | | `ack` | `id`, `channel?` | A publicação (ou operação de presença) com esse `id` foi aceita. | | `presence` | `op`, `channel`, `member` ou `members` | Evento de presença (`enter`, `update`, `leave`) ou resposta ao `get` (`sync`). | | `error` | `code`, `message`, `channel?`, `id?` | Uma operação falhou. Os `code` estão em [Erros](#erros). A conexão continua aberta. | Exemplo de troca: ```json ← { "action": "connected", "connection_id": "c_8f3a1d" } → { "action": "subscribe", "channel": "chat:lobby", "rewind": 20 } ← { "action": "attached", "channel": "chat:lobby" } → { "action": "publish", "channel": "chat:lobby", "id": "m-1", "data": { "text": "Olá!" } } ← { "action": "message", "channel": "chat:lobby", "id": "1790338000000-0", "data": { "text": "Olá!" }, "timestamp": 1790338000000, "client_id": "user-42" } ← { "action": "ack", "id": "m-1" } ``` O `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`. ### Close codes | Código | Significado | O que fazer | | -------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | | `1012` | O nó está reiniciando (deploy ou manutenção). | Reconecte já, com um pequeno jitter. | | `1013` | O nó está cheio. | Reconecte com backoff exponencial. | | `4001` | Token inválido ou expirado (`realtime.token_invalid`). | Emita um token novo e reconecte; repetir o mesmo token não adianta. | | `4003` | Realtime desativado para a conta (`realtime.disabled`). | Não reconecte. | | `4029` | A conta atingiu o limite de conexões (`realtime.connection_limit`). | Reconecte com backoff exponencial. | | Qualquer outro | Queda de rede ou erro inesperado. | Reconecte com backoff exponencial. | ## SSE: leitura sem WebSocket 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`: ```text GET https:///v1/sse?token=&channel=chat:lobby&channel=alerts&rewind=20 ``` * Informe cada canal em um `channel=` (ou vários em `channels=a,b`), até . O token precisa de `subscribe` em todos. * Cada `data:` é um frame JSON igual ao do WebSocket (`connected`, `attached`, `message`, `presence`, `error`); linhas `: ping` mantêm a conexão viva. * 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. * Para publicar, use a [API](#publicar) ou um WebSocket. ```js const url = new URL(connectUrl); // o connect_url da resposta do token const sse = new EventSource( `https://${url.host}/v1/sse?token=${encodeURIComponent(token)}&channel=chat:lobby&rewind=20`, ); sse.onmessage = (event) => { const frame = JSON.parse(event.data); if (frame.action === 'message') console.log(frame.data); }; ``` ## Presença 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. | Frame do cliente | Exige | Efeito | | -------------------------------------------------------------------------------- | ----------- | --------------------------------------------- | | `{"action":"presence","op":"enter","channel":"chat:lobby","data":{…},"id":"p1"}` | `presence` | Entra na presença; responde `ack`. | | `{"action":"presence","op":"update","channel":"chat:lobby","data":{…}}` | `presence` | Atualiza os dados do membro. | | `{"action":"presence","op":"leave","channel":"chat:lobby"}` | `presence` | Sai da presença. | | `{"action":"presence","op":"get","channel":"chat:lobby","id":"p2"}` | `subscribe` | Responde `op: "sync"` com todos os `members`. | Os 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é membros; acima disso, `realtime.presence_limit`. Se um nó cair sem avisar, os membros dele somem da lista em até 30 segundos. ### Listar a presença do canal Devolve os membros do canal neste instante. É útil para mostrar "quem está online" a partir do seu servidor, sem abrir uma conexão. ## Reconexão 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. ### Backoff Use backoff exponencial **com jitter** (um valor aleatório até o teto da tentativa) e teto de : 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`. ### Token novo e `retry_after_ms` 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. ### Recuperar mensagens com `rewind` 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. Escolha o `rewind` pelo tempo que a conexão costuma ficar fora (até mensagens). Se o canal teve mais mensagens que isso durante a queda, a diferença se perdeu: o histórico guarda só as últimas. ## Histórico Cada canal guarda as últimas mensagens publicadas, por até . O `rewind` usa esse histórico; para consultá-lo a partir do seu servidor, use a API. ### Consultar histórico do canal 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 (o padrão é o máximo). É uma leitura medida: entra na [franquia diária de leituras](/cobranca-e-limites#franquia-diaria) da conta. ### Publicar mensagem pelo servidor Publicar pela API é útil para eventos que nascem no seu backend (um pedido pago, um alerta). A mensagem chega aos assinantes como qualquer outra. * `data`: qualquer JSON, até (somados ao `name`). * `name` (opcional, até 128 caracteres): classifica a mensagem. * `client_id` (opcional): o remetente que os assinantes veem. A resposta traz `id`, `timestamp` (milissegundos Unix) e `charged`: `true` quando a mensagem passou da franquia mensal e foi cobrada. Com `X-Dry-Run: true`, a API valida o canal e o tamanho, não publica nem cobra, e responde `200` com a estimativa: ```json { "success": true, "data": { "cost_units": 0, "within_quota": true, "free_used": 18250, "free_monthly": 200000, "would_charge": false } } ``` `cost_units` usa a escala do backend (10.000 unidades = 1 crédito) e é `0` dentro da franquia. ## Canais e estatísticas ### Listar canais ativos 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 , padrão 50) e `offset`; a resposta traz `total` e, quando há mais páginas, `next_offset` para a chamada seguinte. ### Consultar uso e conexões 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`. ## Webhooks de canais 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). ### Consultar o webhook de canais 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`. ### Configurar o webhook de canais * `url`: URL pública `http` ou `https`; endereços de rede interna ou privada são recusados. * `events`: ao menos um dos eventos da tabela abaixo. * `channels`: padrões de canal (nome, prefixo com `*` ou `*`), até . Vazio quer dizer todos os canais, **menos** `channel.message`: as mensagens são opt-in e exigem ao menos um padrão. * `enabled` (opcional, padrão `true`): `false` pausa as entregas sem apagar a configuração. A chamada cria o webhook ou substitui o atual, e o `secret` é mantido. A configuração é lida em cache por até 30 segundos. ### Trocar o secret do webhook Gera um novo `secret` (e a `standard_key` correspondente). O anterior deixa de valer; atualize o seu receptor logo em seguida. ### Remover o webhook de canais Apaga a configuração; nenhum evento é mais enviado. ### Eventos ### Entrega A plataforma faz um `POST` JSON na sua URL: ```json { "id": "evt_5b0c9a7e-3c1f-4a52-9d57-2a6f1f0e8a11", "type": "presence.member_added", "timestamp": 1790338000000, "account_id": "7a1d4c8e-2f6b-4e9a-b3c5-0d8f1a6e4b27", "data": { "channel": "chat:lobby", "member": { "connection_id": "c_8f3a1d", "client_id": "user-42", "data": { "nick": "ana" }, "timestamp": 1790338000000 } } } ``` `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. * **Assinatura:** Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature`), com a `standard_key` da configuração; veja [Validar a assinatura](/webhooks#verificar-assinatura). * **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é . * **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. ## SDK JavaScript 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. ```bash npm install @respawn/realtime ``` ### No servidor `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: ```ts import { RealtimeRest } from '@respawn/realtime'; const rest = new RealtimeRest({ apiKey: process.env.RESPAWN_API_TOKEN!, baseUrl: 'https://api.respawntech.dev', }); // rota que o navegador chama (Express): devolve token, expires_at e connect_url app.post('/realtime/token', async (req, res) => { res.json( await rest.createToken({ client_id: req.user.id, capabilities: { 'room:*': ['subscribe', 'publish', 'presence'] }, ttl_seconds: 600, }), ); }); await rest.publish('room:1', 'pedido', { id: 42, status: 'pago' }); const history = await rest.history('room:1', { limit: 20 }); const members = await rest.presence('room:1'); ``` ### No navegador `Realtime` pede o token ao seu servidor por `authUrl` (um `POST` com cookies) ou por `authCallback`: ```ts import { Realtime } from '@respawn/realtime'; const rt = new Realtime({ authUrl: '/realtime/token' }); rt.on('state', (state, err) => console.log('conexão:', state, err?.code)); const room = rt.channel('room:1'); await room.subscribe((m) => console.log(m.name, m.data), { rewind: 20 }); await room.publish('chat', { text: 'oi' }); await room.presence.watch((members) => render(members)); await room.presence.enter({ nick: 'ana' }); ``` Os 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). ### React Com `@respawn/realtime/react`: ```tsx import { useState } from 'react'; import { Realtime } from '@respawn/realtime'; import { RealtimeProvider, useChannel, usePresence } from '@respawn/realtime/react'; const rt = new Realtime({ authUrl: '/realtime/token' }); export function App() { return ( ); } function Room() { const [msgs, setMsgs] = useState<{ id: string; data: unknown }[]>([]); const { publish } = useChannel('room:1', (m) => setMsgs((all) => [...all, m]), { rewind: 20 }); const { members } = usePresence('room:1', { nick: 'ana' }); return (

    {members.length} online

      {msgs.map((m) =>
    • {JSON.stringify(m.data)}
    • )}
    ); } ``` ## Limites | Limite | Valor | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | Mensagens grátis por mês (por conta) | | | Tamanho de uma mensagem (`data`) | | | Tamanho do nome do canal | caracteres | | Canais assinados por conexão | | | Publicações por segundo (por conta) | | | Conexões simultâneas (por conta) | | | Membros na presença de um canal | | | Validade do token | (padrão), de até | | Padrões de canal por token | até | | Retenção do histórico | | | Mensagens guardadas e por `rewind` | até | | Canais por página (`GET /channels`) | até | | Padrões de canal do webhook | até | Os 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. ## Erros 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)). Erros de validação comuns (`validation.failed`, `not_found`) estão em [Erros](/erros). ## Preço A 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`. # Verificação de URL ## Visão geral 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. ## Custos 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. ## Endpoints ### Verificar URL Query: `url` (obrigatório, URL absoluta). Resultados são mantidos em cache por 1 hora. 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. ### Favicon de um domínio Query: `domain` (obrigatório). ## Erros # Página web para Markdown ## Visão geral 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`. ## Custos 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). ## Endpoints ### Converter página em Markdown `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`. 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`. 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`). ## Erros # Webhook Relay ## Visão geral 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). * **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). * **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. * **Mensagem:** um evento que você publica (`event_type` + `payload`). * **Entrega:** uma mensagem a caminho de um endpoint. Uma mensagem para 3 endpoints gera 3 entregas, cada uma com seus retries. * **Tentativa:** cada chamada HTTP de uma entrega, registrada com status, latência e o começo da resposta. * **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). Todas 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). ## Primeiros passos **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. **Teste a conexão** com o [ping de teste](#webhooks-endpoints-test): um evento `webhook.test` grátis. **Envie uma mensagem** com `event_type` e `payload`. Use `X-Dry-Run: true` antes para ver quantas entregas ela gera e quanto custa. **Verifique a assinatura** no receptor e responda `2xx` rápido. Veja os exemplos em [Validar a assinatura](/webhooks#verificar-assinatura). **Acompanhe** as entregas no [log de mensagens](#mensagens) e reenvie o que falhou. ## Custos A 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. A 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. 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. ### Consultar uso do mês 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)). ## Projetos 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. * Toda conta tem um **projeto padrão** (`is_default: true`, chamado `Default`), criado no primeiro uso. Ele não pode ser excluído. * `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. * 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. * As listagens (`GET /endpoints`, `/sources`, `/event-types`, `/routes` e `/messages`) aceitam `?project_id=` como filtro; sem ele, trazem todos os projetos. * 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`). * Limites e cobrança continuam por conta: veja [Limites](#limites). Por padrão não há limite de projetos. ### Listar projetos Paginado e com busca: `q` procura no nome (sem diferenciar maiúsculas), `page` começa em 1 e `page_size` vai até (padrão ). 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). ### Criar projeto `name` (obrigatório) tem até caracteres; `description`, até . ### Consultar projeto ### Alterar projeto Envie só os campos que quer mudar. ### Excluir projeto 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`. ### Duplicar projeto Cria um projeto novo com a mesma descrição e cópias de tudo, já ligadas entre si: * **Endpoints** com **secrets novos** (mesma URL, eventos, headers, limite de envio, formato e estado ativo; o histórico de falhas não é copiado). * **Fontes** com **`ingest_url` novas** (mesmas configurações e o mesmo secret do provedor). Cadastre as URLs novas no provedor. * Rotas, tipos de evento e o layout do canvas. 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. ## Endpoints ### Criar endpoint * `url` (obrigatório): URL pública `http` ou `https`. Endereços privados, locais ou de metadados de nuvem são recusados (`webhook.invalid_url`). * `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. * `project_id`: projeto do endpoint (padrão: o projeto padrão). Um endpoint não muda de projeto. * `format`: `raw` (padrão), `slack`, `discord` ou `teams`. Veja [Slack, Discord e Teams](#formatos). * `custom_headers`: até 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. * `rate_limit_per_sec`: teto de envios por segundo para este endpoint (`0` = sem teto próprio). * O `secret` vem só nesta resposta; depois, use [Revelar secret](#webhooks-endpoints-secret). Até endpoints por conta, somando todos os projetos. ### Listar endpoints Filtro opcional por `project_id`. Um endpoint desativado automaticamente traz `is_active: false` e `disabled_reason`; um que está falhando traz `failing_since`. ### Consultar endpoint ### Alterar endpoint Envie só os campos que quer mudar (inclusive `format`). `is_active: true` reativa um endpoint desativado e zera a sequência de falhas. ### Excluir endpoint As entregas pendentes para o endpoint são canceladas. ### Revelar secret `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. ### Trocar secret Durante as entregas levam duas assinaturas em `webhook-signature` (secret novo e antigo), para você trocar o secret do receptor sem perder eventos. ### Enviar ping de teste 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. ### Reenviar entregas com falha Reenvia para este endpoint todas as entregas que terminaram com falha (`failed`, `exhausted` ou `canceled`) desde `since` (RFC 3339), até por chamada. Use depois de corrigir um receptor que ficou fora do ar. Cada entrega reenviada é cobrada como nova. ### Slack, Discord e Teams 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: * O corpo vira `{"text": "…"}` (Slack e Teams) ou `{"content": "…"}` (Discord), com `Content-Type: application/json`. * 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. * Textos longos são cortados com `…` para caber no limite da plataforma: caracteres no Slack, no Discord e no Teams. * 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. A mensagem guardada no log não muda: o log, o reenvio e a recuperação funcionam igual aos endpoints `raw`. ## Mensagens ### Enviar mensagem * `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 `*`). * `payload` (obrigatório): qualquer JSON, até . Se o tipo de evento tiver [JSON Schema](#tipos-de-evento) no projeto, o payload precisa segui-lo (`webhook.payload_invalid`, sem cobrança). * `project_id` (opcional): projeto da mensagem (padrão: o projeto padrão). * `endpoint_ids` (opcional): restringe o envio a estes endpoints, entre os do projeto que assinam o evento. * `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. * Com `X-Dry-Run: true`, nada é enviado nem cobrado: a resposta mostra `deliveries`, `free_deliveries` e `charged_units`. O receptor recebe o envelope `{ "event_id", "type", "timestamp", "payload" }` descrito em [Webhooks](/webhooks#corpo); `event_id` é o `message_id` e `webhook-id` é `msg_`. ### Enviar mensagens em lote Até 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`. ### Listar mensagens 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 . * `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). * `event_type` e `endpoint_id`: filtros exatos. * `failed=true`: só mensagens com ao menos uma entrega `failed`, `exhausted` ou `blocked_no_credits`. * `q`: busca, de 1 a 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 . ### Consultar mensagem 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. | `status` da entrega | Significado | | -------------------- | -------------------------------------------------------------------------------------------------------- | | `pending` | Aguardando a próxima tentativa (`next_attempt_at`). | | `sending` | Tentativa em andamento. | | `succeeded` | O endpoint respondeu `2xx`. | | `exhausted` | Todas as tentativas falharam. Pode ser reenviada. | | `failed` | Parou de vez: endpoint respondeu `410 Gone`, foi desativado ou a URL ficou inválida. Pode ser reenviada. | | `canceled` | Cancelada, por exemplo porque o endpoint foi excluído com a entrega pendente. | | `blocked_no_credits` | Mensagem recebida de um provedor sem saldo para a entrega (veja [Sem créditos](#sem-creditos)). | ### Reenviar mensagem Sem body, envia a mensagem de novo para os endpoints cuja entrega terminou com falha (`failed`, `exhausted` ou `canceled`) e que estão ativos. 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. ## Tipos de evento 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. ### Declarar tipo de evento `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é tipos por conta, somando todos os projetos. ### Listar tipos de evento Filtro opcional por `project_id`. ### Excluir tipo de evento Os envios desse tipo continuam aceitos, sem validação de schema. ## Receber webhooks de terceiros 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. | `provider` | Como a requisição é verificada | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `stripe` | Header `Stripe-Signature` com o signing secret do Stripe (`whsec_…`), tolerância de . | | `github` | Header `X-Hub-Signature-256` com o secret do webhook do GitHub. | | `shopify` | Header `X-Shopify-Hmac-Sha256` com o secret do app Shopify. | | `hmac_generic` | Header `X-Webhook-Signature: sha256=` (HMAC-SHA256 do corpo). | | `standard_webhooks` | Headers `webhook-id`, `webhook-timestamp` e `webhook-signature`. | | `basic_auth` | Header `Authorization: Basic …`; o secret é `usuario:senha`. | | `none` | Sem verificação: qualquer um com a URL pode enviar. Evite em produção. | `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. ### Criar fonte de ingestão * `name` e `provider` (obrigatórios); `project_id` é opcional (padrão: o projeto padrão). * `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"`). * `event_type_path`: caminho no JSON recebido de onde vem o tipo do evento (ex.: `type`, `data.object.status`). Sem ele, o tipo é `ingest`. * `dedupe_key_path`: caminho do identificador do evento (ex.: `id`). Um reenvio do provedor com a mesma chave responde `200` sem gerar novas entregas. * `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. * Cadastre a `ingest_url` da resposta no provedor. Até fontes por conta, somando todos os projetos. ### Listar fontes Filtro opcional por `project_id`. ### Consultar fonte ### Alterar fonte 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. ### Excluir fonte As rotas da fonte são excluídas junto e a `ingest_url` para de responder. ### Trocar URL de ingestão Gera uma nova `ingest_url`; a anterior deixa de funcionar na hora. Atualize a URL no provedor logo em seguida. ### Definir secret do provedor Um secret vazio só é aceito com `provider: "none"`. ### Consultar último evento recebido 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. ### Enviar evento de teste pela fonte 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`. É grátis e fica fora da franquia mensal. Não verifica assinatura nem deduplica, mas conta no limite de requisições por minuto da fonte. ### Criar rota 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é ; vazio, a rota leva todos os eventos da fonte. Até rotas por fonte. ### Listar rotas Filtros opcionais por `source_id` e `project_id`. ### Alterar rota Muda os tipos de evento da rota (lista vazia = todos) ou a pausa com `is_active: false`. Envie só os campos que quer mudar. ### Excluir rota ### Receber evento de um provedor É 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: | Status | Quando | | ------ | ------------------------------------------------------------------------------------ | | `200` | Aceito, duplicado (reenvio do provedor) ou sem rota para o tipo. | | `401` | Assinatura inválida, ou secret do provedor ainda não definido; nada é gravado. | | `404` | Fonte desconhecida ou desativada, ou ferramenta não liberada na conta. | | `429` | Mais de requisições por minuto para a fonte. | | `5xx` | Falha ao gravar; o provedor deve tentar de novo. | ### Exemplo: Stripe [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. [Crie uma fonte](#webhooks-sources-create) com `provider: "stripe"`, `event_type_path: "type"` e `dedupe_key_path: "id"`, ainda sem o secret. 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). [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). Os 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. ### Sem créditos 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 , elas terminam como `failed`. ## Canvas do dashboard 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). ### Consultar mapa do fluxo 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. ### Consultar layout do canvas ### Salvar layout do canvas `positions` guarda as coordenadas de cada cartão: chaves `platform`, `source:` ou `endpoint:`. Cartões sem posição são arrumados automaticamente pelo dashboard. ## Alertas 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). | Tipo | Quando | | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `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. | | `webhook_endpoint_disabled` | A desativação automática desliga um endpoint que falha há (não quando você o pausa nem por `410 Gone`). | | `webhook_quota` | As entregas do mês atingem da franquia mensal, uma vez por limiar e por mês. | Os alertas nunca atrasam nem bloqueiam um envio. ## Entrega e retries 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 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. ## API legada da conta 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)). ## Erros Erros de validação comuns (`validation.failed`, `not_found`) e de saldo (`billing.insufficient_credits`) estão em [Erros](/erros). ## Limites | Limite | Valor | | ------------------------------------------ | -------------------------------------------------- | | Entregas grátis por mês (por conta) | | | Tamanho do `payload` / corpo recebido | | | Projetos por conta | | | Endpoints por conta | | | Headers customizados por endpoint | | | Tipos de evento por conta | | | Mensagens por lote | | | Entregas por recuperação | | | Fontes por conta | | | Rotas por fonte | | | Tipos de evento por rota | | | Requisições recebidas por fonte | por minuto | | Espera por créditos (`blocked_no_credits`) | | | Retenção do log de mensagens | | | Janela da busca no corpo das mensagens | | Os limites por conta somam todos os projetos. Os valores são os padrões da plataforma e podem ser ajustados por ela. # AI agents (MCP) 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. The MCP server is being rolled out gradually. Until it is active in your environment, `/mcp` responds `404` with the code `mcp.disabled`. ## Connection | | | | -------------- | ------------------------------------------ | | URL | | | Transport | Streamable HTTP, stateless, JSON responses | | Authentication | `Authorization: Bearer ` | | Required scope | `mcp:use`, plus each tool's scopes | Never 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. ## Create a key for the agent In the [dashboard](https://dashboard.respawntech.dev), under **APIs**, create a token just for the agent. The presets fill in scopes and controls: * **Support agent**: knowledge base search and balance lookup. * **Full agent**: every tool. * **Read-only**: lookups only. An agent key should have a **daily spend limit** (suggestion: ) and an **expiration date**. See [Key controls](/en/autenticacao#controles-da-chave). ## Configure the client Clients that only speak stdio use the `mcp-remote` bridge, as in the last example. ## Tools 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. **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. ## Connection parameters 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". For example, a support agent that only reads the knowledge base, with responses in Portuguese: Parameters you repeat across connections can become a **profile** in the dashboard (`profile=`), and a key can have a default profile. ## Costs and spend control * 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). * 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. * `simulate=true` in the URL makes every paid tool return only the estimate. It's the way to test an agent without spending. * 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`. * The **panic button** in the dashboard cuts off access for every agent on the account immediately (`mcp.agents_disabled`). * The dashboard statement breaks down spending by channel (API, MCP, dashboard) and by key. ## Idempotency Agents often repeat calls. Tools that create or change something accept `idempotency_key`; without it, an identical call (same key, tool and arguments) within returns the previous result without running or charging again (`_meta["tech.respawn/replayed"]`). ## Limits | Limit | Value | | -------------------- | --------------------------------------------------------------------------- | | Requests | per minute per key, shared with the API | | Request body | | | Tool result | | | `tools_search` calls | per minute | ## Errors 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"}}`. Tool errors are the same as in the API; see [Error codes](/en/erros). # Authentication ## API token Every route under `/v1/tools/*` requires an **API token**, created in the [dashboard](https://dashboard.respawntech.dev) under **APIs**. | Method | Example | | ------------------------------ | ------------------------------------ | | Dedicated header (recommended) | `X-API-Token: rsk_live_...` | | Authorization | `Authorization: ApiKey rsk_live_...` | | Authorization (alternative) | `Authorization: Token rsk_live_...` | 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. ## Account requirements Before any call to the tools, the account must have: * **A verified email** — otherwise, `auth.email_not_verified` (403). * **A non-negative balance** — a suspended account returns `billing.user_bankrupt` (402). * **Enough credits** — paid operations without balance return `billing.insufficient_credits` (402). ## Scopes 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. 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. ### Recommended combinations | Use case | Suggested scopes | | -------------------------------------------- | ---------------------------------------------------------------- | | Create and manage QR codes | `tools:qr:dynamic:manage` | | Read-only QR dashboard | `tools:qr:dynamic:read` | | PDF pipeline | `tools:pdf:manage` + `tools:pdf:read` | | Check URLs before displaying them | `tools:safety:read` | | Assistant that searches the knowledge base | `tools:rag:query` | | Upload and maintain knowledge base documents | `tools:rag:manage` | | [AI agent (MCP)](/en/agentes-de-ia) | `mcp:use` + the scopes of the tools it will use + `account:read` | | Full integration | `tools:manage` (simpler, less secure) | ## Key controls 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): * **Expiration**: after the date, the token stops working (`auth.unauthorized`). * **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. * **Knowledge base restriction**: the token only sees the selected knowledge bases; others respond `not_found`, and it cannot create new ones (`forbidden`). * **Presets**: when creating the key, "Read-only", "Support agent" and "Full agent" fill in scopes and controls in one go. Expiration, limit and knowledge bases can be changed later; scopes cannot. To change scopes, create another key. ## Authentication errors ## Dashboard vs. API token * **Dashboard** — browsing the dashboard does not consume the daily free quota or debit metered reads. * **API token** — metered reads share the [daily free quota](/en/cobranca-e-limites#franquia-diaria) of requests ( on premium accounts). After that, each read debits credits. ## Example Create a QR code with dry-run to validate the token, the scopes and the cost without debiting anything: # Billing and limits ## Credit model * Your balance is shown in **credits** in the dashboard, and every amount in this documentation is in credits. * Paid operations are debited on confirmation — never on dry-run. * 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). * Each token can have a [daily spend limit](/en/autenticacao#controles-da-chave): going over it responds `billing.spend_limit_reached`. Creation costs may include add-ons (custom slug, design, PDF retention…). Run a dry-run with the real payload to see the exact total. ## Dry-run and cost preview | Header | Effect | | ---------------------- | ---------------------------------------------------------------- | | `X-Dry-Run: true` | Validates and calculates the cost without persisting or debiting | | `X-Preview-Cost: true` | Alias for `X-Dry-Run` | | `X-Include-Cost: true` | Includes `meta.cost` in the response | Dry-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. The [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. ### Quote for a paid operation On creation and conversion `POST`s, `data` contains an itemized quote: ```json { "success": true, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "data": { "total_cost_credits": 15000, "total_cost_units": 15000, "items": [ { "description": { "key": "billing.pdf.convert_base" }, "cost_credits": 10000, "type": "base" }, { "description": { "key": "billing.pdf.retention.7" }, "cost_credits": 5000, "type": "storage" } ], "is_monthly_recurring": false, "currency": "CREDITS" } } ``` 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. For Dynamic QR, the quote also includes `slot_usage` (slot usage and whether the creation will open a new paid slot) and `recurring_cost`. ### Preview of a metered read 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: ```json { "success": true, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "data": { "operation": "qr_dynamic.analytics", "metering": { "within_quota": true, "quota_limit": 2000, "quota_used": 125, "quota_remaining": 1875, "cost_units": 300, "would_charge": false } }, "meta": { "cost": { "units": 300, "credits": 0.03, "charged": false, "within_quota": true, "quota_limit": 2000, "quota_used": 125, "quota_remaining": 1875 } } } ``` With `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). ## Idempotency Send `X-Idempotency-Key` (UUID recommended) on any tool route: * the key is valid for ****; * same key + same payload → the original response is reused, with no new charge; * same key + different payload → `idempotency.conflict` (409). Always use it on creation and conversion `POST`s so network retries don't charge twice. Exception: [Knowledge base](/en/ferramentas/base-de-conhecimento#limites) routes ignore the header because they are already idempotent (repeated content is neither reindexed nor charged). ## Rate limit Each API token can make ** requests per minute** on `/v1/tools/*` routes. Every response reports the current status: ```http X-RateLimit-Limit: 60 X-RateLimit-Remaining: 42 X-RateLimit-Reset: 1712345678 ``` When exceeded, the API returns `rate_limit.exceeded` (429). Wait until `X-RateLimit-Reset` before retrying. ## Retrying with backoff Only retry what can succeed on the next attempt: * **`429`**: wait until the time in `X-RateLimit-Reset` (Unix seconds). * **`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. * **Other `4xx`** (validation, scope, balance): don't retry. The response won't change until you fix the request. On 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. ```js // Retries 429, 5xx and network failures; returns other errors right away. async function withRetry(send, attempts = 5) { for (let attempt = 1; ; attempt++) { let response; try { response = await send(); } catch (error) { if (attempt === attempts) throw error; // network failure } if (response && response.status !== 429 && response.status < 500) return response; if (attempt === attempts) return response; const reset = Number(response?.headers.get('X-RateLimit-Reset')); const wait = response?.status === 429 && reset ? Math.max(0, reset * 1000 - Date.now()) : Math.min(2 ** (attempt - 1) * 1000, 30_000) * (0.5 + Math.random()); await new Promise((resolve) => setTimeout(resolve, wait)); } } const key = crypto.randomUUID(); // the same key on every attempt const response = await withRetry(() => fetch(url, { method: 'POST', headers: { 'X-API-Token': process.env.RESPAWN_API_TOKEN, 'X-Idempotency-Key': key, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }), ); ``` ```python import os, random, time, uuid import requests # Retries 429, 5xx and network failures; returns other errors right away. def with_retry(send, attempts=5): for attempt in range(1, attempts + 1): try: response = send() except (requests.ConnectionError, requests.Timeout): if attempt == attempts: raise response = None if response is not None and response.status_code != 429 and response.status_code < 500: return response if attempt == attempts: return response reset = response.headers.get("X-RateLimit-Reset") if response is not None else None if response is not None and response.status_code == 429 and reset: wait = max(0, int(reset) - time.time()) else: wait = min(2 ** (attempt - 1), 30) * (0.5 + random.random()) time.sleep(wait) key = str(uuid.uuid4()) # the same key on every attempt response = with_retry(lambda: requests.post( url, json=payload, headers={"X-API-Token": os.environ["RESPAWN_API_TOKEN"], "X-Idempotency-Key": key}, timeout=30, )) ``` ## Daily free read quota Reads made with an **API token** (listings, status, analytics, downloads…) count toward a daily free quota shared across all tools: | Item | Value | | ------------------ | -------------------------------------- | | Free reads per day | **** | | Premium accounts | **** | | Reset | | Every metered read returns your position in the quota: ```http X-API-Quota-Limit: 2000 X-API-Quota-Used: 847 X-API-Quota-Remaining: 1153 ``` Requests made from the [dashboard](https://dashboard.respawntech.dev) do **not** count toward the quota and are not charged. ### Metered reads Once the quota is used up, each API read debits: Write operations with no cost of their own (`PATCH`, activate, deactivate, `DELETE`) and creation dry-runs are always free. The quota covers most integrations. To track state changes, prefer [webhooks](/en/webhooks) over repeatedly polling the status. ## Timeouts * Most routes: ****. * File downloads: ****. # Error codes ## Success response ```json { "success": true, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "data": {}, "meta": {} } ``` | Field | Description | | ---------- | ------------------------------------------------ | | `success` | `true` on OK responses | | `trace_id` | Correlation ID (also in the `X-Trace-Id` header) | | `data` | Resource or result | | `meta` | Optional — cost (`meta.cost`), pagination, etc. | With `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`. ## Error format ```json { "success": false, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "error": { "code": "billing.insufficient_credits", "message": "Créditos insuficientes para esta operação.", "details": { "required": 100, "available": 50 } } } ``` | Field | Description | | --------------- | ----------------------------------------------------------------------------------------------- | | `error.code` | Stable identifier — handle errors by it, never by `message` | | `error.message` | Human-readable text; may change between versions | | `error.details` | Optional structured context; on `validation.failed`, `details.fields` contains per-field errors | | `trace_id` | Include it in support tickets | ## Errors common to all tools ### Authentication and permissions ### Credits and account ### Request and limits ### AI agents (MCP) ### Platform ### Video ## Errors by tool Asynchronous jobs (PDF) also expose `error_code` on the resource itself when `status` is `failed`; codes marked as **asynchronous** appear only there. ## Best practices 1. Handle `402` with a top-up flow in your system. 2. On `429`, respect `X-RateLimit-Reset` before trying again. 3. Use `X-Dry-Run` to catch `validation.failed` at no cost. 4. Retry only `5xx` and network errors, always with the same `X-Idempotency-Key`. 5. Log the `trace_id` of every error response. # API Documentation ## Tools ## Getting started ### Create an API token 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). ### Estimate the cost with dry-run 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). ### Make the call All tool routes use the `https://api.respawntech.dev/v1/tools` prefix. Every endpoint includes ready-to-use examples in cURL, JavaScript and Python. ## Common headers | Header | Value | Effect | | ------------------- | -------------- | ----------------------------------------------------------------------- | | `X-API-Token` | `rsk_live_...` | Authentication (recommended) | | `X-Dry-Run` | `true` | Simulates the operation without persisting or debiting | | `X-Include-Cost` | `true` | Includes `meta.cost` in the response | | `X-Idempotency-Key` | UUID | Prevents duplicate charges on retries () | Responses include `X-Trace-Id` (use it in support tickets), `X-RateLimit-*` and, on metered reads, `X-API-Quota-*`. ## Guides # Use with AI 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**. ## Two MCP servers | | Documentation MCP | [Account MCP](/en/agentes-de-ia) | | -------------- | --------------------------------------------- | ------------------------------------------------------------------------------- | | Purpose | Answer questions about the API while you code | Use the tools with your account (create a QR, convert a PDF, transcribe audio…) | | URL | | | | Authentication | None | API token with `mcp:use` | | Cost | Free | Same as the API | | Access | Read-only public documentation | Acts on the account, with scopes and a spend limit | Both can be connected at the same time: an agent looks up the docs, then uses the account MCP to act. The documentation MCP is being rolled out gradually. Until it is active, answers `404` with the `mcp.disabled` code. llms.txt and the Markdown pages already work. ## Connection | | | | -------------- | --------------------------------------------------------------------- | | URL | | | Transport | Streamable HTTP | | Authentication | None (don't send a token) | | Cost | Free | | Limit | requests per minute per IP | | Language | Optional `lang` parameter on every tool: `pt-br`, `en`, `es` or `fr` | Answers come from the same content as this site, including the current prices. ## Configure the client In **Claude.ai** and **ChatGPT**, add a custom connector and enter only the URL . No login or token is needed. Clients that only speak stdio use the `mcp-remote` bridge, as in the last example. ## Tools A 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. ## Without MCP: llms.txt and Markdown For agents that only read the web: * [`/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. * [`/llms-full.txt`](/llms-full.txt): every page in a single text file. * Markdown pages: `/md/.md` in Portuguese and `/md//.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`. * [`/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. # Webhooks 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: | Origin | Where it is configured | Cost | | ---------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------ | | **Account events** (e.g. `qr.dynamic.scanned`) | **Webhooks** in the [dashboard](https://dashboard.respawntech.dev) | Free | | **Job callbacks** (e.g. PDF conversion) | `webhook_url` in each request | Free | | **Webhook Relay** (your own events) | The [Webhook Relay](/en/ferramentas/webhook-relay) API | Per delivery beyond the monthly free quota | ## Account events 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. These 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). ## Request sent ```http POST Content-Type: application/json User-Agent: RespawnTech-Webhook/2.0 webhook-id: msg_2Vf8tK1pQ9zX webhook-timestamp: 1790338000 webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= X-Webhook-Signature: sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd ``` | Header | Content | | --------------------- | ----------------------------------------------------------------------------------------------- | | `webhook-id` | Message identifier. It is the **same across every retry**: use it to drop duplicates. | | `webhook-timestamp` | Time of this attempt, in Unix seconds. | | `webhook-signature` | `v1,`, one signature per active secret, space separated (two during a secret rotation). | | `X-Webhook-Signature` | Legacy `sha256=` signature, without timestamp. Kept for older receivers. | The 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`). ### Body Account events and Webhook Relay messages use the same envelope: ```json { "event_id": "550e8400-e29b-41d4-a716-446655440000", "type": "qr.dynamic.scanned", "timestamp": 1790338000, "payload": { "resource_id": "…", "slug": "promo-verao", "ip": "anonymous_hash", "user_agent": "Mozilla/5.0 …", "referer": "https://…" } } ``` The 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. ## Verifying the signature The signed content is `webhook-id + "." + webhook-timestamp + "." + raw body`, HMAC-SHA256, base64-encoded. To verify: 1. Read the **raw body**, before any JSON parsing (reformatting the JSON changes the signature). 2. Reject timestamps more than away from your clock (replay protection). 3. Compute the signature and compare it, **in constant time**, with each item of `webhook-signature`. One match is enough. With a Standard Webhooks library, use `standard_webhooks_secret` (the `whsec_…` form). Without a library, the HMAC key is the bytes of the raw `secret`. ```js // npm i standardwebhooks import express from 'express'; import { Webhook } from 'standardwebhooks'; const wh = new Webhook(process.env.WEBHOOK_SECRET); // whsec_… const app = express(); app.post('/hook', express.raw({ type: '*/*' }), (req, res) => { let event; try { event = wh.verify(req.body, req.headers); // throws when invalid } catch { return res.sendStatus(401); } // process `event` asynchronously and answer fast res.sendStatus(204); }); ``` ```python # pip install standardwebhooks import os from flask import Flask, request from standardwebhooks.webhooks import Webhook wh = Webhook(os.environ["WEBHOOK_SECRET"]) # whsec_… app = Flask(__name__) @app.post("/hook") def hook(): try: event = wh.verify(request.get_data(), dict(request.headers)) except Exception: return "", 401 # process `event` asynchronously and answer fast return "", 204 ``` ```go // Without a library: secret is the raw value (not the whsec_ form). func verify(secret string, r *http.Request, body []byte) bool { id, ts := r.Header.Get("webhook-id"), r.Header.Get("webhook-timestamp") t, err := strconv.ParseInt(ts, 10, 64) if err != nil || math.Abs(time.Since(time.Unix(t, 0)).Seconds()) > 300 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(id + "." + ts + ".")) mac.Write(body) want := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil)) for _, sig := range strings.Fields(r.Header.Get("webhook-signature")) { if hmac.Equal([]byte(sig), []byte(want)) { return true } } return false } ``` 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. ## Secrets and rotation | Destination | Where to get the secret | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Account event endpoint | On creation, or through the reveal-secret option under **Webhooks** in the dashboard. | | Webhook Relay endpoint | On creation, or with [Reveal secret](/en/ferramentas/webhook-relay#webhooks-endpoints-secret). | | Job callback (`webhook_url`) | The account signing secret, one for every callback: `GET /v1/account/webhooks/signing-secret` (account route, with the dashboard session). | Every secret comes in two forms: `secret` (raw) and `standard_webhooks_secret` (`whsec_` + base64 of the same bytes). Both verify the same signatures. When you [rotate an endpoint's secret](/en/ferramentas/webhook-relay#webhooks-endpoints-rotate_secret), the old one keeps signing for : during that window `webhook-signature` carries both signatures, so you can update the receiver without losing events. ## Delivery and retries 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: | Attempt | Wait before it | | ------- | -------------- | | 1 | — | | 2 | 5 seconds | | 3 | 5 minutes | | 4 | 30 minutes | | 5 | 2 hours | | 6 | 5 hours | | 7 and 8 | 10 hours | About 27 hours in total, with ±10% jitter. Special cases: * **`429` and `503`** with `Retry-After`: the next attempt honors the header when it asks for longer (up to 10 hours). * **`410 Gone`**: the delivery stops and the endpoint is disabled. * 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). Good practices in the receiver: * Answer **2xx** as fast as possible and process the event asynchronously. * **Drop duplicates by `webhook-id`**: a delivery can arrive more than once (e.g. your server processed it but the response was lost). * Don't rely on arrival order; use the body's `timestamp` when order matters. ### Automatic disabling An endpoint that keeps failing for 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. The history of messages, deliveries and attempts is kept for . ## Job callback 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: ```json { "job_id": "job_abc123", "success": true, "result_url": "https://…", "metadata": { "conversion_id": "…" } } ``` On 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`. ## Your own events 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). # Audio ## Overview Every audio operation is **asynchronous** and charged up front: 1. `POST` to the operation → **202 Accepted** with `job_id`, `status: pending` and `poll_after_seconds`. 2. A worker processes it in the background. 3. 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. The 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. 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`…). ## Audio input Operations that take an audio require **exactly one** of these fields: | Field | What it is | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `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 | | `audio_base64` | The audio inline as base64, up to decoded | | `file_id` | Id of an audio file already uploaded to the account | Accepted formats: mp3, wav, ogg, m4a, flac and webm. The limit is of audio and 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. Every 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. ## Costs A "started minute" rounds up. An audio whose length cannot be read from its header is charged at the 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). ### Dry-run 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: ```json { "success": true, "data": { "service_code": "audio_transcribe_minute", "units": 3, "rate": 15000, "total_units": 45000, "duration_ms": 125000, "duration_known": true, "parts": [{ "service_code": "audio_transcribe_minute", "units": 3, "rate": 15000, "total": 45000 }] } } ``` `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`. ## Operations ### Transcribe audio `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. ### Generate speech from text `text` takes up to 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. ### List the voices Closed catalog of voices for `speak` and `revoice`. `language` filters by language. Free. ### Generate a sound effect or music `kind`: `effect` (1 to , default 5 s) or `music` (instrumental, 1 to , 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. ### Analyze audio 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. ### Convert or normalize audio `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. ### Summarize audio 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. ### Translate audio `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). ### Speak the recording again in another voice 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`. ### List jobs Most recent jobs first, with `limit` (default 20, up to ) 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 (). ### Status, result and download 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). `status`: `pending`, `processing`, `completed` or `failed`. ## Cloned voices and consent (MCP only) 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`). * **`voice_create`** clones the **account holder's own voice** from a 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`. * **`voice_list`** lists the account's voices (`voice_id`, name, language, `status`, expiry). Free. * **`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. ### Responsible use * **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. * **No imitation.** Neither speech, `revoice` nor sound generation accepts imitating existing artists, voices or works. * **Limits:** up to cloned voices per account, creations per day and uses per hour (`audio.voice_limit`, `audio.voice_rate_limited`). * **Stored data:** the sample stays encrypted for up to 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 . * **Tell your listeners:** if you publish synthetic audio, say the voice is generated, especially when it is a cloned voice. ## Scopes | Scope | Operations | | ------------------------ | ------------------------------------------------------------------------------- | | `tools:audio:transcribe` | `transcribe`, `summarize`, `translate` | | `tools:audio:speak` | `speak`, `generate`, `revoice` (in MCP, also `voice_create` and `voice_delete`) | | `tools:audio:process` | `analyze`, `convert` | | `tools:audio:read` | `voices`, `jobs`, `jobs/{id}` (the three scopes above also read their own jobs) | `tools:manage` covers everything. The full list is in [Authentication](/en/autenticacao). ## Errors `audio.*` in the response or, for jobs that fail after being accepted, in the job's `error_code`. # Knowledge base ## Overview 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. **Create a knowledge base** with the language of your documents (it improves keyword search). **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. **Search** with the user's question and use the returned chunks as context for the answer. **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). **Record feedback** (optional): confirmed chunks rank higher in similar future searches, and rejected ones drop out. ## Costs 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). Ingestion 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. ## Knowledge bases ### Create knowledge base * `name` (required, up to 120 characters) and `description` (up to 1,000). * `language`: `simple` (default), `portuguese`, `english`, `spanish` or `french`. Sets the dictionary used by keyword search. * `default_mode`: `fast` (default) or `accurate` (extra rerank, more precise and slightly more expensive). * 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`). ### List knowledge bases Also returns the account's usage against its limits (`usage`). ### Retrieve knowledge base ### Available filters 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"]}`. ### Update knowledge base 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. ### Reindex knowledge base Reprocesses every document with the knowledge base's current settings. Each chunk is charged as in a new ingestion. ### Knowledge base health dashboard 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. ### Document index 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. ### Find duplicates and contradictions 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. ### Switch the embeddings model 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. ### Mark documents as outdated 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. ### Delete knowledge base Deletion is asynchronous and removes documents, chunks and feedback. ## Documents ### Add or replace document Security…", "source_uri": "https://help.example.com/articles/42", "metadata": { "category": "account" } }` }} > * `content` (required): plain text or Markdown in UTF-8, up to . For PDF, Office and images, use [Import file](#rag-documents-file). * `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. * `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. * Recognized secrets in the text (API keys, tokens) are replaced with `[REDACTED:type]` before indexing; `redactions` counts how many. * `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. * 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. ### Import file (PDF, Office, image) `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 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`). ### Retrieve file import ### List documents Optional `status` filter (`pending`, `processing`, `ready`, `failed`, `deleting`). Cursor pagination: pass the `next_cursor` you received as `after`. `limit` defaults to 20, maximum 100. ### Retrieve document ### Update title or metadata Doesn't reprocess the content and costs nothing. ### View indexed sections Shows how the document was split, useful for tuning headings and chunk size. ### Retry failed document Only for documents with `status: failed`; others respond `rag.document_not_failed`. ### Delete document ## Synced sources 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::…`, 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. | `kind` | `start_url` | What it imports | | --------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `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. | | `sitemap` | sitemap (or index) URL | Imports the listed pages, up to `max_pages`. `max_depth` does not apply. | | `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. | | `gdrive` | URL of a `/folders/` folder | Folder shared as "anyone with the link"; Docs become Markdown. Only available if the server has the integration enabled. | ### List synced sources ### Create synced source * `name` (up to 200) and `start_url` (http or https) are required; `kind` defaults to `site`. * `max_pages`: 1 to 1000 (default 100). `max_depth`: 1 to 10 (default 3). * `frequency_hours`: `0` (manual only), `6`, `24` (default) or `168`. * 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. ### Update source All fields are optional. With `enabled: false` or `frequency_hours: 0`, `next_sync_at` becomes null. ### Sync now Responds `409` if the source is already syncing. `pages_total`, `pages_new`, `pages_changed` and `pages_removed` summarize the run. ### Sync history ### Delete source ## Search ### Search modes and reranker * **`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. * **`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`). * **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`. * If the embeddings service fails, search still responds by keywords only (`mode_used: lexical`), charged as cache. ### Query knowledge base * `query` (required, up to characters). * `top_k`: how many chunks to return (default 5, maximum ). * `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. * `filters`: only chunks from documents whose metadata contains these values. * `min_score` (only in `accurate`) and `max_per_document` (0–20) refine the result. * `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). * `highlights` marks, in Unicode character positions, the terms found in `content`. Repeated queries within are served from cache (`cached: true`) and cost less. The returned chunks are content from your documents. When passing them to a language model, treat them as data, not instructions. ## Relevance feedback ### Record relevance feedback * `label`: `positive` (the chunk answers the question) or `negative`. * `end_user_ref` (optional): identifies who rated it, when the vote comes from your end user. Only a hash is stored. * Up to votes per minute. Similar questions are grouped, and a vote applies to the whole group. ### List feedback ### Delete a feedback group ## Standalone rerank ### Rerank texts Orders **your own** list of texts by relevance to the question, with no knowledge base involved. Up to texts per call (each is truncated at 2,000 characters). `index` points to the position in the array you sent. ## Answer with citations ### Answer with citations 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). * 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. * 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). * `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. * 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. * Charged as an `accurate` search; generation has no separate charge. Use the `query_id` in [feedback](#rag-feedback-add) to feed the analytics CTR. ## Embeddable chat widget 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. ```html ``` Attributes: `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`. ### Create public key (widget) * `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. * `rate_per_minute`: 1 to 600 (default 30), per key. `generate` turns on AI-generated text in this key's answers. * Up to 20 keys per knowledge base. The `key` is shown **only once**; we store only the hash. 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. ### List public keys ### Revoke public key ### Answer through the widget (public route) 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). ### Widget script ## Question analytics ### Question analytics 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). * `answer_rate`: answered ÷ total questions. * `feedback_ctr`: answered questions that received a vote (by `query_id`) ÷ answered. * `positive_rate`: positive votes ÷ votes. * `top_questions` and `unanswered_questions`: up to 20 grouped questions each, so you can find out what to write in the knowledge base. 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. ## Routing ### Decide between options (routing) 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. ## Pricing and account limits ### Retrieve pricing ### Retrieve limits Values in effect for your account (already including the plan bonus), useful for validating an upload before making it. ## Events (webhooks) Register an endpoint in the dashboard to receive: * `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. * `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. Delivery is best-effort, as with other events. See the [event list](/en/webhooks). ## Using it through MCP 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. ## Errors Codes without an HTTP status show up in the `error_code` field of a document with `status: failed`. ## Limits | Limit | Value | | ------------------------------------- | -------------------------------------------- | | Document size | | | Knowledge bases per account | | | Indexed chunks per account | | | Documents processing at the same time | | | Query length | characters | | Chunks per search (`top_k`) | | | Texts per standalone rerank | | | Feedback votes per minute | | | Imported file | 50 MB | | Pages per source | 1,000 (default 100) | | Public keys per knowledge base | 20 | Knowledge 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`). # CI Error Extractor ## Overview 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`. ## Costs 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. ## Endpoints ### Extract the error from a log `log` (required) is the log text. `max_lines` (optional, default 40, max 200) caps how many error lines are returned. ## Errors # Learnings memory ## How it works A learning is a short text with a `kind`, tied to a `project`: | `kind` | To keep | | ------------ | ------------------------------------------- | | `pitfall` | Pitfalls and mistakes that must not repeat. | | `decision` | Decisions made and why. | | `convention` | Project conventions. | | `howto` | How to do something (run, publish, debug). | | `other` | Whatever doesn't fit the others. | Learnings 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`. It is also available in the [MCP](/en/agentes-de-ia) as `learning_save`, `learning_recall` and `learning_forget` (`learnings` toolset). ## Duplicates and asynchronous ingestion * **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. * **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. ## Costs 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). ## Endpoints ### Save a learning `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`. ### Recall learnings `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. ### Forget a learning # Notifications ## How it works 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**: * **Chosen channel:** the request's `channel`, by channel name. * **Default channel:** the channel flagged `is_default`, when the request has no `channel`. * **Account email:** with no channel configured at all, the notification goes to the account's email. It 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. ## Channels Each channel has a `name`, unique in the account, and a `kind`: | `kind` | Fields | How it delivers | | --------- | ----------------- | ------------------------------------------------------------------------------------------- | | `email` | none | Sends **only to the account's email**. You can't choose another recipient. | | `webhook` | `url`, `secret` | Makes a `POST` to the URL, signed as in the [Webhook Relay](/en/ferramentas/webhook-relay). | | `ntfy` | `server`, `topic` | Publishes to the [ntfy](https://ntfy.sh) topic on the given server. | `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. On public ntfy servers, anyone who knows the topic can read the notifications. Use a long, unguessable name, or your own server. ## Limits, retry and history * **Rate limit:** there is a limit of notifications per period; above it the send fails with `notify.rate_limited`. * **Webhook retry:** a failed delivery is retried a few times before becoming `notify.delivery_failed`. The receiver must handle repeats and check the signature. * **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**. ## Costs 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. ## Endpoints ### Send a notification `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. ### Delivery history Paginated by `page` and `page_size` (up to 100). It doesn't include the message bodies. ### List channels ### Create a channel `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`. ### Get a channel ### Update a channel Send only the fields that change. A disabled channel returns `notify.channel_disabled` when chosen in a send. ### Delete a channel ### Test a channel Sends a test notification through the channel, to check the configuration before you rely on it. ## Errors # PDF Convert ## Overview PDF Convert is **asynchronous**: 1. `POST /convert` or `POST /batch` → **202 Accepted** with the `conversion_id` (the `{id}` used by the other routes). 2. A worker renders it in the background (headless Chromium). 3. Track it through the [`webhook_url` callback](/en/webhooks#pdf-callback) or by calling `GET /{id}`. 4. Download it with `GET /{id}/download` once `status` is `completed`. Credits are debited when the job is accepted. If enqueuing a batch fails after the debit, the amount is refunded automatically. ## Costs A batch accepts up to ** 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). ## Conversion ### Convert HTML to PDF/PNG/JPG

    Fatura

    ", "css": "body { font-family: sans-serif; }", "format": "pdf", "page_size": "A4", "retention_days": 7 }`, }} > 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`). | Field | Limit / values | | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `html` | max. | | `url` | alternative to `html`: public `http`/`https` address, up to 2048 characters (`pdf.invalid_url` if private or invalid) | | `css` | max. | | `format` | `pdf` (default), `png`, `jpg` / `jpeg` | | `file_name` | name of the generated file | | `title`, `author`, `subject`, `keywords` | document metadata, up to 1000 characters each | | `header_html` / `footer_html` | header and footer repeated on every page | | `google_fonts_url` | Google Fonts CSS URL to load | | `wait_selector` / `wait_delay` | waits for a selector to appear / a delay in ms before rendering | | `enable_js` / `custom_js` | runs sandboxed JavaScript (SPAs, charts) / extra script | | `data` | object with variables for `{{campo}}` placeholders in the HTML | | `retention_days` | `0` (ephemeral, \~24h), `1`–`30` or `-1` (permanent) | | `webhook_url` | receives the [callback](/en/webhooks#pdf-callback) when finished | | `template_id`, `template_version`, `font_ids`, `post_process` | [saved templates](#templates), [fonts](#fontes) and [post-processing](#pos-processamento) | Page size, margins, media, numbering and quality are in [Page and render options](#opcoes-de-pagina). Keep the `conversion_id`: it's the `{id}` used to check the status and download the file.
    ### Batch with template

    {{title}}

    ", "css": "h1 { color: #333; }" }, "config": { "page_size": "A4", "format": "pdf", "retention_days": 7 }, "items": [ { "custom_id": "fatura-001", "file_name": "fatura-001.pdf", "data": { "title": "Fatura 001" } }, { "custom_id": "fatura-002", "file_name": "fatura-002.pdf", "data": { "title": "Fatura 002" } } ], "delivery": { "method": "zip", "webhook_url": "https://example.com/hooks/pdf" } }`, }} > | Field | Description | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `template.html` | **required** — HTML with `{{campo}}` placeholders (Go template syntax: `{{if}}` and `{{range}}` also work) | | `template.css` | Shared CSS | | `items[]` | **required**, 1 to items: `custom_id` (**required**, your identifier), `file_name` and `data` | | `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` | | `delivery.method` | `zip` or `list`: how the result is delivered when finished | | `delivery.webhook_url` | receives the [callback](/en/webhooks#pdf-callback) when finished | 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).
    ### Retry a failure

    Fatura corrigida

    ", "format": "pdf", "page_size": "A4" }`, }} > 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.
    ## Page and render options 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). | Field | Values | | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `page_size` | `A3`, `A4` (default), `A5`, `Letter`, `Legal`, `Tabloid` or `Custom` | | `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` | | `orientation` | `portrait` (default) or `landscape` | | `margin_top`, `margin_bottom`, `margin_left`, `margin_right` | page margins | | `scale` | render scale | | `print_background` | prints background colors and images (default `true`) | | `media_type` | `print` or `screen`: which `@media` the CSS sees | | `color_scheme` | `light` or `dark`: value of `prefers-color-scheme` | | `page_numbers` | turns on the page counter at the position: `header-left`, `header-center`, `header-right`, `footer-left`, `footer-center` or `footer-right` | | `page_number_format` | counter text, with `{page}` and `{total}` (default `{page} / {total}`, up to 200 characters). Requires `page_numbers` | | `jpeg_quality` | 1 to 100 (default 90). Only applies with `format: jpg` | Example: a report on 8 x 11 inch paper with a counter in the footer: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "url": "https://example.com/relatorio", "page_size": "Custom", "paper_width": 8, "paper_height": 11, "media_type": "print", "page_numbers": "footer-center", "page_number_format": "Page {page} of {total}" }' ``` ## Post-processing `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). | Field | Description | | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `post_process.watermark.text` | watermark text, up to 200 characters | | `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` | | `watermark.font_size` | 1 to 300 | | `watermark.color` | color in `#rrggbb` format | | `watermark.opacity`, `watermark.scale` | 0 to 1 | | `watermark.rotation` | -180 to 180 degrees | | `watermark.position` | `center`, `top-left`, `top-center`, `top-right`, `left`, `right`, `bottom-left`, `bottom-center` or `bottom-right` | | `watermark.on_top` | `true` draws the mark over the content | | `post_process.pdfa` | `true` converts to PDF/A-2b (long-term archiving) | | `post_process.protection.user_password` | **required** when there is a `protection`, up to 128 characters: password to open the file | | `protection.owner_password` | owner password, up to 128 characters | | `protection.permissions` | what the reader may do: `print`, `copy`, `modify`, `annotate`, `fill_forms`, `assemble`. Empty list = no permissions | `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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "html": "

    Contrato

    ", "format": "pdf", "post_process": { "watermark": { "text": "CONFIDENTIAL", "color": "#cc0000", "opacity": 0.2, "rotation": -45, "position": "center" }, "protection": { "user_password": "reader-password", "permissions": ["print"] } } }' ``` ## Saved templates 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. * 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. * The template's `css`, `header_html` and `footer_html` act as defaults; if you send them in the conversion, yours win. * Unknown template or version: `pdf.template_not_found`. Name up to 120 characters; `html` follows the limit and `css` the one. * 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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "template_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a", "template_version": 0, "data": { "numero": "001" } }' ``` ### List saved templates `limit` (default and maximum 100) and `offset`. The response has `items` and `total`. ### Save template

    Fatura {{numero}}

    ", "css": "h1 { color: #333; }", "header_html": "", "footer_html": "" }`, }} > `name` (up to 120 characters) and `html` are required; `description`, `css`, `header_html` and `footer_html` are optional. Creates version 1.
    ### Get template Optional `version` (default: the current one). Returns the `template` and the content of the `version`. ### List template versions ### Rename template or save a new version

    Fatura {{numero}}

    {{cliente}}

    ", "css": "h1 { color: #333; }" }`, }} > 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.
    ### Delete template Deletes the template and all its versions. Conversions already generated are not affected. ## Fonts 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`. * Up to **50 fonts per account** (`pdf.font_limit`). * A `family` + `weight` + `style` combination can only exist once (`pdf.font_exists`, 409). * The format is detected from the file content, not from the extension. ### List fonts ### Upload font 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`. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/fonts' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -F 'file=@Inter-Regular.woff2' \ -F 'family=Inter' \ -F 'weight=400' \ -F 'style=normal' ``` Then convert with the font: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "html": "

    Hello

    ", "font_ids": ["2b3c4d5e-6f70-4a8b-9c0d-1e2f3a4b5c6d"] }' ```
    ### Delete font Removes the font and its stored file. ## Lookup and download ### List conversions 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. ### Conversion status `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). ### Download file Requires `status: completed`. The file is decrypted and decompressed during download. `X-Dry-Run` does not apply to downloads. 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. ## Batches ### List batches Batch `status`: `processing`, `delivering`, `completed`, `partially_failed` or `failed`. ### Batch details ### Download batch ZIP Available once the batch finishes, including partially if some items failed. It also accepts the signed `?sig=&exp=` link, no token needed. ## Errors ## Quirks * 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. * For recurring PDFs, save a [template](#templates) instead of resending the HTML on every call. * `retention_days: 0` is ephemeral (\~24h) — ideal for previews. * Prefer `webhook_url` over polling the status in a loop: every API lookup counts toward the daily free quota. # Dynamic QR ## Overview A dynamic QR code separates the **printed image** (a stable short link) from the **destination** (editable without reprinting). Typical flow: 1. `POST` with `X-Dry-Run: true` to validate the payload and see the cost. 2. Final `POST` with `X-Idempotency-Key` → you get the QR image, the `resource_id` and the `slug`. 3. Use the returned image (or generate your own pointing to `full_url`, available from `GET /{id}`). 4. 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. `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`. ## Costs API 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. ## Creation and management ### Create a dynamic QR code 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. 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`. ### List QR codes Query: `limit` (default 20, max. ), `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`). ### Details ### Update destination, name or design 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. ### Reactivate ### Deactivate A deactivated QR code shows an inactive QR page on the public redirect and keeps occupying a slot until it's deleted. ### Delete Ends the slot's storage charges. ### Check slug availability ## Analytics ### Analytics overview 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. ### Scans by period 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. ### Device breakdown `device` is `Mobile`, `Desktop` or `Tablet`. As with scans by period, the period fields are zeroed. ### Scan history Pagination: `limit` (default 50, max. ), `offset` and `period` (default: last 30 days). `location` is `Unknown` when both city and country are unknown. ## Public redirect * **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. * **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`). * **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. * Responses are never cached (`Cache-Control: no-store`). ```bash curl -sS -I 'https://api.respawntech.dev/r/promo-verao' ``` ## Custom domain 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. The 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)). ### Register the domain ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "hostname": "qr.suaempresa.com", "validation_method": "http", "root_url": "https://www.suaempresa.com" }' ``` `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`. ### Publish the CNAME 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. | Type | Name | Value | | ------- | ----------------------------- | ----------------------------------- | | `CNAME` | `qr` (or `qr.suaempresa.com`) | the `value` of the `routing` record | 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. ### Validate the certificate 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. ### Verify until it is active The platform checks the domain periodically; to avoid waiting, force the verification: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains/$DOMAIN_ID/refresh' \ -H "X-API-Token: $RESPAWN_API_TOKEN" ``` 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. ### Create QR codes on the domain 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`. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Cartaz da loja", "target_url": "https://loja.example.com/promo", "custom_slug": "promo-verao", "custom_domain_id": "'"$DOMAIN_ID"'" }' ``` The QR code's `full_url` becomes `https://qr.suaempresa.com/promo-verao`. The slug only needs to be unique within the domain. `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. 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. ### List custom domains ### Domain details ### Register a custom domain ### Edit the domain's fallback pages Send `""` to clear one of the fields. ### Verify the domain now ### Remove a domain ## Scan behavior By 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)). On 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. ### Redirect rules `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. | Field | Meaning | | ------------------------ | -------------------------------------------------------------------------------------------------- | | `id` | Rule name (`[a-zA-Z0-9_-]`, up to 40); appears in scans and in the analytics `rule_stats` | | `target_url` | Destination when the rule matches (absolute URL) | | `countries` | ISO 3166-1 alpha-2 countries in uppercase (`["BR", "PT"]`) | | `devices` | `mobile`, `tablet`, `desktop` | | `os` | `ios`, `android`, `windows`, `macos`, `linux`, `other` | | `languages` | Visitor's preferred language: `pt` matches `pt` and `pt-br`; `pt-br` matches only `pt-br` | | `days` | Days of the week, `0` (Sunday) to `6` (Saturday), in the `timezone` | | `start_time`, `end_time` | Time-of-day window (`HH:MM`); if the end is before the start, it crosses midnight. Both or neither | | `timezone` | IANA time zone (`America/Sao_Paulo`); UTC if empty | | `from`, `until` | Period in which the rule applies (RFC 3339 date and time) | Each list accepts up to 50 values. Within a rule the conditions add up (AND); within a list, one value is enough (OR). ### A/B test `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. ### Password `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. ### Pixels `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`). ### Redirect for expired and exhausted `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`. ### Full example Copy it, swap the URLs and run: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Campanha com regras", "target_url": "https://loja.example.com/global", "redirect_rules": [ { "id": "br-mobile", "target_url": "https://loja.example.com/br", "countries": ["BR"], "devices": ["mobile"] }, { "id": "horario-comercial", "target_url": "https://loja.example.com/atendimento", "days": [1, 2, 3, 4, 5], "start_time": "09:00", "end_time": "18:00", "timezone": "America/Sao_Paulo" } ], "ab_variants": [ { "id": "a", "target_url": "https://loja.example.com/lp-a", "weight": 50 }, { "id": "b", "target_url": "https://loja.example.com/lp-b", "weight": 50 } ], "pixels": { "ga4_measurement_id": "G-ABCD1234" }, "expired_url": "https://loja.example.com/encerrada", "limit_reached_url": "https://loja.example.com/esgotado" }' ``` To remove everything later, without recreating the QR code (replace `$QR_ID` with the `resource_id` from creation): ```bash curl -sS -X PATCH "https://api.respawntech.dev/v1/tools/image/qr-code/dynamic/$QR_ID" \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "redirect_rules": [], "ab_variants": [], "pixels": {}, "password": "" }' ``` `GET /{id}` returns the configuration in `scan_behavior` (`redirect_rules`, `ab_variants`, `has_password`, `pixels`, `expired_url`, `limit_reached_url`, `content`). ## Hosted content 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`. | `type` | Field | The scan shows | | ----------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | `vcard` | `vcard` (`FirstName`, `LastName`, `Org`, `Title`, `Phone`, `Email`, `Website`, `Address`) | Contact page and `.vcf` download (`?format=vcf`). Requires a name or organization | | `file` | `file` (`url`, `name`) | Opens the hosted file or PDF (absolute URL) | | `links` | `links` (`title`, `description`, `avatar_url`, `links[]` with `label` and `url`) | Link in bio, 1 to 50 links | | `menu` | `menu` (`title`, `currency`, `sections[]` → `items[]` with `name`, `description`, `price`) | Menu, up to 30 sections and 300 items | | `wifi` | `wifi` (`SSID`, `Password`, `Security`, `Hidden`) | Network name and password to copy | | `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 | The `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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Link na bio", "content": { "type": "links", "links": { "title": "Loja Exemplo", "links": [ { "label": "Site", "url": "https://loja.example.com" }, { "label": "Instagram", "url": "https://instagram.com/lojaexemplo" } ] } } }' ``` Contact card example: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Cartão da Ana", "content": { "type": "vcard", "vcard": { "FirstName": "Ana", "LastName": "Souza", "Org": "Loja Exemplo", "Phone": "+5511999990000", "Email": "ana@loja.example.com" } } }' ``` ## Frames and printing `frame` draws a call to action around the code and `dpi` makes the image print-ready. Both work with any `response_types`. * `frame`: `text` (up to 32 characters; shrinks to fit), `color`, `text_color` (hex) and `position` (`top` or `bottom`). * `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. * `eps`: new vector format, with the frame and Helvetica-Bold font with accents. It comes out without a logo. In PDF and EPS, the eyes come out square and only the `dots` and square module styles become vector; the gradient is not applied. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Cartaz A5", "target_url": "https://loja.example.com/promo", "response_types": ["pdf", "eps"], "frame": { "text": "Escaneie aqui", "color": "#111111", "text_color": "#FFFFFF", "position": "bottom" }, "dpi": 300 }' ``` ## Organization, bulk editing and batches ### Folders and tags `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`. ### List folders ### Create a folder The name is 1 to 80 characters and unique per account (`dynamic_qr.folder_taken`). ### Rename a folder ### Delete a folder ### Edit several QR codes at once 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`. ### Create QR codes in batches Up to 1,000 rows per batch, in one of these ways: * 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`); * a `text/csv` body directly, with `folder_id` and `tags` as query parameters. 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`: ```csv name,target_url,custom_slug,tags Loja Centro,https://loja.example.com/centro,loja-centro,varejo|sp Loja Norte,https://loja.example.com/norte,loja-norte,varejo ``` ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/batches?tags=campanha' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'X-Idempotency-Key: 7f1c2e84-5b3a-4d6c-9e01-a2b3c4d5e6f7' \ -H 'Content-Type: text/csv' \ --data-binary @lojas.csv ``` 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`). ### List batches ### Batch details `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). ### Export scans as CSV 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`. ### Purge scan history 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. ## Errors ## Quirks and limits * Malicious URLs are blocked automatically (`dynamic_qr.malicious_url`). * Anti-loop protection: the destination cannot point to the redirect domain itself. * The first QR code doesn't occupy a paid slot; the rest occupy slots in groups of 3. * `skip_image_generation: true` lowers the cost when you generate the QR image yourself. * Slug: 3–50 characters `[a-zA-Z0-9-]`. Listings: up to items per page. # Realtime ## How it works Realtime carries messages from your server, or from other clients, to connected browsers, without you running WebSocket servers. * **Channel:** a free-form name of up to 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. * **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`). * **`connect_url`:** the WebSocket address the client connects to, returned in the same response as the token and already carrying `?token=`. * **Message:** JSON `data` published to a channel, with an `id`, a `timestamp` (Unix milliseconds) and the `client_id` of whoever published it. * **Presence:** the list of who is connected and "present" on a channel, with free-form data for each member (a nickname, a status). The flow is always the same: The browser asks **your server** for a token; your server already knows who the user is. 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`. The browser connects to `connect_url`, subscribes to channels and publishes. If the connection drops, the browser asks for a new token and [reconnects](#reconexao), recovering the missed messages with `rewind`. 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. Your 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. ## Token and capabilities ### Issue a connection token * `capabilities` (required): an object mapping a channel pattern to the list of allowed operations, up to 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`. * `client_id` (optional, up to 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. * `ttl_seconds` (optional): token lifetime, from up to , default . The token is only checked when connecting: an open connection stays valid after it expires, but the next reconnection requires a new token. 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**. Give 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. 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`. ## Connecting from the browser 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. ```js import { createServer } from 'node:http'; createServer(async (req, res) => { if (req.url !== '/realtime-token') return res.writeHead(404).end(); const api = await fetch('https://api.respawntech.dev/v1/tools/realtime/tokens', { method: 'POST', headers: { 'X-API-Token': process.env.RESPAWN_API_TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: 'user-42', // the authenticated user capabilities: { 'chat:*': ['subscribe', 'publish', 'presence'] }, }), }); const { data } = await api.json(); // token, expires_at, connect_url and retry_after_ms go together to the browser res.writeHead(api.status, { 'Content-Type': 'application/json' }).end(JSON.stringify(data)); }).listen(3000); ``` ```python import os import requests from flask import Flask, jsonify app = Flask(__name__) @app.post("/realtime-token") def realtime_token(): api = requests.post( "https://api.respawntech.dev/v1/tools/realtime/tokens", headers={"X-API-Token": os.environ["RESPAWN_API_TOKEN"]}, json={ "client_id": "user-42", # the authenticated user "capabilities": {"chat:*": ["subscribe", "publish", "presence"]}, }, timeout=30, ) # token, expires_at, connect_url and retry_after_ms go together to the browser return jsonify(api.json()["data"]), api.status_code ``` In 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). ```html Realtime
      ``` ## Protocol 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}$`. ### Client to server | `action` | Fields | What it does | | ------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `subscribe` | `channel`, `rewind?` | Subscribes to the channel (requires `subscribe` on the token). With `rewind` (a number of messages, up to ), it resends the channel's latest messages before the new ones. Answers `attached`. | | `unsubscribe` | `channel` | Stops receiving the channel. Answers `detached`. | | `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. | | `presence` | `op`, `channel`, `data?`, `id?` | [Presence](#presenca) operations: `op` is `enter`, `update`, `leave` or `get`. | ### Server to client | `action` | Fields | When | | ----------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `connected` | `connection_id` | The connection was accepted; you can subscribe now. | | `attached` | `channel` | The channel subscription is active (after the `rewind`, if requested). | | `detached` | `channel` | The subscription ended. | | `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. | | `ack` | `id`, `channel?` | The publish (or presence operation) with that `id` was accepted. | | `presence` | `op`, `channel`, `member` or `members` | A presence event (`enter`, `update`, `leave`) or the answer to `get` (`sync`). | | `error` | `code`, `message`, `channel?`, `id?` | An operation failed. The `code` values are in [Errors](#erros). The connection stays open. | Example exchange: ```json ← { "action": "connected", "connection_id": "c_8f3a1d" } → { "action": "subscribe", "channel": "chat:lobby", "rewind": 20 } ← { "action": "attached", "channel": "chat:lobby" } → { "action": "publish", "channel": "chat:lobby", "id": "m-1", "data": { "text": "Hello!" } } ← { "action": "message", "channel": "chat:lobby", "id": "1790338000000-0", "data": { "text": "Hello!" }, "timestamp": 1790338000000, "client_id": "user-42" } ← { "action": "ack", "id": "m-1" } ``` The `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`. ### Close codes | Code | Meaning | What to do | | --------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------ | | `1012` | The node is restarting (deploy or maintenance). | Reconnect now, with a small jitter. | | `1013` | The node is full. | Reconnect with exponential backoff. | | `4001` | Invalid or expired token (`realtime.token_invalid`). | Issue a new token and reconnect; repeating the same token will not help. | | `4003` | Realtime disabled for the account (`realtime.disabled`). | Do not reconnect. | | `4029` | The account reached its connection limit (`realtime.connection_limit`). | Reconnect with exponential backoff. | | Any other | Network drop or unexpected error. | Reconnect with exponential backoff. | ## SSE: reading without WebSocket 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`: ```text GET https:///v1/sse?token=&channel=chat:lobby&channel=alerts&rewind=20 ``` * Pass each channel in its own `channel=` (or several in `channels=a,b`), up to . The token needs `subscribe` on all of them. * Each `data:` is a JSON frame identical to the WebSocket's (`connected`, `attached`, `message`, `presence`, `error`); `: ping` lines keep the connection alive. * 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. * To publish, use the [API](#publicar) or a WebSocket. ```js const url = new URL(connectUrl); // the connect_url from the token response const sse = new EventSource( `https://${url.host}/v1/sse?token=${encodeURIComponent(token)}&channel=chat:lobby&rewind=20`, ); sse.onmessage = (event) => { const frame = JSON.parse(event.data); if (frame.action === 'message') console.log(frame.data); }; ``` ## Presence 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. | Client frame | Requires | Effect | | -------------------------------------------------------------------------------- | ----------- | ------------------------------------------------ | | `{"action":"presence","op":"enter","channel":"chat:lobby","data":{…},"id":"p1"}` | `presence` | Enters presence; answers `ack`. | | `{"action":"presence","op":"update","channel":"chat:lobby","data":{…}}` | `presence` | Updates the member's data. | | `{"action":"presence","op":"leave","channel":"chat:lobby"}` | `presence` | Leaves presence. | | `{"action":"presence","op":"get","channel":"chat:lobby","id":"p2"}` | `subscribe` | Answers `op: "sync"` with every `members` entry. | Subscribers 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 members; beyond that, `realtime.presence_limit`. If a node dies without warning, its members disappear from the list within 30 seconds. ### List channel presence Returns the channel's members at this moment. It is useful for showing "who is online" from your server, without opening a connection. ## Reconnection 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. ### Backoff Use exponential backoff **with jitter** (a random value up to the attempt's cap) and a cap of : 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`. ### New token and `retry_after_ms` 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. ### Recovering messages with `rewind` 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. Choose `rewind` based on how long the connection is usually away (up to messages). If the channel had more messages than that during the outage, the difference is lost: the history keeps only the latest ones. ## History Each channel keeps the latest published messages, for up to . `rewind` uses this history; to query it from your server, use the API. ### Get channel history 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 (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). ### Publish a message from the server 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. * `data`: any JSON, up to (added to the `name`). * `name` (optional, up to 128 characters): classifies the message. * `client_id` (optional): the sender subscribers see. The response carries `id`, `timestamp` (Unix milliseconds) and `charged`: `true` when the message went past the monthly allowance and was billed. With `X-Dry-Run: true`, the API validates the channel and the size, neither publishes nor charges, and answers `200` with the estimate: ```json { "success": true, "data": { "cost_units": 0, "within_quota": true, "free_used": 18250, "free_monthly": 200000, "would_charge": false } } ``` `cost_units` uses the backend scale (10,000 units = 1 credit) and is `0` within the allowance. ## Channels and statistics ### List active channels 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 , default 50) and `offset`; the response carries `total` and, when there are more pages, `next_offset` for the next call. ### Get usage and connections 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`. ## Channel webhooks 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). ### Get the channel webhook 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`. ### Configure the channel webhook * `url`: a public `http` or `https` URL; internal or private network addresses are rejected. * `events`: at least one of the events in the table below. * `channels`: channel patterns (a name, a prefix with `*` or `*`), up to . Empty means every channel, **except** `channel.message`: messages are opt-in and require at least one pattern. * `enabled` (optional, default `true`): `false` pauses deliveries without deleting the configuration. The call creates the webhook or replaces the current one, and the `secret` is kept. The configuration is cached for up to 30 seconds. ### Rotate the webhook secret Generates a new `secret` (and the matching `standard_key`). The previous one stops being valid; update your receiver right after. ### Remove the channel webhook Deletes the configuration; no more events are sent. ### Events ### Delivery The platform makes a JSON `POST` to your URL: ```json { "id": "evt_5b0c9a7e-3c1f-4a52-9d57-2a6f1f0e8a11", "type": "presence.member_added", "timestamp": 1790338000000, "account_id": "7a1d4c8e-2f6b-4e9a-b3c5-0d8f1a6e4b27", "data": { "channel": "chat:lobby", "member": { "connection_id": "c_8f3a1d", "client_id": "user-42", "data": { "nick": "ana" }, "timestamp": 1790338000000 } } } ``` `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. * **Signature:** Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature`), using the configuration's `standard_key`; see [Verify the signature](/en/webhooks#verificar-assinatura). * **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 . * **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. ## JavaScript SDK 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. ```bash npm install @respawn/realtime ``` ### On the server `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: ```ts import { RealtimeRest } from '@respawn/realtime'; const rest = new RealtimeRest({ apiKey: process.env.RESPAWN_API_TOKEN!, baseUrl: 'https://api.respawntech.dev', }); // route the browser calls (Express): returns token, expires_at and connect_url app.post('/realtime/token', async (req, res) => { res.json( await rest.createToken({ client_id: req.user.id, capabilities: { 'room:*': ['subscribe', 'publish', 'presence'] }, ttl_seconds: 600, }), ); }); await rest.publish('room:1', 'order', { id: 42, status: 'paid' }); const history = await rest.history('room:1', { limit: 20 }); const members = await rest.presence('room:1'); ``` ### In the browser `Realtime` asks your server for the token through `authUrl` (a `POST` with cookies) or `authCallback`: ```ts import { Realtime } from '@respawn/realtime'; const rt = new Realtime({ authUrl: '/realtime/token' }); rt.on('state', (state, err) => console.log('connection:', state, err?.code)); const room = rt.channel('room:1'); await room.subscribe((m) => console.log(m.name, m.data), { rewind: 20 }); await room.publish('chat', { text: 'hi' }); await room.presence.watch((members) => render(members)); await room.presence.enter({ nick: 'ana' }); ``` The 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). ### React With `@respawn/realtime/react`: ```tsx import { useState } from 'react'; import { Realtime } from '@respawn/realtime'; import { RealtimeProvider, useChannel, usePresence } from '@respawn/realtime/react'; const rt = new Realtime({ authUrl: '/realtime/token' }); export function App() { return ( ); } function Room() { const [msgs, setMsgs] = useState<{ id: string; data: unknown }[]>([]); const { publish } = useChannel('room:1', (m) => setMsgs((all) => [...all, m]), { rewind: 20 }); const { members } = usePresence('room:1', { nick: 'ana' }); return (

      {members.length} online

        {msgs.map((m) =>
      • {JSON.stringify(m.data)}
      • )}
      ); } ``` ## Limits | Limit | Value | | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | Free messages per month (per account) | | | Message size (`data`) | | | Channel name length | characters | | Channels subscribed per connection | | | Publishes per second (per account) | | | Simultaneous connections (per account) | | | Members in a channel's presence | | | Token lifetime | (default), from up to | | Channel patterns per token | up to | | History retention | | | Messages kept and per `rewind` | up to | | Channels per page (`GET /channels`) | up to | | Webhook channel patterns | up to | The values are the platform defaults and the platform can adjust them. API requests also follow the account's [rate limit](/en/cobranca-e-limites). ## Errors 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)). Common validation errors (`validation.failed`, `not_found`) are in [Errors](/en/erros). ## Pricing Billing 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`. # URL Safety Check ## Overview 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. ## Costs 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. ## Endpoints ### Check a URL Query: `url` (required, absolute URL). Results are cached for 1 hour. 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. ### Domain favicon Query: `domain` (required). ## Errors # Web Page to Markdown ## Overview 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`. ## Costs 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. ## Endpoints ### Convert a page to Markdown `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. 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`. 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`). ## Errors # Webhook Relay ## Overview 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). * **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). * **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`. * **Message:** an event you publish (`event_type` + `payload`). * **Delivery:** one message on its way to one endpoint. A message to 3 endpoints creates 3 deliveries, each with its own retries. * **Attempt:** each HTTP call of a delivery, logged with status, latency and the start of the response. * **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). Every 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. ## Getting started **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. **Test the connection** with the [test ping](#webhooks-endpoints-test): a free `webhook.test` event. **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. **Verify the signature** in the receiver and answer `2xx` quickly. See the examples in [Verifying the signature](/en/webhooks#verificar-assinatura). **Track** deliveries in the [message log](#mensagens) and retry what failed. ## Costs Billing 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. The free quota and billing are **per account**: every project shares the same monthly free quota, and a delivery costs the same in any project. 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. ### Retrieve this month's usage 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)). ## Projects 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. * Every account has a **default project** (`is_default: true`, named `Default`), created on first use. It cannot be deleted. * `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. * 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. * The lists (`GET /endpoints`, `/sources`, `/event-types`, `/routes` and `/messages`) take `?project_id=` as a filter; without it, they return every project. * 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`). * Limits and billing stay per account: see [Limits](#limites). By default there is no project limit. ### List projects Paged and searchable: `q` searches the name (case-insensitive), `page` starts at 1 and `page_size` goes up to (default ). 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). ### Create project `name` (required) has up to characters; `description`, up to . ### Retrieve project ### Update project Send only the fields you want to change. ### Delete project 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`. ### Duplicate project Creates a new project with the same description and copies of everything, already wired together: * **Endpoints** with **new secrets** (same URL, events, headers, send rate, format and active flag; the failure history is not copied). * **Sources** with **new `ingest_url`s** (same settings and the same provider secret). Register the new URLs with the provider. * Routes, event types and the canvas layout. 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. ## Endpoints ### Create endpoint * `url` (required): public `http` or `https` URL. Private, local or cloud-metadata addresses are refused (`webhook.invalid_url`). * `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. * `project_id`: the endpoint's project (default: the default project). An endpoint does not move between projects. * `format`: `raw` (default), `slack`, `discord` or `teams`. See [Slack, Discord and Teams](#formatos). * `custom_headers`: up to extra headers sent with every delivery (e.g. a token of your customer). Signature headers, `Content-Type`, `User-Agent` and `Host` cannot be overridden. * `rate_limit_per_sec`: cap of sends per second for this endpoint (`0` = no own cap). * The `secret` is only returned in this response; afterwards, use [Reveal secret](#webhooks-endpoints-secret). Up to endpoints per account, across every project. ### List endpoints Optional `project_id` filter. An automatically disabled endpoint has `is_active: false` and `disabled_reason`; one that is failing has `failing_since`. ### Retrieve endpoint ### Update endpoint Send only the fields you want to change (including `format`). `is_active: true` re-enables a disabled endpoint and clears its failing streak. ### Delete endpoint Pending deliveries to the endpoint are canceled. ### Reveal secret `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. ### Rotate secret For , deliveries carry two signatures in `webhook-signature` (new and old secret), so you can switch the receiver's secret without losing events. ### Send test ping 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`. ### Replay failed deliveries Resends to this endpoint every delivery that ended in failure (`failed`, `exhausted` or `canceled`) since `since` (RFC 3339), up to per call. Use it after fixing a receiver that was down. Each replayed delivery is charged as a new one. ### Slack, Discord and Teams 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: * The body becomes `{"text": "…"}` (Slack and Teams) or `{"content": "…"}` (Discord), with `Content-Type: application/json`. * 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. * Long texts are cut with `…` to fit the platform's limit: characters on Slack, on Discord and on Teams. * The signature headers are still sent, computed over that body; the original headers of a source with `forward_original_headers` are not forwarded. The message stored in the log does not change: the log, retry and recovery work exactly as for `raw` endpoints. ## Messages ### Send message * `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 `*`). * `payload` (required): any JSON, up to . If the event type has a [JSON Schema](#tipos-de-evento) in the project, the payload must match it (`webhook.payload_invalid`, not charged). * `project_id` (optional): the message's project (default: the default project). * `endpoint_ids` (optional): restricts the send to these endpoints, among the project's endpoints subscribed to the event. * `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. * With `X-Dry-Run: true`, nothing is sent or charged: the response shows `deliveries`, `free_deliveries` and `charged_units`. 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_`. ### Send messages in batch Up to 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`. ### List messages 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 . * `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). * `event_type` and `endpoint_id`: exact filters. * `failed=true`: only messages with at least one `failed`, `exhausted` or `blocked_no_credits` delivery. * `q`: search, 1 to 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 . ### Retrieve message 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. | Delivery `status` | Meaning | | -------------------- | ----------------------------------------------------------------------------------------------------------- | | `pending` | Waiting for the next attempt (`next_attempt_at`). | | `sending` | Attempt in progress. | | `succeeded` | The endpoint answered `2xx`. | | `exhausted` | Every attempt failed. Can be retried. | | `failed` | Stopped for good: the endpoint answered `410 Gone`, was disabled or its URL became invalid. Can be retried. | | `canceled` | Canceled, for example because the endpoint was deleted while the delivery was pending. | | `blocked_no_credits` | Message received from a provider without balance for the delivery (see [Out of credits](#sem-creditos)). | ### Retry message Without a body, sends the message again to the active endpoints whose delivery ended in failure (`failed`, `exhausted` or `canceled`). 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. ## Event types 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. ### Declare event type `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 types per account, across every project. ### List event types Optional `project_id` filter. ### Delete event type Sends of that type are still accepted, without schema validation. ## Receiving third-party webhooks 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. | `provider` | How the request is verified | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `stripe` | `Stripe-Signature` header with the Stripe signing secret (`whsec_…`), tolerance of . | | `github` | `X-Hub-Signature-256` header with the GitHub webhook secret. | | `shopify` | `X-Shopify-Hmac-Sha256` header with the Shopify app secret. | | `hmac_generic` | `X-Webhook-Signature: sha256=` header (HMAC-SHA256 of the body). | | `standard_webhooks` | `webhook-id`, `webhook-timestamp` and `webhook-signature` headers. | | `basic_auth` | `Authorization: Basic …` header; the secret is `user:password`. | | `none` | No verification: anyone with the URL can post. Avoid in production. | `hmac_generic`, `standard_webhooks` and `basic_auth` work with any system that is not in the list: pick the one the sending system supports. ### Create ingestion source * `name` and `provider` (required); `project_id` is optional (default: the default project). * `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"`). * `event_type_path`: path in the received JSON holding the event type (e.g. `type`, `data.object.status`). Without it, the type is `ingest`. * `dedupe_key_path`: path of the event identifier (e.g. `id`). A provider resend with the same key answers `200` without creating new deliveries. * `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. * Register the `ingest_url` from the response with the provider. Up to sources per account, across every project. ### List sources Optional `project_id` filter. ### Retrieve source ### Update source 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. ### Delete source The source's routes are deleted with it and the `ingest_url` stops answering. ### Rotate ingestion URL Issues a new `ingest_url`; the previous one stops working immediately. Update the URL at the provider right away. ### Set provider secret An empty secret is only accepted with `provider: "none"`. ### Retrieve last received event 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. ### Send a test event through the source 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`. It is free and outside the monthly free quota. It skips signature verification and deduplication, but counts toward the source's limit of requests per minute. ### Create route 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 ; empty, the route takes every event of the source. Up to routes per source. ### List routes Optional `source_id` and `project_id` filters. ### Update route Changes the route's event types (empty list = all) or pauses it with `is_active: false`. Send only the fields you want to change. ### Delete route ### Receive a provider event 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: | Status | When | | ------ | ------------------------------------------------------------------------------------- | | `200` | Accepted, duplicate (provider resend) or no route for the type. | | `401` | Invalid signature, or provider secret not set yet; nothing is stored. | | `404` | Unknown or disabled source, or tool not enabled for the account. | | `429` | More than requests per minute for the source. | | `5xx` | Storage failed; the provider should retry. | ### Example: Stripe [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. [Create a source](#webhooks-sources-create) with `provider: "stripe"`, `event_type_path: "type"` and `dedupe_key_path: "id"`, still without the secret. 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). [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). Your 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. ### Out of credits 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 , they end as `failed`. ## Dashboard canvas 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). ### Retrieve flow map 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. ### Retrieve canvas layout ### Save canvas layout `positions` stores each card's coordinates: keys `platform`, `source:` or `endpoint:`. Cards without a position are placed automatically by the dashboard. ## Alerts The account owner is notified in the dashboard and by e-mail. Each type can be turned off in the notification preferences (webhooks category). | Type | When | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `webhook_endpoint_failing` | An endpoint starts failing (first failed attempt after being healthy). Not sent again until a `2xx` response ends the streak. | | `webhook_endpoint_disabled` | Automatic disabling turns off an endpoint that has been failing for (not when you pause it, nor on `410 Gone`). | | `webhook_quota` | The month's deliveries reach of the monthly free quota, once per threshold and month. | Alerts never delay or block a send. ## Delivery and retries The retry schedule, what counts as success, `Retry-After`, `410 Gone` and the automatic disabling of endpoints that fail for 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. ## Legacy account API 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)). ## Errors Common validation errors (`validation.failed`, `not_found`) and balance errors (`billing.insufficient_credits`) are in [Errors](/en/erros). ## Limits | Limit | Value | | --------------------------------------- | -------------------------------------------------- | | Free deliveries per month (per account) | | | `payload` / received body size | | | Projects per account | | | Endpoints per account | | | Custom headers per endpoint | | | Event types per account | | | Messages per batch | | | Deliveries per recovery | | | Sources per account | | | Routes per source | | | Event types per route | | | Requests received per source | per minute | | Wait for credits (`blocked_no_credits`) | | | Message log retention | | | Message body search window | | Per-account limits count every project. These are the platform defaults, which the platform may adjust. # Agentes de IA (MCP) 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. 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`. ## Conexión | | | | ----------------- | --------------------------------------------------- | | URL | | | Transporte | Streamable HTTP, sin sesión, respuestas JSON | | Autenticación | `Authorization: Bearer ` | | Scope obligatorio | `mcp:use`, además de los scopes de cada herramienta | Nunca 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. ## Crea una clave para el agente En el [dashboard](https://dashboard.respawntech.dev), en **APIs**, crea un token solo para el agente. Las plantillas rellenan scopes y controles: * **Agente de soporte**: búsqueda en la base de conocimiento y consulta de saldo. * **Agente completo**: todas las herramientas. * **Solo lectura**: solo consultas. Una clave de agente debe tener un **límite diario de gasto** (sugerencia: ) y una **fecha de expiración**. Consulta [Controles de la clave](/es/autenticacao#controles-da-chave). ## Configura el cliente Los clientes que solo hablan stdio usan el puente `mcp-remote`, como en el último ejemplo. ## Herramientas 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. Las 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. ## Parámetros de la conexión 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". Por ejemplo, un agente de soporte que solo lee la base de conocimiento, con respuestas en portugués: Los parámetros que repites en varias conexiones pueden convertirse en un **perfil** en el dashboard (`profile=`), y una clave puede tener un perfil predeterminado. ## Costos y control de gasto * 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). * 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. * `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. * 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`. * El **botón de pánico** del dashboard corta al instante el acceso de todos los agentes de la cuenta (`mcp.agents_disabled`). * El extracto del dashboard desglosa el gasto por canal (API, MCP, dashboard) y por clave. ## Idempotencia 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 devuelve el resultado anterior sin ejecutar ni cobrar de nuevo (`_meta["tech.respawn/replayed"]`). ## Límites | Límite | Valor | | ---------------------------- | --------------------------------------------------------------------------------- | | Solicitudes | por minuto por clave, sumadas a las de la API | | Cuerpo de la solicitud | | | Resultado de una herramienta | | | Llamadas a `tools_search` | por minuto | ## Errores 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"}}`. Los errores de las herramientas son los mismos que en la API; consulta [Códigos de error](/es/erros). # Autenticación ## Token de API Todas las rutas en `/v1/tools/*` requieren un **token de API**, creado en el [dashboard](https://dashboard.respawntech.dev) en **APIs**. | Forma | Ejemplo | | ----------------------------- | ------------------------------------ | | Header dedicado (recomendado) | `X-API-Token: rsk_live_...` | | Authorization | `Authorization: ApiKey rsk_live_...` | | Authorization (alternativo) | `Authorization: Token rsk_live_...` | 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. ## Requisitos de la cuenta Antes de cualquier llamada a las herramientas, la cuenta debe tener: * **Correo electrónico verificado** — de lo contrario, `auth.email_not_verified` (403). * **Saldo no negativo** — una cuenta suspendida devuelve `billing.user_bankrupt` (402). * **Créditos suficientes** — las operaciones de pago sin saldo devuelven `billing.insufficient_credits` (402). ## Scopes 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. 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. ### Combinaciones recomendadas | Caso de uso | Scopes sugeridos | | ---------------------------------------------- | ------------------------------------------------------------------------- | | Generar y gestionar códigos QR | `tools:qr:dynamic:manage` | | Panel de solo lectura de QRs | `tools:qr:dynamic:read` | | Pipeline de PDF | `tools:pdf:manage` + `tools:pdf:read` | | Verificar URLs antes de mostrarlas | `tools:safety:read` | | Asistente que busca en la base de conocimiento | `tools:rag:query` | | Subir y mantener documentos de la base | `tools:rag:manage` | | [Agente de IA (MCP)](/es/agentes-de-ia) | `mcp:use` + los scopes de las herramientas que va a usar + `account:read` | | Integración completa | `tools:manage` (más simple, menos seguro) | ## Controles de la clave 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): * **Expiración**: pasada la fecha, el token deja de funcionar (`auth.unauthorized`). * **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. * **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`). * **Plantillas**: al crear la clave, "Solo lectura", "Agente de soporte" y "Agente completo" rellenan scopes y controles de una vez. La expiración, el límite y las bases se pueden cambiar después; los scopes, no. Para cambiar los scopes, crea otra clave. ## Errores de autenticación ## Dashboard vs. token de API * **Dashboard** — la navegación en el panel no consume la cuota diaria gratuita ni debita lecturas medidas. * **Token de API** — las lecturas medidas comparten la [cuota diaria gratuita](/es/cobranca-e-limites#franquia-diaria) de solicitudes ( en cuentas premium). Después de eso, cada lectura debita créditos. ## Ejemplo Crear un QR con dry-run para validar el token, los scopes y el costo sin debitar nada: # Cobros y límites ## Modelo de créditos * El saldo se muestra en **créditos** en el dashboard, y todos los valores de esta documentación están en créditos. * Las operaciones de pago se debitan al confirmarse — nunca en el dry-run. * 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). * Cada token puede tener un [límite diario de gasto](/es/autenticacao#controles-da-chave): superarlo responde `billing.spend_limit_reached`. 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. ## Dry-run y vista previa de costo | Header | Efecto | | ---------------------- | -------------------------------------------------- | | `X-Dry-Run: true` | Valida y calcula el costo sin persistir ni debitar | | `X-Preview-Cost: true` | Alias de `X-Dry-Run` | | `X-Include-Cost: true` | Incluye `meta.cost` en la respuesta | El 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. Las 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. ### Presupuesto de una operación de pago En los `POST` de creación y conversión, `data` incluye el presupuesto detallado por ítem: ```json { "success": true, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "data": { "total_cost_credits": 15000, "total_cost_units": 15000, "items": [ { "description": { "key": "billing.pdf.convert_base" }, "cost_credits": 10000, "type": "base" }, { "description": { "key": "billing.pdf.retention.7" }, "cost_credits": 5000, "type": "storage" } ], "is_monthly_recurring": false, "currency": "CREDITS" } } ``` 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. En 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`. ### Vista previa de una lectura medida 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: ```json { "success": true, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "data": { "operation": "qr_dynamic.analytics", "metering": { "within_quota": true, "quota_limit": 2000, "quota_used": 125, "quota_remaining": 1875, "cost_units": 300, "would_charge": false } }, "meta": { "cost": { "units": 300, "credits": 0.03, "charged": false, "within_quota": true, "quota_limit": 2000, "quota_used": 125, "quota_remaining": 1875 } } } ``` Con `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ó). ## Idempotencia Envía `X-Idempotency-Key` (se recomienda un UUID) en cualquier ruta de herramientas: * la clave es válida por ****; * misma clave + mismo payload → se reutiliza la respuesta original, sin un nuevo cobro; * misma clave + payload distinto → `idempotency.conflict` (409). Úsala siempre en los `POST` de creación y conversión para que los reintentos de red no cobren dos veces. Excepció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). ## Rate limit Cada token de API puede hacer ** solicitudes por minuto** en las rutas `/v1/tools/*`. Cada respuesta informa el estado: ```http X-RateLimit-Limit: 60 X-RateLimit-Remaining: 42 X-RateLimit-Reset: 1712345678 ``` Al excederlo, la API devuelve `rate_limit.exceeded` (429). Espera hasta `X-RateLimit-Reset` antes de reenviar. ## Reintentar con espera (retry) Reintenta solo lo que puede salir bien en el siguiente intento: * **`429`**: espera hasta la hora de `X-RateLimit-Reset` (segundos Unix). * **`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. * **Demás `4xx`** (validación, scope, saldo): no reintentes. La respuesta no cambia hasta que corrijas la solicitud. En 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. ```js // Reintenta 429, 5xx y fallos de red; devuelve los demás errores de inmediato. async function withRetry(send, attempts = 5) { for (let attempt = 1; ; attempt++) { let response; try { response = await send(); } catch (error) { if (attempt === attempts) throw error; // fallo de red } if (response && response.status !== 429 && response.status < 500) return response; if (attempt === attempts) return response; const reset = Number(response?.headers.get('X-RateLimit-Reset')); const wait = response?.status === 429 && reset ? Math.max(0, reset * 1000 - Date.now()) : Math.min(2 ** (attempt - 1) * 1000, 30_000) * (0.5 + Math.random()); await new Promise((resolve) => setTimeout(resolve, wait)); } } const key = crypto.randomUUID(); // la misma clave en todos los intentos const response = await withRetry(() => fetch(url, { method: 'POST', headers: { 'X-API-Token': process.env.RESPAWN_API_TOKEN, 'X-Idempotency-Key': key, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }), ); ``` ```python import os, random, time, uuid import requests # Reintenta 429, 5xx y fallos de red; devuelve los demás errores de inmediato. def with_retry(send, attempts=5): for attempt in range(1, attempts + 1): try: response = send() except (requests.ConnectionError, requests.Timeout): if attempt == attempts: raise response = None if response is not None and response.status_code != 429 and response.status_code < 500: return response if attempt == attempts: return response reset = response.headers.get("X-RateLimit-Reset") if response is not None else None if response is not None and response.status_code == 429 and reset: wait = max(0, int(reset) - time.time()) else: wait = min(2 ** (attempt - 1), 30) * (0.5 + random.random()) time.sleep(wait) key = str(uuid.uuid4()) # la misma clave en todos los intentos response = with_retry(lambda: requests.post( url, json=payload, headers={"X-API-Token": os.environ["RESPAWN_API_TOKEN"], "X-Idempotency-Key": key}, timeout=30, )) ``` ## Cuota diaria gratuita de lecturas Las lecturas hechas con **token de API** (listados, estado, analytics, descargas…) cuentan para una cuota diaria gratuita compartida entre todas las herramientas: | Ítem | Valor | | -------------------------- | -------------------------------------- | | Lecturas gratuitas por día | **** | | Cuentas premium | **** | | Reinicio | | Cada lectura medida devuelve la posición en la cuota: ```http X-API-Quota-Limit: 2000 X-API-Quota-Used: 847 X-API-Quota-Remaining: 1153 ``` Las solicitudes hechas desde el [dashboard](https://dashboard.respawntech.dev) **no** cuentan para la cuota ni se cobran. ### Lecturas medidas Una vez agotada la cuota, cada lectura vía API debita: Las operaciones de escritura sin costo propio (`PATCH`, activar, desactivar, `DELETE`) y el dry-run de creación siempre son gratuitos. 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. ## Timeouts * La mayoría de las rutas: ****. * Descargas de archivos: ****. # Códigos de error ## Respuesta exitosa ```json { "success": true, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "data": {}, "meta": {} } ``` | Campo | Descripción | | ---------- | ----------------------------------------------------- | | `success` | `true` en respuestas correctas | | `trace_id` | ID de correlación (también en el header `X-Trace-Id`) | | `data` | Recurso o resultado | | `meta` | Opcional — costo (`meta.cost`), paginación, etc. | Con `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`. ## Formato de error ```json { "success": false, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "error": { "code": "billing.insufficient_credits", "message": "Créditos insuficientes para esta operação.", "details": { "required": 100, "available": 50 } } } ``` | Campo | Descripción | | --------------- | ------------------------------------------------------------------------------------------------------ | | `error.code` | Identificador estable — maneja los errores por él, nunca por el `message` | | `error.message` | Texto legible; puede cambiar entre versiones | | `error.details` | Contexto estructurado opcional; en `validation.failed`, `details.fields` incluye los errores por campo | | `trace_id` | Inclúyelo en los tickets de soporte | ## Errores comunes a todas las herramientas ### Autenticación y permisos ### Créditos y cuenta ### Solicitud y límites ### Agentes de IA (MCP) ### Plataforma ### Vídeo ## Errores por herramienta Los 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í. ## Buenas prácticas 1. Maneja `402` con un flujo de recarga en tu sistema. 2. En `429`, respeta `X-RateLimit-Reset` antes de volver a intentar. 3. Usa `X-Dry-Run` para detectar `validation.failed` sin costo. 4. Reintenta solo los errores `5xx` y de red, siempre con la misma `X-Idempotency-Key`. 5. Registra el `trace_id` de toda respuesta con error. # Documentación de la API ## Herramientas ## Primeros pasos ### Crea un token de API 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). ### Estima el costo con dry-run 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). ### Haz la llamada Todas las rutas de herramientas usan el prefijo `https://api.respawntech.dev/v1/tools`. Cada endpoint incluye ejemplos listos en cURL, JavaScript y Python. ## Headers comunes | Header | Valor | Efecto | | ------------------- | -------------- | --------------------------------------------------------------------- | | `X-API-Token` | `rsk_live_...` | Autenticación (recomendado) | | `X-Dry-Run` | `true` | Simula la operación sin persistir ni debitar | | `X-Include-Cost` | `true` | Incluye `meta.cost` en la respuesta | | `X-Idempotency-Key` | UUID | Evita cobros duplicados en reenvíos () | Las respuestas incluyen `X-Trace-Id` (úsalo en tickets de soporte), `X-RateLimit-*` y, en lecturas medidas, `X-API-Quota-*`. ## Guías # Usar con IA 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**. ## Dos servidores MCP | | MCP de documentación | [MCP de la cuenta](/es/agentes-de-ia) | | ------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------- | | Para qué | Responder preguntas sobre la API mientras programas | Usar las herramientas con tu cuenta (crear un QR, convertir un PDF, transcribir audio…) | | URL | | | | Autenticación | Ninguna | Token de API con `mcp:use` | | Costo | Gratis | El mismo que la API | | Acceso | Solo lectura de la documentación pública | Actúa en la cuenta, con alcances y límite de gasto | Los dos pueden estar conectados a la vez: un agente consulta la documentación y después usa el MCP de la cuenta para ejecutar. El MCP de documentación se está habilitando de forma gradual. Mientras no esté activo, responde `404` con el código `mcp.disabled`. El llms.txt y las páginas en Markdown ya funcionan. ## Conexión | | | | ------------- | ------------------------------------------------------------------------- | | URL | | | Transporte | Streamable HTTP | | Autenticación | Ninguna (no envíes token) | | Costo | Gratis | | Límite | solicitudes por minuto por IP | | Idioma | Parámetro `lang` opcional en cada herramienta: `pt-br`, `en`, `es` o `fr` | Las respuestas salen del mismo contenido de este sitio, incluidos los precios vigentes. ## Configura el cliente En **Claude.ai** y **ChatGPT**, añade un conector personalizado (custom connector) e indica solo la URL . No hace falta login ni token. Los clientes que solo hablan stdio usan el puente `mcp-remote`, como en el último ejemplo. ## Herramientas Un 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. ## Sin MCP: llms.txt y Markdown Para agentes que solo leen la web: * [`/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. * [`/llms-full.txt`](/llms-full.txt): todas las páginas en un solo archivo de texto. * Páginas en Markdown: `/md/.md` en portugués y `/md//.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`. * [`/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. # Webhooks 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: | Origen | Dónde se configura | Costo | | ------------------------------------------------------ | ----------------------------------------------------------------- | ------------------------------------------------- | | **Eventos de la cuenta** (p. ej. `qr.dynamic.scanned`) | **Webhooks** en el [dashboard](https://dashboard.respawntech.dev) | Gratis | | **Callbacks de jobs** (p. ej. conversión de PDF) | `webhook_url` en cada solicitud | Gratis | | **Webhook Relay** (tus propios eventos) | API de [Webhook Relay](/es/ferramentas/webhook-relay) | Por entrega más allá de la cuota mensual gratuita | ## Eventos de la cuenta 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. Estos 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). ## Solicitud enviada ```http POST Content-Type: application/json User-Agent: RespawnTech-Webhook/2.0 webhook-id: msg_2Vf8tK1pQ9zX webhook-timestamp: 1790338000 webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= X-Webhook-Signature: sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd ``` | Header | Contenido | | --------------------- | ------------------------------------------------------------------------------------------------------- | | `webhook-id` | Identificador del mensaje. Es el **mismo en todos los reintentos**: úsalo para descartar duplicados. | | `webhook-timestamp` | Momento de este intento, en segundos Unix. | | `webhook-signature` | `v1,`, una firma por secret activo, separadas por espacio (dos durante una rotación de secret). | | `X-Webhook-Signature` | Firma heredada `sha256=`, sin timestamp. Se mantiene para receptores antiguos. | Los 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`). ### Cuerpo Los eventos de la cuenta y los mensajes de Webhook Relay usan el mismo sobre: ```json { "event_id": "550e8400-e29b-41d4-a716-446655440000", "type": "qr.dynamic.scanned", "timestamp": 1790338000, "payload": { "resource_id": "…", "slug": "promo-verao", "ip": "hash_anonimo", "user_agent": "Mozilla/5.0 …", "referer": "https://…" } } ``` El 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. ## Validar la firma El contenido firmado es `webhook-id + "." + webhook-timestamp + "." + cuerpo en bruto`, con HMAC-SHA256 y el resultado en base64. Para validar: 1. Lee el **cuerpo en bruto**, antes de cualquier parseo de JSON (reformatear el JSON cambia la firma). 2. Rechaza timestamps con más de de diferencia con tu reloj (protección contra replay). 3. Calcula la firma y compárala, **en tiempo constante**, con cada elemento de `webhook-signature`. Basta con que coincida uno. Con una biblioteca Standard Webhooks, usa el `standard_webhooks_secret` (formato `whsec_…`). Sin biblioteca, la clave del HMAC son los bytes del `secret` en bruto. ```js // npm i standardwebhooks import express from 'express'; import { Webhook } from 'standardwebhooks'; const wh = new Webhook(process.env.WEBHOOK_SECRET); // whsec_… const app = express(); app.post('/hook', express.raw({ type: '*/*' }), (req, res) => { let event; try { event = wh.verify(req.body, req.headers); // lanza un error si no es válido } catch { return res.sendStatus(401); } // procesa `event` de forma asíncrona y responde rápido res.sendStatus(204); }); ``` ```python # pip install standardwebhooks import os from flask import Flask, request from standardwebhooks.webhooks import Webhook wh = Webhook(os.environ["WEBHOOK_SECRET"]) # whsec_… app = Flask(__name__) @app.post("/hook") def hook(): try: event = wh.verify(request.get_data(), dict(request.headers)) except Exception: return "", 401 # procesa `event` de forma asíncrona y responde rápido return "", 204 ``` ```go // Sin biblioteca: secret es el valor en bruto (no el whsec_). func verify(secret string, r *http.Request, body []byte) bool { id, ts := r.Header.Get("webhook-id"), r.Header.Get("webhook-timestamp") t, err := strconv.ParseInt(ts, 10, 64) if err != nil || math.Abs(time.Since(time.Unix(t, 0)).Seconds()) > 300 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(id + "." + ts + ".")) mac.Write(body) want := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil)) for _, sig := range strings.Fields(r.Header.Get("webhook-signature")) { if hmac.Equal([]byte(sig), []byte(want)) { return true } } return false } ``` 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. ## Secrets y rotación | Destino | Dónde obtener el secret | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | Endpoint de eventos de la cuenta | Al crearlo, o con la opción de revelar el secret en **Webhooks** del dashboard. | | Endpoint de Webhook Relay | Al crearlo, o con [Revelar secret](/es/ferramentas/webhook-relay#webhooks-endpoints-secret). | | 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). | Todo secret viene en dos formas: `secret` (en bruto) y `standard_webhooks_secret` (`whsec_` + base64 de los mismos bytes). Ambas validan las mismas firmas. Al [rotar el secret](/es/ferramentas/webhook-relay#webhooks-endpoints-rotate_secret) de un endpoint, el anterior sigue firmando durante : en ese periodo `webhook-signature` trae las dos firmas y actualizas el receptor sin perder eventos. ## Entrega y reintentos 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: | Intento | Espera antes | | ------- | ------------ | | 1 | — | | 2 | 5 segundos | | 3 | 5 minutos | | 4 | 30 minutos | | 5 | 2 horas | | 6 | 5 horas | | 7 y 8 | 10 horas | Son unas 27 horas en total, con una variación de ±10%. Casos especiales: * **`429` y `503`** con `Retry-After`: el siguiente intento respeta el header cuando pide más tiempo (hasta 10 horas). * **`410 Gone`**: la entrega se detiene y el endpoint se desactiva. * 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). Buenas prácticas en el receptor: * Responde **2xx** lo antes posible y procesa el evento de forma asíncrona. * **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ó). * No dependas del orden de llegada; usa el `timestamp` del cuerpo cuando el orden importe. ### Desactivación automática Un endpoint que falla sin parar durante 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. El historial de mensajes, entregas e intentos se guarda durante . ## Callback de jobs 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: ```json { "job_id": "job_abc123", "success": true, "result_url": "https://…", "metadata": { "conversion_id": "…" } } ``` En 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`. ## Tus propios eventos 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). # Audio ## Visión general Toda operación de audio es **asíncrona** y se cobra al instante: 1. `POST` a la operación → **202 Accepted** con `job_id`, `status: pending` y `poll_after_seconds`. 2. Un worker procesa en segundo plano. 3. 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. Los 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. 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`…). ## Entrada del audio Las operaciones que reciben un audio piden **exactamente uno** de estos campos: | Campo | Qué es | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `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 | | `audio_base64` | El audio inline en base64, hasta decodificado | | `file_id` | Id de un archivo de audio ya subido a la cuenta | Formatos aceptados: mp3, wav, ogg, m4a, flac y webm. El límite es de de audio y 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. Todas 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. ## Costos El "minuto iniciado" se redondea hacia arriba. Un audio cuya duración no se puede leer del encabezado se cobra por el tope de 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). ### Dry-run 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: ```json { "success": true, "data": { "service_code": "audio_transcribe_minute", "units": 3, "rate": 15000, "total_units": 45000, "duration_ms": 125000, "duration_known": true, "parts": [{ "service_code": "audio_transcribe_minute", "units": 3, "rate": 15000, "total": 45000 }] } } ``` `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`. ## Operaciones ### Transcribir audio `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. ### Generar voz a partir de texto `text` acepta hasta 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. ### Listar las voces Catálogo cerrado de voces de `speak` y de `revoice`. `language` filtra por idioma. No se cobra. ### Generar efecto de sonido o música `kind`: `effect` (1 a , por defecto 5 s) o `music` (instrumental, 1 a , 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. ### Analizar audio 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. ### Convertir o normalizar audio `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. ### Resumir audio 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. ### Traducir audio `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). ### Volver a hablar la grabación con otra voz 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`. ### Listar jobs Los jobs más recientes primero, con `limit` (por defecto 20, hasta ) 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 (). ### Estado, resultado y descarga 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). `status`: `pending`, `processing`, `completed` o `failed`. ## Voces clonadas y consentimiento (solo MCP) 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`). * **`voice_create`** clona la voz **del titular de la cuenta** a partir de una muestra de . 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`. * **`voice_list`** lista las voces de la cuenta (`voice_id`, nombre, idioma, `status`, vencimiento). Gratis. * **`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. ### Uso responsable * **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. * **Nada de imitación.** Ni el habla, ni el `revoice`, ni la generación de sonidos aceptan imitar artistas, voces u obras existentes. * **Límites:** hasta voces clonadas por cuenta, creaciones por día y usos por hora (`audio.voice_limit`, `audio.voice_rate_limited`). * **Datos guardados:** la muestra permanece cifrada hasta 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 . * **Avisa a quien escucha:** si publicas audio sintético, di que la voz es generada, sobre todo cuando sea una voz clonada. ## Scopes | Scope | Operaciones | | ------------------------ | ---------------------------------------------------------------------------------------- | | `tools:audio:transcribe` | `transcribe`, `summarize`, `translate` | | `tools:audio:speak` | `speak`, `generate`, `revoice` (en MCP, también `voice_create` y `voice_delete`) | | `tools:audio:process` | `analyze`, `convert` | | `tools:audio:read` | `voices`, `jobs`, `jobs/{id}` (los tres scopes anteriores también leen sus propios jobs) | `tools:manage` cubre todo. La lista completa está en [Autenticación](/es/autenticacao). ## Errores `audio.*` en la respuesta o, en jobs que fallan después de aceptados, en el `error_code` del job. # Base de conocimiento ## Visión general 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. **Crea una base** con el idioma de los documentos (mejora la búsqueda por palabras clave). **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. **Busca** con la pregunta del usuario y usa los fragmentos devueltos como contexto de la respuesta. **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). **Registra feedback** (opcional): los fragmentos confirmados suben en las próximas búsquedas similares y los rechazados desaparecen. ## Costos 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). La 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. ## Bases ### Crear base * `name` (obligatorio, hasta 120 caracteres) y `description` (hasta 1.000). * `language`: `simple` (predeterminado), `portuguese`, `english`, `spanish` o `french`. Define el diccionario de la búsqueda por palabras clave. * `default_mode`: `fast` (predeterminado) o `accurate` (rerank adicional, más preciso y algo más caro). * 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`). ### Listar bases También devuelve el uso de la cuenta respecto a los límites (`usage`). ### Consultar base ### Filtros disponibles 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"]}`. ### Modificar base 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. ### Reindexar base Reprocesa todos los documentos con la configuración actual de la base. Cada fragmento se cobra como en una ingesta nueva. ### Panel de salud de la base 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. ### Índice de los documentos 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. ### Encontrar duplicados y contradicciones 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. ### Cambiar el modelo de embeddings 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. ### Marcar documentos como desactualizados 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. ### Eliminar base La eliminación es asíncrona y borra documentos, fragmentos y feedback. ## Documentos ### Añadir o reemplazar documento Seguridad…", "source_uri": "https://ayuda.example.com/articulos/42", "metadata": { "categoria": "cuenta" } }` }} > * `content` (obligatorio): texto plano o Markdown en UTF-8, hasta . Para PDF, Office e imágenes, usa [Importar archivo](#rag-documents-file). * `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. * `paths` (hasta 50) y `commit`: los archivos de los que viene el documento, para [marcarlo como desactualizado](#rag-collections-changes) cuando cambien. * Los secretos reconocidos en el texto (claves de API, tokens) se sustituyen por `[REDACTED:tipo]` antes de indexar; `redactions` cuenta cuántos. * `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. * 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. ### Importar archivo (PDF, Office, imagen) 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 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`). ### Consultar importación de archivo ### Listar documentos 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. ### Consultar documento ### Modificar título o metadatos No reprocesa el contenido y no cobra nada. ### Ver secciones indexadas Muestra cómo se dividió el documento; útil para ajustar títulos y tamaño de los fragmentos. ### Reprocesar documento fallido Solo para documentos con `status: failed`; los demás responden `rag.document_not_failed`. ### Eliminar documento ## Fuentes sincronizadas 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::…`, 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. | `kind` | `start_url` | Qué importa | | --------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `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. | | `sitemap` | URL del sitemap (o índice) | Importa las páginas listadas, hasta `max_pages`. `max_depth` no se aplica. | | `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. | | `gdrive` | URL de una carpeta `/folders/` | Carpeta compartida como "cualquiera con el enlace"; los Docs se convierten a Markdown. Solo existe si el servidor tiene la integración activada. | ### Listar fuentes sincronizadas ### Crear fuente sincronizada * `name` (hasta 200) y `start_url` (http o https) son obligatorios; `kind` predeterminado `site`. * `max_pages`: 1 a 1000 (predeterminado 100). `max_depth`: 1 a 10 (predeterminado 3). * `frequency_hours`: `0` (solo manual), `6`, `24` (predeterminado) o `168`. * 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. ### Modificar fuente Todos los campos son opcionales. Con `enabled: false` o `frequency_hours: 0`, `next_sync_at` queda nulo. ### Sincronizar ahora Responde `409` si la fuente ya se está sincronizando. `pages_total`, `pages_new`, `pages_changed` y `pages_removed` resumen la ejecución. ### Historial de sincronizaciones ### Eliminar fuente ## Búsqueda ### Modos de búsqueda y reranker * **`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. * **`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`). * **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`. * Si el servicio de embeddings falla, la búsqueda aún responde solo por palabras clave (`mode_used: lexical`), cobrada como caché. ### Buscar en la base * `query` (obligatorio, hasta caracteres). * `top_k`: cuántos fragmentos devolver (predeterminado 5, máximo ). * `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ó. * `filters`: solo fragmentos de documentos cuyos metadatos contienen estos valores. * `min_score` (solo en `accurate`) y `max_per_document` (0–20) afinan el resultado. * `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). * `highlights` marca, en posiciones de carácter Unicode, los términos encontrados en `content`. Las consultas repetidas en menos de se responden desde la caché (`cached: true`) y cuestan menos. Los fragmentos devueltos son contenido de tus documentos. Al pasarlos a un modelo de lenguaje, trátalos como datos, no como instrucciones. ## Feedback de relevancia ### Registrar feedback de relevancia * `label`: `positive` (el fragmento responde a la pregunta) o `negative`. * `end_user_ref` (opcional): identifica quién evaluó, cuando el voto viene de tu usuario final. Solo se guarda como hash. * Hasta votos por minuto. Las preguntas similares se agrupan, y el voto pasa a valer para todo el grupo. ### Listar feedback ### Eliminar un grupo de feedback ## Rerank independiente ### Reordenar textos (rerank) Ordena **tu propia** lista de textos por relevancia respecto a la pregunta, sin ninguna base de por medio. Hasta textos por llamada (cada uno se corta en 2.000 caracteres). `index` apunta a la posición en el array enviado. ## Respuesta con citas ### Responder con citas 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). * 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. * 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). * `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. * 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. * 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. ## Widget de chat incrustable 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. ```html ``` Atributos: `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`. ### Crear clave pública (widget) * `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. * `rate_per_minute`: 1 a 600 (predeterminado 30), por clave. `generate` activa el texto generado por IA en las respuestas de esa clave. * Hasta 20 claves por base. La `key` aparece **una sola vez**; guardamos solo el hash. 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. ### Listar claves públicas ### Revocar clave pública ### Responder desde el widget (ruta pública) 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). ### Script del widget ## Analytics de preguntas ### Analytics de las preguntas 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). * `answer_rate`: respondidas ÷ total de preguntas. * `feedback_ctr`: respondidas que recibieron voto (por el `query_id`) ÷ respondidas. * `positive_rate`: votos positivos ÷ votos. * `top_questions` y `unanswered_questions`: hasta 20 preguntas agrupadas cada una, para que descubras qué escribir en la base. 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. ## Enrutamiento ### Decidir entre opciones (enrutamiento) 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. ## Precios y límites de la cuenta ### Consultar precios ### Consultar límites Valores vigentes de tu cuenta (ya con la bonificación del plan), útiles para validar un envío antes de hacerlo. ## Eventos (webhooks) Registra un endpoint en el dashboard para recibir: * `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. * `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". El envío es de mejor esfuerzo, como en los demás eventos. Consulta la [lista de eventos](/es/webhooks). ## Usar por MCP 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. ## Errores Los códigos sin estado HTTP aparecen en el campo `error_code` de un documento con `status: failed`. ## Límites | Límite | Valor | | ------------------------------------------- | -------------------------------------------- | | Tamaño de un documento | | | Bases por cuenta | | | Fragmentos indexados por cuenta | | | Documentos en procesamiento al mismo tiempo | | | Tamaño de la pregunta | caracteres | | Fragmentos por búsqueda (`top_k`) | | | Textos por rerank independiente | | | Votos de feedback por minuto | | | Archivo importado | 50 MB | | Páginas por fuente | 1.000 (predeterminado 100) | | Claves públicas por base | 20 | Las 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`). # Extractor de errores de CI ## Visión general 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`. ## Costos 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. ## Endpoints ### Extraer el error de un log `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. ## Errores # Memoria de aprendizajes ## Cómo funciona Un aprendizaje es un texto corto con un `kind`, ligado a un `project`: | `kind` | Para guardar | | ------------ | ---------------------------------------------- | | `pitfall` | Trampas y errores que no deben repetirse. | | `decision` | Decisiones tomadas y el motivo. | | `convention` | Convenciones del proyecto. | | `howto` | Cómo hacer algo (ejecutar, publicar, depurar). | | `other` | Lo que no cabe en los demás. | Los 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. También está disponible en el [MCP](/es/agentes-de-ia) como `learning_save`, `learning_recall` y `learning_forget` (toolset `learnings`). ## Duplicados e ingesta asíncrona * **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. * **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. ## Costos 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.*`). ## Endpoints ### Guardar aprendizaje `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`. ### Recuperar aprendizajes `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. ### Olvidar aprendizaje # Notificaciones ## Cómo funciona 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**: * **Canal elegido:** el `channel` de la solicitud, por el nombre del canal. * **Canal predeterminado:** el canal marcado con `is_default`, cuando la solicitud no trae `channel`. * **Correo de la cuenta:** sin ningún canal configurado, la notificación va al correo de la cuenta. Tambié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. ## Canales Cada canal tiene un `name` único en la cuenta y un `kind`: | `kind` | Campos | Cómo entrega | | --------- | ----------------- | ------------------------------------------------------------------------------------------- | | `email` | ninguno | Envía **solo al correo de la cuenta**. No se puede elegir otro destinatario. | | `webhook` | `url`, `secret` | Hace un `POST` a la URL, firmado como en el [Webhook Relay](/es/ferramentas/webhook-relay). | | `ntfy` | `server`, `topic` | Publica en el tópico de [ntfy](https://ntfy.sh) del servidor indicado. | `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. En servidores públicos de ntfy, quien conozca el tópico lee las notificaciones. Usa un nombre largo e impredecible, o un servidor propio. ## Límites, retry e historial * **Rate limit:** hay un límite de notificaciones por período; por encima de él el envío falla con `notify.rate_limited`. * **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. * **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**. ## Costos 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. ## Endpoints ### Enviar notificació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. ### Historial de envíos Paginado por `page` y `page_size` (hasta 100). No incluye el cuerpo de los mensajes. ### Listar canales ### Crear canal `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`. ### Consultar canal ### Modificar canal Envía solo los campos que cambian. Un canal desactivado responde `notify.channel_disabled` cuando se elige en un envío. ### Eliminar canal ### Probar canal Envía una notificación de prueba por el canal, para comprobar la configuración antes de depender de él. ## Errores # PDF Convert ## Descripción general PDF Convert es **asíncrono**: 1. `POST /convert` o `POST /batch` → **202 Accepted** con el `conversion_id` (el `{id}` de las demás rutas). 2. Un worker renderiza en segundo plano (Chromium headless). 3. Sigue el progreso mediante el [callback `webhook_url`](/es/webhooks#pdf-callback) o consultando `GET /{id}`. 4. Descarga con `GET /{id}/download` cuando `status` sea `completed`. 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. ## Costos El batch acepta hasta ** í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). ## Conversión ### Convertir HTML a PDF/PNG/JPG

      Fatura

      ", "css": "body { font-family: sans-serif; }", "format": "pdf", "page_size": "A4", "retention_days": 7 }`, }} > 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`). | Campo | Límite / valores | | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `html` | máx. | | `url` | alternativa a `html`: dirección pública `http`/`https`, hasta 2048 caracteres (`pdf.invalid_url` si es privada o inválida) | | `css` | máx. | | `format` | `pdf` (predeterminado), `png`, `jpg` / `jpeg` | | `file_name` | nombre del archivo generado | | `title`, `author`, `subject`, `keywords` | metadatos del documento, hasta 1000 caracteres cada uno | | `header_html` / `footer_html` | encabezado y pie de página repetidos en cada página | | `google_fonts_url` | URL de CSS de Google Fonts a cargar | | `wait_selector` / `wait_delay` | espera a que aparezca un selector / un retraso en ms antes de renderizar | | `enable_js` / `custom_js` | ejecuta JavaScript en sandbox (SPAs, gráficos) / script adicional | | `data` | objeto con variables para los placeholders `{{campo}}` en el HTML | | `retention_days` | `0` (efímero, \~24 h), `1`–`30` o `-1` (permanente) | | `webhook_url` | recibe el [callback](/es/webhooks#pdf-callback) al terminar | | `template_id`, `template_version`, `font_ids`, `post_process` | [plantillas guardadas](#templates), [fuentes](#fontes) y [posprocesamiento](#pos-processamento) | Tamaño de página, márgenes, medio, numeración y calidad están en [Opciones de página y renderizado](#opcoes-de-pagina). Guarda el `conversion_id`: es el `{id}` que se usa para consultar el estado y descargar el archivo.
      ### Batch con plantilla

      {{title}}

      ", "css": "h1 { color: #333; }" }, "config": { "page_size": "A4", "format": "pdf", "retention_days": 7 }, "items": [ { "custom_id": "fatura-001", "file_name": "fatura-001.pdf", "data": { "title": "Fatura 001" } }, { "custom_id": "fatura-002", "file_name": "fatura-002.pdf", "data": { "title": "Fatura 002" } } ], "delivery": { "method": "zip", "webhook_url": "https://example.com/hooks/pdf" } }`, }} > | Campo | Descripción | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `template.html` | **obligatorio** — HTML con placeholders `{{campo}}` (sintaxis de Go template: `{{if}}` y `{{range}}` también funcionan) | | `template.css` | CSS compartido | | `items[]` | **obligatorio**, de 1 a ítems: `custom_id` (**obligatorio**, tu identificador), `file_name` y `data` | | `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` | | `delivery.method` | `zip` o `list`: cómo se entrega el resultado al terminar | | `delivery.webhook_url` | recibe el [callback](/es/webhooks#pdf-callback) al terminar | 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).
      ### Reprocesar una falla

      Fatura corrigida

      ", "format": "pdf", "page_size": "A4" }`, }} > 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.
      ## Opciones de página y renderizado 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). | Campo | Valores | | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `page_size` | `A3`, `A4` (predeterminado), `A5`, `Letter`, `Legal`, `Tabloid` o `Custom` | | `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` | | `orientation` | `portrait` (predeterminado) o `landscape` | | `margin_top`, `margin_bottom`, `margin_left`, `margin_right` | márgenes de la página | | `scale` | escala del renderizado | | `print_background` | imprime colores e imágenes de fondo (predeterminado `true`) | | `media_type` | `print` o `screen`: qué `@media` ve el CSS | | `color_scheme` | `light` o `dark`: valor de `prefers-color-scheme` | | `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` | | `page_number_format` | texto del contador, con `{page}` y `{total}` (predeterminado `{page} / {total}`, hasta 200 caracteres). Requiere `page_numbers` | | `jpeg_quality` | de 1 a 100 (predeterminado 90). Solo vale con `format: jpg` | Ejemplo: informe en papel de 8 x 11 pulgadas, con contador en el pie: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "url": "https://example.com/relatorio", "page_size": "Custom", "paper_width": 8, "paper_height": 11, "media_type": "print", "page_numbers": "footer-center", "page_number_format": "Página {page} de {total}" }' ``` ## Posprocesamiento `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). | Campo | Descripción | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `post_process.watermark.text` | texto de la marca de agua, hasta 200 caracteres | | `post_process.watermark.image` | alternativa al texto: imagen PNG o JPEG en base64, hasta 1 MiB. Envía **exactamente uno** entre `text` e `image` | | `watermark.font_size` | de 1 a 300 | | `watermark.color` | color en formato `#rrggbb` | | `watermark.opacity`, `watermark.scale` | de 0 a 1 | | `watermark.rotation` | de -180 a 180 grados | | `watermark.position` | `center`, `top-left`, `top-center`, `top-right`, `left`, `right`, `bottom-left`, `bottom-center` o `bottom-right` | | `watermark.on_top` | `true` dibuja la marca sobre el contenido | | `post_process.pdfa` | `true` convierte a PDF/A-2b (archivado a largo plazo) | | `post_process.protection.user_password` | **obligatoria** si hay `protection`, hasta 128 caracteres: contraseña para abrir el archivo | | `protection.owner_password` | contraseña de propietario, hasta 128 caracteres | | `protection.permissions` | qué puede hacer el lector: `print`, `copy`, `modify`, `annotate`, `fill_forms`, `assemble`. Lista vacía = ningún permiso | `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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "html": "

      Contrato

      ", "format": "pdf", "post_process": { "watermark": { "text": "CONFIDENCIAL", "color": "#cc0000", "opacity": 0.2, "rotation": -45, "position": "center" }, "protection": { "user_password": "contraseña-lector", "permissions": ["print"] } } }' ``` ## Plantillas guardadas 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. * 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. * 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. * Plantilla o versión inexistente: `pdf.template_not_found`. Nombre de hasta 120 caracteres; el `html` sigue el límite de y el `css` el de . * 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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "template_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a", "template_version": 0, "data": { "numero": "001" } }' ``` ### Listar plantillas guardadas `limit` (predeterminado y máximo 100) y `offset`. La respuesta trae `items` y `total`. ### Guardar plantilla

      Fatura {{numero}}

      ", "css": "h1 { color: #333; }", "header_html": "", "footer_html": "" }`, }} > `name` (hasta 120 caracteres) y `html` son obligatorios; `description`, `css`, `header_html` y `footer_html` son opcionales. Crea la versión 1.
      ### Consultar plantilla `version` opcional (predeterminado: la actual). Devuelve la `template` y el contenido de la `version`. ### Listar versiones de la plantilla ### Renombrar plantilla o guardar una nueva versión

      Fatura {{numero}}

      {{cliente}}

      ", "css": "h1 { color: #333; }" }`, }} > 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.
      ### Eliminar plantilla Elimina la plantilla y todas sus versiones. Las conversiones ya generadas no se ven afectadas. ## Fuentes 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`. * Hasta **50 fuentes por cuenta** (`pdf.font_limit`). * Una combinación de `family` + `weight` + `style` solo puede existir una vez (`pdf.font_exists`, 409). * El formato se detecta por el contenido del archivo, no por la extensión. ### Listar fuentes ### Subir fuente 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`. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/fonts' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -F 'file=@Inter-Regular.woff2' \ -F 'family=Inter' \ -F 'weight=400' \ -F 'style=normal' ``` Luego, convierte con la fuente: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "html": "

      Hola

      ", "font_ids": ["2b3c4d5e-6f70-4a8b-9c0d-1e2f3a4b5c6d"] }' ```
      ### Eliminar fuente Elimina la fuente y el archivo almacenado. ## Consulta y descarga ### Listar conversiones 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. ### Estado de la conversió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). ### Descargar archivo Requiere `status: completed`. El archivo se descifra y se descomprime durante la descarga. `X-Dry-Run` no aplica a las descargas. 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. ## Batches ### Listar batches `status` del batch: `processing`, `delivering`, `completed`, `partially_failed` o `failed`. ### Detalle del batch ### Descarga ZIP del batch Disponible cuando el batch termina — incluso parcialmente, si algunos ítems fallan. También acepta el enlace firmado `?sig=&exp=`, sin token. ## Errores ## Particularidades * 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. * Para PDFs recurrentes, guarda una [plantilla](#templates) en lugar de reenviar el HTML en cada llamada. * `retention_days: 0` es efímero (\~24 h) — ideal para vistas previas. * Prefiere `webhook_url` a consultar el estado en bucle: cada consulta vía API cuenta para la cuota diaria gratuita. # QR Dinámico ## Descripción general El QR dinámico separa la **imagen impresa** (enlace corto estable) del **destino** (editable sin reimprimir). Flujo típico: 1. `POST` con `X-Dry-Run: true` para validar el payload y ver el costo. 2. `POST` definitivo con `X-Idempotency-Key` → recibe la imagen del QR, el `resource_id` y el `slug`. 3. Usa la imagen devuelta (o genera la tuya apuntando a `full_url`, disponible en `GET /{id}`). 4. 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. `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`. ## Costos Las 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. ## Creación y gestión ### Crear QR dinámico 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. 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`. ### Listar QRs Query: `limit` (predeterminado 20, máx. ), `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`). ### Detalle ### Actualizar destino, nombre o diseño 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. ### Reactivar ### Desactivar Un QR desactivado muestra una página de QR inactivo en el redirect público y sigue ocupando un slot hasta que se elimine. ### Eliminar Termina el cobro de almacenamiento del slot. ### Verificar disponibilidad de slug ## Analytics ### Resumen de analytics 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. ### Escaneos por período 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. ### Distribución por dispositivo `device` es `Mobile`, `Desktop` o `Tablet`. Al igual que en escaneos por período, los campos de período vienen en cero. ### Historial de escaneos Paginación: `limit` (predeterminado 50, máx. ), `offset` y `period` (predeterminado: últimos 30 días). `location` llega como `Unknown` cuando la ciudad y el país son desconocidos. ## Redirect público * **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. * **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`). * **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. * Las respuestas nunca se almacenan en caché (`Cache-Control: no-store`). ```bash curl -sS -I 'https://api.respawntech.dev/r/promo-verao' ``` ## Dominio propio 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. El 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)). ### Registra el dominio ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "hostname": "qr.suaempresa.com", "validation_method": "http", "root_url": "https://www.suaempresa.com" }' ``` `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`. ### Publica el CNAME 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. | Tipo | Nombre | Valor | | ------- | ---------------------------- | --------------------------------- | | `CNAME` | `qr` (o `qr.suaempresa.com`) | el `value` del registro `routing` | 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. ### Valida el certificado 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. ### Verifica hasta que quede activo La plataforma consulta el dominio periódicamente; para no esperar, fuerza la verificación: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains/$DOMAIN_ID/refresh' \ -H "X-API-Token: $RESPAWN_API_TOKEN" ``` 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. ### Crea QRs en el dominio 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`. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Cartaz da loja", "target_url": "https://loja.example.com/promo", "custom_slug": "promo-verao", "custom_domain_id": "'"$DOMAIN_ID"'" }' ``` El `full_url` del QR pasa a ser `https://qr.suaempresa.com/promo-verao`. El slug solo necesita ser único dentro del dominio. `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. 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. ### Listar dominios propios ### Detalle del dominio ### Registrar dominio propio ### Editar páginas de fallback del dominio Envía `""` para limpiar uno de los campos. ### Verificar el dominio ahora ### Eliminar dominio ## Comportamiento del escaneo Por 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)). En 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. ### Reglas de redirect `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. | Campo | Significado | | ------------------------ | ------------------------------------------------------------------------------------------------------ | | `id` | Nombre de la regla (`[a-zA-Z0-9_-]`, hasta 40); aparece en los escaneos y en `rule_stats` de analytics | | `target_url` | Destino cuando la regla coincide (URL absoluta) | | `countries` | Países ISO 3166-1 alfa-2 en mayúsculas (`["BR", "PT"]`) | | `devices` | `mobile`, `tablet`, `desktop` | | `os` | `ios`, `android`, `windows`, `macos`, `linux`, `other` | | `languages` | Idioma preferido del visitante: `pt` coincide con `pt` y `pt-br`; `pt-br` solo con `pt-br` | | `days` | Días de la semana, `0` (domingo) a `6` (sábado), en el `timezone` | | `start_time`, `end_time` | Ventana del día (`HH:MM`); si el fin viene antes del inicio, cruza la medianoche. Los dos o ninguno | | `timezone` | Zona horaria IANA (`America/Sao_Paulo`); UTC si está vacío | | `from`, `until` | Período en que la regla vale (fecha y hora RFC 3339) | Cada lista acepta hasta 50 valores. Dentro de una regla las condiciones se suman (Y); dentro de una lista, basta un valor (O). ### Prueba A/B `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. ### Contraseña `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. ### Píxeles `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`). ### Redirect de vencido y agotado `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`. ### Ejemplo completo Copia, cambia las URLs y ejecuta: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Campanha com regras", "target_url": "https://loja.example.com/global", "redirect_rules": [ { "id": "br-mobile", "target_url": "https://loja.example.com/br", "countries": ["BR"], "devices": ["mobile"] }, { "id": "horario-comercial", "target_url": "https://loja.example.com/atendimento", "days": [1, 2, 3, 4, 5], "start_time": "09:00", "end_time": "18:00", "timezone": "America/Sao_Paulo" } ], "ab_variants": [ { "id": "a", "target_url": "https://loja.example.com/lp-a", "weight": 50 }, { "id": "b", "target_url": "https://loja.example.com/lp-b", "weight": 50 } ], "pixels": { "ga4_measurement_id": "G-ABCD1234" }, "expired_url": "https://loja.example.com/encerrada", "limit_reached_url": "https://loja.example.com/esgotado" }' ``` Para quitar todo después, sin recrear el QR (cambia `$QR_ID` por el `resource_id` de la creación): ```bash curl -sS -X PATCH "https://api.respawntech.dev/v1/tools/image/qr-code/dynamic/$QR_ID" \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "redirect_rules": [], "ab_variants": [], "pixels": {}, "password": "" }' ``` `GET /{id}` devuelve la configuración en `scan_behavior` (`redirect_rules`, `ab_variants`, `has_password`, `pixels`, `expired_url`, `limit_reached_url`, `content`). ## Contenido alojado 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`. | `type` | Campo | El escaneo muestra | | ----------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `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 | | `file` | `file` (`url`, `name`) | Abre el archivo o PDF alojado (URL absoluta) | | `links` | `links` (`title`, `description`, `avatar_url`, `links[]` con `label` y `url`) | Link en bio, de 1 a 50 enlaces | | `menu` | `menu` (`title`, `currency`, `sections[]` → `items[]` con `name`, `description`, `price`) | Menú, hasta 30 secciones y 300 ítems | | `wifi` | `wifi` (`SSID`, `Password`, `Security`, `Hidden`) | Nombre de la red y contraseña para copiar | | `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 | Los 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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Link na bio", "content": { "type": "links", "links": { "title": "Loja Exemplo", "links": [ { "label": "Site", "url": "https://loja.example.com" }, { "label": "Instagram", "url": "https://instagram.com/lojaexemplo" } ] } } }' ``` Ejemplo de tarjeta de contacto: ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Cartão da Ana", "content": { "type": "vcard", "vcard": { "FirstName": "Ana", "LastName": "Souza", "Org": "Loja Exemplo", "Phone": "+5511999990000", "Email": "ana@loja.example.com" } } }' ``` ## Marcos e impresió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`. * `frame`: `text` (hasta 32 caracteres; se reduce para caber), `color`, `text_color` (hex) y `position` (`top` o `bottom`). * `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. * `eps`: formato vectorial nuevo, con el marco y fuente Helvetica-Bold con acentos. Sale sin logo. En 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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Cartaz A5", "target_url": "https://loja.example.com/promo", "response_types": ["pdf", "eps"], "frame": { "text": "Escaneie aqui", "color": "#111111", "text_color": "#FFFFFF", "position": "bottom" }, "dpi": 300 }' ``` ## Organización, edición masiva y lote ### Carpetas y tags `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`. ### Listar carpetas ### Crear carpeta El nombre tiene de 1 a 80 caracteres y es único en la cuenta (`dynamic_qr.folder_taken`). ### Renombrar carpeta ### Eliminar carpeta ### Editar varios QRs a la vez 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`. ### Crear QRs en lote Hasta 1.000 filas por lote, de una de estas formas: * 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`); * cuerpo `text/csv` directo, con `folder_id` y `tags` como parámetros de query. 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`: ```csv name,target_url,custom_slug,tags Loja Centro,https://loja.example.com/centro,loja-centro,varejo|sp Loja Norte,https://loja.example.com/norte,loja-norte,varejo ``` ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/batches?tags=campanha' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'X-Idempotency-Key: 7f1c2e84-5b3a-4d6c-9e01-a2b3c4d5e6f7' \ -H 'Content-Type: text/csv' \ --data-binary @lojas.csv ``` 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`). ### Listar lotes ### Detalle del lote `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). ### Exportar escaneos en CSV 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`. ### Borrar el historial de escaneos Borra las filas de escaneo (todas, o las anteriores a `before`, `YYYY-MM-DD` UTC). `total_scans` y `scan_limit` se mantienen. Es irreversible. ## Errores ## Particularidades y límites * Las URLs maliciosas se bloquean automáticamente (`dynamic_qr.malicious_url`). * Protección anti-loop: el destino no puede apuntar al propio dominio de redirect. * El primer QR no ocupa un slot de pago; los demás ocupan slots en grupos de 3. * `skip_image_generation: true` reduce el costo cuando generas la imagen del QR por tu cuenta. * Slug: 3–50 caracteres `[a-zA-Z0-9-]`. Listados: hasta ítems por página. # Realtime ## Cómo funciona Realtime lleva mensajes de tu servidor, o de otros clientes, a los navegadores conectados, sin que mantengas servidores de WebSocket. * **Canal:** un nombre libre de hasta 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. * **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`). * **`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=`. * **Mensaje:** `data` JSON publicado en un canal, con `id`, `timestamp` (milisegundos Unix) y el `client_id` de quien lo publicó. * **Presencia:** la lista de quién está conectado y "presente" en un canal, con datos libres de cada miembro (un apodo, un estado). El flujo es siempre el mismo: El navegador pide un token a **tu servidor**, que ya sabe quién es el usuario. 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`. El navegador se conecta a `connect_url`, se suscribe a canales y publica. Si la conexión se cae, el navegador pide un token nuevo y [reconecta](#reconexao), recuperando los mensajes perdidos con `rewind`. 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. Tu 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. ## Token y capabilities ### Emitir token de conexión * `capabilities` (obligatorio): objeto de patrón de canal a la lista de operaciones permitidas, hasta 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`. * `client_id` (opcional, hasta 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. * `ttl_seconds` (opcional): validez del token, de hasta , por defecto . 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. 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**. Da 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. 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`. ## Conexión desde el navegador 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. ```js import { createServer } from 'node:http'; createServer(async (req, res) => { if (req.url !== '/realtime-token') return res.writeHead(404).end(); const api = await fetch('https://api.respawntech.dev/v1/tools/realtime/tokens', { method: 'POST', headers: { 'X-API-Token': process.env.RESPAWN_API_TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: 'user-42', // the authenticated user capabilities: { 'chat:*': ['subscribe', 'publish', 'presence'] }, }), }); const { data } = await api.json(); // token, expires_at, connect_url and retry_after_ms go together to the browser res.writeHead(api.status, { 'Content-Type': 'application/json' }).end(JSON.stringify(data)); }).listen(3000); ``` ```python import os import requests from flask import Flask, jsonify app = Flask(__name__) @app.post("/realtime-token") def realtime_token(): api = requests.post( "https://api.respawntech.dev/v1/tools/realtime/tokens", headers={"X-API-Token": os.environ["RESPAWN_API_TOKEN"]}, json={ "client_id": "user-42", # the authenticated user "capabilities": {"chat:*": ["subscribe", "publish", "presence"]}, }, timeout=30, ) # token, expires_at, connect_url and retry_after_ms go together to the browser return jsonify(api.json()["data"]), api.status_code ``` En 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). ```html Realtime
        ``` ## Protocolo 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}$`. ### Del cliente al servidor | `action` | Campos | Qué hace | | ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `subscribe` | `channel`, `rewind?` | Se suscribe al canal (exige `subscribe` en el token). Con `rewind` (número de mensajes, hasta ), reenvía los últimos mensajes del canal antes de los nuevos. Responde `attached`. | | `unsubscribe` | `channel` | Deja de recibir el canal. Responde `detached`. | | `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. | | `presence` | `op`, `channel`, `data?`, `id?` | Operaciones de [presencia](#presenca): `op` es `enter`, `update`, `leave` o `get`. | ### Del servidor al cliente | `action` | Campos | Cuándo | | ----------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `connected` | `connection_id` | La conexión fue aceptada; ya puedes suscribirte. | | `attached` | `channel` | La suscripción al canal está activa (tras el `rewind`, si se pidió). | | `detached` | `channel` | La suscripción terminó. | | `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. | | `ack` | `id`, `channel?` | La publicación (u operación de presencia) con ese `id` fue aceptada. | | `presence` | `op`, `channel`, `member` o `members` | Evento de presencia (`enter`, `update`, `leave`) o respuesta a `get` (`sync`). | | `error` | `code`, `message`, `channel?`, `id?` | Una operación falló. Los `code` están en [Errores](#erros). La conexión sigue abierta. | Ejemplo de intercambio: ```json ← { "action": "connected", "connection_id": "c_8f3a1d" } → { "action": "subscribe", "channel": "chat:lobby", "rewind": 20 } ← { "action": "attached", "channel": "chat:lobby" } → { "action": "publish", "channel": "chat:lobby", "id": "m-1", "data": { "text": "Hello!" } } ← { "action": "message", "channel": "chat:lobby", "id": "1790338000000-0", "data": { "text": "Hello!" }, "timestamp": 1790338000000, "client_id": "user-42" } ← { "action": "ack", "id": "m-1" } ``` El `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`. ### Close codes | Código | Significado | Qué hacer | | -------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------ | | `1012` | El nodo se está reiniciando (despliegue o mantenimiento). | Reconecta ya, con un pequeño jitter. | | `1013` | El nodo está lleno. | Reconecta con backoff exponencial. | | `4001` | Token inválido o expirado (`realtime.token_invalid`). | Emite un token nuevo y reconecta; repetir el mismo token no sirve. | | `4003` | Realtime desactivado para la cuenta (`realtime.disabled`). | No reconectes. | | `4029` | La cuenta alcanzó el límite de conexiones (`realtime.connection_limit`). | Reconecta con backoff exponencial. | | Cualquier otro | Caída de red o error inesperado. | Reconecta con backoff exponencial. | ## SSE: lectura sin WebSocket 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`: ```text GET https:///v1/sse?token=&channel=chat:lobby&channel=alerts&rewind=20 ``` * Indica cada canal en un `channel=` (o varios en `channels=a,b`), hasta . El token necesita `subscribe` en todos. * Cada `data:` es un frame JSON igual al del WebSocket (`connected`, `attached`, `message`, `presence`, `error`); las líneas `: ping` mantienen viva la conexió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. * Para publicar, usa la [API](#publicar) o un WebSocket. ```js const url = new URL(connectUrl); // the connect_url from the token response const sse = new EventSource( `https://${url.host}/v1/sse?token=${encodeURIComponent(token)}&channel=chat:lobby&rewind=20`, ); sse.onmessage = (event) => { const frame = JSON.parse(event.data); if (frame.action === 'message') console.log(frame.data); }; ``` ## Presencia 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. | Frame del cliente | Exige | Efecto | | -------------------------------------------------------------------------------- | ----------- | ---------------------------------------------- | | `{"action":"presence","op":"enter","channel":"chat:lobby","data":{…},"id":"p1"}` | `presence` | Entra en la presencia; responde `ack`. | | `{"action":"presence","op":"update","channel":"chat:lobby","data":{…}}` | `presence` | Actualiza los datos del miembro. | | `{"action":"presence","op":"leave","channel":"chat:lobby"}` | `presence` | Sale de la presencia. | | `{"action":"presence","op":"get","channel":"chat:lobby","id":"p2"}` | `subscribe` | Responde `op: "sync"` con todos los `members`. | Los 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 miembros; por encima, `realtime.presence_limit`. Si un nodo cae sin avisar, sus miembros desaparecen de la lista en hasta 30 segundos. ### Listar la presencia del canal 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. ## Reconexión 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. ### Backoff Usa backoff exponencial **con jitter** (un valor aleatorio hasta el tope del intento) y tope de : 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`. ### Token nuevo y `retry_after_ms` 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. ### Recuperar mensajes con `rewind` 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. Elige el `rewind` según el tiempo que la conexión suele estar fuera (hasta mensajes). Si el canal tuvo más mensajes que eso durante la caída, la diferencia se pierde: el historial guarda solo los últimos. ## Historial Cada canal guarda los últimos mensajes publicados, por hasta . El `rewind` usa ese historial; para consultarlo desde tu servidor, usa la API. ### Consultar historial del canal 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 (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. ### Publicar mensaje desde el servidor 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. * `data`: cualquier JSON, hasta (sumado al `name`). * `name` (opcional, hasta 128 caracteres): clasifica el mensaje. * `client_id` (opcional): el remitente que ven los suscriptores. La respuesta trae `id`, `timestamp` (milisegundos Unix) y `charged`: `true` cuando el mensaje superó la franquicia mensual y se cobró. 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: ```json { "success": true, "data": { "cost_units": 0, "within_quota": true, "free_used": 18250, "free_monthly": 200000, "would_charge": false } } ``` `cost_units` usa la escala del backend (10.000 unidades = 1 crédito) y es `0` dentro de la franquicia. ## Canales y estadísticas ### Listar canales activos 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 , por defecto 50) y `offset`; la respuesta trae `total` y, cuando hay más páginas, `next_offset` para la llamada siguiente. ### Consultar uso y conexiones 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`. ## Webhooks de canales 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). ### Consultar el webhook de canales 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`. ### Configurar el webhook de canales * `url`: URL pública `http` o `https`; las direcciones de red interna o privada se rechazan. * `events`: al menos uno de los eventos de la tabla de abajo. * `channels`: patrones de canal (un nombre, un prefijo con `*` o `*`), hasta . Vacío significa todos los canales, **excepto** `channel.message`: los mensajes son opt-in y exigen al menos un patrón. * `enabled` (opcional, por defecto `true`): `false` pausa los envíos sin borrar la configuració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. ### Cambiar el secret del webhook Genera un nuevo `secret` (y la `standard_key` correspondiente). El anterior deja de valer; actualiza tu receptor enseguida. ### Eliminar el webhook de canales Borra la configuración; ya no se envía ningún evento. ### Eventos ### Entrega La plataforma hace un `POST` JSON a tu URL: ```json { "id": "evt_5b0c9a7e-3c1f-4a52-9d57-2a6f1f0e8a11", "type": "presence.member_added", "timestamp": 1790338000000, "account_id": "7a1d4c8e-2f6b-4e9a-b3c5-0d8f1a6e4b27", "data": { "channel": "chat:lobby", "member": { "connection_id": "c_8f3a1d", "client_id": "user-42", "data": { "nick": "ana" }, "timestamp": 1790338000000 } } } ``` `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. * **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). * **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 . * **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. ## SDK de JavaScript 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. ```bash npm install @respawn/realtime ``` ### En el servidor `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: ```ts import { RealtimeRest } from '@respawn/realtime'; const rest = new RealtimeRest({ apiKey: process.env.RESPAWN_API_TOKEN!, baseUrl: 'https://api.respawntech.dev', }); // route the browser calls (Express): returns token, expires_at and connect_url app.post('/realtime/token', async (req, res) => { res.json( await rest.createToken({ client_id: req.user.id, capabilities: { 'room:*': ['subscribe', 'publish', 'presence'] }, ttl_seconds: 600, }), ); }); await rest.publish('room:1', 'order', { id: 42, status: 'paid' }); const history = await rest.history('room:1', { limit: 20 }); const members = await rest.presence('room:1'); ``` ### En el navegador `Realtime` pide el token a tu servidor por `authUrl` (un `POST` con cookies) o por `authCallback`: ```ts import { Realtime } from '@respawn/realtime'; const rt = new Realtime({ authUrl: '/realtime/token' }); rt.on('state', (state, err) => console.log('connection:', state, err?.code)); const room = rt.channel('room:1'); await room.subscribe((m) => console.log(m.name, m.data), { rewind: 20 }); await room.publish('chat', { text: 'hi' }); await room.presence.watch((members) => render(members)); await room.presence.enter({ nick: 'ana' }); ``` Los 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). ### React Con `@respawn/realtime/react`: ```tsx import { useState } from 'react'; import { Realtime } from '@respawn/realtime'; import { RealtimeProvider, useChannel, usePresence } from '@respawn/realtime/react'; const rt = new Realtime({ authUrl: '/realtime/token' }); export function App() { return ( ); } function Room() { const [msgs, setMsgs] = useState<{ id: string; data: unknown }[]>([]); const { publish } = useChannel('room:1', (m) => setMsgs((all) => [...all, m]), { rewind: 20 }); const { members } = usePresence('room:1', { nick: 'ana' }); return (

        {members.length} online

          {msgs.map((m) =>
        • {JSON.stringify(m.data)}
        • )}
        ); } ``` ## Límites | Límite | Valor | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Mensajes gratis por mes (por cuenta) | | | Tamaño de un mensaje (`data`) | | | Largo del nombre del canal | caracteres | | Canales suscritos por conexión | | | Publicaciones por segundo (por cuenta) | | | Conexiones simultáneas (por cuenta) | | | Miembros en la presencia de un canal | | | Validez del token | (por defecto), de hasta | | Patrones de canal por token | hasta | | Retención del historial | | | Mensajes guardados y por `rewind` | hasta | | Canales por página (`GET /channels`) | hasta | | Patrones de canal del webhook | hasta | Los 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. ## Errores 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)). Los errores de validación comunes (`validation.failed`, `not_found`) están en [Errores](/es/erros). ## Precio El 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`. # Verificación de URL ## Descripción general Ú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. ## Costos 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. ## Endpoints ### Verificar URL Query: `url` (obligatorio, URL absoluta). Los resultados se guardan en caché durante 1 hora. 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. ### Favicon de un dominio Query: `domain` (obligatorio). ## Errores # Página web a Markdown ## Visión general 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`. ## Costos 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). ## Endpoints ### Convertir una página a Markdown `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`. 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`. 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`). ## Errores # Webhook Relay ## Visión general 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). * **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). * **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`. * **Mensaje:** un evento que publicas (`event_type` + `payload`). * **Entrega:** un mensaje camino de un endpoint. Un mensaje para 3 endpoints genera 3 entregas, cada una con sus reintentos. * **Intento:** cada llamada HTTP de una entrega, registrada con estado, latencia y el comienzo de la respuesta. * **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). Todas 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). ## Primeros pasos **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. **Prueba la conexión** con el [ping de prueba](#webhooks-endpoints-test): un evento `webhook.test` gratis. **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. **Verifica la firma** en el receptor y responde `2xx` rápido. Consulta los ejemplos en [Validar la firma](/es/webhooks#verificar-assinatura). **Sigue** las entregas en el [registro de mensajes](#mensagens) y reenvía lo que falló. ## Costos El 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. La cuota y el cobro son **por cuenta**: todos los proyectos comparten la misma cuota mensual, y una entrega cuesta lo mismo en cualquier proyecto. 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. ### Consultar el uso del mes 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)). ## Proyectos 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. * Toda cuenta tiene un **proyecto por defecto** (`is_default: true`, llamado `Default`), creado en el primer uso. No se puede eliminar. * `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. * 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. * Los listados (`GET /endpoints`, `/sources`, `/event-types`, `/routes` y `/messages`) aceptan `?project_id=` como filtro; sin él, traen todos los proyectos. * 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`). * Los límites y el cobro siguen siendo por cuenta: consulta [Límites](#limites). Por defecto no hay límite de proyectos. ### Listar proyectos Paginado y con búsqueda: `q` busca en el nombre (sin distinguir mayúsculas), `page` empieza en 1 y `page_size` llega hasta (por defecto ). 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). ### Crear proyecto `name` (obligatorio) tiene hasta caracteres; `description`, hasta . ### Consultar proyecto ### Modificar proyecto Envía solo los campos que quieres cambiar. ### Eliminar proyecto 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`. ### Duplicar proyecto Crea un proyecto nuevo con la misma descripción y copias de todo, ya conectadas entre sí: * **Endpoints** con **secrets nuevos** (misma URL, eventos, headers, límite de envío, formato y estado activo; el historial de fallos no se copia). * **Fuentes** con **`ingest_url` nuevas** (misma configuración y el mismo secret del proveedor). Registra las URL nuevas en el proveedor. * Rutas, tipos de evento y el layout del canvas. 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. ## Endpoints ### Crear endpoint * `url` (obligatorio): URL pública `http` o `https`. Las direcciones privadas, locales o de metadatos de la nube se rechazan (`webhook.invalid_url`). * `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). * `project_id`: proyecto del endpoint (por defecto: el proyecto por defecto). Un endpoint no cambia de proyecto. * `format`: `raw` (por defecto), `slack`, `discord` o `teams`. Consulta [Slack, Discord y Teams](#formatos). * `custom_headers`: hasta 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. * `rate_limit_per_sec`: tope de envíos por segundo para este endpoint (`0` = sin tope propio). * El `secret` solo viene en esta respuesta; después, usa [Revelar secret](#webhooks-endpoints-secret). Hasta endpoints por cuenta, sumando todos los proyectos. ### Listar endpoints Filtro opcional por `project_id`. Un endpoint desactivado automáticamente trae `is_active: false` y `disabled_reason`; uno que está fallando trae `failing_since`. ### Consultar endpoint ### Modificar endpoint Envía solo los campos que quieres cambiar (incluido `format`). `is_active: true` reactiva un endpoint desactivado y reinicia la racha de fallos. ### Eliminar endpoint Las entregas pendientes para el endpoint se cancelan. ### Revelar secret `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. ### Rotar secret Durante las entregas llevan dos firmas en `webhook-signature` (secret nuevo y anterior), para que cambies el secret del receptor sin perder eventos. ### Enviar ping de prueba 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. ### Reenviar entregas fallidas Reenvía a este endpoint todas las entregas que terminaron en fallo (`failed`, `exhausted` o `canceled`) desde `since` (RFC 3339), hasta por llamada. Úsalo después de corregir un receptor que estuvo caído. Cada entrega reenviada se cobra como nueva. ### Slack, Discord y Teams 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: * El cuerpo pasa a ser `{"text": "…"}` (Slack y Teams) o `{"content": "…"}` (Discord), con `Content-Type: application/json`. * 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. * Los textos largos se cortan con `…` para caber en el límite de la plataforma: caracteres en Slack, en Discord y en Teams. * 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. El mensaje guardado en el registro no cambia: el registro, el reenvío y la recuperación funcionan igual que en los endpoints `raw`. ## Mensajes ### Enviar mensaje * `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 `*`). * `payload` (obligatorio): cualquier JSON, hasta . Si el tipo de evento tiene [JSON Schema](#tipos-de-evento) en el proyecto, el payload debe cumplirlo (`webhook.payload_invalid`, sin cobro). * `project_id` (opcional): proyecto del mensaje (por defecto: el proyecto por defecto). * `endpoint_ids` (opcional): restringe el envío a estos endpoints, entre los del proyecto suscritos al evento. * `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. * Con `X-Dry-Run: true` no se envía ni se cobra nada: la respuesta muestra `deliveries`, `free_deliveries` y `charged_units`. 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_`. ### Enviar mensajes en lote Hasta 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`. ### Listar mensajes 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 . * `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). * `event_type` y `endpoint_id`: filtros exactos. * `failed=true`: solo mensajes con al menos una entrega `failed`, `exhausted` o `blocked_no_credits`. * `q`: búsqueda, de 1 a 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 . ### Consultar mensaje 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. | `status` de la entrega | Significado | | ---------------------- | --------------------------------------------------------------------------------------------------------------------- | | `pending` | Esperando el próximo intento (`next_attempt_at`). | | `sending` | Intento en curso. | | `succeeded` | El endpoint respondió `2xx`. | | `exhausted` | Todos los intentos fallaron. Se puede reenviar. | | `failed` | Se detuvo del todo: el endpoint respondió `410 Gone`, fue desactivado o su URL dejó de ser válida. Se puede reenviar. | | `canceled` | Cancelada, por ejemplo porque el endpoint se eliminó con la entrega pendiente. | | `blocked_no_credits` | Mensaje recibido de un proveedor sin saldo para la entrega (consulta [Sin créditos](#sem-creditos)). | ### Reenviar mensaje Sin body, envía de nuevo el mensaje a los endpoints activos cuya entrega terminó en fallo (`failed`, `exhausted` o `canceled`). 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. ## Tipos de evento 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. ### Declarar tipo de evento `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 tipos por cuenta, sumando todos los proyectos. ### Listar tipos de evento Filtro opcional por `project_id`. ### Eliminar tipo de evento Los envíos de ese tipo se siguen aceptando, sin validación de schema. ## Recibir webhooks de terceros 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. | `provider` | Cómo se verifica la solicitud | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `stripe` | Header `Stripe-Signature` con el signing secret de Stripe (`whsec_…`), tolerancia de . | | `github` | Header `X-Hub-Signature-256` con el secret del webhook de GitHub. | | `shopify` | Header `X-Shopify-Hmac-Sha256` con el secret de la app de Shopify. | | `hmac_generic` | Header `X-Webhook-Signature: sha256=` (HMAC-SHA256 del cuerpo). | | `standard_webhooks` | Headers `webhook-id`, `webhook-timestamp` y `webhook-signature`. | | `basic_auth` | Header `Authorization: Basic …`; el secret es `usuario:contraseña`. | | `none` | Sin verificación: cualquiera con la URL puede enviar. Evítalo en producció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. ### Crear fuente de ingesta * `name` y `provider` (obligatorios); `project_id` es opcional (por defecto: el proyecto por defecto). * `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"`). * `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`. * `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. * `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. * Registra la `ingest_url` de la respuesta en el proveedor. Hasta fuentes por cuenta, sumando todos los proyectos. ### Listar fuentes Filtro opcional por `project_id`. ### Consultar fuente ### Modificar fuente 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. ### Eliminar fuente Las rutas de la fuente se eliminan con ella y la `ingest_url` deja de responder. ### Rotar URL de ingesta Genera una nueva `ingest_url`; la anterior deja de funcionar al instante. Actualiza la URL en el proveedor enseguida. ### Definir secret del proveedor Un secret vacío solo se acepta con `provider: "none"`. ### Consultar el último evento recibido 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. ### Enviar un evento de prueba por la fuente 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`. Es gratis y queda fuera de la cuota mensual. No verifica la firma ni deduplica, pero cuenta en el límite de solicitudes por minuto de la fuente. ### Crear ruta 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 ; vacío, la ruta lleva todos los eventos de la fuente. Hasta rutas por fuente. ### Listar rutas Filtros opcionales por `source_id` y `project_id`. ### Modificar ruta 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. ### Eliminar ruta ### Recibir evento de un proveedor 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: | Estado | Cuándo | | ------ | ------------------------------------------------------------------------------------- | | `200` | Aceptado, duplicado (reenvío del proveedor) o sin ruta para el tipo. | | `401` | Firma inválida, o secret del proveedor aún no definido; no se guarda nada. | | `404` | Fuente desconocida o desactivada, o herramienta no habilitada en la cuenta. | | `429` | Más de solicitudes por minuto para la fuente. | | `5xx` | Fallo al guardar; el proveedor debe reintentar. | ### Ejemplo: Stripe [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. [Crea una fuente](#webhooks-sources-create) con `provider: "stripe"`, `event_type_path: "type"` y `dedupe_key_path: "id"`, todavía sin el secret. 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). [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). Tus 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. ### Sin créditos 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 , terminan como `failed`. ## Canvas del dashboard 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). ### Consultar el mapa del flujo 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. ### Consultar el layout del canvas ### Guardar el layout del canvas `positions` guarda las coordenadas de cada tarjeta: claves `platform`, `source:` o `endpoint:`. Las tarjetas sin posición las coloca automáticamente el dashboard. ## Alertas 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). | Tipo | Cuándo | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `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. | | `webhook_endpoint_disabled` | La desactivación automática apaga un endpoint que falla desde hace (no cuando lo pausas ni por `410 Gone`). | | `webhook_quota` | Las entregas del mes alcanzan el de la cuota mensual, una vez por umbral y por mes. | Las alertas nunca retrasan ni bloquean un envío. ## Entrega y reintentos El calendario de reintentos, qué cuenta como éxito, `Retry-After`, `410 Gone` y la desactivación automática de endpoints que fallan durante 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. ## API heredada de la cuenta 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)). ## Errores Los errores de validación comunes (`validation.failed`, `not_found`) y de saldo (`billing.insufficient_credits`) están en [Errores](/es/erros). ## Límites | Límite | Valor | | ------------------------------------------------ | -------------------------------------------------- | | Entregas gratis por mes (por cuenta) | | | Tamaño del `payload` / cuerpo recibido | | | Proyectos por cuenta | | | Endpoints por cuenta | | | Headers personalizados por endpoint | | | Tipos de evento por cuenta | | | Mensajes por lote | | | Entregas por recuperación | | | Fuentes por cuenta | | | Rutas por fuente | | | Tipos de evento por ruta | | | Solicitudes recibidas por fuente | por minuto | | Espera por créditos (`blocked_no_credits`) | | | Retención del registro de mensajes | | | Ventana de búsqueda en el cuerpo de los mensajes | | Los límites por cuenta suman todos los proyectos. Son los valores por defecto de la plataforma, que ella puede ajustar. # Agents IA (MCP) 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. 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`. ## Connexion | | | | ---------------- | --------------------------------------------- | | URL | | | Transport | Streamable HTTP, sans session, réponses JSON | | Authentification | `Authorization: Bearer ` | | Scope requis | `mcp:use`, en plus des scopes de chaque outil | Ne 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. ## Créer une clé pour l'agent 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 : * **Agent de support** : recherche dans la base de connaissances et consultation du solde. * **Agent complet** : tous les outils. * **Lecture seule** : consultations uniquement. Une clé d'agent devrait avoir une **limite de dépense quotidienne** (suggestion : ) et une **date d'expiration**. Voir [Contrôles de la clé](/fr/autenticacao#controles-da-chave). ## Configurer le client Les clients qui ne parlent que stdio passent par la passerelle `mcp-remote`, comme dans le dernier exemple. ## Outils 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. Les 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. ## Paramètres de connexion 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 ». Par exemple, un agent de support qui ne fait que lire la base de connaissances, avec des réponses en portugais : Les paramètres répétés d'une connexion à l'autre peuvent devenir un **profil** dans le dashboard (`profile=`), et une clé peut avoir un profil par défaut. ## Coûts et contrôle des dépenses * 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). * 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. * `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. * 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`. * Le **bouton panique** du dashboard coupe immédiatement l'accès de tous les agents du compte (`mcp.agents_disabled`). * Le relevé du dashboard ventile les dépenses par canal (API, MCP, dashboard) et par clé. ## Idempotence 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 renvoie le résultat précédent sans exécution ni nouvelle facturation (`_meta["tech.respawn/replayed"]`). ## Limites | Limite | Valeur | | ----------------------- | ---------------------------------------------------------------------------------------- | | Requêtes | par minute et par clé, cumulées avec celles de l'API | | Corps de la requête | | | Résultat d'un outil | | | Appels à `tools_search` | par minute | ## Erreurs 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"}}`. Les erreurs des outils sont les mêmes que dans l'API ; voir [Codes d'erreur](/fr/erros). # Authentification ## Token d'API Toutes les routes `/v1/tools/*` exigent un **token d'API**, créé dans le [dashboard](https://dashboard.respawntech.dev) sous **APIs**. | Méthode | Exemple | | --------------------------- | ------------------------------------ | | En-tête dédié (recommandé) | `X-API-Token: rsk_live_...` | | Authorization | `Authorization: ApiKey rsk_live_...` | | Authorization (alternative) | `Authorization: Token rsk_live_...` | 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. ## Prérequis du compte Avant tout appel aux outils, le compte doit avoir : * **Une adresse e-mail vérifiée** — sinon, `auth.email_not_verified` (403). * **Un solde non négatif** — un compte suspendu renvoie `billing.user_bankrupt` (402). * **Suffisamment de crédits** — les opérations payantes sans solde renvoient `billing.insufficient_credits` (402). ## Scopes 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. 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. ### Combinaisons recommandées | Cas d'usage | Scopes suggérés | | ------------------------------------------------ | ------------------------------------------------------------------ | | Générer et gérer des QR codes | `tools:qr:dynamic:manage` | | Tableau de bord en lecture seule des QR | `tools:qr:dynamic:read` | | Pipeline PDF | `tools:pdf:manage` + `tools:pdf:read` | | Vérifier des URL avant de les afficher | `tools:safety:read` | | Assistant qui interroge la base de connaissances | `tools:rag:query` | | Envoyer et maintenir les documents de la base | `tools:rag:manage` | | [Agent IA (MCP)](/fr/agentes-de-ia) | `mcp:use` + les scopes des outils qu'il utilisera + `account:read` | | Intégration complète | `tools:manage` (plus simple, moins sûr) | ## Contrôles de la clé 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) : * **Expiration** : passée la date, le token cesse de fonctionner (`auth.unauthorized`). * **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. * **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`). * **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. L'expiration, la limite et les bases peuvent être modifiées ensuite ; les scopes, non. Pour changer de scopes, créez une autre clé. ## Erreurs d'authentification ## Dashboard vs token d'API * **Dashboard** — la navigation dans le tableau de bord ne consomme pas le quota gratuit quotidien et ne débite pas de lectures mesurées. * **Token d'API** — les lectures mesurées partagent le [quota gratuit quotidien](/fr/cobranca-e-limites#franquia-diaria) de requêtes ( pour les comptes premium). Au-delà, chaque lecture débite des crédits. ## Exemple Créer un QR en dry-run pour valider le token, les scopes et le coût sans rien débiter : # Facturation et limites ## Modèle de crédits * Le solde est affiché en **crédits** dans le dashboard, et tous les montants de cette documentation sont exprimés en crédits. * Les opérations payantes sont débitées à la confirmation — jamais lors du dry-run. * 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). * Chaque token peut avoir une [limite de dépense quotidienne](/fr/autenticacao#controles-da-chave) : la dépasser renvoie `billing.spend_limit_reached`. 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. ## Dry-run et aperçu du coût | En-tête | Effet | | ---------------------- | --------------------------------------------------- | | `X-Dry-Run: true` | Valide et calcule le coût sans persister ni débiter | | `X-Preview-Cost: true` | Alias de `X-Dry-Run` | | `X-Include-Cost: true` | Inclut `meta.cost` dans la réponse | Le 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é. Les 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. ### Devis d'une opération payante Sur les `POST` de création et de conversion, `data` contient le devis ligne par ligne : ```json { "success": true, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "data": { "total_cost_credits": 15000, "total_cost_units": 15000, "items": [ { "description": { "key": "billing.pdf.convert_base" }, "cost_credits": 10000, "type": "base" }, { "description": { "key": "billing.pdf.retention.7" }, "cost_credits": 5000, "type": "storage" } ], "is_monthly_recurring": false, "currency": "CREDITS" } } ``` 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. Pour 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`. ### Aperçu d'une lecture mesurée 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 : ```json { "success": true, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "data": { "operation": "qr_dynamic.analytics", "metering": { "within_quota": true, "quota_limit": 2000, "quota_used": 125, "quota_remaining": 1875, "cost_units": 300, "would_charge": false } }, "meta": { "cost": { "units": 300, "credits": 0.03, "charged": false, "within_quota": true, "quota_limit": 2000, "quota_used": 125, "quota_remaining": 1875 } } } ``` Avec `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). ## Idempotence Envoyez `X-Idempotency-Key` (UUID recommandé) sur n'importe quelle route des outils : * la clé est valable **** ; * même clé + même payload → la réponse d'origine est réutilisée, sans nouvelle facturation ; * même clé + payload différent → `idempotency.conflict` (409). Utilisez-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. Exception : 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é). ## Rate limit Chaque token d'API peut effectuer ** requêtes par minute** sur les routes `/v1/tools/*`. Chaque réponse indique la situation : ```http X-RateLimit-Limit: 60 X-RateLimit-Remaining: 42 X-RateLimit-Reset: 1712345678 ``` En cas de dépassement, l'API renvoie `rate_limit.exceeded` (429). Attendez `X-RateLimit-Reset` avant de renvoyer la requête. ## Réessayer avec attente (retry) Ne réessayez que ce qui peut réussir à la tentative suivante : * **`429`** : attendez l’heure indiquée par `X-RateLimit-Reset` (secondes Unix). * **`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. * **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. Sur 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. ```js // Réessaie 429, 5xx et les erreurs réseau ; renvoie les autres erreurs tout de suite. async function withRetry(send, attempts = 5) { for (let attempt = 1; ; attempt++) { let response; try { response = await send(); } catch (error) { if (attempt === attempts) throw error; // erreur réseau } if (response && response.status !== 429 && response.status < 500) return response; if (attempt === attempts) return response; const reset = Number(response?.headers.get('X-RateLimit-Reset')); const wait = response?.status === 429 && reset ? Math.max(0, reset * 1000 - Date.now()) : Math.min(2 ** (attempt - 1) * 1000, 30_000) * (0.5 + Math.random()); await new Promise((resolve) => setTimeout(resolve, wait)); } } const key = crypto.randomUUID(); // la même clé à chaque tentative const response = await withRetry(() => fetch(url, { method: 'POST', headers: { 'X-API-Token': process.env.RESPAWN_API_TOKEN, 'X-Idempotency-Key': key, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }), ); ``` ```python import os, random, time, uuid import requests # Réessaie 429, 5xx et les erreurs réseau ; renvoie les autres erreurs tout de suite. def with_retry(send, attempts=5): for attempt in range(1, attempts + 1): try: response = send() except (requests.ConnectionError, requests.Timeout): if attempt == attempts: raise response = None if response is not None and response.status_code != 429 and response.status_code < 500: return response if attempt == attempts: return response reset = response.headers.get("X-RateLimit-Reset") if response is not None else None if response is not None and response.status_code == 429 and reset: wait = max(0, int(reset) - time.time()) else: wait = min(2 ** (attempt - 1), 30) * (0.5 + random.random()) time.sleep(wait) key = str(uuid.uuid4()) # la même clé à chaque tentative response = with_retry(lambda: requests.post( url, json=payload, headers={"X-API-Token": os.environ["RESPAWN_API_TOKEN"], "X-Idempotency-Key": key}, timeout=30, )) ``` ## Quota gratuit quotidien de lectures 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 : | Élément | Valeur | | --------------------------- | -------------------------------------- | | Lectures gratuites par jour | **** | | Comptes premium | **** | | Réinitialisation | | Chaque lecture mesurée renvoie la position dans le quota : ```http X-API-Quota-Limit: 2000 X-API-Quota-Used: 847 X-API-Quota-Remaining: 1153 ``` Les requêtes effectuées depuis le [dashboard](https://dashboard.respawntech.dev) ne sont **pas** décomptées du quota ni facturées. ### Lectures mesurées Une fois le quota épuisé, chaque lecture via l'API débite : Les opérations d'écriture sans coût propre (`PATCH`, activer, désactiver, `DELETE`) et le dry-run de création sont toujours gratuits. 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. ## Timeouts * La plupart des routes : ****. * Téléchargements de fichiers : ****. # Codes d'erreur ## Réponse de succès ```json { "success": true, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "data": {}, "meta": {} } ``` | Champ | Description | | ---------- | --------------------------------------------------------- | | `success` | `true` pour les réponses OK | | `trace_id` | ID de corrélation (également dans l'en-tête `X-Trace-Id`) | | `data` | Ressource ou résultat | | `meta` | Facultatif — coût (`meta.cost`), pagination, etc. | Avec `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`. ## Format d'erreur ```json { "success": false, "trace_id": "550e8400-e29b-41d4-a716-446655440000", "error": { "code": "billing.insufficient_credits", "message": "Créditos insuficientes para esta operação.", "details": { "required": 100, "available": 50 } } } ``` | Champ | Description | | --------------- | --------------------------------------------------------------------------------------------------------- | | `error.code` | Identifiant stable — traitez les erreurs à partir de celui-ci, jamais à partir de `message` | | `error.message` | Texte lisible ; peut changer d'une version à l'autre | | `error.details` | Contexte structuré facultatif ; pour `validation.failed`, `details.fields` contient les erreurs par champ | | `trace_id` | À indiquer dans vos tickets de support | ## Erreurs communes à tous les outils ### Authentification et permissions ### Crédits et compte ### Requête et limites ### Agents IA (MCP) ### Plateforme ### Vidéo ## Erreurs par outil Les 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. ## Bonnes pratiques 1. Traitez les `402` avec un parcours de rechargement dans votre système. 2. En cas de `429`, respectez `X-RateLimit-Reset` avant de réessayer. 3. Utilisez `X-Dry-Run` pour détecter `validation.failed` sans frais. 4. Ne réessayez que les erreurs `5xx` et réseau, toujours avec la même `X-Idempotency-Key`. 5. Enregistrez le `trace_id` de chaque réponse en erreur. # Documentation de l'API ## Outils ## Premiers pas ### Créez un token d'API 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). ### Estimez le coût avec le dry-run 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). ### Effectuez l'appel 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. ## En-têtes courants | En-tête | Valeur | Effet | | ------------------- | -------------- | ------------------------------------------------------------------------------ | | `X-API-Token` | `rsk_live_...` | Authentification (recommandé) | | `X-Dry-Run` | `true` | Simule l'opération sans persister ni débiter | | `X-Include-Cost` | `true` | Inclut `meta.cost` dans la réponse | | `X-Idempotency-Key` | UUID | Évite la double facturation lors des renvois () | Les réponses incluent `X-Trace-Id` (à utiliser dans vos tickets de support), `X-RateLimit-*` et, pour les lectures mesurées, `X-API-Quota-*`. ## Guides # Utiliser avec l’IA 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**. ## Deux serveurs MCP | | MCP de documentation | [MCP du compte](/fr/agentes-de-ia) | | ---------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | 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…) | | URL | | | | Authentification | Aucune | Jeton d’API avec `mcp:use` | | Coût | Gratuit | Le même que l’API | | Accès | Lecture seule de la documentation publique | Agit sur le compte, avec portées et limite de dépense | Les deux peuvent être connectés en même temps : un agent consulte la documentation, puis utilise le MCP du compte pour agir. Le MCP de documentation est ouvert progressivement. Tant qu’il n’est pas actif, répond `404` avec le code `mcp.disabled`. llms.txt et les pages Markdown fonctionnent déjà. ## Connexion | | | | ---------------- | -------------------------------------------------------------------------- | | URL | | | Transport | Streamable HTTP | | Authentification | Aucune (n’envoyez pas de jeton) | | Coût | Gratuit | | Limite | requêtes par minute par IP | | Langue | Paramètre `lang` facultatif sur chaque outil : `pt-br`, `en`, `es` ou `fr` | Les réponses proviennent du même contenu que ce site, y compris les prix en vigueur. ## Configurer le client Dans **Claude.ai** et **ChatGPT**, ajoutez un connecteur personnalisé (custom connector) en indiquant seulement l’URL . 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. ## Outils Un 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. ## Sans MCP : llms.txt et Markdown Pour les agents qui ne lisent que le web : * [`/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. * [`/llms-full.txt`](/llms-full.txt) : toutes les pages dans un seul fichier texte. * Pages Markdown : `/md/.md` en portugais et `/md//.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`. * [`/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. # Webhooks 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 : | Origine | Où la configurer | Coût | | ----------------------------------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------- | | **Événements du compte** (ex. : `qr.dynamic.scanned`) | **Webhooks** dans le [tableau de bord](https://dashboard.respawntech.dev) | Gratuit | | **Callbacks de jobs** (ex. : conversion PDF) | `webhook_url` dans chaque requête | Gratuit | | **Webhook Relay** (vos propres événements) | API [Webhook Relay](/fr/ferramentas/webhook-relay) | Par livraison au-delà du quota mensuel gratuit | ## Événements du compte 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. Ces 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). ## Requête envoyée ```http POST Content-Type: application/json User-Agent: RespawnTech-Webhook/2.0 webhook-id: msg_2Vf8tK1pQ9zX webhook-timestamp: 1790338000 webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= X-Webhook-Signature: sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd ``` | En-tête | Contenu | | --------------------- | ------------------------------------------------------------------------------------------------------------------ | | `webhook-id` | Identifiant du message. Il est **identique à chaque relance** : utilisez-le pour écarter les doublons. | | `webhook-timestamp` | Moment de cette tentative, en secondes Unix. | | `webhook-signature` | `v1,`, une signature par secret actif, séparées par une espace (deux pendant un renouvellement de secret). | | `X-Webhook-Signature` | Signature historique `sha256=`, sans horodatage. Conservée pour les anciens récepteurs. | Les 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`). ### Corps Les événements du compte et les messages Webhook Relay utilisent la même enveloppe : ```json { "event_id": "550e8400-e29b-41d4-a716-446655440000", "type": "qr.dynamic.scanned", "timestamp": 1790338000, "payload": { "resource_id": "…", "slug": "promo-verao", "ip": "hash_anonyme", "user_agent": "Mozilla/5.0 …", "referer": "https://…" } } ``` Le 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é. ## Valider la signature Le contenu signé est `webhook-id + "." + webhook-timestamp + "." + corps brut`, en HMAC-SHA256 encodé en base64. Pour valider : 1. Lisez le **corps brut**, avant tout parsing JSON (reformater le JSON change la signature). 2. Refusez les horodatages éloignés de plus de de votre horloge (protection contre le rejeu). 3. Calculez la signature et comparez-la, **en temps constant**, à chaque élément de `webhook-signature`. Une seule correspondance suffit. Avec 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. ```js // npm i standardwebhooks import express from 'express'; import { Webhook } from 'standardwebhooks'; const wh = new Webhook(process.env.WEBHOOK_SECRET); // whsec_… const app = express(); app.post('/hook', express.raw({ type: '*/*' }), (req, res) => { let event; try { event = wh.verify(req.body, req.headers); // lève une erreur si invalide } catch { return res.sendStatus(401); } // traitez `event` de façon asynchrone et répondez vite res.sendStatus(204); }); ``` ```python # pip install standardwebhooks import os from flask import Flask, request from standardwebhooks.webhooks import Webhook wh = Webhook(os.environ["WEBHOOK_SECRET"]) # whsec_… app = Flask(__name__) @app.post("/hook") def hook(): try: event = wh.verify(request.get_data(), dict(request.headers)) except Exception: return "", 401 # traitez `event` de façon asynchrone et répondez vite return "", 204 ``` ```go // Sans bibliothèque : secret est la valeur brute (pas le whsec_). func verify(secret string, r *http.Request, body []byte) bool { id, ts := r.Header.Get("webhook-id"), r.Header.Get("webhook-timestamp") t, err := strconv.ParseInt(ts, 10, 64) if err != nil || math.Abs(time.Since(time.Unix(t, 0)).Seconds()) > 300 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(id + "." + ts + ".")) mac.Write(body) want := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil)) for _, sig := range strings.Fields(r.Header.Get("webhook-signature")) { if hmac.Equal([]byte(sig), []byte(want)) { return true } } return false } ``` 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. ## Secrets et renouvellement | Destination | Où obtenir le secret | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 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. | | Endpoint Webhook Relay | À la création, ou via [Révéler le secret](/fr/ferramentas/webhook-relay#webhooks-endpoints-secret). | | 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). | Chaque secret existe sous deux formes : `secret` (brut) et `standard_webhooks_secret` (`whsec_` + base64 des mêmes octets). Les deux valident les mêmes signatures. Quand vous [renouvelez le secret](/fr/ferramentas/webhook-relay#webhooks-endpoints-rotate_secret) d’un endpoint, l’ancien continue de signer pendant : pendant cette période `webhook-signature` porte les deux signatures, et vous mettez à jour le récepteur sans perdre d’événements. ## Livraison et relances 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 : | Tentative | Attente avant | | --------- | ------------- | | 1 | — | | 2 | 5 secondes | | 3 | 5 minutes | | 4 | 30 minutes | | 5 | 2 heures | | 6 | 5 heures | | 7 et 8 | 10 heures | Environ 27 heures au total, avec une variation de ±10 %. Cas particuliers : * **`429` et `503`** avec `Retry-After` : la tentative suivante respecte l’en-tête quand il demande plus de temps (jusqu’à 10 heures). * **`410 Gone`** : la livraison s’arrête et l’endpoint est désactivé. * 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). Bonnes pratiques côté récepteur : * Répondez **2xx** le plus vite possible et traitez l’événement de façon asynchrone. * **É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). * Ne dépendez pas de l’ordre d’arrivée ; utilisez le `timestamp` du corps quand l’ordre compte. ### Désactivation automatique Un endpoint qui échoue sans interruption pendant 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. L’historique des messages, livraisons et tentatives est conservé pendant . ## Callback de jobs 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 : ```json { "job_id": "job_abc123", "success": true, "result_url": "https://…", "metadata": { "conversion_id": "…" } } ``` En 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`. ## Vos propres événements 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). # Audio ## Vue d'ensemble Toute opération audio est **asynchrone** et facturée tout de suite : 1. `POST` sur l'opération → **202 Accepted** avec `job_id`, `status: pending` et `poll_after_seconds`. 2. Un worker traite en arrière-plan. 3. 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. Les 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. 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`…). ## Entrée audio Les opérations qui prennent un audio exigent **exactement un** de ces champs : | Champ | De quoi il s'agit | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `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é à | | `audio_base64` | L'audio en ligne, en base64, jusqu'à décodés | | `file_id` | Id d'un fichier audio déjà envoyé au compte | Formats acceptés : mp3, wav, ogg, m4a, flac et webm. La limite est de d'audio et de 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. Toutes 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. ## Coûts La « 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 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). ### Dry-run 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 : ```json { "success": true, "data": { "service_code": "audio_transcribe_minute", "units": 3, "rate": 15000, "total_units": 45000, "duration_ms": 125000, "duration_known": true, "parts": [{ "service_code": "audio_transcribe_minute", "units": 3, "rate": 15000, "total": 45000 }] } } ``` `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`. ## Opérations ### Transcrire un audio `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. ### Générer de la parole à partir d'un texte `text` accepte jusqu'à 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. ### Lister les voix Catalogue fermé de voix pour `speak` et `revoice`. `language` filtre par langue. Gratuit. ### Générer un effet sonore ou une musique `kind` : `effect` (1 à , 5 s par défaut) ou `music` (instrumentale, 1 à , 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. ### Analyser un audio 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. ### Convertir ou normaliser un audio `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. ### Résumer un audio 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. ### Traduire un audio `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). ### Reprononcer l'enregistrement avec une autre voix 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`. ### Lister les jobs Les jobs les plus récents d'abord, avec `limit` (20 par défaut, jusqu'à ) 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 (). ### Statut, résultat et téléchargement 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). `status` : `pending`, `processing`, `completed` ou `failed`. ## Voix clonées et consentement (MCP uniquement) 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`). * **`voice_create`** clone la voix **du titulaire du compte** à partir d'un échantillon de . 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`. * **`voice_list`** liste les voix du compte (`voice_id`, nom, langue, `status`, expiration). Gratuit. * **`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. ### Usage responsable * **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. * **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. * **Limites :** jusqu'à voix clonées par compte, créations par jour et usages par heure (`audio.voice_limit`, `audio.voice_rate_limited`). * **Données conservées :** l'échantillon reste chiffré jusqu'à 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 . * **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. ## Scopes | Scope | Opérations | | ------------------------ | ------------------------------------------------------------------------------------------ | | `tools:audio:transcribe` | `transcribe`, `summarize`, `translate` | | `tools:audio:speak` | `speak`, `generate`, `revoice` (en MCP, aussi `voice_create` et `voice_delete`) | | `tools:audio:process` | `analyze`, `convert` | | `tools:audio:read` | `voices`, `jobs`, `jobs/{id}` (les trois scopes ci-dessus lisent aussi leurs propres jobs) | `tools:manage` couvre tout. La liste complète est dans [Authentification](/fr/autenticacao). ## Erreurs `audio.*` dans la réponse ou, pour les jobs qui échouent après acceptation, dans le `error_code` du job. # Base de connaissances ## Vue d'ensemble 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. **Créez une base** avec la langue de vos documents (cela améliore la recherche par mots-clés). **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. **Recherchez** avec la question de l'utilisateur et utilisez les fragments renvoyés comme contexte de la réponse. **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). **Enregistrez du feedback** (facultatif) : les fragments confirmés remontent dans les recherches similaires suivantes et les fragments rejetés disparaissent. ## Coûts 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). L'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. ## Bases ### Créer une base * `name` (obligatoire, jusqu'à 120 caractères) et `description` (jusqu'à 1 000). * `language` : `simple` (par défaut), `portuguese`, `english`, `spanish` ou `french`. Définit le dictionnaire de la recherche par mots-clés. * `default_mode` : `fast` (par défaut) ou `accurate` (rerank supplémentaire, plus précis et un peu plus cher). * 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). ### Lister les bases Renvoie aussi l'utilisation du compte par rapport aux limites (`usage`). ### Consulter une base ### Filtres disponibles 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"]}`. ### Modifier une base 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. ### Réindexer une base Retraite tous les documents avec la configuration actuelle de la base. Chaque fragment est facturé comme lors d'une nouvelle ingestion. ### Tableau de santé de la base 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. ### Index des documents 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. ### Trouver les doublons et les contradictions 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. ### Changer le modèle d'embeddings 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. ### Marquer des documents comme périmés 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. ### Supprimer une base La suppression est asynchrone et efface documents, fragments et feedback. ## Documents ### Ajouter ou remplacer un document Sécurité…", "source_uri": "https://aide.example.com/articles/42", "metadata": { "categorie": "compte" } }` }} > * `content` (obligatoire) : texte brut ou Markdown en UTF-8, jusqu'à . Pour les PDF, Office et images, utilisez [Importer un fichier](#rag-documents-file). * `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. * `paths` (jusqu'à 50) et `commit` : les fichiers dont provient le document, pour [le marquer comme périmé](#rag-collections-changes) lorsqu'ils changent. * 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. * `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. * 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. ### Importer un fichier (PDF, Office, image) 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 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`). ### Consulter l'importation d'un fichier ### Lister les documents 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. ### Consulter un document ### Modifier le titre ou les métadonnées Ne retraite pas le contenu et n'est pas facturé. ### Voir les sections indexées Montre comment le document a été découpé ; utile pour ajuster les titres et la taille des fragments. ### Relancer un document en échec Uniquement pour les documents au `status: failed` ; les autres répondent `rag.document_not_failed`. ### Supprimer un document ## Sources synchronisées 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::…`, 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. | `kind` | `start_url` | Ce qui est importé | | --------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `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. | | `sitemap` | URL du sitemap (ou de l'index) | Importe les pages listées, jusqu'à `max_pages`. `max_depth` ne s'applique pas. | | `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. | | `gdrive` | URL d'un dossier `/folders/` | 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. | ### Lister les sources synchronisées ### Créer une source synchronisée * `name` (jusqu'à 200) et `start_url` (http ou https) sont obligatoires ; `kind` vaut `site` par défaut. * `max_pages` : 1 à 1000 (100 par défaut). `max_depth` : 1 à 10 (3 par défaut). * `frequency_hours` : `0` (manuel uniquement), `6`, `24` (par défaut) ou `168`. * 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. ### Modifier une source Tous les champs sont facultatifs. Avec `enabled: false` ou `frequency_hours: 0`, `next_sync_at` devient nul. ### Synchroniser maintenant 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. ### Historique des synchronisations ### Supprimer une source ## Recherche ### Modes de recherche et reranker * **`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. * **`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`). * **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`. * 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. ### Rechercher dans une base * `query` (obligatoire, jusqu'à caractères). * `top_k` : nombre de fragments à renvoyer (5 par défaut, au maximum). * `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é. * `filters` : uniquement les fragments des documents dont les métadonnées contiennent ces valeurs. * `min_score` (uniquement en `accurate`) et `max_per_document` (0–20) affinent le résultat. * `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). * `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 sont servies depuis le cache (`cached: true`) et coûtent moins cher. 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. ## Feedback de pertinence ### Enregistrer un feedback de pertinence * `label` : `positive` (le fragment répond à la question) ou `negative`. * `end_user_ref` (facultatif) : identifie l'auteur de l'évaluation, quand le vote vient de votre utilisateur final. Seul un hash est conservé. * Jusqu'à votes par minute. Les questions similaires sont regroupées, et le vote s'applique à tout le groupe. ### Lister le feedback ### Supprimer un groupe de feedback ## Rerank autonome ### Réordonner des textes (rerank) Trie **votre propre** liste de textes selon leur pertinence pour la question, sans aucune base. Jusqu'à textes par appel (chacun est tronqué à 2 000 caractères). `index` renvoie à la position dans le tableau envoyé. ## Réponse avec citations ### Répondre avec des citations 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). * 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. * 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é). * `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. * 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. * 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. ## Widget de chat intégrable 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. ```html ``` Attributs : `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`. ### Créer une clé publique (widget) * `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. * `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é. * Jusqu'à 20 clés par base. La `key` n'apparaît qu'**une seule fois** ; nous ne conservons que le hash. 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. ### Lister les clés publiques ### Révoquer une clé publique ### Répondre via le widget (route publique) 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). ### Script du widget ## Analytics des questions ### Analytics des questions 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). * `answer_rate` : questions répondues ÷ total des questions. * `feedback_ctr` : réponses ayant reçu un vote (via le `query_id`) ÷ réponses. * `positive_rate` : votes positifs ÷ votes. * `top_questions` et `unanswered_questions` : jusqu'à 20 questions regroupées chacune, pour découvrir quoi écrire dans la base. 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. ## Routage ### Décider entre des options (routage) 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. ## Prix et limites du compte ### Consulter les prix ### Consulter les limites Valeurs en vigueur pour votre compte (bonus de l'offre inclus), utiles pour valider un envoi avant de le faire. ## Événements (webhooks) Enregistrez un endpoint dans le dashboard pour recevoir : * `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. * `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 ». L'envoi se fait au mieux, comme pour les autres événements. Voir la [liste des événements](/fr/webhooks). ## Utiliser via MCP 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. ## Erreurs Les codes sans statut HTTP apparaissent dans le champ `error_code` d'un document au `status: failed`. ## Limites | Limite | Valeur | | ---------------------------------------------- | -------------------------------------------- | | Taille d'un document | | | Bases par compte | | | Fragments indexés par compte | | | Documents en cours de traitement simultanément | | | Longueur de la question | caractères | | Fragments par recherche (`top_k`) | | | Textes par rerank autonome | | | Votes de feedback par minute | | | Fichier importé | 50 MB | | Pages par source | 1 000 (100 par défaut) | | Clés publiques par base | 20 | Les 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`). # Extracteur d'erreur de CI ## Vue d'ensemble 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`. ## Coûts 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. ## Endpoints ### Extraire l'erreur d'un log `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. ## Erreurs # Mémoire d'apprentissages ## Fonctionnement Un apprentissage est un texte court avec un `kind`, lié à un `project` : | `kind` | À conserver | | ------------ | -------------------------------------------------------- | | `pitfall` | Pièges et erreurs à ne pas répéter. | | `decision` | Décisions prises et leur raison. | | `convention` | Conventions du projet. | | `howto` | Comment faire quelque chose (lancer, publier, déboguer). | | `other` | Ce qui ne rentre pas dans les autres. | Les 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. Elle est aussi disponible dans le [MCP](/fr/agentes-de-ia) comme `learning_save`, `learning_recall` et `learning_forget` (toolset `learnings`). ## Doublons et ingestion asynchrone * **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. * **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. ## Coûts 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.*`). ## Endpoints ### Enregistrer un apprentissage `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`. ### Retrouver des apprentissages `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. ### Oublier un apprentissage # Notifications ## Fonctionnement 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** : * **Canal choisi :** le `channel` de la requête, par nom de canal. * **Canal par défaut :** le canal marqué `is_default`, quand la requête n'a pas de `channel`. * **E-mail du compte :** sans aucun canal configuré, la notification va à l'e-mail du compte. Elle 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. ## Canaux Chaque canal a un `name` unique dans le compte et un `kind` : | `kind` | Champs | Mode de livraison | | --------- | ----------------- | --------------------------------------------------------------------------------------------- | | `email` | aucun | Envoie **uniquement à l'e-mail du compte**. Impossible de choisir un autre destinataire. | | `webhook` | `url`, `secret` | Fait un `POST` sur l'URL, signé comme dans le [Webhook Relay](/fr/ferramentas/webhook-relay). | | `ntfy` | `server`, `topic` | Publie sur le topic [ntfy](https://ntfy.sh) du serveur indiqué. | `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. 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. ## Limites, retry et historique * **Rate limit :** il existe une limite de notifications par période ; au-delà, l'envoi échoue avec `notify.rate_limited`. * **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. * **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**. ## Coûts 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. ## Endpoints ### Envoyer une notification `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. ### Historique des envois Paginé par `page` et `page_size` (jusqu'à 100). Il n'inclut pas le corps des messages. ### Lister les canaux ### Créer un canal `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`. ### Consulter un canal ### Modifier un canal Envoyez uniquement les champs qui changent. Un canal désactivé renvoie `notify.channel_disabled` quand il est choisi dans un envoi. ### Supprimer un canal ### Tester un canal Envoie une notification de test par le canal, pour vérifier la configuration avant de vous y fier. ## Erreurs # PDF Convert ## Vue d'ensemble PDF Convert est **asynchrone** : 1. `POST /convert` ou `POST /batch` → **202 Accepted** avec le `conversion_id` (le `{id}` des autres routes). 2. Un worker effectue le rendu en arrière-plan (Chromium headless). 3. Suivez l'avancement via le [callback `webhook_url`](/fr/webhooks#pdf-callback) ou en appelant `GET /{id}`. 4. Téléchargez avec `GET /{id}/download` lorsque `status` vaut `completed`. 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. ## Coûts Le batch accepte jusqu'à ** é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). ## Conversion ### Convertir du HTML en PDF/PNG/JPG

        Fatura

        ", "css": "body { font-family: sans-serif; }", "format": "pdf", "page_size": "A4", "retention_days": 7 }`, }} > 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`). | Champ | Limite / valeurs | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `html` | max. | | `url` | alternative à `html` : adresse publique `http`/`https`, jusqu'à 2048 caractères (`pdf.invalid_url` si privée ou invalide) | | `css` | max. | | `format` | `pdf` (par défaut), `png`, `jpg` / `jpeg` | | `file_name` | nom du fichier généré | | `title`, `author`, `subject`, `keywords` | métadonnées du document, 1000 caractères maximum chacune | | `header_html` / `footer_html` | en-tête et pied de page répétés sur chaque page | | `google_fonts_url` | URL CSS Google Fonts à charger | | `wait_selector` / `wait_delay` | attend l'apparition d'un sélecteur / un délai en ms avant le rendu | | `enable_js` / `custom_js` | exécute du JavaScript en sandbox (SPA, graphiques) / script supplémentaire | | `data` | objet contenant les variables des placeholders `{{campo}}` dans le HTML | | `retention_days` | `0` (éphémère, \~24 h), `1`–`30` ou `-1` (permanent) | | `webhook_url` | reçoit le [callback](/fr/webhooks#pdf-callback) à la fin | | `template_id`, `template_version`, `font_ids`, `post_process` | [templates enregistrés](#templates), [polices](#fontes) et [post-traitement](#pos-processamento) | 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). Conservez le `conversion_id` : c'est le `{id}` utilisé pour consulter le statut et télécharger le fichier.
        ### Batch avec template

        {{title}}

        ", "css": "h1 { color: #333; }" }, "config": { "page_size": "A4", "format": "pdf", "retention_days": 7 }, "items": [ { "custom_id": "fatura-001", "file_name": "fatura-001.pdf", "data": { "title": "Fatura 001" } }, { "custom_id": "fatura-002", "file_name": "fatura-002.pdf", "data": { "title": "Fatura 002" } } ], "delivery": { "method": "zip", "webhook_url": "https://example.com/hooks/pdf" } }`, }} > | Champ | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `template.html` | **obligatoire** — HTML avec des placeholders `{{campo}}` (syntaxe Go template : `{{if}}` et `{{range}}` fonctionnent aussi) | | `template.css` | CSS partagé | | `items[]` | **obligatoire**, 1 à éléments : `custom_id` (**obligatoire**, votre identifiant), `file_name` et `data` | | `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` | | `delivery.method` | `zip` ou `list` : comment le résultat est livré à la fin | | `delivery.webhook_url` | reçoit le [callback](/fr/webhooks#pdf-callback) à la fin | 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).
        ### Relancer un échec

        Fatura corrigida

        ", "format": "pdf", "page_size": "A4" }`, }} > 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.
        ## Options de page et de rendu 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). | Champ | Valeurs | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `page_size` | `A3`, `A4` (par défaut), `A5`, `Letter`, `Legal`, `Tabloid` ou `Custom` | | `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` | | `orientation` | `portrait` (par défaut) ou `landscape` | | `margin_top`, `margin_bottom`, `margin_left`, `margin_right` | marges de la page | | `scale` | échelle du rendu | | `print_background` | imprime les couleurs et images d'arrière-plan (par défaut `true`) | | `media_type` | `print` ou `screen` : quel `@media` le CSS voit | | `color_scheme` | `light` ou `dark` : valeur de `prefers-color-scheme` | | `page_numbers` | active le compteur de pages à la position : `header-left`, `header-center`, `header-right`, `footer-left`, `footer-center` ou `footer-right` | | `page_number_format` | texte du compteur, avec `{page}` et `{total}` (par défaut `{page} / {total}`, 200 caractères maximum). Exige `page_numbers` | | `jpeg_quality` | de 1 à 100 (par défaut 90). Ne s'applique qu'avec `format: jpg` | Exemple : un rapport sur papier de 8 x 11 pouces, avec compteur en pied de page : ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "url": "https://example.com/relatorio", "page_size": "Custom", "paper_width": 8, "paper_height": 11, "media_type": "print", "page_numbers": "footer-center", "page_number_format": "Page {page} sur {total}" }' ``` ## Post-traitement `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). | Champ | Description | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `post_process.watermark.text` | texte du filigrane, 200 caractères maximum | | `post_process.watermark.image` | alternative au texte : image PNG ou JPEG en base64, 1 MiB maximum. Envoyez **exactement un** parmi `text` et `image` | | `watermark.font_size` | de 1 à 300 | | `watermark.color` | couleur au format `#rrggbb` | | `watermark.opacity`, `watermark.scale` | de 0 à 1 | | `watermark.rotation` | de -180 à 180 degrés | | `watermark.position` | `center`, `top-left`, `top-center`, `top-right`, `left`, `right`, `bottom-left`, `bottom-center` ou `bottom-right` | | `watermark.on_top` | `true` dessine le filigrane au-dessus du contenu | | `post_process.pdfa` | `true` convertit en PDF/A-2b (archivage à long terme) | | `post_process.protection.user_password` | **obligatoire** s'il y a une `protection`, 128 caractères maximum : mot de passe pour ouvrir le fichier | | `protection.owner_password` | mot de passe propriétaire, 128 caractères maximum | | `protection.permissions` | ce que le lecteur peut faire : `print`, `copy`, `modify`, `annotate`, `fill_forms`, `assemble`. Liste vide = aucune permission | `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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "html": "

        Contrato

        ", "format": "pdf", "post_process": { "watermark": { "text": "CONFIDENTIEL", "color": "#cc0000", "opacity": 0.2, "rotation": -45, "position": "center" }, "protection": { "user_password": "mot-de-passe-lecteur", "permissions": ["print"] } } }' ``` ## Templates enregistrés 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. * 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. * 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. * Template ou version inexistant : `pdf.template_not_found`. Nom de 120 caractères maximum ; le `html` suit la limite de et le `css` celle de . * 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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "template_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a", "template_version": 0, "data": { "numero": "001" } }' ``` ### Lister les templates enregistrés `limit` (par défaut et maximum 100) et `offset`. La réponse contient `items` et `total`. ### Enregistrer un template

        Fatura {{numero}}

        ", "css": "h1 { color: #333; }", "header_html": "", "footer_html": "" }`, }} > `name` (120 caractères maximum) et `html` sont obligatoires ; `description`, `css`, `header_html` et `footer_html` sont facultatifs. Crée la version 1.
        ### Consulter un template `version` facultative (par défaut : la version actuelle). Renvoie le `template` et le contenu de la `version`. ### Lister les versions du template ### Renommer le template ou enregistrer une nouvelle version

        Fatura {{numero}}

        {{cliente}}

        ", "css": "h1 { color: #333; }" }`, }} > 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.
        ### Supprimer un template Supprime le template et toutes ses versions. Les conversions déjà générées ne sont pas affectées. ## Polices 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`. * Jusqu'à **50 polices par compte** (`pdf.font_limit`). * Une combinaison `family` + `weight` + `style` ne peut exister qu'une fois (`pdf.font_exists`, 409). * Le format est détecté d'après le contenu du fichier, pas l'extension. ### Lister les polices ### Envoyer une police 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`. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/fonts' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -F 'file=@Inter-Regular.woff2' \ -F 'family=Inter' \ -F 'weight=400' \ -F 'style=normal' ``` Puis convertissez avec la police : ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/document/pdf/convert' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "html": "

        Bonjour

        ", "font_ids": ["2b3c4d5e-6f70-4a8b-9c0d-1e2f3a4b5c6d"] }' ```
        ### Supprimer une police Supprime la police et le fichier stocké. ## Consultation et téléchargement ### Lister les conversions 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. ### Statut de la conversion `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). ### Télécharger le fichier 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. 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. ## Batches ### Lister les batches `status` du batch : `processing`, `delivering`, `completed`, `partially_failed` ou `failed`. ### Détail du batch ### Téléchargement ZIP du batch 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. ## Erreurs ## Particularités * 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. * Pour les PDF récurrents, enregistrez un [template](#templates) au lieu de renvoyer le HTML à chaque appel. * `retention_days: 0` est éphémère (\~24 h) — idéal pour les aperçus. * 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. # QR Dynamique ## Vue d'ensemble Le QR dynamique sépare l'**image imprimée** (lien court stable) de la **destination** (modifiable sans réimprimer). Flux typique : 1. `POST` avec `X-Dry-Run: true` pour valider le payload et connaître le coût. 2. `POST` définitif avec `X-Idempotency-Key` → renvoie l'image du QR, le `resource_id` et le `slug`. 3. Utilisez l'image renvoyée (ou générez la vôtre pointant vers `full_url`, disponible via `GET /{id}`). 4. 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. `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`. ## Coûts Les 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. ## Création et gestion ### Créer un QR dynamique 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. 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`. ### Lister les QR Query : `limit` (20 par défaut, max. ), `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`). ### Détail ### Modifier la destination, le nom ou le design 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. ### Réactiver ### Désactiver Un QR désactivé affiche une page de QR inactif sur la redirection publique et continue d'occuper un slot jusqu'à sa suppression. ### Supprimer Met fin à la facturation du stockage du slot. ### Vérifier la disponibilité d'un slug ## Analytics ### Vue d'ensemble des analytics 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. ### Scans par période 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. ### Répartition par appareil `device` vaut `Mobile`, `Desktop` ou `Tablet`. Comme pour les scans par période, les champs de période sont à zéro. ### Historique des scans Pagination : `limit` (50 par défaut, max. ), `offset` et `period` (par défaut : 30 derniers jours). `location` vaut `Unknown` lorsque la ville et le pays sont inconnus. ## Redirection publique * **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. * **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`). * **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. * Les réponses ne sont jamais mises en cache (`Cache-Control: no-store`). ```bash curl -sS -I 'https://api.respawntech.dev/r/promo-verao' ``` ## Domaine personnalisé 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. Le 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)). ### Enregistrez le domaine ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "hostname": "qr.suaempresa.com", "validation_method": "http", "root_url": "https://www.suaempresa.com" }' ``` `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`. ### Publiez le CNAME 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. | Type | Nom | Valeur | | ------- | ----------------------------- | ---------------------------------------- | | `CNAME` | `qr` (ou `qr.suaempresa.com`) | le `value` de l'enregistrement `routing` | 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. ### Validez le certificat 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. ### Vérifiez jusqu'à ce qu'il soit actif La plateforme interroge le domaine périodiquement ; pour ne pas attendre, forcez la vérification : ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/domains/$DOMAIN_ID/refresh' \ -H "X-API-Token: $RESPAWN_API_TOKEN" ``` 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. ### Créez des QR sur le domaine 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`. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Cartaz da loja", "target_url": "https://loja.example.com/promo", "custom_slug": "promo-verao", "custom_domain_id": "'"$DOMAIN_ID"'" }' ``` Le `full_url` du QR devient `https://qr.suaempresa.com/promo-verao`. Le slug doit seulement être unique au sein du domaine. `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. 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. ### Lister les domaines personnalisés ### Détail du domaine ### Enregistrer un domaine personnalisé ### Modifier les pages de repli du domaine Envoyez `""` pour vider l'un des champs. ### Vérifier le domaine maintenant ### Supprimer le domaine ## Comportement du scan Par 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)). Dans 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. ### Règles de redirection `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. | Champ | Signification | | ------------------------ | ---------------------------------------------------------------------------------------------------------- | | `id` | Nom de la règle (`[a-zA-Z0-9_-]`, jusqu'à 40) ; apparaît dans les scans et dans `rule_stats` des analytics | | `target_url` | Destination lorsque la règle correspond (URL absolue) | | `countries` | Pays ISO 3166-1 alpha-2 en majuscules (`["BR", "PT"]`) | | `devices` | `mobile`, `tablet`, `desktop` | | `os` | `ios`, `android`, `windows`, `macos`, `linux`, `other` | | `languages` | Langue préférée du visiteur : `pt` correspond à `pt` et `pt-br` ; `pt-br` seulement à `pt-br` | | `days` | Jours de la semaine, `0` (dimanche) à `6` (samedi), dans le `timezone` | | `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 | | `timezone` | Fuseau IANA (`America/Sao_Paulo`) ; UTC si vide | | `from`, `until` | Période pendant laquelle la règle s'applique (date et heure RFC 3339) | Chaque 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). ### Test A/B `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. ### Mot de passe `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. ### Pixels `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`). ### Redirection d'expiré et d'épuisé `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`. ### Exemple complet Copiez, changez les URL et exécutez : ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Campanha com regras", "target_url": "https://loja.example.com/global", "redirect_rules": [ { "id": "br-mobile", "target_url": "https://loja.example.com/br", "countries": ["BR"], "devices": ["mobile"] }, { "id": "horario-comercial", "target_url": "https://loja.example.com/atendimento", "days": [1, 2, 3, 4, 5], "start_time": "09:00", "end_time": "18:00", "timezone": "America/Sao_Paulo" } ], "ab_variants": [ { "id": "a", "target_url": "https://loja.example.com/lp-a", "weight": 50 }, { "id": "b", "target_url": "https://loja.example.com/lp-b", "weight": 50 } ], "pixels": { "ga4_measurement_id": "G-ABCD1234" }, "expired_url": "https://loja.example.com/encerrada", "limit_reached_url": "https://loja.example.com/esgotado" }' ``` Pour tout retirer ensuite, sans recréer le QR (remplacez `$QR_ID` par le `resource_id` de la création) : ```bash curl -sS -X PATCH "https://api.respawntech.dev/v1/tools/image/qr-code/dynamic/$QR_ID" \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "redirect_rules": [], "ab_variants": [], "pixels": {}, "password": "" }' ``` `GET /{id}` renvoie la configuration dans `scan_behavior` (`redirect_rules`, `ab_variants`, `has_password`, `pixels`, `expired_url`, `limit_reached_url`, `content`). ## Contenu hébergé 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`. | `type` | Champ | Le scan affiche | | ----------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | `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 | | `file` | `file` (`url`, `name`) | Ouvre le fichier ou PDF hébergé (URL absolue) | | `links` | `links` (`title`, `description`, `avatar_url`, `links[]` avec `label` et `url`) | Lien en bio, de 1 à 50 liens | | `menu` | `menu` (`title`, `currency`, `sections[]` → `items[]` avec `name`, `description`, `price`) | Menu, jusqu'à 30 sections et 300 éléments | | `wifi` | `wifi` (`SSID`, `Password`, `Security`, `Hidden`) | Nom du réseau et mot de passe à copier | | `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 | Les 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. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Link na bio", "content": { "type": "links", "links": { "title": "Loja Exemplo", "links": [ { "label": "Site", "url": "https://loja.example.com" }, { "label": "Instagram", "url": "https://instagram.com/lojaexemplo" } ] } } }' ``` Exemple de carte de contact : ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Cartão da Ana", "content": { "type": "vcard", "vcard": { "FirstName": "Ana", "LastName": "Souza", "Org": "Loja Exemplo", "Phone": "+5511999990000", "Email": "ana@loja.example.com" } } }' ``` ## Cadres et impression `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`. * `frame` : `text` (jusqu'à 32 caractères ; rétrécit pour tenir), `color`, `text_color` (hex) et `position` (`top` ou `bottom`). * `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. * `eps` : nouveau format vectoriel, avec le cadre et la police Helvetica-Bold avec accents. Sort sans logo. Dans 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é. ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/dynamic' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "Cartaz A5", "target_url": "https://loja.example.com/promo", "response_types": ["pdf", "eps"], "frame": { "text": "Escaneie aqui", "color": "#111111", "text_color": "#FFFFFF", "position": "bottom" }, "dpi": 300 }' ``` ## Organisation, modification en masse et lots ### Dossiers et tags `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`. ### Lister les dossiers ### Créer un dossier Le nom compte de 1 à 80 caractères et est unique dans le compte (`dynamic_qr.folder_taken`). ### Renommer un dossier ### Supprimer un dossier ### Modifier plusieurs QR à la fois 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`. ### Créer des QR en lot Jusqu'à 1 000 lignes par lot, de l'une de ces façons : * 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`) ; * corps `text/csv` direct, avec `folder_id` et `tags` en paramètres de query. 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` : ```csv name,target_url,custom_slug,tags Loja Centro,https://loja.example.com/centro,loja-centro,varejo|sp Loja Norte,https://loja.example.com/norte,loja-norte,varejo ``` ```bash curl -sS -X POST 'https://api.respawntech.dev/v1/tools/image/qr-code/batches?tags=campanha' \ -H "X-API-Token: $RESPAWN_API_TOKEN" \ -H 'X-Idempotency-Key: 7f1c2e84-5b3a-4d6c-9e01-a2b3c4d5e6f7' \ -H 'Content-Type: text/csv' \ --data-binary @lojas.csv ``` 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`). ### Lister les lots ### Détail du lot `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). ### Exporter les scans en CSV 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`. ### Effacer l'historique des scans 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. ## Erreurs ## Particularités et limites * Les URL malveillantes sont bloquées automatiquement (`dynamic_qr.malicious_url`). * Protection anti-boucle : la destination ne peut pas pointer vers le domaine de redirection lui-même. * Le premier QR n'occupe pas de slot payant ; les suivants occupent des slots par groupes de 3. * `skip_image_generation: true` réduit le coût lorsque vous générez vous-même l'image du QR. * Slug : 3 à 50 caractères `[a-zA-Z0-9-]`. Listes : jusqu'à éléments par page. # Realtime ## Fonctionnement Realtime achemine les messages de votre serveur, ou d'autres clients, vers les navigateurs connectés, sans que vous ayez à maintenir des serveurs WebSocket. * **Canal :** un nom libre de 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. * **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`). * **`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=`. * **Message :** `data` JSON publié sur un canal, avec un `id`, un `timestamp` (millisecondes Unix) et le `client_id` de son auteur. * **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). Le flux est toujours le même : Le navigateur demande un token à **votre serveur**, qui connaît déjà l'utilisateur. 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`. Le navigateur se connecte à `connect_url`, s'abonne aux canaux et publie. 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'é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. Votre 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. ## Token et capabilities ### Émettre un token de connexion * `capabilities` (obligatoire) : objet associant un motif de canal à la liste des opérations autorisées, jusqu'à 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`. * `client_id` (facultatif, jusqu'à 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. * `ttl_seconds` (facultatif) : durée de validité du token, de à , 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. 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**. Donnez à 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. 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`. ## Connexion depuis le navigateur 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é. ```js import { createServer } from 'node:http'; createServer(async (req, res) => { if (req.url !== '/realtime-token') return res.writeHead(404).end(); const api = await fetch('https://api.respawntech.dev/v1/tools/realtime/tokens', { method: 'POST', headers: { 'X-API-Token': process.env.RESPAWN_API_TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: 'user-42', // the authenticated user capabilities: { 'chat:*': ['subscribe', 'publish', 'presence'] }, }), }); const { data } = await api.json(); // token, expires_at, connect_url and retry_after_ms go together to the browser res.writeHead(api.status, { 'Content-Type': 'application/json' }).end(JSON.stringify(data)); }).listen(3000); ``` ```python import os import requests from flask import Flask, jsonify app = Flask(__name__) @app.post("/realtime-token") def realtime_token(): api = requests.post( "https://api.respawntech.dev/v1/tools/realtime/tokens", headers={"X-API-Token": os.environ["RESPAWN_API_TOKEN"]}, json={ "client_id": "user-42", # the authenticated user "capabilities": {"chat:*": ["subscribe", "publish", "presence"]}, }, timeout=30, ) # token, expires_at, connect_url and retry_after_ms go together to the browser return jsonify(api.json()["data"]), api.status_code ``` Dans 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). ```html Realtime
          ``` ## Protocole 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}$`. ### Du client vers le serveur | `action` | Champs | Effet | | ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `subscribe` | `channel`, `rewind?` | S'abonne au canal (exige `subscribe` dans le token). Avec `rewind` (nombre de messages, jusqu'à ), renvoie les derniers messages du canal avant les nouveaux. Répond `attached`. | | `unsubscribe` | `channel` | Cesse de recevoir le canal. Répond `detached`. | | `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. | | `presence` | `op`, `channel`, `data?`, `id?` | Opérations de [présence](#presenca) : `op` vaut `enter`, `update`, `leave` ou `get`. | ### Du serveur vers le client | `action` | Champs | Quand | | ----------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `connected` | `connection_id` | La connexion est acceptée ; vous pouvez vous abonner. | | `attached` | `channel` | L'abonnement au canal est actif (après le `rewind`, s'il a été demandé). | | `detached` | `channel` | L'abonnement est terminé. | | `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. | | `ack` | `id`, `channel?` | La publication (ou l'opération de présence) portant cet `id` a été acceptée. | | `presence` | `op`, `channel`, `member` ou `members` | Événement de présence (`enter`, `update`, `leave`) ou réponse à `get` (`sync`). | | `error` | `code`, `message`, `channel?`, `id?` | Une opération a échoué. Les `code` sont dans [Erreurs](#erros). La connexion reste ouverte. | Exemple d'échange : ```json ← { "action": "connected", "connection_id": "c_8f3a1d" } → { "action": "subscribe", "channel": "chat:lobby", "rewind": 20 } ← { "action": "attached", "channel": "chat:lobby" } → { "action": "publish", "channel": "chat:lobby", "id": "m-1", "data": { "text": "Hello!" } } ← { "action": "message", "channel": "chat:lobby", "id": "1790338000000-0", "data": { "text": "Hello!" }, "timestamp": 1790338000000, "client_id": "user-42" } ← { "action": "ack", "id": "m-1" } ``` L'`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`. ### Close codes | Code | Signification | Que faire | | ---------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `1012` | Le nœud redémarre (déploiement ou maintenance). | Reconnectez-vous tout de suite, avec un petit jitter. | | `1013` | Le nœud est plein. | Reconnectez-vous avec un backoff exponentiel. | | `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. | | `4003` | Realtime désactivé pour le compte (`realtime.disabled`). | Ne vous reconnectez pas. | | `4029` | Le compte a atteint sa limite de connexions (`realtime.connection_limit`). | Reconnectez-vous avec un backoff exponentiel. | | Tout autre | Coupure réseau ou erreur inattendue. | Reconnectez-vous avec un backoff exponentiel. | ## SSE : lecture sans WebSocket 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` : ```text GET https:///v1/sse?token=&channel=chat:lobby&channel=alerts&rewind=20 ``` * Indiquez chaque canal dans un `channel=` (ou plusieurs dans `channels=a,b`), jusqu'à . Le token doit avoir `subscribe` sur tous. * Chaque `data:` est un frame JSON identique à celui du WebSocket (`connected`, `attached`, `message`, `presence`, `error`) ; des lignes `: ping` maintiennent la connexion. * 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. * Pour publier, utilisez l'[API](#publicar) ou un WebSocket. ```js const url = new URL(connectUrl); // the connect_url from the token response const sse = new EventSource( `https://${url.host}/v1/sse?token=${encodeURIComponent(token)}&channel=chat:lobby&rewind=20`, ); sse.onmessage = (event) => { const frame = JSON.parse(event.data); if (frame.action === 'message') console.log(frame.data); }; ``` ## Présence 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. | Frame du client | Exige | Effet | | -------------------------------------------------------------------------------- | ----------- | -------------------------------------------- | | `{"action":"presence","op":"enter","channel":"chat:lobby","data":{…},"id":"p1"}` | `presence` | Entre dans la présence ; répond `ack`. | | `{"action":"presence","op":"update","channel":"chat:lobby","data":{…}}` | `presence` | Met à jour les données du membre. | | `{"action":"presence","op":"leave","channel":"chat:lobby"}` | `presence` | Quitte la présence. | | `{"action":"presence","op":"get","channel":"chat:lobby","id":"p2"}` | `subscribe` | Répond `op: "sync"` avec tous les `members`. | Les 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'à 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. ### Lister la présence du canal Renvoie les membres du canal à cet instant. Utile pour afficher « qui est en ligne » depuis votre serveur, sans ouvrir de connexion. ## Reconnexion 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. ### Backoff Utilisez un backoff exponentiel **avec jitter** (une valeur aléatoire jusqu'au plafond de la tentative) et un plafond de : 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`. ### Nouveau token et `retry_after_ms` 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. ### Récupérer les messages avec `rewind` 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. Choisissez le `rewind` selon le temps que la connexion reste habituellement absente (jusqu'à 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. ## Historique Chaque canal conserve les derniers messages publiés, pendant au plus. Le `rewind` utilise cet historique ; pour le consulter depuis votre serveur, utilisez l'API. ### Consulter l'historique du canal 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 (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. ### Publier un message depuis le serveur 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. * `data` : n'importe quel JSON, jusqu'à (ajouté au `name`). * `name` (facultatif, 128 caractères au plus) : classe le message. * `client_id` (facultatif) : l'expéditeur que voient les abonnés. La réponse contient `id`, `timestamp` (millisecondes Unix) et `charged` : `true` quand le message a dépassé le quota mensuel et a été facturé. 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 : ```json { "success": true, "data": { "cost_units": 0, "within_quota": true, "free_used": 18250, "free_monthly": 200000, "would_charge": false } } ``` `cost_units` utilise l'échelle du backend (10 000 unités = 1 crédit) et vaut `0` dans le quota. ## Canaux et statistiques ### Lister les canaux actifs 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 à , 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. ### Consulter l'usage et les connexions 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`. ## Webhooks de canaux 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). ### Consulter le webhook de canaux 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`. ### Configurer le webhook de canaux * `url` : URL publique `http` ou `https` ; les adresses de réseau interne ou privé sont refusées. * `events` : au moins un des événements du tableau ci-dessous. * `channels` : motifs de canal (un nom, un préfixe avec `*` ou `*`), jusqu'à . Vide signifie tous les canaux, **sauf** `channel.message` : les messages sont en opt-in et exigent au moins un motif. * `enabled` (facultatif, `true` par défaut) : `false` suspend les envois sans supprimer la configuration. 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. ### Changer le secret du webhook Génère un nouveau `secret` (et la `standard_key` correspondante). L'ancien cesse d'être valable ; mettez à jour votre récepteur aussitôt. ### Supprimer le webhook de canaux Supprime la configuration ; plus aucun événement n'est envoyé. ### Événements ### Livraison La plateforme envoie un `POST` JSON à votre URL : ```json { "id": "evt_5b0c9a7e-3c1f-4a52-9d57-2a6f1f0e8a11", "type": "presence.member_added", "timestamp": 1790338000000, "account_id": "7a1d4c8e-2f6b-4e9a-b3c5-0d8f1a6e4b27", "data": { "channel": "chat:lobby", "member": { "connection_id": "c_8f3a1d", "client_id": "user-42", "data": { "nick": "ana" }, "timestamp": 1790338000000 } } } ``` `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. * **Signature :** Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature`), avec la `standard_key` de la configuration ; voir [Valider la signature](/fr/webhooks#verificar-assinatura). * **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 au plus. * **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. ## SDK JavaScript 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. ```bash npm install @respawn/realtime ``` ### Côté serveur `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 : ```ts import { RealtimeRest } from '@respawn/realtime'; const rest = new RealtimeRest({ apiKey: process.env.RESPAWN_API_TOKEN!, baseUrl: 'https://api.respawntech.dev', }); // route the browser calls (Express): returns token, expires_at and connect_url app.post('/realtime/token', async (req, res) => { res.json( await rest.createToken({ client_id: req.user.id, capabilities: { 'room:*': ['subscribe', 'publish', 'presence'] }, ttl_seconds: 600, }), ); }); await rest.publish('room:1', 'order', { id: 42, status: 'paid' }); const history = await rest.history('room:1', { limit: 20 }); const members = await rest.presence('room:1'); ``` ### Dans le navigateur `Realtime` demande le token à votre serveur par `authUrl` (un `POST` avec cookies) ou par `authCallback` : ```ts import { Realtime } from '@respawn/realtime'; const rt = new Realtime({ authUrl: '/realtime/token' }); rt.on('state', (state, err) => console.log('connection:', state, err?.code)); const room = rt.channel('room:1'); await room.subscribe((m) => console.log(m.name, m.data), { rewind: 20 }); await room.publish('chat', { text: 'hi' }); await room.presence.watch((members) => render(members)); await room.presence.enter({ nick: 'ana' }); ``` Les é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). ### React Avec `@respawn/realtime/react` : ```tsx import { useState } from 'react'; import { Realtime } from '@respawn/realtime'; import { RealtimeProvider, useChannel, usePresence } from '@respawn/realtime/react'; const rt = new Realtime({ authUrl: '/realtime/token' }); export function App() { return ( ); } function Room() { const [msgs, setMsgs] = useState<{ id: string; data: unknown }[]>([]); const { publish } = useChannel('room:1', (m) => setMsgs((all) => [...all, m]), { rewind: 20 }); const { members } = usePresence('room:1', { nick: 'ana' }); return (

          {members.length} online

            {msgs.map((m) =>
          • {JSON.stringify(m.data)}
          • )}
          ); } ``` ## Limites | Limite | Valeur | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | Messages gratuits par mois (par compte) | | | Taille d'un message (`data`) | | | Longueur du nom du canal | caractères | | Canaux souscrits par connexion | | | Publications par seconde (par compte) | | | Connexions simultanées (par compte) | | | Membres dans la présence d'un canal | | | Validité du token | (par défaut), de à | | Motifs de canal par token | jusqu'à | | Rétention de l'historique | | | Messages conservés et par `rewind` | jusqu'à | | Canaux par page (`GET /channels`) | jusqu'à | | Motifs de canal du webhook | jusqu'à | Les 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. ## Erreurs 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)). Les erreurs de validation courantes (`validation.failed`, `not_found`) sont dans [Erreurs](/fr/erros). ## Tarifs La 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`. # Vérification d'URL ## Vue d'ensemble À 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. ## Coûts 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. ## Endpoints ### Vérifier une URL Query : `url` (obligatoire, URL absolue). Les résultats sont mis en cache pendant 1 heure. 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. ### Favicon d'un domaine Query : `domain` (obligatoire). ## Erreurs # Page web vers Markdown ## Vue d'ensemble 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`. ## Coûts 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). ## Endpoints ### Convertir une page en Markdown `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`. 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`. 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`). ## Erreurs # Webhook Relay ## Vue d'ensemble 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). * **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). * **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`. * **Message :** un événement que vous publiez (`event_type` + `payload`). * **Livraison :** un message en route vers un endpoint. Un message vers 3 endpoints crée 3 livraisons, chacune avec ses relances. * **Tentative :** chaque appel HTTP d’une livraison, journalisé avec le statut, la latence et le début de la réponse. * **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). Toutes 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). ## Premiers pas **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. **Testez la connexion** avec le [ping de test](#webhooks-endpoints-test) : un événement `webhook.test` gratuit. **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. **Vérifiez la signature** côté récepteur et répondez `2xx` rapidement. Voir les exemples dans [Valider la signature](/fr/webhooks#verificar-assinatura). **Suivez** les livraisons dans le [journal des messages](#mensagens) et renvoyez ce qui a échoué. ## Coûts La 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. Le 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. 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. ### Consulter l’usage du mois 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)). ## Projets 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. * Chaque compte a un **projet par défaut** (`is_default: true`, nommé `Default`), créé à la première utilisation. Il ne peut pas être supprimé. * `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. * 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. * Les listes (`GET /endpoints`, `/sources`, `/event-types`, `/routes` et `/messages`) acceptent `?project_id=` comme filtre ; sans lui, elles renvoient tous les projets. * 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`). * Les limites et la facturation restent par compte : voir [Limites](#limites). Par défaut, le nombre de projets n’est pas limité. ### Lister les projets Paginé et avec recherche : `q` cherche dans le nom (sans tenir compte de la casse), `page` commence à 1 et `page_size` va jusqu’à ( 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). ### Créer un projet `name` (obligatoire) fait jusqu’à caractères ; `description`, jusqu’à . ### Consulter un projet ### Modifier un projet Envoyez uniquement les champs à modifier. ### Supprimer un projet 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`. ### Dupliquer un projet Crée un nouveau projet avec la même description et des copies de tout, déjà reliées entre elles : * **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é). * **Sources** avec de **nouvelles `ingest_url`** (mêmes réglages et même secret du fournisseur). Enregistrez les nouvelles URL chez le fournisseur. * Routes, types d’événement et disposition du canvas. 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éé. ## Endpoints ### Créer un endpoint * `url` (obligatoire) : URL publique `http` ou `https`. Les adresses privées, locales ou de métadonnées cloud sont refusées (`webhook.invalid_url`). * `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. * `project_id` : projet de l’endpoint (par défaut : le projet par défaut). Un endpoint ne change pas de projet. * `format` : `raw` (par défaut), `slack`, `discord` ou `teams`. Voir [Slack, Discord et Teams](#formatos). * `custom_headers` : jusqu’à 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. * `rate_limit_per_sec` : plafond d’envois par seconde pour cet endpoint (`0` = pas de plafond propre). * Le `secret` n’est renvoyé que dans cette réponse ; ensuite, utilisez [Révéler le secret](#webhooks-endpoints-secret). Jusqu’à endpoints par compte, tous projets confondus. ### Lister les endpoints Filtre facultatif par `project_id`. Un endpoint désactivé automatiquement porte `is_active: false` et `disabled_reason` ; un endpoint en échec porte `failing_since`. ### Consulter un endpoint ### Modifier un endpoint 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. ### Supprimer un endpoint Les livraisons en attente vers l’endpoint sont annulées. ### Révéler le secret `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. ### Renouveler le secret Pendant , les livraisons portent deux signatures dans `webhook-signature` (nouveau et ancien secret), pour changer le secret du récepteur sans perdre d’événements. ### Envoyer un ping de test 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é. ### Rejouer les livraisons en échec Renvoie à cet endpoint toutes les livraisons terminées en échec (`failed`, `exhausted` ou `canceled`) depuis `since` (RFC 3339), jusqu’à par appel. À utiliser après avoir réparé un récepteur resté hors ligne. Chaque livraison rejouée est facturée comme une nouvelle. ### Slack, Discord et Teams 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 : * Le corps devient `{"text": "…"}` (Slack et Teams) ou `{"content": "…"}` (Discord), avec `Content-Type: application/json`. * 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. * Les textes longs sont coupés avec `…` pour tenir dans la limite de la plateforme : caractères sur Slack, sur Discord et sur Teams. * 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. Le message enregistré dans le journal ne change pas : le journal, le renvoi et la récupération fonctionnent comme pour les endpoints `raw`. ## Messages ### Envoyer un message * `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 à `*`). * `payload` (obligatoire) : n’importe quel JSON, jusqu’à . 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). * `project_id` (facultatif) : projet du message (par défaut : le projet par défaut). * `endpoint_ids` (facultatif) : limite l’envoi à ces endpoints, parmi ceux du projet abonnés à l’événement. * `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. * Avec `X-Dry-Run: true`, rien n’est envoyé ni facturé : la réponse indique `deliveries`, `free_deliveries` et `charged_units`. 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_`. ### Envoyer des messages par lot Jusqu’à 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`. ### Lister les messages 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 . * `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). * `event_type` et `endpoint_id` : filtres exacts. * `failed=true` : seulement les messages ayant au moins une livraison `failed`, `exhausted` ou `blocked_no_credits`. * `q` : recherche, de 1 à 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 . ### Consulter un message 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. | `status` de la livraison | Signification | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | `pending` | En attente de la prochaine tentative (`next_attempt_at`). | | `sending` | Tentative en cours. | | `succeeded` | L’endpoint a répondu `2xx`. | | `exhausted` | Toutes les tentatives ont échoué. Peut être renvoyée. | | `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. | | `canceled` | Annulée, par exemple parce que l’endpoint a été supprimé avec la livraison en attente. | | `blocked_no_credits` | Message reçu d’un fournisseur sans solde pour la livraison (voir [Sans crédits](#sem-creditos)). | ### Renvoyer un message Sans body, renvoie le message aux endpoints actifs dont la livraison s’est terminée en échec (`failed`, `exhausted` ou `canceled`). 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. ## Types d’événement 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. ### Déclarer un type d’événement `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’à types par compte, tous projets confondus. ### Lister les types d’événement Filtre facultatif par `project_id`. ### Supprimer un type d’événement Les envois de ce type restent acceptés, sans validation de schéma. ## Recevoir des webhooks de tiers 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. | `provider` | Vérification de la requête | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `stripe` | En-tête `Stripe-Signature` avec le signing secret Stripe (`whsec_…`), tolérance de . | | `github` | En-tête `X-Hub-Signature-256` avec le secret du webhook GitHub. | | `shopify` | En-tête `X-Shopify-Hmac-Sha256` avec le secret de l’app Shopify. | | `hmac_generic` | En-tête `X-Webhook-Signature: sha256=` (HMAC-SHA256 du corps). | | `standard_webhooks` | En-têtes `webhook-id`, `webhook-timestamp` et `webhook-signature`. | | `basic_auth` | En-tête `Authorization: Basic …` ; le secret est `utilisateur:motdepasse`. | | `none` | Aucune vérification : quiconque possède l’URL peut envoyer. À éviter en production. | `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. ### Créer une source d’ingestion * `name` et `provider` (obligatoires) ; `project_id` est facultatif (par défaut : le projet par défaut). * `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"`). * `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`. * `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. * `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. * Enregistrez l’`ingest_url` de la réponse chez le fournisseur. Jusqu’à sources par compte, tous projets confondus. ### Lister les sources Filtre facultatif par `project_id`. ### Consulter une source ### Modifier une source 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. ### Supprimer une source Les routes de la source sont supprimées avec elle et l’`ingest_url` cesse de répondre. ### Renouveler l’URL d’ingestion Génère une nouvelle `ingest_url` ; l’ancienne cesse de fonctionner immédiatement. Mettez à jour l’URL chez le fournisseur sans attendre. ### Définir le secret du fournisseur Un secret vide n’est accepté qu’avec `provider: "none"`. ### Consulter le dernier événement reçu 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. ### Envoyer un événement de test par la source 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`. 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 requêtes par minute de la source. ### Créer une route 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’à ; vide, la route prend tous les événements de la source. Jusqu’à routes par source. ### Lister les routes Filtres facultatifs par `source_id` et `project_id`. ### Modifier une route 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. ### Supprimer une route ### Recevoir un événement d’un fournisseur 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 : | Statut | Quand | | ------ | --------------------------------------------------------------------------------------- | | `200` | Accepté, doublon (renvoi du fournisseur) ou aucune route pour le type. | | `401` | Signature invalide, ou secret du fournisseur pas encore défini ; rien n’est enregistré. | | `404` | Source inconnue ou désactivée, ou outil non activé pour le compte. | | `429` | Plus de requêtes par minute pour la source. | | `5xx` | Échec de l’enregistrement ; le fournisseur doit relancer. | ### Exemple : Stripe [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. [Créez une source](#webhooks-sources-create) avec `provider: "stripe"`, `event_type_path: "type"` et `dedupe_key_path: "id"`, encore sans le secret. 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). [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). Vos 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. ### Sans crédits 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 , elles se terminent en `failed`. ## Canvas du tableau de bord 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). ### Consulter la carte du flux 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. ### Consulter la disposition du canvas ### Enregistrer la disposition du canvas `positions` stocke les coordonnées de chaque carte : clés `platform`, `source:` ou `endpoint:`. Les cartes sans position sont placées automatiquement par le tableau de bord. ## Alertes 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). | Type | Quand | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `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. | | `webhook_endpoint_disabled` | La désactivation automatique coupe un endpoint en échec depuis (pas quand vous le mettez en pause, ni sur `410 Gone`). | | `webhook_quota` | Les livraisons du mois atteignent du quota mensuel gratuit, une fois par seuil et par mois. | Les alertes ne retardent ni ne bloquent jamais un envoi. ## Livraison et relances 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 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. ## API historique du compte 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)). ## Erreurs Les erreurs de validation courantes (`validation.failed`, `not_found`) et de solde (`billing.insufficient_credits`) sont dans [Erreurs](/fr/erros). ## Limites | Limite | Valeur | | ----------------------------------------------- | -------------------------------------------------- | | Livraisons gratuites par mois (par compte) | | | Taille du `payload` / corps reçu | | | Projets par compte | | | Endpoints par compte | | | En-têtes personnalisés par endpoint | | | Types d’événement par compte | | | Messages par lot | | | Livraisons par récupération | | | Sources par compte | | | Routes par source | | | Types d’événement par route | | | Requêtes reçues par source | par minute | | Attente de crédits (`blocked_no_credits`) | | | Conservation du journal des messages | | | Fenêtre de recherche dans le corps des messages | | Les limites par compte comptent tous les projets. Ce sont les valeurs par défaut de la plateforme, qu’elle peut ajuster.