# LoopPay — integração para IAs Documentação humana: https://flowpayments.net/documentacao Guia passo a passo para IA: https://flowpayments.net/documentacao/ia Referência completa em texto: https://flowpayments.net/llms-full.txt Contrato OpenAPI 3.1: https://flowpayments.net/openapi.json Exemplo de receptor: /examples/looppay-webhook-node.mjs Base pública temporária: https://flowpayments.net/api/v1. Use https://flowpayments.net como domínio da plataforma. localhost só funciona na máquina do servidor. Autenticação: Authorization: Bearer LOOPPAY_API_KEY. A chave LoopPay pertence a uma aplicação e usa formato loop + 16 caracteres. Não use a chave do Supabase/Woovi, não coloque segredos no frontend e não peça segredos pelo chat. Escritas: application/json e Idempotency-Key UUID. Salve o UUID por operação antes de enviar. Reutilize o mesmo UUID e corpo se repetir. Nunca refaça uma cobrança ou saque com novo UUID por causa de timeout. Listas: data.items, data.total, data.page e data.limit. limit máximo 100. GET /products retorna principais; consulte /products/{id}/subproducts para cada um. Não existe lista global entre aplicações. Dinheiro: centavos inteiros; agregados podem ser strings decimais inteiras. Datas UTC ISO 8601. Recebimento implementado: Pix BRL. Ambiente sandbox/production definido no servidor, não por parâmetro do cliente. Sucesso: {data,requestId}. Erro: {error:{code,message},requestId,operationId?}. Em 503 a operação pode estar aceita: consulte operationId. Pagamentos: GET /payments?requestKey=operationId. Saques e catálogo usam operationId como ID reservado. Uma operação pendente retorna 409 até ser recuperada; não reexecute cegamente. Permissões: products:read/write, stock:read/write, payments:read/write, orders:read/write, customers:read, withdrawals:read/write, withdrawals_approval:read/write, metrics:read, reviews:read/write, webhooks:read/write, logs:read. Escrita requer leitura correspondente ao gerar a chave. Não presuma que outras permissões do painel tenham endpoints públicos. 120 chamadas/minuto por chave; 429 inclui Retry-After: 60. Também há limites por recurso. Não invente endpoints ausentes no contrato. ## Fluxo do catálogo 1. Crie categorias e produtos. name e priceCents obrigatórios. imageUrl HTTPS ou POST /products/id/image (base64, PNG/JPEG/WebP até 2 MB). 2. Subprodutos são opcionais, têm IDs e estoque próprios; até 50 por principal, sem aninhamento. Edite por PATCH /products/id_do_subproduto. 3. Para estoque automático, deliveryType=automatic; stockFormat=lines (uma unidade/linha) ou block (todo o texto/uma unidade). stockText requer stock:write. Sem estoque automático, entrega manual. 4. GET /products/id/stock retorna stockText e version. PUT substitui todo o estoque e exige essa version; 409 stock_changed pede releitura. Nunca exponha stockText ao navegador do comprador. 5. Crie pagamentos de produto com productId + quantity + name + email. O servidor determina o preço. Avulso: title + amountCents + name + email. 6. payment.completed confirmado pela instituição credita saldo e, em produção, consome o estoque uma única vez. POST /payments/id/refresh valida na instituição; não existe endpoint v1 para forçar aprovação. 7. GET /payments/id/delivery retorna entrega apenas depois de confirmação em produção. POST registra conteúdo manual quando delivery_status=waiting_seller. Não sobrescreve entrega já feita. ## Saques e métricas POST /withdrawals reserva saldo e cria transferência sem aprovar; POST /withdrawals/id/approve exige withdrawals_approval:write. completed exige confirmação da instituição; não confunda created/processing com dinheiro enviado. Mínimo líquido R$ 10,00; taxa R$ 0,70. Chave Pix criptografada e respostas mascaradas. Produção usa saldo real compartilhado do proprietário; sandbox usa somente saldo simulado. /metrics informa receita, taxas, vendas e MRR normalizado em 30 dias; /balance informa saldo; /reports exporta JSON paginado. from inclusivo/to exclusivo em UTC. Depósitos não contam como receita. Sandbox não consome estoque nem cria dinheiro real. /reviews consulta, modera e responde avaliações de compras verificadas; não cria avaliações falsas nem altera nota ou texto do comprador. ## Webhooks Eventos disponíveis: payment.created, payment.pending, payment.completed, payment.expired, delivery.completed, withdrawal.created, withdrawal.processing, withdrawal.completed, withdrawal.failed, product.created, product.updated, product.stock_updated, product.archived, review.created, review.updated, webhook.test. Cadastre HTTPS público em /webhooks e guarde secret (whsec_) somente no servidor receptor. Segredo exibido apenas na resposta original; repetição idempotente não o exibe. /rotate gera outro e invalida o antigo para próximas tentativas. Eventos financeiros filtram sandbox/production; catálogo/avaliações têm environment null. Payload: {id,type,created_at,application_id,environment,data}. Sem conteúdo privado do estoque, entrega, chave Pix ou e-mail do comprador. Cabeçalhos: X-LoopPay-Event-Id, X-LoopPay-Delivery-Id, X-LoopPay-Timestamp (segundos Unix), X-LoopPay-Signature. Assinatura: v1=HEX(HMAC-SHA256(secret, timestamp + '.' + corpo_bruto)). Valide antes de JSON.parse usando comparação constante. Rejeite timestamps com diferença maior que 300 segundos. At-least-once: eventos podem chegar repetidos e fora de ordem. Deduplique event.id em banco UNIQUE e persista em uma fila durável antes de retornar 2xx. Um Set em memória não serve em produção. Não execute negócios duplicados. Até 8 tentativas, intervalos de 60,300,900,3600,10800,21600,86400 segundos. Timeouts e HTTP não 2xx causam repetição; redirecionamentos não são seguidos. Até 3 reprocessamentos manuais de entrega failed. Pausas cancelam tentativas futuras; requisições já em trânsito podem terminar. Cadastro não envia histórico passado. /webhooks/id/test enfileira webhook.test, mesmo sem selecioná-lo nos eventos. HTTP 202 não significa entregue. O servidor LoopPay precisa agendar POST /api/webhooks/worker com WEBHOOKS_CRON_SECRET (ou PAYMENTS_CRON_SECRET). Sem agendador, o painel permite Processar pendentes. Nunca forneça este segredo administrativo à IA do cliente. Logs: Fluxos → Logs → Webhooks e /webhooks/id/deliveries; chamadas API: /logs. ## Prompt inicial Integre meu sistema à API LoopPay v1. Antes de escrever código, leia https://flowpayments.net/llms-full.txt e https://flowpayments.net/openapi.json. Use https://flowpayments.net/api/v1 como base e LOOPPAY_API_KEY somente no servidor. Não peça que eu cole a chave no chat e não use credenciais Supabase ou Woovi. Implemente somente endpoints existentes no contrato. Comece com GET /products?limit=20&page=1 e percorra todas as páginas. Produtos, subprodutos, fotos, estoque, cobranças e saques devem seguir os nomes, tipos e permissões documentados. Em escritas, persista um UUID Idempotency-Key por operação e reutilize o mesmo corpo nas novas tentativas. Não crie outro pagamento ou saque após um timeout sem consultar o recurso. Valide webhooks pelo corpo bruto, timestamp e HMAC SHA-256 antes de fazer JSON.parse. Deduplique event.id em banco com restrição UNIQUE e confirme apenas depois de persistir em uma fila durável. No sandbox não há saldo real nem consumo de estoque. Não simule aprovação em produção. Não registre chaves, estoque privado, entrega ou dados pessoais nos logs. Trate erros HTTP, paginação e limites. Entregue o código, variáveis de ambiente sem segredos, testes automatizados e as instruções para executar. ## Referência de endpoints ### GET /api/v1/products Listar produtos Permissão: products:read Produtos principais da aplicação. Use q para buscar pelo nome. Os subprodutos têm uma listagem própria. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "name": "Acesso mensal", "description": "Licença digital por 30 dias", "price_cents": 2500, "delivery_type": "automatic", "stock_format": "lines", "stock_count": 2, "image_url": "https://sua-loja.com/produto.png", "parent_id": null, "visible": true, "checkout_path": "/checkout/00000000-0000-4000-8000-000000000001" } ], "total": 1, "page": 1, "limit": 20 } ``` ### POST /api/v1/products Criar produto Permissão: products:write Cria o produto no banco e retorna o endereço de checkout. Valores recebidos usam camelCase; projeções do banco usam snake_case. - name (string, obrigatório): Nome de 2 a 120 caracteres. - priceCents (integer, obrigatório): Preço unitário em centavos. De 1 a 999999999; preço × quantidade mínima deve ser pelo menos 200. - description (string): Descrição de até 10000 caracteres. Padrão: vazio. - slug (string): Endereço com até 100 caracteres. Gerado a partir do nome se omitido. - comparePriceCents (integer | null): Preço anterior; deve ser igual ou maior que o preço atual. Padrão: null. - minQuantity (integer): Quantidade mínima de 1 a 10000. Padrão: 1. - maxQuantity (integer): Máximo por pedido, de minQuantity a 10000. Padrão: 10000. - deliveryType (manual | automatic): manual: entrega pelo vendedor. automatic: consome o estoque após confirmação em produção. Padrão: manual. - stockFormat (lines | block): lines: uma unidade por linha. block: todo o texto é uma unidade. Padrão: lines. - stockText (string): Conteúdo privado de até 100000 caracteres. Exige também stock:write. Omitido na edição preserva o estoque. - instructions (string): Instruções após pagamento, até 5000 caracteres. Nunca são enviadas ao comprador antes da confirmação. - imageUrl (string): URL HTTPS pública, até 2000 caracteres. Também pode enviar a imagem em /image. - videoUrl (string): URL HTTPS do vídeo, até 2000 caracteres. - categoryId (uuid | null): Categoria da mesma aplicação. Padrão: null. - visible (boolean): Exibe e permite a compra quando true. Padrão: true. false preserva o histórico. Corpo de exemplo: ```json { "name": "Acesso mensal", "priceCents": 2500, "description": "Licença por 30 dias", "deliveryType": "manual", "imageUrl": "https://sua-loja.com/produto.png" } ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "name": "Acesso mensal", "description": "Licença digital por 30 dias", "price_cents": 2500, "delivery_type": "automatic", "stock_format": "lines", "stock_count": 2, "image_url": "https://sua-loja.com/produto.png", "parent_id": null, "visible": true, "checkout_path": "/checkout/00000000-0000-4000-8000-000000000001" } ``` ### GET /api/v1/products/{id} Consultar produto Permissão: products:read Retorna nome, preço, descrição, foto, estoque disponível, entrega, categoria, parent_id e links. Não retorna conteúdo privado do estoque. Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "name": "Acesso mensal", "description": "Licença digital por 30 dias", "price_cents": 2500, "delivery_type": "automatic", "stock_format": "lines", "stock_count": 2, "image_url": "https://sua-loja.com/produto.png", "parent_id": null, "visible": true, "checkout_path": "/checkout/00000000-0000-4000-8000-000000000001" } ``` ### PATCH /api/v1/products/{id} Atualizar produto Permissão: products:write Envie apenas os campos que deseja alterar. Para desativar sem apagar pedidos, envie visible: false. Ao mudar o formato do estoque ou o tipo de entrega, envie também stockText e tenha stock:write. - name (string): Nome de 2 a 120 caracteres. - priceCents (integer): Preço unitário em centavos. De 1 a 999999999; preço × quantidade mínima deve ser pelo menos 200. - description (string): Descrição de até 10000 caracteres. Padrão: vazio. - slug (string): Endereço com até 100 caracteres. Gerado a partir do nome se omitido. - comparePriceCents (integer | null): Preço anterior; deve ser igual ou maior que o preço atual. Padrão: null. - minQuantity (integer): Quantidade mínima de 1 a 10000. Padrão: 1. - maxQuantity (integer): Máximo por pedido, de minQuantity a 10000. Padrão: 10000. - deliveryType (manual | automatic): manual: entrega pelo vendedor. automatic: consome o estoque após confirmação em produção. Padrão: manual. - stockFormat (lines | block): lines: uma unidade por linha. block: todo o texto é uma unidade. Padrão: lines. - stockText (string): Conteúdo privado de até 100000 caracteres. Exige também stock:write. Omitido na edição preserva o estoque. - instructions (string): Instruções após pagamento, até 5000 caracteres. Nunca são enviadas ao comprador antes da confirmação. - imageUrl (string): URL HTTPS pública, até 2000 caracteres. Também pode enviar a imagem em /image. - videoUrl (string): URL HTTPS do vídeo, até 2000 caracteres. - categoryId (uuid | null): Categoria da mesma aplicação. Padrão: null. - visible (boolean): Exibe e permite a compra quando true. Padrão: true. false preserva o histórico. Corpo de exemplo: ```json { "name": "Acesso mensal Premium", "priceCents": 3000, "visible": true } ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "name": "Acesso mensal", "description": "Licença digital por 30 dias", "price_cents": 2500, "delivery_type": "automatic", "stock_format": "lines", "stock_count": 2, "image_url": "https://sua-loja.com/produto.png", "parent_id": null, "visible": true, "checkout_path": "/checkout/00000000-0000-4000-8000-000000000001" } ``` ### POST /api/v1/products/{id}/image Enviar foto Permissão: products:write Valida a assinatura do arquivo, hospeda no bucket público product-media e vincula ao produto. Até 2 MB; PNG, JPEG ou WebP. Não envie arquivos de entrega neste bucket. - base64 (string, obrigatório): Conteúdo do arquivo em base64, sem prefixo data:. - contentType (string, obrigatório): image/png, image/jpeg ou image/webp; deve corresponder ao arquivo. Corpo de exemplo: ```json { "base64": "BASE64_DO_ARQUIVO", "contentType": "image/png" } ``` Resposta de exemplo (dentro de data): ```json { "productId": "00000000-0000-4000-8000-000000000001", "imageUrl": "https://seu-projeto.supabase.co/storage/v1/object/public/product-media/foto.png" } ``` ### GET /api/v1/products/{id}/subproducts Listar subprodutos Permissão: products:read Opções independentes do produto principal. Cada opção tem id, nome, preço, descrição, foto e estoque próprios. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "name": "Acesso mensal", "description": "Licença digital por 30 dias", "price_cents": 2500, "delivery_type": "automatic", "stock_format": "lines", "stock_count": 2, "image_url": "https://sua-loja.com/produto.png", "parent_id": null, "visible": true, "checkout_path": "/checkout/00000000-0000-4000-8000-000000000001" } ], "total": 1, "page": 1, "limit": 20 } ``` ### POST /api/v1/products/{id}/subproducts Criar subproduto Permissão: products:write Usa os mesmos campos de um produto. Até 50 opções por produto, sem níveis aninhados. Para editar depois, use PATCH /products/{id_do_subproduto}. Comprar usa productId do subproduto. O produto principal continua sendo uma opção. - name (string, obrigatório): Nome de 2 a 120 caracteres. - priceCents (integer, obrigatório): Preço unitário em centavos. De 1 a 999999999; preço × quantidade mínima deve ser pelo menos 200. - description (string): Descrição de até 10000 caracteres. Padrão: vazio. - slug (string): Endereço com até 100 caracteres. Gerado a partir do nome se omitido. - comparePriceCents (integer | null): Preço anterior; deve ser igual ou maior que o preço atual. Padrão: null. - minQuantity (integer): Quantidade mínima de 1 a 10000. Padrão: 1. - maxQuantity (integer): Máximo por pedido, de minQuantity a 10000. Padrão: 10000. - deliveryType (manual | automatic): manual: entrega pelo vendedor. automatic: consome o estoque após confirmação em produção. Padrão: manual. - stockFormat (lines | block): lines: uma unidade por linha. block: todo o texto é uma unidade. Padrão: lines. - stockText (string): Conteúdo privado de até 100000 caracteres. Exige também stock:write. Omitido na edição preserva o estoque. - instructions (string): Instruções após pagamento, até 5000 caracteres. Nunca são enviadas ao comprador antes da confirmação. - imageUrl (string): URL HTTPS pública, até 2000 caracteres. Também pode enviar a imagem em /image. - videoUrl (string): URL HTTPS do vídeo, até 2000 caracteres. - categoryId (uuid | null): Categoria da mesma aplicação. Padrão: null. - visible (boolean): Exibe e permite a compra quando true. Padrão: true. false preserva o histórico. Corpo de exemplo: ```json { "name": "Acesso anual", "priceCents": 24000, "description": "12 meses de acesso", "deliveryType": "manual" } ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "name": "Acesso mensal", "description": "Licença digital por 30 dias", "price_cents": 2500, "delivery_type": "automatic", "stock_format": "lines", "stock_count": 2, "image_url": "https://sua-loja.com/produto.png", "parent_id": "00000000-0000-4000-8000-000000000001", "visible": true, "checkout_path": "/checkout/00000000-0000-4000-8000-000000000001" } ``` ### GET /api/v1/products/{id}/stock Consultar estoque privado Permissão: stock:read Retorna o conteúdo descriptografado somente para a integração autorizada. A versão deve acompanhar a substituição do estoque. Não exponha esta chamada no navegador do comprador. Resposta de exemplo (dentro de data): ```json { "productId": "00000000-0000-4000-8000-000000000001", "stockText": "LICENCA-A\nLICENCA-B", "stockCount": 2, "stockFormat": "lines", "version": "2026-11-02T18:00:00.000Z" } ``` ### PUT /api/v1/products/{id}/stock Substituir estoque Permissão: stock:write Substitui todo o conteúdo, não acrescenta. Para repor, leia a versão atual e envie o conteúdo desejado. Se outra operação alterou o produto, retorna 409 stock_changed. Estoque automático é consumido uma única vez por pagamento confirmado. - stockText (string, obrigatório): Até 100000 caracteres; linhas vazias não contam. - stockFormat (lines | block, obrigatório): Uma unidade por linha ou todo o conteúdo em uma unidade. - version (string, obrigatório): Valor version da consulta de estoque. Corpo de exemplo: ```json { "stockText": "LICENCA-A\nLICENCA-B", "stockFormat": "lines", "version": "2026-11-02T18:00:00.000Z" } ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "stock_count": 2, "stock_format": "lines", "updated_at": "2026-11-02T18:01:00Z" } ``` ### GET /api/v1/categories Listar categorias Permissão: products:read Categorias da aplicação, paginadas. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "name": "Licenças", "slug": "licencas", "visible": true } ], "total": 1, "page": 1, "limit": 20 } ``` ### POST /api/v1/categories Criar categoria Permissão: products:write Organiza os produtos da aplicação. - name (string, obrigatório): 2 a 80 caracteres; não pode ser Sem categoria. - subtitle (string): Até 160 caracteres. - description (string): Até 2000 caracteres. - slug (string): Até 100 caracteres; gerado do nome se omitido. - visible (boolean): Padrão: true. Corpo de exemplo: ```json { "name": "Licenças", "subtitle": "Entrega digital", "visible": true } ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "name": "Licenças", "slug": "licencas", "visible": true } ``` ### PATCH /api/v1/categories/{id} Editar categoria Permissão: products:write Aceita os mesmos campos da criação; envie somente as alterações. Corpo de exemplo: ```json { "name": "Licenças digitais", "visible": true } ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "name": "Licenças digitais", "visible": true } ``` ### POST /api/v1/payments Gerar pagamento Permissão: payments:write Gera cobrança Pix dinâmica na Woovi e salva a reserva na aplicação da chave. Informe productId e quantity para vender um produto/subproduto; o servidor determina o preço. Para cobrança avulsa, omita productId e informe title e amountCents. Moeda de recebimento: BRL. - name (string, obrigatório): Nome do comprador; 2 a 100 caracteres. - email (string, obrigatório): E-mail válido, até 254 caracteres. - productId (uuid): ID do produto ou subproduto da aplicação. Não enviar amountCents neste modo. - quantity (integer): Padrão 1; deve respeitar os limites e o estoque do produto. - title (string): Obrigatório para cobrança avulsa; 2 a 120 caracteres. - amountCents (integer): Obrigatório para cobrança avulsa; 200 a 100000000 centavos. Corpo de exemplo: ```json { "productId": "00000000-0000-4000-8000-000000000001", "quantity": 1, "name": "Cliente exemplo", "email": "cliente@exemplo.com" } ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "environment": "sandbox", "amount_cents": 2500, "fee_cents": 100, "net_cents": 2400, "status": "pending", "br_code": "000201…", "expires_at": "2026-11-02T19:00:00Z", "paid_at": null, "kind": "product", "productId": "00000000-0000-4000-8000-000000000001", "quantity": 1, "requestKey": "00000000-0000-4000-8000-000000000001" } ``` ### GET /api/v1/payments Listar pagamentos Permissão: payments:read Filtre por status (creating, pending, completed, expired) ou requestKey retornado na criação. Ambiente definido pelo servidor. A consulta não força confirmação na instituição. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "environment": "sandbox", "amount_cents": 2500, "fee_cents": 100, "net_cents": 2400, "status": "pending", "br_code": "000201…", "expires_at": "2026-11-02T19:00:00Z", "paid_at": null, "kind": "product", "productId": "00000000-0000-4000-8000-000000000001", "quantity": 1 } ], "total": 1, "page": 1, "limit": 20 } ``` ### GET /api/v1/payments/{id} Consultar pagamento Permissão: payments:read Retorna o estado salvo. completed é confirmação; pending não libera saldo. Conteúdo de entrega fica na rota /delivery. Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "environment": "sandbox", "amount_cents": 2500, "fee_cents": 100, "net_cents": 2400, "status": "pending", "br_code": "000201…", "expires_at": "2026-11-02T19:00:00Z", "paid_at": null, "kind": "product", "productId": "00000000-0000-4000-8000-000000000001", "quantity": 1 } ``` ### POST /api/v1/payments/{id}/refresh Validar pagamento na instituição Permissão: payments:write Consulta a cobrança na Woovi e confere correlação, valor e ambiente. Somente uma confirmação válida credita a carteira; chamadas repetidas não duplicam crédito ou entrega. Não aceita status informado pelo cliente. Envie {} no corpo. Corpo de exemplo: ```json {} ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "environment": "sandbox", "amount_cents": 2500, "fee_cents": 100, "net_cents": 2400, "status": "completed", "br_code": "000201…", "expires_at": "2026-11-02T19:00:00Z", "paid_at": "2026-11-02T18:02:00Z", "kind": "product", "productId": "00000000-0000-4000-8000-000000000001", "quantity": 1 } ``` ### GET /api/v1/payments/{id}/delivery Consultar entrega do pedido Permissão: orders:read Conteúdo e instruções ficam disponíveis após confirmação em produção. No modo automático, o comprador também os vê no checkout original. No sandbox, o estoque não é consumido e content é null. Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "status": "delivered", "content": "LICENCA-A", "instructions": "Use esta licença no seu aplicativo." } ``` ### POST /api/v1/payments/{id}/delivery Registrar entrega manual Permissão: orders:write Salva uma entrega privada para produto pago em produção com delivery_status waiting_seller. O conteúdo aparece no checkout do comprador. Não substitui uma entrega já concluída. - content (string, obrigatório): Conteúdo de entrega com 1 a 100000 caracteres. Corpo de exemplo: ```json { "content": "Seu acesso está disponível: instruções de ativação." } ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "delivery_status": "delivered" } ``` ### POST /api/v1/withdrawals Criar saque Permissão: withdrawals:write Reserva saldo e cria a transferência sem aprová-la. Tarifa R$ 0,70; mínimo líquido R$ 10,00. O saldo é da conta do proprietário e é compartilhado entre lojas. O ambiente acompanha a configuração do servidor. Não é necessário chamar Account Data Check. - keyType (CPF | CNPJ | EMAIL | PHONE | RANDOM, obrigatório): Tipo da chave Pix. - key (string, obrigatório): Chave de destino; telefone em formato internacional. Guardada criptografada. - amountCents (integer, obrigatório): Valor líquido, ou total caso deductFee seja true. Máximo 100000000. - deductFee (boolean): false: soma tarifa ao total reservado. true: deduz tarifa do valor informado. Corpo de exemplo: ```json { "keyType": "EMAIL", "key": "financeiro@exemplo.com", "amountCents": 1000, "deductFee": false } ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "pix_key_type": "EMAIL", "pix_key_mask": "•••• .com", "amount_cents": 1000, "fee_cents": 70, "total_cents": 1070, "status": "created", "provider_status": "CREATED" } ``` ### GET /api/v1/withdrawals Histórico de saques Permissão: withdrawals:read Lista solicitações da aplicação no ambiente atual. As chaves Pix aparecem mascaradas. Resposta de exemplo (dentro de data): ```json { "environment": "sandbox", "items": [ { "id": "00000000-0000-4000-8000-000000000001", "pix_key_type": "EMAIL", "pix_key_mask": "•••• .com", "amount_cents": 1000, "fee_cents": 70, "total_cents": 1070, "status": "created", "provider_status": "CREATED" } ], "total": 1, "page": 1, "limit": 20 } ``` ### GET /api/v1/withdrawals/{id} Consultar saque Permissão: withdrawals:read Consulta local paginada, com items contendo no máximo a solicitação informada. Resposta de exemplo (dentro de data): ```json { "environment": "sandbox", "items": [ { "id": "00000000-0000-4000-8000-000000000001", "pix_key_type": "EMAIL", "pix_key_mask": "•••• .com", "amount_cents": 1000, "fee_cents": 70, "total_cents": 1070, "status": "created", "provider_status": "CREATED" } ], "total": 1, "page": 1, "limit": 20 } ``` ### POST /api/v1/withdrawals/{id}/approve Aprovar envio Permissão: withdrawals_approval:write Autoriza o envio à chave cadastrada. Use uma chave com esta permissão apenas no servidor. CREATED ainda não é transferência concluída; completed exige CONFIRMED da instituição. Envie {}. Corpo de exemplo: ```json {} ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "pix_key_type": "EMAIL", "pix_key_mask": "•••• .com", "amount_cents": 1000, "fee_cents": 70, "total_cents": 1070, "status": "processing", "provider_status": "APPROVED" } ``` ### POST /api/v1/withdrawals/{id}/refresh Conferir saque na instituição Permissão: withdrawals:write Atualiza o estado pela Woovi. Falha definitiva libera a reserva uma única vez; resposta desconhecida mantém o valor reservado. Em produção, chamadas não retornam de completed para failed. Envie {}. Corpo de exemplo: ```json {} ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "pix_key_type": "EMAIL", "pix_key_mask": "•••• .com", "amount_cents": 1000, "fee_cents": 70, "total_cents": 1070, "status": "completed", "provider_status": "CONFIRMED" } ``` ### GET /api/v1/balance Saldo disponível Permissão: metrics:read Saldo em centavos como string. Em produção, considera a carteira real e reservas de saque. No sandbox, considera somente os pagamentos simulados e saques de teste. Resposta de exemplo (dentro de data): ```json { "currency": "BRL", "environment": "sandbox", "availableCents": "2400", "scope": "owner_account", "sharedAcrossApplications": true } ``` ### GET /api/v1/metrics Receita, vendas e MRR Permissão: metrics:read Parâmetros from e to em ISO 8601; início inclusivo e fim exclusivo. Padrão: últimos 30 dias. Receita exclui depósitos. MRR usa assinaturas ativas e pagas, normalizadas em 30 dias; é indicador de recorrência, não cobrança automática. Resposta de exemplo (dentro de data): ```json { "currency": "BRL", "environment": "production", "grossCents": "2500", "feeCents": "100", "netCents": "2400", "approvedPayments": 1, "sales": 1, "pendingPayments": 0, "mrrCents": "2500", "mrrBasis": "30_days_active_paid_subscriptions" } ``` ### GET /api/v1/reports Relatório de pagamentos Permissão: metrics:read Exportação JSON paginada com valores brutos, taxas, líquidos, ambiente, datas e estado. Use from e to (ISO 8601). Datas UTC; centavos inteiros. Percorra as páginas até atingir total. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "environment": "sandbox", "amount_cents": 2500, "fee_cents": 100, "net_cents": 2400, "status": "pending", "br_code": "000201…", "expires_at": "2026-11-02T19:00:00Z", "paid_at": null, "kind": "product", "productId": "00000000-0000-4000-8000-000000000001", "quantity": 1 } ], "total": 1, "page": 1, "limit": 20, "environment": "sandbox", "currency": "BRL" } ``` ### GET /api/v1/customers Listar clientes Permissão: customers:read Clientes da aplicação, criados por compras reais ou pelo painel. Dados pessoais devem ser utilizados somente pela integração autorizada. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "name": "Cliente exemplo", "email": "cliente@exemplo.com", "tags": [] } ], "total": 1, "page": 1, "limit": 20 } ``` ### GET /api/v1/orders Listar pedidos Permissão: orders:read Pedidos registrados em produção, com produto, valor, cliente, estado e referência. Pagamentos de sandbox não geram pedidos reais. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "order_reference": "00000000-0000-4000-8000-000000000001", "product_name": "Acesso mensal", "amount_cents": 2500, "status": "approved", "delivery_type": "automatic" } ], "total": 1, "page": 1, "limit": 20 } ``` ### GET /api/v1/reviews Consultar avaliações Permissão: reviews:read Filtro status: pending, published ou hidden. A API não fabrica avaliações: o envio pelo comprador continua exigindo uma compra verificada e autenticação. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "product_id": "00000000-0000-4000-8000-000000000001", "display_name": "Cliente", "rating": 5, "body": "Entrega rápida.", "status": "pending", "owner_reply": null } ], "total": 1, "page": 1, "limit": 20 } ``` ### PATCH /api/v1/reviews/{id} Moderar e responder Permissão: reviews:write Publica, oculta ou responde uma avaliação recebida. Não altera a nota nem o texto do comprador. - status (pending | published | hidden): Novo estado da moderação. - reply (string): Resposta de até 2000 caracteres. Vazio remove a resposta. Corpo de exemplo: ```json { "status": "published", "reply": "Obrigado pela confiança!" } ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "status": "published", "owner_reply": "Obrigado pela confiança!" } ``` ### GET /api/v1/logs Consultar logs da API Permissão: logs:read Requisições autenticadas da aplicação: método, rota normalizada, HTTP status, duração, erro, ID da chave e requestId. Sem cabeçalhos de autenticação, corpo, conteúdo do estoque ou chave Pix. Também disponível em Fluxos → Logs → Requisições da API. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "method": "POST", "route": "/api/v1/payments", "status": 201, "duration_ms": 340, "error_code": null } ], "total": 1, "page": 1, "limit": 20 } ``` ### GET /api/v1/categories/{id} Consultar categorias por ID Permissão: products:read Consulta filtrada pelo UUID. A resposta usa items com no máximo um recurso da aplicação. Categorias da aplicação, paginadas. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "name": "Licenças", "slug": "licencas", "visible": true } ], "total": 1, "page": 1, "limit": 20 } ``` ### GET /api/v1/customers/{id} Consultar clientes por ID Permissão: customers:read Consulta filtrada pelo UUID. A resposta usa items com no máximo um recurso da aplicação. Clientes da aplicação, criados por compras reais ou pelo painel. Dados pessoais devem ser utilizados somente pela integração autorizada. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "name": "Cliente exemplo", "email": "cliente@exemplo.com", "tags": [] } ], "total": 1, "page": 1, "limit": 20 } ``` ### GET /api/v1/orders/{id} Consultar pedidos por ID Permissão: orders:read Consulta filtrada pelo UUID. A resposta usa items com no máximo um recurso da aplicação. Pedidos registrados em produção, com produto, valor, cliente, estado e referência. Pagamentos de sandbox não geram pedidos reais. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "order_reference": "00000000-0000-4000-8000-000000000001", "product_name": "Acesso mensal", "amount_cents": 2500, "status": "approved", "delivery_type": "automatic" } ], "total": 1, "page": 1, "limit": 20 } ``` ### GET /api/v1/reviews/{id} Consultar avaliações por ID Permissão: reviews:read Consulta filtrada pelo UUID. A resposta usa items com no máximo um recurso da aplicação. Filtro status: pending, published ou hidden. A API não fabrica avaliações: o envio pelo comprador continua exigindo uma compra verificada e autenticação. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "product_id": "00000000-0000-4000-8000-000000000001", "display_name": "Cliente", "rating": 5, "body": "Entrega rápida.", "status": "pending", "owner_reply": null } ], "total": 1, "page": 1, "limit": 20 } ``` ### GET /api/v1/webhooks Listar destinos Permissão: webhooks:read Lista destinos da aplicação, sem o segredo de assinatura. Até 10 destinos não arquivados. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "name": "Sistema de pedidos", "url": "https://seu-sistema.com/webhooks/looppay", "events": [ "payment.completed", "delivery.completed" ], "environment": "sandbox", "enabled": true, "version": 1 } ], "total": 1, "page": 1, "limit": 20 } ``` ### GET /api/v1/webhooks/{id} Consultar destino Permissão: webhooks:read Consulta nome, URL, eventos, ambiente, estado e versão. O segredo não é retornado. Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "name": "Sistema de pedidos", "url": "https://seu-sistema.com/webhooks/looppay", "events": [ "payment.completed", "delivery.completed" ], "environment": "sandbox", "enabled": true, "version": 1 } ``` ### POST /api/v1/webhooks Cadastrar webhook Permissão: webhooks:write Cria um destino HTTPS público. Retorna o segredo apenas na resposta original, criptografado no banco. A repetição idempotente retorna metadados sem secret. Se perder o segredo, use /rotate. - name (string, obrigatório): Nome de 2 a 80 caracteres. - url (string, obrigatório): URL HTTPS pública na porta 443, até 2000 caracteres. Sem usuário, senha, fragmento ou IP literal. Segredos não devem ser colocados na URL. - events (array, obrigatório): Lista dos eventos documentados. Ao menos um. Ex.: payment.completed e delivery.completed. - environment (sandbox | production, obrigatório): Filtra pagamentos e saques. Produtos e avaliações são eventos de catálogo, com environment null. - enabled (boolean, obrigatório): true ativa; false pausa. Corpo de exemplo: ```json { "name": "Sistema de pedidos", "url": "https://seu-sistema.com/webhooks/looppay", "events": [ "payment.completed", "delivery.completed" ], "environment": "sandbox", "enabled": true } ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "name": "Sistema de pedidos", "url": "https://seu-sistema.com/webhooks/looppay", "events": [ "payment.completed", "delivery.completed" ], "environment": "sandbox", "enabled": true, "version": 1, "secret": "whsec_SEGREDO_DE_ASSINATURA" } ``` ### PATCH /api/v1/webhooks/{id} Alterar ou pausar destino Permissão: webhooks:write Envie somente os campos desejados. Mudanças se aplicam às tentativas futuras; uma requisição já em andamento pode terminar no destino anterior. Tentativas de um destino pausado são canceladas ao serem processadas. - name (string): Nome de 2 a 80 caracteres. - url (string): URL HTTPS pública na porta 443, até 2000 caracteres. Sem usuário, senha, fragmento ou IP literal. Segredos não devem ser colocados na URL. - events (array): Lista dos eventos documentados. Ao menos um. Ex.: payment.completed e delivery.completed. - environment (sandbox | production): Filtra pagamentos e saques. Produtos e avaliações são eventos de catálogo, com environment null. - enabled (boolean): true ativa; false pausa. Corpo de exemplo: ```json { "enabled": false } ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "name": "Sistema de pedidos", "url": "https://seu-sistema.com/webhooks/looppay", "events": [ "payment.completed", "delivery.completed" ], "environment": "sandbox", "enabled": false, "version": 2 } ``` ### DELETE /api/v1/webhooks/{id} Arquivar destino Permissão: webhooks:write Arquiva e desativa o endpoint, preservando o histórico. Envie {} e um Idempotency-Key. Corpo de exemplo: ```json {} ``` Resposta de exemplo (dentro de data): ```json { "archived": true } ``` ### POST /api/v1/webhooks/{id}/rotate Trocar segredo de assinatura Permissão: webhooks:write Gera e retorna um novo secret apenas na resposta original. O antigo deixa de ser usado nos próximos envios. Atualize o receptor imediatamente; uma requisição já em andamento pode usar o segredo anterior. Corpo de exemplo: ```json {} ``` Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "name": "Sistema de pedidos", "url": "https://seu-sistema.com/webhooks/looppay", "events": [ "payment.completed", "delivery.completed" ], "environment": "sandbox", "enabled": true, "version": 2, "secret": "whsec_NOVO_SEGREDO" } ``` ### POST /api/v1/webhooks/{id}/test Enfileirar evento de teste Permissão: webhooks:write Cria webhook.test para o destino ativo, independente dos eventos selecionados. Não gera pagamentos. HTTP 202 significa enfileirado; acompanhe a entrega. O worker precisa estar agendado. Corpo de exemplo: ```json {} ``` Resposta de exemplo (dentro de data): ```json { "eventId": "00000000-0000-4000-8000-000000000001", "queued": true } ``` ### GET /api/v1/webhooks/{id}/deliveries Listar entregas Permissão: webhooks:read Histórico paginado com HTTP status, tentativas, próxima tentativa e erro. Sem corpo da resposta remota ou segredos. Resposta de exemplo (dentro de data): ```json { "items": [ { "id": "00000000-0000-4000-8000-000000000001", "endpoint_id": "00000000-0000-4000-8000-000000000001", "event_id": "00000000-0000-4000-8000-000000000001", "status": "retrying", "attempts": 1, "http_status": 500, "error_code": "http_error", "next_attempt_at": "2026-11-02T18:01:00Z" } ], "total": 1, "page": 1, "limit": 20 } ``` ### GET /api/v1/webhook-deliveries/{id} Inspecionar entrega e tentativas Permissão: webhooks:read Retorna os metadados da entrega, payload do evento e histórico de tentativas. Status: pending, processing, retrying, succeeded, failed e cancelled. Resposta de exemplo (dentro de data): ```json { "id": "00000000-0000-4000-8000-000000000001", "status": "succeeded", "attempts": [ { "attempt": 1, "http_status": 200, "duration_ms": 130, "error_code": null } ], "payload": { "id": "00000000-0000-4000-8000-000000000001", "type": "payment.completed", "environment": "sandbox", "data": { "id": "00000000-0000-4000-8000-000000000001", "status": "completed", "amount_cents": 2500 } } } ``` ### POST /api/v1/webhook-deliveries/{id}/retry Reprocessar entrega falha Permissão: webhooks:write Somente failed, destino ativo e no máximo 3 reprocessamentos manuais. Enfileira novo ciclo de até 8 tentativas. O event_id continua o mesmo: deduplique no receptor. Envie {}. Corpo de exemplo: ```json {} ``` Resposta de exemplo (dentro de data): ```json { "queued": true } ``` ## Receptor de webhook Node.js ```javascript // Node.js 20+. Deploy behind HTTPS at a public domain. // Set LOOPPAY_WEBHOOK_SECRET to the whsec_ value shown when creating the endpoint. import {createServer} from 'node:http'; import {createHmac,timingSafeEqual} from 'node:crypto'; const secret=process.env.LOOPPAY_WEBHOOK_SECRET; if(!secret)throw new Error('Configure LOOPPAY_WEBHOOK_SECRET'); const server=createServer(async(req,res)=>{ if(req.method!=='POST'||req.url!=='/webhooks/looppay'){res.writeHead(404).end();return;} try{ const chunks=[];let size=0; for await(const chunk of req){size+=chunk.length;if(size>131072){res.writeHead(413).end();return;}chunks.push(chunk);} const raw=Buffer.concat(chunks),timestamp=String(req.headers['x-looppay-timestamp']||''),signature=String(req.headers['x-looppay-signature']||''); const expected='v1='+createHmac('sha256',secret).update(timestamp+'.').update(raw).digest('hex'); if(!/^\d{10,11}$/.test(timestamp)||Math.abs(Math.floor(Date.now()/1000)-Number(timestamp))>300||!/^v1=[a-f0-9]{64}$/.test(signature)||!timingSafeEqual(Buffer.from(signature),Buffer.from(expected))){res.writeHead(401).end('Invalid signature');return;} const event=JSON.parse(raw.toString('utf8')); if(event.id!==req.headers['x-looppay-event-id']){res.writeHead(400).end('Invalid event');return;} // IN YOUR APPLICATION, BEFORE ACKNOWLEDGING: // 1. Persist event.id under a UNIQUE constraint in your database. // 2. Atomically enqueue the verified event in a durable inbox/job table. // 3. A duplicate event.id should return 200 without duplicating business actions. // 4. Only return 2xx after that database transaction commits. // Example intentionally returns 501 until durable persistence is implemented. // Do not replace this with an in-memory Set for production. void event; res.writeHead(501).end('Implement durable inbox persistence before acknowledging'); }catch{res.writeHead(400).end('Invalid request');} }); server.requestTimeout=15000; server.listen(Number(process.env.PORT||8080)); ```