Referência da API
Hoje o Obsyd processa PIX — e só PIX. Crie a cobrança, mande o cliente para o checkout e libere o pedido pelo webhook. Valores sempre em centavos inteiros; datas em ISO 8601 (UTC).
Nesta páginaÍndice
Comece em 3 passos
REST sobre HTTPS, JSON na entrada e na saída. Toda resposta traz um campo object identificando o recurso.
Não existe cartão, boleto, cripto nem assinatura na API pública, e não prometemos data para nenhum deles — esta página documenta só o que existe e funciona. Mandar method diferente de pix, ou um campo de cartão (card, installments…), não é ignorado em silêncio: a chamada volta 400 method_not_supported com details.supported: ["pix"]. Aceitar e cobrar errado seria pior do que recusar.
Em API keys, gere uma sk_test_ para homologar. O segredo aparece uma única vez — guarde em variável de ambiente.
POST /charges devolve o código PIX, o QR e uma checkout_url pronta para receber o cliente.
Assine o webhook charge.paid e libere o pedido por ele. O redirect do cliente não é prova de pagamento.
Todos os caminhos desta página são relativos a ela.
Chaves em Painel → API keys. sk_test_ usa o sandbox.
A tela Integrar monta um prompt com este contrato inteiro — a sua stack, as suas chaves, o código do webhook na sua linguagem — pronto para colar no ChatGPT, Claude, Cursor ou v0.
O que muda em relação ao gateway que você usa hoje
Curto, e nada aqui é enfeite: cada linha resolve um bug que costuma virar chamado de suporte.
| Tema | Como costuma ser | Como é aqui |
|---|---|---|
| Valor | Ora reais com decimal, ora centavos, dependendo da rota. | Centavos inteiros em toda a API, em toda chave. amount_cents é sempre o total cobrado. |
| Frete e desconto | Manda amount + shipping_fee, cobra só o amount, e a diferença aparece na conciliação. | Ou o gateway soma as partes, ou recusa com amount_mismatch dizendo expected_cents e received_cents. Nunca aceita em silêncio. |
| metadata | Volta na consulta, mas some no webhook. | Ecoado no webhook, exatamente como você mandou — junto com external_id e track. |
| Webhook | Sem assinatura, ou “confie no IP de origem”. | HMAC-SHA256 sobre <t>.<corpo cru>, com timestamp para barrar replay. |
| Entrega falhou | Você descobre pelo cliente reclamando. | Log de entrega no painel, com status HTTP, erro, tentativas e botão de reenviar. |
| Tempo real | Polling é bloqueado e só sobra webhook — quem não tem URL pública fica sem saber que a cobrança foi paga. | SSE em GET /v1/events: uma conexão, sem URL pública, com Last-Event-ID para retomar sem perder evento. |
| Credencial | Uma chave, acesso total. | Escopos por chave e allowlist de IP por chave, valendo em todas as rotas daquela credencial. |
| Saque | Idempotência opcional — quando existe. | Idempotency-Key obrigatória. Um 504 nunca significa “falhou”. |
| Campo vazio | "paid_at": null, "external_id": "". | Chave sem valor não vem na resposta — você não implementa contra um campo que nunca chega. |
Autenticação
Toda chamada exige a chave secreta do merchant, sempre do lado do servidor. A chave também define o ambiente da requisição.
Authorization: Bearer sk_live_a1b2c3d4e5f6g7h8i9j0k1l2- Formato:
sk_live_…em produção,sk_test_…no sandbox. O segredo é exibido uma única vez, na criação. - A chave secreta nunca vai para o navegador, o app, o repositório ou uma variável
NEXT_PUBLIC_*. Quem chama a v1 é o seu servidor. - Chave ausente, malformada ou revogada devolve
401 unauthorized; conta bloqueada devolve403 forbidden. - A chave isola os ambientes: uma chave
livenunca enxerga cobrançatest(e vice-versa), inclusive na listagem. - Cada chamada atualiza o
last_used_atda chave — dá para auditar o uso em Painel → API keys.
| Rota | Limite por chave | Escopo exigido |
|---|---|---|
| POST /v1/charges · GET /v1/charges | 60 req/min | transactions |
| GET /v1/charges/:id | 120 req/min | transactions |
| GET /v1/events | 30 aberturas/min | transactionsmáx. 5 conexões simultâneas por conta |
| GET /v1/balance | 120 req/min | data |
| POST /v1/withdrawals | 20 req/min | withdrawals |
| GET /v1/withdrawals · GET /v1/withdrawals/:id | 120 req/min | withdrawals ou data |
- Ao estourar o limite:
429 rate_limitedcomRetry-Afterem segundos. A janela é de um minuto, por chave. - O limite é aplicado antes da checagem de escopo: sondar permissão também custa cota, então uma chave sem escopo não vira ferramenta de varredura.
Escopos da credencial
Uma credencial que só cria cobrança não consegue sacar. É isso que limita o estrago de um vazamento — então crie uma chave por finalidade, não uma chave que faz tudo.
| Escopo | O que concede | Rotas cobertas |
|---|---|---|
transactions | Criar, consultar e listar cobranças PIX, e abrir o stream de eventos. É o mínimo para vender — e não dá acesso ao saldo nem ao saque. | POST /v1/charges · GET /v1/charges · GET /v1/charges/:id · GET /v1/events |
withdrawals | Pedir saque do saldo para a chave PIX da conta, e consultar saques. Tira dinheiro de casa: só marque se a integração realmente saca sozinha. | POST /v1/withdrawals · GET /v1/withdrawals · GET /v1/withdrawals/:id |
data | Ler saldo e consultar saques. Não cria e não move nada. | GET /v1/balance · GET /v1/withdrawals (leitura) |
checkout | Reservado para os links de pagamento hospedados. Nenhuma rota da v1 exige este escopo hoje — marcá-lo não muda nada, e não marcá-lo não quebra nada. | — |
- Chave nova nasce com
transactionse mais nada. O resto se marca conscientemente. - Chave criada antes dos escopos (sem escopo gravado) continua com acesso total — quem já integrou não acorda com
403. - Os escopos de uma chave ativa podem ser trocados no painel sem gerar segredo novo (API keys → Editar acesso).
- Consultar saque aceita
withdrawalsoudata: quem só lê relatório não precisa de permissão para tirar dinheiro.
HTTP/1.1 403 Forbidden
{
"error": {
"code": "insufficient_scope",
"message": "Esta credencial não tem o escopo \"withdrawals\" (Saques). Edite os escopos da chave em Integração → API keys, ou use outra credencial.",
"details": {
"required_scope": "withdrawals",
"scopes": ["transactions"]
}
}
}
// quando a rota aceita mais de um escopo, vem tambem
// details.required_scopes: ["withdrawals", "data"]Allowlist de IP
Cada chave pode ter uma lista de IPs autorizados — e ela vale para TODAS as rotas que aquela chave chama, não só para saque.
- O que ela protege: com a allowlist ligada, uma chave vazada só funciona a partir da sua infraestrutura. Trancar só a rota de saque seria trancar o cofre e deixar a janela aberta — quem tem a chave ainda criaria cobrança, leria o seu cadastro de clientes e listaria o seu faturamento.
- Aceita IPv4, IPv6 e CIDR nos dois. Host puro vira
/32(ou/128). - IPv4 mapeado em IPv6 (
::ffff:203.0.113.7) é reduzido para IPv4 antes de comparar — a regra203.0.113.7continua casando atrás de um proxy IPv6. - Máximo de 50 regras por chave. Allowlist não é lista de clientes.
- O IP é lido dos cabeçalhos escritos pela borda (
x-nf-client-connection-ip,cf-connecting-ip,true-client-ip,fly-client-ip,x-real-ip) e só depois dex-forwarded-for— cuja primeira entrada vem do próprio chamador. - Chamada de fora da lista é registrada na auditoria da conta: é o sinal mais barato de chave vazada.
203.0.113.7 # host unico (vira /32)
203.0.113.0/24 # faixa IPv4
2001:db8::/32 # faixa IPv6
::ffff:203.0.113.7 # IPv4 mapeado — reduzido para IPv4 antes de compararHTTP/1.1 403 Forbidden
{
"error": {
"code": "ip_not_allowed",
"message": "O IP 203.0.113.9 não está na allowlist desta chave. A allowlist vale para todas as rotas desta credencial.",
"details": { "ip": "203.0.113.9" }
}
}ip_not_allowed com details.ip: null). Em ambiente sem IP estável (funções serverless de terceiros, CI), deixe a allowlist vazia e proteja a chave por outro meio.Idempotência
Rede falha no meio. O header Idempotency-Key existe para que repetir a mesma chamada não crie o recurso duas vezes.
# 1a chamada — cria
POST /api/v1/charges
Idempotency-Key: pedido-1042
→ 201 Created
# repeticao (timeout, retry, duplo clique) — NAO cria outra
POST /api/v1/charges
Idempotency-Key: pedido-1042
→ 200 OK
Idempotent-Replayed: true- A chave é única por conta (não por ambiente) e aceita até 120 caracteres.
- Repetir a mesma chave devolve a mesma cobrança, com o mesmo
ide o mesmo código PIX — mesmo que o corpo enviado seja diferente. Uma chave nova por pedido, sempre. - Se a criação falhou com
acquirer_unavailable, repita com uma chave nova — a anterior já está amarrada à cobrança que falhou.
- Sem o header, a chamada é recusada com
400 idempotency_key_required. - Acima de 120 caracteres a chamada é recusada, nunca truncada: duas chaves diferentes viradas na mesma chave é exatamente o bug que a idempotência evita.
- A dedupe acontece dentro da transação que debita o ledger — dois
POSTsimultâneos com a mesma chave não produzem dois saques. - Mesma chave com valor diferente devolve
409 idempotency_key_reused, com o saque existente emdetails.
POST paga duas vezes; com chave, a repetição devolve o mesmo saque e o cabeçalho Idempotent-Replayed: true. Trate qualquer timeout como “não sei ainda” — nunca como “falhou”.Modo de teste
Homologue a integração inteira sem mover um centavo — mesmos endpoints, mesmos webhooks, mesma assinatura, mesmo formato de erro.
- Uma chave
sk_test_roteia para a adquirente sandbox: o código PIX é fictício e a cobrança se paga sozinha alguns segundos depois de criada (padrão de 20 s, ajustável pelo admin). - Para não esperar, use Simular pagamento na tela de Transações — o status muda e os webhooks reais são disparados.
- Nada de teste entra no saldo:
GET /v1/balancecom chave de teste responde zerado, com um aviso no camponote. - Saque não existe em teste:
POST /v1/withdrawalscomsk_test_responde400 test_mode_unsupported. - O campo
modeaparece na cobrança e no payload do webhook — use-o se um mesmo endpoint receber os dois ambientes. - Ir para produção é trocar a variável de ambiente da chave secreta. Nada mais.
{
"object": "balance",
"mode": "test",
"available_cents": 0,
"pending_cents": 0,
"currency": "BRL",
"note": "Cobranças de teste não movimentam saldo."
}/api/v1/chargesCriar cobrança PIX
Gera o código PIX e devolve a cobrança completa, incluindo a página de pagamento hospedada. Responde 201 na criação e 200 quando a Idempotency-Key é repetida.
| Campo | Tipo | Descrição |
|---|---|---|
amount_centsobrigatório | integer | Total cobrado, em centavos. Mínimo 100 (R$ 1,00), máximo 100000000. Obrigatório exceto quando vêm items[] — aí é derivado. |
items[] | array (≤ 200) | { id?, name, quantity, unit_price_cents }. quantity inteiro ≥ 1; unit_price_cents em CENTAVOS. Cada item volta com total_cents. |
shipping_fee_cents | integer ≥ 0 | Frete, em centavos. |
extra_fee_cents | integer ≥ 0 | Taxa extra (conveniência, embalagem, serviço). |
discount_cents | integer ≥ 0 | Desconto, em centavos. |
description | string (≤ 200) | Aparece na página de pagamento e no extrato do painel. |
customer | object | name (≤120), email (≤160), phone (≤30), document (≤20), address. Campos extras são preservados. Telefone com ou sem +55, CEP com ou sem traço e CPF/CNPJ com ou sem pontuação entram do mesmo jeito — o original continua salvo. |
external_id | string (≤ 120) | Seu id de pedido. Volta em todas as respostas e webhooks, filtra a listagem via ?external_id= e consulta a cobrança em GET /v1/charges/{external_id}. |
metadata | object | Chaves livres, devolvidas como enviadas — inclusive no webhook. return_url (http/https) vira o botão “voltar para a loja” depois do pagamento. |
expires_in_sec | integer (60–86400) | Validade do código PIX, em segundos.padrão 1800 |
splits[] | array (≤ 20) | Divisão do recebimento entre contas Obsyd. Ver Divisão (splits). |
track | object | UTMs e click ids do comprador. Ver Rastreamento. |
client_ip | string (≤ 60) | IP do comprador — não o do seu servidor. Omitido, cai para o IP de quem chamou a API. |
client_ua | string (≤ 300) | User-agent do comprador. Omitido, cai para o User-Agent da requisição. |
| Campo | Tipo | Descrição |
|---|---|---|
Authorizationobrigatório | string | Bearer sk_live_… ou Bearer sk_test_… (ou o cabeçalho X-Api-Key). |
Content-Typeobrigatório | string | application/json |
Idempotency-Key | string (≤ 120) | Repetir a mesma chave devolve a mesma cobrança, com status 200 e o cabeçalho Idempotent-Replayed: true. Use o id do seu pedido. |
X-Obsyd-Track | string (json) | Alternativa ao campo track do corpo, para quem já tem as UTMs num header. |
curl -X POST https://app.obsydpagamentos.com/api/v1/charges \
-H "Authorization: Bearer sk_live_SUA_CHAVE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-1042" \
-d '{
"amount_cents": 1990,
"description": "Pedido 1042",
"external_id": "1042",
"customer": {
"name": "Ana Souza",
"email": "ana@exemplo.com",
"phone": "11999998888",
"document": "52998224725"
},
"metadata": { "return_url": "https://sualoja.com/obrigado" },
"expires_in_sec": 1800
}'{
"id": "9c3a1f4e-7b21-4a0f-9f52-6f2a1c8b0d31",
"object": "charge",
"status": "pending",
"mode": "live",
"method": "pix",
"amount_cents": 1990,
"fee_cents": 99,
"net_cents": 1891,
"currency": "BRL",
"description": "Pedido 1042",
"external_id": "1042",
"customer": {
"name": "Ana Souza",
"email": "ana@exemplo.com",
"phone": "11999998888",
"document": "52998224725"
},
"metadata": { "return_url": "https://sualoja.com/obrigado" },
"pix": {
"code": "00020126580014br.gov.bcb.pix0136f5a1...5204000053039865802BR6304AB12",
"qr_url": "https://app.obsydpagamentos.com/api/public/charges/9c3a1f4e-7b21-4a0f-9f52-6f2a1c8b0d31/qr.png"
},
"checkout_url": "https://app.obsydpagamentos.com/pay/9c3a1f4e-7b21-4a0f-9f52-6f2a1c8b0d31",
"expires_at": "2026-08-22T15:30:00.000Z",
"created_at": "2026-08-22T15:00:00.000Z",
"updated_at": "2026-08-22T15:00:00.000Z"
}
// repare: nao existe "paid_at": null nem "failed_reason": null.
// chave sem valor NAO vem na resposta.checkout_urlé a página pronta: QR, copia-e-cola, contagem regressiva e confirmação em tempo real. Redirecionar o cliente para ela é o caminho mais curto.- Para checkout próprio, use
pix.code(copia-e-cola) epix.qr_url(PNG).qr_base64quase nunca vem — e, quando não vem, a chave simplesmente não existe. fee_centsenet_centsjá saem com a sua taxa aplicada.net_centsé o que entra no saldo quando a cobrança é paga.- Se nenhuma adquirente conseguir gerar o PIX, a resposta é
502/503comacquirer_unavailablee a cobrança fica comofailed. Ao repetir, use uma Idempotency-Key nova — a anterior já está amarrada à cobrança que falhou.
Composição do valor
Itens, frete, taxa extra e desconto — e por que a API recusa em vez de aceitar uma soma que não fecha.
+ shipping_fee_cents + extra_fee_cents − discount_cents
- Com items[]: o total é derivado. Você pode omitir
amount_centse deixar o Obsyd somar. Se mandar os dois e não bater, a chamada é recusada. - Sem items[]:
amount_centsé obrigatório e continua sendo o total cobrado. As partes acessórias, se vierem, precisam caber dentro dele — frete + taxa − desconto nunca pode passar do total. O subtotal implícito volta emitems_total_cents. - O bloco de composição só aparece na resposta quando existe composição (algum item, frete, taxa ou desconto diferente de zero).
- Valor abaixo do mínimo é
invalid_request(nãoamount_mismatch), comdetails.expected_cents: 100.
curl -X POST https://app.obsydpagamentos.com/api/v1/charges \
-H "Authorization: Bearer sk_live_SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "id": "SKU-1", "name": "Camiseta preta P", "quantity": 2, "unit_price_cents": 8990 },
{ "id": "SKU-9", "name": "Meia par", "quantity": 1, "unit_price_cents": 2500 }
],
"shipping_fee_cents": 1990,
"discount_cents": 2000,
"external_id": "1042"
}'
# 2 x 8990 + 2500 + 1990 - 2000 = 20470
# amount_cents pode ser omitido: o Obsyd soma.HTTP/1.1 400 Bad Request
{
"error": {
"code": "amount_mismatch",
"message": "A soma das partes não bate com amount_cents. Confira itens, frete, taxa extra e desconto — ou omita amount_cents e deixe o Obsyd somar.",
"details": {
"expected_cents": 20470,
"received_cents": 20000,
"items_total_cents": 20480,
"shipping_fee_cents": 1990,
"extra_fee_cents": 0,
"discount_cents": 2000
}
}
}amount_mismatchtambém aparece sem itens, quando frete + taxa − desconto passa doamount_centsinformado — o sintoma clássico de quem mandou o subtotal achando que era o total. Aíexpected_centsé o total dos acessórios.- Trate
amount_mismatchcomo bug do seu carrinho, não como falha do gateway: os dois números vêm na resposta justamente para você achar a diferença no log.
Divisão do recebimento (splits)
Campo opcional splits[] no POST /charges — até 20 recebedores, em valor fixo ou percentual, misturáveis no mesmo array.
| Chave | Regra |
|---|---|
merchant_id ou merchant_slug | Obrigatório, exatamente um dos dois. merchant_id precisa ser uuid; o slug é normalizado (trim + minúsculo). |
amount_cents ou percent | Obrigatório, exatamente um por item. |
amount_cents | Inteiro em CENTAVOS, maior que zero. |
percent | Número em (0, 100], no máximo 3 casas decimais. Incide sobre o amount_cents da cobrança (o total cobrado) e é resolvido para centavos (half-up) na criação; o percentual original fica gravado para auditoria. |
description | Opcional, até 160 caracteres. |
curl -X POST https://app.obsydpagamentos.com/api/v1/charges \
-H "Authorization: Bearer sk_live_SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{
"amount_cents": 10000,
"external_id": "1042",
"splits": [
{ "merchant_slug": "afiliado-ana", "amount_cents": 2500, "description": "Comissão" },
{ "merchant_id": "8f3a2b10-9c44-4d7e-8a01-2b6c9f0e3a55", "percent": 12.5 }
]
}'
# os dois formatos podem ser misturados no mesmo array.- Teto: a soma das partes precisa caber em
amount_cents − fee_cents. A taxa da plataforma sai antes da divisão — quem divide, divide o líquido. Por issopercent: 100não passa. - Toda a validação acontece antes de a cobrança existir: recusar a divisão com o PIX já emitido deixaria uma cobrança órfã na sua mão.
- O recebedor precisa existir e estar ativo, não pode ser a própria conta vendedora e não pode repetir (nem
slugcontraiddo mesmo merchant). details.indexedetails.fieldsempre apontam o item problemático — no estouro do teto, o item que estourou a soma corrida.details.reasoné estável:not_a_list,too_many,invalid_item,recipient_required,recipient_ambiguous,invalid_merchant_id,value_required,value_ambiguous,invalid_amount,invalid_percent,percent_precision,percent_rounds_to_zero,duplicate_recipient,merchant_not_found,merchant_inactive,recipient_is_sender,exceeds_net.- No dinheiro: nada acontece na criação — split de cobrança expirada não move nada. No pagamento, e só em
mode: live, o ledger recebe umsplit_outnegativo na conta vendedora e umsplit_inpositivo em cada recebedor. É idempotente: reentrega de webhook, polling e simulação podem repetir à vontade. - Estorno não reverte o split — a volta é um ajuste manual, conta por conta.
- A divisão sai na resposta do
POST /charges, mas ainda não no payload do webhook: se precisar dela do seu lado, consulte a cobrança ao recebercharge.paid.
Rastreamento: track, UTMs, IP e user-agent
O IP e o user-agent que chegam no nosso webhook são da infraestrutura do gateway, não do comprador. Ou o dado é capturado na criação da cobrança, ou está perdido para sempre.
{
"amount_cents": 1990,
"external_id": "1042",
"client_ip": "203.0.113.42",
"client_ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) …",
"track": {
"utm_source": "instagram",
"utm_medium": "stories",
"utm_campaign": "black-friday",
"fbclid": "IwAR2x…",
"src": "perfil-bio"
}
}- Três formas de mandar, e elas se combinam (a última vence no conflito): as UTMs do
Refererda requisição, o headerX-Obsyd-Trackcom um JSON, e o campotrackno corpo. - Chaves aceitas: qualquer
utm_*(utm_+ até 24 caracteres) e maisfbclid,gclid,gbraid,wbraid,ttclid,msclkid,twclid,li_fat_id,epik,irclickid,kwai_click_id,sck,src,xcod,ref,referrer,affiliate_id. O que não estiver na lista é descartado. - Teto de 20 chaves × 300 caracteres, tudo convertido para string.
- Chamando do seu servidor, mande client_ip explicitamente: sem ele o gateway registra o IP do seu servidor, que não serve para antifraude nem para atribuição.
- A página de pagamento hospedada também captura UTMs da própria URL, mas só preenche chave que ainda não existe — o que veio pela API nunca é sobrescrito.
- O que foi capturado é ecoado no objeto
chargee, portanto, no webhook que você recebe: a atribuição fecha sem você guardar nada do seu lado. Sem nenhuma chave capturada,tracksimplesmente não aparece.
Objeto charge
O mesmo corpo em toda a API v1, no campo data dos webhooks e no stream de eventos. Chave sem valor não vem na resposta — não existe null de enfeite.
| Campo | Tipo | Descrição |
|---|---|---|
id | string (uuid) | Identificador da cobrança no Obsyd. |
object | "charge" | Discriminador do recurso. |
status | enum | pending · paid · expired · refunded · failed. |
mode | "live" | "test" | Ambiente da chave que criou a cobrança. |
method | "pix" | Único método disponível hoje. |
amount_cents | integer | Total cobrado, em centavos. |
fee_cents | integer | Taxa do Obsyd sobre esta cobrança. |
net_cents | integer | Líquido creditado no saldo quando paga. |
currency | string | Sempre BRL. |
itemsopcional | array | Só quando você mandou itens; cada item ganha total_cents. |
items_total_centsopcional | integer | Subtotal. Só quando há composição. |
shipping_fee_centsopcional | integer | Frete. Só quando há composição. |
extra_fee_centsopcional | integer | Taxa extra. Só quando há composição. |
discount_centsopcional | integer | Desconto. Só quando há composição. |
descriptionopcional | string | Descrição enviada na criação. |
external_idopcional | string | Seu id de pedido. |
customeropcional | object | Exatamente o que você enviou. |
metadataopcional | object | Exatamente o que você enviou — inclusive no webhook. |
trackopcional | object | UTMs e click ids capturados na criação. |
client_ipopcional | string | IP do comprador, capturado na criação. |
client_uaopcional | string | User-agent do comprador. |
pixopcional | object | code (copia-e-cola), qr_base64 e qr_url (PNG público). |
splitsopcional | array | Só na resposta do POST /charges, e só quando há divisão. |
checkout_url | string (url) | Página de pagamento hospedada. |
expires_atopcional | string | Validade do PIX, ISO 8601. |
paid_atopcional | string | Momento da confirmação. |
refunded_atopcional | string | Momento do estorno. |
failed_reasonopcional | string | Motivo quando status = failed. |
created_at | string | Criação, ISO 8601 — também serve de cursor na listagem. |
updated_at | string | Última alteração, ISO 8601. |
eventsopcional | array | Somente em GET /v1/charges/:id. |
null. Programe com "paid_at" in charge ou optional chaining, nunca comparando com null./api/v1/charges/{id}Consultar uma cobrança
Fonte da verdade do status. Traz o objeto charge completo e a trilha de eventos. Aceita o id do Obsyd OU o seu external_id.
| Campo | Tipo | Descrição |
|---|---|---|
idobrigatório | string (uuid | external_id) | O id devolvido na criação ou o seu external_id — ele volta em toda consulta e em todo webhook, então consultar por ele é o caminho natural de quem já tem o número do pedido. Cobrança de outro merchant ou de outro ambiente responde 404. |
| Campo | Tipo | Descrição |
|---|---|---|
type | string | created, status_changed, routing_failover ou ignored_transition. |
from | string | Status anterior. Omitido quando não há. |
to | string | Status resultante. Omitido quando não há. |
source | string | Quem provocou: api, webhook, poll, admin, sandbox, cron ou system. |
at | string | Data/hora do evento, ISO 8601. |
# pelo id do Obsyd
curl https://app.obsydpagamentos.com/api/v1/charges/9c3a1f4e-7b21-4a0f-9f52-6f2a1c8b0d31 \
-H "Authorization: Bearer sk_live_SUA_CHAVE"
# ou pelo SEU numero de pedido (external_id)
curl https://app.obsydpagamentos.com/api/v1/charges/1042 \
-H "Authorization: Bearer sk_live_SUA_CHAVE"/api/v1/chargesListar cobranças
Ordenado por created_at decrescente, com paginação por cursor. Só devolve cobranças do ambiente da chave usada.
| Campo | Tipo | Descrição |
|---|---|---|
status | enum | pending, paid, expired, refunded ou failed. |
from | string (ISO 8601) | created_at maior ou igual. Data e hora completas com fuso, ex.: 2026-08-01T00:00:00Z. |
to | string (ISO 8601) | created_at menor ou igual, mesmo formato. |
external_id | string | Filtra pelo seu id de pedido (igualdade exata). |
limit | integer (1–100) | Itens por página.padrão 50 |
cursor | string (ISO 8601) | next_cursor da página anterior. Traz apenas cobranças mais antigas que ele. |
curl -G https://app.obsydpagamentos.com/api/v1/charges \
-H "Authorization: Bearer sk_live_SUA_CHAVE" \
--data-urlencode "status=paid" \
--data-urlencode "from=2026-08-01T00:00:00Z" \
--data-urlencode "limit=50"has_moreindica se existe página seguinte;next_cursoré ocreated_atdo último item devolvido.- Para conciliação diária, combine
status=paidcomfrom/toe somenet_cents— nãoamount_cents.
Checkout hospedado e QR
A página de pagamento e o PNG do QR são públicos: não pedem chave e podem ir para o navegador, para um e-mail ou para o WhatsApp.
checkout_urlaponta para/pay/{id}: QR, copia-e-cola, contagem regressiva e confirmação em tempo real, sem você escrever nada. Logo, cor de destaque e texto de suporte se configuram em Configurações → Página de pagamento.metadata.return_url(http/https) vira o botão de volta para a loja depois do pagamento confirmado. Nunca há redirecionamento automático.- O PNG aceita
?size=entre 128 e 1024 (padrão 512) e é cacheado por 5 minutos. Cobrança sem código PIX devolve404. - Existe também
GET /api/public/charges/{id}/status, sem autenticação, com apenasid,status,paid_ateexpires_at— é o que a página de pagamento usa. Serve para acompanhar a tela do cliente, nunca para liberar o pedido.
<!-- QR em PNG, publico, sem chave (128 a 1024 px) -->
<img src="https://app.obsydpagamentos.com/api/public/charges/9c3a1f4e-7b21-4a0f-9f52-6f2a1c8b0d31/qr.png?size=320"
alt="QR Code do PIX" width="320" height="320" />
<!-- ou a pagina de pagamento inteira -->
<a href="https://app.obsydpagamentos.com/pay/9c3a1f4e-7b21-4a0f-9f52-6f2a1c8b0d31">Pagar com PIX</a>curl https://app.obsydpagamentos.com/api/public/charges/9c3a1f4e-7b21-4a0f-9f52-6f2a1c8b0d31/status
{ "id": "9c3a1f4e-7b21-4a0f-9f52-6f2a1c8b0d31", "status": "paid", "paid_at": "2026-08-22T15:04:10.912Z", "expires_at": "2026-08-22T15:30:00.000Z" }Webhooks
Cadastre a URL em Painel → Webhooks e escolha os eventos. Cada endpoint recebe um segredo próprio, usado para assinar todas as entregas.
| Evento | Quando dispara |
|---|---|
charge.paid | PIX confirmado. net_cents vai para o saldo. É o evento que deve liberar o pedido. |
charge.expired | Passou de expires_at sem pagamento. |
charge.refunded | Estorno concluído; o valor sai do saldo. |
charge.failed | Não foi possível gerar ou cobrar o PIX; veja failed_reason. |
test.ping | Enviado pelo botão “enviar teste” do painel. Aqui data não é uma cobrança. |
POST /webhooks/hidefy HTTP/1.1
Host: sualoja.com
Content-Type: application/json
User-Agent: Obsyd-Webhooks/1.0
X-Obsyd-Event: charge.paid
X-Obsyd-Delivery-Id: 4d8e2c07-1f5b-4a3d-9c11-77a0b3e5d2aa
X-Obsyd-Signature: t=1787059451,v1=9f5c1d3b8e0a...c0
{
"id": "4d8e2c07-1f5b-4a3d-9c11-77a0b3e5d2aa",
"object": "event",
"type": "charge.paid",
"created_at": "2026-08-22T15:04:11.482Z",
"data": {
"id": "9c3a1f4e-7b21-4a0f-9f52-6f2a1c8b0d31",
"object": "charge",
"status": "paid",
"mode": "live",
"amount_cents": 1990,
"net_cents": 1891,
"external_id": "1042",
"metadata": { "return_url": "https://sualoja.com/obrigado" },
"track": { "utm_source": "instagram", "utm_campaign": "black-friday" },
"paid_at": "2026-08-22T15:04:10.912Z"
}
}| Cabeçalho | Conteúdo |
|---|---|
X-Obsyd-Event | Tipo do evento, igual ao campo type do corpo. |
X-Obsyd-Delivery-Id | Id da entrega (uuid). Use como chave de idempotência — a mesma entrega pode chegar duas vezes. |
X-Obsyd-Signature | t=<unix>,v1=<hmac-sha256 hex>, calculado sobre <t>.<corpo cru>. |
User-Agent | Obsyd-Webhooks/1.0 |
import crypto from "node:crypto";
/**
* Confere o cabecalho "t=<unix>,v1=<hmac>" contra o corpo CRU.
* Nunca use JSON.stringify(req.body): qualquer reserializacao quebra o HMAC.
*/
export function verifyObsydSignature(secret, header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(
String(header ?? "").split(",").map((p) => p.split("=")),
);
const t = Number(parts.t);
if (!t || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(parts.v1, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}- Responda
2xxem até 8 segundos. Depois disso a conexão é abortada e a entrega vira retentativa. - Redirecionamentos não são seguidos: um
3xxconta como falha. Cadastre a URL final, com HTTPS. - Retentativas automáticas em 1 min, 5 min, 30 min, 2 h e 12 h. Esgotadas, a entrega fica como falha — e pode ser reenviada em Painel → Webhooks, com o log completo (status HTTP, último erro, número de tentativas).
- Valide a assinatura sobre o corpo cru: qualquer reserialização (parse + stringify) quebra o HMAC. Tolerância recomendada de 300 s para o
t. - Deduplique por X-Obsyd-Delivery-Id e trate entrega fora de ordem: o
charge.expiredde uma cobrança que depois foi paga pode chegar depois docharge.paid. Estado final não volta atrás. - Um mesmo endpoint recebe eventos de teste e de produção — separe pelo
data.modequando precisar. - Em caso de dúvida sobre um pagamento,
GET /v1/charges/{id}é a fonte da verdade.
/api/v1/eventsTempo real sem polling (SSE)
Quem não tem URL pública — app desktop, script de conciliação, ERP atrás de NAT, ambiente de desenvolvimento — abre UMA conexão e escuta. Quem faz polling é o nosso servidor, no nosso banco.
| Campo | Tipo | Descrição |
|---|---|---|
Authorizationobrigatório | string | Bearer sk_live_…. O stream é filtrado por conta e por ambiente: sk_test_ só vê cobrança de teste. |
Accept | string | text/event-stream |
Last-Event-ID | string | <updated_at>|<charge_id> — o id do último evento processado. Retoma exatamente do ponto seguinte. Também aceito como ?last_event_id=. |
| Campo | Tipo | Descrição |
|---|---|---|
events | string (csv) | Filtra os tipos, ex.: charge.paid,charge.refunded. Omitido, vêm os quatro. |
since | string (ISO 8601) | Começa no passado. Cursor mais antigo que 24 h é aparado para 24 h. |
curl -N https://app.obsydpagamentos.com/api/v1/events \
-H "Authorization: Bearer sk_live_SUA_CHAVE" \
-H "Accept: text/event-stream"
# retomar de onde parou (nada repetido, nada perdido)
curl -N https://app.obsydpagamentos.com/api/v1/events \
-H "Authorization: Bearer sk_live_SUA_CHAVE" \
-H "Last-Event-ID: 2026-08-22T15:04:10.912Z|9c3a1f4e-7b21-4a0f-9f52-6f2a1c8b0d31"
# so os eventos que interessam
curl -N "https://app.obsydpagamentos.com/api/v1/events?events=charge.paid,charge.refunded" \
-H "Authorization: Bearer sk_live_SUA_CHAVE"| Quadro | Para que serve |
|---|---|
hidefy.ready | Abre o cano na hora, para o proxy não segurar o header. Traz cursor, resumed, heartbeat_sec, poll_ms e max_connection_sec. Não tem id:, então nunca vira Last-Event-ID. |
charge.paid · charge.expiredcharge.refunded · charge.failed | data: é exatamente o envelope do webhook — { id, object: "event", type, created_at, data: <charge> }. Quem já trata webhook não escreve código novo. created_at é o instante da mudança, não o do envio. |
: ping | Comentário a cada 15 s, para o proxy não derrubar a conexão ociosa. |
hidefy.reconnecthidefy.error | Corte anunciado (vida máxima de 30 min) e falha persistente de leitura. Os dois trazem o cursor no payload: reconecte com ele. |
- Garantia do cursor: o
id:de cada evento é<updated_at>|<charge_id>, um cursor completo em si mesmo. Só o timestamp perderia uma de duas cobranças pagas no mesmo instante; o par com desempate por id não perde. - Cursor corrompido não devolve
400— isso viraria loop de reconexão. Ele cai para “agora”, e o quadroreadyavisaresumed: false. - Persista o
iddepois de processar o evento: cair no meio faz o Obsyd reentregar, e reentrega tratada é melhor do que evento perdido. - A sexta conexão simultânea da mesma conta recebe
429 rate_limitedcomdetails.reason: "too_many_connections". Um stream por processo é o suficiente. EventSourcede navegador não mandaAuthorization— e a chave secreta nunca deveria ir para o navegador. Consuma o SSE do seu servidor.
/api/v1/balanceConsultar saldo
Saldo da conta em centavos. Disponível é o que pode ser sacado agora; pendente ainda não liquidou.
curl https://app.obsydpagamentos.com/api/v1/balance \
-H "Authorization: Bearer sk_live_SUA_CHAVE"- Esta rota exige o escopo
data— uma chave só detransactionsrecebe403 insufficient_scope. - Saques em análise já saem de
available_centsno momento do pedido — não há risco de contar duas vezes. - Com chave
sk_test_a resposta é sempre zerada, com o camponoteexplicando.
/api/v1/withdrawalsPedir um saque
Tira dinheiro do saldo para a chave PIX cadastrada na conta. Só existe em produção, e o header Idempotency-Key é obrigatório.
| Campo | Tipo | Descrição |
|---|---|---|
amount_centsobrigatório | integer > 0 | Valor em centavos, acima do mínimo configurado. Abaixo dele: 400 below_minimum com details.min_cents. |
pix_key | string (≤ 140) | Destino declarado. É conferência, não escolha: precisa bater com a chave cadastrada na conta, senão 400 pix_key_mismatch. |
pix_key_type | enum | cpf, cnpj, email, phone ou evp. Obrigatório junto com pix_key — mandar um sem o outro é invalid_request. |
external_id | string (≤ 120) | Seu id de referência. Filtra a listagem via ?external_id=. |
| Campo | Tipo | Descrição |
|---|---|---|
Idempotency-Keyobrigatório | string (≤ 120) | Um valor único por saque. Sem ele: 400 idempotency_key_required. Repetido com o mesmo valor devolve o mesmo saque (200 + Idempotent-Replayed: true); repetido com valor diferente devolve 409 idempotency_key_reused. |
curl -X POST https://app.obsydpagamentos.com/api/v1/withdrawals \
-H "Authorization: Bearer sk_live_SUA_CHAVE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: saque-2026-08-22-001" \
-d '{
"amount_cents": 50000,
"external_id": "fechamento-agosto"
}'| Campo da resposta | Significado |
|---|---|
amount_cents | O valor pedido, em centavos. |
fee_cents | Taxa de saque, quando houver. |
total_debit_cents | amount_cents + fee_cents — o que realmente sai do saldo. É esse número que precisa caber em available_cents. |
status | requested → approved → paid, ou rejected (o valor volta para o saldo) / cancelled. |
reviewed_at · paid_at | Vêm como null de propósito: aqui o nulo é a informação (“ainda não foi pago”), e sumiria se a chave sumisse. |
note · receipt_url | Observação da análise e comprovante. Omitidos enquanto não existem. |
- Pré-condições: conta ativa (senão
403 merchant_inactive), KYC aprovado (senão403 kyc_required) e chave PIX cadastrada (senão400 pix_key_missing). - Em modo de teste a rota responde
400 test_mode_unsupported: saque não é simulável. - A dedupe da
Idempotency-Keyacontece dentro da transação que debita o ledger — doisPOSTsimultâneos com a mesma chave não geram dois saques.
/api/v1/withdrawals · /api/v1/withdrawals/{id}Consultar saques
Lista paginada por cursor (created_at desc) e consulta individual. Aceita o escopo withdrawals ou data — quem só lê relatório não precisa de permissão para sacar.
| Campo | Tipo | Descrição |
|---|---|---|
status | enum | requested, approved, paid, rejected ou cancelled. |
external_id | string | Igualdade exata. |
from | string (ISO 8601) | created_at maior ou igual. |
to | string (ISO 8601) | created_at menor ou igual. |
limit | integer (1–100) | Itens por página.padrão 50 |
cursor | string (ISO 8601) | next_cursor da página anterior. |
# lista paginada
curl -G https://app.obsydpagamentos.com/api/v1/withdrawals \
-H "Authorization: Bearer sk_live_SUA_CHAVE" \
--data-urlencode "status=paid" \
--data-urlencode "limit=20"
# um saque especifico
curl https://app.obsydpagamentos.com/api/v1/withdrawals/b41d0f9a-2c77-4c6e-9c02-3f5b1a77e900 \
-H "Authorization: Bearer sk_live_SUA_CHAVE"- Saque de outra conta responde
404 not_found— nunca403, para não confirmar a existência do id. - Estas rotas também são só de produção: com
sk_test_a resposta é400 test_mode_unsupported.
Status da cobrança
Uma cobrança nasce pending. As transições abaixo são as únicas aceitas — qualquer outra é ignorada e registrada como ignored_transition.
| Status | Significado | Pode virar |
|---|---|---|
| Aguardando | Cobrança criada, código PIX válido até expires_at. | paid · expired · failed |
| Pago | PIX confirmado. paid_at preenchido e net_cents creditado no saldo. | refunded |
| Expirado | Passou da validade sem pagamento. | paid (webhook atrasado ainda credita) |
| Estornado | Estorno concluído; o valor sai do saldo. refunded_at preenchido. | — estado final |
| Falhou | Não foi possível gerar o PIX; o motivo fica em failed_reason. | paid (raro: adquirente confirma depois) |
charge.paid, consulte GET /v1/charges/{id} ou escute o stream. E lembre: estado final não volta atrás.Erros
Toda falha responde JSON com o mesmo formato. Trate pelo código — ele é estável e nunca é renomeado; a mensagem é humana e pode mudar.
{
"error": {
"code": "invalid_request",
"message": "Corpo inválido.",
"details": [
{ "path": "expires_in_sec", "message": "Too big: expected number to be <=86400" }
]
}
}detailsé opcional e muda de forma conforme o código: um array de{ path, message }na validação de schema, um objeto nos erros de negócio.
| Código | HTTP | Quando acontece / o que fazer |
|---|---|---|
invalid_request | 400 | Campo faltando ou fora do intervalo — details[] traz path e message de cada problema. Também é usado com 403 quando a conta do merchant não está ativa. |
amount_mismatch | 400 | A soma das partes não bate com amount_cents. details traz expected_cents e received_cents — corrija o carrinho, não repita a chamada. |
invalid_split | 400 | Divisão inválida. details.index/details.field apontam o item e details.reason diz o motivo. |
method_not_supported | 400 | Você mandou method diferente de pix ou um campo de cartão. details.supported lista o que existe. |
unauthorized | 401 | Chave ausente, malformada ou revogada. Confira o cabeçalho e a chave. |
forbidden | 403 | Conta bloqueada ou fora de operação. Fale com o suporte. |
insufficient_scope | 403 | A credencial não tem o escopo da rota. details.required_scope (e/ou required_scopes) diz exatamente o que marcar no painel. |
ip_not_allowed | 403 | O IP de origem não está na allowlist da chave. details.ip traz o IP que vimos. |
not_found | 404 | Recurso inexistente, de outro merchant ou do outro ambiente (test × live). |
conflict | 409 | Estado incompatível com a operação pedida. |
rate_limited | 429 | Limite da chave estourado. Espere os segundos indicados em Retry-After e repita. No SSE também cobre details.reason: "too_many_connections". |
acquirer_unavailable | 502 / 503 | Nenhuma adquirente conseguiu gerar o PIX; a cobrança fica failed. Repita com uma Idempotency-Key nova. |
internal | 500 | Erro nosso. Repita; se persistir, chame o suporte com o horário da chamada. |
| Código | HTTP | Significado |
|---|---|---|
idempotency_key_required | 400 | Faltou o header Idempotency-Key. Ele é obrigatório no saque, e o motivo está acima. |
idempotency_key_reused | 409 | A mesma chave já foi usada com outro valor. details.existing_withdrawal_id e details.existing_amount_cents. |
insufficient_balance | 400 | details: requested_cents, fee_cents e total_debit_cents. |
below_minimum | 400 | Abaixo do saque mínimo. details.min_cents. |
pix_key_missing | 400 | A conta não tem chave PIX cadastrada. |
pix_key_mismatch | 400 | O destino informado não bate com a chave cadastrada. |
merchant_inactive | 403 | Conta não está ativa. |
kyc_required | 403 | Saques liberam após a aprovação da conta. |
test_mode_unsupported | 400 | Saque não existe em modo de teste. Use uma chave sk_live_. |
- Repita:
5xxe429, com backoff. No saque, sempre com a mesmaIdempotency-Key. - Não repita:
4xxde validação — corrija o payload. - Códigos novos podem aparecer; os existentes nunca são renomeados. Trate qualquer código desconhecido pela faixa do status HTTP.
A tela Integrar monta um prompt com este contrato inteiro — a sua stack, as suas chaves, o código do webhook na sua linguagem — para a IA escrever a integração e você revisar.