Pular para o conteúdo
xFlow Payments

Blog

API Pix em Go

Para integrar a API Pix em Go, envie POST /api/v1/charges com net/http e req.SetBasicAuth (pk_live_ como usuário, sk_live_ como senha), mostre o pix.qrCodeBase64 e o pix.copyPaste da resposta e confirme o pagamento pelo webhook transaction.paid, validando o HMAC-SHA256 do corpo cru com crypto/hmac e hmac.Equal. Antes da produção, teste com chaves pk_test_.

Atualizado em · Equipe xFlow Payments · 8 min de leitura

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_... e sk_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_KEY e XFLOW_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.

go
// 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:

go
// 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:

go
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... pending

O 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:

go
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.

go
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 do r.Body consome o corpo, e o HMAC precisa dos bytes exatos. No Gin, pegue o corpo com c.GetRawData() antes de qualquer bind. No Echo, use io.ReadAll(c.Request().Body).
  • hmac.Equal compara 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 trazem livemode: 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.

go
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 campo details aponta o problema.
  • 429 rate_limited: espere o RetryAfter e repita com backoff.
  • 502 charge_failed: falha ao gerar a cobrança no provedor. Repita com backoff.
go
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

  1. Troque pk_test_/sk_test_ por pk_live_/sk_live_ só no ambiente de produção.
  2. Cadastre a URL de produção do webhook e guarde o segredo do endpoint.
  3. Mantenha o filtro de livemode e a baixa idempotente.
  4. Registre o request_id de toda resposta com erro.
  5. 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.

Continue lendo

SoluçãoAPI PixA API Pix da xFlow Payments é uma API REST para criar cobranças Pix com QR Code e copia e cola, estornar, sacar e receber webhooks assinados. Uma única integração conecta a sua operação a 11 adquirentes, com roteamento por regras e retentativa automática.BlogWebhook Pix: eventos, assinatura HMAC e boas práticasWebhook Pix é o aviso que a API envia para a sua URL quando algo acontece com uma cobrança ou um saque, como o pagamento confirmado. Na xFlow, cada aviso é um POST JSON assinado com HMAC-SHA256, reenviado até 3 vezes até você responder 2xx.BlogAPI Pix sandbox: como testar sem mover dinheiroO sandbox da API Pix da xFlow funciona com chaves pk_test_ e sk_test_: mesmos endpoints, mesmas respostas com livemode: false, nenhum dinheiro real e um endpoint para simular o pagamento que dispara o webhook transaction.paid.BlogIdempotência em API de pagamento: retry sem duplicarIdempotência é a garantia de que repetir a mesma requisição não repete o efeito. Na API da xFlow, você envia o header Idempotency-Key ao criar cobranças e saques: num retry com a mesma chave, a API devolve a mesma cobrança ou o mesmo saque, sem gerar outro Pix nem debitar o saldo de novo.BlogAPI Pix em JavaPara integrar a API Pix em Java, envie POST /api/v1/charges com o java.net.http.HttpClient e Basic auth (pk_live_ como usuário, sk_live_ como senha), mostre o pix.qrCodeBase64 e o pix.copyPaste da resposta e confirme o pagamento pelo webhook transaction.paid, validando o HMAC-SHA256 do corpo cru com javax.crypto.Mac e MessageDigest.isEqual. Antes da produção, teste com chaves pk_test_.