# Quibly Pay > Pagamentos PIX e por cartão para produtores digitais. A API pública v3 cria cobranças PIX direto e sessões de checkout, que levam o comprador ao checkout hospedado para pagar com PIX ou cartão e voltar para o seu site. URL base da API: [https://api.quiblypay.com](https://api.quiblypay.com). Faça as chamadas pelo seu servidor. POST /v3/pix/qrcode cria uma cobrança; POST /v3/pix/transaction consulta seu status; POST /v3/pix/payment solicita um saque. POST /v3/checkout/sessions cria uma sessão de checkout hospedado (PIX ou cartão) com um success_url de volta para o seu site. ## Documentação de integração * [Referência completa de integração](https://app.quiblypay.com/llms-full.txt): Autenticação, rotas v3, exemplos executáveis no servidor e webhooks assinados. * [Documentação para leitura](https://docs.quiblypay.com/docs): O mesmo conteúdo de integração. ## Optional * [Painel do produtor](https://app.quiblypay.com): Crie credenciais de API e links de pagamento. * [Checkout hospedado](https://pay.quiblypay.com): Compartilhe a URL específica do checkout copiada do painel. # Introdução (https://docs.quiblypay.com/docs) > Pagamentos PIX e por cartão para produtores digitais. A API pública v3 cria cobranças PIX direto e sessões de checkout, que levam o comprador ao checkout hospedado para pagar com PIX ou cartão e voltar para o seu site. URL base da API: [https://api.quiblypay.com](https://api.quiblypay.com). Faça as chamadas pelo seu servidor. POST /v3/pix/qrcode cria uma cobrança; POST /v3/pix/transaction consulta seu status; POST /v3/pix/payment solicita um saque. POST /v3/checkout/sessions cria uma sessão de checkout hospedado (PIX ou cartão) com um success_url de volta para o seu site. ## Documentação de integração * [Referência completa de integração](https://app.quiblypay.com/llms-full.txt): Autenticação, rotas v3, exemplos executáveis no servidor e webhooks assinados. * [Documentação para leitura](https://docs.quiblypay.com/docs): O mesmo conteúdo de integração. ## Optional * [Painel do produtor](https://app.quiblypay.com): Crie credenciais de API e links de pagamento. * [Checkout hospedado](https://pay.quiblypay.com): Compartilhe a URL específica do checkout copiada do painel. # Credenciais (https://docs.quiblypay.com/docs/comecando/credenciais) 1. No painel, abra Desenvolvedor > Credenciais de API. Crie uma credencial SANDBOX com os escopos cashin e read. Uma credencial é `client_id` + `client_secret`; guarde o `client_secret` com segurança quando ele aparecer. 2. Em Desenvolvedor > Webhooks, cadastre a URL HTTPS pública do receptor, escolha os eventos e defina seu segredo de webhook, ou deixe o campo vazio e copie o segredo gerado. Mantenha "Usar nas notificações por cobrança" habilitado para entregar as cobranças criadas com urlnoty. Seu receptor compara o header X-Quibly-Secret com esse segredo. # Sandbox (https://docs.quiblypay.com/docs/comecando/sandbox) 3. Chame POST /v3/sandbox/pix/qrcode, exiba qrcode como texto PIX copia e cola ou converta exatamente esse texto em uma imagem QR. Salve transactionId junto ao pedido. 4. Chame POST /v3/sandbox/simulate-payment com `transaction_id`. Receba e verifique transaction.paid; depois chame POST /v3/sandbox/pix/transaction para confirmar PAID, o titular e o valor antes de entregar o pedido. 5. Quando estiver pronto, use uma credencial PRODUCTION e os caminhos /v3/pix/.... Não chame simulate-payment em produção. # Primeira cobrança (https://docs.quiblypay.com/docs/comecando/primeira-cobranca) Os exemplos criam uma cobrança no sandbox. Configure `QP_CLIENT_ID` com sua credencial `qp_test_` e `QP_CLIENT_SECRET` com o segredo correspondente. Configure `QP_WEBHOOK_URL` com a URL HTTPS pública do receptor. Em produção, use os dados reais do pagador exigidos pelo provedor. Para chamar outro endpoint, mantenha as credenciais e substitua o caminho e os campos da operação: consulta/simulação usam `transaction_id`; saque usa valor, `chave_pix`, os campos do destinatário e o header Idempotency-Key. Antes de enviar `urlnoty`, cadastre o receptor em Desenvolvedor > Webhooks, selecione os eventos e mantenha "Usar nas notificações por cobrança" habilitado. Guarde `QP_WEBHOOK_SECRET` para validar X-Quibly-Secret. Ao verificar HMAC, use a chave separada `QP_WEBHOOK_SIGNING_KEY`, revelada em Avançado: chave de assinatura. Um receptor em outra origem deve verificar HMAC porque não recebe o segredo no header. Consulte as [regras de entrega](/docs/webhooks/eventos). ### curl (form-urlencoded) ```sh curl --fail-with-body https://api.quiblypay.com/v3/sandbox/pix/qrcode \ --data-urlencode "client_id=$QP_CLIENT_ID" \ --data-urlencode "client_secret=$QP_CLIENT_SECRET" \ --data-urlencode 'valor=25.00' \ --data-urlencode 'nome=Cliente Sandbox' \ --data-urlencode 'descricao=Pedido de exemplo' \ --data-urlencode "urlnoty=$QP_WEBHOOK_URL" # Salve o transactionId retornado em QP_TRANSACTION_ID e simule: curl --fail-with-body https://api.quiblypay.com/v3/sandbox/simulate-payment \ --data-urlencode "client_id=$QP_CLIENT_ID" \ --data-urlencode "client_secret=$QP_CLIENT_SECRET" \ --data-urlencode "transaction_id=$QP_TRANSACTION_ID" # Confirme o status na consulta: curl --fail-with-body https://api.quiblypay.com/v3/sandbox/pix/transaction \ --data-urlencode "client_id=$QP_CLIENT_ID" \ --data-urlencode "client_secret=$QP_CLIENT_SECRET" \ --data-urlencode "transaction_id=$QP_TRANSACTION_ID" ``` ### Node.js (fetch, JSON, somente no servidor) ```js async function quibly(path, fields, headers = {}) { const response = await fetch(`https://api.quiblypay.com${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify({ ...fields, client_id: process.env.QP_CLIENT_ID, client_secret: process.env.QP_CLIENT_SECRET, }), }); const data = await response.json(); if (!response.ok) throw new Error(`${response.status}: ${data.message}`); return data; } const charge = await quibly('/v3/sandbox/pix/qrcode', { valor: '25.00', nome: 'Cliente Sandbox', descricao: 'Pedido de exemplo', urlnoty: process.env.QP_WEBHOOK_URL, }); // Salve charge.transactionId e exiba charge.qrcode. await quibly('/v3/sandbox/simulate-payment', { transaction_id: charge.transactionId }); const confirmed = await quibly('/v3/sandbox/pix/transaction', { transaction_id: charge.transactionId, }); console.log(confirmed.status); // Saque, se desejado: quibly('/v3/sandbox/pix/payment', // { valor: '25.00', chave_pix: 'CHAVE_PIX_DO_TITULAR', cpf: 'DOCUMENTO_DO_TITULAR' }, // { 'Idempotency-Key': 'ID_UNICO_DO_SEU_SAQUE' }); ``` ### Python (biblioteca padrão, form-urlencoded) ```python import json, os from urllib.request import Request, urlopen from urllib.parse import urlencode from urllib.error import HTTPError def quibly(path, fields, headers=None): body = urlencode({**fields, "client_id": os.environ["QP_CLIENT_ID"], "client_secret": os.environ["QP_CLIENT_SECRET"]}).encode() request = Request("https://api.quiblypay.com" + path, data=body, headers={"Content-Type": "application/x-www-form-urlencoded", **(headers or {})}, method="POST") try: with urlopen(request, timeout=30) as response: return json.load(response) except HTTPError as error: data = json.load(error) raise RuntimeError(f"{error.code}: {data.get('message')}") from error charge = quibly("/v3/sandbox/pix/qrcode", {"valor": "25.00", "nome": "Cliente Sandbox", "urlnoty": os.environ["QP_WEBHOOK_URL"]}) # Salve charge["transactionId"] e exiba charge["qrcode"]. quibly("/v3/sandbox/simulate-payment", {"transaction_id": charge["transactionId"]}) confirmed = quibly("/v3/sandbox/pix/transaction", {"transaction_id": charge["transactionId"]}) print(confirmed["status"]) ``` ### PHP (extensão cURL, form-urlencoded) ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_POSTFIELDS => http_build_query($fields), CURLOPT_HTTPHEADER => array_merge(['Content-Type: application/x-www-form-urlencoded'], $headers)]); $raw = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); if ($raw === false) { $error = curl_error($ch); curl_close($ch); throw new RuntimeException($error); } curl_close($ch); $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); if ($status < 200 || $status >= 300) throw new RuntimeException($status . ': ' . ($data['message'] ?? 'Erro da API')); return $data; } $charge = quibly('/v3/sandbox/pix/qrcode', ['valor' => '25.00', 'nome' => 'Cliente Sandbox', 'urlnoty' => getenv('QP_WEBHOOK_URL')]); // Salve $charge['transactionId'] e exiba $charge['qrcode']. quibly('/v3/sandbox/simulate-payment', ['transaction_id' => $charge['transactionId']]); $confirmed = quibly('/v3/sandbox/pix/transaction', ['transaction_id' => $charge['transactionId']]); echo $confirmed['status']; ``` # Autenticação (https://docs.quiblypay.com/docs/autenticacao) Cada POST inclui `client_id` e `client_secret` no corpo, sem token Bearer. O `client_id` começa com `qp_live_` para PRODUCTION ou `qp_test_` para SANDBOX. O `client_secret` é um segredo separado de 64 caracteres hexadecimais. Nunca o exponha no navegador, em variáveis de ambiente públicas, registros ou URLs de checkout. Os exemplos leem as credenciais das variáveis de ambiente do servidor `QP_CLIENT_ID` e `QP_CLIENT_SECRET`; nenhuma credencial de exemplo é utilizável. Escopos: cashin para criar QR e simular pagamentos, read para consultar transações e cashout para saques. Chave revogada, segredo incorreto ou ambiente incompatível retornam 401. Uma conta com API v3 desabilitada retorna 403 `FEATURE_DISABLED`. As permissões de entrada e saída de dinheiro da conta continuam valendo. A lista de IPs autorizados vale para toda a conta. Em saques de produção, uma lista vazia bloqueia todas as chamadas. Cadastre o IP de saída do servidor em Credenciais > IPs autorizados e habilite MFA na conta. Para saques no sandbox, cashin e read, uma lista vazia permite chamadas; uma lista preenchida permite apenas os IPs cadastrados. O X-Forwarded-For enviado pelo cliente não substitui o IP da conexão. Criar, regenerar ou revogar chaves e alterar a lista de IPs exige confirmação adicional por MFA. Corpos aceitos: application/json e application/x-www-form-urlencoded. Campos de texto multipart também são aceitos; arquivos são rejeitados. As respostas são JSON, inclusive nos erros. Envie valores decimais em reais, como "25.00", sem centavos inteiros nem separadores de milhar. O interpretador legado aceita vírgula decimal e prefixos numéricos; prefira valores decimais sem ambiguidades. Os valores devem arredondar para uma quantidade positiva de centavos e não podem exceder R$ 1.000.000.000. Os mínimos e as tarifas da conta continuam valendo. # API v3 (https://docs.quiblypay.com/docs/api-v3) Todas as respostas POST bem-sucedidas usam HTTP 200 e incluem statusCode: 200. Os sete caminhos abaixo aceitam as credenciais descritas em Autenticação (as [sessões de checkout](/docs/api-v3/checkout-sessions) têm página própria). GET, PUT, PATCH e DELETE retornam HTTP 405 nesses caminhos; use POST também nas consultas. | Endpoint | Escopo | Operação | | --------------------------------- | ------- | ------------------------------------------- | | POST /v3/pix/qrcode | cashin | Cobrança em produção | | POST /v3/pix/transaction | read | Consulta em produção | | POST /v3/pix/payment | cashout | Saque em produção | | POST /v3/sandbox/pix/qrcode | cashin | Cobrança simulada | | POST /v3/sandbox/pix/transaction | read | Consulta no sandbox | | POST /v3/sandbox/pix/payment | cashout | Saque simulado | | POST /v3/sandbox/simulate-payment | cashin | Confirmação de cobrança pendente no sandbox | # POST /v3/pix/qrcode (https://docs.quiblypay.com/docs/api-v3/pix-qrcode) `POST /v3/pix/qrcode` valor é obrigatório. nome (nome do pagador), cpf (documento do pagador), descricao e urlnoty são lidos como textos ou números convertidos em textos; campos omitidos viram textos vazios. No sandbox, o nome padrão do pagador é Cliente Sandbox. A validação do provedor pode exigir outros dados do pagador em produção. urlnoty é opcional; quando presente, deve usar HTTPS e um destino público. ```json { "statusCode": 200, "qrcode": "PIX_COPY_AND_PASTE_TEXT", "checkout_url": "", "transactionId": "TRANSACTION_ID", "amount": 25 } ``` qrcode contém o texto PIX copia e cola, não uma imagem base64. `checkout_url` está vazio atualmente; não redirecione o cliente para ele. Para mandar o comprador a uma página de pagamento hospedada, crie uma [sessão de checkout](/docs/api-v3/checkout-sessions). O sandbox acrescenta mode: "sandbox" e gera um QR simulado que não pode ser pago em banco. Tarifas maiores que a cobrança ou valores abaixo do mínimo configurado são rejeitados. A criação de QR não trata Idempotency-Key: repetir a chamada pode criar outra cobrança. Salve o resultado e investigue chamadas sem resposta antes de criar possíveis duplicatas. Enviar `urlnoty` não configura sozinho a entrega do webhook. Cadastre um webhook ativo e habilite "Usar nas notificações por cobrança". Sem ele, apenas credenciais que ainda tenham uma chave HMAC legada podem enviar notificações por cobrança. O header X-Quibly-Secret só é enviado para a mesma origem do endpoint marcado; outras origens devem verificar X-Quibly-Signature. [Configure as entregas e as chaves](/docs/webhooks/eventos) antes de usar `urlnoty`. # POST /v3/pix/transaction (https://docs.quiblypay.com/docs/api-v3/pix-transaction) `POST /v3/pix/transaction` `transaction_id` é obrigatório. Observe o sublinhado na chamada e transactionId em camelCase nas respostas. A consulta é restrita à conta e ao ambiente da credencial. ```json { "statusCode": 200, "transactionId": "TRANSACTION_ID", "status": "PENDING", "amount": 25, "created_at": "2026-10-04 12:00:00" } ``` Para PAID, a resposta também contém `net_amount`, tax, descricao, nome, document, `confirmed_date` (que pode ser nulo) e type (DEPOSIT ou WITHDRAW). amount é o valor bruto em reais, tax é a tarifa e `net_amount` é o valor menos a tarifa. As datas usam YYYY-MM-DD HH:mm:ss no fuso `America/Sao_Paulo`. PROCESSING é apresentado como PENDING; FAILED, como CANCELLED; os demais status permanecem iguais. A consulta no sandbox acrescenta mode: "sandbox". Somente PAID confirma o pagamento. # POST /v3/pix/payment (https://docs.quiblypay.com/docs/api-v3/pix-payment) `POST /v3/pix/payment` Obrigatórios: valor e `chave_pix` não vazia. Aceitos: nome (nome do destinatário), cpf (documento do destinatário), descricao e urlnoty. Em produção, é necessário ter permissão cashout, MFA na conta, IP autorizado, saldo suficiente incluindo tarifas, destino de titularidade da conta e atender aos requisitos de KYC e limites aplicáveis. O saque é destinado ao titular da conta; não permite transferências genéricas a terceiros. Envie um header Idempotency-Key único, de 1 a 128 caracteres, para cada saque desejado; reutilize a chave com o mesmo corpo ao repetir a chamada. Alterar o corpo mantendo a chave pode gerar conflito de idempotência. Sem esse header, o mesmo valor e a mesma chave PIX dentro de 120 segundos reutilizam o saque. Dois saques idênticos intencionais precisam de chaves distintas. ```json { "statusCode": 200, "message": "Saque PIX processado com sucesso", "transactionId": "TRANSACTION_ID" } ``` Um saque aceito e retido para análise retorna message "Saque PIX recebido e em análise". Um alerta de fraude/MED aberto, outro saque em processamento ou saques automáticos desabilitados podem causar análise. HTTP 200 não garante liquidação final; consulte a transação. O sandbox retorna message "Saque PIX simulado com sucesso (Sandbox)." e mode: "sandbox"; nunca transfere dinheiro real. Acima dos limites de produção por transação ou por dia, a chamada é rejeitada antes do débito, sem retenção para análise. # Endpoints sandbox (https://docs.quiblypay.com/docs/api-v3/sandbox) Use credenciais SANDBOX. Os corpos seguem as páginas de cada operação, com estes caminhos: * POST /v3/sandbox/pix/qrcode (cashin) * POST /v3/sandbox/pix/transaction (read) * POST /v3/sandbox/pix/payment (cashout) O QR é simulado e não pode ser pago em banco. As respostas incluem `mode: "sandbox"`. Saques não transferem dinheiro real. [Simule o pagamento](/docs/api-v3/simulate-payment) para confirmar uma cobrança pendente. # POST /v3/sandbox/simulate-payment (https://docs.quiblypay.com/docs/api-v3/simulate-payment) `POST /v3/sandbox/simulate-payment` `transaction_id` é obrigatório. A cobrança deve pertencer à conta, estar em SANDBOX, pendente e dentro do prazo de validade. Cobranças inexistentes, já pagas ou expiradas retornam 404. ```json { "statusCode": 200, "message": "Simulação concluída com sucesso. Webhook adicionado na queue (se houver url).", "netAmountAdded": 24, "transactionId": "TRANSACTION_ID" } ``` netAmountAdded depende da tarifa configurada. O resultado não inclui mode. Os webhooks exigem uma URL elegível e uma chave de assinatura. # Sessões de checkout (https://docs.quiblypay.com/docs/api-v3/checkout-sessions) Uma sessão de checkout é uma página de pagamento hospedada para um pedido, no modelo do Stripe Checkout. O seu servidor cria a sessão e redireciona o comprador para `url`. O comprador paga com PIX ou cartão em pay.quiblypay.com e volta para o seu `success_url`. O redirecionamento NÃO prova o pagamento: entregue o pedido só depois do webhook `transaction.paid` (ele traz `sessionId`) ou de uma consulta que devolva `status: "paid"`. | Endpoint | Escopo | Operação | | ------------------------------------------- | ------ | ------------------------------------- | | POST /v3/checkout/sessions | cashin | Cria uma sessão em produção | | POST /v3/checkout/sessions/retrieve | read | Consulta uma sessão em produção | | POST /v3/checkout/sessions/expire | cashin | Cancela uma sessão aberta em produção | | POST /v3/sandbox/checkout/sessions | cashin | Cria uma sessão no sandbox | | POST /v3/sandbox/checkout/sessions/retrieve | read | Consulta uma sessão no sandbox | | POST /v3/sandbox/checkout/sessions/expire | cashin | Cancela uma sessão aberta no sandbox | Estas rotas aceitam só `application/json` (o corpo tem objetos aninhados). `client_id` e `client_secret` vão no mesmo corpo, como nas outras rotas v3, e valem as mesmas regras de whitelist de IP e de ambiente. A criação aceita até 60 chamadas por minuto por IP e o header `Idempotency-Key`: a mesma chave com o mesmo corpo devolve a mesma sessão. ## Corpo da criação | Campo | Obrigatório | Regras | | ------------------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | amount | sim | Reais com até 2 casas, em texto (`"150.00"`) ou número; maior que zero, pelo menos o mínimo da conta e no máximo 1.000.000.000 | | title | sim | 1 a 120 caracteres, mostrado ao comprador | | description | não | Até 500 caracteres | | external_reference | não | O ID do seu pedido, até 100 caracteres. Volta na consulta e no webhook | | customer | não | `{name, email, cpf, phone}`: preenche o checkout. A página mostra os dados mascarados (Maria S., ma\*\*\*@..., \*\*\*.\*\*\*.\*47-25); os campos que o comprador não altera valem como você enviou | | methods | não | `["pix"]`, `["card"]` ou os dois (padrão: os dois). Só cartão exige o cartão habilitado na conta | | success_url | sim | Para onde o comprador volta depois de pagar (veja URLs de retorno) | | cancel_url | não | Para onde levam "Voltar para a loja" e uma sessão vencida ou cancelada. Sem ele, vale o `success_url` | | expires_in | não | Segundos até a sessão vencer: 300 a 86400, padrão 3600 | | metadata | não | Até 20 chaves de texto (chave até 40 e valor até 500 caracteres). Volta na consulta e no webhook | | urlnoty | não | URL de webhook das cobranças desta sessão, com as mesmas regras do `urlnoty` de [/v3/pix/qrcode](/docs/api-v3/pix-qrcode) | ```json { "statusCode": 200, "id": "cs_live_A1b2C3d4E5f6G7h8I9j0K1l2", "url": "https://pay.quiblypay.com/s/cs_live_A1b2C3d4E5f6G7h8I9j0K1l2", "status": "open", "amount": "150.00", "currency": "BRL", "title": "Pedido #981", "description": null, "external_reference": "981", "metadata": { "pedido": "981" }, "methods": ["pix", "card"], "success_url": "https://loja.example.com/obrigado?pedido=981", "cancel_url": "https://loja.example.com/carrinho", "expires_at": "2026-10-05T15:00:00.000Z", "created_at": "2026-10-05T14:00:00.000Z", "paid_at": null, "canceled_at": null, "transaction_id": null, "payment_method": null } ``` Aqui `amount` é um texto decimal (as rotas PIX legadas devolvem número). No sandbox, os ids começam com `cs_test_` e a resposta acrescenta `mode: "sandbox"`. O id da sessão na `url` é a única chave da página: trate a `url` como um link de pagamento de uso único, não como segredo, e nunca coloque o `client_secret` nela. ## Consulta e cancelamento As duas rotas recebem `{"id": "cs_..."}` e devolvem o mesmo objeto. `status` é `open`, `paid`, `expired` ou `canceled`. Depois do pagamento, `transaction_id` e `payment_method` (`pix` ou `card`) identificam a venda; [POST /v3/pix/transaction](/docs/api-v3/pix-transaction) com esse `transaction_id` devolve o valor e a tarifa. Uma sessão de outra conta ou do outro ambiente devolve 404. `expire` cancela uma sessão aberta (`status: "canceled"`). Chamar de novo não muda nada, e uma sessão paga devolve 409. Um PIX gerado antes de a sessão vencer ou ser cancelada ainda pode ser pago no banco até o próprio PIX vencer (no máximo uma hora e nunca mais de 5 minutos depois da sessão). Se ele for pago, o dinheiro é creditado, a sessão vira `paid` e o `transaction.paid` é enviado. Trate um pagamento que chega depois de você desistir do pedido: estorne ou entregue. ## URLs de retorno Depois do pagamento, a página mostra "Pagamento confirmado" e, cerca de 3 segundos depois, redireciona para o `success_url` com `session_id` e `status` acrescentados à query. Os parâmetros e o fragmento que já existiam são mantidos: `https://loja.example.com/obrigado?pedido=981&session_id=cs_live_...&status=paid`. Uma sessão vencida ou cancelada leva o comprador ao `cancel_url` (ou ao `success_url`) com `status=expired` ou `status=canceled`. O `success_url` e o `cancel_url` precisam usar https, não ter usuário nem senha, não apontar para IP privado nem host interno (localhost, .local, .internal) e ter até 2048 caracteres. Uma sessão de sandbox aceita também `http://localhost`, `http://127.0.0.1` e `http://[::1]` para testes locais. **Domínios de retorno por credencial (opcional).** Em Desenvolvedor > Credenciais de API, cada credencial tem "Domínios de retorno" (até 10; salvar exige a verificação em duas etapas). Com a lista vazia, vale qualquer URL https pública, ou seja, quem tem uma credencial válida consegue fazer o checkout redirecionar para qualquer site. Com a lista preenchida, o `success_url` e o `cancel_url` precisam bater com ela, comparando o host: * compara o host da URL interpretada, nunca o texto (`https://loja.example.com@evil.com` tem host evil.com e é recusada antes da lista); * `*.loja.example.com` aceita qualquer subdomínio, em qualquer nível (`pay.loja.example.com`, `a.b.loja.example.com`), mas NÃO aceita `loja.example.com`: cadastre o domínio principal separado se precisar dele; * a porta não entra na comparação (`https://loja.example.com:8443/...` bate com `loja.example.com`); * nomes com acento são gravados e comparados em punycode; * o curinga sobre sufixo público é recusado ao salvar (`*.com.br`, `*.adv.br`, `*.co.jp` e hospedagem compartilhada como `*.vercel.app` ou `*.github.io`): cadastre o host exato (`loja.vercel.app`); * a lista é da credencial e vale no ambiente dela: uma credencial SANDBOX com lista também restringe as sessões de sandbox, e só os hosts locais acima escapam. Uma URL fora da lista devolve HTTP 400 `{"statusCode":400,"error":"RETURN_URL_NOT_ALLOWED","message":"...","details":{"field":"success_url","host":"evil.com"}}`. A lista cadastrada nunca volta no erro. ## Webhook e entrega do pedido A venda gera um `transaction.paid` normal (mesmos destinos, segredo e assinatura descritos em [Webhooks](/docs/webhooks/eventos)) com três campos a mais: `sessionId`, `externalReference` e `metadata`. A cobrança herda o `urlnoty` e a credencial da sessão, então valem as regras de entrega por cobrança. Uma sessão é paga uma vez. Numa corrida rara (o comprador paga um PIX e um cartão da mesma sessão ao mesmo tempo), os dois pagamentos são creditados e chegam dois `transaction.paid` com o mesmo `sessionId` e `transactionId` diferentes. Entregue o pedido uma vez por `sessionId` ou `external_reference` e trate o segundo pagamento como caso de estorno (a nossa equipe é avisada). ## Erros da sessão | HTTP | Erro | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `VALIDATION_ERROR` com o campo na mensagem (amount, title, URLs, expires_in, metadata, methods, urlnoty), `RETURN_URL_NOT_ALLOWED`, valor abaixo do mínimo ou acima do máximo | | 401 / 403 | As mesmas regras de credencial, escopo, IP e `FEATURE_DISABLED` das outras rotas v3 | | 404 | Sessão inexistente nesta conta e neste ambiente | | 409 | `expire` numa sessão paga, ou `IDEMPOTENCY_CONFLICT` (a mesma Idempotency-Key com outro corpo) | ## Sandbox Crie com uma credencial `qp_test_` em `/v3/sandbox/checkout/sessions` e abra a `url`: a página mostra "Modo de teste". Pague com os cartões de teste do [checkout hospedado](/docs/checkout). O PIX do sandbox não pode ser pago em banco: depois de gerá-lo na página, abra a transação de teste no painel (Transações) e use Marcar como pago. A consulta só devolve `transaction_id` depois do pagamento. # Eventos (https://docs.quiblypay.com/docs/webhooks/eventos) A fila de saída de pagamentos emite transaction.paid para transações DEPOSIT e WITHDRAW pagas. Esta referência não garante outros eventos de pagamento enviados. Destinos das entregas: * Webhooks cadastrados em Desenvolvedor > Webhooks recebem os eventos selecionados para transações PRODUCTION. * Destinos por cobrança, em SANDBOX e PRODUCTION: o urlnoty enviado ao criar a cobrança e a URL de webhook da chave de API. Eles usam o webhook marcado "Usar nas notificações por cobrança", que deve estar ativo e é único por conta. Sem webhook marcado, ou enquanto ele estiver pausado, uma credencial criada antes de 2026-10-05 que ainda tenha o segredo legado `whsec_` continua assinando com ele, apenas por assinatura; caso contrário, nenhuma entrega é criada. Marcar um webhook faz essas entregas usarem o segredo e a chave de assinatura dele; atualize o receptor antes. Sem chave, nada é enviado. * Se um webhook cadastrado e um destino por cobrança tiverem a mesma URL normalizada, ocorre uma única entrega, como webhook cadastrado. Dois valores diferentes protegem a entrega: * Segredo do webhook: definido por você, com 16 a 128 caracteres ASCII imprimíveis e sem espaços, ou gerado uma única vez. Ele é enviado no header X-Quibly-Secret. Compare-o com o segredo armazenado em tempo constante. Só é enviado para a URL do webhook cadastrado e para URLs por cobrança com exatamente a mesma origem (esquema, host e porta) do webhook marcado. Outras origens, como uma URL de automação de terceiros, não o recebem e devem verificar a assinatura. Entregas legadas com `whsec_` nunca incluem esse header. * Chave de assinatura: uma chave própria de cada webhook (`whsig_...`), usada apenas na assinatura HMAC opcional. Revele-a em Desenvolvedor > Webhooks > Avançado: chave de assinatura, com confirmação por dois fatores, e faça a rotação nesse mesmo local. Webhooks criados antes de 2026-10-05 assinam com o próprio segredo até a rotação da chave de assinatura; faça a rotação para separar os dois valores. Para entregas legadas com `whsec_`, a chave de assinatura é o valor `whsec_` completo. Alterar o segredo ou rotacionar a chave de assinatura também afeta as retentativas já enfileiradas. Nenhum desses valores é `client_secret`. Após verificar a entrega, elimine duplicatas de X-Quibly-Event-Id de forma atômica em armazenamento durável. Garanta também uma única entrega do produto por transactionId, pois destinos diferentes podem ter IDs de entrega distintos para a mesma transação. Consulte /transaction com suas credenciais, verifique PAID e o valor/pedido esperado, salve ou enfileire o trabalho e retorne 2xx prontamente. Se a consulta falhar, permita uma nova tentativa; nunca marque o evento como processado antes de salvar seu trabalho com segurança. Eventos duplicados já processados com sucesso devem retornar 2xx. Qualquer 2xx indica sucesso. A entrega tem um prazo total de 15 segundos. Há até cinco tentativas automáticas: imediata, depois com intervalos de 60, 300, 900 e 3600 segundos após as falhas. Respostas fora de 2xx ou erros de rede geram retentativas até esgotar o limite. O reenvio manual preserva o ID do evento. Destinos excluídos, desabilitados ou alterados podem cancelar entregas pendentes; chaves de API revogadas invalidam os destinos vinculados a elas. # Payload (https://docs.quiblypay.com/docs/webhooks/payload) ```json { "event": "transaction.paid", "transactionId": "TRANSACTION_ID", "externalRef": "PROVIDER_REFERENCE", "amount": 25, "netAmount": 24, "tax": 1, "status": "PAID", "type": "DEPOSIT", "confirmed_date": "2026-10-04 12:00:00" } ``` externalRef vem da referência do provedor e pode ser nulo. Uma venda feita numa sessão de checkout traz também sessionId (cs_live\_... ou cs_test\_...), externalReference (o seu external_reference, diferente de externalRef) e metadata (o metadata da sessão); veja [Sessões de checkout](/docs/api-v3/checkout-sessions). DEPOSIT inclui netAmount e tax; WITHDRAW omite ambos. A tarifa do exemplo é ilustrativa, não uma promessa de preço. confirmed_date é uma data legada no fuso America/Sao_Paulo. O payload da fila de saída v3 não tem campo de ambiente; diferencie o sandbox pela credencial/receptor e confirme pelo caminho de consulta correspondente. Headers, cujos nomes não diferenciam maiúsculas de minúsculas: * Content-Type: application/json * User-Agent: QuiblyPay-Webhook/1.0 * X-Quibly-Secret: segredo do webhook, somente para o destino que o possui; veja as [regras de entrega](/docs/webhooks/eventos) * X-Quibly-Signature: `sha256=<64 caracteres hexadecimais minúsculos>` * X-Quibly-Timestamp: horário Unix em segundos * X-Quibly-Delivery: ID da entrega * X-Quibly-Event-Id: `evt_`, estável entre retentativas # Verificação HMAC (https://docs.quiblypay.com/docs/webhooks/verificacao) Validação simples: rejeite a chamada se X-Quibly-Secret não for igual ao segredo do webhook, usando comparação em tempo constante. Isso basta para a maioria das integrações que recebem esse header. Em outra origem, onde ele não é enviado, valide a assinatura HMAC. Assinatura opcional: HMAC-SHA256 hexadecimal usando toda a chave de assinatura literal (`whsig_...` ou o valor legado `whsec_`...) como chave UTF-8, sobre timestamp + "." + os bytes exatos do corpo JSON cru. Não remova o prefixo, não decodifique a chave de base64 nem serialize o JSON novamente. Capture o corpo cru antes de qualquer interpretador JSON. Compare as assinaturas em tempo constante. Os exemplos abaixo recomendam uma janela de validade de 300 segundos contra reenvios maliciosos; essa janela é uma política do receptor, não uma configuração da API. Cada retentativa é assinada com seu horário atual. ### Comparação simples do segredo (Node.js) ```js import { timingSafeEqual } from 'node:crypto'; export function hasQuiblySecret(headerValue, secret) { const a = Buffer.from(headerValue ?? ''); const b = Buffer.from(secret); return a.length === b.length && timingSafeEqual(a, b); } // secret = process.env.QP_WEBHOOK_SECRET; headerValue = o header x-quibly-secret da chamada. // Rejeite com 401 antes de processar o evento se o resultado for falso. ``` ### Verificação no receptor Node.js ```js import { createHmac, timingSafeEqual } from 'node:crypto'; export function verifyQuibly(rawBody, timestamp, signature, secret) { // rawBody deve ser o Buffer original, sem usar JSON.stringify(parsedBody). if (!/^\d+$/.test(timestamp ?? '') || !/^sha256=[a-f0-9]{64}$/.test(signature ?? '')) return false; const seconds = Number(timestamp); if (!Number.isSafeInteger(seconds) || Math.abs(Date.now() / 1000 - seconds) > 300) return false; const expected = createHmac('sha256', secret) .update(timestamp + '.') .update(rawBody) .digest(); const received = Buffer.from(signature.slice(7), 'hex'); return received.length === expected.length && timingSafeEqual(expected, received); } // secret = a chave de assinatura em process.env.QP_WEBHOOK_SIGNING_KEY. // Extraia x-quibly-timestamp e x-quibly-signature da chamada recebida. // Rejeite com 401 antes de interpretar ou processar o JSON se verifyQuibly retornar falso. ``` ### Verificação no receptor Python ```python import hashlib, hmac, re, time def verify_quibly(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool: if not re.fullmatch(r"[0-9]+", timestamp or "") or not re.fullmatch(r"sha256=[a-f0-9]{64}", signature or ""): return False if abs(time.time() - int(timestamp)) > 300: return False expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature[7:]) # Use os bytes crus da chamada no seu framework antes de interpretar o JSON. # Chave de assinatura: os.environ["QP_WEBHOOK_SIGNING_KEY"]. Rejeite chamadas inválidas com HTTP 401. ``` ### Verificação no receptor PHP ```php 300) return false; $expected = hash_hmac('sha256', $timestamp . '.' . $raw, $secret); return hash_equals($expected, substr($signature, 7)); } $raw = file_get_contents('php://input'); if (!verifyQuibly($raw, $_SERVER['HTTP_X_QUIBLY_TIMESTAMP'] ?? '', $_SERVER['HTTP_X_QUIBLY_SIGNATURE'] ?? '', getenv('QP_WEBHOOK_SIGNING_KEY'))) { http_response_code(401); exit; } $event = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); // Elimine duplicatas em armazenamento durável, confirme em /transaction e enfileire a entrega antes de retornar 204. ``` ### Verificação com curl (shell e Python) curl é um cliente HTTP, não um servidor de webhook. Para reenviar uma entrega capturada ao receptor, preserve o corpo e os headers exatos. O verificador abaixo usa HMAC da biblioteca padrão do Python e comparação em tempo constante. Configure `QP_WEBHOOK_SIGNING_KEY` com segurança; nunca escreva uma chave real no código. `QP_TIMESTAMP` e `QP_SIGNATURE` devem conter os headers capturados. body.json deve conter os bytes exatos recebidos, sem acrescentar uma quebra de linha. ```sh # Verifique localmente antes de reenviar. Entregas com mais de 300 segundos são rejeitadas. # set -e interrompe o script se a verificação falhar, impedindo o reenvio de uma entrega inválida. set -e python3 - <<'PYCODE' import hashlib, hmac, os, pathlib, re, time raw = pathlib.Path('body.json').read_bytes() ts = os.environ['QP_TIMESTAMP'] sig = os.environ['QP_SIGNATURE'] assert re.fullmatch(r'[0-9]+', ts) and re.fullmatch(r'sha256=[a-f0-9]{64}', sig) assert abs(time.time() - int(ts)) <= 300 expected = hmac.new(os.environ['QP_WEBHOOK_SIGNING_KEY'].encode(), ts.encode() + b'.' + raw, hashlib.sha256).hexdigest() assert hmac.compare_digest(expected, sig[7:]), 'Assinatura inválida' PYCODE # Reenvie somente após verificar com sucesso. Preserve os IDs de evento/entrega capturados. curl --fail-with-body "$QP_RECEIVER_URL" \ -H 'Content-Type: application/json' \ -H "X-Quibly-Timestamp: $QP_TIMESTAMP" \ -H "X-Quibly-Signature: $QP_SIGNATURE" \ -H "X-Quibly-Event-Id: $QP_EVENT_ID" \ -H "X-Quibly-Delivery: $QP_DELIVERY_ID" \ --data-binary @body.json ``` # Erros (https://docs.quiblypay.com/docs/erros) O status HTTP e o campo JSON statusCode coincidem. O formato básico é `{"statusCode":400,"message":"Valor inválido."}`. Alguns erros também incluem error; `FEATURE_DISABLED` inclui feature: "apiV3". As mensagens estão em português; prefira o status HTTP e os códigos de erro estáveis, quando disponíveis. | HTTP | Exemplos | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 400 | Credenciais ausentes (`API_KEY_MISSING`), valor inválido, `chave_pix`/`transaction_id` ausente, urlnoty inválida, valor abaixo do mínimo, saldo insuficiente incluindo tarifas, limite diário, `WITHDRAW_LIMIT_PER_TX`, `WITHDRAW_LIMIT_DAILY`, Idempotency-Key inválida | | 401 | Credenciais incorretas, chave revogada, ambiente incompatível ou conta bloqueada | | 403 | `API_KEY_SCOPE`, `API_KEY_IP`, `CASHOUT_IP_NOT_ALLOWED`, `FEATURE_DISABLED`, entrada/saída de dinheiro desabilitada, MFA ausente, documento do destino incompatível, requisitos de KYC | | 404 | Transação inexistente ou de outra conta/ambiente; simulação já confirmada ou expirada | | 405 | Método HTTP incorreto | | 409 | Conflito de idempotência ou chamada ainda em processamento | | 500 | Erro interno genérico, provedor de cobrança indisponível ou falha no processamento do saque | As sessões de checkout acrescentam `RETURN_URL_NOT_ALLOWED` e `VALIDATION_ERROR` (400) e o 409 no `expire` de sessão paga; veja [Sessões de checkout](/docs/api-v3/checkout-sessions). Uma chamada sem resposta ou um erro 500 não provam que a operação deixou de ocorrer. Reutilize a chave de idempotência do saque ao repetir a chamada e reconcilie os IDs de transação salvos. Não entregue um pedido com base em webhook não verificado ou no redirecionamento do cliente. # Checkout hospedado e links de pagamento (https://docs.quiblypay.com/docs/checkout) Para pagamentos por cartão, crie um checkout de produto ou link de pagamento no painel e compartilhe a URL copiada, normalmente `https://pay.quiblypay.com/`. Os links legados `/p/` e `/l/` redirecionam para a URL curta. O endereço base do checkout não identifica um pedido específico. Os produtos também oferecem uma URL separada de sandbox. Nunca inclua segredos de API em URLs de checkout. A API v3 não tem endpoint que receba dados de cartão. Para cobrar um cartão a partir do seu site, crie uma [sessão de checkout](/docs/api-v3/checkout-sessions): o comprador digita o cartão no checkout hospedado, conforme os métodos habilitados pelo produtor, e volta para o seu success_url. O PIX do sandbox não pode ser pago em banco; use a simulação ou a ação Marcar como pago no painel antes do vencimento. Para testar cartão exclusivamente no checkout sandbox, use os números de teste abaixo: | Número | Resultado | | ------------------- | ------------------------------------------------------- | | 4242 4242 4242 4242 | Aprovado | | 5555 5555 5555 4444 | Aprovado | | 4000 0000 0000 0002 | Recusado | | 4000 0000 0000 9995 | Saldo insuficiente | | 4000 0000 0000 0069 | Cartão vencido | | 4000 0000 0000 0127 | CVV inválido | | 4000 0000 0000 3220 | Pendente, depois aprovado em cerca de um minuto | | 4000 0000 0000 0259 | Aprovado com fluxo simulado de contestação (chargeback) | Use uma validade futura e qualquer CVV de três dígitos. A produção rejeita esses cartões de teste. Nunca informe um cartão real no sandbox. Crie as credenciais em Desenvolvedor > Credenciais de API, teste com `qp_test_`, valide o webhook e confirme a transação antes de entregar o produto. Para vender sem código ou receber por cartão, copie o link do checkout no painel. # Boas práticas e segurança (https://docs.quiblypay.com/docs/seguranca) * Mantenha credenciais e segredo de webhook somente no servidor. * Use HTTPS público nos destinos de webhook. * Compare X-Quibly-Secret em tempo constante quando o destino receber esse header. Para outras origens, valide X-Quibly-Signature com a chave de assinatura do endpoint sobre bytes crus e limite a idade da assinatura. Deduplique eventos em armazenamento durável. * Confirme PAID, titularidade e valor via consulta antes de entregar o pedido. * A criação de QR não trata Idempotency-Key: investigue chamadas sem resposta antes de repeti-las. * Reutilize a mesma Idempotency-Key ao repetir um saque; chaves diferentes representam intenções diferentes. * Habilite MFA e autorize os IPs de saída antes de usar cashout em produção. * Separe ambientes e nunca teste cartão real no sandbox. [Detalhes de autenticação](/docs/autenticacao), [saques](/docs/api-v3/pix-payment) e [webhooks](/docs/webhooks/verificacao).