Em Go, a API Pix se integra só com a biblioteca padrão: net/http cria a cobrança, a resposta traz o QR Code e o copia e cola, e um handler recebe o webhook que confirma o pagamento, validado com crypto/hmac. O código abaixo segue a documentação da API, usa Go 1.22 ou superior e a base https://app.xflowpayments.com/api/v1. Para a visão geral, comece pelo guia da API Pix.
O que você precisa
- Uma chave gerada no painel, em Desenvolvedores > Chaves de API. A chave pública (
pk_live_...) fica sempre visível. O segredo (sk_live_...) aparece uma única vez. - Para testar, uma chave do ambiente Teste (
pk_test_...esk_test_...). Ela pode ser criada antes da aprovação do cadastro. - Go 1.22 ou superior, por causa dos padrões com método no
http.ServeMux("POST /webhooks/xflow"). - As variáveis de ambiente
XFLOW_PUBLIC_KEY,XFLOW_SECRET_KEYeXFLOW_WEBHOOK_SECRET.
O segredo fica só no servidor. Não o grave no repositório nem em binário distribuído para clientes.
Um cliente com net/http
A autenticação é HTTP Basic, e req.SetBasicAuth monta o header. Valores monetários são inteiros em centavos. Um detalhe que pega muita gente: o http.DefaultClient não tem timeout, então crie o seu.
// xflow/xflow.go
package xflow
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const base = "https://app.xflowpayments.com/api/v1"
var httpClient = &http.Client{Timeout: 30 * time.Second}
type Erro struct {
Status int `json:"-"`
Code string `json:"error"`
Message string `json:"message"`
RequestID string `json:"request_id"`
RetryAfter time.Duration `json:"-"`
}
func (e *Erro) Error() string {
return fmt.Sprintf("%d %s: %s (%s)", e.Status, e.Code, e.Message, e.RequestID)
}
func Chamar(metodo, caminho string, corpo any, idempotencyKey string, saida any) error {
var leitor io.Reader
if corpo != nil {
b, err := json.Marshal(corpo)
if err != nil {
return err
}
leitor = bytes.NewReader(b)
}
req, err := http.NewRequest(metodo, base+caminho, leitor)
if err != nil {
return err
}
req.SetBasicAuth(os.Getenv("XFLOW_PUBLIC_KEY"), os.Getenv("XFLOW_SECRET_KEY"))
if corpo != nil {
req.Header.Set("Content-Type", "application/json")
}
if idempotencyKey != "" {
req.Header.Set("Idempotency-Key", idempotencyKey)
}
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
dados, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode >= 400 {
e := &Erro{Status: resp.StatusCode}
_ = json.Unmarshal(dados, e)
if s, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
e.RetryAfter = time.Duration(s) * time.Second
}
return e
}
if saida == nil || len(dados) == 0 {
return nil
}
return json.Unmarshal(dados, saida)
}Toda resposta traz um request_id (req_...), no corpo e no header x-request-id, e o Error() já o inclui. Grave no log: com ele você acha a chamada em Integrações > Logs da API, que guarda cada requisição autenticada por 30 dias.
Criar a cobrança Pix
Declare os tipos no mesmo pacote, com as tags do JSON da API:
// xflow/tipos.go (package xflow)
type Customer struct {
Name string `json:"name"`
Email string `json:"email"`
Document string `json:"document"` // CPF ou CNPJ, só dígitos
}
type NovaCobranca struct {
AmountCents int64 `json:"amountCents"`
Customer Customer `json:"customer"`
Description string `json:"description,omitempty"`
ExternalReference string `json:"external_reference,omitempty"`
}
type Cobranca struct {
ID string `json:"id"`
Livemode bool `json:"livemode"`
Status string `json:"status"`
AmountCents int64 `json:"amountCents"`
ExternalReference string `json:"external_reference"`
Pix struct {
CopyPaste string `json:"copyPaste"`
QRCodeBase64 string `json:"qrCodeBase64"`
EndToEnd string `json:"endToEnd"`
} `json:"pix"`
}E chame:
pedidoID := "1234"
var cobranca xflow.Cobranca
err := xflow.Chamar("POST", "/charges", xflow.NovaCobranca{
AmountCents: 1990, // R$ 19,90
Customer: xflow.Customer{Name: "Cliente Exemplo", Email: "cliente@exemplo.com", Document: "00000000000"},
Description: "Pedido #" + pedidoID,
ExternalReference: "pedido-" + pedidoID,
}, "pedido-"+pedidoID, &cobranca)
if err != nil {
log.Fatal(err)
}
fmt.Println(cobranca.ID, cobranca.Status) // clx... pendingO quarto argumento é a Idempotency-Key, única por pedido. Se a conexão cair e você repetir com a mesma chave, a API devolve a mesma cobrança em vez de gerar outro Pix. Com a mesma chave e outro corpo, a resposta é 409 idempotency_conflict. O porquê está em idempotência em API de pagamento. O external_reference (até 120 caracteres) liga a cobrança ao seu pedido e volta em toda resposta e webhook. Valor fora do mínimo ou do máximo da conta retorna 400 validation.
Mostrar o QR Code e o copia e cola
pix.qrCodeBase64 é um PNG em data-URL. Com html/template, há uma pegadinha: o pacote troca URLs data: no src por #ZgotmplZ. Marque o valor como confiável com template.URL, já que ele vem da própria API:
var pagina = template.Must(template.New("pix").Parse(`
<img src="{{.QRCode}}" alt="QR Code Pix" width="240" height="240">
<textarea readonly>{{.CopiaECola}}</textarea>`))
pagina.Execute(w, map[string]any{
"QRCode": template.URL(cobranca.Pix.QRCodeBase64),
"CopiaECola": cobranca.Pix.CopyPaste,
})Se preferir servir a imagem numa rota própria, decodifique a parte depois da vírgula com base64.StdEncoding.DecodeString e responda com Content-Type: image/png. pix.copyPaste é o código BR Code que o pagador cola no app do banco: repasse sem alterar. Os dois campos vêm na criação e em GET /api/v1/charges/{id}, mas não na listagem.
Receber o webhook
Cadastre a URL em Integrações > Webhooks. Ela precisa ser https:// e pública. Cada entrega é um POST JSON com os headers x-webhook-event e x-webhook-signature: sha256=..., o HMAC-SHA256 do corpo cru com o segredo do endpoint.
var segredo = []byte(os.Getenv("XFLOW_WEBHOOK_SECRET"))
func assinaturaValida(corpo []byte, header string) bool {
mac := hmac.New(sha256.New, segredo)
mac.Write(corpo)
esperado := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(esperado), []byte(header))
}
type Evento struct {
Event string `json:"event"`
Data struct {
Livemode bool `json:"livemode"`
ExternalReference string `json:"external_reference"`
Pix struct {
EndToEnd string `json:"endToEnd"`
} `json:"pix"`
} `json:"data"`
}
func webhook(w http.ResponseWriter, r *http.Request) {
corpo, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
if err != nil {
http.Error(w, "corpo inválido", http.StatusBadRequest)
return
}
if !assinaturaValida(corpo, r.Header.Get("X-Webhook-Signature")) {
w.WriteHeader(http.StatusUnauthorized)
return
}
var ev Evento
if err := json.Unmarshal(corpo, &ev); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if ev.Event == "transaction.paid" && ev.Data.Livemode {
if err := darBaixaNoPedido(r.Context(), ev.Data.ExternalReference, ev.Data.Pix.EndToEnd); err != nil {
w.WriteHeader(http.StatusInternalServerError) // sem 2xx, a entrega é reenviada
return
}
}
w.WriteHeader(http.StatusOK)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /webhooks/xflow", webhook)
log.Fatal(http.ListenAndServe(":8080", mux))
}darBaixaNoPedido é a sua função de negócio. O que importa no handler:
- Leia os bytes antes do
json.Unmarshal. Decodificar direto dor.Bodyconsome o corpo, e o HMAC precisa dos bytes exatos. No Gin, pegue o corpo comc.GetRawData()antes de qualquer bind. No Echo, useio.ReadAll(c.Request().Body). hmac.Equalcompara sem vazar tempo. Não compare assinatura com==.- Responda 2xx para confirmar. Sem isso, a xFlow reenvia até 3 vezes. A baixa precisa ignorar pedido que já está pago.
- Filtre
livemode. Eventos gerados com chave de teste trazemlivemode: false.
Em produção, o ListenAndServe costuma ficar atrás de um proxy com HTTPS. Todos os eventos, incluindo estorno e saque, estão em webhook Pix.
Testar no sandbox
Com as chaves de teste nas variáveis, o mesmo código roda sem mover dinheiro. Toda resposta traz livemode: false, nenhum provedor Pix é chamado e o pix.copyPaste é um marcador que não pode ser pago.
var teste xflow.Cobranca
err := xflow.Chamar("POST", "/charges", xflow.NovaCobranca{
AmountCents: 1990,
Customer: xflow.Customer{Name: "Cliente", Email: "c@exemplo.com", Document: "00000000000"},
}, "", &teste)
if err == nil {
err = xflow.Chamar("POST", "/test/charges/"+teste.ID+"/pay", nil, "", nil)
}
var paga xflow.Cobranca
if err == nil {
err = xflow.Chamar("GET", "/charges/"+teste.ID, nil, "", &paga)
}
fmt.Println(paga.Status, paga.Livemode, err) // paid false <nil>A cobrança vira paid, a taxa é calculada com as taxas reais da sua conta e o webhook transaction.paid chega com livemode: false. Com chave de produção, a simulação responde 403 test_mode_only. Saques não rodam em teste (403 test_mode_unsupported). O roteiro completo está em sandbox da API Pix.
Erros e limite de requisições
Os erros chegam como *xflow.Erro. Use errors.As para olhar o código:
401 unauthorized: chave inválida ou revogada, ou IP fora da allowlist.400 validation: corpo inválido. O campodetailsaponta o problema.429 rate_limited: espere oRetryAftere repita com backoff.502 charge_failed: falha ao gerar a cobrança no provedor. Repita com backoff.
func comRetentativa(chamada func() error) error {
for tentativa := 1; ; tentativa++ {
err := chamada()
var e *xflow.Erro
if err == nil || !errors.As(err, &e) || e.Status != http.StatusTooManyRequests || tentativa == 3 {
return err
}
espera := e.RetryAfter
if espera == 0 {
espera = time.Duration(1<<tentativa) * time.Second
}
time.Sleep(espera)
}
}O limite é por conta, e toda resposta autenticada traz x-ratelimit-limit, x-ratelimit-remaining e x-ratelimit-reset. Numa falha de rede ao criar a cobrança, repita com a mesma Idempotency-Key. A tabela completa está em erros da API Pix.
Antes de ir para produção
- Troque
pk_test_/sk_test_porpk_live_/sk_live_só no ambiente de produção. - Cadastre a URL de produção do webhook e guarde o segredo do endpoint.
- Mantenha o filtro de
livemodee a baixa idempotente. - Registre o
request_idde toda resposta com erro. - Se quiser, ative a allowlist de IP em Chaves de API. Fora da lista, a resposta é
401.
Prefira o webhook ao polling de GET /api/v1/charges/{id}. O mesmo fluxo em outras linguagens está em API Pix em Java e API Pix em Python, e a página Desenvolvedores reúne chaves, logs e todos os guias.
Perguntas frequentes
Preciso de biblioteca externa para usar a API Pix em Go?
Não. net/http, encoding/json e crypto/hmac, da biblioteca padrão, cobrem as chamadas, o JSON e a validação do webhook. A autenticação da API da xFlow é HTTP Basic, montada com req.SetBasicAuth.
Por que o QR Code aparece como #ZgotmplZ no html/template?
Porque o html/template bloqueia URLs com esquema data: no atributo src. Como o pix.qrCodeBase64 vem da própria API, converta o valor com template.URL antes de passá-lo ao template.
Como validar a assinatura do webhook Pix em Go?
Leia o corpo com io.ReadAll antes de qualquer json.Unmarshal, calcule o HMAC com hmac.New(sha256.New, segredo) sobre esses bytes, monte sha256= seguido de hex.EncodeToString e compare com o header x-webhook-signature usando hmac.Equal.
Como simular um pagamento Pix em Go?
Com chaves de teste (pk_test_/sk_test_), crie a cobrança e chame POST /api/v1/test/charges/{id}/pay. A cobrança vira paid e o webhook transaction.paid chega com livemode: false.