Pular para o conteúdo principal

Go

Cliente em Go usando o pacote oficial golang.org/x/oauth2/clientcredentials (mantido pelo time Go da Google). Ele entrega um *http.Client com renovação automática de token. Só falta empacotar com retry para erros transitórios.

Pré-requisitos

  • Go 1.21 ou superior.
  • Credenciais em variáveis de ambiente.
export BANQER_TENANT=sua-empresa
export BANQER_CLIENT_ID=...
export BANQER_CLIENT_SECRET=...

Dependência

go get golang.org/x/oauth2/clientcredentials

Cliente completo

// banqer/client.go
package banqer

import (
"context"
"encoding/json"
"fmt"
"io"
"math"
"math/rand"
"net/http"
"net/url"
"os"
"strconv"
"time"

"golang.org/x/oauth2/clientcredentials"
)

type Client struct {
BaseURL string
HTTP *http.Client
}

type Error struct {
Status int
Reason string
Message string
}

func (e *Error) Error() string {
return fmt.Sprintf("[%d %s] %s", e.Status, e.Reason, e.Message)
}

// New cria um cliente para a API Banqer usando credenciais do ambiente.
func New(ctx context.Context) *Client {
tenant := mustEnv("BANQER_TENANT")
cfg := clientcredentials.Config{
ClientID: mustEnv("BANQER_CLIENT_ID"),
ClientSecret: mustEnv("BANQER_CLIENT_SECRET"),
TokenURL: fmt.Sprintf("https://auth.banqer.com.br/realms/%s/protocol/openid-connect/token", tenant),
}
// cfg.Client(ctx) retorna um http.Client que injeta Bearer + renova
// o token automaticamente em ~80% do TTL.
httpClient := cfg.Client(ctx)
httpClient.Timeout = 30 * time.Second

return &Client{
BaseURL: fmt.Sprintf("https://api-%s.banqer.com.br", tenant),
HTTP: httpClient,
}
}

// Get faz GET autenticado em `path` e decodifica o JSON em `out`.
func (c *Client) Get(ctx context.Context, path string, params url.Values, out any) error {
u, err := url.Parse(c.BaseURL + path)
if err != nil {
return err
}
if params != nil {
u.RawQuery = params.Encode()
}

const maxAttempts = 3
for attempt := 1; ; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return err
}
req.Header.Set("Accept", "application/json")

res, err := c.HTTP.Do(req)
if err != nil {
if attempt < maxAttempts {
time.Sleep(backoff(attempt))
continue
}
return &Error{Status: 0, Reason: "NETWORK_ERROR", Message: err.Error()}
}

// 429: respeita Retry-After.
if res.StatusCode == http.StatusTooManyRequests && attempt < maxAttempts {
wait := 5 * time.Second
if ra := res.Header.Get("Retry-After"); ra != "" {
if n, err := strconv.Atoi(ra); err == nil {
wait = time.Duration(n) * time.Second
}
}
res.Body.Close()
time.Sleep(wait)
continue
}
// 5xx transitorio: backoff exponencial.
if res.StatusCode >= 500 && res.StatusCode < 600 && attempt < maxAttempts {
res.Body.Close()
time.Sleep(backoff(attempt))
continue
}

body, _ := io.ReadAll(res.Body)
res.Body.Close()

if res.StatusCode >= 200 && res.StatusCode < 300 {
if out == nil {
return nil
}
return json.Unmarshal(body, out)
}

// Erro 4xx (fora do 401 que o oauth2 ja trata): bug no cliente.
return parseError(res.StatusCode, body)
}
}

func parseError(status int, body []byte) *Error {
var parsed struct {
Message string `json:"message"`
Details []struct {
Reason string `json:"reason"`
} `json:"details"`
}
_ = json.Unmarshal(body, &parsed)
reason := "UNKNOWN"
if len(parsed.Details) > 0 && parsed.Details[0].Reason != "" {
reason = parsed.Details[0].Reason
}
msg := parsed.Message
if msg == "" {
msg = string(body)
}
return &Error{Status: status, Reason: reason, Message: msg}
}

func backoff(attempt int) time.Duration {
d := time.Duration(math.Pow(2, float64(attempt)))*time.Second +
time.Duration(rand.Intn(1000))*time.Millisecond
if d > 30*time.Second {
return 30 * time.Second
}
return d
}

func mustEnv(k string) string {
v := os.Getenv(k)
if v == "" {
panic("env var nao definida: " + k)
}
return v
}

Uso

// main.go
package main

import (
"context"
"fmt"
"net/url"

"example.com/yourapp/banqer"
)

type Credor struct {
IDCredor int `json:"idCredor"`
Nome string `json:"nome"`
NomeSms string `json:"nomeSms,omitempty"`
}

type CredoresResponse struct {
Credores []Credor `json:"credores"`
NextPageToken string `json:"nextPageToken"`
}

func main() {
ctx := context.Background()
client := banqer.New(ctx)

var res CredoresResponse
if err := client.Get(ctx, "/v1/credores", nil, &res); err != nil {
panic(err)
}
for _, c := range res.Credores {
fmt.Printf("%d - %s\n", c.IDCredor, c.Nome)
}

// Com filtro
params := url.Values{}
params.Set("atrasoMin", "90")
params.Set("pageSize", "200")

var fichas struct {
Fichas []struct {
ID string `json:"idFichaCobranca"`
IDExibicao int `json:"idFichaCobrancaExibicao"`
SaldoDevedor float64 `json:"saldoDevedor"`
Atraso int `json:"atraso"`
} `json:"fichas"`
NextPageToken string `json:"nextPageToken"`
}
if err := client.Get(ctx, "/v1/fichas", params, &fichas); err != nil {
panic(err)
}
fmt.Printf("%d fichas em atraso >= 90 dias\n", len(fichas.Fichas))
}

Paginação como iterator

Go 1.23+ tem range-over-function. Versão idiomática:

// PaginaFichas itera por todas as paginas. Use com range.
func (c *Client) PaginaFichas(ctx context.Context, params url.Values) func(yield func(map[string]any) bool) {
return func(yield func(map[string]any) bool) {
token := ""
p := url.Values{}
for k, v := range params {
p[k] = v
}
p.Set("pageSize", "200")
for {
p.Set("pageToken", token)
var res struct {
Fichas []map[string]any `json:"fichas"`
NextPageToken string `json:"nextPageToken"`
}
if err := c.Get(ctx, "/v1/fichas", p, &res); err != nil {
return
}
for _, item := range res.Fichas {
if !yield(item) {
return
}
}
if res.NextPageToken == "" {
return
}
token = res.NextPageToken
}
}
}

// Uso:
for ficha := range client.PaginaFichas(ctx, params) {
fmt.Println(ficha["idFichaCobrancaExibicao"])
}

Erros comuns

SintomaCausa provável
oauth2: cannot fetch tokenTokenURL errado, ou tenant errado. Confira a env var.
context deadline exceeded em endpoint heavyBoleto pode demorar. Use context.WithTimeout(ctx, 60*time.Second) para esses casos.
Campos chegando zero valueTag JSON errada. A API usa camelCase: json:"idFichaCobranca" (não id_ficha_cobranca).