Em C#, a API Pix se integra com o HttpClient do próprio .NET: um POST cria a cobrança, a resposta já traz o QR Code e o copia e cola, e um endpoint do ASP.NET Core recebe o webhook que confirma o pagamento. O código abaixo segue a documentação da API, usa .NET 8 ou superior e a base https://app.xflowpayments.com/api/v1. Para a visão geral da integração, veja o 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_...), que pode ser criada antes da aprovação do cadastro. - .NET 8 ou superior, sem pacote extra.
- As chaves na configuração:
Xflow:PublicKey,Xflow:SecretKeyeXflow:WebhookSecret. Em desenvolvimento, usedotnet user-secrets. Em produção, variáveis de ambiente com dois sublinhados, comoXflow__SecretKey.
O segredo fica no servidor. Nunca o coloque em código versionado, em Blazor WebAssembly ou num app que roda no aparelho do cliente.
Um HttpClient tipado
A autenticação é HTTP Basic: usuário é a chave pública, senha é o segredo. Registre o cliente uma vez no Program.cs. O AddHttpClient gerencia as conexões e evita o problema clássico de criar um HttpClient novo a cada chamada.
// Program.cs
using System.Net.Http.Headers;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<XflowClient>(http =>
{
http.BaseAddress = new Uri("https://app.xflowpayments.com/api/v1/"); // termina em "/"
var cred = $"{builder.Configuration["Xflow:PublicKey"]}:{builder.Configuration["Xflow:SecretKey"]}";
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes(cred)));
http.Timeout = TimeSpan.FromSeconds(30);
});// XflowClient.cs
using System.Net.Http.Json;
using System.Text.Json.Nodes;
public class XflowClient(HttpClient http)
{
public async Task<JsonNode?> ChamarAsync(HttpMethod metodo, string caminho,
object? corpo = null, string? idempotencyKey = null, CancellationToken ct = default)
{
using var req = new HttpRequestMessage(metodo, caminho); // sem "/" no início
if (corpo is not null) req.Content = JsonContent.Create(corpo);
if (idempotencyKey is not null) req.Headers.Add("Idempotency-Key", idempotencyKey);
using var resp = await http.SendAsync(req, ct);
var texto = await resp.Content.ReadAsStringAsync(ct);
var json = texto.Length > 0 ? JsonNode.Parse(texto) : null;
if (!resp.IsSuccessStatusCode)
throw new XflowException((int)resp.StatusCode, json, resp.Headers.RetryAfter?.Delta);
return json;
}
}
public class XflowException(int status, JsonNode? corpo, TimeSpan? retryAfter)
: Exception($"{status} {corpo?["error"]}: {corpo?["message"]} ({corpo?["request_id"]})")
{
public int Status { get; } = status;
public string? Codigo { get; } = (string?)corpo?["error"];
public TimeSpan? RetryAfter { get; } = retryAfter;
}Atenção à barra: com BaseAddress terminando em /api/v1/, o caminho relativo é charges. Escrever /charges aponta para a raiz do domínio. Toda resposta traz um request_id (req_...), no corpo e no header x-request-id, e a mensagem da exceção já o inclui. Com ele você encontra a chamada em Integrações > Logs da API, que guarda cada requisição autenticada por 30 dias.
Criar a cobrança Pix
Injete o XflowClient no seu serviço ou endpoint e chame:
var pedidoId = "1234";
var cobranca = await xflow.ChamarAsync(HttpMethod.Post, "charges", new
{
amountCents = 1990, // R$ 19,90
customer = new { name = "Cliente Exemplo", email = "cliente@exemplo.com", document = "00000000000" },
description = $"Pedido #{pedidoId}",
external_reference = $"pedido-{pedidoId}",
}, idempotencyKey: $"pedido-{pedidoId}");
var id = (string?)cobranca?["id"]; // "clx...", status "pending"
var qrCode = (string?)cobranca?["pix"]?["qrCodeBase64"];
var copiaECola = (string?)cobranca?["pix"]?["copyPaste"];Três detalhes:
- Nomes do JSON. O
JsonContentusa a política camelCase. No tipo anônimo, os nomes já saem certos. Se trocar por umrecordcomExternalReference, ele viraexternalReference: marque a propriedade com[JsonPropertyName("external_reference")]. Idempotency-Keyúnica por pedido. Repetir a chamada com a mesma chave devolve a mesma cobrança, sem gerar outro Pix. Com outro corpo, a resposta é409 idempotency_conflict. Veja idempotência em API de pagamento.- Centavos. Se o total está em
decimalde reais, multiplique por 100 e confira que não sobrou fração antes de converter paraint. Valor fora do mínimo ou do máximo da conta retorna400 validation.
Mostrar o QR Code e o copia e cola
pix.qrCodeBase64 é um PNG em data-URL, pronto para o src. Numa view Razor:
<img src="@Model.QrCode" alt="QR Code Pix" width="240" height="240" />
<textarea readonly>@Model.CopiaECola</textarea>Num app MAUI ou desktop, pegue os bytes do PNG com Convert.FromBase64String(qrCode[(qrCode.IndexOf(',') + 1)..]). 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. Mais em QR Code Pix dinâmico por API.
Receber o webhook com Minimal API
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.
// Program.cs, depois de var app = builder.Build();
// no topo do arquivo: using System.Security.Cryptography; using System.Text.Json.Nodes;
var segredo = Encoding.UTF8.GetBytes(app.Configuration["Xflow:WebhookSecret"]!);
app.MapPost("/webhooks/xflow", async (HttpRequest request, PedidoService pedidos) =>
{
using var ms = new MemoryStream();
await request.Body.CopyToAsync(ms); // corpo cru, antes de qualquer desserialização
var corpo = ms.ToArray();
var esperado = "sha256=" + Convert.ToHexString(HMACSHA256.HashData(segredo, corpo)).ToLowerInvariant();
var recebido = request.Headers["x-webhook-signature"].ToString();
if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(esperado), Encoding.UTF8.GetBytes(recebido)))
return Results.Unauthorized();
var evento = JsonNode.Parse(corpo)!;
var data = evento["data"]!;
if ((string?)evento["event"] == "transaction.paid" && (bool?)data["livemode"] == true)
await pedidos.DarBaixaAsync((string?)data["external_reference"], (string?)data["pix"]?["endToEnd"]);
return Results.Ok(); // exceção acima vira 500 e a entrega é reenviada
});PedidoService é a sua camada de negócio, registrada no contêiner de serviços. Os cuidados:
- Não declare o corpo como parâmetro tipado. Um
recordno lugar doHttpRequestfaria o ASP.NET Core desserializar o JSON, e você perderia os bytes originais que o HMAC exige. Convert.ToHexStringdevolve maiúsculas. A assinatura vem em minúsculas, por isso oToLowerInvariant().FixedTimeEqualscompara em tempo constante. Não use==para assinatura.- Responda 2xx. Sem isso, a xFlow reenvia até 3 vezes. A baixa precisa ignorar pedido que já está pago.
- Filtre
livemode: false, que marca eventos gerados com chave de teste.
Se a aplicação exige autenticação em todas as rotas, marque esta com .AllowAnonymous(): quem protege o endpoint é a assinatura. Todos os eventos, incluindo estorno e saque, estão em webhook Pix.
Testar no sandbox
Com a chave de teste na configuração, o código é o mesmo. As respostas trazem livemode: false, nenhum provedor Pix é chamado e o pix.copyPaste é um marcador que não pode ser pago.
var teste = await xflow.ChamarAsync(HttpMethod.Post, "charges", new
{
amountCents = 1990,
customer = new { name = "Cliente", email = "c@exemplo.com", document = "00000000000" },
});
var testeId = (string?)teste?["id"];
await xflow.ChamarAsync(HttpMethod.Post, $"test/charges/{testeId}/pay");
var paga = await xflow.ChamarAsync(HttpMethod.Get, $"charges/{testeId}");
Console.WriteLine($"{paga?["status"]} {paga?["livemode"]}"); // paid falseA cobrança vira paid, a taxa sai das 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 passo a passo está em sandbox da API Pix.
Erros e limite de requisições
| HTTP | Código | O que fazer |
|---|---|---|
| 401 | unauthorized |
Confira as chaves e, se usa allowlist de IP, o IP do servidor. |
| 400 | validation |
Corrija o corpo. O campo details aponta o problema. |
| 403 | account_inactive |
Conta em análise ou bloqueada. Siga com a chave de teste. |
| 429 | rate_limited |
Espere o retry-after e repita com backoff. |
| 502 | charge_failed |
Falha ao gerar a cobrança no provedor. Repita com backoff. |
O limite é por conta, e toda resposta autenticada traz x-ratelimit-limit, x-ratelimit-remaining e x-ratelimit-reset. Para o 429, um filtro de exceção resolve:
async Task<JsonNode?> ComRetentativa(Func<Task<JsonNode?>> chamada)
{
for (var tentativa = 1; ; tentativa++)
{
try { return await chamada(); }
catch (XflowException e) when (e.Status == 429 && tentativa < 3)
{
await Task.Delay(e.RetryAfter ?? TimeSpan.FromSeconds(1 << tentativa));
}
}
}Numa falha de rede ao criar a cobrança, repita com a mesma Idempotency-Key. A lista de códigos está em erros da API Pix.
Antes de ir para produção
- Troque
pk_test_/sk_test_porpk_live_/sk_live_só na configuração 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 Node.js, e a página Desenvolvedores reúne chaves, logs e todos os guias.
Perguntas frequentes
Preciso de pacote NuGet para usar a API Pix em C#?
Não. HttpClient, System.Net.Http.Json e System.Text.Json já vêm no .NET. A API da xFlow é REST com JSON e autenticação HTTP Basic, que você monta no header Authorization com a chave pública e o segredo.
Por que a chamada vai para o endereço errado quando uso BaseAddress?
Porque o caminho relativo começa com barra. Com BaseAddress terminando em /api/v1/, use "charges", sem barra no início. Com "/charges", o HttpClient descarta o /api/v1 e chama a raiz do domínio.
Como validar o webhook Pix no ASP.NET Core?
Leia o corpo cru de request.Body para um array de bytes, calcule HMACSHA256.HashData com o segredo do endpoint, converta para hexadecimal em minúsculas, prefixe sha256= e compare com o header x-webhook-signature usando CryptographicOperations.FixedTimeEquals. Só depois desserialize o JSON.
Como testar a API Pix em C# sem pagar de verdade?
Use uma chave de teste (pk_test_/sk_test_), crie a cobrança normalmente e chame POST /api/v1/test/charges/{id}/pay. A cobrança vira paid e o webhook transaction.paid chega com livemode: false.