> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wyzecash.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Go

> Integre a WyzeCash em Go só com a biblioteca padrão: um cliente pronto para copiar, com idempotência, novas tentativas seguras, listas, erros, webhooks e tempo real.

A API é REST com JSON, então o `net/http` da biblioteca padrão basta. Esta página traz um **cliente pronto**, para colar no seu projeto, que já faz o que importa: chave no cabeçalho, `Idempotency-Key`, novas tentativas só quando é seguro, listas por cursor e erros claros.

> [!NOTE]
>
> O SDK oficial de Go está a caminho. Quando sair, esta página passa a usar o pacote; o cliente abaixo continua funcionando, porque fala com a mesma API.

> [!WARNING]
>
> Rode só no servidor. A chave de API vai em todo pedido: nunca a coloque em front-end, app ou repositório. Guarde em variável de ambiente (`BUKIO_API_KEY`).

## O cliente

Go 1.22 ou mais novo, sem dependências. Salve como `bukiopay/client.go` no seu projeto:

```go bukiopay/client.go theme={null}
package bukiopay

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"time"
)

const APIURL = "https://api.wyzecash.com.br"

// Error é o erro da API: use Code no seu sistema; Message vem em português.
type Error struct {
	Status    int
	Code      string
	Message   string
	RequestID string
	Details   map[string]any
}

func (e *Error) Error() string { return fmt.Sprintf("bukiopay: %d %s: %s", e.Status, e.Code, e.Message) }

type Client struct {
	APIKey     string
	MaxRetries int
	HTTP       *http.Client
}

// New cria o cliente. Sem chave, lê a variável BUKIO_API_KEY.
func New(apiKey string) *Client {
	if apiKey == "" {
		apiKey = os.Getenv("BUKIO_API_KEY")
	}
	return &Client{APIKey: apiKey, MaxRetries: 2, HTTP: &http.Client{Timeout: 60 * time.Second}}
}

// Request faz o pedido e lê a resposta em out. retrySafe diz se pode repetir em erro de rede ou 5xx:
// sim para consultas e para criar cobrança (com a mesma Idempotency-Key); nunca para saque, Pix
// enviado e estorno.
func (c *Client) Request(ctx context.Context, method, path string, query url.Values, body any, idempotencyKey string, retrySafe bool, out any) error {
	var payload []byte
	if body != nil {
		var err error
		if payload, err = json.Marshal(body); err != nil {
			return err
		}
	}
	u := APIURL + path
	if len(query) > 0 {
		u += "?" + query.Encode()
	}
	for attempt := 0; ; attempt++ {
		req, err := http.NewRequestWithContext(ctx, method, u, bytes.NewReader(payload))
		if err != nil {
			return err
		}
		req.Header.Set("Authorization", "Bearer "+c.APIKey)
		req.Header.Set("Accept", "application/json")
		if body != nil {
			req.Header.Set("Content-Type", "application/json")
		}
		if idempotencyKey != "" {
			req.Header.Set("Idempotency-Key", idempotencyKey)
		}
		res, err := c.HTTP.Do(req)
		if err != nil {
			if retrySafe && attempt < c.MaxRetries {
				time.Sleep(time.Duration(500<<attempt) * time.Millisecond)
				continue
			}
			return err
		}
		data, _ := io.ReadAll(res.Body)
		res.Body.Close()
		if res.StatusCode == http.StatusTooManyRequests && attempt < c.MaxRetries {
			wait, _ := strconv.Atoi(res.Header.Get("Retry-After"))
			time.Sleep(time.Duration(max(wait, 1)) * time.Second)
			continue
		}
		if res.StatusCode >= 500 && retrySafe && attempt < c.MaxRetries {
			time.Sleep(time.Duration(500<<attempt) * time.Millisecond)
			continue
		}
		if res.StatusCode >= 400 {
			var e struct {
				Error struct {
					Code    string         `json:"code"`
					Message string         `json:"message"`
					Details map[string]any `json:"details"`
				} `json:"error"`
				RequestID string `json:"requestId"`
			}
			_ = json.Unmarshal(data, &e)
			return &Error{Status: res.StatusCode, Code: e.Error.Code, Message: e.Error.Message, RequestID: e.RequestID, Details: e.Error.Details}
		}
		if out == nil || len(data) == 0 {
			return nil
		}
		return json.Unmarshal(data, out)
	}
}

// Page é uma página de lista: items e o cursor da próxima (vazio quando acabou).
type Page[T any] struct {
	Items      []T    `json:"items"`
	NextCursor string `json:"nextCursor"`
}

// Each passa por todas as páginas de uma lista, buscando a próxima só quando precisa.
func Each[T any](ctx context.Context, c *Client, path string, query url.Values, fn func(T) error) error {
	q := url.Values{}
	for k, v := range query {
		q[k] = v
	}
	for {
		var page Page[T]
		if err := c.Request(ctx, http.MethodGet, path, q, nil, "", true, &page); err != nil {
			return err
		}
		for _, item := range page.Items {
			if err := fn(item); err != nil {
				return err
			}
		}
		if page.NextCursor == "" {
			return nil
		}
		q.Set("cursor", page.NextCursor)
	}
}
```

## Exemplos

> [tab] Pix
>
> ```go theme={null}
> type Payment struct {
> 	ID        string `json:"id"`
> 	Status    string `json:"status"`
> 	Amount    int    `json:"amount"`
> 	CopyPaste string `json:"copyPaste"`
> 	ExpiresAt string `json:"expiresAt"`
> }
>
> bukio := bukiopay.New("")
>
> var cobranca Payment
> err := bukio.Request(ctx, "POST", "/v1/payments", nil,
> 	map[string]any{"amount": 15000, "description": "Pedido 1052"}, // R$ 150,00, sempre em centavos
> 	"pedido-1052", // Idempotency-Key: o código do pedido no seu sistema
> 	true, &cobranca)
>
> fmt.Println(cobranca.ID, cobranca.CopyPaste) // BKY-... e o Pix Copia e Cola
> ```
>
> O Pix vale 30 minutos (`expiresAt`). Libere o pedido só no webhook `payment.paid`.

> [tab] Cartão
>
> ```go theme={null}
> var cobranca struct {
> 	ID         string `json:"id"`
> 	PaymentURL string `json:"paymentUrl"`
> }
> err := bukio.Request(ctx, "POST", "/v1/payments", nil,
> 	map[string]any{"amount": 15000, "method": "card", "description": "Pedido 1053"},
> 	"pedido-1053", true, &cobranca)
> // Mande o cliente para cobranca.PaymentURL (página segura de pagamento).
> ```

> [tab] Saque
>
> ```go theme={null}
> var saque struct {
> 	ID     string `json:"id"`
> 	Status string `json:"status"`
> }
> err := bukio.Request(ctx, "POST", "/v1/withdrawals", nil, map[string]any{
> 	"amount":      10000,
> 	"destination": map[string]string{"keyType": "email", "key": "financeiro@exemplo.com"},
> }, "saque-2026-11-05", false, &saque) // false: saque nunca é repetido sozinho
> // saque.Status == "processing": confirme pelo webhook withdrawal.completed
> ```

> [tab] Listas
>
> ```go theme={null}
> err := bukiopay.Each(ctx, bukio, "/v1/payments", url.Values{"status": {"paid"}}, func(p Payment) error {
> 	fmt.Println(p.ID, p.Amount)
> 	return nil
> })
> ```

## Erros

```go theme={null}
var apiErr *bukiopay.Error
if errors.As(err, &apiErr) {
	log.Println(apiErr.Status, apiErr.Code, apiErr.Message, apiErr.RequestID)
}
```

| Situação | O cliente tenta de novo? |
| - | - |
| `429` (passou de 100 pedidos por minuto) | Sim, esperando o `Retry-After` |
| Erro de rede ou `5xx` com `retrySafe` (consultas e criar cobrança) | Sim (a mesma `Idempotency-Key` evita cobrança duplicada) |
| Saque, Pix enviado e estorno (`retrySafe` falso) | **Nunca.** Confira pela consulta ou pelo webhook antes de pedir de novo |
| `4xx` | Não: corrija o pedido (veja [Erros](https://docs.wyzecash.com.br/erros.md)) |

## Webhooks

Confira a assinatura `X-Bukio-Signature` com o **corpo cru**, antes de ler o JSON:

```go bukiopay/webhook.go theme={null}
package bukiopay

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"strconv"
	"strings"
	"time"
)

type WebhookEvent struct {
	ID        string          `json:"id"`
	Type      string          `json:"type"`
	CreatedAt string          `json:"created_at"`
	Data      json.RawMessage `json:"data"`
}

var ErrInvalidWebhook = errors.New("bukiopay: aviso não é da WyzeCash")

// VerifyWebhook confere a assinatura (até 5 minutos) e devolve o evento.
func VerifyWebhook(body []byte, signature, secret string) (*WebhookEvent, error) {
	var t int64
	var sigs []string
	for _, part := range strings.Split(signature, ",") {
		k, v, _ := strings.Cut(strings.TrimSpace(part), "=")
		switch k {
		case "t":
			t, _ = strconv.ParseInt(v, 10, 64)
		case "v1":
			sigs = append(sigs, v)
		}
	}
	if t == 0 || len(sigs) == 0 {
		return nil, ErrInvalidWebhook
	}
	if d := time.Now().Unix() - t; d > 300 || d < -300 {
		return nil, ErrInvalidWebhook
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(strconv.FormatInt(t, 10) + "."))
	mac.Write(body)
	expected := hex.EncodeToString(mac.Sum(nil))
	for _, s := range sigs {
		if hmac.Equal([]byte(expected), []byte(s)) {
			var event WebhookEvent
			if err := json.Unmarshal(body, &event); err != nil {
				return nil, ErrInvalidWebhook
			}
			return &event, nil
		}
	}
	return nil, ErrInvalidWebhook
}
```

```go main.go theme={null}
http.HandleFunc("POST /webhooks/bukiopay", func(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	event, err := bukiopay.VerifyWebhook(body, r.Header.Get("X-Bukio-Signature"), os.Getenv("BUKIO_WEBHOOK_SECRET"))
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	w.WriteHeader(http.StatusOK) // responda logo; processe depois
	if event.Type == "payment.paid" {
		go liberarPedido(event.ID, event.Data) // use event.ID para não processar duas vezes
	}
})
```

Responda `2xx` em até 10 segundos. Veja [Validando a assinatura](https://docs.wyzecash.com.br/webhooks/assinatura.md) e a [lista de eventos](https://docs.wyzecash.com.br/webhooks/configurar.md).

## Tempo real (WebSocket)

Sem endereço HTTPS para webhooks (bot, worker)? Receba os mesmos eventos pelo WebSocket. A biblioteca padrão não tem cliente WebSocket; este exemplo usa `github.com/coder/websocket`:

```bash theme={null}
go get github.com/coder/websocket
```

```go theme={null}
func ouvir(ctx context.Context) error {
	header := http.Header{"Authorization": {"Bearer " + os.Getenv("BUKIO_API_KEY")}}
	conn, _, err := websocket.Dial(ctx, "wss://api.wyzecash.com.br/v1/ws", &websocket.DialOptions{HTTPHeader: header})
	if err != nil {
		return err
	}
	defer conn.CloseNow()
	for {
		var msg struct {
			ID    string          `json:"id"`
			Event string          `json:"event"`
			Data  json.RawMessage `json:"data"`
		}
		if err := wsjson.Read(ctx, conn, &msg); err != nil {
			return err // reconecte esperando cada vez mais, mandando o último ID em since
		}
		switch {
		case msg.Event == "ping":
			wsjson.Write(ctx, conn, map[string]string{"action": "ping"})
		case msg.Event == "authenticated":
			wsjson.Write(ctx, conn, map[string]any{"action": "subscribe", "pattern": "payment.*"})
		case msg.ID != "" && msg.Event == "payment.paid":
			liberarPedido(msg.ID, msg.Data)
		}
	}
}
```

Fechamento `1008` quer dizer chave recusada: não reconecte com a mesma chave. Protocolo completo, `since`, erros e limites em [WebSocket](https://docs.wyzecash.com.br/webhooks/websocket.md).

## Próximos passos

* [Sua primeira cobrança](https://docs.wyzecash.com.br/inicio-rapido.md) e [Antes de ir para produção](https://docs.wyzecash.com.br/producao.md).
* Todas as rotas, com os campos: [Referência da API](https://docs.wyzecash.com.br/referencia/visao-geral.md).
* Convenções (centavos, datas, `Idempotency-Key`, cursores): [Convenções](https://docs.wyzecash.com.br/convencoes.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.